Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/superofficedx-vscode-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@
"id": "superOfficeDX.scriptExplorer",
"name": "Scripts",
"icon": "/resources/logo.svg"
},
{
"id": "superOfficeDX.extraTablesExplorer",
"name": "Extra Tables Explorer",
"icon": "/resources/logo.svg"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Node } from '../../providers/treeViewDataProvider';
import { SuperofficeAuthenticationProvider } from '../../providers/superofficeAuthenticationProvider';
import { IHttpService } from '../../services/httpService';
import { INodeService } from '../../services/nodeService';
import { ScriptInfo } from '../../types/script';
import { ScriptInfo } from '../../types/odata/script';

import {
IScriptCommands,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Node } from '../../providers/treeViewDataProvider';
import { SuperofficeAuthenticationProvider } from '../../providers/superofficeAuthenticationProvider';
import { IHttpService } from '../../services/httpService';
import { INodeService } from '../../services/nodeService';
import { ScriptInfo } from '../../types/script';
import { ScriptInfo } from '../../types/odata/script';
import { SuperOfficeAuthenticationSession } from '../../types/authSession';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const ConfigurationKeys = {
// Providers
AuthenticationProvider: Symbol('AuthenticationProvider'),
TreeViewDataProvider: Symbol('TreeViewDataProvider'),
ExtraTablesTreeViewDataProvider: Symbol('ExtraTablesTreeViewDataProvider'),
TextDocumentContentProvider: Symbol('TextDocumentContentProvider'),

// VS Code Context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { CustomTextDocumentContentProvider } from "../../providers/textDocumentContentProvider";
import { SuperofficeAuthenticationProvider } from "../../providers/superofficeAuthenticationProvider";
import { TreeViewDataProvider } from "../../providers/treeViewDataProvider";
import { ExtraTablesTreeViewDataProvider } from "../../providers/extraTablesTreeViewDataProvider";
import { DIContainer } from "../core/diContainer";
import { ConfigurationKeys } from "./configurationKeys";

Expand All @@ -26,6 +27,14 @@ export function configureProviders(container: DIContainer): void {
)
);

container.registerSingleton(ConfigurationKeys.ExtraTablesTreeViewDataProvider, () =>
new ExtraTablesTreeViewDataProvider(
container.resolve(ConfigurationKeys.ExtensionContext),
container.resolve(ConfigurationKeys.AuthenticationProvider),
container.resolve(ConfigurationKeys.HttpService)
)
);

