From d86b0fdaa0527329a131238a1b890021367a0dbe Mon Sep 17 00:00:00 2001 From: 1ncounter <1ncounter.100@gmail.com> Date: Wed, 24 Jul 2024 11:14:39 +0800 Subject: [PATCH] feat: add some common codes --- packages/engine-core/package.json | 3 - .../lifeCycleService.ts => command/action.ts} | 0 packages/engine-core/src/command/command.ts | 13 +- .../src/command/commandRegistry.ts | 25 +- .../engine-core/src/command/commandService.ts | 22 +- packages/engine-core/src/command/index.ts | 3 + .../src/{extension => common}/registry.ts | 6 + .../src/configuration/configuration.ts | 156 ++-- .../configuration/configurationChangeEvent.ts | 86 +++ .../src/configuration/configurationModel.ts | 93 +-- .../configuration/configurationRegistry.ts | 515 ++++++++++--- .../src/configuration/configurationService.ts | 184 ++++- .../src/configuration/configurations.ts | 592 +++++++++++++++ .../engine-core/src/configuration/index.ts | 3 +- .../engine-core/src/extension/extension.ts | 37 + .../src/extension/extensionHost.ts | 61 ++ .../src/extension/extensionManagement.ts | 92 +++ .../src/extension/extensionService.ts | 37 + packages/engine-core/src/extension/index.ts | 1 - packages/engine-core/src/index.ts | 8 +- .../engine-core/src/keybinding/keybinding.ts | 39 + .../src/keybinding/keybindingRegistry.ts | 26 + .../lifeCycleService.ts} | 0 packages/engine-core/src/main.ts | 26 + packages/engine-core/src/plugin/context.ts | 74 -- packages/engine-core/src/plugin/index.ts | 2 - packages/engine-core/src/plugin/manager.ts | 240 ------ packages/engine-core/src/plugin/runtime.ts | 82 -- packages/engine-core/src/plugin/types.ts | 75 -- packages/engine-core/src/plugin/utils.ts | 72 -- packages/engine-core/src/resource.ts | 5 - packages/engine-core/src/resource/index.ts | 1 + .../engine-core/src/resource/resourceModel.ts | 61 ++ .../src/resource/resourceService.ts | 51 ++ packages/engine-core/src/workbench/index.ts | 1 + .../src/workbench/layout/layout.ts | 28 + .../src/workbench/layout/layoutService.ts | 9 +- .../src/workbench/parts/action/index.ts} | 0 .../src/workbench/parts/sideBar/sideBar.ts} | 0 .../src/workbench/parts/topBar/index.ts} | 0 .../src/workbench/widget/widget.ts | 25 + .../src/workbench/widget/widgetModel.ts | 0 .../src/workbench/widget/widgetRegistry.ts | 37 + .../src/workbench/workbenchService.ts | 14 + .../src/workspace/window/windowService.ts | 0 .../engine-core/src/workspace/workspace.ts | 16 +- .../src/workspace/workspaceService.ts | 12 + packages/plugin-command/README.md | 11 - .../__tests__/node-command.test.ts | 110 --- packages/plugin-command/package.json | 51 -- .../plugin-command/src/history-command.ts | 43 -- packages/plugin-command/src/index.ts | 25 - packages/plugin-command/src/node-command.ts | 497 ------------ .../plugin-command/tsconfig.declaration.json | 10 - packages/plugin-command/tsconfig.json | 7 - packages/plugin-command/vite.config.ts | 9 - packages/plugin-designer/.gitignore | 106 --- packages/plugin-designer/build.json | 5 - packages/plugin-designer/package.json | 63 -- packages/plugin-designer/src/index.scss | 4 - packages/plugin-designer/src/index.tsx | 163 ---- .../plugin-designer/tsconfig.declaration.json | 10 - packages/plugin-designer/tsconfig.json | 7 - packages/plugin-designer/vite.config.ts | 10 - packages/plugin-outline-pane/package.json | 62 -- .../src/controllers/pane-controller.ts | 707 ------------------ .../src/controllers/tree-master.ts | 184 ----- .../src/controllers/tree-node.ts | 366 --------- .../src/controllers/tree.ts | 130 ---- .../plugin-outline-pane/src/helper/consts.ts | 2 - .../src/helper/dwell-timer.ts | 57 -- .../src/helper/indent-track.ts | 59 -- .../src/icons/arrow-right.tsx | 11 - .../plugin-outline-pane/src/icons/cond.tsx | 11 - .../plugin-outline-pane/src/icons/delete.tsx | 11 - .../src/icons/eye-close.tsx | 12 - .../plugin-outline-pane/src/icons/eye.tsx | 12 - .../plugin-outline-pane/src/icons/filter.tsx | 11 - .../plugin-outline-pane/src/icons/index.ts | 12 - .../plugin-outline-pane/src/icons/lock.tsx | 11 - .../plugin-outline-pane/src/icons/loop.tsx | 11 - .../plugin-outline-pane/src/icons/outline.tsx | 12 - .../src/icons/radio-active.tsx | 12 - .../plugin-outline-pane/src/icons/radio.tsx | 12 - .../plugin-outline-pane/src/icons/setting.tsx | 12 - .../plugin-outline-pane/src/icons/unlock.tsx | 12 - packages/plugin-outline-pane/src/index.tsx | 168 ----- .../plugin-outline-pane/src/locale/en-US.json | 23 - .../plugin-outline-pane/src/locale/index.ts | 4 - .../plugin-outline-pane/src/locale/zh-CN.json | 23 - .../plugin-outline-pane/src/ric-shim.d.ts | 1 - .../src/views/filter-tree.ts | 93 --- .../plugin-outline-pane/src/views/filter.tsx | 101 --- .../plugin-outline-pane/src/views/pane.tsx | 77 -- .../plugin-outline-pane/src/views/style.less | 430 ----------- .../src/views/tree-branches.tsx | 213 ------ .../src/views/tree-node.tsx | 264 ------- .../src/views/tree-title.tsx | 387 ---------- .../plugin-outline-pane/src/views/tree.tsx | 220 ------ .../tsconfig.declaration.json | 10 - packages/plugin-outline-pane/tsconfig.json | 7 - packages/plugin-outline-pane/vite.config.ts | 10 - .../src/runtime/createComponent.tsx | 8 +- .../react-renderer/src/runtime/elements.tsx | 10 +- packages/renderer-core/package.json | 3 - packages/renderer-core/src/main.ts | 5 +- .../src/services/code-runtime/codeScope.ts | 33 +- .../src/services/extension/boosts.ts | 5 +- .../extension/extensionHostService.ts | 3 +- .../src/services/package/managementService.ts | 74 +- .../src/services/runtimeIntlService.ts | 10 +- .../src/services/runtimeUtilService.ts | 10 +- .../src/services/schema/schemaService.ts | 3 +- .../src/utils/store.ts} | 5 +- .../instantiation/instantiationService.ts | 46 -- .../shared/src/{abilities => common}/event.ts | 0 .../shared/src/{abilities => common}/index.ts | 4 +- .../instantiation/decorators.ts | 0 .../instantiation/index.ts | 1 - .../instantiation/instantiationService.ts | 67 ++ .../shared/src/{abilities => common}/intl.ts | 4 +- packages/shared/src/common/linkedList.ts | 133 ++++ .../src/{abilities => common}/logger.ts | 7 +- .../shared/src/{utils => common}/platform.ts | 26 +- packages/shared/src/{ => common}/signals.ts | 6 +- packages/shared/src/index.ts | 3 +- packages/shared/src/types/json.ts | 83 +- packages/shared/src/types/specs/asset-spec.ts | 2 +- .../shared/src/types/specs/lowcode-spec.ts | 4 +- .../shared/src/types/specs/material-spec.ts | 12 +- packages/shared/src/utils/index.ts | 5 +- packages/shared/src/utils/iterable.ts | 7 + packages/shared/src/utils/resource.ts | 24 + packages/shared/src/utils/types/constraint.ts | 39 + packages/shared/src/utils/types/index.ts | 3 + .../utils/{is-promise.ts => types/type.ts} | 10 + 136 files changed, 2434 insertions(+), 5945 deletions(-) rename packages/engine-core/src/{lifeCycle/lifeCycleService.ts => command/action.ts} (100%) create mode 100644 packages/engine-core/src/command/index.ts rename packages/engine-core/src/{extension => common}/registry.ts (85%) create mode 100644 packages/engine-core/src/configuration/configurationChangeEvent.ts create mode 100644 packages/engine-core/src/configuration/configurations.ts create mode 100644 packages/engine-core/src/extension/extension.ts create mode 100644 packages/engine-core/src/extension/extensionHost.ts create mode 100644 packages/engine-core/src/extension/extensionManagement.ts create mode 100644 packages/engine-core/src/extension/extensionService.ts delete mode 100644 packages/engine-core/src/extension/index.ts create mode 100644 packages/engine-core/src/keybinding/keybinding.ts create mode 100644 packages/engine-core/src/keybinding/keybindingRegistry.ts rename packages/engine-core/src/{workbench/window/windowService.ts => lifecycle/lifeCycleService.ts} (100%) create mode 100644 packages/engine-core/src/main.ts delete mode 100644 packages/engine-core/src/plugin/context.ts delete mode 100644 packages/engine-core/src/plugin/index.ts delete mode 100644 packages/engine-core/src/plugin/manager.ts delete mode 100644 packages/engine-core/src/plugin/runtime.ts delete mode 100644 packages/engine-core/src/plugin/types.ts delete mode 100644 packages/engine-core/src/plugin/utils.ts delete mode 100644 packages/engine-core/src/resource.ts create mode 100644 packages/engine-core/src/resource/index.ts create mode 100644 packages/engine-core/src/resource/resourceModel.ts create mode 100644 packages/engine-core/src/resource/resourceService.ts create mode 100644 packages/engine-core/src/workbench/index.ts create mode 100644 packages/engine-core/src/workbench/layout/layout.ts rename packages/{plugin-command/vitest.config.ts => engine-core/src/workbench/parts/action/index.ts} (100%) rename packages/{plugin-designer/vitest.config.ts => engine-core/src/workbench/parts/sideBar/sideBar.ts} (100%) rename packages/{plugin-outline-pane/vitest.config.ts => engine-core/src/workbench/parts/topBar/index.ts} (100%) create mode 100644 packages/engine-core/src/workbench/widget/widget.ts create mode 100644 packages/engine-core/src/workbench/widget/widgetModel.ts create mode 100644 packages/engine-core/src/workbench/widget/widgetRegistry.ts create mode 100644 packages/engine-core/src/workbench/workbenchService.ts create mode 100644 packages/engine-core/src/workspace/window/windowService.ts create mode 100644 packages/engine-core/src/workspace/workspaceService.ts delete mode 100644 packages/plugin-command/README.md delete mode 100644 packages/plugin-command/__tests__/node-command.test.ts delete mode 100644 packages/plugin-command/package.json delete mode 100644 packages/plugin-command/src/history-command.ts delete mode 100644 packages/plugin-command/src/index.ts delete mode 100644 packages/plugin-command/src/node-command.ts delete mode 100644 packages/plugin-command/tsconfig.declaration.json delete mode 100644 packages/plugin-command/tsconfig.json delete mode 100644 packages/plugin-command/vite.config.ts delete mode 100644 packages/plugin-designer/.gitignore delete mode 100644 packages/plugin-designer/build.json delete mode 100644 packages/plugin-designer/package.json delete mode 100644 packages/plugin-designer/src/index.scss delete mode 100644 packages/plugin-designer/src/index.tsx delete mode 100644 packages/plugin-designer/tsconfig.declaration.json delete mode 100644 packages/plugin-designer/tsconfig.json delete mode 100644 packages/plugin-designer/vite.config.ts delete mode 100644 packages/plugin-outline-pane/package.json delete mode 100644 packages/plugin-outline-pane/src/controllers/pane-controller.ts delete mode 100644 packages/plugin-outline-pane/src/controllers/tree-master.ts delete mode 100644 packages/plugin-outline-pane/src/controllers/tree-node.ts delete mode 100644 packages/plugin-outline-pane/src/controllers/tree.ts delete mode 100644 packages/plugin-outline-pane/src/helper/consts.ts delete mode 100644 packages/plugin-outline-pane/src/helper/dwell-timer.ts delete mode 100644 packages/plugin-outline-pane/src/helper/indent-track.ts delete mode 100644 packages/plugin-outline-pane/src/icons/arrow-right.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/cond.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/delete.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/eye-close.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/eye.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/filter.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/index.ts delete mode 100644 packages/plugin-outline-pane/src/icons/lock.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/loop.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/outline.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/radio-active.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/radio.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/setting.tsx delete mode 100644 packages/plugin-outline-pane/src/icons/unlock.tsx delete mode 100644 packages/plugin-outline-pane/src/index.tsx delete mode 100644 packages/plugin-outline-pane/src/locale/en-US.json delete mode 100644 packages/plugin-outline-pane/src/locale/index.ts delete mode 100644 packages/plugin-outline-pane/src/locale/zh-CN.json delete mode 100644 packages/plugin-outline-pane/src/ric-shim.d.ts delete mode 100644 packages/plugin-outline-pane/src/views/filter-tree.ts delete mode 100644 packages/plugin-outline-pane/src/views/filter.tsx delete mode 100644 packages/plugin-outline-pane/src/views/pane.tsx delete mode 100644 packages/plugin-outline-pane/src/views/style.less delete mode 100644 packages/plugin-outline-pane/src/views/tree-branches.tsx delete mode 100644 packages/plugin-outline-pane/src/views/tree-node.tsx delete mode 100644 packages/plugin-outline-pane/src/views/tree-title.tsx delete mode 100644 packages/plugin-outline-pane/src/views/tree.tsx delete mode 100644 packages/plugin-outline-pane/tsconfig.declaration.json delete mode 100644 packages/plugin-outline-pane/tsconfig.json delete mode 100644 packages/plugin-outline-pane/vite.config.ts rename packages/{shared/src/abilities/storage.ts => renderer-core/src/utils/store.ts} (95%) delete mode 100644 packages/shared/src/abilities/instantiation/instantiationService.ts rename packages/shared/src/{abilities => common}/event.ts (100%) rename packages/shared/src/{abilities => common}/index.ts (52%) rename packages/shared/src/{abilities => common}/instantiation/decorators.ts (100%) rename packages/shared/src/{abilities => common}/instantiation/index.ts (70%) create mode 100644 packages/shared/src/common/instantiation/instantiationService.ts rename packages/shared/src/{abilities => common}/intl.ts (97%) create mode 100644 packages/shared/src/common/linkedList.ts rename packages/shared/src/{abilities => common}/logger.ts (95%) rename packages/shared/src/{utils => common}/platform.ts (70%) rename packages/shared/src/{ => common}/signals.ts (98%) create mode 100644 packages/shared/src/utils/iterable.ts create mode 100644 packages/shared/src/utils/resource.ts create mode 100644 packages/shared/src/utils/types/constraint.ts rename packages/shared/src/utils/{is-promise.ts => types/type.ts} (55%) diff --git a/packages/engine-core/package.json b/packages/engine-core/package.json index 498cbbc6c..6a38b1f2d 100644 --- a/packages/engine-core/package.json +++ b/packages/engine-core/package.json @@ -35,9 +35,6 @@ "devDependencies": { "@types/lodash-es": "^4.17.12" }, - "peerDependencies": { - "@alilc/lowcode-shared": "workspace:*" - }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" diff --git a/packages/engine-core/src/lifeCycle/lifeCycleService.ts b/packages/engine-core/src/command/action.ts similarity index 100% rename from packages/engine-core/src/lifeCycle/lifeCycleService.ts rename to packages/engine-core/src/command/action.ts diff --git a/packages/engine-core/src/command/command.ts b/packages/engine-core/src/command/command.ts index be4e9a13c..4214f8a7d 100644 --- a/packages/engine-core/src/command/command.ts +++ b/packages/engine-core/src/command/command.ts @@ -1,4 +1,4 @@ -import { type InstanceAccessor } from '@alilc/lowcode-shared'; +import { type InstanceAccessor, type TypeConstraint } from '@alilc/lowcode-shared'; export interface ICommandEvent { commandId: string; @@ -16,19 +16,12 @@ export interface ICommand { } export interface ICommandMetadata { - /** - * A short summary of what the command does. This will be used in: - * - API commands - * - when showing keybindings that have no other UX - * - when searching for commands in the Command Palette - */ readonly description: string; readonly args?: ReadonlyArray<{ readonly name: string; readonly isOptional?: boolean; readonly description?: string; - // readonly constraint?: TypeConstraint; - // readonly schema?: IJSONSchema; + readonly constraint?: TypeConstraint; + readonly default?: any; }>; - readonly returns?: string; } diff --git a/packages/engine-core/src/command/commandRegistry.ts b/packages/engine-core/src/command/commandRegistry.ts index d9223d1e9..bc5f7296f 100644 --- a/packages/engine-core/src/command/commandRegistry.ts +++ b/packages/engine-core/src/command/commandRegistry.ts @@ -3,9 +3,14 @@ import { type EventDisposable, type EventListener, Emitter, + LinkedList, + TypeConstraint, + validateConstraints, + Iterable, } from '@alilc/lowcode-shared'; import { ICommand, ICommandHandler } from './command'; -import { Registry } from '../extension'; +import { Extensions, Registry } from '../common/registry'; +import { ICommandService } from './commandService'; export type ICommandsMap = Map; @@ -24,10 +29,10 @@ export interface ICommandRegistry { class CommandsRegistry implements ICommandRegistry { private readonly _commands = new Map>(); - private readonly _onDidRegisterCommand = new Emitter(); + private readonly _didRegisterCommandEmitter = new Emitter(); onDidRegisterCommand(fn: EventListener) { - return this._onDidRegisterCommand.on(fn); + return this._didRegisterCommandEmitter.on(fn); } registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): EventDisposable { @@ -66,21 +71,21 @@ class CommandsRegistry implements ICommandRegistry { const removeFn = commands.unshift(idOrCommand); - const ret = toDisposable(() => { + const ret = () => { removeFn(); const command = this._commands.get(id); if (command?.isEmpty()) { this._commands.delete(id); } - }); + }; // tell the world about this command - this._onDidRegisterCommand.emit(id); + this._didRegisterCommandEmitter.emit(id); return ret; } - registerCommandAlias(oldId: string, newId: string): IDisposable { + registerCommandAlias(oldId: string, newId: string): EventDisposable { return this.registerCommand(oldId, (accessor, ...args) => accessor.get(ICommandService).executeCommand(newId, ...args), ); @@ -108,8 +113,4 @@ class CommandsRegistry implements ICommandRegistry { const commandsRegistry = new CommandsRegistry(); -export const Extension = { - command: 'base.contributions.command', -}; - -Registry.add(Extension.command, commandsRegistry); +Registry.add(Extensions.Command, commandsRegistry); diff --git a/packages/engine-core/src/command/commandService.ts b/packages/engine-core/src/command/commandService.ts index 0be16247f..f17ab8c43 100644 --- a/packages/engine-core/src/command/commandService.ts +++ b/packages/engine-core/src/command/commandService.ts @@ -1,6 +1,6 @@ -import { createDecorator, Provide } from '@alilc/lowcode-shared'; -import { Registry } from '../extension'; -import { ICommandRegistry, Extension } from './commandRegistry'; +import { createDecorator, Provide, IInstantiationService } from '@alilc/lowcode-shared'; +import { Registry, Extensions } from '../common/registry'; +import { ICommandRegistry } from './commandRegistry'; export interface ICommandService { executeCommand(commandId: string, ...args: any[]): Promise; @@ -10,11 +10,23 @@ export const ICommandService = createDecorator('commandService' @Provide(ICommandService) export class CommandService implements ICommandService { - executeCommand(id: string, ...args: any[]): Promise { - const command = Registry.as(Extension.command).getCommand(id); + constructor(@IInstantiationService private instantiationService: IInstantiationService) {} + executeCommand(id: string, ...args: any[]): Promise { + return this.tryExecuteCommand(id, args); + } + + private tryExecuteCommand(id: string, args: any[]): Promise { + const command = Registry.as(Extensions.Command).getCommand(id); if (!command) { return Promise.reject(new Error(`command '${id}' not found`)); } + + try { + const result = this.instantiationService.invokeFunction(command.handler, ...args); + return Promise.resolve(result); + } catch (err) { + return Promise.reject(err); + } } } diff --git a/packages/engine-core/src/command/index.ts b/packages/engine-core/src/command/index.ts new file mode 100644 index 000000000..a68c210c2 --- /dev/null +++ b/packages/engine-core/src/command/index.ts @@ -0,0 +1,3 @@ +export * from './command'; +export * from './commandRegistry'; +export * from './commandService'; diff --git a/packages/engine-core/src/extension/registry.ts b/packages/engine-core/src/common/registry.ts similarity index 85% rename from packages/engine-core/src/extension/registry.ts rename to packages/engine-core/src/common/registry.ts index 153cfdba8..df89ed433 100644 --- a/packages/engine-core/src/extension/registry.ts +++ b/packages/engine-core/src/common/registry.ts @@ -37,3 +37,9 @@ class RegistryImpl implements IRegistry { } export const Registry: IRegistry = new RegistryImpl(); + +export const Extensions = { + Configuration: 'base.contributions.configuration', + Command: 'base.contributions.command', + Widget: 'base.contributions.widget', +}; diff --git a/packages/engine-core/src/configuration/configuration.ts b/packages/engine-core/src/configuration/configuration.ts index d8271c089..b8fc8f7d9 100644 --- a/packages/engine-core/src/configuration/configuration.ts +++ b/packages/engine-core/src/configuration/configuration.ts @@ -1,81 +1,105 @@ -import { type StringDictionary, Emitter, type EventListener } from '@alilc/lowcode-shared'; -import { ConfigurationModel } from './configurationModel'; -import { - type IConfigurationRegistry, - type IRegisteredConfigurationPropertySchema, - Extension, -} from './configurationRegistry'; -import { Registry } from '../extension'; +import { type StringDictionary } from '@alilc/lowcode-shared'; +import { uniq } from 'lodash-es'; -export interface IConfigurationOverrides { - overrideIdentifier?: string | null; +export interface IInspectValue { + readonly value?: T; + readonly override?: T; + readonly overrides?: { readonly identifiers: string[]; readonly value: T }[]; } -export interface IConfigurationUpdateOverrides { - overrideIdentifiers?: string[] | null; +export function toValuesTree(properties: StringDictionary): any { + const root = Object.create(null); + + for (const key of Object.keys(properties)) { + addToValueTree(root, key, properties[key]); + } + + return root; } -export class DefaultConfiguration { - private emitter = new Emitter<{ - defaults: ConfigurationModel; - properties: string[]; - }>(); +export function addToValueTree( + settingsTreeRoot: any, + key: string, + value: any, + conflictReporter: (message: string) => void = console.error, +): void { + const segments = key.split('.'); + const last = segments.pop()!; - private _configurationModel = ConfigurationModel.createEmptyModel(); - - get configurationModel(): ConfigurationModel { - return this._configurationModel; + let curr = settingsTreeRoot; + for (let i = 0; i < segments.length; i++) { + const s = segments[i]; + let obj = curr[s]; + switch (typeof obj) { + case 'undefined': + obj = curr[s] = Object.create(null); + break; + case 'object': + if (obj === null) { + conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is null`); + return; + } + break; + default: + conflictReporter( + `Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`, + ); + return; + } + curr = obj; } - initialize(): ConfigurationModel { - this.resetConfigurationModel(); - Registry.as(Extension.Configuration).onDidUpdateConfiguration( - ({ properties }) => this.onDidUpdateConfiguration([...properties]), - ); + if (typeof curr === 'object' && curr !== null) { + try { + curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606 + } catch (e) { + conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`); + } + } else { + conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`); + } +} - return this.configurationModel; +export function removeFromValueTree(valueTree: any, key: string): void { + const segments = key.split('.'); + doRemoveFromValueTree(valueTree, segments); +} + +function doRemoveFromValueTree(valueTree: any, segments: string[]): void { + const first = segments.shift()!; + if (segments.length === 0) { + // Reached last segment + delete valueTree[first]; + return; } - reload(): ConfigurationModel { - this.resetConfigurationModel(); - return this.configurationModel; - } - - onDidChangeConfiguration( - listener: EventListener<[{ defaults: ConfigurationModel; properties: string[] }]>, - ) { - return this.emitter.on(listener); - } - - private onDidUpdateConfiguration(properties: string[]): void { - this.updateConfigurationModel( - properties, - Registry.as(Extension.Configuration).getConfigurationProperties(), - ); - this.emitter.emit({ defaults: this.configurationModel, properties }); - } - - private resetConfigurationModel(): void { - this._configurationModel = ConfigurationModel.createEmptyModel(); - - const properties = Registry.as( - Extension.Configuration, - ).getConfigurationProperties(); - - this.updateConfigurationModel(Object.keys(properties), properties); - } - - private updateConfigurationModel( - properties: string[], - configurationProperties: StringDictionary, - ): void { - for (const key of properties) { - const propertySchema = configurationProperties[key]; - if (propertySchema) { - this.configurationModel.setValue(key, propertySchema.default); - } else { - this.configurationModel.removeValue(key); + if (Object.keys(valueTree).includes(first)) { + const value = valueTree[first]; + if (typeof value === 'object' && !Array.isArray(value)) { + doRemoveFromValueTree(value, segments); + if (Object.keys(value).length === 0) { + delete valueTree[first]; } } } } + +const OVERRIDE_IDENTIFIER_PATTERN = `\\[([^\\]]+)\\]`; +const OVERRIDE_IDENTIFIER_REGEX = new RegExp(OVERRIDE_IDENTIFIER_PATTERN, 'g'); +export const OVERRIDE_PROPERTY_PATTERN = `^(${OVERRIDE_IDENTIFIER_PATTERN})+$`; +export const OVERRIDE_PROPERTY_REGEX = new RegExp(OVERRIDE_PROPERTY_PATTERN); + +export function overrideIdentifiersFromKey(key: string): string[] { + const identifiers: string[] = []; + if (OVERRIDE_PROPERTY_REGEX.test(key)) { + let matches = OVERRIDE_IDENTIFIER_REGEX.exec(key); + while (matches?.length) { + const identifier = matches[1].trim(); + if (identifier) { + identifiers.push(identifier); + } + matches = OVERRIDE_IDENTIFIER_REGEX.exec(key); + } + } + return uniq(identifiers); +} diff --git a/packages/engine-core/src/configuration/configurationChangeEvent.ts b/packages/engine-core/src/configuration/configurationChangeEvent.ts new file mode 100644 index 000000000..0636c5992 --- /dev/null +++ b/packages/engine-core/src/configuration/configurationChangeEvent.ts @@ -0,0 +1,86 @@ +import { isEqual } from 'lodash-es'; +import { + Configuration, + type IConfigurationData, + type IConfigurationOverrides, +} from './configurations'; + +export interface IConfigurationChange { + keys: string[]; + overrides: [string, string[]][]; +} + +export interface IConfigurationChangeEvent { + readonly affectedKeys: ReadonlySet; + readonly change: IConfigurationChange; + + affectsConfiguration(section: string, overrides?: IConfigurationOverrides): boolean; +} + +export class ConfigurationChangeEvent implements IConfigurationChangeEvent { + private readonly _marker = '\n'; + private readonly _markerCode1 = this._marker.charCodeAt(0); + private readonly _markerCode2 = '.'.charCodeAt(0); + private readonly _affectsConfigStr: string; + + readonly affectedKeys = new Set(); + + constructor( + readonly change: IConfigurationChange, + private readonly previous: { data: IConfigurationData } | undefined, + private readonly currentConfiguraiton: Configuration, + ) { + for (const key of change.keys) { + this.affectedKeys.add(key); + } + for (const [, keys] of change.overrides) { + for (const key of keys) { + this.affectedKeys.add(key); + } + } + + // Example: '\nfoo.bar\nabc.def\n' + this._affectsConfigStr = this._marker; + for (const key of this.affectedKeys) { + this._affectsConfigStr += key + this._marker; + } + } + + private _previousConfiguration: Configuration | undefined = undefined; + get previousConfiguration(): Configuration | undefined { + if (!this._previousConfiguration && this.previous) { + this._previousConfiguration = Configuration.parse(this.previous.data); + } + return this._previousConfiguration; + } + + affectsConfiguration(section: string, overrides?: IConfigurationOverrides): boolean { + // we have one large string with all keys that have changed. we pad (marker) the section + // and check that either find it padded or before a segment character + const needle = this._marker + section; + const idx = this._affectsConfigStr.indexOf(needle); + if (idx < 0) { + // NOT: (marker + section) + return false; + } + const pos = idx + needle.length; + if (pos >= this._affectsConfigStr.length) { + return false; + } + const code = this._affectsConfigStr.charCodeAt(pos); + if (code !== this._markerCode1 && code !== this._markerCode2) { + // NOT: section + (marker | segment) + return false; + } + + if (overrides) { + const value1 = this.previousConfiguration + ? this.previousConfiguration.getValue(section, overrides) + : undefined; + const value2 = this.currentConfiguraiton.getValue(section, overrides); + return !isEqual(value1, value2); + } + + return true; + } +} diff --git a/packages/engine-core/src/configuration/configurationModel.ts b/packages/engine-core/src/configuration/configurationModel.ts index 74821c9b5..e98be578e 100644 --- a/packages/engine-core/src/configuration/configurationModel.ts +++ b/packages/engine-core/src/configuration/configurationModel.ts @@ -1,13 +1,15 @@ import { type StringDictionary } from '@alilc/lowcode-shared'; import { get as lodasgGet, isEqual, uniq, cloneDeep, isObject } from 'lodash-es'; -import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configurationRegistry'; +import { + type IInspectValue, + addToValueTree, + removeFromValueTree, + toValuesTree, + OVERRIDE_PROPERTY_REGEX, + overrideIdentifiersFromKey, +} from './configuration'; -export type InspectValue = { - readonly value?: V; - readonly override?: V; - readonly overrides?: { readonly identifiers: string[]; readonly value: V }[]; - merged?: V; -}; +export type InspectValue = IInspectValue & { merged?: V }; export interface IConfigurationModel { contents: any; @@ -327,80 +329,3 @@ export class ConfigurationModel implements IConfigurationModel { return uniq(result); } } - -function removeFromValueTree(valueTree: any, key: string): void { - const segments = key.split('.'); - doRemoveFromValueTree(valueTree, segments); -} - -function doRemoveFromValueTree(valueTree: any, segments: string[]): void { - const first = segments.shift()!; - if (segments.length === 0) { - // Reached last segment - delete valueTree[first]; - return; - } - - if (Object.keys(valueTree).includes(first)) { - const value = valueTree[first]; - if (typeof value === 'object' && !Array.isArray(value)) { - doRemoveFromValueTree(value, segments); - if (Object.keys(value).length === 0) { - delete valueTree[first]; - } - } - } -} - -function addToValueTree( - settingsTreeRoot: any, - key: string, - value: any, - conflictReporter: (message: string) => void = console.error, -): void { - const segments = key.split('.'); - const last = segments.pop()!; - - let curr = settingsTreeRoot; - for (let i = 0; i < segments.length; i++) { - const s = segments[i]; - let obj = curr[s]; - switch (typeof obj) { - case 'undefined': - obj = curr[s] = Object.create(null); - break; - case 'object': - if (obj === null) { - conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is null`); - return; - } - break; - default: - conflictReporter( - `Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`, - ); - return; - } - curr = obj; - } - - if (typeof curr === 'object' && curr !== null) { - try { - curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606 - } catch (e) { - conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`); - } - } else { - conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`); - } -} - -function toValuesTree(properties: StringDictionary): any { - const root = Object.create(null); - - for (const key in properties) { - addToValueTree(root, key, properties[key]); - } - - return root; -} diff --git a/packages/engine-core/src/configuration/configurationRegistry.ts b/packages/engine-core/src/configuration/configurationRegistry.ts index 034ecc2d8..a6e983f68 100644 --- a/packages/engine-core/src/configuration/configurationRegistry.ts +++ b/packages/engine-core/src/configuration/configurationRegistry.ts @@ -2,31 +2,14 @@ import { type Event, Emitter, type StringDictionary, - type JSONValueType, + type JSONSchemaType, jsonTypes, + IJSONSchema, + types, } from '@alilc/lowcode-shared'; -import { uniq, isUndefined } from 'lodash-es'; -import { Registry } from '../extension/registry'; - -const OVERRIDE_IDENTIFIER_PATTERN = `\\[([^\\]]+)\\]`; -const OVERRIDE_IDENTIFIER_REGEX = new RegExp(OVERRIDE_IDENTIFIER_PATTERN, 'g'); -export const OVERRIDE_PROPERTY_PATTERN = `^(${OVERRIDE_IDENTIFIER_PATTERN})+$`; -export const OVERRIDE_PROPERTY_REGEX = new RegExp(OVERRIDE_PROPERTY_PATTERN); - -export function overrideIdentifiersFromKey(key: string): string[] { - const identifiers: string[] = []; - if (OVERRIDE_PROPERTY_REGEX.test(key)) { - let matches = OVERRIDE_IDENTIFIER_REGEX.exec(key); - while (matches?.length) { - const identifier = matches[1].trim(); - if (identifier) { - identifiers.push(identifier); - } - matches = OVERRIDE_IDENTIFIER_REGEX.exec(key); - } - } - return uniq(identifiers); -} +import { isUndefined, isObject } from 'lodash-es'; +import { Extensions, Registry } from '../common/registry'; +import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration'; export interface IConfigurationRegistry { /** @@ -37,7 +20,10 @@ export interface IConfigurationRegistry { /** * Register multiple configurations to the registry. */ - registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): void; + registerConfigurations( + configurations: IConfigurationNode[], + validate?: boolean, + ): ReadonlySet; /** * Deregister multiple configurations from the registry. @@ -45,15 +31,15 @@ export interface IConfigurationRegistry { deregisterConfigurations(configurations: IConfigurationNode[]): void; /** - * Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values. - * Property or default value changes are not allowed. + * Register multiple default configurations to the registry. */ - notifyConfigurationSchemaUpdated(): void; + registerDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void; + /** - * Event that fires whenever a configuration has been - * registered. + * Deregister multiple default configurations from the registry. */ - readonly onDidSchemaChange: Event; + deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void; + /** * Event that fires whenever a configuration has been * registered. @@ -63,10 +49,6 @@ export interface IConfigurationRegistry { defaultsOverrides?: boolean; }>; - /** - * Returns all configuration nodes contributed to this registry. - */ - getConfigurations(): IConfigurationNode[]; /** * Returns all configurations settings of all configuration nodes contributed to this registry. */ @@ -75,12 +57,22 @@ export interface IConfigurationRegistry { * Returns all excluded configurations settings of all configuration nodes contributed to this registry. */ getExcludedConfigurationProperties(): StringDictionary; + + /** + * Return the registered default configurations + */ + getRegisteredDefaultConfigurations(): IConfigurationDefaults[]; + + /** + * Return the registered configuration defaults overrides + */ + getConfigurationDefaultsOverrides(): Map; } export interface IConfigurationNode { id?: string; order?: number; - type?: JSONValueType | JSONValueType[]; + type?: JSONSchemaType | JSONSchemaType[]; title?: string; description?: string; properties?: StringDictionary; @@ -88,45 +80,78 @@ export interface IConfigurationNode { extensionInfo?: IExtensionInfo; } -export interface IConfigurationPropertySchema { - type?: JSONValueType; - default?: any; - tags?: string[]; +export interface IConfigurationPropertySchema extends IJSONSchema { + /** + * 当该属性为“false”时,将从注册表中排除该属性。默认为包含。 + */ included?: boolean; - deprecated?: boolean; - deprecationMessage?: string; + /** + * 不允许扩展为此设置贡献配置默认值。 + */ + disallowConfigurationDefault?: boolean; + /** + * 与属性关联的标签列表。 + * - 标签可用于过滤 + */ + tags?: string[]; } +/** + * 扩展信息,用来查找对应属性的源扩展 + */ export interface IExtensionInfo { - id: string; - displayName?: string; + name: string; } export type ConfigurationDefaultValueSource = IExtensionInfo | Map; +export interface IRegisteredConfigurationPropertySchema extends IConfigurationPropertySchema { + defaultDefaultValue?: any; + source?: IExtensionInfo; // Source of the Property + defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value +} + export interface IConfigurationDefaults { overrides: StringDictionary; source?: IExtensionInfo; } -export interface IRegisteredConfigurationPropertySchema extends IConfigurationPropertySchema { - source?: IExtensionInfo; // Source of the Property - defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value +export interface IConfigurationDefaultOverride { + readonly value: any; + readonly source?: IExtensionInfo; // Source of the default override } +export interface IConfigurationDefaultOverrideValue { + readonly value: any; + readonly source?: IExtensionInfo | Map; +} + +export const allSettings: { + properties: StringDictionary; + patternProperties: StringDictionary; +} = { properties: {}, patternProperties: {} }; + export class ConfigurationRegistry implements IConfigurationRegistry { - private configurationContributors: IConfigurationNode[]; + private registeredConfigurationDefaults: IConfigurationDefaults[] = []; + private configurationDefaultsOverrides: Map< + string, + { + configurationDefaultOverrides: IConfigurationDefaultOverride[]; + configurationDefaultOverrideValue?: IConfigurationDefaultOverrideValue; + } + >; + private configurationProperties: StringDictionary; private excludedConfigurationProperties: StringDictionary; + private overrideIdentifiers = new Set(); - private schemaChangeEmitter = new Emitter(); - private updateConfigurationEmitter = new Emitter<{ + private propertiesChangeEmitter = new Emitter<{ properties: ReadonlySet; defaultsOverrides?: boolean; }>(); constructor() { - this.configurationContributors = []; + this.configurationDefaultsOverrides = new Map(); this.configurationProperties = {}; this.excludedConfigurationProperties = {}; } @@ -135,12 +160,16 @@ export class ConfigurationRegistry implements IConfigurationRegistry { this.registerConfigurations([configuration], validate); } - registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): void { + registerConfigurations( + configurations: IConfigurationNode[], + validate: boolean = true, + ): ReadonlySet { const properties = new Set(); - this.doRegisterConfigurations(configurations, validate, properties); - this.schemaChangeEmitter.emit(); - this.updateConfigurationEmitter.emit({ properties }); + this.doRegisterConfigurations(configurations, validate, properties); + this.propertiesChangeEmitter.emit({ properties }); + + return properties; } private doRegisterConfigurations( @@ -156,7 +185,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry { bucket, ); - this.configurationContributors.push(configuration); + this.registerJSONConfiguration(configuration); }); } @@ -168,7 +197,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry { ): void { const properties = configuration.properties; if (properties) { - for (const key in properties) { + for (const key of Object.keys(properties)) { const property: IRegisteredConfigurationPropertySchema = properties[key]; if (validate && this.validateProperty(key)) { @@ -178,14 +207,12 @@ export class ConfigurationRegistry implements IConfigurationRegistry { property.source = extensionInfo; // update default value - this.updatePropertyDefaultValue(property); + property.defaultDefaultValue = properties[key].default; + this.updatePropertyDefaultValue(key, property); // Add to properties maps // Property is included by default if 'included' is unspecified - if ( - Object.prototype.hasOwnProperty.call(properties[key], 'included') && - !properties[key].included - ) { + if (properties[key].included === false) { this.excludedConfigurationProperties[key] = properties[key]; continue; } @@ -216,12 +243,26 @@ export class ConfigurationRegistry implements IConfigurationRegistry { return null; } - private updatePropertyDefaultValue(property: IRegisteredConfigurationPropertySchema): void { + private updatePropertyDefaultValue( + key: string, + property: IRegisteredConfigurationPropertySchema, + ): void { let defaultValue = undefined; let defaultSource = undefined; + const configurationdefaultOverride = + this.configurationDefaultsOverrides.get(key)?.configurationDefaultOverrideValue; + + // Prevent overriding the default value if the property is disallowed to be overridden by configuration defaults from extensions + if ( + configurationdefaultOverride && + (!property.disallowConfigurationDefault || !configurationdefaultOverride.source) + ) { + defaultValue = configurationdefaultOverride.value; + defaultSource = configurationdefaultOverride.source; + } if (isUndefined(defaultValue)) { - defaultValue = property.default; + defaultValue = property.defaultDefaultValue; defaultSource = undefined; } if (isUndefined(defaultValue)) { @@ -235,9 +276,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry { deregisterConfigurations(configurations: IConfigurationNode[]): void { const properties = new Set(); this.doDeregisterConfigurations(configurations, properties); - - this.schemaChangeEmitter.emit(); - this.updateConfigurationEmitter.emit({ properties }); + this.propertiesChangeEmitter.emit({ properties }); } private doDeregisterConfigurations( @@ -246,9 +285,10 @@ export class ConfigurationRegistry implements IConfigurationRegistry { ): void { const deregisterConfiguration = (configuration: IConfigurationNode) => { if (configuration.properties) { - for (const key in configuration.properties) { + for (const key of Object.keys(configuration.properties)) { bucket.add(key); delete this.configurationProperties[key]; + this.removeFromSchema(key); } } configuration.allOf?.forEach((node) => deregisterConfiguration(node)); @@ -256,46 +296,351 @@ export class ConfigurationRegistry implements IConfigurationRegistry { for (const configuration of configurations) { deregisterConfiguration(configuration); - - const index = this.configurationContributors.indexOf(configuration); - if (index !== -1) { - this.configurationContributors.splice(index, 1); - } } } - notifyConfigurationSchemaUpdated(): void { - this.schemaChangeEmitter.emit(); + registerDefaultConfigurations(configurationDefaults: IConfigurationDefaults[]): void { + const properties = new Set(); + + this.doRegisterDefaultConfigurations(configurationDefaults, properties); + this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true }); + } + + private doRegisterDefaultConfigurations( + configurationDefaults: IConfigurationDefaults[], + bucket: Set, + ) { + this.registeredConfigurationDefaults.push(...configurationDefaults); + + const overrideIdentifiers: string[] = []; + + for (const { overrides, source } of configurationDefaults) { + for (const key in overrides) { + bucket.add(key); + + const configurationDefaultOverridesForKey = + this.configurationDefaultsOverrides.get(key) ?? + this.configurationDefaultsOverrides + .set(key, { configurationDefaultOverrides: [] }) + .get(key)!; + + const value = overrides[key]; + configurationDefaultOverridesForKey.configurationDefaultOverrides.push({ value, source }); + + // Configuration defaults for Override Identifiers + if (OVERRIDE_PROPERTY_REGEX.test(key)) { + const newDefaultOverride = this.mergeDefaultConfigurationsForOverrideIdentifier( + key, + value, + source, + configurationDefaultOverridesForKey.configurationDefaultOverrideValue, + ); + if (!newDefaultOverride) { + continue; + } + + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = + newDefaultOverride; + this.updateDefaultOverrideProperty(key, newDefaultOverride, source); + overrideIdentifiers.push(...overrideIdentifiersFromKey(key)); + } + + // Configuration defaults for Configuration Properties + else { + const newDefaultOverride = this.mergeDefaultConfigurationsForConfigurationProperty( + key, + value, + source, + configurationDefaultOverridesForKey.configurationDefaultOverrideValue, + ); + if (!newDefaultOverride) { + continue; + } + + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = + newDefaultOverride; + const property = this.configurationProperties[key]; + if (property) { + this.updatePropertyDefaultValue(key, property); + this.updateSchema(key, property); + } + } + } + } + + this.doRegisterOverrideIdentifiers(overrideIdentifiers); + } + + private updateDefaultOverrideProperty( + key: string, + newDefaultOverride: IConfigurationDefaultOverrideValue, + source: IExtensionInfo | undefined, + ): void { + const property: IRegisteredConfigurationPropertySchema = { + type: 'object', + default: newDefaultOverride.value, + description: `Configure settings to be overridden for the {0} language.`, + defaultDefaultValue: newDefaultOverride.value, + source, + defaultValueSource: source, + }; + this.configurationProperties[key] = property; + } + + private mergeDefaultConfigurationsForOverrideIdentifier( + overrideIdentifier: string, + configurationValueObject: StringDictionary, + valueSource: IExtensionInfo | undefined, + existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined, + ): IConfigurationDefaultOverrideValue | undefined { + const defaultValue = existingDefaultOverride?.value || {}; + const source = existingDefaultOverride?.source ?? new Map(); + + if (!(source instanceof Map)) return; + + for (const propertyKey of Object.keys(configurationValueObject)) { + const propertyDefaultValue = configurationValueObject[propertyKey]; + + const isObjectSetting = + isObject(propertyDefaultValue) && + (isUndefined(defaultValue[propertyKey]) || isObject(defaultValue[propertyKey])); + + // If the default value is an object, merge the objects and store the source of each keys + if (isObjectSetting) { + defaultValue[propertyKey] = { + ...(defaultValue[propertyKey] ?? {}), + ...propertyDefaultValue, + }; + // Track the source of each value in the object + if (valueSource) { + for (const objectKey in propertyDefaultValue) { + source.set(`${propertyKey}.${objectKey}`, valueSource); + } + } + } + + // Primitive values are overridden + else { + defaultValue[propertyKey] = propertyDefaultValue; + if (valueSource) { + source.set(propertyKey, valueSource); + } else { + source.delete(propertyKey); + } + } + } + + return { value: defaultValue, source }; + } + + private mergeDefaultConfigurationsForConfigurationProperty( + propertyKey: string, + value: any, + valuesSource: IExtensionInfo | undefined, + existingDefaultOverride: IConfigurationDefaultOverrideValue | undefined, + ): IConfigurationDefaultOverrideValue | undefined { + const property = this.configurationProperties[propertyKey]; + const existingDefaultValue = existingDefaultOverride?.value ?? property?.defaultDefaultValue; + let source: ConfigurationDefaultValueSource | undefined = valuesSource; + + const isObjectSetting = + isObject(value) && + ((property !== undefined && property.type === 'object') || + (property === undefined && + (isUndefined(existingDefaultValue) || isObject(existingDefaultValue)))); + + // If the default value is an object, merge the objects and store the source of each keys + if (isObjectSetting) { + source = existingDefaultOverride?.source ?? new Map(); + + // This should not happen + if (!(source instanceof Map)) { + console.error('defaultValueSource is not a Map'); + return undefined; + } + + for (const objectKey in value) { + if (valuesSource) { + source.set(`${propertyKey}.${objectKey}`, valuesSource); + } + } + value = { ...(isObject(existingDefaultValue) ? existingDefaultValue : {}), ...value }; + } + + return { value, source }; + } + + public deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void { + const properties = new Set(); + this.doDeregisterDefaultConfigurations(defaultConfigurations, properties); + + this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true }); + } + + private doDeregisterDefaultConfigurations( + defaultConfigurations: IConfigurationDefaults[], + bucket: Set, + ): void { + for (const defaultConfiguration of defaultConfigurations) { + const index = this.registeredConfigurationDefaults.indexOf(defaultConfiguration); + if (index !== -1) { + this.registeredConfigurationDefaults.splice(index, 1); + } + } + + for (const { overrides, source } of defaultConfigurations) { + for (const key in overrides) { + const configurationDefaultOverridesForKey = this.configurationDefaultsOverrides.get(key); + if (!configurationDefaultOverridesForKey) { + continue; + } + + const index = configurationDefaultOverridesForKey.configurationDefaultOverrides.findIndex( + (configurationDefaultOverride) => + source + ? isSameExtension(configurationDefaultOverride.source, source) + : configurationDefaultOverride.value === overrides[key], + ); + if (index === -1) { + continue; + } + + configurationDefaultOverridesForKey.configurationDefaultOverrides.splice(index, 1); + if (configurationDefaultOverridesForKey.configurationDefaultOverrides.length === 0) { + this.configurationDefaultsOverrides.delete(key); + } + + if (OVERRIDE_PROPERTY_REGEX.test(key)) { + let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined; + + // configuration override defaults - merges defaults + for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { + configurationDefaultOverrideValue = + this.mergeDefaultConfigurationsForOverrideIdentifier( + key, + configurationDefaultOverride.value, + configurationDefaultOverride.source, + configurationDefaultOverrideValue, + ); + } + + if ( + configurationDefaultOverrideValue && + !types.isEmptyObject(configurationDefaultOverrideValue.value) + ) { + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = + configurationDefaultOverrideValue; + this.updateDefaultOverrideProperty(key, configurationDefaultOverrideValue, source); + } else { + this.configurationDefaultsOverrides.delete(key); + delete this.configurationProperties[key]; + } + } else { + let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined; + + // configuration override defaults - merges defaults + for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) { + configurationDefaultOverrideValue = + this.mergeDefaultConfigurationsForConfigurationProperty( + key, + configurationDefaultOverride.value, + configurationDefaultOverride.source, + configurationDefaultOverrideValue, + ); + } + + configurationDefaultOverridesForKey.configurationDefaultOverrideValue = + configurationDefaultOverrideValue; + + const property = this.configurationProperties[key]; + if (property) { + this.updatePropertyDefaultValue(key, property); + this.updateSchema(key, property); + } + } + bucket.add(key); + } + } + this.updateOverridePropertyPatternKey(); + } + + private doRegisterOverrideIdentifiers(overrideIdentifiers: string[]) { + for (const overrideIdentifier of overrideIdentifiers) { + this.overrideIdentifiers.add(overrideIdentifier); + } + this.updateOverridePropertyPatternKey(); + } + + private updateOverridePropertyPatternKey() { + for (const overrideIdentifier of this.overrideIdentifiers.values()) { + const overrideIdentifierProperty = `[${overrideIdentifier}]`; + const propertiesSchema: IJSONSchema = { + type: 'object', + description: 'overrideSettings.defaultDescription', + errorMessage: 'overrideSettings.errorMessage', + }; + this.updatePropertyDefaultValue(overrideIdentifierProperty, propertiesSchema); + allSettings.properties[overrideIdentifierProperty] = propertiesSchema; + } } getConfigurationProperties(): StringDictionary { return this.configurationProperties; } - getConfigurations(): IConfigurationNode[] { - return this.configurationContributors; - } - getExcludedConfigurationProperties(): StringDictionary { return this.excludedConfigurationProperties; } + getRegisteredDefaultConfigurations(): IConfigurationDefaults[] { + return [...this.registeredConfigurationDefaults]; + } + + getConfigurationDefaultsOverrides(): Map { + const configurationDefaultsOverrides = new Map(); + for (const [key, value] of this.configurationDefaultsOverrides) { + if (value.configurationDefaultOverrideValue) { + configurationDefaultsOverrides.set(key, value.configurationDefaultOverrideValue); + } + } + return configurationDefaultsOverrides; + } + onDidUpdateConfiguration( fn: (change: { properties: ReadonlySet; defaultsOverrides?: boolean | undefined; }) => void, ) { - return this.updateConfigurationEmitter.on(fn); + return this.propertiesChangeEmitter.on(fn); } - onDidSchemaChange(fn: () => void) { - return this.schemaChangeEmitter.on(fn); + private registerJSONConfiguration(configuration: IConfigurationNode) { + const register = (configuration: IConfigurationNode) => { + const properties = configuration.properties; + if (properties) { + Object.keys(properties).forEach((key) => { + this.updateSchema(key, properties[key]); + }); + } + const subNodes = configuration.allOf; + subNodes?.forEach(register); + }; + register(configuration); + } + + private updateSchema(key: string, property: IConfigurationPropertySchema): void { + allSettings.properties[key] = property; + } + + private removeFromSchema(key: string): void { + delete allSettings.properties[key]; } } -export const Extension = { - Configuration: 'base.contributions.configuration', -}; +function isSameExtension(a?: IExtensionInfo, b?: IExtensionInfo): boolean { + if (!a || !b) return false; + return a.name === b.name; +} -Registry.add(Extension.Configuration, new ConfigurationRegistry()); +Registry.add(Extensions.Configuration, new ConfigurationRegistry()); diff --git a/packages/engine-core/src/configuration/configurationService.ts b/packages/engine-core/src/configuration/configurationService.ts index 466312a09..9f79cc121 100644 --- a/packages/engine-core/src/configuration/configurationService.ts +++ b/packages/engine-core/src/configuration/configurationService.ts @@ -1,19 +1,29 @@ -import { createDecorator, Provide, type Event } from '@alilc/lowcode-shared'; -import { IConfigurationOverrides, IConfigurationUpdateOverrides } from './configuration'; - -export interface IConfigurationChangeEvent { - readonly affectedKeys: ReadonlySet; - readonly change: IConfigurationChange; - - affectsConfiguration(configuration: string, overrides?: string[]): boolean; -} - -export interface IConfigurationChange { - keys: string[]; - overrides: [string, string[]][]; -} +import { + createDecorator, + Emitter, + Provide, + type Event, + type EventListener, +} from '@alilc/lowcode-shared'; +import { + Configuration, + DefaultConfiguration, + type IConfigurationData, + type IConfigurationOverrides, + type IConfigurationValue, + UserConfiguration, +} from './configurations'; +import { ConfigurationModel } from './configurationModel'; +import { isEqual } from 'lodash-es'; +import { + ConfigurationChangeEvent, + type IConfigurationChangeEvent, + type IConfigurationChange, +} from './configurationChangeEvent'; export interface IConfigurationService { + initialize(): Promise; + /** * Fetches the value of the section for the given overrides. * Value can be of native type or an object keyed off the section name. @@ -42,17 +52,17 @@ export interface IConfigurationService { * @param value The new value */ updateValue(key: string, value: any): Promise; - updateValue( - key: string, - value: any, - overrides: IConfigurationOverrides | IConfigurationUpdateOverrides, - ): Promise; + updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise; - inspect(key: string, overrides?: IConfigurationOverrides): Readonly; + inspect(key: string, overrides?: IConfigurationOverrides): IConfigurationValue>; reloadConfiguration(): Promise; - keys(): string[]; + keys(): { + default: string[]; + user: string[]; + memory?: string[]; + }; onDidChangeConfiguration: Event; } @@ -60,4 +70,134 @@ export interface IConfigurationService { export const IConfigurationService = createDecorator('configurationService'); @Provide(IConfigurationService) -export class ConfigurationService implements IConfigurationService {} +export class ConfigurationService implements IConfigurationService { + private configuration: Configuration; + private readonly defaultConfiguration: DefaultConfiguration; + private readonly userConfiguration: UserConfiguration; + + private readonly didChangeEmitter = new Emitter(); + + constructor() { + this.defaultConfiguration = new DefaultConfiguration(); + this.userConfiguration = new UserConfiguration({}); + this.configuration = new Configuration( + this.defaultConfiguration.configurationModel, + ConfigurationModel.createEmptyModel(), + ConfigurationModel.createEmptyModel(), + ); + } + + async initialize(): Promise { + const [defaultModel, userModel] = await Promise.all([ + this.defaultConfiguration.initialize(), + this.userConfiguration.loadConfiguration(), + ]); + + this.configuration = new Configuration( + defaultModel, + userModel, + ConfigurationModel.createEmptyModel(), + ); + } + + getConfigurationData(): IConfigurationData { + return this.configuration.toData(); + } + + getValue(): T; + getValue(section: string): T; + getValue(overrides: IConfigurationOverrides): T; + getValue(section: string, overrides: IConfigurationOverrides): T; + getValue(arg1?: unknown, arg2?: unknown): any { + const section = typeof arg1 === 'string' ? arg1 : undefined; + const overrides = isConfigurationOverrides(arg1) + ? arg1 + : isConfigurationOverrides(arg2) + ? arg2 + : {}; + + return this.configuration.getValue(section, overrides); + } + + updateValue(key: string, value: any): Promise; + updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise; + async updateValue(key: string, value: any, arg3?: IConfigurationOverrides): Promise { + const overrides: IConfigurationOverrides | undefined = isConfigurationOverrides(arg3) + ? arg3 + : undefined; + + const inspect = this.inspect(key, { + overrideIdentifier: overrides?.overrideIdentifier, + }); + + // Remove the setting, if the value is same as default value + if (isEqual(value, inspect.defaultValue)) { + value = undefined; + } + + if (overrides?.overrideIdentifier) { + const overrideIdentifier = overrides.overrideIdentifier; + const existingOverride = this.configuration.userConfiguration.overrides.find((override) => + override.identifiers.includes(overrideIdentifier), + ); + if (!existingOverride) { + overrides.overrideIdentifier = undefined; + } + } + + const path = overrides?.overrideIdentifier ? [overrides.overrideIdentifier, key] : [key]; + + // modify user config later todo... + await this.userConfiguration.syncRemoteConfiguration(path, value); + } + + inspect(key: string, overrides: IConfigurationOverrides = {}): IConfigurationValue { + return this.configuration.inspect(key, overrides); + } + + keys(): { + default: string[]; + user: string[]; + } { + return this.configuration.keys(); + } + + async reloadConfiguration(): Promise { + const configurationModel = await this.userConfiguration.loadConfiguration(); + this.onDidChangeUserConfiguration(configurationModel); + } + + private onDidChangeUserConfiguration(user: ConfigurationModel) { + const previous = this.configuration.toData(); + const change = this.configuration.compareAndUpdateUserConfiguration(user); + this.trigger(change, previous); + } + + private trigger(configurationChange: IConfigurationChange, previous: IConfigurationData): void { + const event = new ConfigurationChangeEvent( + configurationChange, + { data: previous }, + this.configuration, + ); + this.didChangeEmitter.emit(event); + } + + onDidChangeConfiguration(listener: EventListener) { + return this.didChangeEmitter.on(listener); + } +} + +export function isConfigurationOverrides(thing: any): thing is IConfigurationOverrides { + return ( + thing && + typeof thing === 'object' && + (!thing.overrideIdentifier || typeof thing.overrideIdentifier === 'string') + ); +} + +export function keyFromOverrideIdentifiers(overrideIdentifiers: string[]): string { + return overrideIdentifiers.reduce( + (result, overrideIdentifier) => `${result}[${overrideIdentifier}]`, + '', + ); +} diff --git a/packages/engine-core/src/configuration/configurations.ts b/packages/engine-core/src/configuration/configurations.ts new file mode 100644 index 000000000..a5acc526c --- /dev/null +++ b/packages/engine-core/src/configuration/configurations.ts @@ -0,0 +1,592 @@ +import { type StringDictionary, Emitter, type EventListener } from '@alilc/lowcode-shared'; +import { + ConfigurationModel, + type IConfigurationModel, + type InspectValue, + type IOverrides, +} from './configurationModel'; +import { + type IConfigurationPropertySchema, + type IConfigurationRegistry, + type IRegisteredConfigurationPropertySchema, +} from './configurationRegistry'; +import { Registry, Extensions } from '../common/registry'; +import { isEqual, isNil, isPlainObject, get as lodasgGet } from 'lodash-es'; +import { + IInspectValue, + toValuesTree, + OVERRIDE_PROPERTY_REGEX, + overrideIdentifiersFromKey, +} from './configuration'; + +export interface IConfigurationOverrides { + overrideIdentifier?: string | null; +} + +export class DefaultConfiguration { + private emitter = new Emitter<{ + defaults: ConfigurationModel; + properties: string[]; + }>(); + + private _configurationModel = ConfigurationModel.createEmptyModel(); + + get configurationModel(): ConfigurationModel { + return this._configurationModel; + } + + initialize(): ConfigurationModel { + this.resetConfigurationModel(); + Registry.as(Extensions.Configuration).onDidUpdateConfiguration( + ({ properties }) => this.onDidUpdateConfiguration([...properties]), + ); + + return this.configurationModel; + } + + reload(): ConfigurationModel { + this.resetConfigurationModel(); + return this.configurationModel; + } + + onDidChangeConfiguration( + listener: EventListener<[{ defaults: ConfigurationModel; properties: string[] }]>, + ) { + return this.emitter.on(listener); + } + + private onDidUpdateConfiguration(properties: string[]): void { + this.updateConfigurationModel( + properties, + Registry.as(Extensions.Configuration).getConfigurationProperties(), + ); + this.emitter.emit({ defaults: this.configurationModel, properties }); + } + + private resetConfigurationModel(): void { + this._configurationModel = ConfigurationModel.createEmptyModel(); + + const properties = Registry.as( + Extensions.Configuration, + ).getConfigurationProperties(); + + this.updateConfigurationModel(Object.keys(properties), properties); + } + + private updateConfigurationModel( + properties: string[], + configurationProperties: StringDictionary, + ): void { + for (const key of properties) { + const propertySchema = configurationProperties[key]; + if (propertySchema) { + this.configurationModel.setValue(key, propertySchema.default); + } else { + this.configurationModel.removeValue(key); + } + } + } +} + +export interface ConfigurationParseOptions { + include?: string[]; + exclude?: string[]; +} + +class ConfigurationModelParser { + private _raw: any = null; + private _configurationModel: ConfigurationModel | null = null; + private _parseErrors: any[] = []; + + constructor() {} + + get configurationModel(): ConfigurationModel { + return this._configurationModel || ConfigurationModel.createEmptyModel(); + } + + get errors(): any[] { + return this._parseErrors; + } + + parse(content: StringDictionary | null | undefined, options?: ConfigurationParseOptions): void { + if (!isNil(content)) { + const raw = this.doParseContent(content); + this.parseRaw(raw, options); + } + } + + reparse(options: ConfigurationParseOptions): void { + if (this._raw) { + this.parseRaw(this._raw, options); + } + } + + private doParseContent(content: StringDictionary): any { + function flatten(obj: any, parentKey: string = '', result: any = {}): any { + for (const key of Object.keys(obj)) { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (isPlainObject(obj)) { + flatten(obj[key], fullKey, result); + } else { + result[fullKey] = obj[key]; + } + } + + return result; + } + + return flatten(content); + } + + parseRaw(raw: any, options?: ConfigurationParseOptions): void { + this._raw = raw; + + const { contents, keys, overrides, hasExcludedProperties } = this.doParseRaw(raw, options); + + this._configurationModel = new ConfigurationModel( + contents, + keys, + overrides, + hasExcludedProperties ? [raw] : undefined /* raw has not changed */, + ); + } + + protected doParseRaw( + raw: any, + options?: ConfigurationParseOptions, + ): IConfigurationModel & { hasExcludedProperties?: boolean } { + const configurationProperties = Registry.as( + Extensions.Configuration, + ).getConfigurationProperties(); + const filtered = this.filter(raw, configurationProperties, true, options); + + raw = filtered.raw; + + const contents = toValuesTree(raw); + const keys = Object.keys(raw); + const overrides = this.toOverrides(raw); + + return { + contents, + keys, + overrides, + hasExcludedProperties: filtered.hasExcludedProperties, + }; + } + + private filter( + properties: any, + configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema | undefined }, + filterOverriddenProperties: boolean, + options?: ConfigurationParseOptions, + ): { raw: any; hasExcludedProperties: boolean } { + let hasExcludedProperties = false; + + if (!options?.exclude?.length) { + return { raw: properties, hasExcludedProperties }; + } + + const raw: any = {}; + + for (const key in properties) { + if (OVERRIDE_PROPERTY_REGEX.test(key) && filterOverriddenProperties) { + const result = this.filter(properties[key], configurationProperties, false, options); + + raw[key] = result.raw; + hasExcludedProperties = hasExcludedProperties || result.hasExcludedProperties; + } else { + if ( + !options.exclude?.includes(key) /* Check exclude */ && + options.include?.includes(key) /* Check include */ + ) { + /* Check restricted */ raw[key] = properties[key]; + } else { + hasExcludedProperties = true; + } + } + } + + return { raw, hasExcludedProperties }; + } + + private toOverrides(raw: any): IOverrides[] { + const overrides: IOverrides[] = []; + + for (const key of Object.keys(raw)) { + if (OVERRIDE_PROPERTY_REGEX.test(key)) { + const overrideRaw: any = {}; + + for (const keyInOverrideRaw of Object.keys(raw[key])) { + overrideRaw[keyInOverrideRaw] = raw[key][keyInOverrideRaw]; + } + + overrides.push({ + identifiers: overrideIdentifiersFromKey(key), + keys: Object.keys(overrideRaw), + contents: toValuesTree(overrideRaw), + }); + } + } + + return overrides; + } +} + +/** + * 本地优先的用户缓存配置策略 + */ +export class UserConfiguration { + private readonly parser: ConfigurationModelParser; + + constructor(private parseOptions: ConfigurationParseOptions) { + this.parser = new ConfigurationModelParser(); + } + + async loadConfiguration(): Promise { + try { + // const content = await this.fileService.readFile(this.userSettingsResource); + this.parser.parse({}, this.parseOptions); + return this.parser.configurationModel; + } catch (e) { + return ConfigurationModel.createEmptyModel(); + } + } + + reparse(parseOptions?: ConfigurationParseOptions): ConfigurationModel { + if (parseOptions) { + this.parseOptions = parseOptions; + } + this.parser.reparse(this.parseOptions); + return this.parser.configurationModel; + } + + async syncRemoteConfiguration(path: string[], value: any): Promise { + // todo: scheduler + // 本地同步远程服务器 + this.parser.configurationModel.setValue(path.join('.'), value); + } +} + +export interface IConfigurationValue { + readonly defaultValue?: T; + readonly userValue?: T; + readonly memoryValue?: T; + readonly value?: T; + + readonly default?: IInspectValue; + readonly user?: IInspectValue; + readonly memory?: IInspectValue; + + readonly overrideIdentifiers?: string[]; +} + +class ConfigurationInspectValue implements IConfigurationValue { + constructor( + private readonly key: string, + private readonly overrides: IConfigurationOverrides, + private readonly _value: V | undefined, + readonly overrideIdentifiers: string[] | undefined, + private readonly defaultConfiguration: ConfigurationModel, + private readonly userConfiguration: ConfigurationModel, + private readonly memoryConfigurationModel: ConfigurationModel, + ) {} + + get value(): V | undefined { + return this._value; + } + + private toInspectValue( + inspectValue: IInspectValue | undefined | null, + ): IInspectValue | undefined { + return inspectValue?.value !== undefined || + inspectValue?.override !== undefined || + inspectValue?.overrides !== undefined + ? inspectValue + : undefined; + } + + private _defaultInspectValue: InspectValue | undefined; + private get defaultInspectValue(): InspectValue { + if (!this._defaultInspectValue) { + this._defaultInspectValue = this.defaultConfiguration.inspect( + this.key, + this.overrides.overrideIdentifier, + ); + } + return this._defaultInspectValue; + } + + get defaultValue(): V | undefined { + return this.defaultInspectValue.merged; + } + + get default(): IInspectValue | undefined { + return this.toInspectValue(this.defaultInspectValue); + } + + private _userInspectValue: InspectValue | undefined; + private get userInspectValue(): InspectValue { + if (!this._userInspectValue) { + this._userInspectValue = this.userConfiguration.inspect( + this.key, + this.overrides.overrideIdentifier, + ); + } + return this._userInspectValue; + } + + get userValue(): V | undefined { + return this.userInspectValue.merged; + } + + get user(): IInspectValue | undefined { + return this.toInspectValue(this.userInspectValue); + } + + private _memoryInspectValue: InspectValue | undefined; + private get memoryInspectValue(): InspectValue { + if (this._memoryInspectValue === undefined) { + this._memoryInspectValue = this.memoryConfigurationModel.inspect( + this.key, + this.overrides.overrideIdentifier, + ); + } + return this._memoryInspectValue; + } + + get memoryValue(): V | undefined { + return this.memoryInspectValue.merged; + } + + get memory(): IInspectValue | undefined { + return this.toInspectValue(this.memoryInspectValue); + } +} + +export interface IConfigurationData { + defaults: IConfigurationModel; + user: IConfigurationModel; +} + +export interface IConfigurationChange { + keys: string[]; + overrides: [string, string[]][]; +} + +export class Configuration { + static parse(data: IConfigurationData): Configuration { + const parseConfigurationModel = (model: IConfigurationModel): ConfigurationModel => { + return new ConfigurationModel(model.contents, model.keys, model.overrides, undefined); + }; + + const defaultConfiguration = parseConfigurationModel(data.defaults); + const userConfiguration = parseConfigurationModel(data.user); + + return new Configuration( + defaultConfiguration, + userConfiguration, + ConfigurationModel.createEmptyModel(), + ); + } + + private _consolidatedConfiguration: ConfigurationModel | null = null; + + constructor( + private _defaultConfiguration: ConfigurationModel, + private _userConfiguration: ConfigurationModel, + private _memoryConfiguration: ConfigurationModel, + ) {} + + get defaults(): ConfigurationModel { + return this._defaultConfiguration; + } + + get userConfiguration(): ConfigurationModel { + return this._userConfiguration; + } + + getValue(section: string | undefined, overrides: IConfigurationOverrides): any { + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides); + return consolidateConfigurationModel.getValue(section); + } + + updateValue(key: string, value: any): void { + const memoryConfiguration = this._memoryConfiguration; + + if (value === undefined) { + memoryConfiguration.removeValue(key); + } else { + memoryConfiguration.setValue(key, value); + } + } + + inspect(key: string, overrides: IConfigurationOverrides): IConfigurationValue { + const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides); + + const overrideIdentifiers = new Set(); + for (const override of consolidateConfigurationModel.overrides) { + for (const overrideIdentifier of override.identifiers) { + if (consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined) { + overrideIdentifiers.add(overrideIdentifier); + } + } + } + + return new ConfigurationInspectValue( + key, + overrides, + consolidateConfigurationModel.getValue(key), + overrideIdentifiers.size ? [...overrideIdentifiers] : undefined, + this._defaultConfiguration, + this._userConfiguration, + this._memoryConfiguration, + ); + } + + keys(): { + default: string[]; + user: string[]; + } { + return { + default: this._defaultConfiguration.keys.slice(0), + user: this._userConfiguration.keys.slice(0), + }; + } + + toData(): IConfigurationData { + return { + defaults: { + contents: this._defaultConfiguration.contents, + overrides: this._defaultConfiguration.overrides, + keys: this._defaultConfiguration.keys, + }, + user: { + contents: this._userConfiguration.contents, + overrides: this._userConfiguration.overrides, + keys: this._userConfiguration.keys, + }, + }; + } + + private getConsolidatedConfigurationModel( + overrides: IConfigurationOverrides, + ): ConfigurationModel { + let configurationModel = this.getWorkspaceConsolidatedConfiguration(); + if (overrides.overrideIdentifier) { + configurationModel = configurationModel.override(overrides.overrideIdentifier); + } + + return configurationModel; + } + + private getWorkspaceConsolidatedConfiguration(): ConfigurationModel { + if (!this._consolidatedConfiguration) { + this._consolidatedConfiguration = this._defaultConfiguration.merge( + this._userConfiguration, + this._memoryConfiguration, + ); + } + return this._consolidatedConfiguration; + } + + compareAndUpdateUserConfiguration(user: ConfigurationModel): IConfigurationChange { + const { added, updated, removed, overrides } = compare(this.userConfiguration, user); + const keys = [...added, ...updated, ...removed]; + if (keys.length) { + this._userConfiguration = user; + this._consolidatedConfiguration = null; + } + return { keys, overrides }; + } +} + +export interface IConfigurationCompareResult { + added: string[]; + removed: string[]; + updated: string[]; + overrides: [string, string[]][]; +} + +export function compare( + from: ConfigurationModel | undefined, + to: ConfigurationModel | undefined, +): IConfigurationCompareResult { + const { added, removed, updated } = compareConfigurationContents( + to?.rawConfiguration, + from?.rawConfiguration, + ); + const overrides: [string, string[]][] = []; + + const fromOverrideIdentifiers = from?.getAllOverrideIdentifiers() || []; + const toOverrideIdentifiers = to?.getAllOverrideIdentifiers() || []; + + if (to) { + const addedOverrideIdentifiers = toOverrideIdentifiers.filter( + (key) => !fromOverrideIdentifiers.includes(key), + ); + for (const identifier of addedOverrideIdentifiers) { + overrides.push([identifier, to.getKeysForOverrideIdentifier(identifier)]); + } + } + + if (from) { + const removedOverrideIdentifiers = fromOverrideIdentifiers.filter( + (key) => !toOverrideIdentifiers.includes(key), + ); + for (const identifier of removedOverrideIdentifiers) { + overrides.push([identifier, from.getKeysForOverrideIdentifier(identifier)]); + } + } + + if (to && from) { + for (const identifier of fromOverrideIdentifiers) { + if (toOverrideIdentifiers.includes(identifier)) { + const result = compareConfigurationContents( + { + contents: from.getOverrideValue(undefined, identifier) || {}, + keys: from.getKeysForOverrideIdentifier(identifier), + }, + { + contents: to.getOverrideValue(undefined, identifier) || {}, + keys: to.getKeysForOverrideIdentifier(identifier), + }, + ); + overrides.push([identifier, [...result.added, ...result.removed, ...result.updated]]); + } + } + } + + return { added, removed, updated, overrides }; +} + +function compareConfigurationContents( + to: { keys: string[]; contents: any } | undefined, + from: { keys: string[]; contents: any } | undefined, +) { + const added = to + ? from + ? to.keys.filter((key) => from.keys.indexOf(key) === -1) + : [...to.keys] + : []; + const removed = from + ? to + ? from.keys.filter((key) => to.keys.indexOf(key) === -1) + : [...from.keys] + : []; + const updated: string[] = []; + + if (to && from) { + for (const key of from.keys) { + if (to.keys.indexOf(key) !== -1) { + const value1 = lodasgGet(from.contents, key); + const value2 = lodasgGet(to.contents, key); + if (!isEqual(value1, value2)) { + updated.push(key); + } + } + } + } + + return { added, removed, updated }; +} diff --git a/packages/engine-core/src/configuration/index.ts b/packages/engine-core/src/configuration/index.ts index 7efdece07..597242352 100644 --- a/packages/engine-core/src/configuration/index.ts +++ b/packages/engine-core/src/configuration/index.ts @@ -1,3 +1,4 @@ export * from './configurationModel'; export * from './configurationRegistry'; -export * from './configuration'; +export * from './configurations'; +export * from './configurationService'; diff --git a/packages/engine-core/src/extension/extension.ts b/packages/engine-core/src/extension/extension.ts new file mode 100644 index 000000000..b012bb45a --- /dev/null +++ b/packages/engine-core/src/extension/extension.ts @@ -0,0 +1,37 @@ +import { StringDictionary } from '@alilc/lowcode-shared'; +import { IConfigurationNode } from '../configuration'; + +export type ExtensionInitializer = (ctx: Context) => IExtensionInstance; + +/** + * 函数声明插件 + */ +export interface IFunctionExtension extends ExtensionInitializer { + name: string; + version: string; + meta?: IExtensionMetadata; +} + +export interface IExtensionMetadata { + /** + * define dependencies which the plugin depends on + */ + dependencies?: string[]; + + /** + * specify which engine version is compatible with the plugin + * version rule useage semver version, eg: ^1.0.0; + */ + engineVerison?: string; + + /** + * 插件的配置注册信息表 + */ + preferenceConfigurations?: IConfigurationNode[]; +} + +export interface IExtensionInstance { + init(): Promise | void; + destroy(): Promise | void; + exports?(): StringDictionary | undefined | void; +} diff --git a/packages/engine-core/src/extension/extensionHost.ts b/packages/engine-core/src/extension/extensionHost.ts new file mode 100644 index 000000000..b71073238 --- /dev/null +++ b/packages/engine-core/src/extension/extensionHost.ts @@ -0,0 +1,61 @@ +import { type IConfigurationRegistry, type IConfigurationNode } from '../configuration'; +import { Registry, Extensions } from '../common/registry'; +import { type ExtensionInitializer, type IExtensionInstance } from './extension'; +import { invariant } from '@alilc/lowcode-shared'; + +export type ExtensionExportsAccessor = { + [key: string]: any; +}; + +export class ExtensionHost { + private isInited = false; + + private instance: IExtensionInstance; + + private configurationProperties: ReadonlySet; + + constructor( + public name: string, + initializer: ExtensionInitializer, + preferenceConfigurations: IConfigurationNode[], + ) { + const configurationRegistry = Registry.as(Extensions.Configuration); + this.configurationProperties = + configurationRegistry.registerConfigurations(preferenceConfigurations); + + this.instance = initializer({}); + } + + async init(): Promise { + if (this.isInited) return; + + await this.instance.init(); + + this.isInited = true; + } + + async destroy(): Promise { + if (!this.isInited) return; + + await this.instance.destroy(); + + this.isInited = false; + } + + toProxy(): ExtensionExportsAccessor | undefined { + invariant(this.isInited, 'Could not call toProxy before init'); + + const exports = this.instance.exports?.(); + + if (!exports) return; + + return new Proxy(Object.create(null), { + get(target, prop, receiver) { + if (Reflect.has(exports, prop)) { + return exports?.[prop as string]; + } + return Reflect.get(target, prop, receiver); + }, + }); + } +} diff --git a/packages/engine-core/src/extension/extensionManagement.ts b/packages/engine-core/src/extension/extensionManagement.ts new file mode 100644 index 000000000..9a79d56aa --- /dev/null +++ b/packages/engine-core/src/extension/extensionManagement.ts @@ -0,0 +1,92 @@ +import { type Reference } from '@alilc/lowcode-shared'; +import { type IFunctionExtension } from './extension'; +import { type IConfigurationNode } from '../configuration'; +import { ExtensionHost } from './extensionHost'; + +export interface IExtensionGallery { + name: string; + version: string; + reference: Reference | undefined; + dependencies: string[] | undefined; + engineVerison: string | undefined; + preferenceConfigurations: IConfigurationNode[] | undefined; +} + +export interface IExtensionRegisterOptions { + /** + * Will enable plugin registered with auto-initialization immediately + * other than plugin-manager init all plugins at certain time. + * It is helpful when plugin register is later than plugin-manager initialization. + */ + autoInit?: boolean; + /** + * allow overriding existing plugin with same name when override === true + */ + override?: boolean; +} + +export class ExtensionManagement { + private extensionGalleryMap: Map = new Map(); + private extensionHosts: Map = new Map(); + + constructor() {} + + async register( + extension: IFunctionExtension, + { autoInit = false, override = false }: IExtensionRegisterOptions = {}, + ): Promise { + if (!this.validateExtension(extension, override)) return; + + const metadata = extension.meta ?? {}; + const host = new ExtensionHost( + extension.name, + extension, + metadata.preferenceConfigurations ?? [], + ); + + if (autoInit) { + await host.init(); + } + + this.extensionHosts.set(extension.name, host); + + const gallery: IExtensionGallery = { + name: extension.name, + version: extension.version, + reference: undefined, + dependencies: metadata.dependencies, + engineVerison: metadata.engineVerison, + preferenceConfigurations: metadata.preferenceConfigurations, + }; + + this.extensionGalleryMap.set(gallery.name, gallery); + } + + private validateExtension(extension: IFunctionExtension, override: boolean): boolean { + if (!override && this.has(extension.name)) return false; + + return true; + } + + async deregister(name: string): Promise { + if (this.has(name)) { + const host = this.extensionHosts.get(name)!; + await host.destroy(); + + this.extensionGalleryMap.delete(name); + this.extensionHosts.delete(name); + } + } + + has(name: string): boolean { + return this.extensionGalleryMap.has(name); + } + + getExtensionGallery(name: string): IExtensionGallery | undefined { + return this.extensionGalleryMap.get(name); + } + + getExtensionHost(name: string): ExtensionHost | undefined { + return this.extensionHosts.get(name); + } +} diff --git a/packages/engine-core/src/extension/extensionService.ts b/packages/engine-core/src/extension/extensionService.ts new file mode 100644 index 000000000..621fe65a5 --- /dev/null +++ b/packages/engine-core/src/extension/extensionService.ts @@ -0,0 +1,37 @@ +import { createDecorator, Provide } from '@alilc/lowcode-shared'; +import { ExtensionManagement, type IExtensionRegisterOptions } from './extensionManagement'; +import { type IFunctionExtension } from './extension'; +import { ExtensionHost } from './extensionHost'; + +export interface IExtensionService { + register(extension: IFunctionExtension, options?: IExtensionRegisterOptions): Promise; + + deregister(name: string): Promise; + + has(name: string): boolean; + + getExtensionHost(name: string): ExtensionHost | undefined; +} + +export const IExtensionService = createDecorator('extensionService'); + +@Provide(IExtensionService) +export class ExtensionService implements IExtensionService { + private extensionManagement = new ExtensionManagement(); + + register(extension: IFunctionExtension, options?: IExtensionRegisterOptions): Promise { + return this.extensionManagement.register(extension, options); + } + + deregister(name: string): Promise { + return this.extensionManagement.deregister(name); + } + + has(name: string): boolean { + return this.extensionManagement.has(name); + } + + getExtensionHost(name: string): ExtensionHost | undefined { + return this.extensionManagement.getExtensionHost(name); + } +} diff --git a/packages/engine-core/src/extension/index.ts b/packages/engine-core/src/extension/index.ts deleted file mode 100644 index 585b667a0..000000000 --- a/packages/engine-core/src/extension/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './registry'; diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts index 3400c6181..4704080c8 100644 --- a/packages/engine-core/src/index.ts +++ b/packages/engine-core/src/index.ts @@ -1,2 +1,8 @@ export * from './configuration'; -export * from './extension'; +export * from './extension/extension'; +export * from './resource'; +export * from './command'; + +// test +export * from './common/registry'; +export * from './main'; diff --git a/packages/engine-core/src/keybinding/keybinding.ts b/packages/engine-core/src/keybinding/keybinding.ts new file mode 100644 index 000000000..84796f871 --- /dev/null +++ b/packages/engine-core/src/keybinding/keybinding.ts @@ -0,0 +1,39 @@ +/** + * A keybinding is a sequence of chords. + */ +export class Keybinding { + public readonly chords: Chord[]; + + constructor(chords: Chord[]) { + if (chords.length === 0) { + throw illegalArgument(`chords`); + } + this.chords = chords; + } + + public getHashCode(): string { + let result = ''; + for (let i = 0, len = this.chords.length; i < len; i++) { + if (i !== 0) { + result += ';'; + } + result += this.chords[i].getHashCode(); + } + return result; + } + + public equals(other: Keybinding | null): boolean { + if (other === null) { + return false; + } + if (this.chords.length !== other.chords.length) { + return false; + } + for (let i = 0; i < this.chords.length; i++) { + if (!this.chords[i].equals(other.chords[i])) { + return false; + } + } + return true; + } +} diff --git a/packages/engine-core/src/keybinding/keybindingRegistry.ts b/packages/engine-core/src/keybinding/keybindingRegistry.ts new file mode 100644 index 000000000..3d3a54ae5 --- /dev/null +++ b/packages/engine-core/src/keybinding/keybindingRegistry.ts @@ -0,0 +1,26 @@ +export interface IKeybindingItem { + keybinding: Keybinding | null; + command: string | null; + commandArgs?: any; + weight1: number; + weight2: number; + extensionId: string | null; + isBuiltinExtension: boolean; +} + +export interface IKeybindings { + primary?: number; + secondary?: number[]; + win?: { + primary: number; + secondary?: number[]; + }; + linux?: { + primary: number; + secondary?: number[]; + }; + mac?: { + primary: number; + secondary?: number[]; + }; +} diff --git a/packages/engine-core/src/workbench/window/windowService.ts b/packages/engine-core/src/lifecycle/lifeCycleService.ts similarity index 100% rename from packages/engine-core/src/workbench/window/windowService.ts rename to packages/engine-core/src/lifecycle/lifeCycleService.ts diff --git a/packages/engine-core/src/main.ts b/packages/engine-core/src/main.ts new file mode 100644 index 000000000..ac996c950 --- /dev/null +++ b/packages/engine-core/src/main.ts @@ -0,0 +1,26 @@ +import { InstantiationService } from '@alilc/lowcode-shared'; +import { IWorkbenchService } from './workbench'; +import { IConfigurationService } from './configuration'; + +export class MainApplication { + constructor() { + console.log('main application'); + } + + async main() { + const instantiationService = new InstantiationService(); + const configurationService = instantiationService.get(IConfigurationService); + const workbench = instantiationService.get(IWorkbenchService); + + await configurationService.initialize(); + workbench.initialize(); + } +} + +export async function createLowCodeEngineApp(): Promise { + const app = new MainApplication(); + + await app.main(); + + return app; +} diff --git a/packages/engine-core/src/plugin/context.ts b/packages/engine-core/src/plugin/context.ts deleted file mode 100644 index 8309560e1..000000000 --- a/packages/engine-core/src/plugin/context.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { EventEmitter } from '@alilc/lowcode-shared'; -import type { PluginMeta, PluginPreferenceValue, PluginDeclaration } from './types'; -import { PluginManager } from './manager'; - -export interface PluginPreferenceMananger { - getPreferenceValue: ( - key: string, - defaultValue?: PluginPreferenceValue, - ) => PluginPreferenceValue | undefined; -} - -export interface PluginContextOptions> { - pluginName: string; - meta?: PluginMeta; - enhance?: (context: PluginContext, pluginName: string, meta: PluginMeta) => void; -} - -export class PluginContext> { - #pluginManager: PluginManager; - #meta: PluginMeta = {}; - - public pluginName: string; - - public pluginEvent: EventEmitter; - - public preference: PluginPreferenceMananger; - - constructor( - options: PluginContextOptions, - pluginManager: PluginManager, - ) { - this.pluginName = options.pluginName; - this.pluginEvent = new EventEmitter(this.pluginName); - - this.#pluginManager = pluginManager; - if (options.meta) this.#meta = options.meta; - - options.enhance?.(this, this.pluginName, this.#meta); - - /** - * 管理器初始化时可以提供全局配置给到各插件,通过这个方法可以获得本插件对应的配置 - * use this to get preference config for this plugin when init - * todo: 这个全局配置是否真的有必要??? - */ - this.preference = { - getPreferenceValue: (key, defaultValue) => { - if ( - !this.#meta.preferenceDeclaration || - !isValidPreferenceKey(key, this.#meta.preferenceDeclaration) - ) { - return undefined; - } - const globalPluginPreference = - this.#pluginManager.getPluginPreference(this.pluginName) ?? {}; - if (globalPluginPreference[key] === undefined || globalPluginPreference[key] === null) { - return defaultValue; - } - return globalPluginPreference[key]; - }, - }; - } -} - -export function isValidPreferenceKey( - key: string, - preferenceDeclaration?: PluginDeclaration, -): boolean { - if (!preferenceDeclaration || !Array.isArray(preferenceDeclaration.properties)) { - return false; - } - return preferenceDeclaration.properties.some((prop) => { - return prop.key === key; - }); -} diff --git a/packages/engine-core/src/plugin/index.ts b/packages/engine-core/src/plugin/index.ts deleted file mode 100644 index 1d1440826..000000000 --- a/packages/engine-core/src/plugin/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types'; -export * from './manager'; diff --git a/packages/engine-core/src/plugin/manager.ts b/packages/engine-core/src/plugin/manager.ts deleted file mode 100644 index b7063b393..000000000 --- a/packages/engine-core/src/plugin/manager.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { createLogger, invariant } from '@alilc/lowcode-shared'; -import { isPlainObject } from 'lodash-es'; -import { sequencify } from './utils'; -import { PluginRuntime } from './runtime'; -import type { PluginCreater, PluginPreferenceValue, PluginDeclaration } from './types'; -import { PluginContext, type PluginContextOptions, isValidPreferenceKey } from './context'; - -const logger = createLogger({ level: 'warn', bizName: 'pluginManager' }); - -export type PluginPreference = Map>; - -export interface PluginRegisterOptions { - /** - * Will enable plugin registered with auto-initialization immediately - * other than plugin-manager init all plugins at certain time. - * It is helpful when plugin register is later than plugin-manager initialization. - */ - autoInit?: boolean; - /** - * allow overriding existing plugin with same name when override === true - */ - override?: boolean; -} - -/** - * plugin manager - */ -export class PluginManager> { - #pluginsMap: Map> = new Map(); - - #pluginContextMap: Map> = new Map(); - - #contextEnhancer: PluginContextOptions['enhance'] = () => {}; - - #pluginPreference: PluginPreference | undefined; - - constructor(contextEnhancer?: PluginContextOptions['enhance']) { - if (contextEnhancer) { - this.#contextEnhancer = contextEnhancer; - } - } - - #getPluginContext = (options: PluginContextOptions) => { - const { pluginName } = options; - let context = this.#pluginContextMap.get(pluginName); - if (!context) { - context = new PluginContext(options, this); - this.#pluginContextMap.set(pluginName, context); - } - return context; - }; - - /** - * register a plugin - * @param pluginConfigCreator - a creator function which returns the plugin config - * @param options - the plugin options - * @param registerOptions - the plugin register options - */ - async register( - pluginCreater: PluginCreater>, - options?: any, - registerOptions?: PluginRegisterOptions, - ): Promise { - // registerOptions maybe in the second place - if (isPluginRegisterOptions(options)) { - registerOptions = options; - options = {}; - } - - const { pluginName, meta = {} } = pluginCreater; - // const { engines } = meta; - // filter invalid eventPrefix - // const isReservedPrefix = RESERVED_EVENT_PREFIX.find((item) => item === eventPrefix); - // if (isReservedPrefix) { - // meta.eventPrefix = undefined; - // logger.warn( - // `plugin ${pluginName} is trying to use ${eventPrefix} as event prefix, which is a reserved event prefix, please use another one`, - // ); - // } - - const ctx = this.#getPluginContext({ - pluginName: pluginCreater.pluginName, - meta, - enhance: this.#contextEnhancer, - }); - - // const pluginTransducer = engineConfig.get('customPluginTransducer', null); - // const newPluginModel = pluginTransducer - // ? await pluginTransducer(pluginModel, ctx, options) - // : pluginModel; - - // const customFilterValidOptions = engineConfig.get( - // 'customPluginFilterOptions', - // filterValidOptions, - // ); - const newOptions = filterValidOptions(options, meta.preferenceDeclaration); - - const pluginInstance = pluginCreater(ctx, newOptions); - - invariant(pluginName, 'pluginConfigCreator.pluginName required', pluginInstance); - - const allowOverride = registerOptions?.override === true; - - if (this.#pluginsMap.has(pluginName)) { - if (!allowOverride) { - throw new Error(`Plugin with name ${pluginName} exists`); - } else { - // clear existing plugin - const originalPlugin = this.#pluginsMap.get(pluginName); - logger.log( - 'plugin override, originalPlugin with name ', - pluginName, - ' will be destroyed, config:', - originalPlugin?.instance, - ); - originalPlugin?.destroy(); - this.#pluginsMap.delete(pluginName); - } - } - - const pluginRuntime = new PluginRuntime(pluginName, this, pluginInstance, meta); - // support initialization of those plugins which registered - // after normal initialization by plugin-manager - if (registerOptions?.autoInit) { - await pluginInstance.init(); - } - this.#pluginsMap.set(pluginName, pluginRuntime); - logger.log( - `plugin registered with pluginName: ${pluginName}, config: `, - pluginInstance, - 'meta:', - meta, - ); - } - - get(pluginName: string): PluginRuntime | undefined { - return this.#pluginsMap.get(pluginName); - } - - getAll(): PluginRuntime[] { - return [...this.#pluginsMap.values()]; - } - - has(pluginName: string): boolean { - return this.#pluginsMap.has(pluginName); - } - - async delete(pluginName: string): Promise { - const plugin = this.#pluginsMap.get(pluginName); - if (!plugin) return false; - await plugin.destroy(); - return this.#pluginsMap.delete(pluginName); - } - - async init(pluginPreference?: PluginPreference) { - // 管理器初始化时可以提供全局配置给到各插件 - // 是否有必要? - this.#pluginPreference = pluginPreference; - - const pluginNames: string[] = []; - const pluginObj: { [name: string]: PluginRuntime } = {}; - - this.#pluginsMap.forEach((plugin) => { - pluginNames.push(plugin.name); - pluginObj[plugin.name] = plugin; - }); - - const { missingTasks, sequence } = sequencify(pluginObj, pluginNames); - invariant(!missingTasks.length, 'plugin dependency missing', missingTasks); - logger.log('load plugin sequence:', sequence); - - for (const pluginName of sequence) { - try { - await this.#pluginsMap.get(pluginName)!.init(); - } catch (e) /* istanbul ignore next */ { - logger.error( - `Failed to init plugin:${pluginName}, it maybe affect those plugins which depend on this.`, - ); - logger.error(e); - } - } - } - - async destroy() { - for (const plugin of this.#pluginsMap.values()) { - await plugin.destroy(); - } - } - - get size() { - return this.#pluginsMap.size; - } - - getPluginPreference(pluginName: string): Record | undefined { - return this.#pluginPreference?.get(pluginName); - } - - toProxy() { - return new Proxy(this, { - get(target, prop, receiver) { - if (target.#pluginsMap.has(prop as string)) { - // 禁用态的插件,直接返回 undefined - if (target.#pluginsMap.get(prop as string)!.disabled) { - return undefined; - } - return target.#pluginsMap.get(prop as string)?.toProxy(); - } - return Reflect.get(target, prop, receiver); - }, - }); - } - - setDisabled(pluginName: string, flag = true) { - logger.warn(`plugin:${pluginName} has been set disable:${flag}`); - this.#pluginsMap.get(pluginName)?.setDisabled(flag); - } - - async dispose() { - await this.destroy(); - this.#pluginsMap.clear(); - } -} - -function isPluginRegisterOptions(opts: any): opts is PluginRegisterOptions { - return opts && ('autoInit' in opts || 'override' in opts); -} - -function filterValidOptions(opts: any, preferenceDeclaration?: PluginDeclaration) { - if (!opts || !isPlainObject(opts)) return opts; - const filteredOpts = {} as any; - Object.keys(opts).forEach((key) => { - if (isValidPreferenceKey(key, preferenceDeclaration)) { - const v = opts[key]; - if (v !== undefined && v !== null) { - filteredOpts[key] = v; - } - } - }); - return filteredOpts; -} diff --git a/packages/engine-core/src/plugin/runtime.ts b/packages/engine-core/src/plugin/runtime.ts deleted file mode 100644 index 75da5185e..000000000 --- a/packages/engine-core/src/plugin/runtime.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { PluginManager } from './manager'; -import { type PluginInstance, type PluginMeta } from './types'; -import { invariant, createLogger, type Logger } from '@alilc/lowcode-shared'; - -export interface PluginRuntimeExportsAccessor { - [propName: string]: any; -} - -export class PluginRuntime> { - #inited: boolean; - /** - * 标识插件状态,是否被 disabled - */ - #disabled: boolean; - - #logger: Logger; - - constructor( - private pluginName: string, - private manager: PluginManager, - public instance: PluginInstance, - public meta: PluginMeta, - ) { - this.#logger = createLogger({ level: 'warn', bizName: `plugin:${pluginName}` }); - } - - get name() { - return this.pluginName; - } - - get dep() { - if (typeof this.meta.dependencies === 'string') { - return [this.meta.dependencies]; - } - - return this.meta.dependencies || []; - } - - get disabled() { - return this.#disabled; - } - - isInited() { - return this.#inited; - } - - async init(forceInit?: boolean) { - if (this.#inited && !forceInit) return; - this.#logger.log('method init called'); - await this.instance.init?.call(undefined); - this.#inited = true; - } - - async destroy() { - if (!this.#inited) return; - this.#logger.log('method destroy called'); - await this.instance?.destroy?.call(undefined); - this.#inited = false; - } - - setDisabled(flag = true) { - this.#disabled = flag; - } - - toProxy(): PluginRuntimeExportsAccessor { - invariant(this.#inited, 'Could not call toProxy before init'); - - const exports = this.instance.exports?.(); - return new Proxy(this, { - get(target, prop, receiver) { - if ({}.hasOwnProperty.call(exports, prop)) { - return exports?.[prop as string]; - } - return Reflect.get(target, prop, receiver); - }, - }); - } - - async dispose() { - await this.manager.delete(this.name); - } -} diff --git a/packages/engine-core/src/plugin/types.ts b/packages/engine-core/src/plugin/types.ts deleted file mode 100644 index 1f87e2047..000000000 --- a/packages/engine-core/src/plugin/types.ts +++ /dev/null @@ -1,75 +0,0 @@ -export interface PluginInstance { - init(): Promise | void; - destroy?(): Promise | void; - exports?(): any; -} - -export interface PluginMeta { - /** - * define dependencies which the plugin depends on - */ - dependencies?: string[]; - - /** - * specify which engine version is compatible with the plugin - * todo: unified engines naming rules - */ - engines?: { - /** e.g. '^1.0.0' */ - lowcodeEngine?: string; - }; - - preferenceDeclaration?: PluginDeclaration; - - /** - * use 'common' as event prefix when eventPrefix is not set. - * strongly recommend using pluginName as eventPrefix - * - * eg. - * case 1, when eventPrefix is not specified - * event.emit('someEventName') is actually sending event with name 'common:someEventName' - * - * case 2, when eventPrefix is 'myEvent' - * event.emit('someEventName') is actually sending event with name 'myEvent:someEventName' - */ - eventPrefix?: string; - - /** - * 如果要使用 command 注册命令,需要在插件 meta 中定义 commandScope - */ - commandScope?: string; -} - -export interface PluginDeclaration { - // this will be displayed on configuration UI, can be plugin name - title: string; - properties: PluginDeclarationProperty[]; -} - -export interface PluginDeclarationProperty { - // shape like 'name' or 'group.name' or 'group.subGroup.name' - key: string; - // must have either one of description & markdownDescription - description: string; - // value in 'number', 'string', 'boolean' - type: string; - // default value - // NOTE! this is only used in configuration UI, won`t affect runtime - default?: PluginPreferenceValue; - // only works when type === 'string', default value false - useMultipleLineTextInput?: boolean; - // enum values, only works when type === 'string' - enum?: any[]; - // descriptions for enum values - enumDescriptions?: string[]; - // message that describing deprecation of this property - deprecationMessage?: string; -} - -export type PluginPreferenceValue = string | number | boolean; - -export interface PluginCreater { - (ctx: Context, options: any): PluginInstance; - pluginName: string; - meta?: PluginMeta; -} diff --git a/packages/engine-core/src/plugin/utils.ts b/packages/engine-core/src/plugin/utils.ts deleted file mode 100644 index 119ac6167..000000000 --- a/packages/engine-core/src/plugin/utils.ts +++ /dev/null @@ -1,72 +0,0 @@ -interface TaskMap { - [key: string]: { - name: string; - dep: string[]; - }; -} - -interface Options { - tasks: TaskMap; - names: string[]; - results: string[]; - missing: string[]; - recursive: string[][]; - nest: string[]; - parentName: string; -} - -export function sequence({ tasks, names, results, missing, recursive, nest, parentName }: Options) { - names.forEach((name) => { - if (results.indexOf(name) !== -1) { - return; // de-dup results - } - const node = tasks[name]; - if (!node) { - missing.push([parentName, name].filter((d) => !!d).join('.')); - } else if (nest.indexOf(name) > -1) { - nest.push(name); - recursive.push(nest.slice(0)); - nest.pop(); - } else if (node.dep.length) { - nest.push(name); - sequence({ - tasks, - parentName: name, - names: node.dep, - results, - missing, - recursive, - nest, - }); // recurse - nest.pop(); - } - results.push(name); - }); -} - -// tasks: object with keys as task names -// names: array of task names -export function sequencify(tasks: TaskMap, names: string[]) { - let results: string[] = []; // the final sequence - const missing: string[] = []; // missing tasks - const recursive: string[][] = []; // recursive task dependencies - - sequence({ - tasks, - names, - results, - missing, - recursive, - nest: [], - } as any); - - if (missing.length || recursive.length) { - results = []; // results are incomplete at best, completely wrong at worst, remove them to avoid confusion - } - - return { - sequence: results, - missingTasks: missing, - recursiveDependencies: recursive, - }; -} diff --git a/packages/engine-core/src/resource.ts b/packages/engine-core/src/resource.ts deleted file mode 100644 index eebb79a04..000000000 --- a/packages/engine-core/src/resource.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Assets } from '@alilc/lowcode-shared'; - -export interface IResourceManagementService { - setAssets(assets: Assets): void; -} diff --git a/packages/engine-core/src/resource/index.ts b/packages/engine-core/src/resource/index.ts new file mode 100644 index 000000000..946cdcf5b --- /dev/null +++ b/packages/engine-core/src/resource/index.ts @@ -0,0 +1 @@ +export * from './resourceService'; diff --git a/packages/engine-core/src/resource/resourceModel.ts b/packages/engine-core/src/resource/resourceModel.ts new file mode 100644 index 000000000..8879af8ff --- /dev/null +++ b/packages/engine-core/src/resource/resourceModel.ts @@ -0,0 +1,61 @@ +import { type Package, mapPackageToUniqueId } from '@alilc/lowcode-shared'; + +export class ResourceModel { + private packagesRef: Package[] = []; + private idToPackageMap: Map = new Map(); + private packageToLibraryMap: WeakMap = new WeakMap(); + + addOne(pkg: Package): string { + const id = mapPackageToUniqueId(pkg); + + if (!this.idToPackageMap.has(id)) { + this.idToPackageMap.set(id, pkg); + this.packagesRef.push(pkg); + } + + return id; + } + + add(packages: Package[]): string[] { + const ids: string[] = []; + + for (const pkg of packages) { + const id = this.addOne(pkg); + if (id) ids.push(id); + } + + return ids; + } + + getById(id: string): Package | undefined { + return this.idToPackageMap.get(id); + } + + has(id: string): boolean { + return this.idToPackageMap.has(id); + } + + getPackages(): Package[] { + return [...this.packagesRef]; + } + + delete(id: string): void { + const pkg = this.idToPackageMap.get(id); + if (pkg) { + this.packagesRef = this.packagesRef.filter((p) => p !== pkg); + this.packageToLibraryMap.delete(pkg); + this.idToPackageMap.delete(id); + } + } + + setPackageLibrary(id: string, library: any): void { + // 转换成内部的引用 + const refedPackage = this.idToPackageMap.get(id); + if (refedPackage) this.packageToLibraryMap.set(refedPackage, library); + } + + getPackageLibrary(id: string): T | undefined { + const refedPackage = this.idToPackageMap.get(id); + if (refedPackage) return this.packageToLibraryMap.get(refedPackage); + } +} diff --git a/packages/engine-core/src/resource/resourceService.ts b/packages/engine-core/src/resource/resourceService.ts new file mode 100644 index 000000000..a1099f803 --- /dev/null +++ b/packages/engine-core/src/resource/resourceService.ts @@ -0,0 +1,51 @@ +import { + createDecorator, + Provide, + type Package, + type Reference, + mapPackageToUniqueId, + exportByReference, +} from '@alilc/lowcode-shared'; +import { ResourceModel } from './resourceModel'; + +export interface IResourceService { + loadPackage(schema: Package): Promise; + + loadPackages(schemas: Package[]): Promise; + + getByReference(reference: Reference): T | undefined; + + getPackages(idOrName: string): Package[] | undefined; + + getAllPackages(): Package[]; +} + +export const IResourceService = createDecorator('resourceService'); + +@Provide(IResourceService) +export class ResourceService implements IResourceService { + private resourceModel = new ResourceModel(); + + loadPackage(pkg: Package): Promise { + return this.loadPackages([pkg]); + } + + async loadPackages(packags: Package[]): Promise {} + + getByReference(reference: Reference): T | undefined { + const id = mapPackageToUniqueId(reference); + const library = this.resourceModel.getPackageLibrary(id); + + return exportByReference(library, reference); + } + + getPackages(idOrName: string): Package[] | undefined { + return this.resourceModel + .getPackages() + .filter((pkg) => pkg.id === idOrName || pkg.package === idOrName); + } + + getAllPackages(): Package[] { + return this.resourceModel.getPackages(); + } +} diff --git a/packages/engine-core/src/workbench/index.ts b/packages/engine-core/src/workbench/index.ts new file mode 100644 index 000000000..3e3ca64a5 --- /dev/null +++ b/packages/engine-core/src/workbench/index.ts @@ -0,0 +1 @@ +export * from './workbenchService'; diff --git a/packages/engine-core/src/workbench/layout/layout.ts b/packages/engine-core/src/workbench/layout/layout.ts new file mode 100644 index 000000000..727720d13 --- /dev/null +++ b/packages/engine-core/src/workbench/layout/layout.ts @@ -0,0 +1,28 @@ +import { Extensions, Registry } from '../../extension/extension'; +import { IWidgetRegistry } from '../widget/widgetRegistry'; + +export const enum LayoutParts { + TopBar = 1, + SideBar, + BottomBar, + ActionBar, + Main, + AuxiliaryPanel, +} + +export interface ILayout { + /** + * Main container of the application. + */ + mainContainer: HTMLElement; + + registerPart(part: LayoutParts): void; +} + +export class Layout implements ILayout { + constructor(public mainContainer: HTMLElement) { + Registry.as>(Extensions.Widget).onDidRegister(() => {}); + } + + registerPart(part: LayoutParts): void {} +} diff --git a/packages/engine-core/src/workbench/layout/layoutService.ts b/packages/engine-core/src/workbench/layout/layoutService.ts index 4bac019e7..727c1efad 100644 --- a/packages/engine-core/src/workbench/layout/layoutService.ts +++ b/packages/engine-core/src/workbench/layout/layoutService.ts @@ -1,6 +1,7 @@ +import { createDecorator } from '@alilc/lowcode-shared'; + export interface ILayoutService { - /** - * Main container of the application. - */ - mainContainer: HTMLElement; + layout(): void; } + +export const ILayoutService = createDecorator('layoutService'); diff --git a/packages/plugin-command/vitest.config.ts b/packages/engine-core/src/workbench/parts/action/index.ts similarity index 100% rename from packages/plugin-command/vitest.config.ts rename to packages/engine-core/src/workbench/parts/action/index.ts diff --git a/packages/plugin-designer/vitest.config.ts b/packages/engine-core/src/workbench/parts/sideBar/sideBar.ts similarity index 100% rename from packages/plugin-designer/vitest.config.ts rename to packages/engine-core/src/workbench/parts/sideBar/sideBar.ts diff --git a/packages/plugin-outline-pane/vitest.config.ts b/packages/engine-core/src/workbench/parts/topBar/index.ts similarity index 100% rename from packages/plugin-outline-pane/vitest.config.ts rename to packages/engine-core/src/workbench/parts/topBar/index.ts diff --git a/packages/engine-core/src/workbench/widget/widget.ts b/packages/engine-core/src/workbench/widget/widget.ts new file mode 100644 index 000000000..788550984 --- /dev/null +++ b/packages/engine-core/src/workbench/widget/widget.ts @@ -0,0 +1,25 @@ +import { LayoutParts } from '../layout/layout'; + +export interface IWidget { + readonly id: string; + content: View; + action: any; // bind command action + target: LayoutParts; + metadata: IWidgetMetadata; +} + +export interface IWidgetMetadata { + title?: string; + icon?: string; + priority?: number; +} + +export class Widget implements IWidget { + constructor( + public readonly id: string, + public readonly target: LayoutParts, + public readonly content: View, + public readonly action: any, + public readonly metadata: IWidgetMetadata = {}, + ) {} +} diff --git a/packages/engine-core/src/workbench/widget/widgetModel.ts b/packages/engine-core/src/workbench/widget/widgetModel.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/engine-core/src/workbench/widget/widgetRegistry.ts b/packages/engine-core/src/workbench/widget/widgetRegistry.ts new file mode 100644 index 000000000..9213f9852 --- /dev/null +++ b/packages/engine-core/src/workbench/widget/widgetRegistry.ts @@ -0,0 +1,37 @@ +import { type Event, type EventListener, Emitter } from '@alilc/lowcode-shared'; +import { IWidget } from './widget'; +import { Extensions, Registry } from '../../extension/extension'; + +export interface IWidgetRegistry { + onDidRegister: Event[]>; + + registerWidget(widget: IWidget): string; + + registerWidgets(widgets: IWidget[]): string[]; + + getWidgets(): IWidget[]; +} + +export class WidgetRegistry implements IWidgetRegistry { + private _widgets: Map> = new Map(); + + private emitter = new Emitter[]>(); + + onDidRegister(fn: EventListener[]>) { + return this.emitter.on(fn); + } + + getWidgets(): IWidget[] { + return Array.from(this._widgets.values()); + } + + registerWidget(widget: IWidget): string { + return widget.id; + } + + registerWidgets(widgets: IWidget[]): string[] { + return widgets.map((widget) => this.registerWidget(widget)); + } +} + +Registry.add(Extensions.Widget, new WidgetRegistry()); diff --git a/packages/engine-core/src/workbench/workbenchService.ts b/packages/engine-core/src/workbench/workbenchService.ts new file mode 100644 index 000000000..1b8ebd217 --- /dev/null +++ b/packages/engine-core/src/workbench/workbenchService.ts @@ -0,0 +1,14 @@ +import { createDecorator, Provide } from '@alilc/lowcode-shared'; + +export interface IWorkbenchService { + initialize(): void; +} + +export const IWorkbenchService = createDecorator('workbenchService'); + +@Provide(IWorkbenchService) +export class WorkbenchService implements IWorkbenchService { + initialize(): void { + console.log('workbench service'); + } +} diff --git a/packages/engine-core/src/workspace/window/windowService.ts b/packages/engine-core/src/workspace/window/windowService.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/engine-core/src/workspace/workspace.ts b/packages/engine-core/src/workspace/workspace.ts index 13811bc3b..d90b0aeb3 100644 --- a/packages/engine-core/src/workspace/workspace.ts +++ b/packages/engine-core/src/workspace/workspace.ts @@ -1,12 +1,4 @@ -import { createDecorator, Provide } from '@alilc/lowcode-shared'; - -export interface IWorkspaceService { - mount(container: HTMLElement): void; -} - -export const IWorkspaceService = createDecorator('workspaceService'); - -@Provide(IWorkspaceService) -export class WorkspaceService implements IWorkspaceService { - mount(container: HTMLElement): void {} -} +/** + * 工作空间:一个或多个项目的集合 + */ +export interface Workspace {} diff --git a/packages/engine-core/src/workspace/workspaceService.ts b/packages/engine-core/src/workspace/workspaceService.ts new file mode 100644 index 000000000..13811bc3b --- /dev/null +++ b/packages/engine-core/src/workspace/workspaceService.ts @@ -0,0 +1,12 @@ +import { createDecorator, Provide } from '@alilc/lowcode-shared'; + +export interface IWorkspaceService { + mount(container: HTMLElement): void; +} + +export const IWorkspaceService = createDecorator('workspaceService'); + +@Provide(IWorkspaceService) +export class WorkspaceService implements IWorkspaceService { + mount(container: HTMLElement): void {} +} diff --git a/packages/plugin-command/README.md b/packages/plugin-command/README.md deleted file mode 100644 index 8476b47e5..000000000 --- a/packages/plugin-command/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `@alilc/plugin-command` - -> TODO: description - -## Usage - -``` -const pluginCommand = require('@alilc/plugin-command'); - -// TODO: DEMONSTRATE API -``` diff --git a/packages/plugin-command/__tests__/node-command.test.ts b/packages/plugin-command/__tests__/node-command.test.ts deleted file mode 100644 index 2e9d21b35..000000000 --- a/packages/plugin-command/__tests__/node-command.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { checkPropTypes } from '@alilc/lowcode-utils/src/check-prop-types'; -import { nodeSchemaPropType } from '../src/node-command'; - -describe('nodeSchemaPropType', () => { - const componentName = 'NodeComponent'; - const getPropType = (name: string) => nodeSchemaPropType.value.find(d => d.name === name)?.propType; - - it('should validate the id as a string', () => { - const validId = 'node1'; - const invalidId = 123; // Not a string - expect(checkPropTypes(validId, 'id', getPropType('id'), componentName)).toBe(true); - expect(checkPropTypes(invalidId, 'id', getPropType('id'), componentName)).toBe(false); - // is not required - expect(checkPropTypes(undefined, 'id', getPropType('id'), componentName)).toBe(true); - }); - - it('should validate the componentName as a string', () => { - const validComponentName = 'Button'; - const invalidComponentName = false; // Not a string - expect(checkPropTypes(validComponentName, 'componentName', getPropType('componentName'), componentName)).toBe(true); - expect(checkPropTypes(invalidComponentName, 'componentName', getPropType('componentName'), componentName)).toBe(false); - // isRequired - expect(checkPropTypes(undefined, 'componentName', getPropType('componentName'), componentName)).toBe(false); - }); - - it('should validate the props as an object', () => { - const validProps = { key: 'value' }; - const invalidProps = 'Not an object'; // Not an object - expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); - expect(checkPropTypes(invalidProps, 'props', getPropType('props'), componentName)).toBe(false); - }); - - it('should validate the props as a JSExpression', () => { - const validProps = { type: 'JSExpression', value: 'props' }; - expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); - }); - - it('should validate the props as a JSFunction', () => { - const validProps = { type: 'JSFunction', value: 'props' }; - expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); - }); - - it('should validate the props as a JSSlot', () => { - const validProps = { type: 'JSSlot', value: 'props' }; - expect(checkPropTypes(validProps, 'props', getPropType('props'), componentName)).toBe(true); - }); - - it('should validate the condition as a bool', () => { - const validCondition = true; - const invalidCondition = 'Not a bool'; // Not a boolean - expect(checkPropTypes(validCondition, 'condition', getPropType('condition'), componentName)).toBe(true); - expect(checkPropTypes(invalidCondition, 'condition', getPropType('condition'), componentName)).toBe(false); - }); - - it('should validate the condition as a JSExpression', () => { - const validCondition = { type: 'JSExpression', value: '1 + 1 === 2' }; - const invalidCondition = { type: 'JSExpression', value: 123 }; // Not a string - expect(checkPropTypes(validCondition, 'condition', getPropType('condition'), componentName)).toBe(true); - expect(checkPropTypes(invalidCondition, 'condition', getPropType('condition'), componentName)).toBe(false); - }); - - it('should validate the loop as an array', () => { - const validLoop = ['item1', 'item2']; - const invalidLoop = 'Not an array'; // Not an array - expect(checkPropTypes(validLoop, 'loop', getPropType('loop'), componentName)).toBe(true); - expect(checkPropTypes(invalidLoop, 'loop', getPropType('loop'), componentName)).toBe(false); - }); - - it('should validate the loop as a JSExpression', () => { - const validLoop = { type: 'JSExpression', value: 'items' }; - const invalidLoop = { type: 'JSExpression', value: 123 }; // Not a string - expect(checkPropTypes(validLoop, 'loop', getPropType('loop'), componentName)).toBe(true); - expect(checkPropTypes(invalidLoop, 'loop', getPropType('loop'), componentName)).toBe(false); - }); - - it('should validate the loopArgs as an array', () => { - const validLoopArgs = ['item']; - const invalidLoopArgs = 'Not an array'; // Not an array - expect(checkPropTypes(validLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(true); - expect(checkPropTypes(invalidLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(false); - }); - - it('should validate the loopArgs as a JSExpression', () => { - const validLoopArgs = { type: 'JSExpression', value: 'item' }; - const invalidLoopArgs = { type: 'JSExpression', value: 123 }; // Not a string - const validLoopArgs2 = [{ type: 'JSExpression', value: 'item' }, { type: 'JSExpression', value: 'index' }]; - expect(checkPropTypes(validLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(true); - expect(checkPropTypes(invalidLoopArgs, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(false); - expect(checkPropTypes(validLoopArgs2, 'loopArgs', getPropType('loopArgs'), componentName)).toBe(true); - }); - - it('should validate the children as an array', () => { - const validChildren = [{ - id: 'child1', - componentName: 'Button', - }, { - id: 'child2', - componentName: 'Button', - }]; - const invalidChildren = 'Not an array'; // Not an array - const invalidChildren2 = [{}]; // Not an valid array - expect(checkPropTypes(invalidChildren, 'children', getPropType('children'), componentName)).toBe(false); - expect(checkPropTypes(validChildren, 'children', getPropType('children'), componentName)).toBe(true); - expect(checkPropTypes(invalidChildren2, 'children', getPropType('children'), componentName)).toBe(false); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); -}); diff --git a/packages/plugin-command/package.json b/packages/plugin-command/package.json deleted file mode 100644 index 13fe0f254..000000000 --- a/packages/plugin-command/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@alilc/lowcode-plugin-command", - "version": "2.0.0-beta.0", - "description": "> TODO: description", - "author": "liujuping ", - "homepage": "https://github.com/alibaba/lowcode-engine#readme", - "license": "ISC", - "type": "module", - "private": true, - "main": "dist/low-code-plugin-command.cjs", - "module": "dist/low-code-plugin-command.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/low-code-plugin-command.js", - "require": "./dist/low-code-plugin-command.cjs", - "types": "./dist/index.d.ts" - } - }, - "files": [ - "dist", - "src", - "package.json" - ], - "sideEffects": [ - "*.css" - ], - "scripts": { - "build:target": "vite build", - "build:dts": "tsc -p tsconfig.declaration.json && node ../../scripts/rollup-dts.mjs", - "test": "vitest" - }, - "bugs": { - "url": "https://github.com/alibaba/lowcode-engine/issues" - }, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/alibaba/lowcode-engine.git" - } -} diff --git a/packages/plugin-command/src/history-command.ts b/packages/plugin-command/src/history-command.ts deleted file mode 100644 index ea7e491bc..000000000 --- a/packages/plugin-command/src/history-command.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { IPublicModelPluginContext, IPublicTypePlugin } from '@alilc/lowcode-types'; - -export const historyCommand: IPublicTypePlugin = (ctx: IPublicModelPluginContext) => { - const { command, project } = ctx; - return { - init() { - command.registerCommand({ - name: 'undo', - description: 'Undo the last operation.', - handler: () => { - const state = project.currentDocument?.history.getState() || 0; - const enable = !!(state & 1); - if (!enable) { - throw new Error('Can not undo.'); - } - project.currentDocument?.history.back(); - }, - }); - - command.registerCommand({ - name: 'redo', - description: 'Redo the last operation.', - handler: () => { - const state = project.currentDocument?.history.getState() || 0; - const enable = !!(state & 2); - if (!enable) { - throw new Error('Can not redo.'); - } - project.currentDocument?.history.forward(); - }, - }); - }, - destroy() { - command.unregisterCommand('history:undo'); - command.unregisterCommand('history:redo'); - }, - }; -}; - -historyCommand.pluginName = '___history_command___'; -historyCommand.meta = { - commandScope: 'history', -}; diff --git a/packages/plugin-command/src/index.ts b/packages/plugin-command/src/index.ts deleted file mode 100644 index 0264342a6..000000000 --- a/packages/plugin-command/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { IPublicModelPluginContext, IPublicTypePlugin } from '@alilc/lowcode-types'; -import { nodeCommand } from './node-command'; -import { historyCommand } from './history-command'; - -export const CommandPlugin: IPublicTypePlugin = (ctx: IPublicModelPluginContext) => { - const { plugins } = ctx; - - return { - async init() { - await plugins.register(nodeCommand, {}, { autoInit: true }); - await plugins.register(historyCommand, {}, { autoInit: true }); - }, - destroy() { - plugins.delete(nodeCommand.pluginName); - plugins.delete(historyCommand.pluginName); - }, - }; -}; - -CommandPlugin.pluginName = '___default_command___'; -CommandPlugin.meta = { - commandScope: 'common', -}; - -export default CommandPlugin; diff --git a/packages/plugin-command/src/node-command.ts b/packages/plugin-command/src/node-command.ts deleted file mode 100644 index eeda1d168..000000000 --- a/packages/plugin-command/src/node-command.ts +++ /dev/null @@ -1,497 +0,0 @@ -import { IPublicModelPluginContext, IPublicTypeNodeSchema, IPublicTypePlugin, IPublicTypePropType } from '@alilc/lowcode-types'; -import { isNodeSchema } from '@alilc/lowcode-utils'; - -const sampleNodeSchema: IPublicTypePropType = { - type: 'shape', - value: [ - { - name: 'id', - propType: 'string', - }, - { - name: 'componentName', - propType: { - type: 'string', - isRequired: true, - }, - }, - { - name: 'props', - propType: 'object', - }, - { - name: 'condition', - propType: 'any', - }, - { - name: 'loop', - propType: 'any', - }, - { - name: 'loopArgs', - propType: 'any', - }, - { - name: 'children', - propType: 'any', - }, - ], -}; - -export const nodeSchemaPropType: IPublicTypePropType = { - type: 'shape', - value: [ - sampleNodeSchema.value[0], - sampleNodeSchema.value[1], - { - name: 'props', - propType: { - type: 'objectOf', - value: { - type: 'oneOfType', - // 不会强制校验,更多作为提示 - value: [ - 'any', - { - type: 'shape', - value: [ - { - name: 'type', - propType: { - type: 'oneOf', - value: ['JSExpression'], - }, - }, - { - name: 'value', - propType: 'string', - }, - ], - }, - { - type: 'shape', - value: [ - { - name: 'type', - propType: { - type: 'oneOf', - value: ['JSFunction'], - }, - }, - { - name: 'value', - propType: 'string', - }, - ], - }, - { - type: 'shape', - value: [ - { - name: 'type', - propType: { - type: 'oneOf', - value: ['JSSlot'], - }, - }, - { - name: 'value', - propType: { - type: 'oneOfType', - value: [ - sampleNodeSchema, - { - type: 'arrayOf', - value: sampleNodeSchema, - }, - ], - }, - }, - ], - }, - ], - }, - }, - }, - { - name: 'condition', - propType: { - type: 'oneOfType', - value: [ - 'bool', - { - type: 'shape', - value: [ - { - name: 'type', - propType: { - type: 'oneOf', - value: ['JSExpression'], - }, - }, - { - name: 'value', - propType: 'string', - }, - ], - }, - ], - }, - }, - { - name: 'loop', - propType: { - type: 'oneOfType', - value: [ - 'array', - { - type: 'shape', - value: [ - { - name: 'type', - propType: { - type: 'oneOf', - value: ['JSExpression'], - }, - }, - { - name: 'value', - propType: 'string', - }, - ], - }, - ], - }, - }, - { - name: 'loopArgs', - propType: { - type: 'oneOfType', - value: [ - { - type: 'arrayOf', - value: { - type: 'oneOfType', - value: [ - 'any', - { - type: 'shape', - value: [ - { - name: 'type', - propType: { - type: 'oneOf', - value: ['JSExpression'], - }, - }, - { - name: 'value', - propType: 'string', - }, - ], - }, - ], - }, - }, - { - type: 'shape', - value: [ - { - name: 'type', - propType: { - type: 'oneOf', - value: ['JSExpression'], - }, - }, - { - name: 'value', - propType: 'string', - }, - ], - }, - ], - }, - }, - { - name: 'children', - propType: { - type: 'arrayOf', - value: sampleNodeSchema, - }, - }, - ], -}; - -export const nodeCommand: IPublicTypePlugin = (ctx: IPublicModelPluginContext) => { - const { command, project } = ctx; - return { - init() { - command.registerCommand({ - name: 'add', - description: 'Add a node to the canvas.', - handler: (param: { - parentNodeId: string; - nodeSchema: IPublicTypeNodeSchema; - index: number; - }) => { - const { - parentNodeId, - nodeSchema, - index, - } = param; - const { project } = ctx; - const parentNode = project.currentDocument?.getNodeById(parentNodeId); - if (!parentNode) { - throw new Error(`Can not find node '${parentNodeId}'.`); - } - - if (!parentNode.isContainerNode) { - throw new Error(`Node '${parentNodeId}' is not a container node.`); - } - - if (!isNodeSchema(nodeSchema)) { - throw new Error('Invalid node.'); - } - - if (index < 0 || index > (parentNode.children?.size || 0)) { - throw new Error(`Invalid index '${index}'.`); - } - - project.currentDocument?.insertNode(parentNode, nodeSchema, index); - }, - parameters: [ - { - name: 'parentNodeId', - propType: 'string', - description: 'The id of the parent node.', - }, - { - name: 'nodeSchema', - propType: nodeSchemaPropType, - description: 'The node to be added.', - }, - { - name: 'index', - propType: 'number', - description: 'The index of the node to be added.', - }, - ], - }); - - command.registerCommand({ - name: 'move', - description: 'Move a node to another node.', - handler(param: { - nodeId: string; - targetNodeId: string; - index: number; - }) { - const { - nodeId, - targetNodeId, - index = 0, - } = param; - - if (!nodeId) { - throw new Error('Invalid node id.'); - } - - if (!targetNodeId) { - throw new Error('Invalid target node id.'); - } - - const node = project.currentDocument?.getNodeById(nodeId); - const targetNode = project.currentDocument?.getNodeById(targetNodeId); - if (!node) { - throw new Error(`Can not find node '${nodeId}'.`); - } - - if (!targetNode) { - throw new Error(`Can not find node '${targetNodeId}'.`); - } - - if (!targetNode.isContainerNode) { - throw new Error(`Node '${targetNodeId}' is not a container node.`); - } - - if (index < 0 || index > (targetNode.children?.size || 0)) { - throw new Error(`Invalid index '${index}'.`); - } - - project.currentDocument?.removeNode(node); - project.currentDocument?.insertNode(targetNode, node, index); - }, - parameters: [ - { - name: 'nodeId', - propType: { - type: 'string', - isRequired: true, - }, - description: 'The id of the node to be moved.', - }, - { - name: 'targetNodeId', - propType: { - type: 'string', - isRequired: true, - }, - description: 'The id of the target node.', - }, - { - name: 'index', - propType: 'number', - description: 'The index of the node to be moved.', - }, - ], - }); - - command.registerCommand({ - name: 'remove', - description: 'Remove a node from the canvas.', - handler(param: { - nodeId: string; - }) { - const { - nodeId, - } = param; - - const node = project.currentDocument?.getNodeById(nodeId); - if (!node) { - throw new Error(`Can not find node '${nodeId}'.`); - } - - project.currentDocument?.removeNode(node); - }, - parameters: [ - { - name: 'nodeId', - propType: 'string', - description: 'The id of the node to be removed.', - }, - ], - }); - - command.registerCommand({ - name: 'update', - description: 'Update a node.', - handler(param: { - nodeId: string; - nodeSchema: IPublicTypeNodeSchema; - }) { - const { - nodeId, - nodeSchema, - } = param; - - const node = project.currentDocument?.getNodeById(nodeId); - if (!node) { - throw new Error(`Can not find node '${nodeId}'.`); - } - - if (!isNodeSchema(nodeSchema)) { - throw new Error('Invalid node.'); - } - - node.importSchema(nodeSchema); - }, - parameters: [ - { - name: 'nodeId', - propType: 'string', - description: 'The id of the node to be updated.', - }, - { - name: 'nodeSchema', - propType: nodeSchemaPropType, - description: 'The node to be updated.', - }, - ], - }); - - command.registerCommand({ - name: 'updateProps', - description: 'Update the properties of a node.', - handler(param: { - nodeId: string; - props: Record; - }) { - const { - nodeId, - props, - } = param; - - const node = project.currentDocument?.getNodeById(nodeId); - if (!node) { - throw new Error(`Can not find node '${nodeId}'.`); - } - - Object.keys(props).forEach(key => { - node.setPropValue(key, props[key]); - }); - }, - parameters: [ - { - name: 'nodeId', - propType: 'string', - description: 'The id of the node to be updated.', - }, - { - name: 'props', - propType: 'object', - description: 'The properties to be updated.', - }, - ], - }); - - command.registerCommand({ - name: 'removeProps', - description: 'Remove the properties of a node.', - handler(param: { - nodeId: string; - propNames: string[]; - }) { - const { - nodeId, - propNames, - } = param; - - const node = project.currentDocument?.getNodeById(nodeId); - if (!node) { - throw new Error(`Can not find node '${nodeId}'.`); - } - - propNames.forEach(key => { - node.props?.getProp(key)?.remove(); - }); - }, - parameters: [ - { - name: 'nodeId', - propType: 'string', - description: 'The id of the node to be updated.', - }, - { - name: 'propNames', - propType: 'array', - description: 'The properties to be removed.', - }, - ], - }); - }, - destroy() { - command.unregisterCommand('node:add'); - command.unregisterCommand('node:move'); - command.unregisterCommand('node:remove'); - command.unregisterCommand('node:update'); - command.unregisterCommand('node:updateProps'); - command.unregisterCommand('node:removeProps'); - }, - }; -}; - -nodeCommand.pluginName = '___node_command___'; -nodeCommand.meta = { - commandScope: 'node', -}; - diff --git a/packages/plugin-command/tsconfig.declaration.json b/packages/plugin-command/tsconfig.declaration.json deleted file mode 100644 index 54e57efbd..000000000 --- a/packages/plugin-command/tsconfig.declaration.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "emitDeclarationOnly": true, - "declaration": true, - "outDir": "temp", - "stripInternal": true, - "paths": {} - } -} diff --git a/packages/plugin-command/tsconfig.json b/packages/plugin-command/tsconfig.json deleted file mode 100644 index 039e0b4d1..000000000 --- a/packages/plugin-command/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src"] -} diff --git a/packages/plugin-command/vite.config.ts b/packages/plugin-command/vite.config.ts deleted file mode 100644 index de443bcd1..000000000 --- a/packages/plugin-command/vite.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vite'; -import baseConfigFn from '../../vite.base.config' - -export default defineConfig(async () => { - return baseConfigFn({ - name: 'LowCodePluginCommand', - defaultFormats: ['es', 'cjs'] - }) -}); diff --git a/packages/plugin-designer/.gitignore b/packages/plugin-designer/.gitignore deleted file mode 100644 index 2073a217c..000000000 --- a/packages/plugin-designer/.gitignore +++ /dev/null @@ -1,106 +0,0 @@ -# project custom -build -dist -packages/*/lib/ -packages/*/es/ -packages/*/dist/ -packages/*/output/ -package-lock.json -yarn.lock -deploy-space/packages -deploy-space/.env - - -# IDE -.vscode -.idea - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release -lib - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# next.js build output -.next - -# nuxt.js build output -.nuxt - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# mac config files -.DS_Store - -# codealike -codealike.json diff --git a/packages/plugin-designer/build.json b/packages/plugin-designer/build.json deleted file mode 100644 index 3e9260055..000000000 --- a/packages/plugin-designer/build.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": [ - "@alilc/build-plugin-lce" - ] -} diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json deleted file mode 100644 index 20e102f2d..000000000 --- a/packages/plugin-designer/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@alilc/lowcode-plugin-designer", - "version": "2.0.0-beta.0", - "description": "alibaba lowcode editor designer plugin", - "private": true, - "type": "module", - "main": "dist/low-code-plugin-designer.cjs", - "module": "dist/low-code-plugin-designer.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/low-code-plugin-designer.js", - "require": "./dist/low-code-plugin-designer.cjs", - "types": "./dist/index.d.ts" - }, - "./dist/": { - "import": "./dist/", - "require": "./dist/" - } - }, - "files": [ - "dist", - "src", - "package.json" - ], - "scripts": { - "build:target": "vite build", - "build:dts": "tsc -p tsconfig.declaration.json && node ../../scripts/rollup-dts.mjs", - "test": "vitest" - }, - "keywords": [ - "lowcode", - "editor" - ], - "author": "xiayang.xy", - "dependencies": { - "@alilc/lowcode-designer": "workspace:*", - "@alilc/lowcode-engine-core": "workspace:*", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0" - }, - "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "@alilc/lowcode-designer": "workspace:*", - "@alilc/lowcode-engine-core": "workspace:*" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "http", - "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/plugin-designer" - }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", - "bugs": "https://github.com/alibaba/lowcode-engine/issues", - "homepage": "https://github.com/alibaba/lowcode-engine/#readme" -} diff --git a/packages/plugin-designer/src/index.scss b/packages/plugin-designer/src/index.scss deleted file mode 100644 index 395bb8961..000000000 --- a/packages/plugin-designer/src/index.scss +++ /dev/null @@ -1,4 +0,0 @@ -.lowcode-plugin-designer { - width: 100%; - height: 100%; -} diff --git a/packages/plugin-designer/src/index.tsx b/packages/plugin-designer/src/index.tsx deleted file mode 100644 index 00ce969cf..000000000 --- a/packages/plugin-designer/src/index.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { PureComponent } from 'react'; -import { Editor, engineConfig } from '@alilc/lowcode-editor-core'; -import { DesignerView, Designer } from '@alilc/lowcode-designer'; -import { Asset, createLogger } from '@alilc/lowcode-utils'; -import './index.scss'; - -const logger = createLogger({ level: 'warn', bizName: 'plugin:plugin-designer' }); - -export interface PluginProps { - engineEditor: Editor; - // ?? - engineConfig?: any; -} - -interface DesignerPluginState { - componentMetadatas?: any[] | null; - library?: any[] | null; - utilsMetadata?: any[] | null; - extraEnvironment?: any[] | null; - renderEnv?: string; - device?: string; - locale?: string; - designMode?: string; - deviceClassName?: string; - simulatorUrl: Asset | null; - // @TODO 类型定义 - requestHandlersMap: any; -} - -export default class DesignerPlugin extends PureComponent { - static displayName: 'LowcodePluginDesigner'; - - state: DesignerPluginState = { - componentMetadatas: null, - utilsMetadata: null, - library: null, - extraEnvironment: null, - renderEnv: 'default', - device: 'default', - locale: '', - designMode: 'live', - deviceClassName: '', - simulatorUrl: null, - requestHandlersMap: null, - }; - - private _mounted = true; - - constructor(props: any) { - super(props); - this.setupAssets(); - } - - private async setupAssets() { - const editor = this.props.engineEditor; - try { - const assets = await editor.onceGot('assets'); - const renderEnv = engineConfig.get('renderEnv') || editor.get('renderEnv'); - const device = engineConfig.get('device') || editor.get('device'); - const locale = engineConfig.get('locale') || editor.get('locale'); - const designMode = engineConfig.get('designMode') || editor.get('designMode'); - const deviceClassName = engineConfig.get('deviceClassName') || editor.get('deviceClassName'); - const simulatorUrl = engineConfig.get('simulatorUrl') || editor.get('simulatorUrl'); - // @TODO setupAssets 里设置 requestHandlersMap 不太合适 - const requestHandlersMap = - engineConfig.get('requestHandlersMap') || editor.get('requestHandlersMap'); - if (!this._mounted) { - return; - } - engineConfig.onGot('locale', (locale) => { - this.setState({ - locale, - }); - }); - engineConfig.onGot('requestHandlersMap', (requestHandlersMap) => { - this.setState({ - requestHandlersMap, - }); - }); - engineConfig.onGot('device', (device) => { - this.setState({ - device, - }); - }); - const { components, packages, extraEnvironment, utils } = assets; - const state = { - componentMetadatas: components || [], - library: packages || [], - utilsMetadata: utils || [], - extraEnvironment, - renderEnv, - device, - designMode, - deviceClassName, - simulatorUrl, - requestHandlersMap, - locale, - }; - this.setState(state); - } catch (e) { - logger.error(e); - } - } - - componentWillUnmount() { - this._mounted = false; - } - - private handleDesignerMount = (designer: Designer): void => { - const editor = this.props.engineEditor; - editor.set('designer', designer); - editor.eventBus.emit('designer.ready', designer); - editor.onGot('schema', (schema) => { - designer.project.open(schema); - }); - }; - - render(): React.ReactNode { - const editor: Editor = this.props.engineEditor; - const { - componentMetadatas, - utilsMetadata, - library, - extraEnvironment, - renderEnv, - device, - designMode, - deviceClassName, - simulatorUrl, - requestHandlersMap, - locale, - } = this.state; - - if (!library || !componentMetadatas) { - // TODO: use a Loading - return null; - } - - return ( - - ); - } -} diff --git a/packages/plugin-designer/tsconfig.declaration.json b/packages/plugin-designer/tsconfig.declaration.json deleted file mode 100644 index 54e57efbd..000000000 --- a/packages/plugin-designer/tsconfig.declaration.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "emitDeclarationOnly": true, - "declaration": true, - "outDir": "temp", - "stripInternal": true, - "paths": {} - } -} diff --git a/packages/plugin-designer/tsconfig.json b/packages/plugin-designer/tsconfig.json deleted file mode 100644 index 039e0b4d1..000000000 --- a/packages/plugin-designer/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src"] -} diff --git a/packages/plugin-designer/vite.config.ts b/packages/plugin-designer/vite.config.ts deleted file mode 100644 index e3d1909dc..000000000 --- a/packages/plugin-designer/vite.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vite'; -import baseConfigFn from '../../vite.base.config' - -export default defineConfig(async () => { - return baseConfigFn({ - name: 'LowCodePluginDesigner', - defaultFormats: ['es', 'cjs'], - entry: 'src/index.tsx' - }) -}); diff --git a/packages/plugin-outline-pane/package.json b/packages/plugin-outline-pane/package.json deleted file mode 100644 index 1a18869f8..000000000 --- a/packages/plugin-outline-pane/package.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "@alilc/lowcode-plugin-outline-pane", - "version": "1.3.2", - "description": "Outline pane for Ali lowCode engine", - "private": true, - "type": "module", - "main": "dist/low-code-plugin-outline-pane.cjs", - "module": "dist/low-code-plugin-outline-pane.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "import": "./dist/low-code-plugin-outline-pane.js", - "require": "./dist/low-code-plugin-outline-pane.cjs", - "types": "./dist/index.d.ts" - }, - "./dist/": { - "import": "./dist/", - "require": "./dist/" - } - }, - "files": [ - "dist", - "src", - "package.json" - ], - "scripts": { - "build:target": "vite build", - "build:dts": "tsc -p tsconfig.declaration.json && node ../../scripts/rollup-dts.mjs", - "test": "vitest" - }, - "dependencies": { - "@alifd/next": "^1.27.8", - "@alilc/lowcode-engine-core": "workspace:*", - "classnames": "^2.5.1", - "events": "^3.3.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "ric-shim": "^1.0.1" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0" - }, - "peerDependencies": { - "@alifd/next": "^1.27.8", - "@alilc/lowcode-engine-core": "workspace:*", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "license": "MIT", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "http", - "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/plugin-outline-pane" - }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", - "bugs": "https://github.com/alibaba/lowcode-engine/issues", - "homepage": "https://github.com/alibaba/lowcode-engine/#readme" -} diff --git a/packages/plugin-outline-pane/src/controllers/pane-controller.ts b/packages/plugin-outline-pane/src/controllers/pane-controller.ts deleted file mode 100644 index 9d135e23d..000000000 --- a/packages/plugin-outline-pane/src/controllers/pane-controller.ts +++ /dev/null @@ -1,707 +0,0 @@ -import requestIdleCallback, { cancelIdleCallback } from 'ric-shim'; -import { - uniqueId, - isDragNodeObject, - isDragAnyObject, - isLocationChildrenDetail, -} from '@alilc/lowcode-utils'; -import { - IPublicModelDragObject, - IPublicTypeScrollable, - IPublicModelSensor, - IPublicTypeLocationChildrenDetail, - IPublicTypeLocationDetailType, - IPublicModelNode, - IPublicModelDropLocation, - IPublicModelScroller, - IPublicModelScrollTarget, - IPublicModelLocateEvent, -} from '@alilc/lowcode-types'; -import TreeNode from './tree-node'; -import { IndentTrack } from '../helper/indent-track'; -import DwellTimer from '../helper/dwell-timer'; -import { IOutlinePanelPluginContext, ITreeBoard, TreeMaster } from './tree-master'; - -export class PaneController implements IPublicModelSensor, ITreeBoard, IPublicTypeScrollable { - private pluginContext: IOutlinePanelPluginContext; - - private treeMaster?: TreeMaster; - - readonly id = uniqueId('outline'); - - private indentTrack = new IndentTrack(); - - private _sensorAvailable = false; - - /** - * @see IPublicModelSensor - */ - get sensorAvailable() { - return this._sensorAvailable; - } - - private dwell = new DwellTimer((target, event) => { - const { canvas, project } = this.pluginContext; - const document = project.getCurrentDocument(); - let index: any; - let focus: any; - let valid = true; - if (target.hasSlots()) { - index = null; - focus = { type: 'slots' }; - } else { - index = 0; - valid = !!document?.checkNesting(target, event.dragObject as any); - } - canvas.createLocation({ - target, - source: this.id, - event, - detail: { - type: IPublicTypeLocationDetailType.Children, - index, - focus, - valid, - }, - }); - }); - - /** - * @see ITreeBoard - */ - readonly at: string | symbol; - - private tryScrollAgain: number | null = null; - - private sensing = false; - - /** - * @see IScrollable - */ - get bounds(): DOMRect | null { - if (!this._shell) { - return null; - } - return this._shell.getBoundingClientRect(); - } - - private _scrollTarget?: IPublicModelScrollTarget; - - /** - * @see IScrollable - */ - get scrollTarget() { - return this._scrollTarget; - } - - private scroller?: IPublicModelScroller; - - private _shell: HTMLDivElement | null = null; - - constructor(at: string | symbol, treeMaster: TreeMaster) { - this.pluginContext = treeMaster.pluginContext; - this.treeMaster = treeMaster; - this.at = at; - let inited = false; - const setup = () => { - if (inited) { - return false; - } - inited = true; - this.treeMaster?.addBoard(this); - const { canvas } = this.pluginContext; - canvas.dragon?.addSensor(this); - this.scroller = canvas.createScroller(this); - }; - - setup(); - } - - /** -------------------- IPublicModelSensor begin -------------------- */ - - /** - * @see IPublicModelSensor - */ - fixEvent(e: IPublicModelLocateEvent): IPublicModelLocateEvent { - if (e.fixed) { - return e; - } - - const notMyEvent = e.originalEvent.view?.document !== document; - - if (!e.target || notMyEvent) { - e.target = document.elementFromPoint(e.canvasX!, e.canvasY!); - } - - // documentModel : 目标文档 - e.documentModel = this.pluginContext.project.getCurrentDocument(); - - // 事件已订正 - e.fixed = true; - return e; - } - - /** - * @see IPublicModelSensor - */ - locate(e: IPublicModelLocateEvent): IPublicModelDropLocation | undefined | null { - this.sensing = true; - this.scroller?.scrolling(e); - const { globalY, dragObject } = e; - const nodes = dragObject?.nodes; - - const tree = this.treeMaster?.currentTree; - if (!tree || !tree.root || !this._shell) { - return null; - } - - const operationalNodes = nodes?.filter((node: any) => { - const onMoveHook = node.componentMeta?.advanced.callbacks?.onMoveHook; - const canMove = onMoveHook && typeof onMoveHook === 'function' ? onMoveHook(node) : true; - - return canMove; - }); - - // 如果拖拽的是 Node 才需要后面的判断,拖拽 data 不需要 - if (isDragNodeObject(dragObject) && (!operationalNodes || operationalNodes.length === 0)) { - return; - } - - const { project, canvas } = this.pluginContext; - const document = project.getCurrentDocument(); - const pos = getPosFromEvent(e, this._shell); - const irect = this.getInsertionRect(); - const originLoc = document?.dropLocation; - - const componentMeta = e.dragObject?.nodes ? e.dragObject?.nodes?.[0]?.componentMeta : null; - if ( - e.dragObject?.type === 'node' && - componentMeta && - componentMeta.isModal && - document?.focusNode - ) { - return canvas.createLocation({ - target: document?.focusNode, - detail: { - type: IPublicTypeLocationDetailType.Children, - index: 0, - valid: true, - }, - source: this.id, - event: e, - }); - } - - if ( - originLoc && - ((pos && pos === 'unchanged') || - (irect && globalY >= irect.top && globalY <= irect.bottom)) && - dragObject - ) { - const loc = originLoc.clone(e); - const indented = this.indentTrack.getIndentParent(originLoc, loc); - if (indented) { - const [parent, index] = indented; - if (checkRecursion(parent, dragObject)) { - if (tree.getTreeNode(parent).expanded) { - this.dwell.reset(); - return canvas.createLocation({ - target: parent, - source: this.id, - event: e, - detail: { - type: IPublicTypeLocationDetailType.Children, - index, - valid: document?.checkNesting(parent, e.dragObject as any), - }, - }); - } - - (originLoc.detail as IPublicTypeLocationChildrenDetail).focus = { - type: 'node', - node: parent, - }; - // focus try expand go on - this.dwell.focus(parent, e); - } else { - this.dwell.reset(); - } - // FIXME: recreate new location - } else if ((originLoc.detail as IPublicTypeLocationChildrenDetail).near) { - (originLoc.detail as IPublicTypeLocationChildrenDetail).near = undefined; - this.dwell.reset(); - } - return; - } - - this.indentTrack.reset(); - - if (pos && pos !== 'unchanged') { - let treeNode = tree.getTreeNodeById(pos.nodeId); - if (treeNode) { - let { focusSlots } = pos; - let { node } = treeNode; - if (isDragNodeObject(dragObject)) { - const newNodes = operationalNodes; - let i = newNodes?.length ?? 0; - let p: any = node; - while (i-- > 0) { - if (newNodes?.[i]?.contains(p)) { - p = newNodes?.[i]?.parent; - } - } - if (p !== node) { - node = p || document?.focusNode; - treeNode = tree.getTreeNode(node); - focusSlots = false; - } - } - - if (focusSlots) { - this.dwell.reset(); - return canvas.createLocation({ - target: node as IPublicModelNode, - source: this.id, - event: e, - detail: { - type: IPublicTypeLocationDetailType.Children, - index: null, - valid: false, - focus: { type: 'slots' }, - }, - }); - } - - if (!treeNode.isRoot()) { - const loc = this.getNear(treeNode, e); - this.dwell.tryFocus(loc); - return loc; - } - } - } - - const loc = this.drillLocate(tree.root, e); - this.dwell.tryFocus(loc); - return loc; - } - - /** - * @see IPublicModelSensor - */ - isEnter(e: IPublicModelLocateEvent): boolean { - if (!this._shell) { - return false; - } - const rect = this._shell.getBoundingClientRect(); - return ( - e.globalY >= rect.top && - e.globalY <= rect.bottom && - e.globalX >= rect.left && - e.globalX <= rect.right - ); - } - - /** - * @see IPublicModelSensor - */ - deactiveSensor() { - this.sensing = false; - this.scroller?.cancel(); - this.dwell.reset(); - this.indentTrack.reset(); - } - - /** -------------------- IPublicModelSensor end -------------------- */ - - /** -------------------- ITreeBoard begin -------------------- */ - - /** - * @see ITreeBoard - */ - scrollToNode(treeNode: TreeNode, detail?: any, tryTimes = 0) { - if (tryTimes < 1 && this.tryScrollAgain) { - cancelIdleCallback(this.tryScrollAgain); - this.tryScrollAgain = null; - } - if (!this.bounds || !this.scroller || !this.scrollTarget) { - // is a active sensor - return; - } - - let rect: ClientRect | undefined; - if (detail && isLocationChildrenDetail(detail)) { - rect = this.getInsertionRect(); - } else { - rect = this.getTreeNodeRect(treeNode); - } - - if (!rect) { - if (tryTimes < 3) { - this.tryScrollAgain = requestIdleCallback(() => - this.scrollToNode(treeNode, detail, tryTimes + 1), - ); - } - return; - } - const { scrollHeight, top: scrollTop } = this.scrollTarget; - const { height, top, bottom } = this.bounds; - if (rect.top < top || rect.bottom > bottom) { - const opt: any = {}; - opt.top = Math.min( - rect.top + rect.height / 2 + scrollTop - top - height / 2, - scrollHeight - height, - ); - if (rect.height >= height) { - opt.top = Math.min(scrollTop + rect.top - top, opt.top); - } - this.scroller.scrollTo(opt); - } - // make tail scroll be sure - if (tryTimes < 4) { - this.tryScrollAgain = requestIdleCallback(() => this.scrollToNode(treeNode, detail, 4)); - } - } - - /** -------------------- ITreeBoard end -------------------- */ - - private getNear( - treeNode: TreeNode, - e: IPublicModelLocateEvent, - originalIndex?: number, - originalRect?: DOMRect, - ) { - const { canvas, project } = this.pluginContext; - const document = project.getCurrentDocument(); - const { globalY, dragObject } = e; - if (!dragObject) { - return null; - } - // TODO: check dragObject is anyData - const { node, expanded } = treeNode; - let rect = originalRect; - if (!rect) { - rect = this.getTreeNodeRect(treeNode); - if (!rect) { - return null; - } - } - let index = originalIndex; - if (index == null) { - index = node.index; - } - - if (node.isSlotNode) { - // 是个插槽根节点 - if (!treeNode.isContainer() && !treeNode.hasSlots()) { - return canvas.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: IPublicTypeLocationDetailType.Children, - index: null, - near: { node, pos: 'replace' }, - valid: true, // TODO: future validation the slot limit - }, - }); - } - const loc1 = this.drillLocate(treeNode, e); - if (loc1) { - return loc1; - } - - return canvas.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: IPublicTypeLocationDetailType.Children, - index: null, - valid: false, - focus: { type: 'slots' }, - }, - }); - } - - let focusNode: IPublicModelNode | undefined; - // focus - if (!expanded && (treeNode.isContainer() || treeNode.hasSlots())) { - focusNode = node; - } - - // before - const titleRect = this.getTreeTitleRect(treeNode) || rect; - if (globalY < titleRect.top + titleRect.height / 2) { - return canvas.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: IPublicTypeLocationDetailType.Children, - index, - valid: document?.checkNesting(node.parent!, dragObject as any), - near: { node, pos: 'before' }, - focus: checkRecursion(focusNode, dragObject) - ? { type: 'node', node: focusNode } - : undefined, - }, - }); - } - - if (globalY > titleRect.bottom) { - focusNode = undefined; - } - - if (expanded) { - // drill - const loc = this.drillLocate(treeNode, e); - if (loc) { - return loc; - } - } - - // after - return canvas.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: IPublicTypeLocationDetailType.Children, - index: (index || 0) + 1, - valid: document?.checkNesting(node.parent!, dragObject as any), - near: { node, pos: 'after' }, - focus: checkRecursion(focusNode, dragObject) - ? { type: 'node', node: focusNode } - : undefined, - }, - }); - } - - private drillLocate( - treeNode: TreeNode, - e: IPublicModelLocateEvent, - ): IPublicModelDropLocation | null { - const { canvas, project } = this.pluginContext; - const document = project.getCurrentDocument(); - const { dragObject, globalY } = e; - if (!dragObject) { - return null; - } - - if (!checkRecursion(treeNode.node, dragObject)) { - return null; - } - - if (isDragAnyObject(dragObject)) { - // TODO: future - return null; - } - - const container = treeNode.node as IPublicModelNode; - const detail: IPublicTypeLocationChildrenDetail = { - type: IPublicTypeLocationDetailType.Children, - }; - const locationData: any = { - target: container, - detail, - source: this.id, - event: e, - }; - const isSlotContainer = treeNode.hasSlots(); - const isContainer = treeNode.isContainer(); - - if (container.isSlotNode && !treeNode.expanded) { - // 未展开,直接定位到内部第一个节点 - if (isSlotContainer) { - detail.index = null; - detail.focus = { type: 'slots' }; - detail.valid = false; - } else { - detail.index = 0; - detail.valid = document?.checkNesting(container, dragObject as any); - } - } - - let items: TreeNode[] | null = null; - let slotsRect: DOMRect | undefined; - let focusSlots = false; - // isSlotContainer - if (isSlotContainer) { - slotsRect = this.getTreeSlotsRect(treeNode); - if (slotsRect) { - if (globalY <= slotsRect.bottom) { - focusSlots = true; - items = treeNode.slots; - } else if (!isContainer) { - // 不在 slots 范围,又不是 container 的情况,高亮 slots 区 - detail.index = null; - detail.focus = { type: 'slots' }; - detail.valid = false; - return canvas.createLocation(locationData); - } - } - } - - if (!items && isContainer) { - items = treeNode.children; - } - - if (!items) { - return null; - } - - const l = items.length; - let index = 0; - let before = l < 1; - let current: TreeNode | undefined; - let currentIndex = index; - for (; index < l; index++) { - current = items[index]; - currentIndex = index; - const rect = this.getTreeNodeRect(current); - if (!rect) { - continue; - } - - // rect - if (globalY < rect.top) { - before = true; - break; - } - - if (globalY > rect.bottom) { - continue; - } - - const loc = this.getNear(current, e, index, rect); - if (loc) { - return loc; - } - } - - if (focusSlots) { - detail.focus = { type: 'slots' }; - detail.valid = false; - detail.index = null; - } else { - if (current) { - detail.index = before ? currentIndex : currentIndex + 1; - detail.near = { node: current.node, pos: before ? 'before' : 'after' }; - } else { - detail.index = l; - } - detail.valid = document?.checkNesting(container, dragObject as any); - } - - return canvas.createLocation(locationData); - } - - purge() { - const { canvas } = this.pluginContext; - canvas.dragon?.removeSensor(this); - this.treeMaster?.removeBoard(this); - } - - mount(shell: HTMLDivElement | null) { - if (this._shell === shell) { - return; - } - this._shell = shell; - const { canvas, project } = this.pluginContext; - if (shell) { - this._scrollTarget = canvas.createScrollTarget(shell); - this._sensorAvailable = true; - - // check if there is current selection and scroll to it - const selection = project.currentDocument?.selection; - const topNodes = selection?.getTopNodes(true); - const tree = this.treeMaster?.currentTree; - if (topNodes && topNodes[0] && tree) { - const treeNode = tree.getTreeNodeById(topNodes[0].id); - if (treeNode) { - // at this moment, it is possible that pane is not ready yet, so - // put ui related operations to the next loop - setTimeout(() => { - tree.setNodeSelected(treeNode.nodeId); - this.scrollToNode(treeNode, null, 4); - }, 0); - } - } - } else { - this._scrollTarget = undefined; - this._sensorAvailable = false; - } - } - - private getInsertionRect(): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell.querySelector('.insertion')?.getBoundingClientRect(); - } - - private getTreeNodeRect(treeNode: TreeNode): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell - .querySelector(`.tree-node[data-id="${treeNode.nodeId}"]`) - ?.getBoundingClientRect(); - } - - private getTreeTitleRect(treeNode: TreeNode): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell - .querySelector(`.tree-node-title[data-id="${treeNode.nodeId}"]`) - ?.getBoundingClientRect(); - } - - private getTreeSlotsRect(treeNode: TreeNode): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell - .querySelector(`.tree-node-slots[data-id="${treeNode.nodeId}"]`) - ?.getBoundingClientRect(); - } -} - -function checkRecursion( - parent: IPublicModelNode | undefined | null, - dragObject: IPublicModelDragObject, -): boolean { - if (!parent) { - return false; - } - if (isDragNodeObject(dragObject)) { - const { nodes } = dragObject; - if (nodes.some((node: IPublicModelNode) => node.contains(parent))) { - return false; - } - } - return true; -} - -function getPosFromEvent( - { target }: IPublicModelLocateEvent, - stop: Element, -): null | 'unchanged' | { nodeId: string; focusSlots: boolean } { - if (!target || !stop.contains(target)) { - return null; - } - if (target.matches('.insertion')) { - return 'unchanged'; - } - const closest = target.closest('[data-id]'); - if (!closest || !stop.contains(closest)) { - return null; - } - - const nodeId = (closest as HTMLDivElement).dataset.id!; - return { - focusSlots: closest.matches('.tree-node-slots'), - nodeId, - }; -} diff --git a/packages/plugin-outline-pane/src/controllers/tree-master.ts b/packages/plugin-outline-pane/src/controllers/tree-master.ts deleted file mode 100644 index c988501a7..000000000 --- a/packages/plugin-outline-pane/src/controllers/tree-master.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; -import { IPublicModelPluginContext, IPublicTypeActiveTarget, IPublicModelNode, IPublicTypeDisposable, IPublicEnumPluginRegisterLevel } from '@alilc/lowcode-types'; -import TreeNode from './tree-node'; -import { Tree } from './tree'; -import EventEmitter from 'events'; -import { enUS, zhCN } from '../locale'; -import { ReactNode } from 'react'; - -export interface ITreeBoard { - readonly at: string | symbol; - scrollToNode(treeNode: TreeNode, detail?: any): void; -} - -enum EVENT_NAMES { - pluginContextChanged = 'pluginContextChanged', -} - -export interface IOutlinePanelPluginContext extends IPublicModelPluginContext { - extraTitle?: string; - intlNode(id: string, params?: object): ReactNode; - intl(id: string, params?: object): string; - getLocale(): string; -} - -export class TreeMaster { - pluginContext: IOutlinePanelPluginContext; - - private boards = new Set(); - - private treeMap = new Map(); - - private disposeEvents: (IPublicTypeDisposable | undefined)[] = []; - - event = new EventEmitter(); - - constructor(pluginContext: IPublicModelPluginContext, readonly options: { - extraTitle?: string; - }) { - this.setPluginContext(pluginContext); - const { workspace } = this.pluginContext; - this.initEvent(); - if (pluginContext.registerLevel === IPublicEnumPluginRegisterLevel.Workspace) { - this.setPluginContext(workspace.window?.currentEditorView); - let dispose: IPublicTypeDisposable | undefined; - const windowViewTypeChangeEvent = () => { - dispose = workspace.window?.onChangeViewType(() => { - this.setPluginContext(workspace.window?.currentEditorView); - }); - }; - - windowViewTypeChangeEvent(); - - workspace.onChangeActiveWindow(() => { - this.setPluginContext(workspace.window?.currentEditorView); - dispose && dispose(); - windowViewTypeChangeEvent(); - }); - } - } - - private setPluginContext(pluginContext: IPublicModelPluginContext | undefined | null) { - if (!pluginContext) { - return; - } - const { intl, intlNode, getLocale } = pluginContext.common.utils.createIntl({ - 'en-US': enUS, - 'zh-CN': zhCN, - }); - const _pluginContext: IOutlinePanelPluginContext = Object.assign(pluginContext, { - intl, - intlNode, - getLocale, - }); - _pluginContext.extraTitle = this.options && this.options['extraTitle']; - this.pluginContext = _pluginContext; - this.disposeEvent(); - this.initEvent(); - this.emitPluginContextChange(); - } - - private disposeEvent() { - this.disposeEvents.forEach(d => { - d && d(); - }); - } - - private initEvent() { - let startTime: any; - const { event, project, canvas } = this.pluginContext; - const setExpandByActiveTracker = (target: IPublicTypeActiveTarget) => { - const { node, detail } = target; - const tree = this.currentTree; - if (!tree/* || node.document !== tree.document */) { - return; - } - const treeNode = tree.getTreeNode(node); - if (detail && isLocationChildrenDetail(detail)) { - treeNode.expand(true); - } else { - treeNode.expandParents(); - } - this.boards.forEach((board) => { - board.scrollToNode(treeNode, detail); - }); - }; - this.disposeEvents = [ - canvas.dragon?.onDragstart(() => { - startTime = Date.now() / 1000; - // needs? - this.toVision(); - }), - canvas.activeTracker?.onChange(setExpandByActiveTracker), - canvas.dragon?.onDragend(() => { - const endTime: any = Date.now() / 1000; - const nodes = project.currentDocument?.selection?.getNodes(); - event.emit('outlinePane.dragend', { - selected: nodes - ?.map((n) => { - if (!n) { - return; - } - const npm = n?.componentMeta?.npm; - return ( - [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || n?.componentMeta?.componentName - ); - }) - .join('&'), - time: (endTime - startTime).toFixed(2), - }); - }), - project.onRemoveDocument((data: {id: string}) => { - const { id } = data; - this.treeMap.delete(id); - }), - ]; - if (canvas.activeTracker?.target) { - setExpandByActiveTracker(canvas.activeTracker?.target); - } - } - - private toVision() { - const tree = this.currentTree; - if (tree) { - const selection = this.pluginContext.project.getCurrentDocument()?.selection; - selection?.getTopNodes().forEach((node: IPublicModelNode) => { - tree.getTreeNode(node).setExpanded(false); - }); - } - } - - addBoard(board: ITreeBoard) { - this.boards.add(board); - } - - removeBoard(board: ITreeBoard) { - this.boards.delete(board); - } - - purge() { - // todo others purge - } - - onPluginContextChange(fn: () => void) { - this.event.on(EVENT_NAMES.pluginContextChanged, fn); - } - - emitPluginContextChange() { - this.event.emit(EVENT_NAMES.pluginContextChanged); - } - - get currentTree(): Tree | null { - const doc = this.pluginContext.project.getCurrentDocument(); - if (doc) { - const { id } = doc; - if (this.treeMap.has(id)) { - return this.treeMap.get(id)!; - } - const tree = new Tree(this); - this.treeMap.set(id, tree); - return tree; - } - return null; - } -} diff --git a/packages/plugin-outline-pane/src/controllers/tree-node.ts b/packages/plugin-outline-pane/src/controllers/tree-node.ts deleted file mode 100644 index 34d06fee0..000000000 --- a/packages/plugin-outline-pane/src/controllers/tree-node.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { - IPublicTypeTitleContent, - IPublicTypeLocationChildrenDetail, - IPublicModelNode, - IPublicTypeDisposable, -} from '@alilc/lowcode-types'; -import { isI18nData, isLocationChildrenDetail, uniqueId } from '@alilc/lowcode-utils'; -import EventEmitter from 'events'; -import { Tree } from './tree'; -import { IOutlinePanelPluginContext } from './tree-master'; - -/** - * 大纲树过滤结果 - */ -export interface FilterResult { - // 过滤条件是否生效 - filterWorking: boolean; - // 命中子节点 - matchChild: boolean; - // 命中本节点 - matchSelf: boolean; - // 关键字 - keywords: string; -} - -enum EVENT_NAMES { - filterResultChanged = 'filterResultChanged', - - expandedChanged = 'expandedChanged', - - hiddenChanged = 'hiddenChanged', - - lockedChanged = 'lockedChanged', - - titleLabelChanged = 'titleLabelChanged', - - expandableChanged = 'expandableChanged', - - conditionChanged = 'conditionChanged', -} - -export default class TreeNode { - readonly pluginContext: IOutlinePanelPluginContext; - event = new EventEmitter(); - - private _node: IPublicModelNode; - - readonly tree: Tree; - - private _filterResult: FilterResult = { - filterWorking: false, - matchChild: false, - matchSelf: false, - keywords: '', - }; - - /** - * 默认为折叠状态 - * 在初始化根节点时,设置为展开状态 - */ - private _expanded = false; - - id = uniqueId('treeNode'); - - get nodeId(): string { - return this.node.id; - } - - /** - * 是否可以展开 - */ - get expandable(): boolean { - if (this.locked) return false; - return this.hasChildren() || this.hasSlots() || this.dropDetail?.index != null; - } - - get expanded(): boolean { - return this.isRoot(true) || (this.expandable && this._expanded); - } - - /** - * 插入"线"位置信息 - */ - get dropDetail(): IPublicTypeLocationChildrenDetail | undefined | null { - const loc = this.pluginContext.project.getCurrentDocument()?.dropLocation; - return loc && this.isResponseDropping() && isLocationChildrenDetail(loc.detail) ? loc.detail : null; - } - - get depth(): number { - return this.node.zLevel; - } - - get detecting() { - const doc = this.pluginContext.project.currentDocument; - return !!(doc?.isDetectingNode(this.node)); - } - - get hidden(): boolean { - const cv = this.node.isConditionalVisible(); - if (cv == null) { - return !this.node.visible; - } - return !cv; - } - - get locked(): boolean { - return this.node.isLocked; - } - - get selected(): boolean { - // TODO: check is dragging - const selection = this.pluginContext.project.getCurrentDocument()?.selection; - if (!selection) { - return false; - } - return selection?.has(this.node.id); - } - - get title(): IPublicTypeTitleContent { - return this.node.title; - } - - get titleLabel() { - let { title } = this; - if (!title) { - return ''; - } - if ((title as any).label) { - title = (title as any).label; - } - if (typeof title === 'string') { - return title; - } - if (isI18nData(title)) { - const currentLocale = this.pluginContext.getLocale(); - const currentTitle = title[currentLocale]; - return currentTitle; - } - return this.node.componentName; - } - - get icon() { - return this.node.componentMeta?.icon; - } - - get parent(): TreeNode | null { - const { parent } = this.node; - if (parent) { - return this.tree.getTreeNode(parent); - } - return null; - } - - get slots(): TreeNode[] { - // todo: shallowEqual - return this.node.slots.map((node) => this.tree.getTreeNode(node)); - } - - get condition(): boolean { - return this.node.hasCondition() && !this.node.conditionGroup; - } - - get children(): TreeNode[] | null { - return this.node.children?.map((node) => this.tree.getTreeNode(node)) || null; - } - - get node(): IPublicModelNode { - return this._node; - } - - constructor(tree: Tree, node: IPublicModelNode) { - this.tree = tree; - this.pluginContext = tree.pluginContext; - this._node = node; - } - - setLocked(flag: boolean) { - this.node.lock(flag); - this.event.emit(EVENT_NAMES.lockedChanged, flag); - } - deleteNode(node: IPublicModelNode) { - node && node.remove(); - } - onFilterResultChanged(fn: () => void): IPublicTypeDisposable { - this.event.on(EVENT_NAMES.filterResultChanged, fn); - return () => { - this.event.off(EVENT_NAMES.filterResultChanged, fn); - }; - } - onExpandedChanged(fn: (expanded: boolean) => void): IPublicTypeDisposable { - this.event.on(EVENT_NAMES.expandedChanged, fn); - return () => { - this.event.off(EVENT_NAMES.expandedChanged, fn); - }; - } - onHiddenChanged(fn: (hidden: boolean) => void): IPublicTypeDisposable { - this.event.on(EVENT_NAMES.hiddenChanged, fn); - return () => { - this.event.off(EVENT_NAMES.hiddenChanged, fn); - }; - } - onLockedChanged(fn: (locked: boolean) => void): IPublicTypeDisposable { - this.event.on(EVENT_NAMES.lockedChanged, fn); - return () => { - this.event.off(EVENT_NAMES.lockedChanged, fn); - }; - } - - onTitleLabelChanged(fn: (treeNode: TreeNode) => void): IPublicTypeDisposable { - this.event.on(EVENT_NAMES.titleLabelChanged, fn); - - return () => { - this.event.off(EVENT_NAMES.titleLabelChanged, fn); - }; - } - - onConditionChanged(fn: (treeNode: TreeNode) => void): IPublicTypeDisposable { - this.event.on(EVENT_NAMES.conditionChanged, fn); - - return () => { - this.event.off(EVENT_NAMES.conditionChanged, fn); - }; - } - - onExpandableChanged(fn: (expandable: boolean) => void): IPublicTypeDisposable { - this.event.on(EVENT_NAMES.expandableChanged, fn); - return () => { - this.event.off(EVENT_NAMES.expandableChanged, fn); - }; - } - - /** - * 触发 onExpandableChanged 回调 - */ - notifyExpandableChanged(): void { - this.event.emit(EVENT_NAMES.expandableChanged, this.expandable); - } - - notifyTitleLabelChanged(): void { - this.event.emit(EVENT_NAMES.titleLabelChanged, this.title); - } - - notifyConditionChanged(): void { - this.event.emit(EVENT_NAMES.conditionChanged, this.condition); - } - - setHidden(flag: boolean) { - if (this.node.conditionGroup) { - return; - } - if (this.node.visible !== !flag) { - this.node.visible = !flag; - } - this.event.emit(EVENT_NAMES.hiddenChanged, flag); - } - - isFocusingNode(): boolean { - const loc = this.pluginContext.project.getCurrentDocument()?.dropLocation; - if (!loc) { - return false; - } - return ( - isLocationChildrenDetail(loc.detail) && loc.detail.focus?.type === 'node' && loc.detail?.focus?.node.id === this.nodeId - ); - } - - setExpanded(value: boolean) { - this._expanded = value; - this.event.emit(EVENT_NAMES.expandedChanged, value); - } - - isRoot(includeOriginalRoot = false) { - const rootNode = this.pluginContext.project.getCurrentDocument()?.root; - return this.tree.root === this || (includeOriginalRoot && rootNode === this.node); - } - - /** - * 是否是响应投放区 - */ - isResponseDropping(): boolean { - const loc = this.pluginContext.project.getCurrentDocument()?.dropLocation; - if (!loc) { - return false; - } - return loc.target?.id === this.nodeId; - } - - setTitleLabel(label: string) { - const origLabel = this.titleLabel; - if (label === origLabel) { - return; - } - if (label === '') { - this.node.getExtraProp('title', false)?.remove(); - } else { - this.node.getExtraProp('title', true)?.setValue(label); - } - this.event.emit(EVENT_NAMES.titleLabelChanged, this); - } - - /** - * 是否是容器,允许子节点拖入 - */ - isContainer(): boolean { - return this.node.isContainerNode; - } - - /** - * 判断是否有"插槽" - */ - hasSlots(): boolean { - return this.node.hasSlots(); - } - - hasChildren(): boolean { - return !!(this.isContainer() && this.node.children?.notEmptyNode); - } - - select(isMulti: boolean) { - const { node } = this; - - const selection = this.pluginContext.project.getCurrentDocument()?.selection; - if (isMulti) { - selection?.add(node.id); - } else { - selection?.select(node.id); - } - } - - /** - * 展开节点,支持依次展开父节点 - */ - expand(tryExpandParents = false) { - // 这边不能直接使用 expanded,需要额外判断是否可以展开 - // 如果只使用 expanded,会漏掉不可以展开的情况,即在不可以展开的情况下,会触发展开 - if (this.expandable && !this._expanded) { - this.setExpanded(true); - } - if (tryExpandParents) { - this.expandParents(); - } - } - - expandParents() { - let p = this.node.parent; - while (p) { - this.tree.getTreeNode(p).setExpanded(true); - p = p.parent; - } - } - - setNode(node: IPublicModelNode) { - if (this._node !== node) { - this._node = node; - } - } - - get filterReult(): FilterResult { - return this._filterResult; - } - - setFilterReult(val: FilterResult) { - this._filterResult = val; - this.event.emit(EVENT_NAMES.filterResultChanged); - } -} diff --git a/packages/plugin-outline-pane/src/controllers/tree.ts b/packages/plugin-outline-pane/src/controllers/tree.ts deleted file mode 100644 index ca5a43c55..000000000 --- a/packages/plugin-outline-pane/src/controllers/tree.ts +++ /dev/null @@ -1,130 +0,0 @@ -import TreeNode from './tree-node'; -import { IPublicModelNode, IPublicTypePropChangeOptions } from '@alilc/lowcode-types'; -import { IOutlinePanelPluginContext, TreeMaster } from './tree-master'; - -export class Tree { - private treeNodesMap = new Map(); - - readonly id: string | undefined; - - readonly pluginContext: IOutlinePanelPluginContext; - - get root(): TreeNode | null { - if (this.pluginContext.project.currentDocument?.focusNode) { - return this.getTreeNode(this.pluginContext.project.currentDocument.focusNode!); - } - return null; - } - - readonly treeMaster: TreeMaster; - - constructor(treeMaster: TreeMaster) { - this.treeMaster = treeMaster; - this.pluginContext = treeMaster.pluginContext; - const doc = this.pluginContext.project.currentDocument; - this.id = doc?.id; - - doc?.onChangeNodeChildren((info: {node: IPublicModelNode }) => { - const { node } = info; - const treeNode = this.getTreeNodeById(node.id); - treeNode?.notifyExpandableChanged(); - }); - - doc?.history.onChangeCursor(() => { - this.root?.notifyExpandableChanged(); - }); - - doc?.onChangeNodeProp((info: IPublicTypePropChangeOptions) => { - const { node, key } = info; - if (key === '___title___') { - const treeNode = this.getTreeNodeById(node.id); - treeNode?.notifyTitleLabelChanged(); - } else if (key === '___condition___') { - const treeNode = this.getTreeNodeById(node.id); - treeNode?.notifyConditionChanged(); - } - }); - - doc?.onChangeNodeVisible((node: IPublicModelNode, visible: boolean) => { - const treeNode = this.getTreeNodeById(node.id); - treeNode?.setHidden(!visible); - }); - - doc?.onImportSchema(() => { - this.treeNodesMap = new Map(); - }); - } - - setNodeSelected(nodeId: string): void { - // 目标节点选中,其他节点展开 - const treeNode = this.treeNodesMap.get(nodeId); - if (!treeNode) { - return; - } - this.expandAllAncestors(treeNode); - } - - getTreeNode(node: IPublicModelNode): TreeNode { - if (this.treeNodesMap.has(node.id)) { - const tnode = this.treeNodesMap.get(node.id)!; - tnode.setNode(node); - return tnode; - } - - const treeNode = new TreeNode(this, node); - this.treeNodesMap.set(node.id, treeNode); - return treeNode; - } - - getTreeNodeById(id: string) { - return this.treeNodesMap.get(id); - } - - expandAllAncestors(treeNode: TreeNode | undefined | null) { - if (!treeNode) { - return; - } - if (treeNode.isRoot()) { - return; - } - const ancestors = []; - let currentNode: TreeNode | null | undefined = treeNode; - while (!treeNode.isRoot()) { - currentNode = currentNode?.parent; - if (currentNode) { - ancestors.unshift(currentNode); - } else { - break; - } - } - ancestors.forEach((ancestor) => { - ancestor.setExpanded(true); - }); - } - - expandAllDecendants(treeNode: TreeNode | undefined | null) { - if (!treeNode) { - return; - } - treeNode.setExpanded(true); - const children = treeNode && treeNode.children; - if (children) { - children.forEach((child) => { - this.expandAllDecendants(child); - }); - } - } - - collapseAllDecendants(treeNode: TreeNode | undefined | null): void { - if (!treeNode) { - return; - } - treeNode.setExpanded(false); - const children = treeNode && treeNode.children; - if (children) { - children.forEach((child) => { - this.collapseAllDecendants(child); - }); - } - } -} diff --git a/packages/plugin-outline-pane/src/helper/consts.ts b/packages/plugin-outline-pane/src/helper/consts.ts deleted file mode 100644 index 38725d79b..000000000 --- a/packages/plugin-outline-pane/src/helper/consts.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const BackupPaneName = 'outline-backup-pane'; -export const MasterPaneName = 'outline-master-pane'; diff --git a/packages/plugin-outline-pane/src/helper/dwell-timer.ts b/packages/plugin-outline-pane/src/helper/dwell-timer.ts deleted file mode 100644 index c35dc0d11..000000000 --- a/packages/plugin-outline-pane/src/helper/dwell-timer.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; -import { IPublicModelNode, IPublicModelDropLocation, IPublicModelLocateEvent } from '@alilc/lowcode-types'; - - -/** - * 停留检查计时器 - */ -export default class DwellTimer { - private timer: number | undefined; - - private previous?: IPublicModelNode; - - private event?: IPublicModelLocateEvent; - - private decide: (node: IPublicModelNode, event: IPublicModelLocateEvent) => void; - - private timeout = 500; - - constructor(decide: (node: IPublicModelNode, event: IPublicModelLocateEvent) => void, timeout = 500) { - this.decide = decide; - this.timeout = timeout; - } - - focus(node: IPublicModelNode, event: IPublicModelLocateEvent) { - this.event = event; - if (this.previous === node) { - return; - } - this.reset(); - this.previous = node; - this.timer = setTimeout(() => { - this.previous && this.decide(this.previous, this.event!); - this.reset(); - }, this.timeout) as any; - } - - tryFocus(loc?: IPublicModelDropLocation | null) { - if (!loc || !isLocationChildrenDetail(loc.detail)) { - this.reset(); - return; - } - if (loc.detail.focus?.type === 'node') { - this.focus(loc.detail.focus.node, loc.event); - } else { - this.reset(); - } - } - - reset() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = undefined; - } - - this.previous = undefined; - } -} diff --git a/packages/plugin-outline-pane/src/helper/indent-track.ts b/packages/plugin-outline-pane/src/helper/indent-track.ts deleted file mode 100644 index f5c06a8f1..000000000 --- a/packages/plugin-outline-pane/src/helper/indent-track.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; -import { IPublicModelDropLocation, IPublicModelNode } from '@alilc/lowcode-types'; - -const IndentSensitive = 15; -export class IndentTrack { - private indentStart: number | null = null; - - reset() { - this.indentStart = null; - } - - // eslint-disable-next-line max-len - getIndentParent( - lastLoc: IPublicModelDropLocation, - loc: IPublicModelDropLocation, - ): [IPublicModelNode, number | undefined] | null { - if ( - lastLoc.target !== loc.target || - !isLocationChildrenDetail(lastLoc.detail) || - !isLocationChildrenDetail(loc.detail) || - (lastLoc as any).source !== (loc as any).source || - lastLoc.detail.index !== loc.detail.index || - loc.detail.index == null - ) { - this.indentStart = null; - return null; - } - if (this.indentStart == null) { - this.indentStart = lastLoc.event.globalX; - } - const delta = loc.event.globalX - this.indentStart; - const indent = Math.floor(Math.abs(delta) / IndentSensitive); - if (indent < 1) { - return null; - } - this.indentStart = loc.event.globalX; - const direction = delta < 0 ? 'left' : 'right'; - - let parent: IPublicModelNode = loc.target as any; - const { index } = loc.detail; - - if (direction === 'left') { - if (!parent.parent || index < (parent.children?.size || 0) || parent.isSlotNode) { - return null; - } - return [(parent as any).parent, parent.index! + 1]; - } else { - if (index === 0) { - return null; - } - parent = parent.children?.get(index - 1) as any; - if (parent && parent.isContainerNode) { - return [parent, parent.children?.size]; - } - } - - return null; - } -} diff --git a/packages/plugin-outline-pane/src/icons/arrow-right.tsx b/packages/plugin-outline-pane/src/icons/arrow-right.tsx deleted file mode 100644 index 0a1418746..000000000 --- a/packages/plugin-outline-pane/src/icons/arrow-right.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconArrowRight(props: IconProps) { - return ( - - - - ); -} - -IconArrowRight.displayName = 'IconArrowRight'; diff --git a/packages/plugin-outline-pane/src/icons/cond.tsx b/packages/plugin-outline-pane/src/icons/cond.tsx deleted file mode 100644 index 7423b4139..000000000 --- a/packages/plugin-outline-pane/src/icons/cond.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconCond(props: IconProps) { - return ( - - - - ); -} - -IconCond.displayName = 'IconCond'; diff --git a/packages/plugin-outline-pane/src/icons/delete.tsx b/packages/plugin-outline-pane/src/icons/delete.tsx deleted file mode 100644 index 1f9360019..000000000 --- a/packages/plugin-outline-pane/src/icons/delete.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconDelete(props: IconProps) { - return ( - - - - ); -} - -IconDelete.displayName = 'IconDelete'; diff --git a/packages/plugin-outline-pane/src/icons/eye-close.tsx b/packages/plugin-outline-pane/src/icons/eye-close.tsx deleted file mode 100644 index e6e35f326..000000000 --- a/packages/plugin-outline-pane/src/icons/eye-close.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconEyeClose(props: IconProps) { - return ( - - - - - ); -} - -IconEyeClose.displayName = 'IconEyeClose'; diff --git a/packages/plugin-outline-pane/src/icons/eye.tsx b/packages/plugin-outline-pane/src/icons/eye.tsx deleted file mode 100644 index fd1fbad4a..000000000 --- a/packages/plugin-outline-pane/src/icons/eye.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconEye(props: IconProps) { - return ( - - - - - ); -} - -IconEye.displayName = 'IconEye'; diff --git a/packages/plugin-outline-pane/src/icons/filter.tsx b/packages/plugin-outline-pane/src/icons/filter.tsx deleted file mode 100644 index 11e2f719d..000000000 --- a/packages/plugin-outline-pane/src/icons/filter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconFilter(props: IconProps) { - return ( - - - - ); -} - -IconFilter.displayName = 'IconFilter'; diff --git a/packages/plugin-outline-pane/src/icons/index.ts b/packages/plugin-outline-pane/src/icons/index.ts deleted file mode 100644 index d28f61dd2..000000000 --- a/packages/plugin-outline-pane/src/icons/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from './lock'; -export * from './unlock'; -export * from './arrow-right'; -export * from './cond'; -export * from './eye-close'; -export * from './eye'; -export * from './filter'; -export * from './loop'; -export * from './radio-active'; -export * from './radio'; -export * from './setting'; -export * from './delete'; diff --git a/packages/plugin-outline-pane/src/icons/lock.tsx b/packages/plugin-outline-pane/src/icons/lock.tsx deleted file mode 100644 index f5ead19c4..000000000 --- a/packages/plugin-outline-pane/src/icons/lock.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconLock(props: IconProps) { - return ( - - - - ); -} - -IconLock.displayName = 'IconLock'; diff --git a/packages/plugin-outline-pane/src/icons/loop.tsx b/packages/plugin-outline-pane/src/icons/loop.tsx deleted file mode 100644 index f128dd460..000000000 --- a/packages/plugin-outline-pane/src/icons/loop.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconLoop(props: IconProps) { - return ( - - - - ); -} - -IconLoop.displayName = 'IconLoop'; diff --git a/packages/plugin-outline-pane/src/icons/outline.tsx b/packages/plugin-outline-pane/src/icons/outline.tsx deleted file mode 100644 index a3b7654e1..000000000 --- a/packages/plugin-outline-pane/src/icons/outline.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconOutline(props: IconProps) { - return ( - - - - ); -} - -IconOutline.displayName = 'IconOutline'; - diff --git a/packages/plugin-outline-pane/src/icons/radio-active.tsx b/packages/plugin-outline-pane/src/icons/radio-active.tsx deleted file mode 100644 index dcd4e938d..000000000 --- a/packages/plugin-outline-pane/src/icons/radio-active.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconRadioActive(props: IconProps) { - return ( - - - - ); -} - -IconRadioActive.displayName = 'IconRadioActive'; - diff --git a/packages/plugin-outline-pane/src/icons/radio.tsx b/packages/plugin-outline-pane/src/icons/radio.tsx deleted file mode 100644 index 9bb8fdf6f..000000000 --- a/packages/plugin-outline-pane/src/icons/radio.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconRadio(props: IconProps) { - return ( - - - - ); -} - -IconRadio.displayName = 'IconRadio'; - diff --git a/packages/plugin-outline-pane/src/icons/setting.tsx b/packages/plugin-outline-pane/src/icons/setting.tsx deleted file mode 100644 index a42b0ee01..000000000 --- a/packages/plugin-outline-pane/src/icons/setting.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconSetting(props: IconProps) { - return ( - - - - - ); -} - -IconSetting.displayName = 'IconSetting'; diff --git a/packages/plugin-outline-pane/src/icons/unlock.tsx b/packages/plugin-outline-pane/src/icons/unlock.tsx deleted file mode 100644 index 1f651210f..000000000 --- a/packages/plugin-outline-pane/src/icons/unlock.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; - -export function IconUnlock(props: IconProps) { - return ( - - - - - ); -} - -IconUnlock.displayName = 'IconUnlock'; diff --git a/packages/plugin-outline-pane/src/index.tsx b/packages/plugin-outline-pane/src/index.tsx deleted file mode 100644 index c7ec67918..000000000 --- a/packages/plugin-outline-pane/src/index.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { Pane } from './views/pane'; -import { IconOutline } from './icons/outline'; -import { IPublicModelPluginContext, IPublicModelDocumentModel } from '@alilc/lowcode-types'; -import { MasterPaneName, BackupPaneName } from './helper/consts'; -import { TreeMaster } from './controllers/tree-master'; -import { PaneController } from './controllers/pane-controller'; -import { useState, useEffect } from 'react'; - -export function OutlinePaneContext(props: { - treeMaster?: TreeMaster; - - pluginContext: IPublicModelPluginContext; - - options: any; - - paneName: string; - - hideFilter?: boolean; -}) { - const treeMaster = props.treeMaster || new TreeMaster(props.pluginContext, props.options); - const [masterPaneController, setMasterPaneController] = useState( - () => new PaneController(props.paneName || MasterPaneName, treeMaster), - ); - useEffect(() => { - return treeMaster.onPluginContextChange(() => { - setMasterPaneController(new PaneController(props.paneName || MasterPaneName, treeMaster)); - }); - }, []); - - return ( - - ); -} - -export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { - const { skeleton, config, canvas, project } = ctx; - - let isInFloatArea = true; - const hasPreferenceForOutline = config - .getPreference() - .contains('outline-pane-pinned-status-isFloat', 'skeleton'); - if (hasPreferenceForOutline) { - isInFloatArea = config.getPreference().get('outline-pane-pinned-status-isFloat', 'skeleton'); - } - const showingPanes = { - masterPane: false, - backupPane: false, - }; - const treeMaster = new TreeMaster(ctx, options); - return { - async init() { - skeleton.add({ - area: 'leftArea', - name: 'outlinePane', - type: 'PanelDock', - index: -1, - content: { - name: MasterPaneName, - props: { - icon: IconOutline, - description: treeMaster.pluginContext.intlNode('Outline Tree'), - }, - content: OutlinePaneContext, - } as any, - panelProps: { - area: isInFloatArea ? 'leftFloatArea' : 'leftFixedArea', - keepVisibleWhileDragging: true, - ...config.get('defaultOutlinePaneProps'), - }, - contentProps: { - treeTitleExtra: config.get('treeTitleExtra'), - treeMaster, - paneName: MasterPaneName, - }, - }); - - skeleton.add({ - area: 'rightArea', - name: BackupPaneName, - type: 'Panel', - props: { - hiddenWhenInit: true, - }, - content: OutlinePaneContext, - contentProps: { - paneName: BackupPaneName, - treeMaster, - }, - index: 1, - }); - - // 处理 master pane 和 backup pane 切换 - const switchPanes = () => { - const isDragging = canvas.dragon?.dragging; - const hasVisibleTreeBoard = showingPanes.backupPane || showingPanes.masterPane; - const shouldShowBackupPane = isDragging && !hasVisibleTreeBoard; - - if (shouldShowBackupPane) { - skeleton.showPanel(BackupPaneName); - } else { - skeleton.hidePanel(BackupPaneName); - } - }; - canvas.dragon?.onDragstart(() => { - switchPanes(); - }); - canvas.dragon?.onDragend(() => { - switchPanes(); - }); - skeleton.onShowPanel((key?: string) => { - if (key === MasterPaneName) { - showingPanes.masterPane = true; - } - if (key === BackupPaneName) { - showingPanes.backupPane = true; - } - }); - skeleton.onHidePanel((key?: string) => { - if (key === MasterPaneName) { - showingPanes.masterPane = false; - switchPanes(); - } - if (key === BackupPaneName) { - showingPanes.backupPane = false; - } - }); - project.onChangeDocument((document: IPublicModelDocumentModel) => { - if (!document) { - return; - } - - const { selection } = document; - - selection?.onSelectionChange(() => { - const selectedNodes = selection?.getNodes(); - if (!selectedNodes || selectedNodes.length === 0) { - return; - } - const tree = treeMaster.currentTree; - selectedNodes.forEach((node) => { - const treeNode = tree?.getTreeNodeById(node.id); - tree?.expandAllAncestors(treeNode); - }); - }); - }); - }, - }; -}; -OutlinePlugin.meta = { - eventPrefix: 'OutlinePlugin', - preferenceDeclaration: { - title: '大纲树插件配置', - properties: [ - { - key: 'extraTitle', - type: 'object', - description: '副标题', - }, - ], - }, -}; -OutlinePlugin.pluginName = 'OutlinePlugin'; diff --git a/packages/plugin-outline-pane/src/locale/en-US.json b/packages/plugin-outline-pane/src/locale/en-US.json deleted file mode 100644 index b86a7859b..000000000 --- a/packages/plugin-outline-pane/src/locale/en-US.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "Initializing": "Initializing", - "Hide": "Hide", - "Show": "Show", - "Lock": "Lock", - "Unlock": "Unlock", - "Expand": "Expand", - "Collapse": "Collapse", - "Conditional": "Condition", - "Loop": "Loop", - "Slots": "Slots", - "Slot for {prop}": "Slot for {prop}", - "Outline Tree": "Component Tree", - "Filter Node": "Filter Node", - "Check All": "Check All", - "Conditional rendering": "Conditional rendering", - "Loop rendering": "Loop rendering", - "Locked": "Locked", - "Hidden": "Hidden", - "Modal View": "Modal View", - "Rename": "Rename", - "Delete": "Delete" -} diff --git a/packages/plugin-outline-pane/src/locale/index.ts b/packages/plugin-outline-pane/src/locale/index.ts deleted file mode 100644 index b61ab9040..000000000 --- a/packages/plugin-outline-pane/src/locale/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import enUS from './en-US.json'; -import zhCN from './zh-CN.json'; - -export { enUS, zhCN }; diff --git a/packages/plugin-outline-pane/src/locale/zh-CN.json b/packages/plugin-outline-pane/src/locale/zh-CN.json deleted file mode 100644 index 9070d1771..000000000 --- a/packages/plugin-outline-pane/src/locale/zh-CN.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "Initializing": "正在初始化", - "Hide": "隐藏", - "Show": "显示", - "Lock": "锁定", - "Unlock": "解锁", - "Expand": "展开", - "Collapse": "收起", - "Conditional": "条件式", - "Loop": "循环", - "Slots": "插槽", - "Slot for {prop}": "属性 {prop} 的插槽", - "Outline Tree": "大纲树", - "Filter Node": "过滤节点", - "Check All": "全选", - "Conditional rendering": "条件渲染", - "Loop rendering": "循环渲染", - "Locked": "已锁定", - "Hidden": "已隐藏", - "Modal View": "模态视图层", - "Rename": "重命名", - "Delete": "删除" -} diff --git a/packages/plugin-outline-pane/src/ric-shim.d.ts b/packages/plugin-outline-pane/src/ric-shim.d.ts deleted file mode 100644 index ebf77e1a4..000000000 --- a/packages/plugin-outline-pane/src/ric-shim.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'ric-shim'; diff --git a/packages/plugin-outline-pane/src/views/filter-tree.ts b/packages/plugin-outline-pane/src/views/filter-tree.ts deleted file mode 100644 index 793aa03cc..000000000 --- a/packages/plugin-outline-pane/src/views/filter-tree.ts +++ /dev/null @@ -1,93 +0,0 @@ -import TreeNode from '../controllers/tree-node'; - -export const FilterType = { - CONDITION: 'CONDITION', - LOOP: 'LOOP', - LOCKED: 'LOCKED', - HIDDEN: 'HIDDEN', -}; - -export const FILTER_OPTIONS = [{ - value: FilterType.CONDITION, - label: 'Conditional rendering', -}, { - value: FilterType.LOOP, - label: 'Loop rendering', -}, { - value: FilterType.LOCKED, - label: 'Locked', -}, { - value: FilterType.HIDDEN, - label: 'Hidden', -}]; - -export const matchTreeNode = ( - treeNode: TreeNode, - keywords: string, - filterOps: string[], -): boolean => { - // 无效节点 - if (!treeNode || !treeNode.node) { - return false; - } - - // 过滤条件为空,重置过滤结果 - if (!keywords && filterOps.length === 0) { - treeNode.setFilterReult({ - filterWorking: false, - matchChild: false, - matchSelf: false, - keywords: '', - }); - - (treeNode.children || []).concat(treeNode.slots || []).forEach((childNode) => { - matchTreeNode(childNode, keywords, filterOps); - }); - - return false; - } - - const { node } = treeNode; - - // 命中过滤选项 - const matchFilterOps = filterOps.length === 0 || !!filterOps.find((op: string) => { - switch (op) { - case FilterType.CONDITION: - return node.hasCondition(); - case FilterType.LOOP: - return node.hasLoop(); - case FilterType.LOCKED: - return treeNode.locked; - case FilterType.HIDDEN: - return treeNode.hidden; - default: - return false; - } - }); - - // 命中节点名 - const matchKeywords = typeof treeNode.titleLabel === 'string' && treeNode.titleLabel.indexOf(keywords) > -1; - - // 同时命中才展示(根结点永远命中) - const matchSelf = treeNode.isRoot() || (matchFilterOps && matchKeywords); - - // 命中子节点 - const matchChild = !!(treeNode.children || []).concat(treeNode.slots || []) - .map((childNode: TreeNode) => { - return matchTreeNode(childNode, keywords, filterOps); - }).find(Boolean); - - // 如果命中了子节点,需要将该节点展开 - if (matchChild && treeNode.expandable) { - treeNode.setExpanded(true); - } - - treeNode.setFilterReult({ - filterWorking: true, - matchChild, - matchSelf, - keywords, - }); - - return matchSelf || matchChild; -}; diff --git a/packages/plugin-outline-pane/src/views/filter.tsx b/packages/plugin-outline-pane/src/views/filter.tsx deleted file mode 100644 index db53f8a24..000000000 --- a/packages/plugin-outline-pane/src/views/filter.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { PureComponent } from 'react'; -import './style.less'; -import { IconFilter } from '../icons/filter'; -import { Search, Checkbox, Balloon, Divider } from '@alifd/next'; -import TreeNode from '../controllers/tree-node'; -import { Tree } from '../controllers/tree'; -import { matchTreeNode, FILTER_OPTIONS } from './filter-tree'; - -export default class Filter extends PureComponent< - { - tree: Tree; - }, - { - keywords: string; - filterOps: string[]; - } -> { - state = { - keywords: '', - filterOps: [], - }; - - handleSearchChange = (val: string) => { - this.setState( - { - keywords: val.trim(), - }, - this.filterTree, - ); - }; - - handleOptionChange = (val: string[]) => { - this.setState( - { - filterOps: val, - }, - this.filterTree, - ); - }; - - handleCheckAll = () => { - const { filterOps } = this.state; - const final = - filterOps.length === FILTER_OPTIONS.length ? [] : FILTER_OPTIONS.map((op) => op.value); - - this.handleOptionChange(final); - }; - - filterTree() { - const { tree } = this.props; - const { keywords, filterOps } = this.state; - - matchTreeNode(tree.root as TreeNode, keywords, filterOps); - } - - render() { - const { keywords, filterOps } = this.state; - const indeterminate = filterOps.length > 0 && filterOps.length < FILTER_OPTIONS.length; - const checkAll = filterOps.length === FILTER_OPTIONS.length; - - return ( -
- - - -
- } - > - - {this.props.tree.pluginContext.intlNode('Check All')} - - - - {FILTER_OPTIONS.map((op) => ( - - {this.props.tree.pluginContext.intlNode(op.label)} - - ))} - - - - ); - } -} diff --git a/packages/plugin-outline-pane/src/views/pane.tsx b/packages/plugin-outline-pane/src/views/pane.tsx deleted file mode 100644 index 66f842819..000000000 --- a/packages/plugin-outline-pane/src/views/pane.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { PureComponent } from 'react'; -import { Loading } from '@alifd/next'; -import { PaneController } from '../controllers/pane-controller'; -import TreeView from './tree'; -import './style.less'; -import Filter from './filter'; -import { TreeMaster } from '../controllers/tree-master'; -import { Tree } from '../controllers/tree'; -import { IPublicTypeDisposable } from '@alilc/lowcode-types'; - -export class Pane extends PureComponent<{ - treeMaster: TreeMaster; - controller: PaneController; - hideFilter?: boolean; -}, { - tree: Tree | null; - }> { - private controller; - - private simulatorRendererReadyDispose: IPublicTypeDisposable; - private changeDocumentDispose: IPublicTypeDisposable; - private removeDocumentDispose: IPublicTypeDisposable; - - constructor(props: any) { - super(props); - const { controller, treeMaster } = props; - this.controller = controller; - this.state = { - tree: treeMaster.currentTree, - }; - this.simulatorRendererReadyDispose = this.props.treeMaster.pluginContext?.project?.onSimulatorRendererReady(this.changeTree); - this.changeDocumentDispose = this.props.treeMaster.pluginContext?.project?.onChangeDocument(this.changeTree); - this.removeDocumentDispose = this.props.treeMaster.pluginContext?.project?.onRemoveDocument(this.changeTree); - } - - changeTree = () => { - this.setState({ - tree: this.props.treeMaster.currentTree, - }); - }; - - componentWillUnmount() { - this.controller.purge(); - this.simulatorRendererReadyDispose?.(); - this.changeDocumentDispose?.(); - this.removeDocumentDispose?.(); - } - - render() { - const tree = this.state.tree; - - if (!tree) { - return ( -
-

- -

-
- ); - } - - return ( -
- { !this.props.hideFilter && } -
this.controller.mount(shell)} className={`lc-outline-tree-container ${ this.props.hideFilter ? 'lc-hidden-outline-filter' : '' }`}> - -
-
- ); - } -} diff --git a/packages/plugin-outline-pane/src/views/style.less b/packages/plugin-outline-pane/src/views/style.less deleted file mode 100644 index 8a38ba749..000000000 --- a/packages/plugin-outline-pane/src/views/style.less +++ /dev/null @@ -1,430 +0,0 @@ -.lc-outline-pane { - height: 100%; - width: 100%; - position: relative; - z-index: 200; - - > .lc-outline-tree-container { - top: 52px; - left: 0; - bottom: 0; - right: 0; - position: absolute; - overflow: auto; - } - - > .lc-outline-tree-container.lc-hidden-outline-filter { - top: 0; - } - - > .lc-outline-filter { - padding: 12px 16px; - display: flex; - align-items: stretch; - justify-content: right; - - .lc-outline-filter-search-input { - width: 100%; - } - - .lc-outline-filter-icon { - background: var(--color-block-background-light, #ebecf0); - border: 1px solid var(--color-field-border, #c4c6cf); - display: flex; - align-items: center; - border-radius: 0 2px 2px 0; - overflow: hidden; - margin-left: -2px; - z-index: 1; - padding: 0 6px; - - &:hover { - cursor: pointer; - } - } - } -} - -.lc-outline-tree { - @treeNodeHeight: 30px; - overflow: hidden; - margin-bottom: @treeNodeHeight; - user-select: none; - overflow-x: scroll; - - .tree-node-modal { - margin: 5px; - border: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.2)); - border-radius: 3px; - box-shadow: 0 1px 4px 0 var(--color-block-background-shallow, rgba(31, 56, 88, 0.15)); - - .tree-node-modal-title { - position: relative; - background: var(--color-block-background-light, rgba(31, 56, 88, 0.04)); - padding: 0 10px; - height: 32px; - line-height: 32px; - border-bottom: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.2)); - - .tree-node-modal-title-visible-icon { - position: absolute; - top: 4px; - right: 12px; - cursor: pointer; - } - } - - .tree-pane-modal-content { - & > .tree-node-branches::before { - display: none; - } - } - - .tree-node-modal-radio, - .tree-node-modal-radio-active { - margin-right: 4px; - opacity: 0.8; - position: absolute; - top: 7px; - left: 6px; - } - .tree-node-modal-radio-active { - color: var(--color-brand, #006cff); - } - } - - .tree-node-branches::before { - position: absolute; - display: block; - width: 0; - border-left: 1px solid transparent; - height: 100%; - top: 0; - left: 6px; - content: ' '; - z-index: 2; - pointer-events: none; - } - - &:hover { - .tree-node-branches::before { - border-left-color: var(--color-line-darken, #ddd); - } - } - - .insertion { - pointer-events: all !important; - border: 1px dashed var(--color-brand-light); - height: @treeNodeHeight; - box-sizing: border-box; - transform: translateZ(0); - transition: all 0.2s ease-in-out; - &.invalid { - border-color: var(--color-error, var(--color-function-error, red)); - background-color: var(--color-block-background-error, rgba(240, 154, 154, 0.719)); - } - } - - .condition-group-container { - border-bottom: 1px solid var(--color-brown, var(--color-function-brown, #7b605b)); - position: relative; - - &:before { - position: absolute; - display: block; - width: 0; - border-left: 0.5px solid var(--color-brown, var(--color-function-brown, #7b605b)); - height: 100%; - top: 0; - left: 0; - content: ' '; - z-index: 2; - } - > .condition-group-title { - text-align: center; - background-color: var(--color-brown, var(--color-function-brown, #7b605b)); - height: 14px; - > .lc-title { - font-size: 12px; - transform: scale(0.8); - transform-origin: top; - color: var(--color-text-reverse, white); - text-shadow: 0 0 2px var(--color-block-background-shallow, black); - display: block; - } - } - } - .tree-node-slots { - border-bottom: 1px solid var(--color-purple, var(--color-function-purple, rgb(144, 94, 190))); - position: relative; - &::before { - position: absolute; - display: block; - width: 0; - border-left: 0.5px solid var(--color-purple, var(--color-function-purple, rgb(144, 94, 190))); - height: 100%; - top: 0; - left: 0; - content: ' '; - z-index: 2; - } - > .tree-node-slots-title { - text-align: center; - background-color: var(--color-purple, var(--color-function-purple, rgb(144, 94, 190))); - height: 14px; - > .lc-title { - font-size: 12px; - transform: scale(0.8); - transform-origin: top; - color: var(--color-text-reverse, white); - text-shadow: 0 0 2px black; - display: block; - } - } - &.insertion-at-slots { - padding-bottom: @treeNodeHeight; - border-bottom-color: var(--color-error-dark, var(--color-function-error-dark, rgb(182, 55, 55))); - > .tree-node-slots-title { - background-color: var(--color-error-dark, var(--color-function-error-dark, rgb(182, 55, 55))); - } - &::before { - border-left-color: var(--color-error-dark, var(--color-function-error-dark, rgb(182, 55, 55))); - } - } - } - - .tree-node { - .tree-node-expand-btn { - width: 12px; - line-height: 0; - align-self: stretch; - display: flex; - align-items: center; - transition: color 200ms ease; - color: var(--color-icon-normal); - &:hover { - color: var(--color-icon-hover); - } - > svg { - transform-origin: center; - transform: rotate(-90deg); - transition: transform 100ms ease; - } - margin-right: 4px; - } - .tree-node-expand-placeholder { - width: 12px; - height: 12px; - margin-right: 4px; - } - - .tree-node-icon { - transform: translateZ(0); - display: flex; - align-items: center; - margin-right: 4px; - color: var(--color-text); - - & > svg { - width: 16px; - height: 16px; - * { - fill: var(--color-icon-normal, rgba(31, 56, 88, 0.4)); - } - } - & > img { - width: 16px; - height: 16px; - * { - fill: var(--color-icon-normal, rgba(31, 56, 88, 0.4)); - } - } - } - - .tree-node-title { - font-size: var(--font-size-text); - cursor: pointer; - border-bottom: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1)); - display: flex; - align-items: center; - height: @treeNodeHeight; - box-sizing: border-box; - position: relative; - transform: translateZ(0); - padding-right: 5px; - & > :first-child { - margin-left: 2px; - } - - .tree-node-title-label { - flex: 1; - white-space: nowrap; - text-overflow: ellipsis; - display: flex; - align-items: center; - align-self: stretch; - overflow: visible; - margin-right: 5px; - - .tree-node-title-input { - flex: 1; - border: 1px solid var(--color-brand-light); - background-color: var(--color-pane-background); - color: var(--color-text); - line-height: 18px; - padding: 2px; - outline: none; - margin-left: -3px; - border-radius: 2px; - } - } - - .tree-node-hide-btn, - .tree-node-lock-btn, - .tree-node-rename-btn, - .tree-node-delete-btn { - opacity: 0; - color: var(--color-text); - line-height: 0; - align-self: stretch; - display: flex; - align-items: center; - justify-content: center; - width: 22px; - &:hover { - opacity: 1 !important; - } - } - &:hover { - .tree-node-hide-btn, - .tree-node-lock-btn, - .tree-node-rename-btn, - .tree-node-delete-btn { - opacity: 0.5; - } - } - html.lc-cursor-dragging & { - // FIXME: only hide hover shows - .tree-node-hide-btn, - .tree-node-lock-btn, - .tree-node-rename-btn { - display: none; - } - } - &.editing { - & > .tree-node-hide-btn, - & > .tree-node-lock-btn, - & > .tree-node-rename-btn, - & > .tree-node-delete-btn { - display: none; - } - } - - .tree-node-tag { - margin-left: 5px; - display: flex; - align-items: center; - line-height: 0; - &.cond { - color: var(--color-error, var(--color-function-error, rgb(179, 52, 6))); - } - &.loop { - color: var(--color-success, var(--color-function-success, rgb(103, 187, 187))); - } - &.slot { - color: var(--color-purple, var(--color-function-purple, rgb(211, 90, 211))); - } - } - } - - &.is-root { - > .tree-node-title { - padding-left: 5px; - } - } - - &.expanded { - & > .tree-node-title > .tree-node-expand-btn > svg { - transform: rotate(0); - } - } - - &.detecting > .tree-node-title { - background: var(--color-block-background-light); - } - - // 选中节点处理 - &.selected { - & > .tree-node-title { - background: var(--color-block-background-light); - } - - & > .tree-node-branches::before { - border-left-color: var(--color-brand-light); - } - } - - &.hidden { - .tree-node-title-label { - color: var(--color-text-disabled, #9b9b9b); - } - & > .tree-node-title > .tree-node-hide-btn { - opacity: 0.8; - } - .tree-node-branches { - .tree-node-hide-btn { - opacity: 0; - } - } - } - - &.condition-flow { - & > .tree-node-title > .tree-node-hide-btn { - opacity: 1; - } - &.hidden > .tree-node-title > .tree-node-hide-btn { - opacity: 0; - } - } - - &.locked { - & > .tree-node-title > .tree-node-lock-btn { - opacity: 0.8; - } - .tree-node-branches { - .tree-node-lock-btn, - .tree-node-hide-btn { - opacity: 0; - } - } - } - - // 处理拖入节点 - &.dropping { - & > .tree-node-branches::before { - border-left: 1px solid var(--color-brand); - } - & > .tree-node-title { - .tree-node-expand-btn { - color: var(--color-brand); - } - .tree-node-icon { - color: var(--color-brand); - } - .tree-node-title-label > .lc-title { - color: var(--color-brand); - } - } - } - &.highlight { - & > .tree-node-title { - background: var(--color-block-background-shallow); - } - } - - .tree-node-branches { - padding-left: 12px; - position: relative; - } - } -} diff --git a/packages/plugin-outline-pane/src/views/tree-branches.tsx b/packages/plugin-outline-pane/src/views/tree-branches.tsx deleted file mode 100644 index 8b8b5e6b8..000000000 --- a/packages/plugin-outline-pane/src/views/tree-branches.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { PureComponent } from 'react'; -import classNames from 'classnames'; -import { Title } from '@alilc/lowcode-editor-core'; -import TreeNode from '../controllers/tree-node'; -import TreeNodeView from './tree-node'; -import { IPublicModelExclusiveGroup, IPublicTypeDisposable, IPublicTypeLocationChildrenDetail } from '@alilc/lowcode-types'; - -export default class TreeBranches extends PureComponent<{ - treeNode: TreeNode; - isModal?: boolean; - expanded: boolean; - treeChildren: TreeNode[] | null; -}> { - state = { - filterWorking: false, - matchChild: false, - }; - private offExpandedChanged: (() => void) | null; - constructor(props: any) { - super(props); - - const { treeNode } = this.props; - const { filterWorking, matchChild } = treeNode.filterReult; - this.setState({ filterWorking, matchChild }); - } - - componentDidMount() { - const { treeNode } = this.props; - treeNode.onFilterResultChanged(() => { - const { filterWorking: newFilterWorking, matchChild: newMatchChild } = treeNode.filterReult; - this.setState({ filterWorking: newFilterWorking, matchChild: newMatchChild }); - }); - } - - componentWillUnmount(): void { - if (this.offExpandedChanged) { - this.offExpandedChanged(); - } - } - - render() { - const { treeNode, isModal, expanded } = this.props; - const { filterWorking, matchChild } = this.state; - // 条件过滤生效时,如果命中了子节点,需要将该节点展开 - const expandInFilterResult = filterWorking && matchChild; - - if (!expandInFilterResult && !expanded) { - return null; - } - - return ( -
- { - !isModal && - } - -
- ); - } -} - -interface ITreeNodeChildrenState { - filterWorking: boolean; - matchSelf: boolean; - keywords: string | null; - dropDetail: IPublicTypeLocationChildrenDetail | undefined | null; -} -class TreeNodeChildren extends PureComponent<{ - treeNode: TreeNode; - isModal?: boolean; - treeChildren: TreeNode[] | null; -}, ITreeNodeChildrenState> { - state: ITreeNodeChildrenState = { - filterWorking: false, - matchSelf: false, - keywords: null, - dropDetail: null, - }; - offLocationChanged: IPublicTypeDisposable | undefined; - componentDidMount() { - const { treeNode } = this.props; - const { project } = treeNode.pluginContext; - const { filterWorking, matchSelf, keywords } = treeNode.filterReult; - const { dropDetail } = treeNode; - this.setState({ - filterWorking, - matchSelf, - keywords, - dropDetail, - }); - treeNode.onFilterResultChanged(() => { - const { - filterWorking: newFilterWorking, - matchSelf: newMatchChild, - keywords: newKeywords, - } = treeNode.filterReult; - this.setState({ - filterWorking: newFilterWorking, - matchSelf: newMatchChild, - keywords: newKeywords, - }); - }); - this.offLocationChanged = project.currentDocument?.onDropLocationChanged( - () => { - this.setState({ dropDetail: treeNode.dropDetail }); - }, - ); - } - componentWillUnmount(): void { - this.offLocationChanged && this.offLocationChanged(); - } - - render() { - const { isModal } = this.props; - const children: any = []; - let groupContents: any[] = []; - let currentGrp: IPublicModelExclusiveGroup; - const { filterWorking, matchSelf, keywords } = this.state; - - const endGroup = () => { - if (groupContents.length > 0) { - children.push( -
-
- - </div> - {groupContents} - </div>, - ); - groupContents = []; - } - }; - - const { dropDetail } = this.state; - const dropIndex = dropDetail?.index; - const insertion = ( - <div - key="insertion" - className={classNames('insertion', { - invalid: dropDetail?.valid === false, - })} - /> - ); - this.props.treeChildren?.forEach((child, index) => { - const childIsModal = child.node.componentMeta?.isModal || false; - if (isModal != childIsModal) { - return; - } - const { conditionGroup } = child.node; - if (conditionGroup !== currentGrp) { - endGroup(); - } - - if (conditionGroup) { - currentGrp = conditionGroup; - if (index === dropIndex) { - if (groupContents.length > 0) { - groupContents.push(insertion); - } else { - children.push(insertion); - } - } - groupContents.push(<TreeNodeView key={child.nodeId} treeNode={child} isModal={isModal} />); - } else { - if (index === dropIndex) { - children.push(insertion); - } - children.push(<TreeNodeView key={child.nodeId} treeNode={child} isModal={isModal} />); - } - }); - endGroup(); - const length = this.props.treeChildren?.length || 0; - if (dropIndex != null && dropIndex >= length) { - children.push(insertion); - } - - return <div className="tree-node-children">{children}</div>; - } -} - -class TreeNodeSlots extends PureComponent<{ - treeNode: TreeNode; -}> { - render() { - const { treeNode } = this.props; - if (!treeNode.hasSlots()) { - return null; - } - return ( - <div - className={classNames('tree-node-slots', { - 'insertion-at-slots': treeNode.dropDetail?.focus?.type === 'slots', - })} - data-id={treeNode.nodeId} - > - <div className="tree-node-slots-title"> - <Title title={{ type: 'i18n', intl: this.props.treeNode.pluginContext.intlNode('Slots') }} /> - </div> - {treeNode.slots.map(tnode => ( - <TreeNodeView key={tnode.nodeId} treeNode={tnode} /> - ))} - </div> - ); - } -} diff --git a/packages/plugin-outline-pane/src/views/tree-node.tsx b/packages/plugin-outline-pane/src/views/tree-node.tsx deleted file mode 100644 index 59f786628..000000000 --- a/packages/plugin-outline-pane/src/views/tree-node.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { PureComponent } from 'react'; -import classNames from 'classnames'; -import TreeNode from '../controllers/tree-node'; -import TreeTitle from './tree-title'; -import TreeBranches from './tree-branches'; -import { IconEyeClose } from '../icons/eye-close'; -import { IPublicModelModalNodesManager, IPublicTypeDisposable } from '@alilc/lowcode-types'; -import { IOutlinePanelPluginContext } from '../controllers/tree-master'; - -class ModalTreeNodeView extends PureComponent< - { - treeNode: TreeNode; - }, - { - treeChildren: TreeNode[] | null; - } -> { - private modalNodesManager: IPublicModelModalNodesManager | undefined | null; - readonly pluginContext: IOutlinePanelPluginContext; - - constructor(props: { treeNode: TreeNode }) { - super(props); - - // 模态管理对象 - this.pluginContext = props.treeNode.pluginContext; - const { project } = this.pluginContext; - this.modalNodesManager = project.currentDocument?.modalNodesManager; - this.state = { - treeChildren: this.rootTreeNode.children, - }; - } - - hideAllNodes() { - this.modalNodesManager?.hideModalNodes(); - } - - componentDidMount(): void { - const { rootTreeNode } = this; - rootTreeNode.onExpandableChanged(() => { - this.setState({ - treeChildren: rootTreeNode.children, - }); - }); - } - - get rootTreeNode() { - const { treeNode } = this.props; - // 当指定了新的根节点时,要从原始的根节点去获取模态节点 - const { project } = this.pluginContext; - const rootNode = project.currentDocument?.root; - const rootTreeNode = treeNode.tree.getTreeNode(rootNode!); - - return rootTreeNode; - } - - render() { - const { rootTreeNode } = this; - const { expanded } = rootTreeNode; - - const hasVisibleModalNode = !!this.modalNodesManager?.getVisibleModalNode(); - return ( - <div className="tree-node-modal"> - <div className="tree-node-modal-title"> - <span>{this.pluginContext.intlNode('Modal View')}</span> - <div - className="tree-node-modal-title-visible-icon" - onClick={this.hideAllNodes.bind(this)} - > - {hasVisibleModalNode ? <IconEyeClose /> : null} - </div> - </div> - <div className="tree-pane-modal-content"> - <TreeBranches - treeNode={rootTreeNode} - treeChildren={this.state.treeChildren} - expanded={expanded} - isModal - /> - </div> - </div> - ); - } -} - -export default class TreeNodeView extends PureComponent<{ - treeNode: TreeNode; - isModal?: boolean; - isRootNode?: boolean; -}> { - state: { - expanded: boolean; - selected: boolean; - hidden: boolean; - locked: boolean; - detecting: boolean; - isRoot: boolean; - highlight: boolean; - dropping: boolean; - conditionFlow: boolean; - expandable: boolean; - treeChildren: TreeNode[] | null; - filterWorking: boolean; - matchChild: boolean; - matchSelf: boolean; - } = { - expanded: false, - selected: false, - hidden: false, - locked: false, - detecting: false, - isRoot: false, - highlight: false, - dropping: false, - conditionFlow: false, - expandable: false, - treeChildren: [], - filterWorking: false, - matchChild: false, - matchSelf: false, - }; - - eventOffCallbacks: Array<IPublicTypeDisposable | undefined> = []; - constructor(props: any) { - super(props); - - const { treeNode, isRootNode } = this.props; - this.state = { - expanded: isRootNode ? true : treeNode.expanded, - selected: treeNode.selected, - hidden: treeNode.hidden, - locked: treeNode.locked, - detecting: treeNode.detecting, - isRoot: treeNode.isRoot(), - // 是否投放响应 - dropping: treeNode.dropDetail?.index != null, - conditionFlow: treeNode.node.conditionGroup != null, - highlight: treeNode.isFocusingNode(), - expandable: treeNode.expandable, - treeChildren: treeNode.children, - } as any; - } - - componentDidMount() { - const { treeNode } = this.props; - const { project } = treeNode.pluginContext; - - const doc = project.currentDocument; - - treeNode.onExpandedChanged((expanded: boolean) => { - this.setState({ expanded }); - }); - treeNode.onHiddenChanged((hidden: boolean) => { - this.setState({ hidden }); - }); - treeNode.onLockedChanged((locked: boolean) => { - this.setState({ locked }); - }); - treeNode.onExpandableChanged((expandable: boolean) => { - this.setState({ - expandable, - treeChildren: treeNode.children, - }); - }); - treeNode.onFilterResultChanged(() => { - const { - filterWorking: newFilterWorking, - matchChild: newMatchChild, - matchSelf: newMatchSelf, - } = treeNode.filterReult; - this.setState({ - filterWorking: newFilterWorking, - matchChild: newMatchChild, - matchSelf: newMatchSelf, - }); - }); - this.eventOffCallbacks.push( - doc?.onDropLocationChanged(() => { - this.setState({ - dropping: treeNode.dropDetail?.index != null, - }); - }), - ); - - const offSelectionChange = doc?.selection?.onSelectionChange(() => { - this.setState({ selected: treeNode.selected }); - }); - this.eventOffCallbacks.push(offSelectionChange!); - const offDetectingChange = doc?.detecting?.onDetectingChange(() => { - this.setState({ detecting: treeNode.detecting }); - }); - this.eventOffCallbacks.push(offDetectingChange!); - } - componentWillUnmount(): void { - this.eventOffCallbacks?.forEach((offFun: IPublicTypeDisposable | undefined) => { - offFun && offFun(); - }); - } - - shouldShowModalTreeNode(): boolean { - const { treeNode, isRootNode } = this.props; - if (!isRootNode) { - // 只在 当前树 的根节点展示模态节点 - return false; - } - - // 当指定了新的根节点时,要从原始的根节点去获取模态节点 - const { project } = treeNode.pluginContext; - const rootNode = project.currentDocument?.root; - const rootTreeNode = treeNode.tree.getTreeNode(rootNode!); - const modalNodes = rootTreeNode.children?.filter((item) => { - return item.node.componentMeta?.isModal; - }); - return !!(modalNodes && modalNodes.length > 0); - } - - render() { - const { treeNode, isModal, isRootNode } = this.props; - const className = classNames('tree-node', { - // 是否展开 - expanded: this.state.expanded, - // 是否选中的 - selected: this.state.selected, - // 是否隐藏的 - hidden: this.state.hidden, - // 是否锁定的 - locked: this.state.locked, - // 是否悬停中 - detecting: this.state.detecting, - // 是否投放响应 - dropping: this.state.dropping, - 'is-root': this.state.isRoot, - 'condition-flow': this.state.conditionFlow, - highlight: this.state.highlight, - }); - const shouldShowModalTreeNode: boolean = this.shouldShowModalTreeNode(); - - // filter 处理 - const { filterWorking, matchChild, matchSelf } = this.state; - if (!isRootNode && filterWorking && !matchChild && !matchSelf) { - // 条件过滤生效时,如果未命中本节点或子节点,则不展示该节点 - // 根节点始终展示 - return null; - } - return ( - <div className={className} data-id={treeNode.nodeId}> - <TreeTitle - treeNode={treeNode} - isModal={isModal} - expanded={this.state.expanded} - hidden={this.state.hidden} - locked={this.state.locked} - expandable={this.state.expandable} - /> - {shouldShowModalTreeNode && <ModalTreeNodeView treeNode={treeNode} />} - <TreeBranches - treeNode={treeNode} - isModal={false} - expanded={this.state.expanded} - treeChildren={this.state.treeChildren} - /> - </div> - ); - } -} diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx deleted file mode 100644 index 77e71955d..000000000 --- a/packages/plugin-outline-pane/src/views/tree-title.tsx +++ /dev/null @@ -1,387 +0,0 @@ -import { KeyboardEvent, FocusEvent, Fragment, PureComponent } from 'react'; -import classNames from 'classnames'; -import { Title, Tip } from '@alilc/lowcode-designer'; -import { createIcon } from '@alilc/lowcode-utils'; -import { IPublicApiEvent } from '@alilc/lowcode-types'; -import TreeNode from '../controllers/tree-node'; -import { - IconLock, - IconUnlock, - IconArrowRight, - IconEyeClose, - IconEye, - IconCond, - IconLoop, - IconRadioActive, - IconRadio, - IconSetting, - IconDelete, -} from '../icons'; - -function emitOutlineEvent( - event: IPublicApiEvent, - type: string, - treeNode: TreeNode, - rest?: Record<string, unknown>, -) { - const node = treeNode?.node; - const npm = node?.componentMeta?.npm; - const selected = - [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || - node?.componentMeta?.componentName || - ''; - event.emit(`outlinePane.${type}`, { - selected, - ...rest, - }); -} - -export default class TreeTitle extends PureComponent<{ - treeNode: TreeNode; - isModal?: boolean; - expanded: boolean; - hidden: boolean; - locked: boolean; - expandable: boolean; -}> { - state: { - editing: boolean; - title: string; - condition?: boolean; - visible?: boolean; - filterWorking: boolean; - keywords: string; - matchSelf: boolean; - } = { - editing: false, - title: '', - filterWorking: false, - keywords: '', - matchSelf: false, - }; - - private lastInput?: HTMLInputElement; - - private enableEdit = (e: MouseEvent) => { - e.preventDefault(); - this.setState({ - editing: true, - }); - }; - - private cancelEdit() { - this.setState({ - editing: false, - }); - this.lastInput = undefined; - } - - private saveEdit = (e: FocusEvent<HTMLInputElement> | KeyboardEvent<HTMLInputElement>) => { - const { treeNode } = this.props; - const value = (e.target as HTMLInputElement).value || ''; - treeNode.setTitleLabel(value); - emitOutlineEvent(this.props.treeNode.pluginContext.event, 'rename', treeNode, { value }); - this.cancelEdit(); - }; - - private handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => { - if (e.keyCode === 13) { - this.saveEdit(e); - } - if (e.keyCode === 27) { - this.cancelEdit(); - } - }; - - private setCaret = (input: HTMLInputElement | null) => { - if (!input || this.lastInput === input) { - return; - } - input.focus(); - input.select(); - // 光标定位最后一个 - // input.selectionStart = input.selectionEnd; - }; - - componentDidMount() { - const { treeNode } = this.props; - this.setState({ - editing: false, - title: treeNode.titleLabel, - condition: treeNode.condition, - visible: !treeNode.hidden, - }); - treeNode.onTitleLabelChanged(() => { - this.setState({ - title: treeNode.titleLabel, - }); - }); - treeNode.onConditionChanged(() => { - this.setState({ - condition: treeNode.condition, - }); - }); - treeNode.onHiddenChanged((hidden: boolean) => { - this.setState({ - visible: !hidden, - }); - }); - treeNode.onFilterResultChanged(() => { - const { - filterWorking: newFilterWorking, - keywords: newKeywords, - matchSelf: newMatchSelf, - } = treeNode.filterReult; - this.setState({ - filterWorking: newFilterWorking, - keywords: newKeywords, - matchSelf: newMatchSelf, - }); - }); - } - deleteClick = () => { - const { treeNode } = this.props; - const { node } = treeNode; - treeNode.deleteNode(node); - }; - render() { - const { treeNode, isModal } = this.props; - const { pluginContext } = treeNode; - const { editing, filterWorking, matchSelf, keywords } = this.state; - const isCNode = !treeNode.isRoot(); - const { node } = treeNode; - const { componentMeta } = node; - const availableActions = componentMeta - ? componentMeta.availableActions.map((availableAction) => availableAction.name) - : []; - const isNodeParent = node.isParentalNode; - const isContainer = node.isContainerNode; - let style: any; - if (isCNode) { - const { depth } = treeNode; - const indent = depth * 12; - style = { - paddingLeft: indent + (isModal ? 12 : 0), - marginLeft: -indent, - }; - } - const Extra = pluginContext.extraTitle as any; - const { intlNode, config } = pluginContext; - const couldHide = availableActions.includes('hide'); - const couldLock = availableActions.includes('lock'); - const couldUnlock = availableActions.includes('unlock'); - const shouldShowHideBtn = isCNode && isNodeParent && !isModal && couldHide; - const shouldShowLockBtn = - config.get('enableCanvasLock', false) && - isContainer && - isCNode && - isNodeParent && - ((couldLock && !node.isLocked) || (couldUnlock && node.isLocked)); - const shouldEditBtn = isCNode && isNodeParent; - const shouldDeleteBtn = isCNode && isNodeParent && node?.canPerformAction('remove'); - return ( - <div - className={classNames('tree-node-title', { editing })} - style={style} - data-id={treeNode.nodeId} - onClick={() => { - if (isModal) { - if (this.state.visible) { - node.document?.modalNodesManager?.setInvisible(node); - } else { - node.document?.modalNodesManager?.setVisible(node); - } - return; - } - if (node.conditionGroup) { - node.setConditionalVisible(); - } - }} - > - {isModal && this.state.visible && ( - <div - onClick={() => { - node.document?.modalNodesManager?.setInvisible(node); - }} - > - <IconRadioActive className="tree-node-modal-radio-active" /> - </div> - )} - {isModal && !this.state.visible && ( - <div - onClick={() => { - node.document?.modalNodesManager?.setVisible(node); - }} - > - <IconRadio className="tree-node-modal-radio" /> - </div> - )} - {isCNode && ( - <ExpandBtn - expandable={this.props.expandable} - expanded={this.props.expanded} - treeNode={treeNode} - /> - )} - <div className="tree-node-icon">{createIcon(treeNode.icon)}</div> - <div className="tree-node-title-label"> - {editing ? ( - <input - className="tree-node-title-input" - defaultValue={this.state.title} - onBlur={this.saveEdit} - ref={this.setCaret} - onKeyUp={this.handleKeyUp} - /> - ) : ( - <Fragment> - <Title - title={this.state.title} - match={filterWorking && matchSelf} - keywords={keywords} - /> - {Extra && <Extra node={treeNode?.node} />} - {node.slotFor && ( - <a className="tree-node-tag slot"> - {/* todo: click redirect to prop */} - <Tip>{intlNode('Slot for {prop}', { prop: node.slotFor.key })}</Tip> - </a> - )} - {node.hasLoop() && ( - <a className="tree-node-tag loop"> - {/* todo: click todo something */} - <IconLoop /> - <Tip>{intlNode('Loop')}</Tip> - </a> - )} - {this.state.condition && ( - <a className="tree-node-tag cond"> - {/* todo: click todo something */} - <IconCond /> - <Tip>{intlNode('Conditional')}</Tip> - </a> - )} - </Fragment> - )} - </div> - {shouldShowHideBtn && <HideBtn hidden={this.props.hidden} treeNode={treeNode} />} - {shouldShowLockBtn && <LockBtn locked={this.props.locked} treeNode={treeNode} />} - {shouldEditBtn && <RenameBtn treeNode={treeNode} onClick={this.enableEdit} />} - {shouldDeleteBtn && <DeleteBtn treeNode={treeNode} onClick={this.deleteClick} />} - </div> - ); - } -} - -class DeleteBtn extends PureComponent<{ - treeNode: TreeNode; - onClick: () => void; -}> { - render() { - const { intl } = this.props.treeNode.pluginContext; - return ( - <div className="tree-node-delete-btn" onClick={this.props.onClick}> - <IconDelete /> - <Tip>{intl('Delete')}</Tip> - </div> - ); - } -} - -class RenameBtn extends PureComponent<{ - treeNode: TreeNode; - onClick: (e: any) => void; -}> { - render() { - const { intl } = this.props.treeNode.pluginContext; - return ( - <div className="tree-node-rename-btn" onClick={this.props.onClick}> - <IconSetting /> - <Tip>{intl('Rename')}</Tip> - </div> - ); - } -} - -class LockBtn extends PureComponent<{ - treeNode: TreeNode; - locked: boolean; -}> { - render() { - const { treeNode, locked } = this.props; - const { intl } = this.props.treeNode.pluginContext; - return ( - <div - className="tree-node-lock-btn" - onClick={(e) => { - e.stopPropagation(); - treeNode.setLocked(!locked); - }} - > - {locked ? <IconUnlock /> : <IconLock />} - {/* @ts-ignore */} - <Tip>{locked ? intl('Unlock') : intl('Lock')}</Tip> - </div> - ); - } -} - -class HideBtn extends PureComponent< - { - treeNode: TreeNode; - hidden: boolean; - }, - { - hidden: boolean; - } -> { - render() { - const { treeNode, hidden } = this.props; - const { intl } = treeNode.pluginContext; - return ( - <div - className="tree-node-hide-btn" - onClick={(e) => { - e.stopPropagation(); - emitOutlineEvent(treeNode.pluginContext.event, hidden ? 'show' : 'hide', treeNode); - treeNode.setHidden(!hidden); - }} - > - {hidden ? <IconEye /> : <IconEyeClose />} - {/* @ts-ignore */} - <Tip>{hidden ? intl('Show') : intl('Hide')}</Tip> - </div> - ); - } -} - -class ExpandBtn extends PureComponent<{ - treeNode: TreeNode; - expanded: boolean; - expandable: boolean; -}> { - render() { - const { treeNode, expanded, expandable } = this.props; - if (!expandable) { - return <i className="tree-node-expand-placeholder" />; - } - return ( - <div - className="tree-node-expand-btn" - onClick={(e) => { - if (expanded) { - e.stopPropagation(); - } - emitOutlineEvent( - treeNode.pluginContext.event, - expanded ? 'collapse' : 'expand', - treeNode, - ); - treeNode.setExpanded(!expanded); - }} - > - <IconArrowRight size="small" /> - </div> - ); - } -} diff --git a/packages/plugin-outline-pane/src/views/tree.tsx b/packages/plugin-outline-pane/src/views/tree.tsx deleted file mode 100644 index 5d9a99f04..000000000 --- a/packages/plugin-outline-pane/src/views/tree.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { MouseEvent as ReactMouseEvent, PureComponent } from 'react'; -import { isFormEvent, canClickNode, isShaken } from '@alilc/lowcode-utils'; -import { Tree } from '../controllers/tree'; -import TreeNodeView from './tree-node'; -import { IPublicEnumDragObjectType, IPublicModelNode } from '@alilc/lowcode-types'; -import TreeNode from '../controllers/tree-node'; - -function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string { - let target: Element | null = e.target as Element; - if (!target || !stop.contains(target)) { - return null; - } - target = target.closest('[data-id]'); - if (!target || !stop.contains(target)) { - return null; - } - - return (target as HTMLDivElement).dataset.id || null; -} - -export default class TreeView extends PureComponent<{ - tree: Tree; -}> { - private shell: HTMLDivElement | null = null; - - private ignoreUpSelected = false; - - private boostEvent?: MouseEvent; - - state: { - root: TreeNode | null; - } = { - root: null, - }; - - private hover(e: ReactMouseEvent) { - const { project } = this.props.tree.pluginContext; - const detecting = project.currentDocument?.detecting; - if (detecting?.enable) { - return; - } - const node = this.getTreeNodeFromEvent(e)?.node; - node?.id && detecting?.capture(node.id); - } - - private onClick = (e: ReactMouseEvent) => { - if (this.ignoreUpSelected) { - this.boostEvent = undefined; - return; - } - if (this.boostEvent && isShaken(this.boostEvent, e.nativeEvent)) { - this.boostEvent = undefined; - return; - } - this.boostEvent = undefined; - const treeNode = this.getTreeNodeFromEvent(e); - if (!treeNode) { - return; - } - const { node } = treeNode; - - if (!canClickNode(node, e)) { - return; - } - - const { project, event, canvas } = this.props.tree.pluginContext; - const doc = project.currentDocument; - const selection = doc?.selection; - const focusNode = doc?.focusNode; - const { id } = node; - const isMulti = e.metaKey || e.ctrlKey || e.shiftKey; - canvas.activeTracker?.track(node); - if (isMulti && focusNode && !node.contains(focusNode) && selection?.has(id)) { - if (!isFormEvent(e.nativeEvent)) { - selection.remove(id); - } - } else { - selection?.select(id); - const selectedNode = selection?.getNodes()?.[0]; - const npm = selectedNode?.componentMeta?.npm; - const selected = - [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || - selectedNode?.componentMeta?.componentName || - ''; - event.emit('outlinePane.select', { - selected, - }); - } - }; - - private onDoubleClick = (e: ReactMouseEvent) => { - e.preventDefault(); - const treeNode = this.getTreeNodeFromEvent(e); - if (treeNode?.nodeId === this.state.root?.nodeId) { - return; - } - if (!treeNode?.expanded) { - this.props.tree.expandAllDecendants(treeNode); - } else { - this.props.tree.collapseAllDecendants(treeNode); - } - }; - - private onMouseOver = (e: ReactMouseEvent) => { - this.hover(e); - }; - - private getTreeNodeFromEvent(e: ReactMouseEvent) { - if (!this.shell) { - return; - } - const id = getTreeNodeIdByEvent(e, this.shell); - if (!id) { - return; - } - - const { tree } = this.props; - return tree.getTreeNodeById(id); - } - - private onMouseDown = (e: ReactMouseEvent) => { - if (isFormEvent(e.nativeEvent)) { - return; - } - const treeNode = this.getTreeNodeFromEvent(e); - if (!treeNode) { - return; - } - - const { node } = treeNode; - - if (!canClickNode(node, e)) { - return; - } - const { project, canvas } = this.props.tree.pluginContext; - const selection = project.currentDocument?.selection; - const focusNode = project.currentDocument?.focusNode; - - // TODO: shift selection - const isMulti = e.metaKey || e.ctrlKey || e.shiftKey; - const isLeftButton = e.button === 0; - - if (isLeftButton && focusNode && !node.contains(focusNode)) { - let nodes: IPublicModelNode[] = [node]; - this.ignoreUpSelected = false; - if (isMulti) { - // multi select mode, directily add - if (!selection?.has(node.id)) { - canvas.activeTracker?.track(node); - selection?.add(node.id); - this.ignoreUpSelected = true; - } - // todo: remove rootNodes id - selection?.remove(focusNode.id); - // 获得顶层 nodes - if (selection) { - nodes = selection.getTopNodes(); - } - } else if (selection?.has(node.id)) { - nodes = selection.getTopNodes(); - } - this.boostEvent = e.nativeEvent; - canvas.dragon?.boost( - { - type: IPublicEnumDragObjectType.Node, - nodes, - }, - this.boostEvent, - ); - } - }; - - private onMouseLeave = () => { - const { pluginContext } = this.props.tree; - const { project } = pluginContext; - const doc = project.currentDocument; - doc?.detecting.leave(); - }; - - componentDidMount() { - const { tree } = this.props; - const { root } = tree; - const { project } = tree.pluginContext; - this.setState({ root }); - const doc = project.currentDocument; - doc?.onFocusNodeChanged(() => { - this.setState({ - root: tree.root, - }); - }); - doc?.onImportSchema(() => { - this.setState({ - root: tree.root, - }); - }); - } - - render() { - if (!this.state.root) { - return null; - } - return ( - <div - className="lc-outline-tree" - ref={(shell) => { this.shell = shell; }} - onMouseDownCapture={this.onMouseDown} - onMouseOver={this.onMouseOver} - onClick={this.onClick} - onDoubleClick={this.onDoubleClick} - onMouseLeave={this.onMouseLeave} - > - <TreeNodeView - key={this.state.root?.id} - treeNode={this.state.root} - isRootNode - /> - </div> - ); - } -} diff --git a/packages/plugin-outline-pane/tsconfig.declaration.json b/packages/plugin-outline-pane/tsconfig.declaration.json deleted file mode 100644 index 54e57efbd..000000000 --- a/packages/plugin-outline-pane/tsconfig.declaration.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "emitDeclarationOnly": true, - "declaration": true, - "outDir": "temp", - "stripInternal": true, - "paths": {} - } -} diff --git a/packages/plugin-outline-pane/tsconfig.json b/packages/plugin-outline-pane/tsconfig.json deleted file mode 100644 index 039e0b4d1..000000000 --- a/packages/plugin-outline-pane/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": ["src"] -} diff --git a/packages/plugin-outline-pane/vite.config.ts b/packages/plugin-outline-pane/vite.config.ts deleted file mode 100644 index ae1df800f..000000000 --- a/packages/plugin-outline-pane/vite.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vite'; -import baseConfigFn from '../../vite.base.config' - -export default defineConfig(async () => { - return baseConfigFn({ - name: 'LowCodePluginOutlinePane', - defaultFormats: ['es', 'cjs'], - entry: 'src/index.tsx', - }) -}); diff --git a/packages/react-renderer/src/runtime/createComponent.tsx b/packages/react-renderer/src/runtime/createComponent.tsx index 03e300615..2c9accf65 100644 --- a/packages/react-renderer/src/runtime/createComponent.tsx +++ b/packages/react-renderer/src/runtime/createComponent.tsx @@ -1,8 +1,4 @@ -import { - invariant, - isLowCodeComponentPackage, - type ComponentTreeRoot, -} from '@alilc/lowcode-shared'; +import { invariant, specTypes, type ComponentTreeRoot } from '@alilc/lowcode-shared'; import { forwardRef, useRef, useEffect } from 'react'; import { isValidElementType } from 'react-is'; import { useRendererContext } from '../api/context'; @@ -45,7 +41,7 @@ export function getComponentByName( ): ReactComponent { const result = lowCodeComponentsCache.get(name) || packageManager.getComponent(name); - if (isLowCodeComponentPackage(result)) { + if (specTypes.isLowCodeComponentPackage(result)) { const { schema, ...metadata } = result; const lowCodeComponent = createComponent(schema, { diff --git a/packages/react-renderer/src/runtime/elements.tsx b/packages/react-renderer/src/runtime/elements.tsx index 48a798d8f..3bafc53e9 100644 --- a/packages/react-renderer/src/runtime/elements.tsx +++ b/packages/react-renderer/src/runtime/elements.tsx @@ -12,7 +12,13 @@ import { type JSSlot, type JSI18n, } from '@alilc/lowcode-shared'; -import { type ComponentType, type ReactInstance, useMemo, createElement } from 'react'; +import { + type ComponentType, + type ReactInstance, + useMemo, + createElement, + type ReactNode, +} from 'react'; import { useRendererContext } from '../api/context'; import { useReactiveStore } from './hooks/useReactiveStore'; import { getComponentByName, type ComponentOptions } from './createComponent'; @@ -32,7 +38,7 @@ export function createElementByWidget( widget: ReactWidget, codeRuntime: ICodeRuntime, options: ComponentOptions, -) { +): ReactNode { const getElement = (widget: ReactWidget) => { const { key, rawNode } = widget; diff --git a/packages/renderer-core/package.json b/packages/renderer-core/package.json index edda8eb8a..443ca2ac8 100644 --- a/packages/renderer-core/package.json +++ b/packages/renderer-core/package.json @@ -34,9 +34,6 @@ "devDependencies": { "@types/lodash-es": "^4.17.12" }, - "peerDependencies": { - "@alilc/lowcode-shared": "workspace:*" - }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" diff --git a/packages/renderer-core/src/main.ts b/packages/renderer-core/src/main.ts index 3a7750c04..40eed64f6 100644 --- a/packages/renderer-core/src/main.ts +++ b/packages/renderer-core/src/main.ts @@ -1,4 +1,4 @@ -import { Injectable, invariant, InstantiationService } from '@alilc/lowcode-shared'; +import { invariant, InstantiationService } from '@alilc/lowcode-shared'; import { ICodeRuntimeService } from './services/code-runtime'; import { IBoostsService, @@ -12,7 +12,6 @@ import { ILifeCycleService, LifecyclePhase } from './services/lifeCycleService'; import { IComponentTreeModelService } from './services/model'; import type { AppOptions, RendererApplication } from './types'; -@Injectable() export class RendererMain<RenderObject> { private mode: 'development' | 'production' = 'production'; @@ -96,8 +95,6 @@ export function createRenderer<RenderObject = IRenderObject>( invariant(typeof renderAdapter === 'function', 'The first parameter must be a function.'); const instantiationService = new InstantiationService({ defaultScope: 'Singleton' }); - instantiationService.bootstrapModules(); - const rendererMain = instantiationService.createInstance( RendererMain, ) as RendererMain<RenderObject>; diff --git a/packages/renderer-core/src/services/code-runtime/codeScope.ts b/packages/renderer-core/src/services/code-runtime/codeScope.ts index c56e4da01..393bc618d 100644 --- a/packages/renderer-core/src/services/code-runtime/codeScope.ts +++ b/packages/renderer-core/src/services/code-runtime/codeScope.ts @@ -1,4 +1,4 @@ -import { type StringDictionary } from '@alilc/lowcode-shared'; +import { type StringDictionary, LinkedListNode } from '@alilc/lowcode-shared'; import { trustedGlobals } from './globals-es2015'; /* @@ -17,24 +17,13 @@ export interface ICodeScope<T extends StringDictionary = StringDictionary> { createChild<V extends StringDictionary = StringDictionary>(initValue: Partial<V>): ICodeScope<V>; } -/** - * 双链表实现父域值的获取 - */ -interface IScopeNode<T extends StringDictionary> { - parent?: IScopeNode<StringDictionary>; - current: Partial<T>; -} - export class CodeScope<T extends StringDictionary = StringDictionary> implements ICodeScope<T> { - __node: IScopeNode<T>; + node = LinkedListNode.Undefined; private proxyValue: T; constructor(initValue: Partial<T>) { - this.__node = { - current: initValue, - }; - + this.node.current = initValue; this.proxyValue = this.createProxy(); } @@ -43,20 +32,20 @@ export class CodeScope<T extends StringDictionary = StringDictionary> implements } set(name: keyof T, value: any): void { - this.__node.current[name] = value; + this.node.current[name] = value; } setValue(value: Partial<T>, replace = false) { if (replace) { - this.__node.current = { ...value }; + this.node.current = { ...value }; } else { - this.__node.current = Object.assign({}, this.__node.current, value); + this.node.current = Object.assign({}, this.node.current, value); } } createChild<V extends StringDictionary = StringDictionary>(initValue: Partial<V>): ICodeScope<V> { const childScope = new CodeScope(initValue); - childScope.__node.parent = this.__node; + childScope.node.prev = this.node; return childScope; } @@ -75,24 +64,24 @@ export class CodeScope<T extends StringDictionary = StringDictionary> implements private findValue(prop: PropertyKey) { if (prop === Symbol.unscopables) return unscopables; - let node: IScopeNode<StringDictionary> | undefined = this.__node; + let node = this.node; while (node) { if (Object.hasOwnProperty.call(node.current, prop)) { return node.current[prop as string]; } - node = node.parent; + node = node.prev; } } private hasProperty(prop: PropertyKey): boolean { if (prop in unscopables) return true; - let node: IScopeNode<StringDictionary> | undefined = this.__node; + let node = this.node; while (node) { if (prop in node.current) { return true; } - node = node.parent; + node = node.prev; } return false; diff --git a/packages/renderer-core/src/services/extension/boosts.ts b/packages/renderer-core/src/services/extension/boosts.ts index 085551c96..4a0c1cdd8 100644 --- a/packages/renderer-core/src/services/extension/boosts.ts +++ b/packages/renderer-core/src/services/extension/boosts.ts @@ -9,7 +9,10 @@ export type IBoosts<Extends> = IBoostsApi & Extends & { [key: string]: any }; export interface IBoostsApi { readonly codeRuntime: ICodeRuntime; - readonly intl: Pick<IRuntimeIntlService, 't' | 'setLocale' | 'getLocale' | 'addTranslations'>; + readonly intl: Pick< + IRuntimeIntlService, + 'localize' | 'setLocale' | 'getLocale' | 'addTranslations' + >; readonly util: Pick<IRuntimeUtilService, 'add' | 'remove'>; /** diff --git a/packages/renderer-core/src/services/extension/extensionHostService.ts b/packages/renderer-core/src/services/extension/extensionHostService.ts index c0010f7fb..fdd2115d5 100644 --- a/packages/renderer-core/src/services/extension/extensionHostService.ts +++ b/packages/renderer-core/src/services/extension/extensionHostService.ts @@ -1,9 +1,10 @@ -import { createDecorator, Provide, EventEmitter, KeyValueStore } from '@alilc/lowcode-shared'; +import { createDecorator, Provide, EventEmitter } from '@alilc/lowcode-shared'; import { type Plugin, type PluginContext } from './plugin'; import { IBoostsService } from './boosts'; import { IPackageManagementService } from '../package'; import { ISchemaService } from '../schema'; import { ILifeCycleService } from '../lifeCycleService'; +import { KeyValueStore } from '../../utils/store'; interface IPluginRuntime extends Plugin { status: 'setup' | 'ready'; diff --git a/packages/renderer-core/src/services/package/managementService.ts b/packages/renderer-core/src/services/package/managementService.ts index 99838e10d..ea2c117d4 100644 --- a/packages/renderer-core/src/services/package/managementService.ts +++ b/packages/renderer-core/src/services/package/managementService.ts @@ -6,6 +6,9 @@ import { createDecorator, Provide, specTypes, + exportByReference, + mapPackageToId, + type Reference, } from '@alilc/lowcode-shared'; import { get as lodashGet } from 'lodash-es'; import { PackageLoader } from './loader'; @@ -29,11 +32,7 @@ export interface IPackageManagementService { /** 通过包名获取资产包信息 */ getPackageInfo(packageName: string): Package | undefined; - getLibraryByPackageName(packageName: string): any; - - setLibraryByPackageName(packageName: string, library: any): void; - - getLibraryByComponentMap(componentMap: ComponentMap): { key: string; value: any } | undefined; + getModuleByReference<T = any>(reference: Reference): T | undefined; /** 解析组件映射 */ resolveComponentMaps(componentMaps: ComponentMap[]): void; @@ -91,46 +90,13 @@ export class PackageManagementService implements IPackageManagementService { return this.packagesMap.get(packageName)?.raw; } - getLibraryByPackageName(packageName: string) { - const packageInfo = this.getPackageInfo(packageName); + getModuleByReference<T = any>(reference: Reference): T | undefined { + const id = mapPackageToId(reference); + if (this.packageStore.has(id)) { + const library = this.packageStore.get(id); + const result = exportByReference(library, reference); - if (packageInfo) { - return this.packageStore.get(packageInfo.package ?? packageInfo.id!); - } - } - - setLibraryByPackageName(packageName: string, library: any) { - this.packageStore.set(packageName, library); - } - - getLibraryByComponentMap(componentMap: ComponentMap): { key: string; value: any } | undefined { - if (!componentMap.componentName && !componentMap.exportName) return; - - if (componentMap.package && this.packageStore.has(componentMap.package)) { - const library = this.packageStore.get(componentMap.package!); - // export { exportName } from xxx exportName === global.libraryName.exportName - // export exportName from xxx exportName === global.libraryName.default || global.libraryName - // const componentName = exportName.subName, if exportName empty subName donot use - const paths = - componentMap.exportName && componentMap.subName ? componentMap.subName.split('.') : []; - - // if exportName === nil, exportName === componentName; - const exportName = componentMap.exportName ?? componentMap.componentName; - - if (exportName && componentMap.destructuring) { - paths.unshift(exportName); - } - - let result = library; - for (const path of paths) { - result = result[path] || result; - } - - // export { exportName as componentName } from package - return { - key: componentMap.componentName ?? componentMap.exportName!, - value: result, - }; + return result; } } @@ -143,9 +109,9 @@ export class PackageManagementService implements IPackageManagementService { this.componentsRecord[map.componentName] = packageInfo; } } else { - const result = this.getLibraryByComponentMap(map); + const result = this.getModuleByReference(map); if (result) { - this.componentsRecord[result.key] = result.value; + this.componentsRecord[map.componentName] = result; } } } @@ -172,12 +138,12 @@ export class PackageManagementService implements IPackageManagementService { const normalized: NormalizedPackage = { package: packageInfo.package, - id: packageInfo.id ?? packageInfo.package, + id: mapPackageToId(packageInfo), library: packageInfo.library, raw: packageInfo, }; - this.packagesMap.set(normalized.package, normalized); + this.packagesMap.set(normalized.id, normalized); // add normalized to package exports dependency graph const packagesRef = [...this.packagesMap.values()]; @@ -201,14 +167,14 @@ export class PackageManagementService implements IPackageManagementService { } private async loadPackageByNormalized(normalized: NormalizedPackage) { - if (this.packageStore.has(normalized.package)) return; + if (this.packageStore.has(normalized.id)) return; // if it has export source package, wait export source package loaded if (normalized.exportSource) { - if (this.packageStore.has(normalized.package)) { + if (this.packageStore.has(normalized.id)) { const library = lodashGet(window, normalized.library); if (library) { - this.packageStore.set(normalized.package, library); + this.packageStore.set(normalized.id, library); } } } else { @@ -217,14 +183,14 @@ export class PackageManagementService implements IPackageManagementService { const result = await loader.load.call(this, normalized.raw); if (result) { - this.packageStore.set(normalized.package, result); + this.packageStore.set(normalized.id, result); } } // if current package loaded, set the value of the dependency on this package - if (this.packageStore.has(normalized.package)) { + if (this.packageStore.has(normalized.id)) { const chilren = [...this.packagesMap.values()].filter((item) => { - return item.exportSource?.package === normalized.package; + return item.exportSource?.id === normalized.id; }); for (const child of chilren) { diff --git a/packages/renderer-core/src/services/runtimeIntlService.ts b/packages/renderer-core/src/services/runtimeIntlService.ts index 086177dee..4979ffd37 100644 --- a/packages/renderer-core/src/services/runtimeIntlService.ts +++ b/packages/renderer-core/src/services/runtimeIntlService.ts @@ -5,7 +5,7 @@ import { type IntlApi, type Locale, type Translations, - platformLocale, + Platform, } from '@alilc/lowcode-shared'; import { ILifeCycleService, LifecyclePhase } from './lifeCycleService'; import { ICodeRuntimeService } from './code-runtime'; @@ -20,7 +20,7 @@ export interface MessageDescriptor { export interface IRuntimeIntlService { locale: string; - t(descriptor: MessageDescriptor): string; + localize(descriptor: MessageDescriptor): string; setLocale(locale: Locale): void; @@ -35,7 +35,7 @@ export const IRuntimeIntlService = createDecorator<IRuntimeIntlService>('IRuntim export class RuntimeIntlService implements IRuntimeIntlService { private intl: Intl = new Intl(); - public locale: string = platformLocale; + public locale: string = Platform.platformLocale; constructor( @ILifeCycleService private lifeCycleService: ILifeCycleService, @@ -59,7 +59,7 @@ export class RuntimeIntlService implements IRuntimeIntlService { }); } - t(descriptor: MessageDescriptor): string { + localize(descriptor: MessageDescriptor): string { const formatter = this.intl.getFormatter(); return formatter.$t( @@ -86,7 +86,7 @@ export class RuntimeIntlService implements IRuntimeIntlService { private injectScope(): void { const exposed: IntlApi = { i18n: (key, params) => { - return this.t({ key, params }); + return this.localize({ key, params }); }, getLocale: () => { return this.getLocale(); diff --git a/packages/renderer-core/src/services/runtimeUtilService.ts b/packages/renderer-core/src/services/runtimeUtilService.ts index 3a4d3e62b..b2b84eb12 100644 --- a/packages/renderer-core/src/services/runtimeUtilService.ts +++ b/packages/renderer-core/src/services/runtimeUtilService.ts @@ -78,7 +78,7 @@ export class RuntimeUtilService implements IRuntimeUtilService { if ((fn as UtilDescription).type === 'function' || (fn as UtilDescription).type === 'npm') { const utilFn = this.parseUtil(fn as UtilDescription); if (utilFn) { - this.addUtilByName(utilFn.key, utilFn.value, force); + this.addUtilByName(name, utilFn, force); } } else if ((fn as StringDictionary).destructuring) { for (const key of Object.keys(fn)) { @@ -98,13 +98,9 @@ export class RuntimeUtilService implements IRuntimeUtilService { private parseUtil(utilItem: UtilDescription) { if (utilItem.type === 'function') { - const { content } = utilItem; - return { - key: utilItem.name, - value: this.codeRuntimeService.rootRuntime.run(content.value), - }; + return this.codeRuntimeService.rootRuntime.run(utilItem.content.value); } else { - return this.packageManagementService.getLibraryByComponentMap(utilItem.content); + return this.packageManagementService.getModuleByReference(utilItem.content); } } diff --git a/packages/renderer-core/src/services/schema/schemaService.ts b/packages/renderer-core/src/services/schema/schemaService.ts index 0e4bb0bf6..511123afb 100644 --- a/packages/renderer-core/src/services/schema/schemaService.ts +++ b/packages/renderer-core/src/services/schema/schemaService.ts @@ -2,8 +2,6 @@ import { type Project, createDecorator, Provide, - type IStore, - KeyValueStore, EventEmitter, type EventDisposable, } from '@alilc/lowcode-shared'; @@ -11,6 +9,7 @@ import { isObject } from 'lodash-es'; import { schemaValidation } from './validation'; import { ILifeCycleService, LifecyclePhase } from '../lifeCycleService'; import { ICodeRuntimeService } from '../code-runtime'; +import { type IStore, KeyValueStore } from '../../utils/store'; export interface NormalizedSchema extends Project {} diff --git a/packages/shared/src/abilities/storage.ts b/packages/renderer-core/src/utils/store.ts similarity index 95% rename from packages/shared/src/abilities/storage.ts rename to packages/renderer-core/src/utils/store.ts index 798f0a20e..34299f0ae 100644 --- a/packages/shared/src/abilities/storage.ts +++ b/packages/renderer-core/src/utils/store.ts @@ -1,4 +1,4 @@ -import { StringDictionary } from '../types'; +import { type StringDictionary } from '@alilc/lowcode-shared'; /** * MapLike interface @@ -16,9 +16,6 @@ export interface IStore<O, K extends keyof O> { clear(): void; } -/** - * 统一存储接口 - */ export class KeyValueStore<O = StringDictionary, K extends keyof O = keyof O> implements IStore<O, K> { diff --git a/packages/shared/src/abilities/instantiation/instantiationService.ts b/packages/shared/src/abilities/instantiation/instantiationService.ts deleted file mode 100644 index 5ea7ed7ef..000000000 --- a/packages/shared/src/abilities/instantiation/instantiationService.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Container, interfaces } from 'inversify'; -import { buildProviderModule } from 'inversify-binding-decorators'; -import { ServiceIdentifier, Constructor } from './decorators'; - -export interface InstanceAccessor { - get<T>(id: ServiceIdentifier<T>): T; -} - -export class InstantiationService { - container: Container; - - constructor(options?: interfaces.ContainerOptions) { - this.container = new Container(options); - } - - get<T>(serviceIdentifier: ServiceIdentifier<T>) { - return this.container.get<T>(serviceIdentifier); - } - - /** - * Calls a function with a service accessor. - */ - invokeFunction<R, TS extends any[] = []>( - fn: (accessor: InstanceAccessor, ...args: TS) => R, - ...args: TS - ): R { - const accessor: InstanceAccessor = { - get: (id) => { - return this.get(id); - }, - }; - return fn(accessor, ...args); - } - - set<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>) { - this.container.bind<T>(serviceIdentifier).to(constructor); - } - - createInstance<T extends Constructor>(App: T) { - return this.container.resolve<InstanceType<T>>(App); - } - - bootstrapModules() { - this.container.load(buildProviderModule()); - } -} diff --git a/packages/shared/src/abilities/event.ts b/packages/shared/src/common/event.ts similarity index 100% rename from packages/shared/src/abilities/event.ts rename to packages/shared/src/common/event.ts diff --git a/packages/shared/src/abilities/index.ts b/packages/shared/src/common/index.ts similarity index 52% rename from packages/shared/src/abilities/index.ts rename to packages/shared/src/common/index.ts index 3d75c2d6e..48fe94a1a 100644 --- a/packages/shared/src/abilities/index.ts +++ b/packages/shared/src/common/index.ts @@ -1,5 +1,7 @@ export * from './event'; export * from './logger'; -export * from './storage'; export * from './intl'; export * from './instantiation'; +export * from './signals'; +export * as Platform from './platform'; +export * from './linkedList'; diff --git a/packages/shared/src/abilities/instantiation/decorators.ts b/packages/shared/src/common/instantiation/decorators.ts similarity index 100% rename from packages/shared/src/abilities/instantiation/decorators.ts rename to packages/shared/src/common/instantiation/decorators.ts diff --git a/packages/shared/src/abilities/instantiation/index.ts b/packages/shared/src/common/instantiation/index.ts similarity index 70% rename from packages/shared/src/abilities/instantiation/index.ts rename to packages/shared/src/common/instantiation/index.ts index a3c5ead51..ed6f6797c 100644 --- a/packages/shared/src/abilities/instantiation/index.ts +++ b/packages/shared/src/common/instantiation/index.ts @@ -1,3 +1,2 @@ -import '@abraham/reflection'; export * from './instantiationService'; export * from './decorators'; diff --git a/packages/shared/src/common/instantiation/instantiationService.ts b/packages/shared/src/common/instantiation/instantiationService.ts new file mode 100644 index 000000000..a98a27f2a --- /dev/null +++ b/packages/shared/src/common/instantiation/instantiationService.ts @@ -0,0 +1,67 @@ +import '@abraham/reflection'; +import { Container, interfaces, injectable } from 'inversify'; +import { buildProviderModule } from 'inversify-binding-decorators'; +import { ServiceIdentifier, Constructor, createDecorator } from './decorators'; + +export interface InstanceAccessor { + get<T>(id: ServiceIdentifier<T>): T; +} + +export interface IInstantiationService { + get<T>(serviceIdentifier: ServiceIdentifier<T>): T; + + bind<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>): void; + + set<T>(serviceIdentifier: ServiceIdentifier<T>, instance: T): void; + + invokeFunction<R, TS extends any[] = []>( + fn: (accessor: InstanceAccessor, ...args: TS) => R, + ...args: TS + ): R; + + createInstance<T extends Constructor>(App: T): InstanceType<T>; +} + +export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService'); + +export class InstantiationService implements IInstantiationService { + container: Container; + + constructor(options?: interfaces.ContainerOptions) { + this.container = new Container(options); + this.set(IInstantiationService, this); + this.container.load(buildProviderModule()); + } + + get<T>(serviceIdentifier: ServiceIdentifier<T>) { + return this.container.get<T>(serviceIdentifier.toString()); + } + + set<T>(serviceIdentifier: ServiceIdentifier<T>, instance: T): void { + this.container.bind<T>(serviceIdentifier).toConstantValue(instance); + } + + /** + * Calls a function with a service accessor. + */ + invokeFunction<R, TS extends any[] = []>( + fn: (accessor: InstanceAccessor, ...args: TS) => R, + ...args: TS + ): R { + const accessor: InstanceAccessor = { + get: (id) => { + return this.get(id); + }, + }; + return fn(accessor, ...args); + } + + bind<T>(serviceIdentifier: ServiceIdentifier<T>, constructor: Constructor<T>) { + this.container.bind<T>(serviceIdentifier).to(constructor); + } + + createInstance<T extends Constructor>(App: T) { + injectable()(App); + return this.container.resolve<InstanceType<T>>(App); + } +} diff --git a/packages/shared/src/abilities/intl.ts b/packages/shared/src/common/intl.ts similarity index 97% rename from packages/shared/src/abilities/intl.ts rename to packages/shared/src/common/intl.ts index 612aea6f3..31ef55aa3 100644 --- a/packages/shared/src/abilities/intl.ts +++ b/packages/shared/src/common/intl.ts @@ -1,7 +1,7 @@ import { createIntl, createIntlCache, type IntlShape as IntlFormatter } from '@formatjs/intl'; import { mapKeys } from 'lodash-es'; -import { signal, computed, effect, type Signal, type ComputedSignal } from '../signals'; -import { platformLocale } from '../utils'; +import { signal, computed, effect, type Signal, type ComputedSignal } from './signals'; +import { platformLocale } from './platform'; export { IntlFormatter }; diff --git a/packages/shared/src/common/linkedList.ts b/packages/shared/src/common/linkedList.ts new file mode 100644 index 000000000..ab45b420b --- /dev/null +++ b/packages/shared/src/common/linkedList.ts @@ -0,0 +1,133 @@ +export class LinkedListNode<E> { + static readonly Undefined = new LinkedListNode<any>(undefined); + + current: E; + next: LinkedListNode<E>; + prev: LinkedListNode<E>; + + constructor(current: E) { + this.current = current; + this.next = LinkedListNode.Undefined; + this.prev = LinkedListNode.Undefined; + } +} + +/** + * Array Api Like LinkedList + */ +export class LinkedList<E> { + private _first: LinkedListNode<E> = LinkedListNode.Undefined; + private _last: LinkedListNode<E> = LinkedListNode.Undefined; + private _size: number = 0; + + get length(): number { + return this._size; + } + + isEmpty(): boolean { + return this._first === LinkedListNode.Undefined; + } + + clear(): void { + let node = this._first; + while (node !== LinkedListNode.Undefined) { + const next = node.next; + node.prev = LinkedListNode.Undefined; + node.next = LinkedListNode.Undefined; + node = next; + } + + this._first = LinkedListNode.Undefined; + this._last = LinkedListNode.Undefined; + this._size = 0; + } + + unshift(element: E): () => void { + return this._insert(element, false); + } + + push(element: E): () => void { + return this._insert(element, true); + } + + private _insert(element: E, atTheEnd: boolean): () => void { + const newNode = new LinkedListNode(element); + if (this._first === LinkedListNode.Undefined) { + this._first = newNode; + this._last = newNode; + } else if (atTheEnd) { + // push + const oldLast = this._last; + this._last = newNode; + newNode.prev = oldLast; + oldLast.next = newNode; + } else { + // unshift + const oldFirst = this._first; + this._first = newNode; + newNode.next = oldFirst; + oldFirst.prev = newNode; + } + this._size += 1; + + let didRemove = false; + return () => { + if (!didRemove) { + didRemove = true; + this._remove(newNode); + } + }; + } + + shift(): E | undefined { + if (this._first === LinkedListNode.Undefined) { + return undefined; + } else { + const res = this._first.current; + this._remove(this._first); + return res; + } + } + + pop(): E | undefined { + if (this._last === LinkedListNode.Undefined) { + return undefined; + } else { + const res = this._last.current; + this._remove(this._last); + return res; + } + } + + private _remove(node: LinkedListNode<E>): void { + if (node.prev !== LinkedListNode.Undefined && node.next !== LinkedListNode.Undefined) { + // middle + const anchor = node.prev; + anchor.next = node.next; + node.next.prev = anchor; + } else if (node.prev === LinkedListNode.Undefined && node.next === LinkedListNode.Undefined) { + // only node + this._first = LinkedListNode.Undefined; + this._last = LinkedListNode.Undefined; + } else if (node.next === LinkedListNode.Undefined) { + // last + this._last = this._last.prev!; + this._last.next = LinkedListNode.Undefined; + } else if (node.prev === LinkedListNode.Undefined) { + // first + this._first = this._first.next!; + this._first.prev = LinkedListNode.Undefined; + } + + // done + this._size -= 1; + } + + *[Symbol.iterator](): Iterator<E> { + let node = this._first; + while (node !== LinkedListNode.Undefined) { + yield node.current; + node = node.next; + } + } +} diff --git a/packages/shared/src/abilities/logger.ts b/packages/shared/src/common/logger.ts similarity index 95% rename from packages/shared/src/abilities/logger.ts rename to packages/shared/src/common/logger.ts index 58b166180..c9293fdb4 100644 --- a/packages/shared/src/abilities/logger.ts +++ b/packages/shared/src/common/logger.ts @@ -64,7 +64,10 @@ const levelMarks: Record<string, string> = { warn: 'warn', error: 'error', }; -const outputFunction: Record<string, any> = { + +export type OutputFuntion = (...data: any[]) => void; + +const outputFunctionRecord: Record<string, OutputFuntion> = { debug: console.log, log: console.log, info: console.log, @@ -87,7 +90,7 @@ const shouldOutput = ( const output = (logLevel: string, bizName: string) => { return (...args: any[]) => { - return outputFunction[logLevel]?.apply(console, getLogArgs(args, bizName, logLevel)); + return outputFunctionRecord[logLevel]?.apply(console, getLogArgs(args, bizName, logLevel)); }; }; diff --git a/packages/shared/src/utils/platform.ts b/packages/shared/src/common/platform.ts similarity index 70% rename from packages/shared/src/utils/platform.ts rename to packages/shared/src/common/platform.ts index 53cb308fc..c71ca981f 100644 --- a/packages/shared/src/utils/platform.ts +++ b/packages/shared/src/common/platform.ts @@ -11,34 +11,34 @@ export const isMobile = userAgent?.indexOf('Mobi') >= 0; export const platformLocale = window.navigator.language; -export const enum Platform { - Web, +export const enum PlatformEnum { + Unknown, Mac, Linux, Windows, } -export type PlatformName = 'Web' | 'Windows' | 'Mac' | 'Linux'; +export type PlatformName = 'Unknown' | 'Windows' | 'Mac' | 'Linux'; -export function platformToString(platform: Platform): PlatformName { +export function platformToString(platform: PlatformEnum): PlatformName { switch (platform) { - case Platform.Web: - return 'Web'; - case Platform.Mac: + case PlatformEnum.Mac: return 'Mac'; - case Platform.Linux: + case PlatformEnum.Linux: return 'Linux'; - case Platform.Windows: + case PlatformEnum.Windows: return 'Windows'; + default: + return 'Unknown'; } } -export let platform: Platform = Platform.Web; +export let platform: PlatformEnum = PlatformEnum.Unknown; if (isMacintosh) { - platform = Platform.Mac; + platform = PlatformEnum.Mac; } else if (isWindows) { - platform = Platform.Windows; + platform = PlatformEnum.Windows; } else if (isLinux) { - platform = Platform.Linux; + platform = PlatformEnum.Linux; } export const isChrome = !!(userAgent && userAgent.indexOf('Chrome') >= 0); diff --git a/packages/shared/src/signals.ts b/packages/shared/src/common/signals.ts similarity index 98% rename from packages/shared/src/signals.ts rename to packages/shared/src/common/signals.ts index 8db0a2dad..6fac76a1c 100644 --- a/packages/shared/src/signals.ts +++ b/packages/shared/src/common/signals.ts @@ -4,7 +4,7 @@ * https://github.com/tc39/proposal-signals */ -import { AnyFunction, type StringDictionary } from './types'; +import { AnyFunction, type StringDictionary } from '../types'; import { ref, computed, @@ -21,7 +21,7 @@ import { type EffectScheduler, } from '@vue/reactivity'; import { noop, isObject, isPlainObject, isSet, isMap, isFunction } from 'lodash-es'; -import { isPromise } from './utils'; +import { types } from '../utils'; export { ref as signal, @@ -317,7 +317,7 @@ function callWithErrorHandling(fn: AnyFunction, args?: unknown[]) { function callWithAsyncErrorHandling(fn: AnyFunction | AnyFunction[], args?: unknown[]): any { if (isFunction(fn)) { const res = callWithErrorHandling(fn, args); - if (res && isPromise(res)) { + if (res && types.isPromise(res)) { res.catch((err) => { console.error(err); }); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 6b3ff6d0c..d5e5293af 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,4 +1,3 @@ export * from './types'; export * from './utils'; -export * from './signals'; -export * from './abilities'; +export * from './common'; diff --git a/packages/shared/src/types/json.ts b/packages/shared/src/types/json.ts index e002ff5d9..13b23c814 100644 --- a/packages/shared/src/types/json.ts +++ b/packages/shared/src/types/json.ts @@ -1,7 +1,86 @@ -export type JSONValueType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'; - export type JSONValue = number | string | boolean | null; export interface JSONObject { [key: string]: JSONValue | JSONObject | JSONObject[]; } + +export type JSONSchemaType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object'; + +/** + * fork from vscode + */ +export interface IJSONSchema { + id?: string; + $id?: string; + $schema?: string; + type?: JSONSchemaType | JSONSchemaType[]; + title?: string; + default?: any; + definitions?: IJSONSchemaMap; + description?: string; + properties?: IJSONSchemaMap; + patternProperties?: IJSONSchemaMap; + additionalProperties?: boolean | IJSONSchema; + minProperties?: number; + maxProperties?: number; + dependencies?: IJSONSchemaMap | { [prop: string]: string[] }; + items?: IJSONSchema | IJSONSchema[]; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + additionalItems?: boolean | IJSONSchema; + pattern?: string; + minLength?: number; + maxLength?: number; + minimum?: number; + maximum?: number; + exclusiveMinimum?: boolean | number; + exclusiveMaximum?: boolean | number; + multipleOf?: number; + required?: string[]; + $ref?: string; + anyOf?: IJSONSchema[]; + allOf?: IJSONSchema[]; + oneOf?: IJSONSchema[]; + not?: IJSONSchema; + enum?: any[]; + format?: string; + + // schema draft 06 + const?: any; + contains?: IJSONSchema; + propertyNames?: IJSONSchema; + examples?: any[]; + + // schema draft 07 + $comment?: string; + if?: IJSONSchema; + then?: IJSONSchema; + else?: IJSONSchema; + + // schema 2019-09 + unevaluatedProperties?: boolean | IJSONSchema; + unevaluatedItems?: boolean | IJSONSchema; + minContains?: number; + maxContains?: number; + deprecated?: boolean; + dependentRequired?: { [prop: string]: string[] }; + dependentSchemas?: IJSONSchemaMap; + $defs?: { [name: string]: IJSONSchema }; + $anchor?: string; + $recursiveRef?: string; + $recursiveAnchor?: string; + $vocabulary?: any; + + // schema 2020-12 + prefixItems?: IJSONSchema[]; + $dynamicRef?: string; + $dynamicAnchor?: string; + + // internal extensions + errorMessage?: string; +} + +export interface IJSONSchemaMap { + [name: string]: IJSONSchema; +} diff --git a/packages/shared/src/types/specs/asset-spec.ts b/packages/shared/src/types/specs/asset-spec.ts index b1cb73351..a693916d4 100644 --- a/packages/shared/src/types/specs/asset-spec.ts +++ b/packages/shared/src/types/specs/asset-spec.ts @@ -55,7 +55,7 @@ export interface Package { */ id?: string; /** - * npm 包唯一标识,与 id 必须要有一个存在值 + * npm 包唯一标识 */ package?: string; /** diff --git a/packages/shared/src/types/specs/lowcode-spec.ts b/packages/shared/src/types/specs/lowcode-spec.ts index f1dad1bb0..3d934e1d2 100644 --- a/packages/shared/src/types/specs/lowcode-spec.ts +++ b/packages/shared/src/types/specs/lowcode-spec.ts @@ -96,7 +96,7 @@ export interface ComponentMap extends Reference { /** * 协议中的组件名,对应包导出的组件名,是一个有效的 JS 标识符 */ - componentName?: string; + componentName: string; devMode?: 'lowCode' | 'proCode'; } @@ -305,7 +305,7 @@ export interface ComponentNodeProps { export interface NPMUtil { name: string; type: 'npm'; - content: ComponentMap; + content: Reference; } export interface FunctionUtil { diff --git a/packages/shared/src/types/specs/material-spec.ts b/packages/shared/src/types/specs/material-spec.ts index 2132d27e7..8c144982c 100644 --- a/packages/shared/src/types/specs/material-spec.ts +++ b/packages/shared/src/types/specs/material-spec.ts @@ -103,25 +103,17 @@ export interface Reference { */ version: string; /** - * 源码组件库名 + * 包名 */ package?: string; /** - * 是否解构 - */ - destructuring?: boolean; - /** - * 源码组件名称 + * 导出名称 */ exportName?: string; /** * 子组件名 */ subName?: string; - /** - * 引用的资源主入口 - */ - main?: string; } /** diff --git a/packages/shared/src/utils/index.ts b/packages/shared/src/utils/index.ts index 373d724c7..7c9eccaa7 100644 --- a/packages/shared/src/utils/index.ts +++ b/packages/shared/src/utils/index.ts @@ -1,7 +1,8 @@ export * from './invariant'; -export * from './is-promise'; export * from './unique-id'; export * from './types'; -export * from './platform'; export * from './async'; export * from './node'; +export * from './resource'; + +export * as Iterable from './iterable'; diff --git a/packages/shared/src/utils/iterable.ts b/packages/shared/src/utils/iterable.ts new file mode 100644 index 000000000..0667ba909 --- /dev/null +++ b/packages/shared/src/utils/iterable.ts @@ -0,0 +1,7 @@ +export function first<T>(iterable: Iterable<T>): T | undefined { + return iterable[Symbol.iterator]().next().value; +} + +export function isEmpty<T>(iterable: Iterable<T> | undefined | null): boolean { + return !iterable || iterable[Symbol.iterator]().next().done === true; +} diff --git a/packages/shared/src/utils/resource.ts b/packages/shared/src/utils/resource.ts new file mode 100644 index 000000000..8e371c7b3 --- /dev/null +++ b/packages/shared/src/utils/resource.ts @@ -0,0 +1,24 @@ +import { Package, Reference } from '../types'; + +export function mapPackageToUniqueId(schema: Package | Reference): string { + return `${schema.id ?? schema.package}@${schema.version}`; +} + +export function exportByReference<T = any>(target: any, reference: Reference): T | undefined { + let result = target; + + // export { exportName } from xxx exportName === global.libraryName.exportName + // export exportName from xxx exportName === global.libraryName.default || global.libraryName + // const module = exportName.subName, if exportName empty subName do not use + if (reference.exportName) { + const paths = reference.subName ? reference.subName.split('.') : []; + paths.unshift(reference.exportName); + + for (const path of paths) { + result = result[path]; + if (!result) return undefined; + } + } + + return result; +} diff --git a/packages/shared/src/utils/types/constraint.ts b/packages/shared/src/utils/types/constraint.ts new file mode 100644 index 000000000..1991f6ff7 --- /dev/null +++ b/packages/shared/src/utils/types/constraint.ts @@ -0,0 +1,39 @@ +import { isString, isFunction, isNil } from 'lodash-es'; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type TypeConstraint = string | Function; + +export function validateConstraints( + args: unknown[], + constraints: Array<TypeConstraint | undefined>, +): void { + const len = Math.min(args.length, constraints.length); + for (let i = 0; i < len; i++) { + validateConstraint(args[i], constraints[i]); + } +} + +export function validateConstraint(arg: unknown, constraint: TypeConstraint | undefined): void { + if (isString(constraint)) { + if (typeof arg !== constraint) { + throw new Error(`argument does not match constraint: typeof ${constraint}`); + } + } else if (isFunction(constraint)) { + try { + if (arg instanceof constraint) { + return; + } + } catch { + // ignore + } + if (!isNil(arg) && (arg as any).constructor === constraint) { + return; + } + if (constraint.length === 1 && constraint.call(undefined, arg) === true) { + return; + } + throw new Error( + `argument does not match one of these constraints: arg instanceof constraint, arg.constructor === constraint, nor constraint(arg) === true`, + ); + } +} diff --git a/packages/shared/src/utils/types/index.ts b/packages/shared/src/utils/types/index.ts index 2fec07d05..1fb82c76f 100644 --- a/packages/shared/src/utils/types/index.ts +++ b/packages/shared/src/utils/types/index.ts @@ -1,2 +1,5 @@ export * as specTypes from './spec'; export * as jsonTypes from './json'; +export * as types from './type'; + +export * from './constraint'; diff --git a/packages/shared/src/utils/is-promise.ts b/packages/shared/src/utils/types/type.ts similarity index 55% rename from packages/shared/src/utils/is-promise.ts rename to packages/shared/src/utils/types/type.ts index a259538b7..b64a42cf6 100644 --- a/packages/shared/src/utils/is-promise.ts +++ b/packages/shared/src/utils/types/type.ts @@ -7,3 +7,13 @@ export const isPromise = <T = any>(val: unknown): val is Promise<T> => { isFunction((val as any).catch) ); }; + +export const isEmptyObject = (obj: unknown): obj is object => { + if (!isObject(obj)) return false; + for (const key in obj) { + if (Reflect.has(obj, key)) { + return false; + } + } + return true; +};