mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-06-01 21:20:47 +00:00
feat: add some common codes
This commit is contained in:
parent
3d3952ecae
commit
d86b0fdaa0
@ -35,9 +35,6 @@
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@alilc/lowcode-shared": "workspace:*"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"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 {
|
||||
commandId: string;
|
||||
@ -16,19 +16,12 @@ export interface ICommand {
|
||||
}
|
||||
|
||||
export interface ICommandMetadata {
|
||||
/**
|
||||
* A short summary of what the command does. This will be used in:
|
||||
* - API commands
|
||||
* - when showing keybindings that have no other UX
|
||||
* - when searching for commands in the Command Palette
|
||||
*/
|
||||
readonly description: string;
|
||||
readonly args?: ReadonlyArray<{
|
||||
readonly name: string;
|
||||
readonly isOptional?: boolean;
|
||||
readonly description?: string;
|
||||
// readonly constraint?: TypeConstraint;
|
||||
// readonly schema?: IJSONSchema;
|
||||
readonly constraint?: TypeConstraint;
|
||||
readonly default?: any;
|
||||
}>;
|
||||
readonly returns?: string;
|
||||
}
|
||||
|
||||
@ -3,9 +3,14 @@ import {
|
||||
type EventDisposable,
|
||||
type EventListener,
|
||||
Emitter,
|
||||
LinkedList,
|
||||
TypeConstraint,
|
||||
validateConstraints,
|
||||
Iterable,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { ICommand, ICommandHandler } from './command';
|
||||
import { Registry } from '../extension';
|
||||
import { Extensions, Registry } from '../common/registry';
|
||||
import { ICommandService } from './commandService';
|
||||
|
||||
export type ICommandsMap = Map<string, ICommand>;
|
||||
|
||||
@ -24,10 +29,10 @@ export interface ICommandRegistry {
|
||||
class CommandsRegistry implements ICommandRegistry {
|
||||
private readonly _commands = new Map<string, LinkedList<ICommand>>();
|
||||
|
||||
private readonly _onDidRegisterCommand = new Emitter<string>();
|
||||
private readonly _didRegisterCommandEmitter = new Emitter<string>();
|
||||
|
||||
onDidRegisterCommand(fn: EventListener<string>) {
|
||||
return this._onDidRegisterCommand.on(fn);
|
||||
return this._didRegisterCommandEmitter.on(fn);
|
||||
}
|
||||
|
||||
registerCommand(idOrCommand: string | ICommand, handler?: ICommandHandler): EventDisposable {
|
||||
@ -66,21 +71,21 @@ class CommandsRegistry implements ICommandRegistry {
|
||||
|
||||
const removeFn = commands.unshift(idOrCommand);
|
||||
|
||||
const ret = toDisposable(() => {
|
||||
const ret = () => {
|
||||
removeFn();
|
||||
const command = this._commands.get(id);
|
||||
if (command?.isEmpty()) {
|
||||
this._commands.delete(id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// tell the world about this command
|
||||
this._onDidRegisterCommand.emit(id);
|
||||
this._didRegisterCommandEmitter.emit(id);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
registerCommandAlias(oldId: string, newId: string): IDisposable {
|
||||
registerCommandAlias(oldId: string, newId: string): EventDisposable {
|
||||
return this.registerCommand(oldId, (accessor, ...args) =>
|
||||
accessor.get(ICommandService).executeCommand(newId, ...args),
|
||||
);
|
||||
@ -108,8 +113,4 @@ class CommandsRegistry implements ICommandRegistry {
|
||||
|
||||
const commandsRegistry = new CommandsRegistry();
|
||||
|
||||
export const Extension = {
|
||||
command: 'base.contributions.command',
|
||||
};
|
||||
|
||||
Registry.add(Extension.command, commandsRegistry);
|
||||
Registry.add(Extensions.Command, commandsRegistry);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createDecorator, Provide } from '@alilc/lowcode-shared';
|
||||
import { Registry } from '../extension';
|
||||
import { ICommandRegistry, Extension } from './commandRegistry';
|
||||
import { createDecorator, Provide, IInstantiationService } from '@alilc/lowcode-shared';
|
||||
import { Registry, Extensions } from '../common/registry';
|
||||
import { ICommandRegistry } from './commandRegistry';
|
||||
|
||||
export interface ICommandService {
|
||||
executeCommand<T = any>(commandId: string, ...args: any[]): Promise<T | undefined>;
|
||||
@ -10,11 +10,23 @@ export const ICommandService = createDecorator<ICommandService>('commandService'
|
||||
|
||||
@Provide(ICommandService)
|
||||
export class CommandService implements ICommandService {
|
||||
executeCommand<T = any>(id: string, ...args: any[]): Promise<T | undefined> {
|
||||
const command = Registry.as<ICommandRegistry>(Extension.command).getCommand(id);
|
||||
constructor(@IInstantiationService private instantiationService: IInstantiationService) {}
|
||||
|
||||
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) {
|
||||
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 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 { ConfigurationModel } from './configurationModel';
|
||||
import {
|
||||
type IConfigurationRegistry,
|
||||
type IRegisteredConfigurationPropertySchema,
|
||||
Extension,
|
||||
} from './configurationRegistry';
|
||||
import { Registry } from '../extension';
|
||||
import { type StringDictionary } from '@alilc/lowcode-shared';
|
||||
import { uniq } from 'lodash-es';
|
||||
|
||||
export interface IConfigurationOverrides {
|
||||
overrideIdentifier?: string | null;
|
||||
export interface IInspectValue<T> {
|
||||
readonly value?: T;
|
||||
readonly override?: T;
|
||||
readonly overrides?: { readonly identifiers: string[]; readonly value: T }[];
|
||||
}
|
||||
|
||||
export interface IConfigurationUpdateOverrides {
|
||||
overrideIdentifiers?: string[] | null;
|
||||
export function toValuesTree(properties: StringDictionary): any {
|
||||
const root = Object.create(null);
|
||||
|
||||
for (const key of Object.keys(properties)) {
|
||||
addToValueTree(root, key, properties[key]);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
export class DefaultConfiguration {
|
||||
private emitter = new Emitter<{
|
||||
defaults: ConfigurationModel;
|
||||
properties: string[];
|
||||
}>();
|
||||
export function addToValueTree(
|
||||
settingsTreeRoot: any,
|
||||
key: string,
|
||||
value: any,
|
||||
conflictReporter: (message: string) => void = console.error,
|
||||
): void {
|
||||
const segments = key.split('.');
|
||||
const last = segments.pop()!;
|
||||
|
||||
private _configurationModel = ConfigurationModel.createEmptyModel();
|
||||
|
||||
get configurationModel(): ConfigurationModel {
|
||||
return this._configurationModel;
|
||||
let curr = settingsTreeRoot;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const s = segments[i];
|
||||
let obj = curr[s];
|
||||
switch (typeof obj) {
|
||||
case 'undefined':
|
||||
obj = curr[s] = Object.create(null);
|
||||
break;
|
||||
case 'object':
|
||||
if (obj === null) {
|
||||
conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is null`);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
conflictReporter(
|
||||
`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
curr = obj;
|
||||
}
|
||||
|
||||
initialize(): ConfigurationModel {
|
||||
this.resetConfigurationModel();
|
||||
Registry.as<IConfigurationRegistry>(Extension.Configuration).onDidUpdateConfiguration(
|
||||
({ properties }) => this.onDidUpdateConfiguration([...properties]),
|
||||
);
|
||||
if (typeof curr === 'object' && curr !== null) {
|
||||
try {
|
||||
curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606
|
||||
} catch (e) {
|
||||
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
|
||||
}
|
||||
} else {
|
||||
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return this.configurationModel;
|
||||
export function removeFromValueTree(valueTree: any, key: string): void {
|
||||
const segments = key.split('.');
|
||||
doRemoveFromValueTree(valueTree, segments);
|
||||
}
|
||||
|
||||
function doRemoveFromValueTree(valueTree: any, segments: string[]): void {
|
||||
const first = segments.shift()!;
|
||||
if (segments.length === 0) {
|
||||
// Reached last segment
|
||||
delete valueTree[first];
|
||||
return;
|
||||
}
|
||||
|
||||
reload(): ConfigurationModel {
|
||||
this.resetConfigurationModel();
|
||||
return this.configurationModel;
|
||||
}
|
||||
|
||||
onDidChangeConfiguration(
|
||||
listener: EventListener<[{ defaults: ConfigurationModel; properties: string[] }]>,
|
||||
) {
|
||||
return this.emitter.on(listener);
|
||||
}
|
||||
|
||||
private onDidUpdateConfiguration(properties: string[]): void {
|
||||
this.updateConfigurationModel(
|
||||
properties,
|
||||
Registry.as<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 {
|
||||
this.configurationModel.removeValue(key);
|
||||
if (Object.keys(valueTree).includes(first)) {
|
||||
const value = valueTree[first];
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
doRemoveFromValueTree(value, segments);
|
||||
if (Object.keys(value).length === 0) {
|
||||
delete valueTree[first];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OVERRIDE_IDENTIFIER_PATTERN = `\\[([^\\]]+)\\]`;
|
||||
const OVERRIDE_IDENTIFIER_REGEX = new RegExp(OVERRIDE_IDENTIFIER_PATTERN, 'g');
|
||||
export const OVERRIDE_PROPERTY_PATTERN = `^(${OVERRIDE_IDENTIFIER_PATTERN})+$`;
|
||||
export const OVERRIDE_PROPERTY_REGEX = new RegExp(OVERRIDE_PROPERTY_PATTERN);
|
||||
|
||||
export function overrideIdentifiersFromKey(key: string): string[] {
|
||||
const identifiers: string[] = [];
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
let matches = OVERRIDE_IDENTIFIER_REGEX.exec(key);
|
||||
while (matches?.length) {
|
||||
const identifier = matches[1].trim();
|
||||
if (identifier) {
|
||||
identifiers.push(identifier);
|
||||
}
|
||||
matches = OVERRIDE_IDENTIFIER_REGEX.exec(key);
|
||||
}
|
||||
}
|
||||
return uniq(identifiers);
|
||||
}
|
||||
|
||||
@ -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 { 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> = {
|
||||
readonly value?: V;
|
||||
readonly override?: V;
|
||||
readonly overrides?: { readonly identifiers: string[]; readonly value: V }[];
|
||||
merged?: V;
|
||||
};
|
||||
export type InspectValue<V> = IInspectValue<V> & { merged?: V };
|
||||
|
||||
export interface IConfigurationModel {
|
||||
contents: any;
|
||||
@ -327,80 +329,3 @@ export class ConfigurationModel implements IConfigurationModel {
|
||||
return uniq(result);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromValueTree(valueTree: any, key: string): void {
|
||||
const segments = key.split('.');
|
||||
doRemoveFromValueTree(valueTree, segments);
|
||||
}
|
||||
|
||||
function doRemoveFromValueTree(valueTree: any, segments: string[]): void {
|
||||
const first = segments.shift()!;
|
||||
if (segments.length === 0) {
|
||||
// Reached last segment
|
||||
delete valueTree[first];
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(valueTree).includes(first)) {
|
||||
const value = valueTree[first];
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
doRemoveFromValueTree(value, segments);
|
||||
if (Object.keys(value).length === 0) {
|
||||
delete valueTree[first];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToValueTree(
|
||||
settingsTreeRoot: any,
|
||||
key: string,
|
||||
value: any,
|
||||
conflictReporter: (message: string) => void = console.error,
|
||||
): void {
|
||||
const segments = key.split('.');
|
||||
const last = segments.pop()!;
|
||||
|
||||
let curr = settingsTreeRoot;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const s = segments[i];
|
||||
let obj = curr[s];
|
||||
switch (typeof obj) {
|
||||
case 'undefined':
|
||||
obj = curr[s] = Object.create(null);
|
||||
break;
|
||||
case 'object':
|
||||
if (obj === null) {
|
||||
conflictReporter(`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is null`);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
conflictReporter(
|
||||
`Ignoring ${key} as ${segments.slice(0, i + 1).join('.')} is ${JSON.stringify(obj)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
curr = obj;
|
||||
}
|
||||
|
||||
if (typeof curr === 'object' && curr !== null) {
|
||||
try {
|
||||
curr[last] = value; // workaround https://github.com/microsoft/vscode/issues/13606
|
||||
} catch (e) {
|
||||
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
|
||||
}
|
||||
} else {
|
||||
conflictReporter(`Ignoring ${key} as ${segments.join('.')} is ${JSON.stringify(curr)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function toValuesTree(properties: StringDictionary): any {
|
||||
const root = Object.create(null);
|
||||
|
||||
for (const key in properties) {
|
||||
addToValueTree(root, key, properties[key]);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@ -2,31 +2,14 @@ import {
|
||||
type Event,
|
||||
Emitter,
|
||||
type StringDictionary,
|
||||
type JSONValueType,
|
||||
type JSONSchemaType,
|
||||
jsonTypes,
|
||||
IJSONSchema,
|
||||
types,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import { uniq, isUndefined } from 'lodash-es';
|
||||
import { Registry } from '../extension/registry';
|
||||
|
||||
const OVERRIDE_IDENTIFIER_PATTERN = `\\[([^\\]]+)\\]`;
|
||||
const OVERRIDE_IDENTIFIER_REGEX = new RegExp(OVERRIDE_IDENTIFIER_PATTERN, 'g');
|
||||
export const OVERRIDE_PROPERTY_PATTERN = `^(${OVERRIDE_IDENTIFIER_PATTERN})+$`;
|
||||
export const OVERRIDE_PROPERTY_REGEX = new RegExp(OVERRIDE_PROPERTY_PATTERN);
|
||||
|
||||
export function overrideIdentifiersFromKey(key: string): string[] {
|
||||
const identifiers: string[] = [];
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
let matches = OVERRIDE_IDENTIFIER_REGEX.exec(key);
|
||||
while (matches?.length) {
|
||||
const identifier = matches[1].trim();
|
||||
if (identifier) {
|
||||
identifiers.push(identifier);
|
||||
}
|
||||
matches = OVERRIDE_IDENTIFIER_REGEX.exec(key);
|
||||
}
|
||||
}
|
||||
return uniq(identifiers);
|
||||
}
|
||||
import { isUndefined, isObject } from 'lodash-es';
|
||||
import { Extensions, Registry } from '../common/registry';
|
||||
import { OVERRIDE_PROPERTY_REGEX, overrideIdentifiersFromKey } from './configuration';
|
||||
|
||||
export interface IConfigurationRegistry {
|
||||
/**
|
||||
@ -37,7 +20,10 @@ export interface IConfigurationRegistry {
|
||||
/**
|
||||
* Register multiple configurations to the registry.
|
||||
*/
|
||||
registerConfigurations(configurations: IConfigurationNode[], validate?: boolean): void;
|
||||
registerConfigurations(
|
||||
configurations: IConfigurationNode[],
|
||||
validate?: boolean,
|
||||
): ReadonlySet<string>;
|
||||
|
||||
/**
|
||||
* Deregister multiple configurations from the registry.
|
||||
@ -45,15 +31,15 @@ export interface IConfigurationRegistry {
|
||||
deregisterConfigurations(configurations: IConfigurationNode[]): void;
|
||||
|
||||
/**
|
||||
* Signal that the schema of a configuration setting has changes. It is currently only supported to change enumeration values.
|
||||
* Property or default value changes are not allowed.
|
||||
* Register multiple default configurations to the registry.
|
||||
*/
|
||||
notifyConfigurationSchemaUpdated(): void;
|
||||
registerDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
|
||||
|
||||
/**
|
||||
* Event that fires whenever a configuration has been
|
||||
* registered.
|
||||
* Deregister multiple default configurations from the registry.
|
||||
*/
|
||||
readonly onDidSchemaChange: Event<void>;
|
||||
deregisterDefaultConfigurations(defaultConfigurations: IConfigurationDefaults[]): void;
|
||||
|
||||
/**
|
||||
* Event that fires whenever a configuration has been
|
||||
* registered.
|
||||
@ -63,10 +49,6 @@ export interface IConfigurationRegistry {
|
||||
defaultsOverrides?: boolean;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Returns all configuration nodes contributed to this registry.
|
||||
*/
|
||||
getConfigurations(): IConfigurationNode[];
|
||||
/**
|
||||
* Returns all configurations settings of all configuration nodes contributed to this registry.
|
||||
*/
|
||||
@ -75,12 +57,22 @@ export interface IConfigurationRegistry {
|
||||
* Returns all excluded configurations settings of all configuration nodes contributed to this registry.
|
||||
*/
|
||||
getExcludedConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
|
||||
/**
|
||||
* Return the registered default configurations
|
||||
*/
|
||||
getRegisteredDefaultConfigurations(): IConfigurationDefaults[];
|
||||
|
||||
/**
|
||||
* Return the registered configuration defaults overrides
|
||||
*/
|
||||
getConfigurationDefaultsOverrides(): Map<string, IConfigurationDefaultOverrideValue>;
|
||||
}
|
||||
|
||||
export interface IConfigurationNode {
|
||||
id?: string;
|
||||
order?: number;
|
||||
type?: JSONValueType | JSONValueType[];
|
||||
type?: JSONSchemaType | JSONSchemaType[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
properties?: StringDictionary<IConfigurationPropertySchema>;
|
||||
@ -88,45 +80,78 @@ export interface IConfigurationNode {
|
||||
extensionInfo?: IExtensionInfo;
|
||||
}
|
||||
|
||||
export interface IConfigurationPropertySchema {
|
||||
type?: JSONValueType;
|
||||
default?: any;
|
||||
tags?: string[];
|
||||
export interface IConfigurationPropertySchema extends IJSONSchema {
|
||||
/**
|
||||
* 当该属性为“false”时,将从注册表中排除该属性。默认为包含。
|
||||
*/
|
||||
included?: boolean;
|
||||
deprecated?: boolean;
|
||||
deprecationMessage?: string;
|
||||
/**
|
||||
* 不允许扩展为此设置贡献配置默认值。
|
||||
*/
|
||||
disallowConfigurationDefault?: boolean;
|
||||
/**
|
||||
* 与属性关联的标签列表。
|
||||
* - 标签可用于过滤
|
||||
*/
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展信息,用来查找对应属性的源扩展
|
||||
*/
|
||||
export interface IExtensionInfo {
|
||||
id: string;
|
||||
displayName?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ConfigurationDefaultValueSource = IExtensionInfo | Map<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 {
|
||||
overrides: StringDictionary;
|
||||
source?: IExtensionInfo;
|
||||
}
|
||||
|
||||
export interface IRegisteredConfigurationPropertySchema extends IConfigurationPropertySchema {
|
||||
source?: IExtensionInfo; // Source of the Property
|
||||
defaultValueSource?: ConfigurationDefaultValueSource; // Source of the Default Value
|
||||
export interface IConfigurationDefaultOverride {
|
||||
readonly value: any;
|
||||
readonly source?: IExtensionInfo; // Source of the default override
|
||||
}
|
||||
|
||||
export interface IConfigurationDefaultOverrideValue {
|
||||
readonly value: any;
|
||||
readonly source?: IExtensionInfo | Map<string, IExtensionInfo>;
|
||||
}
|
||||
|
||||
export const allSettings: {
|
||||
properties: StringDictionary<IConfigurationPropertySchema>;
|
||||
patternProperties: StringDictionary<IConfigurationPropertySchema>;
|
||||
} = { properties: {}, patternProperties: {} };
|
||||
|
||||
export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
private configurationContributors: IConfigurationNode[];
|
||||
private registeredConfigurationDefaults: IConfigurationDefaults[] = [];
|
||||
private configurationDefaultsOverrides: Map<
|
||||
string,
|
||||
{
|
||||
configurationDefaultOverrides: IConfigurationDefaultOverride[];
|
||||
configurationDefaultOverrideValue?: IConfigurationDefaultOverrideValue;
|
||||
}
|
||||
>;
|
||||
|
||||
private configurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private excludedConfigurationProperties: StringDictionary<IRegisteredConfigurationPropertySchema>;
|
||||
private overrideIdentifiers = new Set<string>();
|
||||
|
||||
private schemaChangeEmitter = new Emitter<void>();
|
||||
private updateConfigurationEmitter = new Emitter<{
|
||||
private propertiesChangeEmitter = new Emitter<{
|
||||
properties: ReadonlySet<string>;
|
||||
defaultsOverrides?: boolean;
|
||||
}>();
|
||||
|
||||
constructor() {
|
||||
this.configurationContributors = [];
|
||||
this.configurationDefaultsOverrides = new Map();
|
||||
this.configurationProperties = {};
|
||||
this.excludedConfigurationProperties = {};
|
||||
}
|
||||
@ -135,12 +160,16 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
this.registerConfigurations([configuration], validate);
|
||||
}
|
||||
|
||||
registerConfigurations(configurations: IConfigurationNode[], validate: boolean = true): void {
|
||||
registerConfigurations(
|
||||
configurations: IConfigurationNode[],
|
||||
validate: boolean = true,
|
||||
): ReadonlySet<string> {
|
||||
const properties = new Set<string>();
|
||||
this.doRegisterConfigurations(configurations, validate, properties);
|
||||
|
||||
this.schemaChangeEmitter.emit();
|
||||
this.updateConfigurationEmitter.emit({ properties });
|
||||
this.doRegisterConfigurations(configurations, validate, properties);
|
||||
this.propertiesChangeEmitter.emit({ properties });
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private doRegisterConfigurations(
|
||||
@ -156,7 +185,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
bucket,
|
||||
);
|
||||
|
||||
this.configurationContributors.push(configuration);
|
||||
this.registerJSONConfiguration(configuration);
|
||||
});
|
||||
}
|
||||
|
||||
@ -168,7 +197,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
): void {
|
||||
const properties = configuration.properties;
|
||||
if (properties) {
|
||||
for (const key in properties) {
|
||||
for (const key of Object.keys(properties)) {
|
||||
const property: IRegisteredConfigurationPropertySchema = properties[key];
|
||||
|
||||
if (validate && this.validateProperty(key)) {
|
||||
@ -178,14 +207,12 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
property.source = extensionInfo;
|
||||
|
||||
// update default value
|
||||
this.updatePropertyDefaultValue(property);
|
||||
property.defaultDefaultValue = properties[key].default;
|
||||
this.updatePropertyDefaultValue(key, property);
|
||||
|
||||
// Add to properties maps
|
||||
// Property is included by default if 'included' is unspecified
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(properties[key], 'included') &&
|
||||
!properties[key].included
|
||||
) {
|
||||
if (properties[key].included === false) {
|
||||
this.excludedConfigurationProperties[key] = properties[key];
|
||||
continue;
|
||||
}
|
||||
@ -216,12 +243,26 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
return null;
|
||||
}
|
||||
|
||||
private updatePropertyDefaultValue(property: IRegisteredConfigurationPropertySchema): void {
|
||||
private updatePropertyDefaultValue(
|
||||
key: string,
|
||||
property: IRegisteredConfigurationPropertySchema,
|
||||
): void {
|
||||
let defaultValue = undefined;
|
||||
let defaultSource = undefined;
|
||||
|
||||
const configurationdefaultOverride =
|
||||
this.configurationDefaultsOverrides.get(key)?.configurationDefaultOverrideValue;
|
||||
|
||||
// Prevent overriding the default value if the property is disallowed to be overridden by configuration defaults from extensions
|
||||
if (
|
||||
configurationdefaultOverride &&
|
||||
(!property.disallowConfigurationDefault || !configurationdefaultOverride.source)
|
||||
) {
|
||||
defaultValue = configurationdefaultOverride.value;
|
||||
defaultSource = configurationdefaultOverride.source;
|
||||
}
|
||||
if (isUndefined(defaultValue)) {
|
||||
defaultValue = property.default;
|
||||
defaultValue = property.defaultDefaultValue;
|
||||
defaultSource = undefined;
|
||||
}
|
||||
if (isUndefined(defaultValue)) {
|
||||
@ -235,9 +276,7 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
deregisterConfigurations(configurations: IConfigurationNode[]): void {
|
||||
const properties = new Set<string>();
|
||||
this.doDeregisterConfigurations(configurations, properties);
|
||||
|
||||
this.schemaChangeEmitter.emit();
|
||||
this.updateConfigurationEmitter.emit({ properties });
|
||||
this.propertiesChangeEmitter.emit({ properties });
|
||||
}
|
||||
|
||||
private doDeregisterConfigurations(
|
||||
@ -246,9 +285,10 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
): void {
|
||||
const deregisterConfiguration = (configuration: IConfigurationNode) => {
|
||||
if (configuration.properties) {
|
||||
for (const key in configuration.properties) {
|
||||
for (const key of Object.keys(configuration.properties)) {
|
||||
bucket.add(key);
|
||||
delete this.configurationProperties[key];
|
||||
this.removeFromSchema(key);
|
||||
}
|
||||
}
|
||||
configuration.allOf?.forEach((node) => deregisterConfiguration(node));
|
||||
@ -256,46 +296,351 @@ export class ConfigurationRegistry implements IConfigurationRegistry {
|
||||
|
||||
for (const configuration of configurations) {
|
||||
deregisterConfiguration(configuration);
|
||||
|
||||
const index = this.configurationContributors.indexOf(configuration);
|
||||
if (index !== -1) {
|
||||
this.configurationContributors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyConfigurationSchemaUpdated(): void {
|
||||
this.schemaChangeEmitter.emit();
|
||||
registerDefaultConfigurations(configurationDefaults: IConfigurationDefaults[]): void {
|
||||
const properties = new Set<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) {
|
||||
this.registeredConfigurationDefaults.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { overrides, source } of defaultConfigurations) {
|
||||
for (const key in overrides) {
|
||||
const configurationDefaultOverridesForKey = this.configurationDefaultsOverrides.get(key);
|
||||
if (!configurationDefaultOverridesForKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = configurationDefaultOverridesForKey.configurationDefaultOverrides.findIndex(
|
||||
(configurationDefaultOverride) =>
|
||||
source
|
||||
? isSameExtension(configurationDefaultOverride.source, source)
|
||||
: configurationDefaultOverride.value === overrides[key],
|
||||
);
|
||||
if (index === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
configurationDefaultOverridesForKey.configurationDefaultOverrides.splice(index, 1);
|
||||
if (configurationDefaultOverridesForKey.configurationDefaultOverrides.length === 0) {
|
||||
this.configurationDefaultsOverrides.delete(key);
|
||||
}
|
||||
|
||||
if (OVERRIDE_PROPERTY_REGEX.test(key)) {
|
||||
let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined;
|
||||
|
||||
// configuration override defaults - merges defaults
|
||||
for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) {
|
||||
configurationDefaultOverrideValue =
|
||||
this.mergeDefaultConfigurationsForOverrideIdentifier(
|
||||
key,
|
||||
configurationDefaultOverride.value,
|
||||
configurationDefaultOverride.source,
|
||||
configurationDefaultOverrideValue,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
configurationDefaultOverrideValue &&
|
||||
!types.isEmptyObject(configurationDefaultOverrideValue.value)
|
||||
) {
|
||||
configurationDefaultOverridesForKey.configurationDefaultOverrideValue =
|
||||
configurationDefaultOverrideValue;
|
||||
this.updateDefaultOverrideProperty(key, configurationDefaultOverrideValue, source);
|
||||
} else {
|
||||
this.configurationDefaultsOverrides.delete(key);
|
||||
delete this.configurationProperties[key];
|
||||
}
|
||||
} else {
|
||||
let configurationDefaultOverrideValue: IConfigurationDefaultOverrideValue | undefined;
|
||||
|
||||
// configuration override defaults - merges defaults
|
||||
for (const configurationDefaultOverride of configurationDefaultOverridesForKey.configurationDefaultOverrides) {
|
||||
configurationDefaultOverrideValue =
|
||||
this.mergeDefaultConfigurationsForConfigurationProperty(
|
||||
key,
|
||||
configurationDefaultOverride.value,
|
||||
configurationDefaultOverride.source,
|
||||
configurationDefaultOverrideValue,
|
||||
);
|
||||
}
|
||||
|
||||
configurationDefaultOverridesForKey.configurationDefaultOverrideValue =
|
||||
configurationDefaultOverrideValue;
|
||||
|
||||
const property = this.configurationProperties[key];
|
||||
if (property) {
|
||||
this.updatePropertyDefaultValue(key, property);
|
||||
this.updateSchema(key, property);
|
||||
}
|
||||
}
|
||||
bucket.add(key);
|
||||
}
|
||||
}
|
||||
this.updateOverridePropertyPatternKey();
|
||||
}
|
||||
|
||||
private doRegisterOverrideIdentifiers(overrideIdentifiers: string[]) {
|
||||
for (const overrideIdentifier of overrideIdentifiers) {
|
||||
this.overrideIdentifiers.add(overrideIdentifier);
|
||||
}
|
||||
this.updateOverridePropertyPatternKey();
|
||||
}
|
||||
|
||||
private updateOverridePropertyPatternKey() {
|
||||
for (const overrideIdentifier of this.overrideIdentifiers.values()) {
|
||||
const overrideIdentifierProperty = `[${overrideIdentifier}]`;
|
||||
const propertiesSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
description: 'overrideSettings.defaultDescription',
|
||||
errorMessage: 'overrideSettings.errorMessage',
|
||||
};
|
||||
this.updatePropertyDefaultValue(overrideIdentifierProperty, propertiesSchema);
|
||||
allSettings.properties[overrideIdentifierProperty] = propertiesSchema;
|
||||
}
|
||||
}
|
||||
|
||||
getConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema> {
|
||||
return this.configurationProperties;
|
||||
}
|
||||
|
||||
getConfigurations(): IConfigurationNode[] {
|
||||
return this.configurationContributors;
|
||||
}
|
||||
|
||||
getExcludedConfigurationProperties(): StringDictionary<IRegisteredConfigurationPropertySchema> {
|
||||
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(
|
||||
fn: (change: {
|
||||
properties: ReadonlySet<string>;
|
||||
defaultsOverrides?: boolean | undefined;
|
||||
}) => void,
|
||||
) {
|
||||
return this.updateConfigurationEmitter.on(fn);
|
||||
return this.propertiesChangeEmitter.on(fn);
|
||||
}
|
||||
|
||||
onDidSchemaChange(fn: () => void) {
|
||||
return this.schemaChangeEmitter.on(fn);
|
||||
private registerJSONConfiguration(configuration: IConfigurationNode) {
|
||||
const register = (configuration: IConfigurationNode) => {
|
||||
const properties = configuration.properties;
|
||||
if (properties) {
|
||||
Object.keys(properties).forEach((key) => {
|
||||
this.updateSchema(key, properties[key]);
|
||||
});
|
||||
}
|
||||
const subNodes = configuration.allOf;
|
||||
subNodes?.forEach(register);
|
||||
};
|
||||
register(configuration);
|
||||
}
|
||||
|
||||
private updateSchema(key: string, property: IConfigurationPropertySchema): void {
|
||||
allSettings.properties[key] = property;
|
||||
}
|
||||
|
||||
private removeFromSchema(key: string): void {
|
||||
delete allSettings.properties[key];
|
||||
}
|
||||
}
|
||||
|
||||
export const Extension = {
|
||||
Configuration: 'base.contributions.configuration',
|
||||
};
|
||||
function isSameExtension(a?: IExtensionInfo, b?: IExtensionInfo): boolean {
|
||||
if (!a || !b) return false;
|
||||
return a.name === b.name;
|
||||
}
|
||||
|
||||
Registry.add(Extension.Configuration, new ConfigurationRegistry());
|
||||
Registry.add(Extensions.Configuration, new ConfigurationRegistry());
|
||||
|
||||
@ -1,19 +1,29 @@
|
||||
import { createDecorator, Provide, type Event } from '@alilc/lowcode-shared';
|
||||
import { IConfigurationOverrides, IConfigurationUpdateOverrides } from './configuration';
|
||||
|
||||
export interface IConfigurationChangeEvent {
|
||||
readonly affectedKeys: ReadonlySet<string>;
|
||||
readonly change: IConfigurationChange;
|
||||
|
||||
affectsConfiguration(configuration: string, overrides?: string[]): boolean;
|
||||
}
|
||||
|
||||
export interface IConfigurationChange {
|
||||
keys: string[];
|
||||
overrides: [string, string[]][];
|
||||
}
|
||||
import {
|
||||
createDecorator,
|
||||
Emitter,
|
||||
Provide,
|
||||
type Event,
|
||||
type EventListener,
|
||||
} from '@alilc/lowcode-shared';
|
||||
import {
|
||||
Configuration,
|
||||
DefaultConfiguration,
|
||||
type IConfigurationData,
|
||||
type IConfigurationOverrides,
|
||||
type IConfigurationValue,
|
||||
UserConfiguration,
|
||||
} from './configurations';
|
||||
import { ConfigurationModel } from './configurationModel';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import {
|
||||
ConfigurationChangeEvent,
|
||||
type IConfigurationChangeEvent,
|
||||
type IConfigurationChange,
|
||||
} from './configurationChangeEvent';
|
||||
|
||||
export interface IConfigurationService {
|
||||
initialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Fetches the value of the section for the given overrides.
|
||||
* Value can be of native type or an object keyed off the section name.
|
||||
@ -42,17 +52,17 @@ export interface IConfigurationService {
|
||||
* @param value The new value
|
||||
*/
|
||||
updateValue(key: string, value: any): Promise<void>;
|
||||
updateValue(
|
||||
key: string,
|
||||
value: any,
|
||||
overrides: IConfigurationOverrides | IConfigurationUpdateOverrides,
|
||||
): Promise<void>;
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides): Promise<void>;
|
||||
|
||||
inspect<T>(key: string, overrides?: IConfigurationOverrides): Readonly<T>;
|
||||
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<Readonly<T>>;
|
||||
|
||||
reloadConfiguration(): Promise<void>;
|
||||
|
||||
keys(): string[];
|
||||
keys(): {
|
||||
default: string[];
|
||||
user: string[];
|
||||
memory?: string[];
|
||||
};
|
||||
|
||||
onDidChangeConfiguration: Event<IConfigurationChangeEvent>;
|
||||
}
|
||||
@ -60,4 +70,134 @@ export interface IConfigurationService {
|
||||
export const IConfigurationService = createDecorator<IConfigurationService>('configurationService');
|
||||
|
||||
@Provide(IConfigurationService)
|
||||
export class ConfigurationService implements IConfigurationService {}
|
||||
export class ConfigurationService implements IConfigurationService {
|
||||
private configuration: Configuration;
|
||||
private readonly defaultConfiguration: DefaultConfiguration;
|
||||
private readonly userConfiguration: UserConfiguration;
|
||||
|
||||
private readonly didChangeEmitter = new Emitter<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 './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 './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 {
|
||||
/**
|
||||
* Main container of the application.
|
||||
*/
|
||||
mainContainer: HTMLElement;
|
||||
layout(): void;
|
||||
}
|
||||
|
||||
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 const IWorkspaceService = createDecorator<IWorkspaceService>('workspaceService');
|
||||
|
||||
@Provide(IWorkspaceService)
|
||||
export class WorkspaceService implements IWorkspaceService {
|
||||
mount(container: HTMLElement): void {}
|
||||
}
|
||||
/**
|
||||
* 工作空间:一个或多个项目的集合
|
||||
*/
|
||||
export interface Workspace {}
|
||||
|
||||
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