container.registerSingleton(ConfigurationKeys.TextDocumentContentProvider, () =>
new CustomTextDocumentContentProvider()
);
Expand Down
5 changes: 5 additions & 0 deletions packages/superofficedx-vscode-core/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ExtensionContext, workspace, window } from 'vscode';
import { CustomTextDocumentContentProvider } from "./providers/textDocumentContentProvider";
import { SuperofficeAuthenticationProvider } from "./providers/superofficeAuthenticationProvider";
import { TreeViewDataProvider } from "./providers/treeViewDataProvider";
import { ExtraTablesTreeViewDataProvider } from "./providers/extraTablesTreeViewDataProvider";
import { registerCommands } from './commands/commandRegistration';
import { getCustomScheme } from './utils';
import { createContainer } from './container/containerRegistration';
Expand All @@ -17,17 +18,21 @@ export async function activate(context: ExtensionContext): Promise<void> {
const textContentProvider = container.resolve<CustomTextDocumentContentProvider>(ConfigurationKeys.TextDocumentContentProvider);
const authProvider = container.resolve<SuperofficeAuthenticationProvider>(ConfigurationKeys.AuthenticationProvider);
const treeViewDataProvider = container.resolve<TreeViewDataProvider>(ConfigurationKeys.TreeViewDataProvider);
const extraTablesTreeViewDataProvider = container.resolve<ExtraTablesTreeViewDataProvider>(ConfigurationKeys.ExtraTablesTreeViewDataProvider);

// Register providers with VS Code
context.subscriptions.push(workspace.registerTextDocumentContentProvider(getCustomScheme(), textContentProvider));
context.subscriptions.push(authProvider);

const treeviewProvider = window.registerTreeDataProvider(TreeViewDataProvider.viewId, treeViewDataProvider);
const extraTablesTreeviewProvider = window.registerTreeDataProvider(ExtraTablesTreeViewDataProvider.viewId, extraTablesTreeViewDataProvider);
context.subscriptions.push(treeviewProvider);
context.subscriptions.push(extraTablesTreeviewProvider);

// Listen for authentication session changes to refresh the tree view
authProvider.onDidChangeSessions(() => {
treeViewDataProvider.refresh();
extraTablesTreeViewDataProvider.refresh();
});

// Register commands
Expand Down
106 changes: 88 additions & 18 deletions packages/superofficedx-vscode-core/src/handlers/httpHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,47 +35,117 @@ export interface IHttpHandler {
}

export class HttpHandler implements IHttpHandler {
private async request<T>(method: string, url: string, body?: unknown, headers: Record<string, string> = {}): Promise<T> {
/**
* Internal method to make HTTP requests
* @param method - HTTP method (GET, POST, PUT, DELETE, PATCH)
* @param url - Request URL
* @param body - Request body (optional)
* @param options - Request options including headers and timeout
* @returns Promise resolving to the response data
*/
private async request<T>(method: string, url: string, body?: unknown, options: HttpRequestOptions = {}): Promise<T> {
const { headers = {}, timeout = 30000 } = options;

try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

const requestHeaders: Record<string, string> = { ...headers };

// Only set Content-Type for requests with body
if (body !== undefined) {
requestHeaders['Content-Type'] = 'application/json';
}

const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
headers: requestHeaders,
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
});

clearTimeout(timeoutId);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new HttpError(
`HTTP ${response.status} ${response.statusText}`,
response.status,
url,
method,
response
);
}

return response.json() as Promise<T>;
}
catch (error) {
throw new Error('Error sending request: ' + error);
}
if (error instanceof HttpError) {
throw error;
}

if (error instanceof Error && error.name === 'AbortError') {
throw new HttpError(`Request timeout after ${timeout}ms`, undefined, url, method);
}

throw new HttpError(
`Request failed: ${error instanceof Error ? error.message : String(error)}`,
undefined,
url,
method
);
}
}

