mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-06-02 05:30:53 +00:00
feat: add some common codes
This commit is contained in:
parent
3d3952ecae
commit
d86b0fdaa0
@ -35,9 +35,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash-es": "^4.17.12"
|
"@types/lodash-es": "^4.17.12"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
|
||||||
"@alilc/lowcode-shared": "workspace:*"
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://registry.npmjs.org/"
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type InstanceAccessor } from '@alilc/lowcode-shared';
|
import { type InstanceAccessor, type TypeConstraint } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
export interface ICommandEvent {
|
export interface ICommandEvent {
|
||||||
commandId: string;
|
commandId: string;
|
||||||
@ -16,19 +16,12 @@ export interface ICommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ICommandMetadata {
|
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 description: string;
|
||||||
readonly args?: ReadonlyArray<{
|
readonly args?: ReadonlyArray<{
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly isOptional?: boolean;
|
readonly isOptional?: boolean;
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
// readonly constraint?: TypeConstraint;
|
readonly constraint?: TypeConstraint;
|
||||||
// readonly schema?: IJSONSchema;
|
readonly default?: any;
|
||||||
}>;
|
}>;
|
||||||
readonly returns?: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,14 @@ import {
|
|||||||
type EventDisposable,
|
type EventDisposable,
|
||||||
type EventListener,
|
type EventListener,
|
||||||
Emitter,
|
Emitter,
|
||||||
|
LinkedList,
|
||||||
|
TypeConstraint,
|
||||||
|
validateConstraints,
|
||||||
|
Iterable,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { ICommand, ICommandHandler } from './command';
|
import { ICommand, ICommandHandler } from './command';
|
||||||
import { Registry } from '../extension';
|
import { Extensions, Registry } from '../common/registry';
|
||||||
|
import { ICommandService } from './commandService';
|
||||||
|
|
||||||
export type ICommandsMap = Map<string, ICommand>;
|
export type ICommandsMap = Map<string, ICommand>;
|
||||||
|
|
||||||
@ -24,10 +29,10 @@ export interface ICommandRegistry {
|
|||||||
class CommandsRegistry implements ICommandRegistry {
|
class CommandsRegistry implements ICommandRegistry {
|
||||||
private readonly _commands = new Map<string, LinkedList<ICommand>>();
|
private readonly _commands = new Map<string, LinkedList<ICommand>>();
|
||||||
|
|
||||||
private readonly _onDidRegisterCommand = new Emitter<string>();
|
private readonly _didRegisterCommandEmitter = new Emitter<string>();
|
||||||
|
|
||||||
onDidRegisterCommand(fn: EventListener<string>) {
|
onDidRegisterCommand(fn: EventListener<string>) {
|
||||||
return this._onDidRegisterCommand.on(fn);
|
return this._didRegisterCommandEmitter.on(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): EventDisposable {
|
registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): EventDisposable {
|
||||||
@ -66,21 +71,21 @@ class CommandsRegistry implements ICommandRegistry {
|
|||||||
|
|
||||||
const removeFn = commands.unshift(idOrCommand);
|
const removeFn = commands.unshift(idOrCommand);
|
||||||
|
|
||||||
const ret = toDisposable(() => {
|
const ret = () => {
|
||||||
removeFn();
|
removeFn();
|
||||||
const command = this._commands.get(id);
|
const command = this._commands.get(id);
|
||||||
if (command?.isEmpty()) {
|
if (command?.isEmpty()) {
|
||||||
this._commands.delete(id);
|
this._commands.delete(id);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// tell the world about this command
|
// tell the world about this command
|
||||||
this._onDidRegisterCommand.emit(id);
|
this._didRegisterCommandEmitter.emit(id);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCommandAlias(oldId: string, newId: string): IDisposable {
|
registerCommandAlias(oldId: string, newId: string): EventDisposable {
|
||||||
return this.registerCommand(oldId, (accessor, ...args) =>
|
return this.registerCommand(oldId, (accessor, ...args) =>
|
||||||
accessor.get(ICommandService).executeCommand(newId, ...args),
|
accessor.get(ICommandService).executeCommand(newId, ...args),
|
||||||
);
|
);
|
||||||
@ -108,8 +113,4 @@ class CommandsRegistry implements ICommandRegistry {
|
|||||||
|
|
||||||
const commandsRegistry = new CommandsRegistry();
|
const commandsRegistry = new CommandsRegistry();
|
||||||
|
|
||||||
export const Extension = {
|
Registry.add(Extensions.Command, commandsRegistry);
|
||||||
command: 'base.contributions.command',
|
|
||||||
};
|
|
||||||
|
|
||||||
Registry.add(Extension.command, commandsRegistry);
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
import { createDecorator, Provide, IInstantiationService } from '@alilc/lowcode-shared';
|
||||||
import { Registry } from '../extension';
|
import { Registry, Extensions } from '../common/registry';
|
||||||
import { ICommandRegistry, Extension } from './commandRegistry';
|
import { ICommandRegistry } from './commandRegistry';
|
||||||
|
|
||||||
export interface ICommandService {
|
export interface ICommandService {
|
||||||
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
|
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
|
||||||
@ -10,11 +10,23 @@ export const ICommandService = createDecorator<ICommandService>('commandService'
|
|||||||
|
|
||||||
@Provide(ICommandService)
|
@Provide(ICommandService)
|
||||||
export class CommandService implements ICommandService {
|
export class CommandService implements ICommandService {
|
||||||
executeCommand<T = any>(id: string, ...args: any[]): Promise<T | undefined> {
|
constructor(@IInstantiationService private instantiationService: IInstantiationService) {}
|
||||||
const command = Registry.as<ICommandRegistry>(Extension.command).getCommand(id);
|
|
||||||
|
|
||||||
|
executeCommand<T = any>(id: string, ...args: any[]): Promise<T | undefined> {
|
||||||
|
return this.tryExecuteCommand(id, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private tryExecuteCommand(id: string, args: any[]): Promise<any> {
|
||||||
|
const command = Registry.as<ICommandRegistry>(Extensions.Command).getCommand(id);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return Promise.reject(new Error(`command '${id}' not found`));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
packages/engine-core/src/command/index.ts
Normal file
3
packages/engine-core/src/command/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './command';
|
||||||
|
export * from './commandRegistry';
|
||||||
|
export * from './commandService';
|
||||||
@ -37,3 +37,9 @@ class RegistryImpl implements IRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Registry: IRegistry = new RegistryImpl();
|
export const Registry: IRegistry = new RegistryImpl();
|
||||||
|
|
||||||
|
export const Extensions = {
|
||||||
|
Configuration: 'base.contributions.configuration',
|
||||||
|
Command: 'base.contributions.command',
|
||||||
|
Widget: 'base.contributions.widget',
|
||||||
|
};
|
||||||
@ -1,81 +1,105 @@
|
|||||||
import { type StringDictionary, Emitter, type EventListener } from '@alilc/lowcode-shared';
|
import { type StringDictionary } from '@alilc/lowcode-shared';
|
||||||
import { ConfigurationModel } from './configurationModel';
|
import { uniq } from 'lodash-es';
|
||||||
import {
|
|
||||||
type IConfigurationRegistry,
|
|
||||||
type IRegisteredConfigurationPropertySchema,
|
|
||||||
Extension,
|
|
||||||
} from './configurationRegistry';
|
|
||||||
import { Registry } from '../extension';
|
|
||||||
|
|
||||||
export interface IConfigurationOverrides {
|
export interface IInspectValue<T> {
|
||||||
overrideIdentifier?: string | null;
|
readonly value?: T;
|
||||||
|
readonly override?: T;
|
||||||
|
readonly overrides?: { readonly identifiers: string[]; readonly value: T }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigurationUpdateOverrides {
|
export function toValuesTree(properties: StringDictionary): any {
|
||||||
overrideIdentifiers?: string[] | null;
|
const root = Object.create(null);
|
||||||
|
|
||||||
|
for (const key of Object.keys(properties)) {
|
||||||
|
addToValueTree(root, key, properties[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DefaultConfiguration {
|
export function addToValueTree(
|
||||||
private emitter = new Emitter<{
|
settingsTreeRoot: any,
|
||||||
defaults: ConfigurationModel;
|
key: string,
|
||||||
properties: string[];
|
value: any,
|
||||||
}>();
|
conflictReporter: (message: string) => void = console.error,
|
||||||
|
): void {
|
||||||
|
const segments = key.split('.');
|
||||||
|
const last = segments.pop()!;
|
||||||
|
|
||||||
private _configurationModel = ConfigurationModel.createEmptyModel();
|
let curr = settingsTreeRoot;
|
||||||
|
for (let i = 0; i < segments.length; i++) {
|
||||||
get configurationModel(): ConfigurationModel {
|
const s = segments[i];
|
||||||
return this._configurationModel;
|
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;
|
||||||
initialize(): ConfigurationModel {
|
default:
|
||||||
this.resetConfigurationModel();
|
conflictReporter(
|
||||||
Registry.as<IConfigurationRegistry>(Extension.Configuration).onDidUpdateConfiguration(
|
`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`,
|
||||||
({ properties }) => this.onDidUpdateConfiguration([...properties]),
|
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
return this.configurationModel;
|
}
|
||||||
|
curr = obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
reload(): ConfigurationModel {
|
if (typeof curr === 'object' && curr !== null) {
|
||||||
this.resetConfigurationModel();
|
try {
|
||||||
return this.configurationModel;
|
curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606
|
||||||
|
} catch (e) {
|
||||||
|
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDidChangeConfiguration(
|
|
||||||
listener: EventListener<[{ defaults: ConfigurationModel; properties: string[] }]>,
|
|
||||||
) {
|
|
||||||
return this.emitter.on(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onDidUpdateConfiguration(properties: string[]): void {
|
|
||||||
this.updateConfigurationModel(
|
|
||||||
properties,
|
|
||||||
Registry.as<IConfigurationRegistry>(Extension.Configuration).getConfigurationProperties(),
|
|
||||||
);
|
|
||||||
this.emitter.emit({ defaults: this.configurationModel, properties });
|
|
||||||
}
|
|
||||||
|
|
||||||
private resetConfigurationModel(): void {
|
|
||||||
this._configurationModel = ConfigurationModel.createEmptyModel();
|
|
||||||
|
|
||||||
const properties = Registry.as<IConfigurationRegistry>(
|
|
||||||
Extension.Configuration,
|
|
||||||
).getConfigurationProperties();
|
|
||||||
|
|
||||||
this.updateConfigurationModel(Object.keys(properties), properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateConfigurationModel(
|
|
||||||
properties: string[],
|
|
||||||
configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>,
|
|
||||||
): void {
|
|
||||||
for (const key of properties) {
|
|
||||||
const propertySchema = configurationProperties[key];
|
|
||||||
if (propertySchema) {
|
|
||||||
this.configurationModel.setValue(key, propertySchema.default);
|
|
||||||
} else {
|
} else {
|
||||||
this.configurationModel.removeValue(key);
|
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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<string>;
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,15 @@
|
|||||||
import { type StringDictionary } from '@alilc/lowcode-shared';
|
import { type StringDictionary } from '@alilc/lowcode-shared';
|
||||||
import { get as lodasgGet, isEqual, uniq, cloneDeep, isObject } from 'lodash-es';
|
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<V> = {
|
export type InspectValue<V> = IInspectValue<V> & { merged?: V };
|
||||||
readonly value?: V;
|
|
||||||
readonly override?: V;
|
|
||||||
readonly overrides?: { readonly identifiers: string[]; readonly value: V }[];
|
|
||||||
merged?: V;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IConfigurationModel {
|
export interface IConfigurationModel {
|
||||||
contents: any;
|
contents: any;
|
||||||
@ -327,80 +329,3 @@ export class ConfigurationModel implements IConfigurationModel {
|
|||||||
return uniq(result);
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,31 +2,14 @@ import {
|
|||||||
type Event,
|
type Event,
|
||||||
Emitter,
|
Emitter,
|
||||||
type StringDictionary,
|
type StringDictionary,
|
||||||
type JSONValueType,
|
type JSONSchemaType,
|
||||||
jsonTypes,
|
jsonTypes,
|
||||||
|
IJSONSchema,
|
||||||
|
types,
|
||||||
} from '@alilc/lowcode-shared';
|
} from '@alilc/lowcode-shared';
|
||||||
import { uniq, isUndefined } from 'lodash-es';
|
import { isUndefined, isObject } from 'lodash-es';
|
||||||
import { Registry } from '../extension/registry';
|
import { Extensions, Registry } from '../common/registry';
|
||||||
|
import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration';
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IConfigurationRegistry {
|
export interface IConfigurationRegistry {
|
||||||
/**
|
/**
|
||||||
@ -37,7 +20,10 @@ export interface IConfigurationRegistry {
|
|||||||
/**
|
/**
|
||||||
* Register multiple configurations to the registry.
|
* Register multiple configurations to the registry.
|
||||||
*/
|
*/
|
||||||
registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): void;
|
registerConfigurations(
|
||||||
|
configurations: IConfigurationNode[],
|
||||||
|
validate?: boolean,
|
||||||
|
): ReadonlySet<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deregister multiple configurations from the registry.
|
* Deregister multiple configurations from the registry.
|
||||||
@ -45,15 +31,15 @@ export interface IConfigurationRegistry {
|
|||||||
deregisterConfigurations(configurations: IConfigurationNode[]): void;
|
deregisterConfigurations(configurations: IConfigurationNode[]): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values.
|
* Register multiple default configurations to the registry.
|
||||||
* Property or default value changes are not allowed.
|
|
||||||
*/
|
*/
|
||||||
notifyConfigurationSchemaUpdated(): void;
|
registerDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event that fires whenever a configuration has been
|
* Deregister multiple default configurations from the registry.
|
||||||
* registered.
|
|
||||||
*/
|
*/
|
||||||
readonly onDidSchemaChange: Event<void>;
|
deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event that fires whenever a configuration has been
|
* Event that fires whenever a configuration has been
|
||||||
* registered.
|
* registered.
|
||||||
@ -63,10 +49,6 @@ export interface IConfigurationRegistry {
|
|||||||
defaultsOverrides?: boolean;
|
defaultsOverrides?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all configuration nodes contributed to this registry.
|
|
||||||
*/
|
|
||||||
getConfigurations(): IConfigurationNode[];
|
|
||||||
/**
|
/**
|
||||||
* Returns all configurations settings of all configuration nodes contributed to this registry.
|
* 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.
|
* Returns all excluded configurations settings of all configuration nodes contributed to this registry.
|
||||||
*/
|
*/
|
||||||
getExcludedConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema>;
|
getExcludedConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the registered default configurations
|
||||||
|
*/
|
||||||
|
getRegisteredDefaultConfigurations(): IConfigurationDefaults[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the registered configuration defaults overrides
|
||||||
|
*/
|
||||||
|
getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverrideValue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigurationNode {
|
export interface IConfigurationNode {
|
||||||
id?: string;
|
id?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
type?: JSONValueType | JSONValueType[];
|
type?: JSONSchemaType | JSONSchemaType[];
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
properties?: StringDictionary<IConfigurationPropertySchema>;
|
properties?: StringDictionary<IConfigurationPropertySchema>;
|
||||||
@ -88,45 +80,78 @@ export interface IConfigurationNode {
|
|||||||
extensionInfo?: IExtensionInfo;
|
extensionInfo?: IExtensionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConfigurationPropertySchema {
|
export interface IConfigurationPropertySchema extends IJSONSchema {
|
||||||
type?: JSONValueType;
|
/**
|
||||||
default?: any;
|
* 当该属性为“false”时,将从注册表中排除该属性。默认为包含。
|
||||||
tags?: string[];
|
*/
|
||||||
included?: boolean;
|
included?: boolean;
|
||||||
deprecated?: boolean;
|
/**
|
||||||
deprecationMessage?: string;
|
* 不允许扩展为此设置贡献配置默认值。
|
||||||
|
*/
|
||||||
|
disallowConfigurationDefault?: boolean;
|
||||||
|
/**
|
||||||
|
* 与属性关联的标签列表。
|
||||||
|
* - 标签可用于过滤
|
||||||
|
*/
|
||||||
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展信息,用来查找对应属性的源扩展
|
||||||
|
*/
|
||||||
export interface IExtensionInfo {
|
export interface IExtensionInfo {
|
||||||
id: string;
|
name: string;
|
||||||
displayName?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigurationDefaultValueSource = IExtensionInfo | Map<string, IExtensionInfo>;
|
export type ConfigurationDefaultValueSource = IExtensionInfo | Map<string, IExtensionInfo>;
|
||||||
|
|
||||||
|
export interface IRegisteredConfigurationPropertySchema extends IConfigurationPropertySchema {
|
||||||
|
defaultDefaultValue?: any;
|
||||||
|
source?: IExtensionInfo; // Source of the Property
|
||||||
|
defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value
|
||||||
|
}
|
||||||
|
|
||||||
export interface IConfigurationDefaults {
|
export interface IConfigurationDefaults {
|
||||||
overrides: StringDictionary;
|
overrides: StringDictionary;
|
||||||
source?: IExtensionInfo;
|
source?: IExtensionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRegisteredConfigurationPropertySchema extends IConfigurationPropertySchema {
|
export interface IConfigurationDefaultOverride {
|
||||||
source?: IExtensionInfo; // Source of the Property
|
readonly value: any;
|
||||||
defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value
|
readonly source?: IExtensionInfo; // Source of the default override
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IConfigurationDefaultOverrideValue {
|
||||||
|
readonly value: any;
|
||||||
|
readonly source?: IExtensionInfo | Map<string, IExtensionInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const allSettings: {
|
||||||
|
properties: StringDictionary<IConfigurationPropertySchema>;
|
||||||
|
patternProperties: StringDictionary<IConfigurationPropertySchema>;
|
||||||
|
} = { properties: {}, patternProperties: {} };
|
||||||
|
|
||||||
export class ConfigurationRegistry implements IConfigurationRegistry {
|
export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||||
private configurationContributors: IConfigurationNode[];
|
private registeredConfigurationDefaults: IConfigurationDefaults[] = [];
|
||||||
|
private configurationDefaultsOverrides: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
configurationDefaultOverrides: IConfigurationDefaultOverride[];
|
||||||
|
configurationDefaultOverrideValue?: IConfigurationDefaultOverrideValue;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
private configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
private configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||||
private excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
private excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||||
|
private overrideIdentifiers = new Set<string>();
|
||||||
|
|
||||||
private schemaChangeEmitter = new Emitter<void>();
|
private propertiesChangeEmitter = new Emitter<{
|
||||||
private updateConfigurationEmitter = new Emitter<{
|
|
||||||
properties: ReadonlySet<string>;
|
properties: ReadonlySet<string>;
|
||||||
defaultsOverrides?: boolean;
|
defaultsOverrides?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configurationContributors = [];
|
this.configurationDefaultsOverrides = new Map();
|
||||||
this.configurationProperties = {};
|
this.configurationProperties = {};
|
||||||
this.excludedConfigurationProperties = {};
|
this.excludedConfigurationProperties = {};
|
||||||
}
|
}
|
||||||
@ -135,12 +160,16 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
this.registerConfigurations([configuration], validate);
|
this.registerConfigurations([configuration], validate);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): void {
|
registerConfigurations(
|
||||||
|
configurations: IConfigurationNode[],
|
||||||
|
validate: boolean = true,
|
||||||
|
): ReadonlySet<string> {
|
||||||
const properties = new Set<string>();
|
const properties = new Set<string>();
|
||||||
this.doRegisterConfigurations(configurations, validate, properties);
|
|
||||||
|
|
||||||
this.schemaChangeEmitter.emit();
|
this.doRegisterConfigurations(configurations, validate, properties);
|
||||||
this.updateConfigurationEmitter.emit({ properties });
|
this.propertiesChangeEmitter.emit({ properties });
|
||||||
|
|
||||||
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
private doRegisterConfigurations(
|
private doRegisterConfigurations(
|
||||||
@ -156,7 +185,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
bucket,
|
bucket,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.configurationContributors.push(configuration);
|
this.registerJSONConfiguration(configuration);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +197,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
): void {
|
): void {
|
||||||
const properties = configuration.properties;
|
const properties = configuration.properties;
|
||||||
if (properties) {
|
if (properties) {
|
||||||
for (const key in properties) {
|
for (const key of Object.keys(properties)) {
|
||||||
const property: IRegisteredConfigurationPropertySchema = properties[key];
|
const property: IRegisteredConfigurationPropertySchema = properties[key];
|
||||||
|
|
||||||
if (validate && this.validateProperty(key)) {
|
if (validate && this.validateProperty(key)) {
|
||||||
@ -178,14 +207,12 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
property.source = extensionInfo;
|
property.source = extensionInfo;
|
||||||
|
|
||||||
// update default value
|
// update default value
|
||||||
this.updatePropertyDefaultValue(property);
|
property.defaultDefaultValue = properties[key].default;
|
||||||
|
this.updatePropertyDefaultValue(key, property);
|
||||||
|
|
||||||
// Add to properties maps
|
// Add to properties maps
|
||||||
// Property is included by default if 'included' is unspecified
|
// Property is included by default if 'included' is unspecified
|
||||||
if (
|
if (properties[key].included === false) {
|
||||||
Object.prototype.hasOwnProperty.call(properties[key], 'included') &&
|
|
||||||
!properties[key].included
|
|
||||||
) {
|
|
||||||
this.excludedConfigurationProperties[key] = properties[key];
|
this.excludedConfigurationProperties[key] = properties[key];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -216,12 +243,26 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updatePropertyDefaultValue(property: IRegisteredConfigurationPropertySchema): void {
|
private updatePropertyDefaultValue(
|
||||||
|
key: string,
|
||||||
|
property: IRegisteredConfigurationPropertySchema,
|
||||||
|
): void {
|
||||||
let defaultValue = undefined;
|
let defaultValue = undefined;
|
||||||
let defaultSource = 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)) {
|
if (isUndefined(defaultValue)) {
|
||||||
defaultValue = property.default;
|
defaultValue = property.defaultDefaultValue;
|
||||||
defaultSource = undefined;
|
defaultSource = undefined;
|
||||||
}
|
}
|
||||||
if (isUndefined(defaultValue)) {
|
if (isUndefined(defaultValue)) {
|
||||||
@ -235,9 +276,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
deregisterConfigurations(configurations: IConfigurationNode[]): void {
|
deregisterConfigurations(configurations: IConfigurationNode[]): void {
|
||||||
const properties = new Set<string>();
|
const properties = new Set<string>();
|
||||||
this.doDeregisterConfigurations(configurations, properties);
|
this.doDeregisterConfigurations(configurations, properties);
|
||||||
|
this.propertiesChangeEmitter.emit({ properties });
|
||||||
this.schemaChangeEmitter.emit();
|
|
||||||
this.updateConfigurationEmitter.emit({ properties });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private doDeregisterConfigurations(
|
private doDeregisterConfigurations(
|
||||||
@ -246,9 +285,10 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
): void {
|
): void {
|
||||||
const deregisterConfiguration = (configuration: IConfigurationNode) => {
|
const deregisterConfiguration = (configuration: IConfigurationNode) => {
|
||||||
if (configuration.properties) {
|
if (configuration.properties) {
|
||||||
for (const key in configuration.properties) {
|
for (const key of Object.keys(configuration.properties)) {
|
||||||
bucket.add(key);
|
bucket.add(key);
|
||||||
delete this.configurationProperties[key];
|
delete this.configurationProperties[key];
|
||||||
|
this.removeFromSchema(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
configuration.allOf?.forEach((node) => deregisterConfiguration(node));
|
configuration.allOf?.forEach((node) => deregisterConfiguration(node));
|
||||||
@ -256,46 +296,351 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
|||||||
|
|
||||||
for (const configuration of configurations) {
|
for (const configuration of configurations) {
|
||||||
deregisterConfiguration(configuration);
|
deregisterConfiguration(configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const index = this.configurationContributors.indexOf(configuration);
|
registerDefaultConfigurations(configurationDefaults: IConfigurationDefaults[]): void {
|
||||||
|
const properties = new Set<string>();
|
||||||
|
|
||||||
|
this.doRegisterDefaultConfigurations(configurationDefaults, properties);
|
||||||
|
this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private doRegisterDefaultConfigurations(
|
||||||
|
configurationDefaults: IConfigurationDefaults[],
|
||||||
|
bucket: Set<string>,
|
||||||
|
) {
|
||||||
|
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<string, IExtensionInfo>();
|
||||||
|
|
||||||
|
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<string, IExtensionInfo>();
|
||||||
|
|
||||||
|
// 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<string>();
|
||||||
|
this.doDeregisterDefaultConfigurations(defaultConfigurations, properties);
|
||||||
|
|
||||||
|
this.propertiesChangeEmitter.emit({ properties, defaultsOverrides: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private doDeregisterDefaultConfigurations(
|
||||||
|
defaultConfigurations: IConfigurationDefaults[],
|
||||||
|
bucket: Set<string>,
|
||||||
|
): void {
|
||||||
|
for (const defaultConfiguration of defaultConfigurations) {
|
||||||
|
const index = this.registeredConfigurationDefaults.indexOf(defaultConfiguration);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.configurationContributors.splice(index, 1);
|
this.registeredConfigurationDefaults.splice(index, 1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyConfigurationSchemaUpdated(): void {
|
for (const { overrides, source } of defaultConfigurations) {
|
||||||
this.schemaChangeEmitter.emit();
|
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<IRegisteredConfigurationPropertySchema> {
|
getConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema> {
|
||||||
return this.configurationProperties;
|
return this.configurationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfigurations(): IConfigurationNode[] {
|
|
||||||
return this.configurationContributors;
|
|
||||||
}
|
|
||||||
|
|
||||||
getExcludedConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema> {
|
getExcludedConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema> {
|
||||||
return this.excludedConfigurationProperties;
|
return this.excludedConfigurationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRegisteredDefaultConfigurations(): IConfigurationDefaults[] {
|
||||||
|
return [...this.registeredConfigurationDefaults];
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverrideValue> {
|
||||||
|
const configurationDefaultsOverrides = new Map<string, IConfigurationDefaultOverrideValue>();
|
||||||
|
for (const [key, value] of this.configurationDefaultsOverrides) {
|
||||||
|
if (value.configurationDefaultOverrideValue) {
|
||||||
|
configurationDefaultsOverrides.set(key, value.configurationDefaultOverrideValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return configurationDefaultsOverrides;
|
||||||
|
}
|
||||||
|
|
||||||
onDidUpdateConfiguration(
|
onDidUpdateConfiguration(
|
||||||
fn: (change: {
|
fn: (change: {
|
||||||
properties: ReadonlySet<string>;
|
properties: ReadonlySet<string>;
|
||||||
defaultsOverrides?: boolean | undefined;
|
defaultsOverrides?: boolean | undefined;
|
||||||
}) => void,
|
}) => void,
|
||||||
) {
|
) {
|
||||||
return this.updateConfigurationEmitter.on(fn);
|
return this.propertiesChangeEmitter.on(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDidSchemaChange(fn: () => void) {
|
private registerJSONConfiguration(configuration: IConfigurationNode) {
|
||||||
return this.schemaChangeEmitter.on(fn);
|
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 = {
|
function isSameExtension(a?: IExtensionInfo, b?: IExtensionInfo): boolean {
|
||||||
Configuration: 'base.contributions.configuration',
|
if (!a || !b) return false;
|
||||||
};
|
return a.name === b.name;
|
||||||
|
}
|
||||||
|
|
||||||
Registry.add(Extension.Configuration, new ConfigurationRegistry());
|
Registry.add(Extensions.Configuration, new ConfigurationRegistry());
|
||||||
|
|||||||
@ -1,19 +1,29 @@
|
|||||||
import { createDecorator, Provide, type Event } from '@alilc/lowcode-shared';
|
import {
|
||||||
import { IConfigurationOverrides, IConfigurationUpdateOverrides } from './configuration';
|
createDecorator,
|
||||||
|
Emitter,
|
||||||
export interface IConfigurationChangeEvent {
|
Provide,
|
||||||
readonly affectedKeys: ReadonlySet<string>;
|
type Event,
|
||||||
readonly change: IConfigurationChange;
|
type EventListener,
|
||||||
|
} from '@alilc/lowcode-shared';
|
||||||
affectsConfiguration(configuration: string, overrides?: string[]): boolean;
|
import {
|
||||||
}
|
Configuration,
|
||||||
|
DefaultConfiguration,
|
||||||
export interface IConfigurationChange {
|
type IConfigurationData,
|
||||||
keys: string[];
|
type IConfigurationOverrides,
|
||||||
overrides: [string, string[]][];
|
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 {
|
export interface IConfigurationService {
|
||||||
|
initialize(): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the value of the section for the given overrides.
|
* Fetches the value of the section for the given overrides.
|
||||||
* Value can be of native type or an object keyed off the section name.
|
* 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
|
* @param value The new value
|
||||||
*/
|
*/
|
||||||
updateValue(key: string, value: any): Promise<void>;
|
updateValue(key: string, value: any): Promise<void>;
|
||||||
updateValue(
|
updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise<void>;
|
||||||
key: string,
|
|
||||||
value: any,
|
|
||||||
overrides: IConfigurationOverrides | IConfigurationUpdateOverrides,
|
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
inspect<T>(key: string, overrides?: IConfigurationOverrides): Readonly<T>;
|
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<Readonly<T>>;
|
||||||
|
|
||||||
reloadConfiguration(): Promise<void>;
|
reloadConfiguration(): Promise<void>;
|
||||||
|
|
||||||
keys(): string[];
|
keys(): {
|
||||||
|
default: string[];
|
||||||
|
user: string[];
|
||||||
|
memory?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
onDidChangeConfiguration: Event<IConfigurationChangeEvent>;
|
onDidChangeConfiguration: Event<IConfigurationChangeEvent>;
|
||||||
}
|
}
|
||||||
@ -60,4 +70,134 @@ export interface IConfigurationService {
|
|||||||
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
||||||
|
|
||||||
@Provide(IConfigurationService)
|
@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<IConfigurationChangeEvent>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.defaultConfiguration = new DefaultConfiguration();
|
||||||
|
this.userConfiguration = new UserConfiguration({});
|
||||||
|
this.configuration = new Configuration(
|
||||||
|
this.defaultConfiguration.configurationModel,
|
||||||
|
ConfigurationModel.createEmptyModel(),
|
||||||
|
ConfigurationModel.createEmptyModel(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
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>(): T;
|
||||||
|
getValue<T>(section: string): T;
|
||||||
|
getValue<T>(overrides: IConfigurationOverrides): T;
|
||||||
|
getValue<T>(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<void>;
|
||||||
|
updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise<void>;
|
||||||
|
async updateValue(key: string, value: any, arg3?: IConfigurationOverrides): Promise<void> {
|
||||||
|
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<T>(key: string, overrides: IConfigurationOverrides = {}): IConfigurationValue<T> {
|
||||||
|
return this.configuration.inspect<T>(key, overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(): {
|
||||||
|
default: string[];
|
||||||
|
user: string[];
|
||||||
|
} {
|
||||||
|
return this.configuration.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
async reloadConfiguration(): Promise<void> {
|
||||||
|
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<IConfigurationChangeEvent>) {
|
||||||
|
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}]`,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
592
packages/engine-core/src/configuration/configurations.ts
Normal file
592
packages/engine-core/src/configuration/configurations.ts
Normal file
@ -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<IConfigurationRegistry>(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<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties(),
|
||||||
|
);
|
||||||
|
this.emitter.emit({ defaults: this.configurationModel, properties });
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetConfigurationModel(): void {
|
||||||
|
this._configurationModel = ConfigurationModel.createEmptyModel();
|
||||||
|
|
||||||
|
const properties = Registry.as<IConfigurationRegistry>(
|
||||||
|
Extensions.Configuration,
|
||||||
|
).getConfigurationProperties();
|
||||||
|
|
||||||
|
this.updateConfigurationModel(Object.keys(properties), properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateConfigurationModel(
|
||||||
|
properties: string[],
|
||||||
|
configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>,
|
||||||
|
): 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<IConfigurationRegistry>(
|
||||||
|
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<ConfigurationModel> {
|
||||||
|
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<void> {
|
||||||
|
// todo: scheduler
|
||||||
|
// 本地同步远程服务器
|
||||||
|
this.parser.configurationModel.setValue(path.join('.'), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConfigurationValue<T> {
|
||||||
|
readonly defaultValue?: T;
|
||||||
|
readonly userValue?: T;
|
||||||
|
readonly memoryValue?: T;
|
||||||
|
readonly value?: T;
|
||||||
|
|
||||||
|
readonly default?: IInspectValue<T>;
|
||||||
|
readonly user?: IInspectValue<T>;
|
||||||
|
readonly memory?: IInspectValue<T>;
|
||||||
|
|
||||||
|
readonly overrideIdentifiers?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigurationInspectValue<V> implements IConfigurationValue<V> {
|
||||||
|
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<V> | undefined | null,
|
||||||
|
): IInspectValue<V> | undefined {
|
||||||
|
return inspectValue?.value !== undefined ||
|
||||||
|
inspectValue?.override !== undefined ||
|
||||||
|
inspectValue?.overrides !== undefined
|
||||||
|
? inspectValue
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _defaultInspectValue: InspectValue<V> | undefined;
|
||||||
|
private get defaultInspectValue(): InspectValue<V> {
|
||||||
|
if (!this._defaultInspectValue) {
|
||||||
|
this._defaultInspectValue = this.defaultConfiguration.inspect<V>(
|
||||||
|
this.key,
|
||||||
|
this.overrides.overrideIdentifier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this._defaultInspectValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaultValue(): V | undefined {
|
||||||
|
return this.defaultInspectValue.merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
get default(): IInspectValue<V> | undefined {
|
||||||
|
return this.toInspectValue(this.defaultInspectValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _userInspectValue: InspectValue<V> | undefined;
|
||||||
|
private get userInspectValue(): InspectValue<V> {
|
||||||
|
if (!this._userInspectValue) {
|
||||||
|
this._userInspectValue = this.userConfiguration.inspect<V>(
|
||||||
|
this.key,
|
||||||
|
this.overrides.overrideIdentifier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this._userInspectValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userValue(): V | undefined {
|
||||||
|
return this.userInspectValue.merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
get user(): IInspectValue<V> | undefined {
|
||||||
|
return this.toInspectValue(this.userInspectValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _memoryInspectValue: InspectValue<V> | undefined;
|
||||||
|
private get memoryInspectValue(): InspectValue<V> {
|
||||||
|
if (this._memoryInspectValue === undefined) {
|
||||||
|
this._memoryInspectValue = this.memoryConfigurationModel.inspect<V>(
|
||||||
|
this.key,
|
||||||
|
this.overrides.overrideIdentifier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this._memoryInspectValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
get memoryValue(): V | undefined {
|
||||||
|
return this.memoryInspectValue.merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
get memory(): IInspectValue<V> | 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<C>(key: string, overrides: IConfigurationOverrides): IConfigurationValue<C> {
|
||||||
|
const consolidateConfigurationModel = this.getConsolidatedConfigurationModel(overrides);
|
||||||
|
|
||||||
|
const overrideIdentifiers = new Set<string>();
|
||||||
|
for (const override of consolidateConfigurationModel.overrides) {
|
||||||
|
for (const overrideIdentifier of override.identifiers) {
|
||||||
|
if (consolidateConfigurationModel.getOverrideValue(key, overrideIdentifier) !== undefined) {
|
||||||
|
overrideIdentifiers.add(overrideIdentifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConfigurationInspectValue<C>(
|
||||||
|
key,
|
||||||
|
overrides,
|
||||||
|
consolidateConfigurationModel.getValue<C>(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 };
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './configurationModel';
|
export * from './configurationModel';
|
||||||
export * from './configurationRegistry';
|
export * from './configurationRegistry';
|
||||||
export * from './configuration';
|
export * from './configurations';
|
||||||
|
export * from './configurationService';
|
||||||
|
|||||||
37
packages/engine-core/src/extension/extension.ts
Normal file
37
packages/engine-core/src/extension/extension.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { StringDictionary } from '@alilc/lowcode-shared';
|
||||||
|
import { IConfigurationNode } from '../configuration';
|
||||||
|
|
||||||
|
export type ExtensionInitializer = <Context = any>(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> | void;
|
||||||
|
destroy(): Promise<void> | void;
|
||||||
|
exports?(): StringDictionary | undefined | void;
|
||||||
|
}
|
||||||
61
packages/engine-core/src/extension/extensionHost.ts
Normal file
61
packages/engine-core/src/extension/extensionHost.ts
Normal file
@ -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<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
initializer: ExtensionInitializer,
|
||||||
|
preferenceConfigurations: IConfigurationNode[],
|
||||||
|
) {
|
||||||
|
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||||
|
this.configurationProperties =
|
||||||
|
configurationRegistry.registerConfigurations(preferenceConfigurations);
|
||||||
|
|
||||||
|
this.instance = initializer({});
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
if (this.isInited) return;
|
||||||
|
|
||||||
|
await this.instance.init();
|
||||||
|
|
||||||
|
this.isInited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
92
packages/engine-core/src/extension/extensionManagement.ts
Normal file
92
packages/engine-core/src/extension/extensionManagement.ts
Normal file
@ -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<string, IExtensionGallery> = new Map();
|
||||||
|
private extensionHosts: Map<string, ExtensionHost> = new Map();
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async register(
|
||||||
|
extension: IFunctionExtension,
|
||||||
|
{ autoInit = false, override = false }: IExtensionRegisterOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
packages/engine-core/src/extension/extensionService.ts
Normal file
37
packages/engine-core/src/extension/extensionService.ts
Normal file
@ -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<void>;
|
||||||
|
|
||||||
|
deregister(name: string): Promise<void>;
|
||||||
|
|
||||||
|
has(name: string): boolean;
|
||||||
|
|
||||||
|
getExtensionHost(name: string): ExtensionHost | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
|
||||||
|
|
||||||
|
@Provide(IExtensionService)
|
||||||
|
export class ExtensionService implements IExtensionService {
|
||||||
|
private extensionManagement = new ExtensionManagement();
|
||||||
|
|
||||||
|
register(extension: IFunctionExtension, options?: IExtensionRegisterOptions): Promise<void> {
|
||||||
|
return this.extensionManagement.register(extension, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
deregister(name: string): Promise<void> {
|
||||||
|
return this.extensionManagement.deregister(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
has(name: string): boolean {
|
||||||
|
return this.extensionManagement.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getExtensionHost(name: string): ExtensionHost | undefined {
|
||||||
|
return this.extensionManagement.getExtensionHost(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from './registry';
|
|
||||||
@ -1,2 +1,8 @@
|
|||||||
export * from './configuration';
|
export * from './configuration';
|
||||||
export * from './extension';
|
export * from './extension/extension';
|
||||||
|
export * from './resource';
|
||||||
|
export * from './command';
|
||||||
|
|
||||||
|
// test
|
||||||
|
export * from './common/registry';
|
||||||
|
export * from './main';
|
||||||
|
|||||||
39
packages/engine-core/src/keybinding/keybinding.ts
Normal file
39
packages/engine-core/src/keybinding/keybinding.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/engine-core/src/keybinding/keybindingRegistry.ts
Normal file
26
packages/engine-core/src/keybinding/keybindingRegistry.ts
Normal file
@ -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[];
|
||||||
|
};
|
||||||
|
}
|
||||||
26
packages/engine-core/src/main.ts
Normal file
26
packages/engine-core/src/main.ts
Normal file
@ -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<MainApplication> {
|
||||||
|
const app = new MainApplication();
|
||||||
|
|
||||||
|
await app.main();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
@ -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<ContextExtra extends Record<string, any>> {
|
|
||||||
pluginName: string;
|
|
||||||
meta?: PluginMeta;
|
|
||||||
enhance?: (context: PluginContext<ContextExtra>, pluginName: string, meta: PluginMeta) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PluginContext<ContextExtra extends Record<string, any>> {
|
|
||||||
#pluginManager: PluginManager<ContextExtra>;
|
|
||||||
#meta: PluginMeta = {};
|
|
||||||
|
|
||||||
public pluginName: string;
|
|
||||||
|
|
||||||
public pluginEvent: EventEmitter;
|
|
||||||
|
|
||||||
public preference: PluginPreferenceMananger;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
options: PluginContextOptions<ContextExtra>,
|
|
||||||
pluginManager: PluginManager<ContextExtra>,
|
|
||||||
) {
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from './types';
|
|
||||||
export * from './manager';
|
|
||||||
@ -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<string, Record<string, PluginPreferenceValue>>;
|
|
||||||
|
|
||||||
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<ContextExtra extends Record<string, any>> {
|
|
||||||
#pluginsMap: Map<string, PluginRuntime<ContextExtra>> = new Map();
|
|
||||||
|
|
||||||
#pluginContextMap: Map<string, PluginContext<ContextExtra>> = new Map();
|
|
||||||
|
|
||||||
#contextEnhancer: PluginContextOptions<ContextExtra>['enhance'] = () => {};
|
|
||||||
|
|
||||||
#pluginPreference: PluginPreference | undefined;
|
|
||||||
|
|
||||||
constructor(contextEnhancer?: PluginContextOptions<ContextExtra>['enhance']) {
|
|
||||||
if (contextEnhancer) {
|
|
||||||
this.#contextEnhancer = contextEnhancer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#getPluginContext = (options: PluginContextOptions<ContextExtra>) => {
|
|
||||||
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<PluginContext<ContextExtra>>,
|
|
||||||
options?: any,
|
|
||||||
registerOptions?: PluginRegisterOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
// 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<ContextExtra> | undefined {
|
|
||||||
return this.#pluginsMap.get(pluginName);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll(): PluginRuntime<ContextExtra>[] {
|
|
||||||
return [...this.#pluginsMap.values()];
|
|
||||||
}
|
|
||||||
|
|
||||||
has(pluginName: string): boolean {
|
|
||||||
return this.#pluginsMap.has(pluginName);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(pluginName: string): Promise<boolean> {
|
|
||||||
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<ContextExtra> } = {};
|
|
||||||
|
|
||||||
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<string, PluginPreferenceValue> | 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;
|
|
||||||
}
|
|
||||||
@ -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<ContextExtra extends Record<string, any>> {
|
|
||||||
#inited: boolean;
|
|
||||||
/**
|
|
||||||
* 标识插件状态,是否被 disabled
|
|
||||||
*/
|
|
||||||
#disabled: boolean;
|
|
||||||
|
|
||||||
#logger: Logger;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private pluginName: string,
|
|
||||||
private manager: PluginManager<ContextExtra>,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
export interface PluginInstance {
|
|
||||||
init(): Promise<void> | void;
|
|
||||||
destroy?(): Promise<void> | 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<Context> {
|
|
||||||
(ctx: Context, options: any): PluginInstance;
|
|
||||||
pluginName: string;
|
|
||||||
meta?: PluginMeta;
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { Assets } from '@alilc/lowcode-shared';
|
|
||||||
|
|
||||||
export interface IResourceManagementService {
|
|
||||||
setAssets(assets: Assets): void;
|
|
||||||
}
|
|
||||||
1
packages/engine-core/src/resource/index.ts
Normal file
1
packages/engine-core/src/resource/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './resourceService';
|
||||||
61
packages/engine-core/src/resource/resourceModel.ts
Normal file
61
packages/engine-core/src/resource/resourceModel.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { type Package, mapPackageToUniqueId } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export class ResourceModel {
|
||||||
|
private packagesRef: Package[] = [];
|
||||||
|
private idToPackageMap: Map<string, Package> = new Map();
|
||||||
|
private packageToLibraryMap: WeakMap<Package, any> = 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<T = any>(id: string): T | undefined {
|
||||||
|
const refedPackage = this.idToPackageMap.get(id);
|
||||||
|
if (refedPackage) return this.packageToLibraryMap.get(refedPackage);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
packages/engine-core/src/resource/resourceService.ts
Normal file
51
packages/engine-core/src/resource/resourceService.ts
Normal file
@ -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<void>;
|
||||||
|
|
||||||
|
loadPackages(schemas: Package[]): Promise<void>;
|
||||||
|
|
||||||
|
getByReference<T = any>(reference: Reference): T | undefined;
|
||||||
|
|
||||||
|
getPackages(idOrName: string): Package[] | undefined;
|
||||||
|
|
||||||
|
getAllPackages(): Package[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IResourceService = createDecorator<IResourceService>('resourceService');
|
||||||
|
|
||||||
|
@Provide(IResourceService)
|
||||||
|
export class ResourceService implements IResourceService {
|
||||||
|
private resourceModel = new ResourceModel();
|
||||||
|
|
||||||
|
loadPackage(pkg: Package): Promise<void> {
|
||||||
|
return this.loadPackages([pkg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPackages(packags: Package[]): Promise<void> {}
|
||||||
|
|
||||||
|
getByReference<T = any>(reference: Reference): T | undefined {
|
||||||
|
const id = mapPackageToUniqueId(reference);
|
||||||
|
const library = this.resourceModel.getPackageLibrary<T>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/engine-core/src/workbench/index.ts
Normal file
1
packages/engine-core/src/workbench/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './workbenchService';
|
||||||
28
packages/engine-core/src/workbench/layout/layout.ts
Normal file
28
packages/engine-core/src/workbench/layout/layout.ts
Normal file
@ -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<View> implements ILayout {
|
||||||
|
constructor(public mainContainer: HTMLElement) {
|
||||||
|
Registry.as<IWidgetRegistry<View>>(Extensions.Widget).onDidRegister(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPart(part: LayoutParts): void {}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
|
import { createDecorator } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
export interface ILayoutService {
|
export interface ILayoutService {
|
||||||
/**
|
layout(): void;
|
||||||
* Main container of the application.
|
|
||||||
*/
|
|
||||||
mainContainer: HTMLElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ILayoutService = createDecorator<ILayoutService>('layoutService');
|
||||||
|
|||||||
25
packages/engine-core/src/workbench/widget/widget.ts
Normal file
25
packages/engine-core/src/workbench/widget/widget.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { LayoutParts } from '../layout/layout';
|
||||||
|
|
||||||
|
export interface IWidget<View> {
|
||||||
|
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<View> implements IWidget<View> {
|
||||||
|
constructor(
|
||||||
|
public readonly id: string,
|
||||||
|
public readonly target: LayoutParts,
|
||||||
|
public readonly content: View,
|
||||||
|
public readonly action: any,
|
||||||
|
public readonly metadata: IWidgetMetadata = {},
|
||||||
|
) {}
|
||||||
|
}
|
||||||
37
packages/engine-core/src/workbench/widget/widgetRegistry.ts
Normal file
37
packages/engine-core/src/workbench/widget/widgetRegistry.ts
Normal file
@ -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<View> {
|
||||||
|
onDidRegister: Event<IWidget<View>[]>;
|
||||||
|
|
||||||
|
registerWidget(widget: IWidget<View>): string;
|
||||||
|
|
||||||
|
registerWidgets(widgets: IWidget<View>[]): string[];
|
||||||
|
|
||||||
|
getWidgets(): IWidget<View>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WidgetRegistry<View> implements IWidgetRegistry<View> {
|
||||||
|
private _widgets: Map<string, IWidget<View>> = new Map();
|
||||||
|
|
||||||
|
private emitter = new Emitter<IWidget<View>[]>();
|
||||||
|
|
||||||
|
onDidRegister(fn: EventListener<IWidget<View>[]>) {
|
||||||
|
return this.emitter.on(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWidgets(): IWidget<View>[] {
|
||||||
|
return Array.from(this._widgets.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWidget(widget: IWidget<View>): string {
|
||||||
|
return widget.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerWidgets(widgets: IWidget<View>[]): string[] {
|
||||||
|
return widgets.map((widget) => this.registerWidget(widget));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Registry.add(Extensions.Widget, new WidgetRegistry<any>());
|
||||||
14
packages/engine-core/src/workbench/workbenchService.ts
Normal file
14
packages/engine-core/src/workbench/workbenchService.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export interface IWorkbenchService {
|
||||||
|
initialize(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IWorkbenchService = createDecorator<IWorkbenchService>('workbenchService');
|
||||||
|
|
||||||
|
@Provide(IWorkbenchService)
|
||||||
|
export class WorkbenchService implements IWorkbenchService {
|
||||||
|
initialize(): void {
|
||||||
|
console.log('workbench service');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,4 @@
|
|||||||
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
/**
|
||||||
|
* 工作空间:一个或多个项目的集合
|
||||||
export interface IWorkspaceService {
|
*/
|
||||||
mount(container: HTMLElement): void;
|
export interface Workspace {}
|
||||||
}
|
|
||||||
|
|
||||||
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
|
|
||||||
|
|
||||||
@Provide(IWorkspaceService)
|
|
||||||
export class WorkspaceService implements IWorkspaceService {
|
|
||||||
mount(container: HTMLElement): void {}
|
|
||||||
}
|
|
||||||
|
|||||||
12
packages/engine-core/src/workspace/workspaceService.ts
Normal file
12
packages/engine-core/src/workspace/workspaceService.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||||
|
|
||||||
|
export interface IWorkspaceService {
|
||||||
|
mount(container: HTMLElement): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
|
||||||
|
|
||||||
|
@Provide(IWorkspaceService)
|
||||||
|
export class WorkspaceService implements IWorkspaceService {
|
||||||
|
mount(container: HTMLElement): void {}
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
# `@alilc/plugin-command`
|
|
||||||
|
|
||||||
> TODO: description
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
const pluginCommand = require('@alilc/plugin-command');
|
|
||||||
|
|
||||||
// TODO: DEMONSTRATE API
|
|
||||||
```
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@alilc/lowcode-plugin-command",
|
|
||||||
"version": "2.0.0-beta.0",
|
|
||||||
"description": "> TODO: description",
|
|
||||||
"author": "liujuping <liujup@foxmail.com>",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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',
|
|
||||||
};
|
|
||||||
@ -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;
|
|
||||||
@ -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<string, any>;
|
|
||||||
}) {
|
|
||||||
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',
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"emitDeclarationOnly": true,
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "temp",
|
|
||||||
"stripInternal": true,
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@ -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']
|
|
||||||
})
|
|
||||||
});
|
|
||||||
106
packages/plugin-designer/.gitignore
vendored
106
packages/plugin-designer/.gitignore
vendored
@ -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
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"@alilc/build-plugin-lce"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
.lowcode-plugin-designer {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@ -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<PluginProps, DesignerPluginState> {
|
|
||||||
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 (
|
|
||||||
<DesignerView
|
|
||||||
onMount={this.handleDesignerMount}
|
|
||||||
className="lowcode-plugin-designer"
|
|
||||||
editor={editor}
|
|
||||||
name={editor.viewName}
|
|
||||||
designer={editor.get('designer')}
|
|
||||||
componentMetadatas={componentMetadatas}
|
|
||||||
shellModelFactory={{} as any}
|
|
||||||
simulatorProps={{
|
|
||||||
library,
|
|
||||||
utilsMetadata,
|
|
||||||
extraEnvironment,
|
|
||||||
renderEnv,
|
|
||||||
device,
|
|
||||||
locale,
|
|
||||||
designMode,
|
|
||||||
deviceClassName,
|
|
||||||
simulatorUrl,
|
|
||||||
requestHandlersMap,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"emitDeclarationOnly": true,
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "temp",
|
|
||||||
"stripInternal": true,
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@ -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'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -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<ITreeBoard>();
|
|
||||||
|
|
||||||
private treeMap = new Map<string, Tree>();
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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<string, TreeNode>();
|
|
||||||
|
|
||||||
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<string, TreeNode>();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export const BackupPaneName = 'outline-backup-pane';
|
|
||||||
export const MasterPaneName = 'outline-master-pane';
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconArrowRight(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M512.002047 771.904425c-10.152221 0.518816-20.442588-2.800789-28.202319-10.598382L77.902254 315.937602c-14.548344-14.618952-14.548344-38.318724 0-52.933583 14.544251-14.614859 38.118156-14.614859 52.662407 0l381.437385 418.531212L893.432269 263.004019c14.544251-14.614859 38.125319-14.614859 52.662407 0 14.552437 14.614859 14.552437 38.314631 0 52.933583L540.205389 761.307066C532.451798 769.103636 522.158361 772.424264 512.002047 771.904425z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconArrowRight.displayName = 'IconArrowRight';
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconCond(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M479.552 276.544l296.896 2.752v75.712L960 249.024l-183.552-106.048v92.48h-271.36l-46.656-2.752-190.784 203.648 30.976 30.976 180.928-190.784z m296.896 484.928l-253.056-2.816-262.976-263.04H64v43.904h175.296l262.912 262.976 274.176 2.816v75.712L960 774.976l-183.616-105.984 0.064 92.48z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconCond.displayName = 'IconCond';
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconDelete(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M224 256v639.84A64 64 0 0 0 287.84 960h448.32A64 64 0 0 0 800 895.84V256h64a32 32 0 1 0 0-64H160a32 32 0 1 0 0 64h64zM384 96c0-17.664 14.496-32 31.904-32h192.192C625.696 64 640 78.208 640 96c0 17.664-14.496 32-31.904 32H415.904A31.872 31.872 0 0 1 384 96z m-96 191.744C288 270.208 302.4 256 320.224 256h383.552C721.6 256 736 270.56 736 287.744v576.512C736 881.792 721.6 896 703.776 896H320.224A32.224 32.224 0 0 1 288 864.256V287.744zM352 352c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconDelete.displayName = 'IconDelete';
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconEyeClose(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M512.7 700.9c-102.1 0-184.9-82.8-184.9-184.9 0-28.6 6.5-55.6 18-79.7l-93.7-93.7C138.9 418.1 65.2 514 65.2 514s200.4 260.7 447.6 260.7c50.2 0 98.6-10.8 143.6-27.9l-63.9-63.9c-24.2 11.5-51.2 18-79.8 18z" />
|
|
||||||
<path d="M960.3 514S759.9 253.3 512.7 253.3c-49.5 0-97.2 10.5-141.7 27.2L243.5 153.1l-45.3 45.3 262.3 262.2c-13.1 13.3-21.2 31.5-21.2 51.6 0 40.6 32.9 73.4 73.4 73.4 20.1 0 38.4-8.1 51.6-21.2l260.9 260.8 45.3-45.3-95.6-95.6C887.2 609.1 960.3 514 960.3 514z m-376.7-20.9c-6.8-25.2-26.6-45.1-51.9-51.9L437.5 347c23-10.3 48.5-16 75.3-16 102.1 0 184.9 82.8 184.9 184.9 0 26.8-5.7 52.2-15.9 75.2l-98.2-98z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconEyeClose.displayName = 'IconEyeClose';
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconEye(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M512 256c-163.8 0-291.4 97.6-448 256 134.8 135.4 248 256 448 256 199.8 0 346.8-152.8 448-253.2C856.4 397.2 709.6 256 512 256z m0 438.6c-98.8 0-179.2-82-179.2-182.6 0-100.8 80.4-182.6 179.2-182.6s179.2 82 179.2 182.6c0 100.8-80.4 182.6-179.2 182.6z" />
|
|
||||||
<path d="M512 448c0-15.8 5.8-30.2 15.2-41.4-5-0.8-10-1.2-15.2-1.2-57.6 0-104.6 47.8-104.6 106.6s47 106.6 104.6 106.6 104.6-47.8 104.6-106.6c0-4.6-0.4-9.2-0.8-13.8-11 8.6-24.6 13.8-39.6 13.8-35.6 0-64.2-28.6-64.2-64z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconEye.displayName = 'IconEye';
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconFilter(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M911.457097 168.557714a35.986286 35.986286 0 0 1-8.009143 40.009143L621.73824 490.276571V914.285714c0 14.848-9.142857 28.013714-22.272 33.718857A42.349714 42.349714 0 0 1 585.166811 950.857143a34.084571 34.084571 0 0 1-25.709714-10.861714l-146.285714-146.285715A36.425143 36.425143 0 0 1 402.309669 768v-277.723429L120.599954 208.566857a35.986286 35.986286 0 0 1-8.009143-40.009143C118.295954 155.428571 131.461669 146.285714 146.309669 146.285714h731.428571c14.848 0 28.013714 9.142857 33.718857 22.272z" fill="#666" p-id="2025" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconFilter.displayName = 'IconFilter';
|
|
||||||
@ -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';
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconLock(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM540 701v53c0 4.4-3.6 8-8 8h-40c-4.4 0-8-3.6-8-8v-53c-12.1-8.7-20-22.9-20-39 0-26.5 21.5-48 48-48s48 21.5 48 48c0 16.1-7.9 30.3-20 39z m152-237H332V240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconLock.displayName = 'IconLock';
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconLoop(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M60.235294 542.117647c0 132.879059 103.062588 240.941176 229.677176 240.941176l0 60.235294c-159.864471 0-289.912471-135.107765-289.912471-301.176471s130.048-301.176471 289.912471-301.176471l254.735059 0-99.147294-99.147294 42.586353-42.586353 171.911529 171.851294-171.851294 171.911529-42.646588-42.646588 99.207529-99.147294-254.795294 0c-126.614588 0-229.677176 108.062118-229.677176 240.941176zM734.087529 240.941176l0 60.235294c126.614588 0 229.677176 108.062118 229.677176 240.941176s-103.062588 240.941176-229.677176 240.941176l-254.795294 0 99.147294-99.147294-42.586353-42.586353-171.851294 171.851294 171.911529 171.911529 42.586353-42.586353-99.207529-99.207529 254.735059 0c159.924706 0 289.972706-135.107765 289.972706-301.176471s-130.048-301.176471-289.912471-301.176471z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconLoop.displayName = 'IconLoop';
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconOutline(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M128 96h512a64 64 0 0 1 64 64v64a64 64 0 0 1-64 64H128a64 64 0 0 1-64-64V160a64 64 0 0 1 64-64z m32 64a32 32 0 1 0 0 64h448a32 32 0 0 0 0-64H160z m224 576h512a64 64 0 0 1 64 64v64a64 64 0 0 1-64 64H384a64 64 0 0 1-64-64v-64a64 64 0 0 1 64-64z m32 64a32 32 0 0 0 0 64h448a32 32 0 0 0 0-64H416z m-32-384h512a64 64 0 0 1 64 64v64a64 64 0 0 1-64 64H384a64 64 0 0 1-64-64v-64a64 64 0 0 1 64-64z m32 64a32 32 0 0 0 0 64h448a32 32 0 0 0 0-64H416z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconOutline.displayName = 'IconOutline';
|
|
||||||
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconRadioActive(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024z m0-256a256 256 0 1 0 0-512 256 256 0 0 0 0 512z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconRadioActive.displayName = 'IconRadioActive';
|
|
||||||
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconRadio(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M512 1024A512 512 0 1 1 512 0a512 512 0 0 1 0 1024z m0-64A448 448 0 1 0 512 64a448 448 0 0 0 0 896z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconRadio.displayName = 'IconRadio';
|
|
||||||
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconSetting(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M965.824 405.952a180.48 180.48 0 0 1-117.12-85.376 174.464 174.464 0 0 1-16-142.08 22.208 22.208 0 0 0-7.04-23.552 480.576 480.576 0 0 0-153.6-89.216 23.104 23.104 0 0 0-24.32 5.76 182.208 182.208 0 0 1-135.68 57.92 182.208 182.208 0 0 1-133.12-56.64 23.104 23.104 0 0 0-26.88-7.04 478.656 478.656 0 0 0-153.6 89.856 22.208 22.208 0 0 0-7.04 23.552 174.464 174.464 0 0 1-16 141.44A180.48 180.48 0 0 1 58.24 405.952a22.4 22.4 0 0 0-17.28 17.792 455.08 455.08 0 0 0 0 176.512 22.4 22.4 0 0 0 17.28 17.792 180.48 180.48 0 0 1 117.12 84.736c25.408 42.944 31.232 94.592 16 142.08a22.208 22.208 0 0 0 7.04 23.552A480.576 480.576 0 0 0 352 957.632h7.68a23.04 23.04 0 0 0 16.64-7.04 184.128 184.128 0 0 1 266.944 0c6.592 8.96 18.752 11.968 28.8 7.04a479.36 479.36 0 0 0 156.16-88.576 22.208 22.208 0 0 0 7.04-23.552 174.464 174.464 0 0 1 13.44-142.72 180.48 180.48 0 0 1 117.12-84.736 22.4 22.4 0 0 0 17.28-17.792 452.613 452.613 0 0 0 0-176.512 23.04 23.04 0 0 0-17.28-17.792z m-42.88 169.408a218.752 218.752 0 0 0-128 98.112 211.904 211.904 0 0 0-21.76 156.736 415.936 415.936 0 0 1-112 63.68 217.472 217.472 0 0 0-149.12-63.68 221.312 221.312 0 0 0-149.12 63.68 414.592 414.592 0 0 1-112-63.68c12.8-53.12 4.288-109.12-23.68-156.096A218.752 218.752 0 0 0 101.12 575.36a386.176 386.176 0 0 1 0-127.36 218.752 218.752 0 0 0 128-98.112c27.2-47.552 34.944-103.68 21.76-156.8a415.296 415.296 0 0 1 112-63.68A221.44 221.44 0 0 0 512 187.392a218.24 218.24 0 0 0 149.12-57.984 413.952 413.952 0 0 1 112 63.744 211.904 211.904 0 0 0 23.04 156.096 218.752 218.752 0 0 0 128 98.112 386.65 386.65 0 0 1 0 127.36l-1.28 0.64z" />
|
|
||||||
<path d="M512 320.576c-105.984 0-192 85.568-192 191.104a191.552 191.552 0 0 0 192 191.104c106.112 0 192.064-85.568 192.064-191.104a190.72 190.72 0 0 0-56.256-135.168 192.448 192.448 0 0 0-135.744-55.936z m0 318.528c-70.656 0-128-57.088-128-127.424 0-70.4 57.344-127.36 128-127.36 70.72 0 128 56.96 128 127.36 0 33.792-13.44 66.176-37.44 90.112a128.32 128.32 0 0 1-90.496 37.312z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconSetting.displayName = 'IconSetting';
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
|
|
||||||
|
|
||||||
export function IconUnlock(props: IconProps) {
|
|
||||||
return (
|
|
||||||
<SVGIcon viewBox="0 0 1024 1024" {...props}>
|
|
||||||
<path d="M832 464H332V240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v68c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-68c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32z m-40 376H232V536h560v304z" />
|
|
||||||
<path d="M484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53c12.1-8.7 20-22.9 20-39 0-26.5-21.5-48-48-48s-48 21.5-48 48c0 16.1 7.9 30.3 20 39z" />
|
|
||||||
</SVGIcon>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconUnlock.displayName = 'IconUnlock';
|
|
||||||
@ -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 (
|
|
||||||
<Pane
|
|
||||||
treeMaster={treeMaster}
|
|
||||||
controller={masterPaneController}
|
|
||||||
key={masterPaneController.id}
|
|
||||||
hideFilter={props.hideFilter}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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';
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
import enUS from './en-US.json';
|
|
||||||
import zhCN from './zh-CN.json';
|
|
||||||
|
|
||||||
export { enUS, zhCN };
|
|
||||||
@ -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": "删除"
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
declare module 'ric-shim';
|
|
||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -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 (
|
|
||||||
<div className="lc-outline-filter">
|
|
||||||
<Search
|
|
||||||
hasClear
|
|
||||||
shape="simple"
|
|
||||||
placeholder={this.props.tree.pluginContext.intl('Filter Node')}
|
|
||||||
className="lc-outline-filter-search-input"
|
|
||||||
value={keywords}
|
|
||||||
onChange={this.handleSearchChange}
|
|
||||||
/>
|
|
||||||
<Balloon
|
|
||||||
v2
|
|
||||||
align="br"
|
|
||||||
closable={false}
|
|
||||||
triggerType="hover"
|
|
||||||
trigger={
|
|
||||||
<div className="lc-outline-filter-icon">
|
|
||||||
<IconFilter />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Checkbox checked={checkAll} indeterminate={indeterminate} onChange={this.handleCheckAll}>
|
|
||||||
{this.props.tree.pluginContext.intlNode('Check All')}
|
|
||||||
</Checkbox>
|
|
||||||
<Divider />
|
|
||||||
<Checkbox.Group
|
|
||||||
value={filterOps}
|
|
||||||
direction="ver"
|
|
||||||
onChange={this.handleOptionChange as any}
|
|
||||||
>
|
|
||||||
{FILTER_OPTIONS.map((op) => (
|
|
||||||
<Checkbox id={op.value} value={op.value} key={op.value}>
|
|
||||||
{this.props.tree.pluginContext.intlNode(op.label)}
|
|
||||||
</Checkbox>
|
|
||||||
))}
|
|
||||||
</Checkbox.Group>
|
|
||||||
</Balloon>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 (
|
|
||||||
<div className="lc-outline-pane">
|
|
||||||
<p className="lc-outline-notice">
|
|
||||||
<Loading
|
|
||||||
style={{
|
|
||||||
display: 'block',
|
|
||||||
marginTop: '40px',
|
|
||||||
}}
|
|
||||||
tip={this.props.treeMaster.pluginContext.intl('Initializing')}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="lc-outline-pane">
|
|
||||||
{ !this.props.hideFilter && <Filter tree={tree} /> }
|
|
||||||
<div ref={(shell) => this.controller.mount(shell)} className={`lc-outline-tree-container ${ this.props.hideFilter ? 'lc-hidden-outline-filter' : '' }`}>
|
|
||||||
<TreeView key={tree.id} tree={tree} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 (
|
|
||||||
<div className="tree-node-branches">
|
|
||||||
{
|
|
||||||
!isModal && <TreeNodeSlots treeNode={treeNode} />
|
|
||||||
}
|
|
||||||
<TreeNodeChildren
|
|
||||||
treeNode={treeNode}
|
|
||||||
isModal={isModal || false}
|
|
||||||
treeChildren={this.props.treeChildren}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 key={currentGrp.id} className="condition-group-container" data-id={currentGrp.firstNode?.id}>
|
|
||||||
<div className="condition-group-title">
|
|
||||||
<Title
|
|
||||||
title={currentGrp.title}
|
|
||||||
match={filterWorking && matchSelf}
|
|
||||||
keywords={keywords}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"emitDeclarationOnly": true,
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "temp",
|
|
||||||
"stripInternal": true,
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user