Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,107 @@
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, // Parameter required for DI container compatibility
private authProvider: SuperofficeAuthenticationProvider,
private httpService: IHttpService) {
// _context is intentionally unused; required for DI container compatibility
}

// 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