public async get<T>(url: string, headers: Record<string, string> = {}): Promise<T> {
return await this.request<T>('GET', url, undefined, headers);
/**
* Make a GET request
* @param url - Request URL
* @param options - Request options including headers and timeout
* @returns Promise resolving to the response data
*/
public async get<T>(url: string, options?: HttpRequestOptions): Promise<T> {
return await this.request<T>('GET', url, undefined, options);
}

public async post<T>(url: string, body: unknown, headers: Record<string, string> = {}): Promise<T> {
return await this.request<T>('POST', url, body, headers);
/**
* Make a POST request
* @param url - Request URL
* @param body - Request body
* @param options - Request options including headers and timeout
* @returns Promise resolving to the response data
*/
public async post<T>(url: string, body: unknown, options?: HttpRequestOptions): Promise<T> {
return await this.request<T>('POST', url, body, options);
}

public async put<T>(url: string, body: unknown, headers: Record<string, string> = {}): Promise<T> {
return await this.request<T>('PUT', url, body, headers);
/**
* Make a PUT request
* @param url - Request URL
* @param body - Request body
* @param options - Request options including headers and timeout
* @returns Promise resolving to the response data
*/
public async put<T>(url: string, body: unknown, options?: HttpRequestOptions): Promise<T> {
return await this.request<T>('PUT', url, body, options);
}

public async delete<T>(url: string, headers: Record<string, string> = {}): Promise<T> {
return await this.request<T>('DELETE', url, undefined, headers);
/**
* Make a DELETE request
* @param url - Request URL
* @param options - Request options including headers and timeout
* @returns Promise resolving to the response data
*/
public async delete<T>(url: string, options?: HttpRequestOptions): Promise<T> {
return await this.request<T>('DELETE', url, undefined, options);
}

public async patch<T>(url: string, body: unknown, headers: Record<string, string> = {}): Promise<T> {
return await this.request<T>('PATCH', url, body, headers);
/**
* Make a PATCH request
* @param url - Request URL
* @param body - Request body
* @param options - Request options including headers and timeout
* @returns Promise resolving to the response data
*/
public async patch<T>(url: string, body: unknown, options?: HttpRequestOptions): Promise<T> {
return await this.request<T>('PATCH', url, body, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { TreeItem, TreeItemCollapsibleState, ThemeIcon, TreeDataProvider, EventEmitter, Event, ExtensionContext } from 'vscode';
import { SuperofficeAuthenticationProvider } from './superofficeAuthenticationProvider';
import { IHttpService } from '../services/httpService';
import { ExtraTable } from '../types/odata/extratable';

export class ExtraTablesNode implements TreeItem {
contextValue: string;
collapsibleState: TreeItemCollapsibleState;

constructor(
public readonly label: string,
public readonly children?: ExtraTablesNode[],
public readonly iconPath?: ThemeIcon
) {
this.contextValue = children && children.length > 0 ? 'extraTablesParent' : 'extraTablesChild';
this.collapsibleState = children && children.length > 0
? TreeItemCollapsibleState.Collapsed
: TreeItemCollapsibleState.None;
}
}

export class ExtraTablesTreeViewDataProvider implements TreeDataProvider<ExtraTablesNode> {
private _onDidChangeTreeData: EventEmitter<ExtraTablesNode | undefined> = new EventEmitter<ExtraTablesNode | undefined>();
readonly onDidChangeTreeData: Event<ExtraTablesNode | undefined> = this._onDidChangeTreeData.event;

public static readonly viewId = 'superOfficeDX.extraTablesExplorer';

constructor(
context: ExtensionContext,
private authProvider: SuperofficeAuthenticationProvider,
private httpService: IHttpService) {
// Constructor parameter required for DI container compatibility
void context; // Mark as used to satisfy linter
}

// Call this method to trigger a refresh of the tree view
public refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}

getTreeItem(element: ExtraTablesNode): TreeItem {
return element;
}

async getChildren(element?: ExtraTablesNode): Promise<ExtraTablesNode[]> {
if (element) {
return element.children || [];
}

const currentSession = this.authProvider.getCurrentSession();
if (currentSession) {
try {
const scriptResponseData = await this.httpService.getExtraTables(currentSession);

// Group scripResponseData by property "extra_tables.table_name"
const groupedTables = new Map<string, ExtraTable[]>();
scriptResponseData.value.forEach(item => {
const tableName = item["extra_tables.table_name"];
if (!groupedTables.has(tableName)) {
groupedTables.set(tableName, []);
}
groupedTables.get(tableName)!.push(item);
});

const tableNodes: ExtraTablesNode[] = [];
groupedTables.forEach((tableItems, tableName) => {
const fieldNodes = tableItems.map(item => {
const typeNode = new ExtraTablesNode(
"type: " + item["extra_tables.(extra_fields->extra_table).type"],
undefined,
new ThemeIcon('symbol-property')
);

const descriptionNode = new ExtraTablesNode(
"description: " + item["extra_tables.(extra_fields->extra_table).description"],
undefined,
new ThemeIcon('symbol-property')
);

return new ExtraTablesNode(
item["extra_tables.(extra_fields->extra_table).field_name"],
[typeNode, descriptionNode],
new ThemeIcon('symbol-field')
);
});

// Create parent node for the table
const tableNode = new ExtraTablesNode(
tableName,
fieldNodes,
new ThemeIcon('database')
);

tableNodes.push(tableNode);
});

return tableNodes;
} catch (err) {
if (err instanceof Error) {
throw new Error(err.message);
} else {
throw new Error(String(err));
}
}
}
return [];
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Uri, TreeItem, TreeItemCollapsibleState, ThemeIcon, Command, TreeDataProvider, EventEmitter, Event, ExtensionContext } from 'vscode';
import { ScriptInfo } from '../types/script';
import { ScriptInfo } from '../types/odata/script';
import { SuperofficeAuthenticationProvider } from './superofficeAuthenticationProvider';
import { IHttpService } from '../services/httpService';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FileType, Uri, workspace, window, FileSystemError } from 'vscode'

import { IFileSystemHandler } from '../handlers/fileSystemHandler';
import { ScriptEntity } from '../types/script';
import { ScriptEntity } from '../types/odata/script';
import { SuoFile } from '../types/suoFile';
import path from 'path';
import { getFileType } from '../utils';
Expand Down
Loading
Loading