From 00a1b1ef3f1736e1add6e8276d8f268b41c9a0c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:31:53 +0000 Subject: [PATCH 01/10] Initial plan From eb1f141b5df06cd42c7efab140fdaa1ab4482647 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:49:29 +0000 Subject: [PATCH 02/10] feat: add Profile/PermissionSet editor webview UI with React - Add reusable WebviewPanel base class for VSCode webview panels - Add ProfileEditorWebview backend with profile data loading and save - Add ProfileEditorCommand to open the editor from context menu or palette - Add React-based UI with virtualized object/field permission tables - Add filter bar with match highlighting for 500+ objects / 800+ fields - Add floating ActionBar for save/reset operations - Add AddPermissionDialog for adding new object/field permissions - Add setObjectPermissions() and setFieldPermissions() public API to SalesforceUserPermissions - Add license and description public getters to SalesforceUserPermissions - Add esbuild script for bundling React webview - Register editProfilePermissions command in constants.ts and commands.yaml - Add React, react-dom, react-window, esbuild as devDependencies" Co-authored-by: Codeneos <787686+Codeneos@users.noreply.github.com> --- .../src/salesforceUserPermissions.ts | 76 +++ packages/vscode-extension/build/webview.mts | 69 +++ packages/vscode-extension/commands.yaml | 10 + packages/vscode-extension/package.json | 31 +- .../src/commands/profiles/index.ts | 1 + .../commands/profiles/profileEditorCommand.ts | 72 +++ packages/vscode-extension/src/constants.ts | 1 + .../vscode-extension/src/lib/webview/index.ts | 3 + .../src/lib/webview/profileEditorWebview.ts | 175 ++++++ .../vscode-extension/src/lib/webview/types.ts | 71 +++ .../src/lib/webview/webviewPanel.ts | 168 ++++++ .../src/webviews/profileEditor/App.tsx | 395 +++++++++++++ .../profileEditor/components/ActionBar.tsx | 53 ++ .../components/AddPermissionDialog.tsx | 147 +++++ .../components/FieldPermissionsTable.tsx | 156 +++++ .../profileEditor/components/FilterBar.tsx | 52 ++ .../components/ObjectPermissionsTable.tsx | 155 +++++ .../components/ProfileHeader.tsx | 43 ++ .../profileEditor/hooks/useVsCodeApi.ts | 41 ++ .../src/webviews/profileEditor/index.tsx | 20 + .../src/webviews/profileEditor/styles.css | 555 ++++++++++++++++++ .../src/webviews/profileEditor/types.ts | 65 ++ pnpm-lock.yaml | 310 ++++++---- 23 files changed, 2555 insertions(+), 114 deletions(-) create mode 100644 packages/vscode-extension/build/webview.mts create mode 100644 packages/vscode-extension/src/commands/profiles/profileEditorCommand.ts create mode 100644 packages/vscode-extension/src/lib/webview/index.ts create mode 100644 packages/vscode-extension/src/lib/webview/profileEditorWebview.ts create mode 100644 packages/vscode-extension/src/lib/webview/types.ts create mode 100644 packages/vscode-extension/src/lib/webview/webviewPanel.ts create mode 100644 packages/vscode-extension/src/webviews/profileEditor/App.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/components/ActionBar.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/components/AddPermissionDialog.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/components/FieldPermissionsTable.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/components/FilterBar.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/components/ObjectPermissionsTable.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/components/ProfileHeader.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/hooks/useVsCodeApi.ts create mode 100644 packages/vscode-extension/src/webviews/profileEditor/index.tsx create mode 100644 packages/vscode-extension/src/webviews/profileEditor/styles.css create mode 100644 packages/vscode-extension/src/webviews/profileEditor/types.ts diff --git a/packages/salesforce/src/salesforceUserPermissions.ts b/packages/salesforce/src/salesforceUserPermissions.ts index 52f5fa625..83ab9a5f4 100644 --- a/packages/salesforce/src/salesforceUserPermissions.ts +++ b/packages/salesforce/src/salesforceUserPermissions.ts @@ -99,6 +99,21 @@ export class SalesforceUserPermissions { return this.metadata.fullName; } + /** + * The license associated with this profile or permission set. + * For profiles this is `userLicense`, for permission sets this is `license`. + */ + public get license() : string | undefined { + return (this.metadata as any).userLicense ?? (this.metadata as any).license; + } + + /** + * Optional human-readable description of this profile or permission set. + */ + public get description() : string | undefined { + return (this.metadata as any).description; + } + constructor( public readonly type: 'Profile' | 'PermissionSet', public readonly developerName: string, @@ -249,6 +264,67 @@ export class SalesforceUserPermissions { this.removeItem('fieldPermissions', name); } + /** + * Adds or updates object-level permissions for the specified Salesforce object. + * + * @param objectName - The API name of the SObject. + * @param permissions - Partial permission flags to set. Salesforce access rules are automatically enforced: + * - modifyAllRecords implies viewAllRecords, allowRead, allowCreate, allowEdit, allowDelete. + * - viewAllRecords implies allowRead. + * - allowCreate / allowEdit / allowDelete imply allowRead. + */ + public setObjectPermissions(objectName: string, permissions: { + allowRead?: boolean; + allowCreate?: boolean; + allowEdit?: boolean; + allowDelete?: boolean; + viewAllRecords?: boolean; + modifyAllRecords?: boolean; + }) { + const existing = this.objects.find(o => o.object === objectName) ?? {}; + const merged = { ...existing, ...permissions, object: objectName }; + + // Enforce Salesforce access rules + if (merged.modifyAllRecords) { + merged.viewAllRecords = true; + merged.allowRead = true; + merged.allowCreate = merged.allowCreate ?? true; + merged.allowEdit = merged.allowEdit ?? true; + merged.allowDelete = merged.allowDelete ?? true; + } + if (merged.viewAllRecords) { + merged.allowRead = true; + } + if (merged.allowCreate || merged.allowEdit || merged.allowDelete) { + merged.allowRead = true; + } + if (!merged.allowRead) { + merged.allowCreate = false; + merged.allowEdit = false; + merged.allowDelete = false; + merged.viewAllRecords = false; + merged.modifyAllRecords = false; + } + + this.update('objectPermissions', merged); + } + + /** + * Sets field-level security permissions for the specified field. + * Enforces the Salesforce rule that editable fields must also be readable. + * + * @param fieldName - Qualified field name in the format "ObjectName.FieldName". + * @param readable - Whether the field should be readable. + * @param editable - Whether the field should be editable. If true, readable is also set to true. + */ + public setFieldPermissions(fieldName: string, readable: boolean, editable: boolean) { + this.update('fieldPermissions', { + field: fieldName, + readable: editable ? true : readable, + editable + }); + } + /** * Checks if the user has access to a specific Apex page by its name. * diff --git a/packages/vscode-extension/build/webview.mts b/packages/vscode-extension/build/webview.mts new file mode 100644 index 000000000..8c6afc5bf --- /dev/null +++ b/packages/vscode-extension/build/webview.mts @@ -0,0 +1,69 @@ +/** + * Build script for React webview bundles. + * Runs independently of tsdown to produce browser-targeted bundles. + */ +import { build, type BuildOptions } from 'esbuild'; +import { join } from 'path'; + +const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--watch'); +const watch = process.argv.includes('--watch'); + +const pkgDir = new URL('..', import.meta.url).pathname; + +const cssInjectPlugin = { + name: 'css-inject', + setup(pluginBuild: any) { + // Inject CSS into a + + +
+ + +`; + } + + private generateNonce(): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + return Array.from({ length: 32 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); + } + + private escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } +} diff --git a/packages/vscode-extension/src/webviews/profileEditor/App.tsx b/packages/vscode-extension/src/webviews/profileEditor/App.tsx new file mode 100644 index 000000000..ab9cbf463 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/App.tsx @@ -0,0 +1,395 @@ +import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import type { + ExtensionMessage, + FieldPermission, + ObjectPermission, + ProfileEditorData, + SObjectField, + WebviewMessage +} from './types'; +import { ProfileHeader } from './components/ProfileHeader'; +import { FilterBar } from './components/FilterBar'; +import { ObjectPermissionsTable } from './components/ObjectPermissionsTable'; +import { FieldPermissionsTable } from './components/FieldPermissionsTable'; +import { ActionBar } from './components/ActionBar'; +import { AddPermissionDialog } from './components/AddPermissionDialog'; + +type Tab = 'objects' | 'fields'; + +interface AppState { + data: ProfileEditorData | null; + objectChanges: Map; + fieldChanges: Map; + status: 'idle' | 'loading' | 'saving' | 'error'; + statusMessage: string; + availableObjects: string[]; + loadedFields: Map; +} + +type AppAction = + | { type: 'init'; data: ProfileEditorData } + | { type: 'loading'; message?: string } + | { type: 'saved' } + | { type: 'reset'; data: ProfileEditorData } + | { type: 'error'; message: string } + | { type: 'objectsLoaded'; objects: string[] } + | { type: 'fieldsLoaded'; objectName: string; fields: SObjectField[] } + | { type: 'changeObject'; permission: ObjectPermission } + | { type: 'changeField'; permission: FieldPermission } + | { type: 'addObject'; permission: ObjectPermission } + | { type: 'addField'; permission: FieldPermission }; + +function reducer(state: AppState, action: AppAction): AppState { + switch (action.type) { + case 'init': + return { + ...state, + data: action.data, + objectChanges: new Map(), + fieldChanges: new Map(), + status: 'idle', + statusMessage: '' + }; + case 'loading': + return { ...state, status: 'loading', statusMessage: action.message ?? 'Loading…' }; + case 'saving': + return { ...state, status: 'saving', statusMessage: 'Saving…' } as AppState; + case 'saved': + return { ...state, objectChanges: new Map(), fieldChanges: new Map(), status: 'idle', statusMessage: '' }; + case 'reset': + return { + ...state, + data: action.data, + objectChanges: new Map(), + fieldChanges: new Map(), + status: 'idle', + statusMessage: '' + }; + case 'error': + return { ...state, status: 'error', statusMessage: action.message }; + case 'objectsLoaded': + return { ...state, availableObjects: action.objects }; + case 'fieldsLoaded': { + const loadedFields = new Map(state.loadedFields); + loadedFields.set(action.objectName, action.fields); + return { ...state, loadedFields }; + } + case 'changeObject': { + const objectChanges = new Map(state.objectChanges); + objectChanges.set(action.permission.objectName, action.permission); + // Also update the main data + if (state.data) { + const objectPermissions = state.data.objectPermissions.map(op => + op.objectName === action.permission.objectName ? action.permission : op + ); + return { ...state, objectChanges, data: { ...state.data, objectPermissions } }; + } + return { ...state, objectChanges }; + } + case 'changeField': { + const fieldChanges = new Map(state.fieldChanges); + fieldChanges.set(action.permission.fieldName, action.permission); + if (state.data) { + const fieldPermissions = state.data.fieldPermissions.map(fp => + fp.fieldName === action.permission.fieldName ? action.permission : fp + ); + return { ...state, fieldChanges, data: { ...state.data, fieldPermissions } }; + } + return { ...state, fieldChanges }; + } + case 'addObject': { + const objectChanges = new Map(state.objectChanges); + objectChanges.set(action.permission.objectName, action.permission); + if (state.data) { + const existing = state.data.objectPermissions.find(op => op.objectName === action.permission.objectName); + const objectPermissions = existing + ? state.data.objectPermissions + : [...state.data.objectPermissions, action.permission]; + return { ...state, objectChanges, data: { ...state.data, objectPermissions } }; + } + return { ...state, objectChanges }; + } + case 'addField': { + const fieldChanges = new Map(state.fieldChanges); + fieldChanges.set(action.permission.fieldName, action.permission); + if (state.data) { + const existing = state.data.fieldPermissions.find(fp => fp.fieldName === action.permission.fieldName); + const fieldPermissions = existing + ? state.data.fieldPermissions + : [...state.data.fieldPermissions, action.permission]; + return { ...state, fieldChanges, data: { ...state.data, fieldPermissions } }; + } + return { ...state, fieldChanges }; + } + default: + return state; + } +} + +const initialState: AppState = { + data: null, + objectChanges: new Map(), + fieldChanges: new Map(), + status: 'loading', + statusMessage: 'Loading profile…', + availableObjects: [], + loadedFields: new Map() +}; + +interface AppProps { + postMessage: (msg: WebviewMessage) => void; +} + +/** + * Main React application for the Salesforce Profile/PermissionSet editor. + */ +export const App: React.FC = ({ postMessage }) => { + const [state, dispatch] = useReducer(reducer, initialState); + const [activeTab, setActiveTab] = useState('objects'); + const [filter, setFilter] = useState(''); + const [objectFilter, setObjectFilter] = useState(''); + const [dialogMode, setDialogMode] = useState<'object' | 'field' | null>(null); + + // Listen for messages from the extension + useEffect(() => { + const handler = (event: MessageEvent) => { + const msg = event.data; + switch (msg.type) { + case 'init': + dispatch({ type: 'init', data: msg.data }); + break; + case 'loading': + dispatch({ type: 'loading', message: msg.message }); + break; + case 'saved': + dispatch({ type: 'saved' }); + break; + case 'reset': + dispatch({ type: 'reset', data: msg.data }); + break; + case 'error': + dispatch({ type: 'error', message: msg.message }); + break; + case 'objectsLoaded': + dispatch({ type: 'objectsLoaded', objects: msg.objects }); + break; + case 'fieldsLoaded': + dispatch({ type: 'fieldsLoaded', objectName: msg.objectName, fields: msg.fields }); + break; + } + }; + window.addEventListener('message', handler); + return () => window.removeEventListener('message', handler); + }, []); + + // Tell the extension we're ready + useEffect(() => { + postMessage({ type: 'ready' }); + }, [postMessage]); + + const handleObjectChange = useCallback( + (perm: ObjectPermission) => dispatch({ type: 'changeObject', permission: perm }), + [] + ); + + const handleFieldChange = useCallback( + (perm: FieldPermission) => dispatch({ type: 'changeField', permission: perm }), + [] + ); + + const handleSave = useCallback(() => { + dispatch({ type: 'loading', message: 'Saving…' } as AppAction); + postMessage({ + type: 'save', + changes: { + objectPermissions: Array.from(state.objectChanges.values()), + fieldPermissions: Array.from(state.fieldChanges.values()) + } + }); + }, [state.objectChanges, state.fieldChanges, postMessage]); + + const handleReset = useCallback(() => { + postMessage({ type: 'reset' }); + }, [postMessage]); + + const handleOpenAddDialog = useCallback( + (mode: 'object' | 'field') => { + if (state.availableObjects.length === 0) { + postMessage({ type: 'loadObjects' }); + } + setDialogMode(mode); + }, + [state.availableObjects, postMessage] + ); + + const handleAddObject = useCallback( + (objectName: string) => { + dispatch({ + type: 'addObject', + permission: { + objectName, + allowRead: true, + allowCreate: false, + allowEdit: false, + allowDelete: false, + viewAllRecords: false, + modifyAllRecords: false + } + }); + }, + [] + ); + + const handleAddField = useCallback( + (objectName: string, fieldName: string) => { + dispatch({ + type: 'addField', + permission: { + fieldName: `${objectName}.${fieldName}`, + objectName, + readable: true, + editable: false + } + }); + }, + [] + ); + + const totalChanges = state.objectChanges.size + state.fieldChanges.size; + const hasChanges = totalChanges > 0; + + const filteredObjectCount = useMemo(() => { + if (!state.data) return 0; + if (!filter) return state.data.objectPermissions.length; + return state.data.objectPermissions.filter(p => + p.objectName.toLowerCase().includes(filter.toLowerCase()) + ).length; + }, [state.data, filter]); + + const filteredFieldCount = useMemo(() => { + if (!state.data) return 0; + const term = filter.toLowerCase(); + return state.data.fieldPermissions.filter(p => { + const matchesObject = !objectFilter || p.objectName.toLowerCase().includes(objectFilter.toLowerCase()); + const matchesField = !term || p.objectName.toLowerCase().includes(term) || p.fieldName.toLowerCase().includes(term); + return matchesObject && matchesField; + }).length; + }, [state.data, filter, objectFilter]); + + if (state.status === 'loading' && !state.data) { + return ( +
+
+ {state.statusMessage} +
+ ); + } + + if (state.status === 'error' && !state.data) { + return ( +
+ +

{state.statusMessage}

+
+ ); + } + + if (!state.data) { + return ( +
+
+ Waiting for data… +
+ ); + } + + return ( +
+ {/* Top error / saving banner */} + {state.status === 'error' && ( +
⚠ {state.statusMessage}
+ )} + {state.status === 'loading' && ( +
⏳ {state.statusMessage}
+ )} + + + + {/* Tabs */} +
+ + +
+ + {/* Filter bar */} + + + {/* Table area */} +
+ {activeTab === 'objects' ? ( + handleOpenAddDialog('object')} + /> + ) : ( + handleOpenAddDialog('field')} + /> + )} +
+ + {/* Floating action bar */} + + + {/* Add permission dialog */} + {dialogMode && ( + setDialogMode(null)} + onAddObject={handleAddObject} + onAddField={handleAddField} + postMessage={postMessage} + loadedFields={state.loadedFields} + /> + )} +
+ ); +}; diff --git a/packages/vscode-extension/src/webviews/profileEditor/components/ActionBar.tsx b/packages/vscode-extension/src/webviews/profileEditor/components/ActionBar.tsx new file mode 100644 index 000000000..99291c298 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/components/ActionBar.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +interface ActionBarProps { + hasChanges: boolean; + isSaving: boolean; + onSave: () => void; + onReset: () => void; + changeCount: number; +} + +/** + * Floating action bar fixed to the bottom of the screen. + * Shows save/reset buttons and unsaved change count. + */ +export const ActionBar: React.FC = ({ + hasChanges, + isSaving, + onSave, + onReset, + changeCount +}) => { + if (!hasChanges && !isSaving) { + return null; + } + + return ( +
+
+ {changeCount > 0 && ( + + {changeCount} unsaved change{changeCount !== 1 ? 's' : ''} + + )} + + +
+
+ ); +}; diff --git a/packages/vscode-extension/src/webviews/profileEditor/components/AddPermissionDialog.tsx b/packages/vscode-extension/src/webviews/profileEditor/components/AddPermissionDialog.tsx new file mode 100644 index 000000000..2a9575a75 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/components/AddPermissionDialog.tsx @@ -0,0 +1,147 @@ +import React, { useState, useEffect } from 'react'; +import type { SObjectField, WebviewMessage } from '../types'; + +interface AddPermissionDialogProps { + mode: 'object' | 'field'; + availableObjects: string[]; + isOpen: boolean; + onClose: () => void; + onAddObject: (objectName: string) => void; + onAddField: (objectName: string, fieldName: string) => void; + postMessage: (msg: WebviewMessage) => void; + loadedFields: Map; +} + +/** + * Dialog for adding new object or field permissions. + */ +export const AddPermissionDialog: React.FC = ({ + mode, + availableObjects, + isOpen, + onClose, + onAddObject, + onAddField, + postMessage, + loadedFields +}) => { + const [selectedObject, setSelectedObject] = useState(''); + const [selectedField, setSelectedField] = useState(''); + const [objectFilter, setObjectFilter] = useState(''); + const [loadingFields, setLoadingFields] = useState(false); + + useEffect(() => { + if (isOpen) { + setSelectedObject(''); + setSelectedField(''); + setObjectFilter(''); + } + }, [isOpen]); + + useEffect(() => { + if (selectedObject && mode === 'field' && !loadedFields.has(selectedObject)) { + setLoadingFields(true); + postMessage({ type: 'loadFields', objectName: selectedObject }); + } else { + setLoadingFields(false); + } + }, [selectedObject, mode, loadedFields, postMessage]); + + useEffect(() => { + if (loadedFields.has(selectedObject)) { + setLoadingFields(false); + } + }, [loadedFields, selectedObject]); + + if (!isOpen) return null; + + const filteredObjects = objectFilter + ? availableObjects.filter(o => o.toLowerCase().includes(objectFilter.toLowerCase())) + : availableObjects; + + const fields = loadedFields.get(selectedObject) ?? []; + + const handleConfirm = () => { + if (mode === 'object' && selectedObject) { + onAddObject(selectedObject); + onClose(); + } else if (mode === 'field' && selectedObject && selectedField) { + onAddField(selectedObject, selectedField); + onClose(); + } + }; + + return ( +
+
+
+

+ {mode === 'object' ? 'Add Object Permission' : 'Add Field Permission'} +

+ +
+ +
+
+ + setObjectFilter(e.target.value)} + /> + +
+ + {mode === 'field' && selectedObject && ( +
+ + {loadingFields ? ( + Loading fields… + ) : ( + + )} +
+ )} +
+ +
+ + +
+
+
+ ); +}; diff --git a/packages/vscode-extension/src/webviews/profileEditor/components/FieldPermissionsTable.tsx b/packages/vscode-extension/src/webviews/profileEditor/components/FieldPermissionsTable.tsx new file mode 100644 index 000000000..97800aaca --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/components/FieldPermissionsTable.tsx @@ -0,0 +1,156 @@ +import React, { useCallback } from 'react'; +import { List } from 'react-window'; +import type { FieldPermission } from '../types'; + +const ROW_HEIGHT = 32; +const HEADER_HEIGHT = 40; + +interface FieldPermissionsTableProps { + permissions: FieldPermission[]; + filter: string; + objectFilter: string; + onChange: (updated: FieldPermission) => void; + onAddField?: () => void; +} + +function highlightMatch(text: string, filter: string): React.ReactNode { + if (!filter) return text; + const idx = text.toLowerCase().indexOf(filter.toLowerCase()); + if (idx === -1) return text; + return ( + <> + {text.slice(0, idx)} + {text.slice(idx, idx + filter.length)} + {text.slice(idx + filter.length)} + + ); +} + +// Row props spread directly into row component in react-window v2 +interface FieldRowRowProps { + permissions: FieldPermission[]; + filter: string; + onChange: (updated: FieldPermission) => void; +} + +interface FieldRowComponentProps extends FieldRowRowProps { + index: number; + style: React.CSSProperties; + ariaAttributes?: Record; +} + +const FieldRow: React.FC = ({ index, permissions, filter, onChange, style }) => { + const perm = permissions[index]; + if (!perm) return null; + + const handleReadable = (checked: boolean) => { + const updated = { ...perm, readable: checked }; + if (!checked) { + updated.editable = false; + } + onChange(updated); + }; + + const handleEditable = (checked: boolean) => { + const updated = { ...perm, editable: checked }; + if (checked) { + updated.readable = true; + } + onChange(updated); + }; + + const fieldPart = perm.fieldName.includes('.') + ? perm.fieldName.split('.').slice(1).join('.') + : perm.fieldName; + + return ( +
+
+ {highlightMatch(perm.objectName, filter)} +
+
+ {highlightMatch(fieldPart, filter)} +
+
+ handleReadable(e.target.checked)} + aria-label={`${perm.fieldName} readable`} + /> +
+
+ handleEditable(e.target.checked)} + aria-label={`${perm.fieldName} editable`} + /> +
+
+ ); +}; + +/** + * Virtualized table for field-level security (FLS) permissions. + * Efficiently renders 800+ fields using react-window. + */ +export const FieldPermissionsTable: React.FC = ({ + permissions, + filter, + objectFilter, + onChange, + onAddField +}) => { + const term = filter.toLowerCase(); + const filtered = permissions.filter(p => { + const matchesObject = !objectFilter || p.objectName.toLowerCase().includes(objectFilter.toLowerCase()); + const matchesField = !term || + p.objectName.toLowerCase().includes(term) || + p.fieldName.toLowerCase().includes(term); + return matchesObject && matchesField; + }); + + const rowProps: FieldRowRowProps = { permissions: filtered, filter: term, onChange }; + + return ( +
+ {/* Sticky header */} +
+
+
+ Object +
+
+ Field + {onAddField && ( + + )} +
+
+ R +
+
+ E +
+
+
+ + {/* Virtualized body */} + +
+ ); +}; diff --git a/packages/vscode-extension/src/webviews/profileEditor/components/FilterBar.tsx b/packages/vscode-extension/src/webviews/profileEditor/components/FilterBar.tsx new file mode 100644 index 000000000..59699c827 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/components/FilterBar.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +interface FilterBarProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + resultCount?: number; + totalCount?: number; +} + +/** + * A search/filter bar used to narrow down visible rows in permission tables. + */ +export const FilterBar: React.FC = ({ + value, + onChange, + placeholder = 'Filter by name…', + resultCount, + totalCount +}) => { + return ( +
+
+ 🔍 + onChange(e.target.value)} + placeholder={placeholder} + spellCheck={false} + autoComplete="off" + /> + {value && ( + + )} +
+ {totalCount !== undefined && ( + + {resultCount ?? totalCount} / {totalCount} + + )} +
+ ); +}; diff --git a/packages/vscode-extension/src/webviews/profileEditor/components/ObjectPermissionsTable.tsx b/packages/vscode-extension/src/webviews/profileEditor/components/ObjectPermissionsTable.tsx new file mode 100644 index 000000000..7fde42d24 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/components/ObjectPermissionsTable.tsx @@ -0,0 +1,155 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { List } from 'react-window'; +import type { ObjectPermission } from '../types'; + +const OBJECT_COLUMNS: { key: keyof Omit; label: string; abbr: string }[] = [ + { key: 'allowRead', label: 'Read', abbr: 'R' }, + { key: 'allowCreate', label: 'Create', abbr: 'C' }, + { key: 'allowEdit', label: 'Edit', abbr: 'E' }, + { key: 'allowDelete', label: 'Delete', abbr: 'D' }, + { key: 'viewAllRecords', label: 'View All', abbr: 'VA' }, + { key: 'modifyAllRecords', label: 'Modify All', abbr: 'MA' }, +]; + +const ROW_HEIGHT = 32; +const HEADER_HEIGHT = 40; + +interface ObjectPermissionsTableProps { + permissions: ObjectPermission[]; + filter: string; + onChange: (updated: ObjectPermission) => void; + onAddObject?: () => void; +} + +function highlightMatch(text: string, filter: string): React.ReactNode { + if (!filter) return text; + const idx = text.toLowerCase().indexOf(filter.toLowerCase()); + if (idx === -1) return text; + return ( + <> + {text.slice(0, idx)} + {text.slice(idx, idx + filter.length)} + {text.slice(idx + filter.length)} + + ); +} + +// Row props spread directly into row component in react-window v2 +interface ObjectRowRowProps { + permissions: ObjectPermission[]; + filter: string; + onChange: (updated: ObjectPermission) => void; +} + +interface ObjectRowComponentProps extends ObjectRowRowProps { + index: number; + style: React.CSSProperties; + ariaAttributes?: Record; +} + +const ObjectRow: React.FC = ({ index, permissions, filter, onChange, style }) => { + const perm = permissions[index]; + if (!perm) return null; + + const handleCheck = (key: keyof Omit, checked: boolean) => { + const updated = { ...perm, [key]: checked }; + + // Enforce Salesforce rules: higher-level perms imply lower ones + if (key === 'modifyAllRecords' && checked) { + updated.viewAllRecords = true; + updated.allowRead = true; + updated.allowCreate = true; + updated.allowEdit = true; + updated.allowDelete = true; + } else if (key === 'viewAllRecords' && checked) { + updated.allowRead = true; + } else if ((key === 'allowCreate' || key === 'allowEdit' || key === 'allowDelete') && checked) { + updated.allowRead = true; + } else if (key === 'allowRead' && !checked) { + updated.allowCreate = false; + updated.allowEdit = false; + updated.allowDelete = false; + updated.viewAllRecords = false; + updated.modifyAllRecords = false; + } + + onChange(updated); + }; + + return ( +
+
+ {highlightMatch(perm.objectName, filter)} +
+ {OBJECT_COLUMNS.map(col => ( +
+ handleCheck(col.key, e.target.checked)} + aria-label={`${perm.objectName} ${col.label}`} + /> +
+ ))} +
+ ); +}; + +/** + * Virtualized table for object-level permissions. + * Renders 500+ objects efficiently using react-window. + */ +export const ObjectPermissionsTable: React.FC = ({ + permissions, + filter, + onChange, + onAddObject +}) => { + const filtered = filter + ? permissions.filter(p => p.objectName.toLowerCase().includes(filter.toLowerCase())) + : permissions; + + const rowProps: ObjectRowRowProps = { permissions: filtered, filter, onChange }; + + return ( +
+ {/* Sticky header */} +
+
+
+ Object + {onAddObject && ( + + )} +
+ {OBJECT_COLUMNS.map(col => ( +
+ {col.abbr} +
+ ))} +
+
+ + {/* Virtualized body */} + +
+ ); +}; diff --git a/packages/vscode-extension/src/webviews/profileEditor/components/ProfileHeader.tsx b/packages/vscode-extension/src/webviews/profileEditor/components/ProfileHeader.tsx new file mode 100644 index 000000000..eadb86352 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/components/ProfileHeader.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import type { ProfileEditorData } from '../types'; + +interface ProfileHeaderProps { + data: ProfileEditorData; +} + +/** + * Displays high-level info about the profile/permission set at the top of the editor. + */ +export const ProfileHeader: React.FC = ({ data }) => { + const objectCount = data.objectPermissions.length; + const fieldCount = data.fieldPermissions.length; + + return ( +
+
+ + {data.profileType === 'Profile' ? '👤' : '🔑'} + +

{data.profileName}

+
+
+ {data.userLicense && ( + + License: {data.userLicense} + + )} + + Objects: {objectCount} + + + Fields: {fieldCount} + + {data.description && ( + + {data.description} + + )} +
+
+ ); +}; diff --git a/packages/vscode-extension/src/webviews/profileEditor/hooks/useVsCodeApi.ts b/packages/vscode-extension/src/webviews/profileEditor/hooks/useVsCodeApi.ts new file mode 100644 index 000000000..2569f59a4 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/hooks/useVsCodeApi.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef } from 'react'; +import type { VsCodeApi, WebviewMessage } from '../types'; + +// The acquireVsCodeApi function is injected by VS Code into the webview context. +declare function acquireVsCodeApi(): VsCodeApi; + +let _api: VsCodeApi | undefined; + +function getVsCodeApi(): VsCodeApi { + if (!_api) { + _api = acquireVsCodeApi(); + } + return _api; +} + +/** + * Hook that provides the VS Code API for message passing. + * Returns a stable postMessage function. + */ +export function useVsCodeApi() { + const apiRef = useRef(undefined); + + if (!apiRef.current) { + try { + apiRef.current = getVsCodeApi(); + } catch { + // Running outside of VS Code (e.g. browser preview) + apiRef.current = { + postMessage: (msg) => console.log('[webview → extension]', msg), + getState: () => undefined, + setState: () => undefined, + }; + } + } + + return { + postMessage: (message: WebviewMessage) => apiRef.current!.postMessage(message), + getState: () => apiRef.current!.getState(), + setState: (state: T) => apiRef.current!.setState(state), + }; +} diff --git a/packages/vscode-extension/src/webviews/profileEditor/index.tsx b/packages/vscode-extension/src/webviews/profileEditor/index.tsx new file mode 100644 index 000000000..a56c8f5c3 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/index.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import { useVsCodeApi } from './hooks/useVsCodeApi'; +import './styles.css'; + +const Root: React.FC = () => { + const { postMessage } = useVsCodeApi(); + return ; +}; + +const container = document.getElementById('root'); +if (container) { + const root = createRoot(container); + root.render( + + + + ); +} diff --git a/packages/vscode-extension/src/webviews/profileEditor/styles.css b/packages/vscode-extension/src/webviews/profileEditor/styles.css new file mode 100644 index 000000000..583906deb --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/styles.css @@ -0,0 +1,555 @@ +/* ─── Base reset ──────────────────────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +button { + cursor: pointer; + font: inherit; + color: inherit; +} + +input[type="text"] { + font: inherit; + color: inherit; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, transparent); + color: var(--vscode-input-foreground); + border-radius: 2px; +} + +input[type="text"]:focus { + outline: 1px solid var(--vscode-focusBorder); +} + +/* ─── App layout ──────────────────────────────────────────────────────────── */ +.app { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; + background: var(--vscode-editor-background); + color: var(--vscode-editor-foreground); +} + +.app__banner { + padding: 4px 12px; + font-size: 12px; + line-height: 1.5; +} + +.app__banner--error { + background: var(--vscode-inputValidation-errorBackground, #5a1d1d); + color: var(--vscode-inputValidation-errorForeground, #f14c4c); + border-bottom: 1px solid var(--vscode-inputValidation-errorBorder, #be1100); +} + +.app__banner--info { + background: var(--vscode-inputValidation-infoBackground, #063b49); + color: var(--vscode-inputValidation-infoForeground, #75beff); + border-bottom: 1px solid var(--vscode-inputValidation-infoBorder, #007acc); +} + +/* ─── Loading / error states ──────────────────────────────────────────────── */ +.loading-state, +.error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + gap: 12px; + color: var(--vscode-descriptionForeground); +} + +.loading-spinner { + width: 24px; + height: 24px; + border: 3px solid var(--vscode-focusBorder, #007acc); + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.error-icon { + font-size: 2rem; + color: var(--vscode-errorForeground, #f14c4c); +} + +/* ─── Profile header ──────────────────────────────────────────────────────── */ +.profile-header { + padding: 10px 16px 8px; + border-bottom: 1px solid var(--vscode-panel-border, rgba(128,128,128,0.2)); + flex-shrink: 0; +} + +.profile-header__title { + display: flex; + align-items: center; + gap: 8px; +} + +.profile-header__badge { + font-size: 1.2rem; + line-height: 1; +} + +.profile-header__name { + font-size: 1.1rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.profile-header__meta { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 6px; +} + +.profile-header__tag { + font-size: 12px; + color: var(--vscode-descriptionForeground); +} + +.profile-header__tag--description { + font-style: italic; +} + +/* ─── Tab bar ─────────────────────────────────────────────────────────────── */ +.tab-bar { + display: flex; + border-bottom: 1px solid var(--vscode-panel-border, rgba(128,128,128,0.2)); + flex-shrink: 0; +} + +.tab-bar__tab { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--vscode-tab-inactiveForeground, #8a8a8a); + font-size: 13px; + user-select: none; + transition: color 0.1s, border-color 0.1s; +} + +.tab-bar__tab:hover { + color: var(--vscode-tab-activeForeground, #fff); + background: var(--vscode-tab-hoverBackground, rgba(255,255,255,0.05)); +} + +.tab-bar__tab--active { + color: var(--vscode-tab-activeForeground, #fff); + border-bottom-color: var(--vscode-focusBorder, #007acc); +} + +.tab-bar__badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 16px; + padding: 0 4px; + font-size: 10px; + font-weight: 600; + border-radius: 8px; + background: var(--vscode-badge-background, #4d4d4d); + color: var(--vscode-badge-foreground, #fff); +} + +/* ─── Filter bar ──────────────────────────────────────────────────────────── */ +.filter-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-bottom: 1px solid var(--vscode-panel-border, rgba(128,128,128,0.2)); + flex-shrink: 0; +} + +.filter-bar__input-wrapper { + display: flex; + align-items: center; + flex: 1; + max-width: 480px; + background: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 2px; + padding: 0 6px; +} + +.filter-bar__input-wrapper:focus-within { + outline: 1px solid var(--vscode-focusBorder, #007acc); +} + +.filter-bar__icon { + font-size: 12px; + opacity: 0.5; + flex-shrink: 0; +} + +.filter-bar__input { + flex: 1; + background: none; + border: none; + outline: none; + padding: 4px 6px; + font-size: 13px; + color: var(--vscode-input-foreground); +} + +.filter-bar__clear { + background: none; + border: none; + font-size: 12px; + opacity: 0.5; + flex-shrink: 0; + line-height: 1; + padding: 2px; +} + +.filter-bar__clear:hover { + opacity: 1; +} + +.filter-bar__count { + font-size: 11px; + color: var(--vscode-descriptionForeground); + white-space: nowrap; +} + +/* ─── Table container ─────────────────────────────────────────────────────── */ +.table-container { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 0; +} + +/* ─── Permissions table ───────────────────────────────────────────────────── */ +.permissions-table { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; +} + +/* The List from react-window needs to fill the remaining space */ +.permissions-table > div:last-child { + flex: 1 1 auto; + min-height: 0; +} + +.table-header { + flex-shrink: 0; + overflow: hidden; + background: var(--vscode-editor-background); + border-bottom: 2px solid var(--vscode-panel-border, rgba(128,128,128,0.3)); +} + +.table-row { + display: flex; + align-items: center; + border-bottom: 1px solid var(--vscode-panel-border, rgba(128,128,128,0.1)); +} + +.table-row:hover:not(.table-row--header) { + background: var(--vscode-list-hoverBackground, rgba(255,255,255,0.04)); +} + +.table-row--header { + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--vscode-descriptionForeground); + height: 40px; +} + +.table-cell { + padding: 0 8px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: 13px; + height: 100%; + display: flex; + align-items: center; +} + +.table-cell--name { + flex: 1; + min-width: 0; +} + +.table-cell--object { + width: 180px; + flex: 0 0 180px; +} + +.table-cell--field { + flex: 1; + min-width: 0; +} + +.table-cell--check { + width: 48px; + flex: 0 0 48px; + justify-content: center; + text-align: center; +} + +.table-cell--header { + color: var(--vscode-descriptionForeground); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; + position: relative; +} + +.table-cell input[type="checkbox"] { + cursor: pointer; + width: 14px; + height: 14px; + accent-color: var(--vscode-checkbox-foreground, #007acc); +} + +/* Filter match highlighting */ +strong { + font-weight: 700; + color: var(--vscode-editor-foreground); + background: var(--vscode-editor-findMatchHighlightBackground, rgba(255,214,0,0.25)); + border-radius: 2px; +} + +.table-add-btn { + margin-left: 4px; + background: none; + border: 1px solid var(--vscode-button-border, transparent); + font-size: 14px; + line-height: 1; + padding: 0 4px; + border-radius: 2px; + color: var(--vscode-textLink-foreground, #3794ff); + opacity: 0.7; + vertical-align: middle; +} + +.table-add-btn:hover { + opacity: 1; + background: var(--vscode-button-hoverBackground, rgba(255,255,255,0.1)); +} + +/* ─── Action bar (floating save/reset) ────────────────────────────────────── */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 100; + pointer-events: none; + padding: 0 16px 12px; + display: flex; + justify-content: flex-end; +} + +.action-bar__content { + display: flex; + align-items: center; + gap: 8px; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border, rgba(128,128,128,0.3)); + border-radius: 6px; + padding: 8px 12px; + box-shadow: 0 2px 12px rgba(0,0,0,0.3); + pointer-events: all; +} + +.action-bar__count { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-right: 4px; +} + +.action-bar__btn { + padding: 5px 14px; + border-radius: 3px; + font-size: 13px; + border: none; +} + +.action-bar__btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.action-bar__btn--reset { + background: var(--vscode-button-secondaryBackground, #3a3d41); + color: var(--vscode-button-secondaryForeground, #ccc); +} + +.action-bar__btn--reset:hover:not(:disabled) { + background: var(--vscode-button-secondaryHoverBackground, #4a4d51); +} + +.action-bar__btn--save { + background: var(--vscode-button-background, #0e639c); + color: var(--vscode-button-foreground, #fff); +} + +.action-bar__btn--save:hover:not(:disabled) { + background: var(--vscode-button-hoverBackground, #1177bb); +} + +/* ─── Dialog ──────────────────────────────────────────────────────────────── */ +.dialog-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 200; + display: flex; + align-items: center; + justify-content: center; +} + +.dialog { + background: var(--vscode-editorWidget-background, #252526); + border: 1px solid var(--vscode-widget-border, rgba(128,128,128,0.35)); + border-radius: 6px; + min-width: 400px; + max-width: 600px; + width: 90%; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5); +} + +.dialog__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--vscode-panel-border, rgba(128,128,128,0.2)); + flex-shrink: 0; +} + +.dialog__title { + font-size: 14px; + font-weight: 600; +} + +.dialog__close { + background: none; + border: none; + font-size: 14px; + opacity: 0.6; + padding: 2px 4px; +} + +.dialog__close:hover { + opacity: 1; +} + +.dialog__body { + padding: 12px 16px; + display: flex; + flex-direction: column; + gap: 12px; + overflow-y: auto; + flex: 1; +} + +.dialog__field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.dialog__label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--vscode-descriptionForeground); +} + +.dialog__input { + padding: 4px 8px; + font-size: 13px; + border-radius: 2px; +} + +.dialog__select { + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 2px; + font: inherit; + font-size: 13px; + padding: 2px; +} + +.dialog__select:focus { + outline: 1px solid var(--vscode-focusBorder, #007acc); +} + +.dialog__loading { + font-size: 13px; + color: var(--vscode-descriptionForeground); + padding: 4px; +} + +.dialog__footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 10px 16px; + border-top: 1px solid var(--vscode-panel-border, rgba(128,128,128,0.2)); + flex-shrink: 0; +} + +.dialog__btn { + padding: 5px 14px; + border-radius: 3px; + font-size: 13px; + border: none; +} + +.dialog__btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.dialog__btn--cancel { + background: var(--vscode-button-secondaryBackground, #3a3d41); + color: var(--vscode-button-secondaryForeground, #ccc); +} + +.dialog__btn--cancel:hover:not(:disabled) { + background: var(--vscode-button-secondaryHoverBackground, #4a4d51); +} + +.dialog__btn--confirm { + background: var(--vscode-button-background, #0e639c); + color: var(--vscode-button-foreground, #fff); +} + +.dialog__btn--confirm:hover:not(:disabled) { + background: var(--vscode-button-hoverBackground, #1177bb); +} diff --git a/packages/vscode-extension/src/webviews/profileEditor/types.ts b/packages/vscode-extension/src/webviews/profileEditor/types.ts new file mode 100644 index 000000000..4d2ead3e1 --- /dev/null +++ b/packages/vscode-extension/src/webviews/profileEditor/types.ts @@ -0,0 +1,65 @@ +/** + * Shared types used by the React webview UI. + * This file mirrors the types from the extension's lib/webview/types.ts. + */ + +export interface ObjectPermission { + objectName: string; + allowRead: boolean; + allowCreate: boolean; + allowEdit: boolean; + allowDelete: boolean; + viewAllRecords: boolean; + modifyAllRecords: boolean; +} + +export interface FieldPermission { + fieldName: string; + objectName: string; + readable: boolean; + editable: boolean; +} + +export interface ProfileEditorData { + profileName: string; + profileType: 'Profile' | 'PermissionSet'; + userLicense?: string; + description?: string; + objectPermissions: ObjectPermission[]; + fieldPermissions: FieldPermission[]; + availableObjects?: string[]; +} + +export interface PermissionChanges { + objectPermissions: ObjectPermission[]; + fieldPermissions: FieldPermission[]; +} + +export interface SObjectField { + name: string; + label: string; + type: string; +} + +export type ExtensionMessage = + | { type: 'init'; data: ProfileEditorData } + | { type: 'loading'; message?: string } + | { type: 'saved' } + | { type: 'reset'; data: ProfileEditorData } + | { type: 'error'; message: string } + | { type: 'objectsLoaded'; objects: string[] } + | { type: 'fieldsLoaded'; objectName: string; fields: SObjectField[] }; + +export type WebviewMessage = + | { type: 'ready' } + | { type: 'save'; changes: PermissionChanges } + | { type: 'reset' } + | { type: 'loadObjects' } + | { type: 'loadFields'; objectName: string }; + +/** VSCode API injected into the webview via acquireVsCodeApi() */ +export interface VsCodeApi { + postMessage(message: WebviewMessage): void; + getState(): T | undefined; + setState(state: T): void; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85c218ce9..e02da5b6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,7 +169,7 @@ importers: version: 1.0.0-rc.3 ts-jest: specifier: ^29.3.4 - version: 29.4.6(@babel/core@7.23.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.6))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.10.11)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.23.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.6))(esbuild@0.27.4)(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.10.11)(typescript@5.9.3)))(typescript@5.9.3) tsdown: specifier: 0.20.3 version: 0.20.3(@arethetypeswrong/core@0.18.2)(typescript@5.9.3)(unplugin-unused@0.5.7) @@ -586,6 +586,15 @@ importers: '@types/luxon': specifier: ^3.3.0 version: 3.7.1 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@types/react-window': + specifier: ^2.0.0 + version: 2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@types/vscode': specifier: ^1.95.0 version: 1.109.0 @@ -625,6 +634,9 @@ importers: chalk: specifier: ^4.1.2 version: 4.1.2 + esbuild: + specifier: ^0.27.4 + version: 0.27.4 escape-string-regexp: specifier: ^4.0.0 version: 4.0.0 @@ -636,7 +648,7 @@ importers: version: 11.3.3 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@25.2.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@25.2.1)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.10.11)(typescript@5.9.3)) js-yaml: specifier: ^4.1.0 version: 4.1.1 @@ -658,6 +670,15 @@ importers: ovsx: specifier: ^0.10.7 version: 0.10.9 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-window: + specifier: ^2.2.7 + version: 2.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) reflect-metadata: specifier: ^0.1.13 version: 0.1.14 @@ -669,7 +690,7 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.3.4 - version: 29.4.6(@babel/core@7.23.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.6))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@25.2.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.23.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.6))(esbuild@0.27.4)(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.10.11)(typescript@5.9.3)))(typescript@5.9.3) tsx: specifier: ^4.19.4 version: 4.21.0 @@ -1015,158 +1036,158 @@ packages: resolution: {integrity: sha512-oeJK41dcdqkvdZy/HctKklJNkt/jh+av3PZARrZEl+fs/8HaHeeYoAvEwOV0u5I6bArTF17JEsTZMY359e/nfQ==} engines: {node: ^12 || ^14 || ^16 || ^17} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -2465,6 +2486,18 @@ packages: resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react-window@2.0.0': + resolution: {integrity: sha512-E8hMDtImEpMk1SjswSvqoSmYvk7GEtyVaTa/GJV++FdDNuMVVEzpAClyJ0nqeKYBrMkGiyH6M1+rPLM0Nu1exQ==} + deprecated: This is a stub types definition. react-window provides its own type definitions, so you do not need this installed. + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/sarif@2.1.7': resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} @@ -3450,6 +3483,9 @@ packages: resolution: {integrity: sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==} engines: {node: '>=18'} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csv-parse@4.16.3: resolution: {integrity: sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==} @@ -3820,8 +3856,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} hasBin: true @@ -6292,9 +6328,24 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + peerDependencies: + react: ^19.2.4 + react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-window@2.2.7: + resolution: {integrity: sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + read-cmd-shim@6.0.0: resolution: {integrity: sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A==} engines: {node: ^20.17.0 || >=22.9.0} @@ -6553,6 +6604,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secretlint@10.2.2: resolution: {integrity: sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==} engines: {node: '>=20.0.0'} @@ -8194,82 +8248,82 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 2.2.5 - '@esbuild/aix-ppc64@0.27.3': + '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.27.4': optional: true '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': @@ -9981,6 +10035,21 @@ snapshots: dependencies: parse-path: 7.1.0 + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react-window@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + react-window: 2.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + transitivePeerDependencies: + - react + - react-dom + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + '@types/sarif@2.1.7': {} '@types/semver@7.3.13': {} @@ -11174,6 +11243,8 @@ snapshots: dependencies: rrweb-cssom: 0.6.0 + csstype@3.2.3: {} + csv-parse@4.16.3: {} csv-parse@5.6.0: {} @@ -11599,34 +11670,34 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.27.3: + esbuild@0.27.4: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 escalade@3.1.1: {} @@ -14625,8 +14696,20 @@ snapshots: strip-json-comments: 2.0.1 optional: true + react-dom@19.2.4(react@19.2.4): + dependencies: + react: 19.2.4 + scheduler: 0.27.0 + react-is@18.2.0: {} + react-window@2.2.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + react@19.2.4: {} + read-cmd-shim@6.0.0: {} read-pkg@9.0.1: @@ -14976,6 +15059,8 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.27.0: {} + secretlint@10.2.2: dependencies: '@secretlint/config-creator': 10.2.2 @@ -15569,7 +15654,7 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.6(@babel/core@7.23.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.6))(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.10.11)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.23.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.6))(esbuild@0.27.4)(jest-util@29.7.0)(jest@29.7.0(@types/node@24.10.11)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@24.10.11)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -15587,6 +15672,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.23.6) + esbuild: 0.27.4 jest-util: 29.7.0 ts-jest@29.4.6(@babel/core@7.23.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.6))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@25.2.1)(typescript@5.9.3)))(typescript@5.9.3): @@ -15707,7 +15793,7 @@ snapshots: tsx@4.21.0: dependencies: - esbuild: 0.27.3 + esbuild: 0.27.4 get-tsconfig: 4.13.5 optionalDependencies: fsevents: 2.3.3 From 73d8e333fdc66ec93da86aabda58baf31b7fd429 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:59:33 +0000 Subject: [PATCH 03/10] fix: address code review feedback - Fix Salesforce field type comparison to be case-insensitive (id vs ID) - Fix modifyAllRecords to unconditionally set all object permissions - Remove duplicate config.vlocity.salesforce.enabled from menu when conditions - Remove unnecessary type cast in App.tsx reducer - Add 'saving' to AppAction union type for proper type safety - Fix handleSave to dispatch 'saving' action instead of 'loading'" Co-authored-by: Codeneos <787686+Codeneos@users.noreply.github.com> --- packages/salesforce/src/salesforceUserPermissions.ts | 6 +++--- packages/vscode-extension/commands.yaml | 4 ++-- packages/vscode-extension/package.json | 4 ++-- .../src/lib/webview/profileEditorWebview.ts | 2 +- .../vscode-extension/src/webviews/profileEditor/App.tsx | 5 +++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/salesforce/src/salesforceUserPermissions.ts b/packages/salesforce/src/salesforceUserPermissions.ts index 83ab9a5f4..404149e03 100644 --- a/packages/salesforce/src/salesforceUserPermissions.ts +++ b/packages/salesforce/src/salesforceUserPermissions.ts @@ -288,9 +288,9 @@ export class SalesforceUserPermissions { if (merged.modifyAllRecords) { merged.viewAllRecords = true; merged.allowRead = true; - merged.allowCreate = merged.allowCreate ?? true; - merged.allowEdit = merged.allowEdit ?? true; - merged.allowDelete = merged.allowDelete ?? true; + merged.allowCreate = true; + merged.allowEdit = true; + merged.allowDelete = true; } if (merged.viewAllRecords) { merged.allowRead = true; diff --git a/packages/vscode-extension/commands.yaml b/packages/vscode-extension/commands.yaml index 63c4d9525..2b9b81cfd 100644 --- a/packages/vscode-extension/commands.yaml +++ b/packages/vscode-extension/commands.yaml @@ -252,9 +252,9 @@ vlocode.editProfilePermissions: menus: - menu: commandPalette - menu: editor/context - when: config.vlocity.salesforce.enabled && resourceScheme == file && resourceFilename =~ /(\.profile-meta\.xml|\.profile|\.permissionset-meta\.xml|\.permissionset)$/ + when: resourceScheme == file && resourceFilename =~ /(\.profile-meta\.xml|\.profile|\.permissionset-meta\.xml|\.permissionset)$/ - menu: explorer/context - when: config.vlocity.salesforce.enabled && resourceScheme == file && resourceFilename =~ /(\.profile-meta\.xml|\.profile|\.permissionset-meta\.xml|\.permissionset)$/ + when: resourceScheme == file && resourceFilename =~ /(\.profile-meta\.xml|\.profile|\.permissionset-meta\.xml|\.permissionset)$/ vlocode.deployRecentValidation: title: 'Salesforce: Deploy Recent Validation' group: v_salesforce diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 4a920053c..c270176ab 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -794,7 +794,7 @@ { "command": "vlocode.editProfilePermissions", "group": "v_salesforce_profile", - "when": "config.vlocity.salesforce.enabled && config.vlocity.salesforce.enabled && resourceScheme == file && resourceFilename =~ /(\\.profile-meta\\.xml|\\.profile|\\.permissionset-meta\\.xml|\\.permissionset)$/" + "when": "config.vlocity.salesforce.enabled && resourceScheme == file && resourceFilename =~ /(\\.profile-meta\\.xml|\\.profile|\\.permissionset-meta\\.xml|\\.permissionset)$/" }, { "command": "vlocode.omniScript.generateLwc", @@ -896,7 +896,7 @@ { "command": "vlocode.editProfilePermissions", "group": "v_salesforce_profile", - "when": "config.vlocity.salesforce.enabled && config.vlocity.salesforce.enabled && resourceScheme == file && resourceFilename =~ /(\\.profile-meta\\.xml|\\.profile|\\.permissionset-meta\\.xml|\\.permissionset)$/" + "when": "config.vlocity.salesforce.enabled && resourceScheme == file && resourceFilename =~ /(\\.profile-meta\\.xml|\\.profile|\\.permissionset-meta\\.xml|\\.permissionset)$/" }, { "command": "vlocode.omniScript.generateLwc", diff --git a/packages/vscode-extension/src/lib/webview/profileEditorWebview.ts b/packages/vscode-extension/src/lib/webview/profileEditorWebview.ts index f69bd0c15..31e912388 100644 --- a/packages/vscode-extension/src/lib/webview/profileEditorWebview.ts +++ b/packages/vscode-extension/src/lib/webview/profileEditorWebview.ts @@ -164,7 +164,7 @@ export class ProfileEditorWebview extends WebviewPanel f.type !== 'id') + .filter(f => f.type?.toLowerCase() !== 'id') .map(f => ({ name: f.name, label: f.label, type: f.type })); this.post({ type: 'fieldsLoaded', objectName, fields }); } catch (err) { diff --git a/packages/vscode-extension/src/webviews/profileEditor/App.tsx b/packages/vscode-extension/src/webviews/profileEditor/App.tsx index ab9cbf463..58791843b 100644 --- a/packages/vscode-extension/src/webviews/profileEditor/App.tsx +++ b/packages/vscode-extension/src/webviews/profileEditor/App.tsx @@ -32,6 +32,7 @@ type AppAction = | { type: 'saved' } | { type: 'reset'; data: ProfileEditorData } | { type: 'error'; message: string } + | { type: 'saving' } | { type: 'objectsLoaded'; objects: string[] } | { type: 'fieldsLoaded'; objectName: string; fields: SObjectField[] } | { type: 'changeObject'; permission: ObjectPermission } @@ -53,7 +54,7 @@ function reducer(state: AppState, action: AppAction): AppState { case 'loading': return { ...state, status: 'loading', statusMessage: action.message ?? 'Loading…' }; case 'saving': - return { ...state, status: 'saving', statusMessage: 'Saving…' } as AppState; + return { ...state, status: 'saving', statusMessage: 'Saving…' }; case 'saved': return { ...state, objectChanges: new Map(), fieldChanges: new Map(), status: 'idle', statusMessage: '' }; case 'reset': @@ -198,7 +199,7 @@ export const App: React.FC = ({ postMessage }) => { ); const handleSave = useCallback(() => { - dispatch({ type: 'loading', message: 'Saving…' } as AppAction); + dispatch({ type: 'saving' }); postMessage({ type: 'save', changes: { From 46d7d23b9ee3ce34b6deb90d598f6273de4c2cbe Mon Sep 17 00:00:00 2001 From: Peter van Gulik Date: Tue, 17 Mar 2026 22:35:10 +0100 Subject: [PATCH 04/10] Refactor code structure for improved readability and maintainability --- build/plugins/react-css-inject.mts | 49 ++ package.json | 3 +- packages/vscode-extension/build/webview.mts | 69 --- packages/vscode-extension/package.json | 6 +- .../commands/profiles/profileEditorCommand.ts | 2 +- .../src/lib/webview/profileEditorWebview.ts | 6 +- .../src/lib/webview/webviewPanel.ts | 12 +- .../src/webviews/declarations.d.ts | 1 + .../src/webviews/profileEditor/index.tsx | 2 +- .../src/webviews/profileEditor/styles.css | 555 ----------------- .../src/webviews/profileEditor/styles.scss | 560 ++++++++++++++++++ .../src/webviews/tsconfig.json | 11 + packages/vscode-extension/tsdown.config.mts | 23 +- pnpm-lock.yaml | 542 ++++++++++++++--- 14 files changed, 1122 insertions(+), 719 deletions(-) create mode 100644 build/plugins/react-css-inject.mts delete mode 100644 packages/vscode-extension/build/webview.mts create mode 100644 packages/vscode-extension/src/webviews/declarations.d.ts delete mode 100644 packages/vscode-extension/src/webviews/profileEditor/styles.css create mode 100644 packages/vscode-extension/src/webviews/profileEditor/styles.scss create mode 100644 packages/vscode-extension/src/webviews/tsconfig.json diff --git a/build/plugins/react-css-inject.mts b/build/plugins/react-css-inject.mts new file mode 100644 index 000000000..7402a0195 --- /dev/null +++ b/build/plugins/react-css-inject.mts @@ -0,0 +1,49 @@ +import type { Plugin } from 'rolldown'; + +const INLINE_STYLE_RE = /(\/\/#region [^\n]+\.(?:css|less|sass|scss|styl|stylus)\?inline\nvar\s+([A-Za-z_$][\w$]*)\s*=\s*[\s\S]*?;\n)(\n\/\/#endregion)/g; + +function injectAfterImports(code: string, helper: string): string { + const lines = code.split('\n'); + let insertionIndex = 0; + + while (insertionIndex < lines.length && lines[insertionIndex].trimStart().startsWith('import ')) { + insertionIndex += 1; + } + + lines.splice(insertionIndex, 0, helper); + return lines.join('\n'); +} + +export default function reactCssInjectPlugin(): Plugin { + return { + name: 'vlocode:webview-css-inject', + renderChunk(code) { + const styleVariables: string[] = []; + const rewritten = code.replace(INLINE_STYLE_RE, (_match, block: string, variableName: string, suffix: string) => { + styleVariables.push(variableName); + return `${block}\n__vlocodeInjectStyle(${variableName});${suffix}`; + }); + + if (styleVariables.length === 0) { + return null; + } + + const helperBlock = [ + 'const __vlocodeInjectedStyles = new Set();', + 'function __vlocodeInjectStyle(css) {', + ' if (typeof document === "undefined" || !css || __vlocodeInjectedStyles.has(css)) {', + ' return;', + ' }', + ' const style = document.createElement("style");', + ' style.textContent = css;', + ' style.setAttribute("data-vlocode-webview-style", "");', + ' document.head.appendChild(style);', + ' __vlocodeInjectedStyles.add(css);', + '}', + '' + ].join('\n'); + + return injectAfterImports(rewritten, helperBlock); + } + }; +} \ No newline at end of file diff --git a/package.json b/package.json index 3b0ed78a8..08e967062 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "prettier": "^3.3.3", "rolldown": "1.0.0-rc.3", "ts-jest": "^29.3.4", - "tsdown": "0.20.3", + "tsdown": "0.21.4", + "@tsdown/css": "0.21.4", "typedoc": "^0.28.12", "typedoc-plugin-rename-defaults": "^0.7.0", "typescript": "catalog:", diff --git a/packages/vscode-extension/build/webview.mts b/packages/vscode-extension/build/webview.mts deleted file mode 100644 index 8c6afc5bf..000000000 --- a/packages/vscode-extension/build/webview.mts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Build script for React webview bundles. - * Runs independently of tsdown to produce browser-targeted bundles. - */ -import { build, type BuildOptions } from 'esbuild'; -import { join } from 'path'; - -const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--watch'); -const watch = process.argv.includes('--watch'); - -const pkgDir = new URL('..', import.meta.url).pathname; - -const cssInjectPlugin = { - name: 'css-inject', - setup(pluginBuild: any) { - // Inject CSS into a