mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2026-01-13 01:21:58 +00:00
move setting to designer
This commit is contained in:
parent
f1b686bad5
commit
8a768177ce
@ -8,6 +8,7 @@ import {
|
||||
obx,
|
||||
computed,
|
||||
autorun,
|
||||
IEditor,
|
||||
} from '@ali/lowcode-globals';
|
||||
import { Project } from '../project';
|
||||
import { Node, DocumentModel, insertChildren, isRootNode, ParentalNode } from '../document';
|
||||
@ -20,6 +21,7 @@ import { Hovering } from './hovering';
|
||||
import { DropLocation, LocationData, isLocationChildrenDetail } from './location';
|
||||
import { OffsetObserver, createOffsetObserver } from './offset-observer';
|
||||
import { focusing } from './focusing';
|
||||
import { SettingTopEntry } from './setting';
|
||||
|
||||
export interface DesignerProps {
|
||||
className?: string;
|
||||
@ -198,6 +200,10 @@ export class Designer {
|
||||
return createOffsetObserver(nodeInstance);
|
||||
}
|
||||
|
||||
createSettingEntry(editor: IEditor, nodes: Node[]) {
|
||||
return new SettingTopEntry(editor, nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得合适的插入位置
|
||||
*/
|
||||
|
||||
@ -6,3 +6,4 @@ export * from './hovering';
|
||||
export * from './location';
|
||||
export * from './offset-observer';
|
||||
export * from './scroller';
|
||||
export * from './setting';
|
||||
|
||||
3
packages/designer/src/designer/setting/index.ts
Normal file
3
packages/designer/src/designer/setting/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './setting-field';
|
||||
export * from './setting-top-entry';
|
||||
export * from './setting-entry';
|
||||
17
packages/designer/src/designer/setting/setting-entry.ts
Normal file
17
packages/designer/src/designer/setting/setting-entry.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { SettingTarget } from '@ali/lowcode-globals';
|
||||
import { ComponentMeta } from '../../component-meta';
|
||||
import { Designer } from '../designer';
|
||||
import { Node } from '../../document';
|
||||
|
||||
export interface SettingEntry extends SettingTarget {
|
||||
readonly nodes: Node[];
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
readonly designer: Designer;
|
||||
|
||||
// 顶端
|
||||
readonly top: SettingEntry;
|
||||
// 父级
|
||||
readonly parent: SettingEntry;
|
||||
|
||||
get(propName: string | number): SettingEntry;
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { TitleContent, computed, isDynamicSetter, SetterType, DynamicSetter, FieldExtraProps, FieldConfig, CustomView, isCustomView, obx } from '@ali/lowcode-globals';
|
||||
import { Transducer } from '../utils';
|
||||
import { SettingPropEntry } from './setting-entry';
|
||||
import { SettingTarget } from './setting-target';
|
||||
import { Transducer } from './utils';
|
||||
import { SettingPropEntry } from './setting-prop-entry';
|
||||
import { SettingEntry } from './setting-entry';
|
||||
|
||||
export class SettingField extends SettingPropEntry implements SettingTarget {
|
||||
export class SettingField extends SettingPropEntry implements SettingEntry {
|
||||
readonly isSettingField = true;
|
||||
readonly isRequired: boolean;
|
||||
readonly transducer: Transducer;
|
||||
@ -35,7 +35,7 @@ export class SettingField extends SettingPropEntry implements SettingTarget {
|
||||
this._expanded = value;
|
||||
}
|
||||
|
||||
constructor(readonly parent: SettingTarget, config: FieldConfig) {
|
||||
constructor(readonly parent: SettingEntry, config: FieldConfig) {
|
||||
super(parent, config.name, config.type);
|
||||
|
||||
const { title, items, setter, extraProps, ...rest } = config;
|
||||
@ -53,6 +53,7 @@ export class SettingField extends SettingPropEntry implements SettingTarget {
|
||||
this.initItems(items);
|
||||
}
|
||||
|
||||
// compatiable old config
|
||||
this.transducer = new Transducer(this, { setter });
|
||||
}
|
||||
|
||||
192
packages/designer/src/designer/setting/setting-prop-entry.ts
Normal file
192
packages/designer/src/designer/setting/setting-prop-entry.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { obx, uniqueId, computed, IEditor } from '@ali/lowcode-globals';
|
||||
import { SettingEntry } from './setting-entry';
|
||||
import { Node } from '../../document';
|
||||
import { ComponentMeta } from '../../component-meta';
|
||||
import { Designer } from '../designer';
|
||||
|
||||
export class SettingPropEntry implements SettingEntry {
|
||||
// === static properties ===
|
||||
readonly editor: IEditor;
|
||||
readonly isSameComponent: boolean;
|
||||
readonly isMultiple: boolean;
|
||||
readonly isSingle: boolean;
|
||||
readonly nodes: Node[];
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
readonly designer: Designer;
|
||||
readonly top: SettingEntry;
|
||||
readonly isGroup: boolean;
|
||||
readonly type: 'field' | 'group';
|
||||
readonly id = uniqueId('entry');
|
||||
|
||||
// ==== dynamic properties ====
|
||||
@obx.ref private _name: string | number;
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
@computed get path() {
|
||||
const path = this.parent.path.slice();
|
||||
if (this.type === 'field') {
|
||||
path.push(this.name);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
extraProps: any = {};
|
||||
|
||||
constructor(readonly parent: SettingEntry, name: string | number, type?: 'field' | 'group') {
|
||||
if (type == null) {
|
||||
const c = typeof name === 'string' ? name.substr(0, 1) : '';
|
||||
if (c === '#') {
|
||||
this.type = 'group';
|
||||
} else {
|
||||
this.type = 'field';
|
||||
}
|
||||
} else {
|
||||
this.type = type;
|
||||
}
|
||||
// initial self properties
|
||||
this._name = name;
|
||||
this.isGroup = this.type === 'group';
|
||||
|
||||
// copy parent static properties
|
||||
this.editor = parent.editor;
|
||||
this.nodes = parent.nodes;
|
||||
this.componentMeta = parent.componentMeta;
|
||||
this.isSameComponent = parent.isSameComponent;
|
||||
this.isMultiple = parent.isMultiple;
|
||||
this.isSingle = parent.isSingle;
|
||||
this.designer = parent.designer;
|
||||
this.top = parent.top;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
setKey(key: string | number) {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName, true)!.key = key;
|
||||
}
|
||||
this._name = key;
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName)?.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 当前属性读写 =====
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
@computed getValue(): any {
|
||||
let val: any = null;
|
||||
if (this.type === 'field') {
|
||||
val = this.parent.getPropValue(this.name);
|
||||
}
|
||||
const { getValue } = this.extraProps;
|
||||
return getValue ? getValue(this, val) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
if (this.type === 'field') {
|
||||
this.parent.setPropValue(this.name, val);
|
||||
}
|
||||
const { setValue } = this.extraProps;
|
||||
if (setValue) {
|
||||
setValue(this, val);
|
||||
}
|
||||
// TODO: emit value change
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子项
|
||||
*/
|
||||
get(propName: string | number) {
|
||||
const path = this.path.concat(propName).join('.');
|
||||
return this.top.get(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string | number, value: any) {
|
||||
const path = this.path.concat(propName).join('.');
|
||||
this.top.setPropValue(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string | number): any {
|
||||
return this.top.getPropValue(this.path.concat(propName).join('.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶层附属属性值
|
||||
*/
|
||||
getExtraPropValue(propName: string) {
|
||||
return this.top.getExtraPropValue(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶层附属属性值
|
||||
*/
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.top.setExtraPropValue(propName, value);
|
||||
}
|
||||
|
||||
// ======= compatibles for vision ======
|
||||
getNode() {
|
||||
return this.top;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.path.join('.');
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.top;
|
||||
}
|
||||
|
||||
onValueChange() {
|
||||
// TODO:
|
||||
return () => {};
|
||||
}
|
||||
|
||||
getDefaultValue() {
|
||||
return this.extraProps.defaultValue;
|
||||
}
|
||||
|
||||
isIgnore() {
|
||||
return false;
|
||||
}
|
||||
/*
|
||||
getConfig<K extends keyof IPropConfig>(configName?: K): IPropConfig[K] | IPropConfig {
|
||||
if (configName) {
|
||||
return this.config[configName];
|
||||
}
|
||||
|
||||
return this.config;
|
||||
}
|
||||
*/
|
||||
}
|
||||
218
packages/designer/src/designer/setting/setting-top-entry.ts
Normal file
218
packages/designer/src/designer/setting/setting-top-entry.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { CustomView, computed, isCustomView, IEditor } from '@ali/lowcode-globals';
|
||||
import { SettingEntry } from './setting-entry';
|
||||
import { SettingField } from './setting-field';
|
||||
import { SettingPropEntry } from './setting-prop-entry';
|
||||
import { Node } from '../../document';
|
||||
import { ComponentMeta } from '../../component-meta';
|
||||
import { Designer } from '../designer';
|
||||
|
||||
function generateSessionId(nodes: Node[]) {
|
||||
return nodes
|
||||
.map((node) => node.id)
|
||||
.sort()
|
||||
.join(',');
|
||||
}
|
||||
|
||||
export class SettingTopEntry implements SettingEntry {
|
||||
private emitter = new EventEmitter();
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private _componentMeta: ComponentMeta | null = null;
|
||||
private _isSame: boolean = true;
|
||||
readonly path = [];
|
||||
readonly top = this;
|
||||
readonly parent = this;
|
||||
|
||||
get componentMeta() {
|
||||
return this._componentMeta;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同样的
|
||||
*/
|
||||
get isSameComponent(): boolean {
|
||||
return this._isSame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
get isSingle(): boolean {
|
||||
return this.nodes.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
get isMultiple(): boolean {
|
||||
return this.nodes.length > 1;
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
readonly first: Node;
|
||||
readonly designer: Designer;
|
||||
|
||||
constructor(readonly editor: IEditor, readonly nodes: Node[]) {
|
||||
if (nodes.length < 1) {
|
||||
throw new ReferenceError('nodes should not be empty');
|
||||
}
|
||||
this.id = generateSessionId(nodes);
|
||||
this.first = nodes[0];
|
||||
this.designer = this.first.document.designer;
|
||||
|
||||
// setups
|
||||
this.setupComponentMeta();
|
||||
|
||||
// clear fields
|
||||
this.setupItems();
|
||||
}
|
||||
|
||||
private setupComponentMeta() {
|
||||
// todo: enhance compile a temp configure.compiled
|
||||
const first = this.first;
|
||||
const meta = first.componentMeta;
|
||||
const l = this.nodes.length;
|
||||
let theSame = true;
|
||||
for (let i = 1; i < l; i++) {
|
||||
const other = this.nodes[i];
|
||||
if (other.componentMeta !== meta) {
|
||||
theSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (theSame) {
|
||||
this._isSame = true;
|
||||
this._componentMeta = meta;
|
||||
} else {
|
||||
this._isSame = false;
|
||||
this._componentMeta = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupItems() {
|
||||
if (this.componentMeta) {
|
||||
this._items = this.componentMeta.configure.map((item) => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item as any);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
@computed getValue(): any {
|
||||
this.first.propsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
this.setProps(val);
|
||||
// TODO: emit value change
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子项
|
||||
*/
|
||||
get(propName: string | number): SettingPropEntry {
|
||||
return new SettingPropEntry(this, propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.setPropValue(propName, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string): any {
|
||||
return this.first.getProp(propName, true)?.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶层附属属性值
|
||||
*/
|
||||
getExtraPropValue(propName: string) {
|
||||
return this.first.getExtraProp(propName, false)?.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶层附属属性值
|
||||
*/
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.getExtraProp(propName, true)?.setValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,替换原有值
|
||||
setProps(data: object) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.setProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,和原有值合并
|
||||
mergeProps(data: object) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.mergeProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach((item) => isPurgeable(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeItems();
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
|
||||
// ==== compatibles for vision =====
|
||||
getProp(propName: string | number) {
|
||||
return this.get(propName);
|
||||
}
|
||||
|
||||
// ==== copy some Node api =====
|
||||
// `VE.Node.getProps`
|
||||
getStatus() {
|
||||
|
||||
}
|
||||
setStatus() {
|
||||
|
||||
}
|
||||
getChildren() {
|
||||
// this.nodes.map()
|
||||
}
|
||||
getDOMNode() {
|
||||
|
||||
}
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
getPage() {
|
||||
return this.first.document;
|
||||
}
|
||||
}
|
||||
|
||||
interface Purgeable {
|
||||
purge(): void;
|
||||
}
|
||||
function isPurgeable(obj: any): obj is Purgeable {
|
||||
return obj && obj.purge;
|
||||
}
|
||||
41
packages/designer/src/designer/setting/utils.js
Normal file
41
packages/designer/src/designer/setting/utils.js
Normal file
@ -0,0 +1,41 @@
|
||||
function getHotterFromSetter(setter) {
|
||||
return setter && (setter.Hotter || (setter.type && setter.type.Hotter)) || []; // eslint-disable-line
|
||||
}
|
||||
|
||||
function getTransducerFromSetter(setter) {
|
||||
return setter && (
|
||||
setter.transducer || setter.Transducer
|
||||
|| (setter.type && (setter.type.transducer || setter.type.Transducer))
|
||||
) || null; // eslint-disable-line
|
||||
}
|
||||
|
||||
function combineTransducer(transducer, arr, context) {
|
||||
if (!transducer && Array.isArray(arr)) {
|
||||
const [toHot, toNative] = arr;
|
||||
transducer = { toHot, toNative };
|
||||
}
|
||||
|
||||
return {
|
||||
toHot: (transducer && transducer.toHot || (x => x)).bind(context), // eslint-disable-line
|
||||
toNative: (transducer && transducer.toNative || (x => x)).bind(context), // eslint-disable-line
|
||||
};
|
||||
}
|
||||
|
||||
export class Transducer {
|
||||
constructor(context, config) {
|
||||
this.setterTransducer = combineTransducer(
|
||||
getTransducerFromSetter(config.setter),
|
||||
getHotterFromSetter(config.setter),
|
||||
context,
|
||||
);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
toHot(data) {
|
||||
return this.setterTransducer.toHot(data);
|
||||
}
|
||||
|
||||
toNative(data) {
|
||||
return this.setterTransducer.toNative(data);
|
||||
}
|
||||
}
|
||||
@ -142,18 +142,24 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
|
||||
children: isDOMText(children) || isJSExpression(children) ? children : '',
|
||||
});
|
||||
} else {
|
||||
_props = new Props(this, this.buildProps(props), extras);
|
||||
// run initialChildren
|
||||
this._children = new NodeChildren(this as ParentalNode, children || []);
|
||||
this._children.interalInitParent();
|
||||
_props = new Props(this, this.upgradeProps(props), extras);
|
||||
}
|
||||
this.props = _props;
|
||||
}
|
||||
|
||||
private buildProps(props: any): any {
|
||||
private upgradeProps(props: any): any {
|
||||
// TODO: run componentMeta(initials|initialValue|accessor)
|
||||
// run transform
|
||||
return props;
|
||||
}
|
||||
|
||||
private transformOut() {
|
||||
|
||||
}
|
||||
|
||||
isContainer(): boolean {
|
||||
return this.isParental() && this.componentMeta.isContainer;
|
||||
}
|
||||
@ -525,7 +531,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
|
||||
this.document.internalRemoveAndPurgeNode(this);
|
||||
}
|
||||
|
||||
// ======= compatibles ====
|
||||
// ======= compatible apis ====
|
||||
isEmpty(): boolean {
|
||||
return this.children ? this.children.isEmpty() : true;
|
||||
}
|
||||
@ -544,6 +550,12 @@ export class Node<Schema extends NodeSchema = NodeSchema> {
|
||||
getParent() {
|
||||
return this.parent;
|
||||
}
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
getNode() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
|
||||
33
packages/globals/src/di/editor.ts
Normal file
33
packages/globals/src/di/editor.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { RegisterOptions } from 'power-di';
|
||||
|
||||
export type KeyType = Function | symbol | string;
|
||||
export type ClassType = Function | (new (...args: any[]) => any);
|
||||
export interface GetOptions {
|
||||
forceNew?: boolean;
|
||||
sourceCls?: ClassType;
|
||||
}
|
||||
export type GetReturnType<T, ClsType> = T extends undefined
|
||||
? ClsType extends {
|
||||
prototype: infer R;
|
||||
}
|
||||
? R
|
||||
: any
|
||||
: T;
|
||||
|
||||
export interface IEditor extends EventEmitter {
|
||||
get<T = undefined, KeyOrType = any>(keyOrType: KeyOrType, opt?: GetOptions): GetReturnType<T, KeyOrType> | undefined;
|
||||
|
||||
has(keyOrType: KeyType): boolean;
|
||||
|
||||
set(key: KeyType, data: any): void;
|
||||
|
||||
onceGot<T = undefined, KeyOrType extends KeyType = any>(keyOrType: KeyOrType): Promise<GetReturnType<T, KeyOrType>>;
|
||||
|
||||
onGot<T = undefined, KeyOrType extends KeyType = any>(
|
||||
keyOrType: KeyOrType,
|
||||
fn: (data: GetReturnType<T, KeyOrType>) => void,
|
||||
): () => void;
|
||||
|
||||
register(data: any, key?: KeyType, options?: RegisterOptions): void;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './setter';
|
||||
export * from './transducer';
|
||||
export * from './ioc-context';
|
||||
export * from './editor';
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { TitleContent } from './title';
|
||||
import { SetterType, DynamicSetter } from './setter-config';
|
||||
import { SettingTarget } from './setting-target';
|
||||
|
||||
|
||||
export interface FieldExtraProps {
|
||||
@ -11,21 +12,21 @@ export interface FieldExtraProps {
|
||||
* default value of target prop for setter use
|
||||
*/
|
||||
defaultValue?: any;
|
||||
getValue?: (field: any, fieldValue: any) => any;
|
||||
setValue?: (field: any, value: any) => void;
|
||||
getValue?: (target: SettingTarget, fieldValue: any) => any;
|
||||
setValue?: (target: SettingTarget, value: any) => void;
|
||||
/**
|
||||
* the field conditional show, is not set always true
|
||||
* @default undefined
|
||||
*/
|
||||
condition?: (field: any) => boolean;
|
||||
condition?: (target: SettingTarget) => boolean;
|
||||
/**
|
||||
* autorun when something change
|
||||
*/
|
||||
autorun?: (field: any) => void;
|
||||
autorun?: (target: SettingTarget) => void;
|
||||
/**
|
||||
* is this field is a virtual field that not save to schema
|
||||
*/
|
||||
virtual?: (field: any) => boolean;
|
||||
virtual?: (target: SettingTarget) => boolean;
|
||||
/**
|
||||
* default collapsed when display accordion
|
||||
*/
|
||||
|
||||
@ -11,3 +11,4 @@ export * from './title';
|
||||
export * from './utils';
|
||||
export * from './value-type';
|
||||
export * from './setter-config';
|
||||
export * from './setting-target';
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { isReactComponent } from '../utils';
|
||||
import { ComponentType, ReactElement, isValidElement } from 'react';
|
||||
import { TitleContent } from './title';
|
||||
import { SettingTarget } from './setting-target';
|
||||
|
||||
export type CustomView = ReactElement | ComponentType<any>;
|
||||
|
||||
export type DynamicProps = (field: any) => object;
|
||||
export type DynamicSetter = (field: any) => string | SetterConfig | CustomView;
|
||||
export type DynamicProps = (target: SettingTarget) => object;
|
||||
export type DynamicSetter = (target: SettingTarget) => string | SetterConfig | CustomView;
|
||||
|
||||
export interface SetterConfig {
|
||||
/**
|
||||
@ -18,11 +19,11 @@ export interface SetterConfig {
|
||||
props?: object | DynamicProps;
|
||||
children?: any;
|
||||
isRequired?: boolean;
|
||||
initialValue?: any | ((field: any) => any);
|
||||
initialValue?: any | ((target: SettingTarget) => any);
|
||||
/* for MixedSetter */
|
||||
title?: TitleContent;
|
||||
// for MixedSetter check this is available
|
||||
condition?: (field: any) => boolean;
|
||||
condition?: (target: SettingTarget) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import { ComponentMeta, Designer, Node } from '@ali/lowcode-designer';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { IEditor } from '../di';
|
||||
|
||||
export interface SettingTarget {
|
||||
|
||||
readonly nodes: Node[];
|
||||
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
|
||||
/**
|
||||
* 同样类型的节点
|
||||
*/
|
||||
@ -15,23 +9,26 @@ export interface SettingTarget {
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
readonly isOneNode: boolean;
|
||||
readonly isSingle: boolean;
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
readonly isMultiNodes: boolean;
|
||||
readonly isMultiple: boolean;
|
||||
|
||||
/**
|
||||
* 编辑器引用
|
||||
*/
|
||||
readonly editor: Editor;
|
||||
|
||||
readonly designer: Designer;
|
||||
readonly editor: IEditor;
|
||||
|
||||
/**
|
||||
* 访问路径
|
||||
*/
|
||||
readonly path: Array<string| number>;
|
||||
|
||||
// 顶端对应 Props
|
||||
/**
|
||||
* 顶端
|
||||
*/
|
||||
readonly top: SettingTarget;
|
||||
|
||||
// 父级
|
||||
@ -53,15 +50,6 @@ export interface SettingTarget {
|
||||
// 设置子项属性值
|
||||
setPropValue(propName: string | number, value: any): void;
|
||||
|
||||
// 取得兄弟项
|
||||
getSibling(propName: string | number): SettingTarget | null;
|
||||
|
||||
// 取得兄弟属性值
|
||||
getSiblingValue(propName: string | number): any;
|
||||
|
||||
// 设置兄弟属性值
|
||||
setSiblingValue(propName: string | number, value: any): void;
|
||||
|
||||
// 获取顶层附属属性值
|
||||
getExtraPropValue(propName: string): any;
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
import { Tab, Breadcrumb } from '@alifd/next';
|
||||
import { Title, createIcon, observer } from '@ali/lowcode-globals';
|
||||
import { Node } from '@ali/lowcode-designer';
|
||||
import { Node, isSettingField, SettingField } from '@ali/lowcode-designer';
|
||||
import { Pane as OutlinePane } from '@ali/lowcode-plugin-outline-pane';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { SettingsMain } from './main';
|
||||
import SettingsPane from './settings-pane';
|
||||
import { isSettingField, SettingField } from './setting-field';
|
||||
|
||||
@observer
|
||||
export default class SettingsMainView extends Component<{ editor: Editor }> {
|
||||
@ -25,7 +24,7 @@ export default class SettingsMainView extends Component<{ editor: Editor }> {
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
if (settings.isMultiNodes) {
|
||||
if (settings.isMultiple) {
|
||||
return (
|
||||
<div className="lc-settings-navigator">
|
||||
{createIcon(settings.componentMeta?.icon, { className: 'lc-settings-navigator-icon'})}
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { obx, computed } from '@ali/lowcode-globals';
|
||||
import { Node, Designer, Selection } from '@ali/lowcode-designer';
|
||||
import { Node, Designer, Selection, SettingTopEntry } from '@ali/lowcode-designer';
|
||||
import { getTreeMaster } from '@ali/lowcode-plugin-outline-pane';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { SettingTopEntry, generateSessionId } from './setting-entry';
|
||||
|
||||
function generateSessionId(nodes: Node[]) {
|
||||
return nodes
|
||||
.map((node) => node.id)
|
||||
.sort()
|
||||
.join(',');
|
||||
}
|
||||
|
||||
export class SettingsMain {
|
||||
private emitter = new EventEmitter();
|
||||
@ -24,7 +30,13 @@ export class SettingsMain {
|
||||
|
||||
private disposeListener: () => void;
|
||||
|
||||
private designer?: Designer;
|
||||
|
||||
constructor(readonly editor: Editor) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
const setupSelection = (selection?: Selection) => {
|
||||
if (selection) {
|
||||
this.setup(selection.getNodes());
|
||||
@ -32,17 +44,16 @@ export class SettingsMain {
|
||||
this.setup([]);
|
||||
}
|
||||
};
|
||||
editor.on('designer.selection.change', setupSelection);
|
||||
this.editor.on('designer.selection.change', setupSelection);
|
||||
this.disposeListener = () => {
|
||||
editor.removeListener('designer.selection.change', setupSelection);
|
||||
this.editor.removeListener('designer.selection.change', setupSelection);
|
||||
};
|
||||
(async () => {
|
||||
const designer = await editor.onceGot(Designer);
|
||||
getTreeMaster(designer).onceEnableBuiltin(() => {
|
||||
this.emitter.emit('outline-visible');
|
||||
});
|
||||
setupSelection(designer.currentSelection);
|
||||
})();
|
||||
const designer = await this.editor.onceGot(Designer);
|
||||
this.designer = designer;
|
||||
getTreeMaster(designer).onceEnableBuiltin(() => {
|
||||
this.emitter.emit('outline-visible');
|
||||
});
|
||||
setupSelection(designer.currentSelection);
|
||||
}
|
||||
|
||||
private setup(nodes: Node[]) {
|
||||
@ -57,7 +68,11 @@ export class SettingsMain {
|
||||
return;
|
||||
}
|
||||
|
||||
this._settings = new SettingTopEntry(this.editor, nodes);
|
||||
if (!this.designer) {
|
||||
this.designer = nodes[0].document.designer;
|
||||
}
|
||||
|
||||
this._settings = this.designer.createSettingEntry(this.editor, nodes);
|
||||
}
|
||||
|
||||
onceOutlineVisible(fn: () => void): () => void {
|
||||
|
||||
@ -1,436 +0,0 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Node, ComponentMeta, Designer } from '@ali/lowcode-designer';
|
||||
import { CustomView, obx, uniqueId, computed, isCustomView } from '@ali/lowcode-globals';
|
||||
import Editor from '@ali/lowcode-editor-core';
|
||||
import { SettingTarget } from './setting-target';
|
||||
import { SettingField } from './setting-field';
|
||||
|
||||
export function generateSessionId(nodes: Node[]) {
|
||||
return nodes
|
||||
.map((node) => node.id)
|
||||
.sort()
|
||||
.join(',');
|
||||
}
|
||||
|
||||
export class SettingTopEntry implements SettingTarget {
|
||||
private emitter = new EventEmitter();
|
||||
private _items: Array<SettingField | CustomView> = [];
|
||||
private _componentMeta: ComponentMeta | null = null;
|
||||
private _isSame: boolean = true;
|
||||
readonly path = [];
|
||||
readonly top = this;
|
||||
readonly parent = this;
|
||||
|
||||
get componentMeta() {
|
||||
return this._componentMeta;
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同样的
|
||||
*/
|
||||
get isSameComponent(): boolean {
|
||||
return this._isSame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个
|
||||
*/
|
||||
get isOneNode(): boolean {
|
||||
return this.nodes.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个
|
||||
*/
|
||||
get isMultiNodes(): boolean {
|
||||
return this.nodes.length > 1;
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
readonly first: Node;
|
||||
readonly designer: Designer;
|
||||
|
||||
constructor(readonly editor: Editor, readonly nodes: Node[], ) {
|
||||
if (nodes.length < 1) {
|
||||
throw new ReferenceError('nodes should not be empty');
|
||||
}
|
||||
this.id = generateSessionId(nodes);
|
||||
this.first = nodes[0];
|
||||
this.designer = this.first.document.designer;
|
||||
|
||||
// setups
|
||||
this.setupComponentMeta();
|
||||
|
||||
// clear fields
|
||||
this.setupItems();
|
||||
}
|
||||
|
||||
private setupComponentMeta() {
|
||||
// todo: enhance compile a temp configure.compiled
|
||||
const first = this.first;
|
||||
const meta = first.componentMeta;
|
||||
const l = this.nodes.length;
|
||||
let theSame = true;
|
||||
for (let i = 1; i < l; i++) {
|
||||
const other = this.nodes[i];
|
||||
if (other.componentMeta !== meta) {
|
||||
theSame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (theSame) {
|
||||
this._isSame = true;
|
||||
this._componentMeta = meta;
|
||||
} else {
|
||||
this._isSame = false;
|
||||
this._componentMeta = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupItems() {
|
||||
if (this.componentMeta) {
|
||||
this._items = this.componentMeta.configure.map((item) => {
|
||||
if (isCustomView(item)) {
|
||||
return item;
|
||||
}
|
||||
return new SettingField(this, item as any);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
@computed getValue(): any {
|
||||
this.first.propsData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
this.setProps(val);
|
||||
// TODO: emit value change
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子项
|
||||
*/
|
||||
get(propName: string | number): SettingPropEntry {
|
||||
return new SettingPropEntry(this, propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.setPropValue(propName, value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string): any {
|
||||
return this.first.getProp(propName, true)?.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兄弟项
|
||||
*/
|
||||
getSibling(propName: string | number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得兄弟属性值
|
||||
*/
|
||||
getSiblingValue(propName: string | number): any {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置兄弟属性值
|
||||
*/
|
||||
setSiblingValue(propName: string | number, value: any): void {
|
||||
// noop
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶层附属属性值
|
||||
*/
|
||||
getExtraPropValue(propName: string) {
|
||||
return this.first.getExtraProp(propName, false)?.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶层附属属性值
|
||||
*/
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.getExtraProp(propName, true)?.setValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,替换原有值
|
||||
setProps(data: object) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.setProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
// 设置多个属性值,和原有值合并
|
||||
mergeProps(data: object) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.mergeProps(data as any);
|
||||
});
|
||||
}
|
||||
|
||||
private disposeItems() {
|
||||
this._items.forEach((item) => isPurgeable(item) && item.purge());
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
purge() {
|
||||
this.disposeItems();
|
||||
this.emitter.removeAllListeners();
|
||||
}
|
||||
|
||||
// ==== compatibles for vision =====
|
||||
getProp(propName: string | number) {
|
||||
return this.get(propName);
|
||||
}
|
||||
}
|
||||
|
||||
export interface Purgeable {
|
||||
purge(): void;
|
||||
}
|
||||
export function isPurgeable(obj: any): obj is Purgeable {
|
||||
return obj && obj.purge;
|
||||
}
|
||||
|
||||
|
||||
export class SettingPropEntry implements SettingTarget {
|
||||
// === static properties ===
|
||||
readonly editor: Editor;
|
||||
readonly isSameComponent: boolean;
|
||||
readonly isMultiNodes: boolean;
|
||||
readonly isOneNode: boolean;
|
||||
readonly nodes: Node[];
|
||||
readonly componentMeta: ComponentMeta | null;
|
||||
readonly designer: Designer;
|
||||
readonly top: SettingTarget;
|
||||
readonly isGroup: boolean;
|
||||
readonly type: 'field' | 'group';
|
||||
readonly id = uniqueId('entry');
|
||||
|
||||
// ==== dynamic properties ====
|
||||
@obx.ref private _name: string | number;
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
@computed get path() {
|
||||
const path = this.parent.path.slice();
|
||||
if (this.type === 'field') {
|
||||
path.push(this.name);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
extraProps: any = {};
|
||||
|
||||
constructor(readonly parent: SettingTarget, name: string | number, type?: 'field' | 'group') {
|
||||
if (type == null) {
|
||||
const c = typeof name === 'string' ? name.substr(0, 1) : '';
|
||||
if (c === '#') {
|
||||
this.type = 'group';
|
||||
} else {
|
||||
this.type = 'field';
|
||||
}
|
||||
} else {
|
||||
this.type = type;
|
||||
}
|
||||
// initial self properties
|
||||
this._name = name;
|
||||
this.isGroup = this.type === 'group';
|
||||
|
||||
// copy parent static properties
|
||||
this.editor = parent.editor;
|
||||
this.nodes = parent.nodes;
|
||||
this.componentMeta = parent.componentMeta;
|
||||
this.isSameComponent = parent.isSameComponent;
|
||||
this.isMultiNodes = parent.isMultiNodes;
|
||||
this.isOneNode = parent.isOneNode;
|
||||
this.designer = parent.designer;
|
||||
this.top = parent.top;
|
||||
}
|
||||
|
||||
setKey(key: string | number) {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName, true)!.key = key;
|
||||
}
|
||||
this._name = key;
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.type !== 'field') {
|
||||
return;
|
||||
}
|
||||
const propName = this.path.join('.');
|
||||
let l = this.nodes.length;
|
||||
while (l-- > 1) {
|
||||
this.nodes[l].getProp(propName)?.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 当前属性读写 =====
|
||||
|
||||
/**
|
||||
* 获取当前属性值
|
||||
*/
|
||||
@computed getValue(): any {
|
||||
let val: any = null;
|
||||
if (this.type === 'field') {
|
||||
val = this.parent.getPropValue(this.name);
|
||||
}
|
||||
const { getValue } = this.extraProps;
|
||||
return getValue ? getValue(this, val) : val;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前属性值
|
||||
*/
|
||||
setValue(val: any) {
|
||||
if (this.type === 'field') {
|
||||
this.parent.setPropValue(this.name, val);
|
||||
}
|
||||
const { setValue } = this.extraProps;
|
||||
if (setValue) {
|
||||
setValue(this, val);
|
||||
}
|
||||
// TODO: emit value change
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子项
|
||||
*/
|
||||
get(propName: string | number) {
|
||||
const path = this.path.concat(propName).join('.');
|
||||
return this.top.get(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置子级属性值
|
||||
*/
|
||||
setPropValue(propName: string | number, value: any) {
|
||||
const path = this.path.concat(propName).join('.');
|
||||
this.top.setPropValue(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子级属性值
|
||||
*/
|
||||
getPropValue(propName: string | number): any {
|
||||
return this.top.getPropValue(this.path.concat(propName).join('.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兄弟项
|
||||
*/
|
||||
getSibling(propName: string | number) {
|
||||
return this.parent.get(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得兄弟属性值
|
||||
*/
|
||||
getSiblingValue(propName: string | number): any {
|
||||
return this.parent.getPropValue(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置兄弟属性值
|
||||
*/
|
||||
setSiblingValue(propName: string | number, value: any): void {
|
||||
this.parent.setPropValue(propName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取顶层附属属性值
|
||||
*/
|
||||
getExtraPropValue(propName: string) {
|
||||
return this.top.getExtraPropValue(propName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶层附属属性值
|
||||
*/
|
||||
setExtraPropValue(propName: string, value: any) {
|
||||
this.top.setExtraPropValue(propName, value);
|
||||
}
|
||||
|
||||
// ======= compatibles for vision ======
|
||||
getNode() {
|
||||
return this.nodes[0];
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.top;
|
||||
}
|
||||
|
||||
onValueChange() {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.path.join('.');
|
||||
}
|
||||
|
||||
getKey() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getDefaultValue() {
|
||||
return this.extraProps.defaultValue;
|
||||
}
|
||||
/*
|
||||
getConfig<K extends keyof IPropConfig>(configName?: K): IPropConfig[K] | IPropConfig {
|
||||
if (configName) {
|
||||
return this.config[configName];
|
||||
}
|
||||
|
||||
return this.config;
|
||||
}
|
||||
*/
|
||||
/*
|
||||
isHidden() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isDisabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSetter() {
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
isIgnore() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -10,9 +10,7 @@ import {
|
||||
} from '@ali/lowcode-globals';
|
||||
import { Field, createField } from '../field';
|
||||
import PopupService from '../popup';
|
||||
import { SettingField, isSettingField } from './setting-field';
|
||||
import { SettingTarget } from './setting-target';
|
||||
import { SettingTopEntry } from './setting-entry';
|
||||
import { SettingField, isSettingField, SettingTopEntry, SettingEntry } from '@ali/lowcode-designer';
|
||||
|
||||
@observer
|
||||
class SettingFieldView extends Component<{ field: SettingField }> {
|
||||
@ -20,7 +18,7 @@ class SettingFieldView extends Component<{ field: SettingField }> {
|
||||
const { field } = this.props;
|
||||
const { extraProps } = field;
|
||||
const { condition, defaultValue } = extraProps;
|
||||
const visible = field.isOneNode && typeof condition === 'function' ? condition(field) !== false : true;
|
||||
const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
@ -99,7 +97,7 @@ class SettingGroupView extends Component<{ field: SettingField }> {
|
||||
const { field } = this.props;
|
||||
const { extraProps } = field;
|
||||
const { condition } = extraProps;
|
||||
const visible = field.isOneNode && typeof condition === 'function' ? condition(field) !== false : true;
|
||||
const visible = field.isSingle && typeof condition === 'function' ? condition(field) !== false : true;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
@ -116,7 +114,7 @@ class SettingGroupView extends Component<{ field: SettingField }> {
|
||||
}
|
||||
}
|
||||
|
||||
export function createSettingFieldView(item: SettingField | CustomView, field: SettingTarget, index?: number) {
|
||||
export function createSettingFieldView(item: SettingField | CustomView, field: SettingEntry, index?: number) {
|
||||
if (isSettingField(item)) {
|
||||
if (item.isGroup) {
|
||||
return <SettingGroupView field={item} key={item.id} />;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ComponentType, ReactElement, isValidElement, ComponentClass } from 'react';
|
||||
import { isI18nData } from '@ali/lowcode-globals';
|
||||
import { isI18nData, SettingTarget } from '@ali/lowcode-globals';
|
||||
|
||||
type Field = any;
|
||||
type Field = SettingTarget;
|
||||
|
||||
export enum DISPLAY_TYPE {
|
||||
NONE = 'none', // => condition'plain'
|
||||
@ -282,7 +282,6 @@ export function upgradePropConfig(config: OldPropConfig) {
|
||||
componentName: 'SlotSetter',
|
||||
initialValue: () => ({
|
||||
type: 'JSSlot',
|
||||
// params:
|
||||
value: initialChildren
|
||||
}),
|
||||
}
|
||||
@ -315,14 +314,14 @@ export function upgradePropConfig(config: OldPropConfig) {
|
||||
initialFn
|
||||
}
|
||||
}
|
||||
extraProps.initialValue = (field: Field, defaultValue?: any) => {
|
||||
extraProps.initialValue = (field: Field, currentValue: any, defaultValue?: any) => {
|
||||
if (defaultValue === undefined) {
|
||||
defaultValue = extraProps.defaultValue;
|
||||
}
|
||||
|
||||
if (typeof initialFn === 'function') {
|
||||
// ?
|
||||
return initialFn(null, defaultValue);
|
||||
return initialFn.call(field, currentValue, defaultValue);
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user