feat: add some common codes

This commit is contained in:
1ncounter 2024-07-24 11:14:39 +08:00
parent 3d3952ecae
commit d86b0fdaa0
136 changed files with 2434 additions and 5945 deletions

View File

@ -35,9 +35,6 @@
"devDependencies": {
"@types/lodash-es": "^4.17.12"
},
"peerDependencies": {
"@alilc/lowcode-shared": "workspace:*"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"

View File

@ -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;
}

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
export * from './command';
export * from './commandRegistry';
export * from './commandService';

View File

@ -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',
};

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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());

View File

@ -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}]`,
'',
);
}

View 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 };
}

View File

@ -1,3 +1,4 @@
export * from './configurationModel';
export * from './configurationRegistry';
export * from './configuration';
export * from './configurations';
export * from './configurationService';

View 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;
}

View 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);
},
});
}
}

View 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);
}
}

View 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);
}
}

View File

@ -1 +0,0 @@
export * from './registry';

View File

@ -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';

View 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;
}
}

View 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[];
};
}

View 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;
}

View File

@ -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;
});
}

View File

@ -1,2 +0,0 @@
export * from './types';
export * from './manager';

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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,
};
}

View File

@ -1,5 +0,0 @@
import { Assets } from '@alilc/lowcode-shared';
export interface IResourceManagementService {
setAssets(assets: Assets): void;
}

View File

@ -0,0 +1 @@
export * from './resourceService';

View 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);
}
}

View 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();
}
}

View File

@ -0,0 +1 @@
export * from './workbenchService';

View 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 {}
}

View File

@ -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');

View 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 = {},
) {}
}

View 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>());

View 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');
}
}

View File

@ -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 {}

View 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 {}
}

View File

@ -1,11 +0,0 @@
# `@alilc/plugin-command`
> TODO: description
## Usage
```
const pluginCommand = require('@alilc/plugin-command');
// TODO: DEMONSTRATE API
```

View File

@ -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();
});
});

View File

@ -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"
}
}

View File

@ -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',
};

View File

@ -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;

View File

@ -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',
};

View File

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declaration": true,
"outDir": "temp",
"stripInternal": true,
"paths": {}
}
}

View File

@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}

View File

@ -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']
})
});

View File

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

View File

@ -1,5 +0,0 @@
{
"plugins": [
"@alilc/build-plugin-lce"
]
}

View File

@ -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"
}

View File

@ -1,4 +0,0 @@
.lowcode-plugin-designer {
width: 100%;
height: 100%;
}

View File

@ -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,
}}
/>
);
}
}

View File

@ -1,10 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": true,
"declaration": true,
"outDir": "temp",
"stripInternal": true,
"paths": {}
}
}

View File

@ -1,7 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}

View File

@ -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'
})
});

View File

@ -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"
}

View File

@ -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,
};
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
});
}
}
}

View File

@ -1,2 +0,0 @@
export const BackupPaneName = 'outline-backup-pane';
export const MasterPaneName = 'outline-master-pane';

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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"
}

View File

@ -1,4 +0,0 @@
import enUS from './en-US.json';
import zhCN from './zh-CN.json';
export { enUS, zhCN };

View File

@ -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": "删除"
}

View File

@ -1 +0,0 @@
declare module 'ric-shim';

View File

@ -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;
};

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

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