feat: upgrade formily (#1880)

* feat: upgrade formily

* fix: upgrade @formily/json-schema

* fix: optimizing performance

* fix: performance code

* refactor: `React.memo` and `observer` component add `displayName` property

* fix: add cache to Schema.compile

* feat: 优化代码

* fix: 修复卡顿最终的问题

* Update SchemaComponentProvider.tsx

* feat: 再次优化代码

* feat: optimized code

---------

Co-authored-by: dream2023 <1098626505@qq.com>
This commit is contained in:
chenos 2023-06-06 19:33:04 +08:00 committed by GitHub
parent 36d16bc015
commit e6a2a292b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
164 changed files with 6226 additions and 5660 deletions

View File

@ -42,6 +42,7 @@ const HelloProvider = React.memo((props) => {
}
return <>{props.children}</>
});
HelloProvider.displayName = 'HelloProvider'
app.use(HelloProvider);
```

View File

@ -4,27 +4,30 @@ import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import { Table, TableColumnType } from 'antd';
import React from 'react';
const ArrayTable = observer((props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const ArrayTable = observer(
(props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
},
{ displayName: 'ArrayTable' },
);
const Value = connect((props) => {
return <li>value: {props.value}</li>;

View File

@ -180,11 +180,11 @@ const Hello = observer((props) => {
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
});
}, { displayName: 'Page' });
export default () => {
return (
@ -240,7 +240,7 @@ const useDragEnd = () => {
const Page = observer((props) => {
return <DndContext onDragEnd={useDragEnd()}>{props.children}</DndContext>;
});
}, { displayName: 'Page' });
function Draggable(props) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
@ -289,7 +289,7 @@ const Block = observer((props) => {
</div>
</Droppable>
);
});
}, { displayName: 'Block' });
export default function App() {
return (

View File

@ -103,7 +103,7 @@ const SingleText = connect(Input);
const UsedObserver = observer((props) => {
const form = useForm();
return <div>UsedObserver: {form.values.t1}</div>
});
}, { displayName: 'UsedObserver' });
const NotUsedObserver = (props) => {
const form = useForm();
@ -310,7 +310,7 @@ const ArrayList = observer((props) => {
</ul>
</>
);
});
}, { displayName: 'ArrayList' });
const Value = connect((props) => {
return <li>value: {props.value}</li>
@ -367,7 +367,7 @@ const ArrayList = observer((props) => {
})}
</ul>
);
});
}, { displayName: 'ArrayList' });
const Value = connect((props) => {
return <li>value: {props.value}</li>
@ -432,7 +432,7 @@ const ArrayTable = observer((props: any) => {
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
}, { displayName: 'ArrayTable' });
const Value = connect((props) => {
return <li>value: {props.value}</li>;

View File

@ -116,11 +116,11 @@ const Hello = observer((props) => {
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
});
}, { displayName: 'Page' });
export default () => {
return (

View File

@ -182,6 +182,7 @@ const HelloProvider = React.memo((props) => {
}
return <>{props.children}</>
});
HelloProvider.displayName = 'HelloProvider'
app.use(HelloProvider);

View File

@ -4,27 +4,30 @@ import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import { Table, TableColumnType } from 'antd';
import React from 'react';
const ArrayTable = observer((props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const ArrayTable = observer(
(props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
},
{ displayName: 'ArrayTable' },
);
const Value = connect((props) => {
return <li>value: {props.value}</li>;

View File

@ -183,11 +183,11 @@ const Hello = observer((props) => {
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
});
}, { displayName: 'Page' });
export default () => {
return (
@ -243,7 +243,7 @@ const useDragEnd = () => {
const Page = observer((props) => {
return <DndContext onDragEnd={useDragEnd()}>{props.children}</DndContext>;
});
}, { displayName: 'Page' });
function Draggable(props) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
@ -292,7 +292,7 @@ const Block = observer((props) => {
</div>
</Droppable>
);
});
}, { displayName: 'Block' });
export default function App() {
return (

View File

@ -103,7 +103,7 @@ const SingleText = connect(Input);
const UsedObserver = observer((props) => {
const form = useForm();
return <div>UsedObserver: {form.values.t1}</div>
});
}, { displayName: 'UsedObserver' });
const NotUsedObserver = (props) => {
const form = useForm();
@ -312,7 +312,7 @@ const ArrayList = observer((props) => {
</ul>
</>
);
});
}, { displayName: 'ArrayList' });
const Value = connect((props) => {
return <li>value: {props.value}</li>
@ -369,7 +369,7 @@ const ArrayList = observer((props) => {
})}
</ul>
);
});
}, { displayName: 'ArrayList' });
const Value = connect((props) => {
return <li>value: {props.value}</li>
@ -434,7 +434,7 @@ const ArrayTable = observer((props: any) => {
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
}, { displayName: 'ArrayTable' });
const Value = connect((props) => {
return <li>value: {props.value}</li>;

View File

@ -116,11 +116,11 @@ const Hello = observer((props) => {
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
});
}, { displayName: 'Page' });
export default () => {
return (

View File

@ -42,6 +42,7 @@ const HelloProvider = React.memo((props) => {
}
return <>{props.children}</>
});
HelloProvider.displayName = 'HelloProvider'
app.use(HelloProvider);
```

View File

@ -4,27 +4,30 @@ import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import { Table, TableColumnType } from 'antd';
import React from 'react';
const ArrayTable = observer((props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const ArrayTable = observer(
(props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
},
{ displayName: 'ArrayTable' },
);
const Value = connect((props) => {
return <li>value: {props.value}</li>;

View File

@ -183,11 +183,11 @@ const Hello = observer((props) => {
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
});
}, { displayName: 'Page' });
export default () => {
return (
@ -243,7 +243,7 @@ const useDragEnd = () => {
const Page = observer((props) => {
return <DndContext onDragEnd={useDragEnd()}>{props.children}</DndContext>;
});
}, { displayName: 'Page' });
function Draggable(props) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
@ -292,7 +292,7 @@ const Block = observer((props) => {
</div>
</Droppable>
);
});
}, { displayName: 'Block' });
export default function App() {
return (

View File

@ -103,7 +103,7 @@ const SingleText = connect(Input);
const UsedObserver = observer((props) => {
const form = useForm();
return <div>UsedObserver: {form.values.t1}</div>
});
}, { displayName: 'UsedObserver' });
const NotUsedObserver = (props) => {
const form = useForm();
@ -312,7 +312,7 @@ const ArrayList = observer((props) => {
</ul>
</>
);
});
}, { displayName: 'ArrayList' });
const Value = connect((props) => {
return <li>value: {props.value}</li>
@ -369,7 +369,7 @@ const ArrayList = observer((props) => {
})}
</ul>
);
});
}, { displayName: 'ArrayList' });
const Value = connect((props) => {
return <li>value: {props.value}</li>
@ -434,7 +434,7 @@ const ArrayTable = observer((props: any) => {
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
}, { displayName: 'ArrayTable' });
const Value = connect((props) => {
return <li>value: {props.value}</li>;

View File

@ -116,11 +116,11 @@ const Hello = observer((props) => {
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
});
}, { displayName: 'Page' });
export default () => {
return (

View File

@ -176,6 +176,7 @@ const HelloProvider = React.memo((props) => {
}
return <>{props.children}</>
});
HelloProvider.displayName = 'HelloProvider'
app.use(HelloProvider);
```

View File

@ -1,5 +1,8 @@
import React from 'react';
export default React.memo((props) => {
const CommonMemo = React.memo((props) => {
return <>{props.children}</>;
});
CommonMemo.displayName = 'CommonMemo';
export default CommonMemo;

View File

@ -374,7 +374,7 @@ const Action: any = observer((props: any) => {
<RecursionField basePath={field.address} schema={schema} onlyRenderProperties />
</VisibleContext.Provider>
);
});
}, { displayName: 'Action' });
Action.Drawer = observer((props: any) => {
const [visible, setVisible] = useContext(VisibleContext);
@ -411,13 +411,13 @@ Action.Drawer = observer((props: any) => {
)}
</>
);
});
}, { displayName: 'Action.Drawer' });
Action.Drawer.Footer = observer((props: any) => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
});
}, { displayName: 'Action.Drawer.Footer' });
const schema: ISchema = {
type: 'object',
@ -500,7 +500,7 @@ const Hello = observer(({ name }) => {
>更新</Button>
</div>
)
});
}, { displayName: 'Hello' });
const RouteSchemaComponent = (props) => {
const route = useRoute();
@ -678,7 +678,7 @@ const useDragEnd = () => {
const Page = observer((props) => {
return <DndContext onDragEnd={useDragEnd()}>{props.children}</DndContext>;
});
}, { displayName: 'Page' });
function Draggable(props) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
@ -727,7 +727,7 @@ const Block = observer((props) => {
</div>
</Droppable>
);
});
}, { displayName: 'Block' });
export default function App() {
return (

View File

@ -10,9 +10,9 @@
"@dnd-kit/core": "^5.0.1",
"@dnd-kit/sortable": "^6.0.0",
"@emotion/css": "^11.7.1",
"@formily/antd": "2.0.20",
"@formily/core": "2.0.20",
"@formily/react": "2.0.20",
"@formily/antd": "2.2.24",
"@formily/core": "2.2.24",
"@formily/react": "2.2.24",
"@nocobase/evaluators": "0.9.4-alpha.2",
"@nocobase/sdk": "0.9.4-alpha.2",
"@nocobase/utils": "0.9.4-alpha.2",

View File

@ -61,6 +61,8 @@ const App = React.memo((props: any) => {
return <C />;
});
App.displayName = 'App';
export class Application {
providers = [];
mainComponent = null;

View File

@ -3,18 +3,21 @@ import { useDesignable } from '@nocobase/client';
import { Button } from 'antd';
import { observer } from '@formily/react';
export const Hello: React.FC<any> = observer(({ name }) => {
const { patch, remove } = useDesignable();
return (
<div>
<h1>Hello {name}!</h1>
<Button
onClick={() => {
patch('x-component-props.name', Math.random());
}}
>
</Button>
</div>
);
});
export const Hello: React.FC<any> = observer(
({ name }) => {
const { patch, remove } = useDesignable();
return (
<div>
<h1>Hello {name}!</h1>
<Button
onClick={() => {
patch('x-component-props.name', Math.random());
}}
>
</Button>
</div>
);
},
{ displayName: 'Hello' },
);

View File

@ -31,36 +31,39 @@ const useDef = (options, props) => {
}
};
export const CollectionFieldsTable: React.FC<TableVoidProps> = observer((props) => {
const { rowKey = 'id', useDataSource = useDef, useSelectedRowKeys = useDefSelectedRowKeys } = props;
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
const result = useDataSource(
{
uid: fieldSchema['x-uid'],
onSuccess(data) {
form.setValues({
[fieldSchema.name]: data?.data,
});
export const CollectionFieldsTable: React.FC<TableVoidProps> = observer(
(props) => {
const { rowKey = 'id', useDataSource = useDef, useSelectedRowKeys = useDefSelectedRowKeys } = props;
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
const result = useDataSource(
{
uid: fieldSchema['x-uid'],
onSuccess(data) {
form.setValues({
[fieldSchema.name]: data?.data,
});
},
},
},
props,
);
return (
<AsyncDataProvider value={result}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<CollectionFieldsTableArray
{...props}
rowKey={rowKey}
loading={result?.['loading']}
useSelectedRowKeys={useSelectedRowKeys}
pagination={false}
/>
</FieldContext.Provider>
</FormContext.Provider>
</AsyncDataProvider>
);
});
props,
);
return (
<AsyncDataProvider value={result}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<CollectionFieldsTableArray
{...props}
rowKey={rowKey}
loading={result?.['loading']}
useSelectedRowKeys={useSelectedRowKeys}
pagination={false}
/>
</FieldContext.Provider>
</FormContext.Provider>
</AsyncDataProvider>
);
},
{ displayName: 'CollectionFieldsTable' },
);

View File

@ -79,149 +79,150 @@ interface CategorizeDataItem {
data: Array<any>;
}
export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
const field = useField<ArrayField>();
const { name } = useRecord();
const { t } = useTranslation();
const compile = useCompile();
const { getInterface, getInheritCollections, getCollection, getCurrentCollectionFields, getInheritedFields } =
useCollectionManager();
const {
showIndex = true,
useSelectedRowKeys = useDef,
useDataSource = useDefDataSource,
onChange,
...others
} = props;
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
const [expandedKeys, setExpendedKeys] = useState(selectedRowKeys);
const inherits = getInheritCollections(name);
const currentFields = getCurrentCollectionFields(name);
useDataSource({
onSuccess(data) {
field.value = data?.data || [];
const tmpData: Array<CategorizeDataItem> = [];
const categorizeMap = new Map<CategorizeKey, any>();
const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
let fieldArr = categorizeMap.get(categorizeKey);
if (!fieldArr) {
fieldArr = [];
}
fieldArr.push(val);
categorizeMap.set(categorizeKey, fieldArr);
};
field.value.forEach((item) => {
const itemInterface = getInterface(item?.interface);
if (item?.primaryKey || item.isForeignKey) {
addCategorizeVal('primaryAndForeignKey', item);
return;
}
const group = itemInterface?.group as CategorizeKey;
switch (group) {
case 'systemInfo':
case 'relation':
addCategorizeVal(group, item);
break;
default:
addCategorizeVal('basic', item);
}
});
if (inherits) {
inherits.forEach((v) => {
sortKeyArr.push(v);
const parentCollection = getCollection(v);
parentCollection.fields.map((k) => {
if (k.interface) {
addCategorizeVal(v, new Proxy(k, {}));
field.value.push(new Proxy(k, {}));
}
});
export const CollectionFieldsTableArray: React.FC<any> = observer(
(props) => {
const sortKeyArr: Array<CategorizeKey> = ['primaryAndForeignKey', 'relation', 'basic', 'systemInfo'];
const field = useField<ArrayField>();
const { name } = useRecord();
const { t } = useTranslation();
const compile = useCompile();
const { getInterface, getInheritCollections, getCollection, getCurrentCollectionFields, getInheritedFields } =
useCollectionManager();
const {
showIndex = true,
useSelectedRowKeys = useDef,
useDataSource = useDefDataSource,
onChange,
...others
} = props;
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
const [categorizeData, setCategorizeData] = useState<Array<CategorizeDataItem>>([]);
const [expandedKeys, setExpendedKeys] = useState(selectedRowKeys);
const inherits = getInheritCollections(name);
const currentFields = getCurrentCollectionFields(name);
useDataSource({
onSuccess(data) {
field.value = data?.data || [];
const tmpData: Array<CategorizeDataItem> = [];
const categorizeMap = new Map<CategorizeKey, any>();
const addCategorizeVal = (categorizeKey: CategorizeKey, val) => {
let fieldArr = categorizeMap.get(categorizeKey);
if (!fieldArr) {
fieldArr = [];
}
fieldArr.push(val);
categorizeMap.set(categorizeKey, fieldArr);
};
field.value.forEach((item) => {
const itemInterface = getInterface(item?.interface);
if (item?.primaryKey || item.isForeignKey) {
addCategorizeVal('primaryAndForeignKey', item);
return;
}
const group = itemInterface?.group as CategorizeKey;
switch (group) {
case 'systemInfo':
case 'relation':
addCategorizeVal(group, item);
break;
default:
addCategorizeVal('basic', item);
}
});
}
sortKeyArr.forEach((key) => {
if (categorizeMap.get(key)?.length > 0) {
const parentCollection = getCollection(key);
tmpData.push({
key,
name:
t(CategorizeKeyNameMap.get(key)) ||
t(`Parent collection fields`) + `(${compile(parentCollection.title)})`,
data: categorizeMap.get(key),
if (inherits) {
inherits.forEach((v) => {
sortKeyArr.push(v);
const parentCollection = getCollection(v);
parentCollection.fields.map((k) => {
if (k.interface) {
addCategorizeVal(v, new Proxy(k, {}));
field.value.push(new Proxy(k, {}));
}
});
});
}
});
setExpendedKeys(sortKeyArr);
setCategorizeData(tmpData);
},
});
const useTableColumns = () => {
const schema = useFieldSchema();
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
const columns = schema
.reduceProperties((buf, s) => {
if (isColumnComponent(s)) {
return buf.concat([s]);
}
}, [])
.map((s: Schema) => {
return {
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
dataIndex: s.name,
key: s.name,
render: (v, record) => {
const index = findIndex(field.value, record);
return (
<RecordIndexProvider index={index}>
<RecordProvider record={record}>
<RecursionField schema={s} name={index} onlyRenderProperties />
</RecordProvider>
</RecordIndexProvider>
);
},
} as TableColumnProps<any>;
});
if (!exists) {
return columns;
}
return columns.concat({
title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER',
sortKeyArr.forEach((key) => {
if (categorizeMap.get(key)?.length > 0) {
const parentCollection = getCollection(key);
tmpData.push({
key,
name:
t(CategorizeKeyNameMap.get(key)) ||
t(`Parent collection fields`) + `(${compile(parentCollection.title)})`,
data: categorizeMap.get(key),
});
}
});
setExpendedKeys(sortKeyArr);
setCategorizeData(tmpData);
},
});
};
const ExpandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
const columns = useTableColumns();
if (!props.loading) {
if (inherits.includes(record.key)) {
columns.pop();
columns.push({
title: <RecursionField name={'column4'} schema={overridingSchema as Schema} onlyRenderSelf />,
dataIndex: 'column4',
key: 'column4',
render: (v, record) => {
const index = findIndex(field.value, record);
return (
<RecordIndexProvider index={index}>
<RecordProvider record={record}>
<SchemaComponent
scope={{ currentCollection: name }}
schema={overridingSchema as Schema}
name={index}
onlyRenderProperties
/>
</RecordProvider>
</RecordIndexProvider>
);
},
const useTableColumns = () => {
const schema = useFieldSchema();
const { exists, render } = useSchemaInitializer(schema['x-initializer']);
const columns = schema
.reduceProperties((buf, s) => {
if (isColumnComponent(s)) {
return buf.concat([s]);
}
}, [])
.map((s: Schema) => {
return {
title: <RecursionField name={s.name} schema={s} onlyRenderSelf />,
dataIndex: s.name,
key: s.name,
render: (v, record) => {
const index = findIndex(field.value, record);
return (
<RecordIndexProvider index={index}>
<RecordProvider record={record}>
<RecursionField schema={s} name={index} onlyRenderProperties />
</RecordProvider>
</RecordIndexProvider>
);
},
} as TableColumnProps<any>;
});
if (!exists) {
return columns;
}
const restProps = {
rowSelection:
props.rowSelection && !inherits.includes(record.key)
? {
return columns.concat({
title: render(),
dataIndex: 'TABLE_COLUMN_INITIALIZER',
key: 'TABLE_COLUMN_INITIALIZER',
});
};
const ExpandedRowRender = (record: CategorizeDataItem, index, indent, expanded) => {
const columns = useTableColumns();
if (!props.loading) {
if (inherits.includes(record.key)) {
columns.pop();
columns.push({
title: <RecursionField name={'column4'} schema={overridingSchema as Schema} onlyRenderSelf />,
dataIndex: 'column4',
key: 'column4',
render: (v, record) => {
const index = findIndex(field.value, record);
return (
<RecordIndexProvider index={index}>
<RecordProvider record={record}>
<SchemaComponent
scope={{ currentCollection: name }}
schema={overridingSchema as Schema}
name={index}
onlyRenderProperties
/>
</RecordProvider>
</RecordIndexProvider>
);
},
});
}
const restProps = {
rowSelection:
props.rowSelection && !inherits.includes(record.key)
? {
type: 'checkbox',
selectedRowKeys,
onChange(selectedRowKeys: any[]) {
@ -229,52 +230,54 @@ export const CollectionFieldsTableArray: React.FC<any> = observer((props) => {
},
...props.rowSelection,
}
: undefined,
};
return (
<Table
{...others}
{...restProps}
components={components}
showHeader={true}
columns={columns}
dataSource={record.data}
pagination={false}
/>
);
}
};
return (
<div
className={css`
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<Table
showHeader={false}
loading={props?.loading}
columns={groupColumns}
dataSource={categorizeData}
pagination={false}
expandable={{
expandedRowRender: ExpandedRowRender,
expandedRowKeys: expandedKeys,
}}
onExpand={(expanded, record) => {
let keys = [];
if (expanded) {
keys = expandedKeys.concat([record.key]);
} else {
keys = expandedKeys.filter((v) => {
return v !== record.key;
});
: undefined,
};
return (
<Table
{...others}
{...restProps}
components={components}
showHeader={true}
columns={columns}
dataSource={record.data}
pagination={false}
/>
);
}
};
return (
<div
className={css`
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
setExpendedKeys(keys);
}}
/>
</div>
);
});
`}
>
<Table
showHeader={false}
loading={props?.loading}
columns={groupColumns}
dataSource={categorizeData}
pagination={false}
expandable={{
expandedRowRender: ExpandedRowRender,
expandedRowKeys: expandedKeys,
}}
onExpand={(expanded, record) => {
let keys = [];
if (expanded) {
keys = expandedKeys.concat([record.key]);
} else {
keys = expandedKeys.filter((v) => {
return v !== record.key;
});
}
setExpendedKeys(keys);
}}
/>
</div>
);
},
{ displayName: 'CollectionFieldsTableArray' },
);

View File

@ -50,17 +50,20 @@ function Droppable(props) {
);
}
const TabTitle = observer(({ item }: { item: any }) => {
return (
<Droppable id={item.id.toString()} data={item}>
<div>
<Draggable id={item.id.toString()} data={item}>
<TabBar item={item} />
</Draggable>
</div>
</Droppable>
);
});
const TabTitle = observer(
({ item }: { item: any }) => {
return (
<Droppable id={item.id.toString()} data={item}>
<div>
<Draggable id={item.id.toString()} data={item}>
<TabBar item={item} />
</Draggable>
</div>
</Droppable>
);
},
{ displayName: 'TabTitle' },
);
const TabBar = ({ item }) => {
const compile = useCompile();
@ -71,45 +74,48 @@ const TabBar = ({ item }) => {
</span>
);
};
const DndProvider = observer((props) => {
const [activeTab, setActiveId] = useState(null);
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh: refreshCM } = useResourceActionContext();
const api = useAPIClient();
const onDragEnd = async (props: DragEndEvent) => {
const { active, over } = props;
setTimeout(() => {
setActiveId(null);
});
if (over && over.id !== active.id) {
await api.resource('collectionCategories').move({
sourceId: active.id,
targetId: over.id,
const DndProvider = observer(
(props) => {
const [activeTab, setActiveId] = useState(null);
const { refresh } = useContext(CollectionCategroriesContext);
const { refresh: refreshCM } = useResourceActionContext();
const api = useAPIClient();
const onDragEnd = async (props: DragEndEvent) => {
const { active, over } = props;
setTimeout(() => {
setActiveId(null);
});
await refresh();
await refreshCM();
if (over && over.id !== active.id) {
await api.resource('collectionCategories').move({
sourceId: active.id,
targetId: over.id,
});
await refresh();
await refreshCM();
}
};
function onDragStart(event) {
setActiveId(event.active?.data.current);
}
};
function onDragStart(event) {
setActiveId(event.active?.data.current);
}
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
},
});
const sensors = useSensors(mouseSensor);
return (
<DndContext sensors={sensors} onDragEnd={onDragEnd} onDragStart={onDragStart}>
{props.children}
<DragOverlay>
{activeTab ? <span style={{ whiteSpace: 'nowrap' }}>{<TabBar item={activeTab} />}</span> : null}
</DragOverlay>
</DndContext>
);
});
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
distance: 10,
},
});
const sensors = useSensors(mouseSensor);
return (
<DndContext sensors={sensors} onDragEnd={onDragEnd} onDragStart={onDragStart}>
{props.children}
<DragOverlay>
{activeTab ? <span style={{ whiteSpace: 'nowrap' }}>{<TabBar item={activeTab} />}</span> : null}
</DragOverlay>
</DndContext>
);
},
{ displayName: 'DndProvider' },
);
export const ConfigurationTabs = () => {
const { data, refresh } = useContext(CollectionCategroriesContext);
const { refresh: refreshCM, run, defaultRequest, setState } = useResourceActionContext();

View File

@ -3,14 +3,17 @@ import { Tag } from 'antd';
import React from 'react';
import { useCompile } from '../../../schema-component';
export const CollectionCategory = observer((props: any) => {
const { value } = props;
const compile = useCompile();
return (
<>
{value.map((item) => {
return <Tag color={item.color}>{compile(item?.name)}</Tag>;
})}
</>
);
});
export const CollectionCategory = observer(
(props: any) => {
const { value } = props;
const compile = useCompile();
return (
<>
{value.map((item) => {
return <Tag color={item.color}>{compile(item?.name)}</Tag>;
})}
</>
);
},
{ displayName: 'CollectionCategory' },
);

View File

@ -4,13 +4,16 @@ import React from 'react';
import { useCompile } from '../../../schema-component';
import { useCollectionManager } from '../../hooks';
export const CollectionFieldInterface = observer((props: any) => {
const { value } = props;
const { getInterface } = useCollectionManager();
const compile = useCompile();
const schema = getInterface(value);
export const CollectionFieldInterface = observer(
(props: any) => {
const { value } = props;
const { getInterface } = useCollectionManager();
const compile = useCompile();
const schema = getInterface(value);
if (!schema) return null;
if (!schema) return null;
return <Tag>{compile(schema.title)}</Tag>;
});
return <Tag>{compile(schema.title)}</Tag>;
},
{ displayName: 'CollectionFieldInterface' },
);

View File

@ -4,11 +4,14 @@ import React from 'react';
import { useCompile } from '../../../schema-component';
import { useCollectionManager } from '../../hooks';
export const CollectionTemplate = observer((props: any) => {
const { value } = props;
const { getTemplate } = useCollectionManager();
const compile = useCompile();
const schema = getTemplate(value);
export const CollectionTemplate = observer(
(props: any) => {
const { value } = props;
const { getTemplate } = useCollectionManager();
const compile = useCompile();
const schema = getTemplate(value);
return <Tag>{compile(schema?.title || '{{t("General collection")}}')}</Tag>;
});
return <Tag>{compile(schema?.title || '{{t("General collection")}}')}</Tag>;
},
{ displayName: 'CollectionTemplate' },
);

View File

@ -6,36 +6,39 @@ import { useTranslation } from 'react-i18next';
import { useCompile } from '../../../schema-component';
import { useCollectionManager } from '../../hooks';
export const FieldSummary = observer((props: any) => {
const { schemaKey } = props;
const { getInterface } = useCollectionManager();
const compile = useCompile();
const { t } = useTranslation();
const schema = getInterface(schemaKey);
export const FieldSummary = observer(
(props: any) => {
const { schemaKey } = props;
const { getInterface } = useCollectionManager();
const compile = useCompile();
const { t } = useTranslation();
const schema = getInterface(schemaKey);
if (!schema) return null;
if (!schema) return null;
return (
<div
className={css`
background: #f6f6f6;
margin-bottom: 24px;
padding: 16px;
`}
>
<div className={css``}>
{t('Field interface')}: <Tag>{compile(schema.title)}</Tag>
</div>
{schema.description ? (
<div
className={css`
margin-top: 8px;
color: rgba(0, 0, 0, 0.45);
`}
>
{compile(schema.description)}
return (
<div
className={css`
background: #f6f6f6;
margin-bottom: 24px;
padding: 16px;
`}
>
<div className={css``}>
{t('Field interface')}: <Tag>{compile(schema.title)}</Tag>
</div>
) : null}
</div>
);
});
{schema.description ? (
<div
className={css`
margin-top: 8px;
color: rgba(0, 0, 0, 0.45);
`}
>
{compile(schema.description)}
</div>
) : null}
</div>
);
},
{ displayName: 'FieldSummary' },
);

View File

@ -6,36 +6,39 @@ import { useTranslation } from 'react-i18next';
import { useCompile } from '../../../schema-component';
import { useCollectionManager } from '../../hooks';
export const TemplateSummay = observer((props: any) => {
const { schemaKey } = props;
const { getTemplate } = useCollectionManager();
const compile = useCompile();
const { t } = useTranslation();
const schema = getTemplate(schemaKey);
export const TemplateSummay = observer(
(props: any) => {
const { schemaKey } = props;
const { getTemplate } = useCollectionManager();
const compile = useCompile();
const { t } = useTranslation();
const schema = getTemplate(schemaKey);
if (!schema) return null;
if (!schema) return null;
return (
<div
className={css`
background: #f6f6f6;
margin-bottom: 24px;
padding: 16px;
`}
>
<div className={css``}>
{t('Collection template')}: <Tag>{compile(schema.title)}</Tag>
</div>
{schema.description ? (
<div
className={css`
margin-top: 8px;
color: rgba(0, 0, 0, 0.45);
`}
>
{compile(schema.description)}
return (
<div
className={css`
background: #f6f6f6;
margin-bottom: 24px;
padding: 16px;
`}
>
<div className={css``}>
{t('Collection template')}: <Tag>{compile(schema.title)}</Tag>
</div>
) : null}
</div>
);
});
{schema.description ? (
<div
className={css`
margin-top: 8px;
color: rgba(0, 0, 0, 0.45);
`}
>
{compile(schema.description)}
</div>
) : null}
</div>
);
},
{ displayName: 'TemplateSummay' },
);

View File

@ -6,109 +6,127 @@ import { useRecord } from '../../../record-provider';
import { useCompile } from '../../../schema-component';
import { useCollectionManager } from '../../hooks';
export const SourceForeignKey = observer(() => {
const record = useRecord();
const { getCollection } = useCollectionManager();
const collection = record?.collectionName ? getCollection(record.collectionName) : record;
const field = useField<Field>();
const form = useForm();
const { getCollectionFields } = useCollectionManager();
return (
<div>
<Select
allowClear
placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled}
value={field.value}
options={getCollectionFields(collection.name)
.filter((field) => field.type)
.map((field) => {
return {
label: field?.uiSchema?.title || field.name,
value: field.name,
};
})}
/>
</div>
);
});
export const SourceForeignKey = observer(
() => {
const record = useRecord();
const { getCollection } = useCollectionManager();
const collection = record?.collectionName ? getCollection(record.collectionName) : record;
const field = useField<Field>();
const form = useForm();
const { getCollectionFields } = useCollectionManager();
return (
<div>
<Select
allowClear
placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled}
value={field.value}
options={getCollectionFields(collection.name)
.filter((field) => field.type)
.map((field) => {
return {
label: field?.uiSchema?.title || field.name,
value: field.name,
};
})}
/>
</div>
);
},
{ displayName: 'SourceForeignKey' },
);
export const ThroughForeignKey = observer(() => {
const field = useField<Field>();
const form = useForm();
const { getCollectionFields } = useCollectionManager();
return (
<div>
<Select
allowClear
placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled}
value={field.value}
options={getCollectionFields(form.values.through)
.filter((field) => field.type)
.map((field) => {
return {
label: field?.uiSchema?.title || field.name,
value: field.name,
};
})}
/>
</div>
);
});
export const ThroughForeignKey = observer(
() => {
const field = useField<Field>();
const form = useForm();
const { getCollectionFields } = useCollectionManager();
return (
<div>
<Select
allowClear
placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled}
value={field.value}
options={getCollectionFields(form.values.through)
.filter((field) => field.type)
.map((field) => {
return {
label: field?.uiSchema?.title || field.name,
value: field.name,
};
})}
/>
</div>
);
},
{ displayName: 'ThroughForeignKey' },
);
export const TargetForeignKey = observer(() => {
const field = useField<Field>();
const form = useForm();
const { getCollectionFields } = useCollectionManager();
return (
<div>
<Select
allowClear
placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled}
value={field.value}
options={getCollectionFields(form.values.target)
.filter((field) => field.type)
.map((field) => {
return {
label: field?.uiSchema?.title || field.name,
value: field.name,
};
})}
/>
</div>
);
});
export const TargetForeignKey = observer(
() => {
const field = useField<Field>();
const form = useForm();
const { getCollectionFields } = useCollectionManager();
return (
<div>
<Select
allowClear
placeholder={'留空时,自动生成 FK 字段'}
disabled={field.disabled}
value={field.value}
options={getCollectionFields(form.values.target)
.filter((field) => field.type)
.map((field) => {
return {
label: field?.uiSchema?.title || field.name,
value: field.name,
};
})}
/>
</div>
);
},
{ displayName: 'TargetForeignKey' },
);
export const SourceCollection = observer(() => {
const record = useRecord();
const { getCollection } = useCollectionManager();
const collection = record?.collectionName ? getCollection(record.collectionName) : record;
const compile = useCompile();
return (
<div>
<Select
disabled
value={collection.name}
options={[{ value: collection.name, label: compile(collection.title) }]}
/>
</div>
);
});
export const SourceCollection = observer(
() => {
const record = useRecord();
const { getCollection } = useCollectionManager();
const collection = record?.collectionName ? getCollection(record.collectionName) : record;
const compile = useCompile();
return (
<div>
<Select
disabled
value={collection.name}
options={[{ value: collection.name, label: compile(collection.title) }]}
/>
</div>
);
},
{ displayName: 'SourceCollection' },
);
export const SourceKey = observer(() => {
return (
<div>
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div>
);
});
export const SourceKey = observer(
() => {
return (
<div>
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div>
);
},
{ displayName: 'SourceKey' },
);
export const TargetKey = observer(() => {
return (
<div>
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div>
);
});
export const TargetKey = observer(
() => {
return (
<div>
<Select disabled value={'id'} options={[{ value: 'id', label: 'ID' }]} />
</div>
);
},
{ displayName: 'TargetKey' },
);

View File

@ -166,107 +166,113 @@ export const ds = {
useDestroyAction,
};
export const SubFieldDataSourceProvider = observer((props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [dataSource, setDataSource] = useState([]);
const record = useRecord();
const api = useAPIClient();
const service = useRequest(
() => {
if (record.interface === 'subTable') {
if (record?.children?.length) {
return Promise.resolve({
data: record?.children || [],
});
export const SubFieldDataSourceProvider = observer(
(props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [dataSource, setDataSource] = useState([]);
const record = useRecord();
const api = useAPIClient();
const service = useRequest(
() => {
if (record.interface === 'subTable') {
if (record?.children?.length) {
return Promise.resolve({
data: record?.children || [],
});
}
return api
.resource('fields')
.list({
paginate: false,
// appends: ['uiSchema'],
sort: 'sort',
filter: {
parentKey: record.key,
},
})
.then((res) => res?.data);
}
return api
.resource('fields')
.list({
paginate: false,
// appends: ['uiSchema'],
sort: 'sort',
filter: {
parentKey: record.key,
},
})
.then((res) => res?.data);
}
return Promise.resolve({
data: [],
});
},
{
onSuccess(data) {
console.log('dataSource1', data?.data);
setDataSource(data?.data);
return Promise.resolve({
data: [],
});
},
},
);
return (
<CollectionProvider collection={collection}>
<DataSourceContext.Provider
value={{
rowKey: 'name',
service,
dataSource,
setDataSource,
selectedRowKeys,
setSelectedRowKeys,
}}
>
{props.children}
</DataSourceContext.Provider>
</CollectionProvider>
);
});
{
onSuccess(data) {
console.log('dataSource1', data?.data);
setDataSource(data?.data);
},
},
);
return (
<CollectionProvider collection={collection}>
<DataSourceContext.Provider
value={{
rowKey: 'name',
service,
dataSource,
setDataSource,
selectedRowKeys,
setSelectedRowKeys,
}}
>
{props.children}
</DataSourceContext.Provider>
</CollectionProvider>
);
},
{ displayName: 'SubFieldDataSourceProvider' },
);
export const DataSourceProvider = observer((props: any) => {
const { rowKey, collection, association } = props;
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [dataSource, setDataSource] = useState([]);
const record = useRecord();
const api = useAPIClient();
const { getCollection } = useCollectionManager();
const coll = getCollection(collection);
const resourceOf = record?.[association.targetKey || 'id'];
const service = useRequest(
() => {
if (resourceOf) {
return api
.request({
resource: `${association.collectionName}.${association.name}`,
resourceOf,
action: 'list',
params: {
appends: coll?.fields?.filter((field) => field.target)?.map((field) => field.name),
},
})
.then((res) => res.data);
}
return Promise.resolve({
data: record?.[association.name] || [],
});
},
{
onSuccess(data) {
setDataSource(data?.data);
export const DataSourceProvider = observer(
(props: any) => {
const { rowKey, collection, association } = props;
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [dataSource, setDataSource] = useState([]);
const record = useRecord();
const api = useAPIClient();
const { getCollection } = useCollectionManager();
const coll = getCollection(collection);
const resourceOf = record?.[association.targetKey || 'id'];
const service = useRequest(
() => {
if (resourceOf) {
return api
.request({
resource: `${association.collectionName}.${association.name}`,
resourceOf,
action: 'list',
params: {
appends: coll?.fields?.filter((field) => field.target)?.map((field) => field.name),
},
})
.then((res) => res.data);
}
return Promise.resolve({
data: record?.[association.name] || [],
});
},
},
);
return (
<CollectionProvider collection={coll}>
<DataSourceContext.Provider
value={{
rowKey,
service,
dataSource,
setDataSource,
selectedRowKeys,
setSelectedRowKeys,
}}
>
{props.children}
</DataSourceContext.Provider>
</CollectionProvider>
);
});
{
onSuccess(data) {
setDataSource(data?.data);
},
},
);
return (
<CollectionProvider collection={coll}>
<DataSourceContext.Provider
value={{
rowKey,
service,
dataSource,
setDataSource,
selectedRowKeys,
setSelectedRowKeys,
}}
>
{props.children}
</DataSourceContext.Provider>
</CollectionProvider>
);
},
{ displayName: 'DataSourceProvider' },
);

View File

@ -217,3 +217,4 @@ function areEqual(prevProps, nextProps) {
}
export const PreviewFields = React.memo(PreviewCom, areEqual);
PreviewFields.displayName = 'PreviewFields';

View File

@ -6,21 +6,27 @@ import { ActionModal } from './Action.Modal';
import { ActionPage } from './Action.Page';
import { ComposedActionDrawer } from './types';
export const ActionContainer: ComposedActionDrawer = observer((props: any) => {
const { openMode } = useActionContext();
if (openMode === 'drawer') {
return <ActionDrawer footerNodeName={'Action.Container.Footer'} {...props} />;
}
if (openMode === 'modal') {
return <ActionModal footerNodeName={'Action.Container.Footer'} {...props} />;
}
return <ActionPage footerNodeName={'Action.Container.Footer'} {...props} />;
});
export const ActionContainer: ComposedActionDrawer = observer(
(props: any) => {
const { openMode } = useActionContext();
if (openMode === 'drawer') {
return <ActionDrawer footerNodeName={'Action.Container.Footer'} {...props} />;
}
if (openMode === 'modal') {
return <ActionModal footerNodeName={'Action.Container.Footer'} {...props} />;
}
return <ActionPage footerNodeName={'Action.Container.Footer'} {...props} />;
},
{ displayName: 'ActionContainer' },
);
ActionContainer.Footer = observer(() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
});
ActionContainer.Footer = observer(
() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
},
{ displayName: 'ActionContainer.Footer' },
);
export default ActionContainer;

View File

@ -14,111 +14,117 @@ const openSizeWidthMap = new Map<OpenSize, string>([
['middle', '50%'],
['large', '70%'],
]);
export const ActionDrawer: ComposedActionDrawer = observer((props) => {
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
const { t } = useTranslation();
const { visible, setVisible, openSize = 'middle' } = useActionContext();
const schema = useFieldSchema();
const field = useField();
const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
const finalOpenSize = openSizeFromParent || openSize;
const footerSchema = schema.reduceProperties((buf, s) => {
if (s['x-component'] === footerNodeName) {
return s;
}
return buf;
});
return (
<>
{createPortal(
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<Drawer
width={openSizeWidthMap.get(finalOpenSize)}
title={field.title}
{...others}
destroyOnClose
open={visible}
onClose={() => setVisible(false, true)}
className={classNames(
others.className,
css`
&.nb-action-popup {
.ant-drawer-header {
display: none;
}
.ant-drawer-body {
padding-top: 14px;
}
.ant-drawer-content {
background: #f0f2f5;
}
}
&.nb-record-picker-selector {
.nb-block-item {
margin-bottom: 24px;
.general-schema-designer {
top: -8px;
bottom: -8px;
left: -8px;
right: -8px;
}
}
}
`,
)}
footer={
footerSchema && (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
)
}
export const ActionDrawer: ComposedActionDrawer = observer(
(props) => {
const { footerNodeName = 'Action.Drawer.Footer', ...others } = props;
const { t } = useTranslation();
const { visible, setVisible, openSize = 'middle' } = useActionContext();
const schema = useFieldSchema();
const field = useField();
const openSizeFromParent = schema.parent?.['x-component-props']?.['openSize'];
const finalOpenSize = openSizeFromParent || openSize;
const footerSchema = schema.reduceProperties((buf, s) => {
if (s['x-component'] === footerNodeName) {
return s;
}
return buf;
});
return (
<>
{createPortal(
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
</Drawer>
</div>,
document.body,
)}
</>
);
});
<Drawer
width={openSizeWidthMap.get(finalOpenSize)}
title={field.title}
{...others}
destroyOnClose
open={visible}
onClose={() => setVisible(false, true)}
className={classNames(
others.className,
css`
&.nb-action-popup {
.ant-drawer-header {
display: none;
}
ActionDrawer.Footer = observer(() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
});
.ant-drawer-body {
padding-top: 14px;
}
.ant-drawer-content {
background: #f0f2f5;
}
}
&.nb-record-picker-selector {
.nb-block-item {
margin-bottom: 24px;
.general-schema-designer {
top: -8px;
bottom: -8px;
left: -8px;
right: -8px;
}
}
}
`,
)}
footer={
footerSchema && (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
)
}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
</Drawer>
</div>,
document.body,
)}
</>
);
},
{ displayName: 'ActionDrawer' },
);
ActionDrawer.Footer = observer(
() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
},
{ displayName: 'ActionDrawer.Footer' },
);
export default ActionDrawer;

View File

@ -3,6 +3,9 @@ import React from 'react';
import Action from './Action';
import { ComposedAction } from './types';
export const ActionLink: ComposedAction = observer((props: any) => {
return <Action {...props} component={'a'} className={'nb-action-link'} />;
});
export const ActionLink: ComposedAction = observer(
(props: any) => {
return <Action {...props} component={'a'} className={'nb-action-link'} />;
},
{ displayName: 'ActionLink' },
);

View File

@ -12,104 +12,110 @@ const openSizeWidthMap = new Map<OpenSize, string>([
['middle', '60%'],
['large', '80%'],
]);
export const ActionModal: ComposedActionDrawer<ModalProps> = observer((props) => {
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
const { visible, setVisible, openSize = 'large' } = useActionContext();
const actualWidth = width ?? openSizeWidthMap.get(openSize);
const schema = useFieldSchema();
const field = useField();
const footerSchema = schema.reduceProperties((buf, s) => {
if (s['x-component'] === footerNodeName) {
return s;
}
return buf;
});
return (
<>
{createPortal(
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<Modal
width={actualWidth}
title={field.title}
{...(others as ModalProps)}
destroyOnClose
visible={visible}
onCancel={() => setVisible(false, true)}
className={classNames(
others.className,
css`
&.nb-action-popup {
.ant-modal-header {
display: none;
}
.ant-modal-body {
padding-top: 16px;
}
.ant-modal-body {
background: #f0f2f5;
}
.ant-modal-close-x {
width: 32px;
height: 32px;
line-height: 32px;
}
}
`,
)}
footer={
footerSchema ? (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
) : (
false
)
}
export const ActionModal: ComposedActionDrawer<ModalProps> = observer(
(props) => {
const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props;
const { visible, setVisible, openSize = 'large' } = useActionContext();
const actualWidth = width ?? openSizeWidthMap.get(openSize);
const schema = useFieldSchema();
const field = useField();
const footerSchema = schema.reduceProperties((buf, s) => {
if (s['x-component'] === footerNodeName) {
return s;
}
return buf;
});
return (
<>
{createPortal(
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
</Modal>
</div>,
document.body,
)}
</>
);
});
<Modal
width={actualWidth}
title={field.title}
{...(others as ModalProps)}
destroyOnClose
visible={visible}
onCancel={() => setVisible(false, true)}
className={classNames(
others.className,
css`
&.nb-action-popup {
.ant-modal-header {
display: none;
}
ActionModal.Footer = observer(() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
});
.ant-modal-body {
padding-top: 16px;
}
.ant-modal-body {
background: #f0f2f5;
}
.ant-modal-close-x {
width: 32px;
height: 32px;
line-height: 32px;
}
}
`,
)}
footer={
footerSchema ? (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
) : (
false
)
}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
</Modal>
</div>,
document.body,
)}
</>
);
},
{ displayName: 'ActionModal' },
);
ActionModal.Footer = observer(
() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
},
{ displayName: 'ActionModal.Footer' },
);
export default ActionModal;

View File

@ -10,65 +10,71 @@ const useScope = (key: string) => {
return scope[key];
};
export const ActionPage: ComposedActionDrawer = observer((props: any) => {
const { footerNodeName = 'Action.Page.Footer', ...others } = props;
const { containerRefKey, visible, setVisible } = useActionContext();
const containerRef = useScope(containerRefKey);
const schema = useFieldSchema();
const field = useField();
const footerSchema = schema.reduceProperties((buf, s) => {
if (s['x-component'] === footerNodeName) {
return s;
}
return buf;
});
return (
<>
{containerRef?.current &&
visible &&
createPortal(
<div>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
{footerSchema && (
<div
className={css`
display: flex;
/* justify-content: flex-end; */
/* flex-direction: row-reverse; */
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
)}
</div>,
containerRef?.current,
)}
</>
);
});
export const ActionPage: ComposedActionDrawer = observer(
(props: any) => {
const { footerNodeName = 'Action.Page.Footer', ...others } = props;
const { containerRefKey, visible, setVisible } = useActionContext();
const containerRef = useScope(containerRefKey);
const schema = useFieldSchema();
const field = useField();
const footerSchema = schema.reduceProperties((buf, s) => {
if (s['x-component'] === footerNodeName) {
return s;
}
return buf;
});
return (
<>
{containerRef?.current &&
visible &&
createPortal(
<div>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] !== footerNodeName;
}}
/>
{footerSchema && (
<div
className={css`
display: flex;
/* justify-content: flex-end; */
/* flex-direction: row-reverse; */
width: 100%;
.ant-btn {
margin-right: 8px;
}
`}
>
<RecursionField
basePath={field.address}
schema={schema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === footerNodeName;
}}
/>
</div>
)}
</div>,
containerRef?.current,
)}
</>
);
},
{ displayName: 'ActionPage' },
);
ActionPage.Footer = observer(() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
});
ActionPage.Footer = observer(
() => {
const field = useField();
const schema = useFieldSchema();
return <RecursionField basePath={field.address} schema={schema} onlyRenderProperties />;
},
{ displayName: 'ActionPage.Footer' },
);
export default ActionPage;

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { Button, Modal, Popover } from 'antd';
import classnames from 'classnames';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useActionContext } from '../..';
import { useDesignable } from '../../';
import { Icon } from '../../../icon';
@ -64,139 +64,158 @@ export const actionDesignerCss = css`
}
`;
export const Action: ComposedAction = observer((props: any) => {
const {
popover,
confirm,
openMode: om,
containerRefKey,
component,
useAction = useA,
className,
icon,
title,
...others
} = props;
const { onClick } = useProps(props);
const [visible, setVisible] = useState(false);
const [formValueChanged, setFormValueChanged] = useState(false);
const Designer = useDesigner();
const field = useField<any>();
const { run } = useAction();
const fieldSchema = useFieldSchema();
const compile = useCompile();
const form = useForm();
const values = useRecord();
const designerProps = fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const disabled = form.disabled || field.disabled || props.disabled;
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable } = useDesignable();
const tarComponent = useComponent(component) || component;
useEffect(() => {
field.linkageProperty = {};
linkageRules
.filter((k) => !k.disabled)
.forEach((v) => {
return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, values);
});
});
}, [linkageRules, values, designable]);
const renderButton = () => {
if (!designable && field?.data?.hidden) {
export const Action: ComposedAction = observer(
(props: any) => {
const {
popover,
confirm,
openMode: om,
containerRefKey,
component,
useAction = useA,
className,
icon,
title,
...others
} = props;
const { onClick } = useProps(props);
const [visible, setVisible] = useState(false);
const [formValueChanged, setFormValueChanged] = useState(false);
const Designer = useDesigner();
const field = useField<any>();
const { run } = useAction();
const fieldSchema = useFieldSchema();
const compile = useCompile();
const form = useForm();
const values = useRecord();
const designerProps = fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const disabled = form.disabled || field.disabled || props.disabled;
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable } = useDesignable();
const tarComponent = useComponent(component) || component;
const renderTitle = useMemo(() => {
if (title) {
return title;
}
if (fieldSchema.title) {
return compile(fieldSchema.title);
}
return null;
}
return (
<SortableItem
{...others}
loading={field?.data?.loading}
icon={<Icon type={icon} />}
disabled={disabled}
style={{
opacity: designable && field?.data?.hidden && 0.1,
}}
onClick={(e: React.MouseEvent) => {
if (!disabled) {
e.preventDefault();
e.stopPropagation();
const onOk = () => {
onClick?.(e);
setVisible(true);
run();
};
if (confirm) {
Modal.confirm({
...confirm,
onOk,
});
} else {
onOk();
}, [compile, fieldSchema.title, title]);
useEffect(() => {
field.linkageProperty = {};
linkageRules
.filter((k) => !k.disabled)
.forEach((v) => {
return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, values);
});
});
}, [linkageRules, values, designable]);
const renderButton = () => {
if (!designable && field?.data?.hidden) {
return null;
}
return (
<SortableItem
{...others}
loading={field?.data?.loading}
icon={<Icon type={icon} />}
disabled={disabled}
style={{
opacity: designable && field?.data?.hidden && 0.1,
}}
onClick={(e: React.MouseEvent) => {
if (!disabled) {
e.preventDefault();
e.stopPropagation();
const onOk = () => {
onClick?.(e);
setVisible(true);
run();
};
if (confirm) {
Modal.confirm({
...confirm,
onOk,
});
} else {
onOk();
}
}
}
}}
component={tarComponent || Button}
className={classnames(className, actionDesignerCss)}
>
{renderTitle}
<Designer {...designerProps} />
</SortableItem>
);
};
return (
<ActionContext.Provider
value={{
button: renderButton(),
visible,
setVisible,
formValueChanged,
setFormValueChanged,
openMode,
openSize,
containerRefKey,
fieldSchema,
}}
component={tarComponent || Button}
className={classnames(className, actionDesignerCss)}
>
{title || compile(fieldSchema.title)}
<Designer {...designerProps} />
</SortableItem>
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && renderButton()}
{!popover && props.children}
</ActionContext.Provider>
);
};
},
{ displayName: 'Action' },
);
return (
<ActionContext.Provider
value={{
button: renderButton(),
visible,
setVisible,
formValueChanged,
setFormValueChanged,
openMode,
openSize,
containerRefKey,
fieldSchema,
}}
>
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && renderButton()}
{!popover && props.children}
</ActionContext.Provider>
);
});
Action.Popover = observer(
(props) => {
const { button, visible, setVisible } = useActionContext();
return (
<Popover
{...props}
destroyTooltipOnHide
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
content={props.children}
>
{button}
</Popover>
);
},
{ displayName: 'Action.Popover' },
);
Action.Popover = observer((props) => {
const { button, visible, setVisible } = useActionContext();
return (
<Popover
{...props}
destroyTooltipOnHide
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
content={props.children}
>
{button}
</Popover>
);
});
Action.Popover.Footer = observer((props) => {
return (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
{props.children}
</div>
);
});
Action.Popover.Footer = observer(
(props) => {
return (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
{props.children}
</div>
);
},
{ displayName: 'Action.Popover.Footer' },
);
Action.Link = ActionLink;
Action.Designer = ActionDesigner;

View File

@ -6,40 +6,41 @@ import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext } from '../../common';
import { useDesignable, useProps } from '../../hooks';
export const ActionBar = observer((props: any) => {
const { layout = 'tow-columns', style, spaceProps, ...others } = useProps(props);
const fieldSchema = useFieldSchema();
const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
const { designable } = useDesignable();
if (layout === 'one-column') {
export const ActionBar = observer(
(props: any) => {
const { layout = 'tow-columns', style, spaceProps, ...others } = useProps(props);
const fieldSchema = useFieldSchema();
const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
const { designable } = useDesignable();
if (layout === 'one-column') {
return (
<DndContext>
<div
style={{ display: 'flex', alignItems: 'center', ...style }}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
{props.children && (
<div style={{ marginRight: 8 }}>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
</div>
)}
<InitializerComponent />
</div>
</DndContext>
);
}
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
return (
<DndContext>
<div
style={{ display: 'flex', alignItems: 'center', ...style }}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
{props.children && (
<div style={{ marginRight: 8 }}>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
</div>
)}
<InitializerComponent />
</div>
</DndContext>
);
}
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
return (
<div
style={
!designable && !hasActions
? undefined
: {
<div
style={
!designable && !hasActions
? undefined
: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
@ -47,38 +48,40 @@ export const ActionBar = observer((props: any) => {
flexShrink: 0,
...style,
}
}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
<div
className={css`
.ant-space:last-child {
margin-left: 8px;
}
`}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
}
{...others}
className={cx(others.className, 'nb-action-bar')}
>
<DndContext>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
if (schema['x-align'] !== 'left') {
return null;
}
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
if (schema['x-align'] === 'left') {
return null;
}
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
</DndContext>
<div
className={css`
.ant-space:last-child {
margin-left: 8px;
}
`}
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
>
<DndContext>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
if (schema['x-align'] !== 'left') {
return null;
}
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
<Space {...spaceProps}>
{fieldSchema.mapProperties((schema, key) => {
if (schema['x-align'] === 'left') {
return null;
}
return <RecursionField key={key} name={key} schema={schema} />;
})}
</Space>
</DndContext>
</div>
<InitializerComponent />
</div>
<InitializerComponent />
</div>
);
});
);
},
{ displayName: 'ActionBar' },
);

View File

@ -18,30 +18,33 @@ const useCloseAction = () => {
};
};
const Editable = observer((props) => {
const field = useField<Field>();
const schema = useFieldSchema();
return (
<div>
<div style={{ marginBottom: 24 }}>
<Select
value={field.componentProps.openMode}
style={{ width: 100 }}
onChange={(value) => {
field.componentProps.openMode = value;
schema['x-component-props']['openMode'] = value;
}}
>
<Select.Option value={'drawer'}>Drawer</Select.Option>
<Select.Option value={'modal'}>Modal</Select.Option>
<Select.Option value={'page'}>Page</Select.Option>
</Select>
const Editable = observer(
(props) => {
const field = useField<Field>();
const schema = useFieldSchema();
return (
<div>
<div style={{ marginBottom: 24 }}>
<Select
value={field.componentProps.openMode}
style={{ width: 100 }}
onChange={(value) => {
field.componentProps.openMode = value;
schema['x-component-props']['openMode'] = value;
}}
>
<Select.Option value={'drawer'}>Drawer</Select.Option>
<Select.Option value={'modal'}>Modal</Select.Option>
<Select.Option value={'page'}>Page</Select.Option>
</Select>
</div>
{props.children}
</div>
{props.children}
</div>
);
});
);
},
{ displayName: 'Editable' },
);
const schema: ISchema = {
type: 'object',

View File

@ -71,52 +71,55 @@ function TypeSelect(props) {
);
}
const EditOptions = observer((props) => {
const record = ArrayTable.useRecord();
const field = useField<Field>();
console.log(record.type);
return (
<div>
<a
onClick={() => {
FormDrawer('Pop-up form', () => {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField
schema={{
type: 'object',
properties: record.type ? types[record.type] || {} : {},
}}
/>
<FormDrawer.Footer>
<FormButtonGroup align="right">
<Submit
onSubmit={() => {
return new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}}
>
Submit
</Submit>
</FormButtonGroup>
</FormDrawer.Footer>
</FormLayout>
);
})
.open({
initialValues: field.value,
const EditOptions = observer(
(props) => {
const record = ArrayTable.useRecord();
const field = useField<Field>();
console.log(record.type);
return (
<div>
<a
onClick={() => {
FormDrawer('Pop-up form', () => {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField
schema={{
type: 'object',
properties: record.type ? types[record.type] || {} : {},
}}
/>
<FormDrawer.Footer>
<FormButtonGroup align="right">
<Submit
onSubmit={() => {
return new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}}
>
Submit
</Submit>
</FormButtonGroup>
</FormDrawer.Footer>
</FormLayout>
);
})
.then((values) => {
field.value = values;
});
}}
>
Edit
</a>
</div>
);
});
.open({
initialValues: field.value,
})
.then((values) => {
field.value = values;
});
}}
>
Edit
</a>
</div>
);
},
{ displayName: 'EditOptions' },
);
const SchemaField = createSchemaField({
components: {

View File

@ -4,63 +4,66 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useCollectionManager } from '../../../collection-manager';
import { AssociationFieldContext } from './context';
export const AssociationFieldProvider = observer((props) => {
const field = useField<Field>();
const { getCollectionJoinField, getCollection } = useCollectionManager();
const fieldSchema = useFieldSchema();
const allowMultiple = fieldSchema['x-component-props']?.multiple !== false;
const allowDissociate = fieldSchema['x-component-props']?.allowDissociate !== false;
export const AssociationFieldProvider = observer(
(props) => {
const field = useField<Field>();
const { getCollectionJoinField, getCollection } = useCollectionManager();
const fieldSchema = useFieldSchema();
const allowMultiple = fieldSchema['x-component-props']?.multiple !== false;
const allowDissociate = fieldSchema['x-component-props']?.allowDissociate !== false;
const collectionField = useMemo(
() => getCollectionJoinField(fieldSchema['x-collection-field']),
[fieldSchema['x-collection-field'], fieldSchema.name],
);
const isFileCollection = useMemo(
() => getCollection(collectionField?.target)?.template === 'file',
[fieldSchema['x-collection-field']],
);
const currentMode = useMemo(
() => fieldSchema['x-component-props']?.mode || (isFileCollection ? 'FileManager' : 'Select'),
[fieldSchema['x-component-props']?.mode],
);
const collectionField = useMemo(
() => getCollectionJoinField(fieldSchema['x-collection-field']),
[fieldSchema['x-collection-field'], fieldSchema.name],
);
const isFileCollection = useMemo(
() => getCollection(collectionField?.target)?.template === 'file',
[fieldSchema['x-collection-field']],
);
const currentMode = useMemo(
() => fieldSchema['x-component-props']?.mode || (isFileCollection ? 'FileManager' : 'Select'),
[fieldSchema['x-component-props']?.mode],
);
const [loading, setLoading] = useState(true);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
if (!collectionField) {
setLoading(false);
return;
}
if (field.value !== null && field.value !== undefined) {
// Nester 子表单时,如果没数据初始化一个 [null] 的占位
if (currentMode === 'Nester' && Array.isArray(field.value)) {
if (field.value.length === 0 && ['belongsToMany', 'hasMany'].includes(collectionField.type)) {
useEffect(() => {
setLoading(true);
if (!collectionField) {
setLoading(false);
return;
}
if (field.value !== null && field.value !== undefined) {
// Nester 子表单时,如果没数据初始化一个 [null] 的占位
if (currentMode === 'Nester' && Array.isArray(field.value)) {
if (field.value.length === 0 && ['belongsToMany', 'hasMany'].includes(collectionField.type)) {
field.value = [{}];
}
}
setLoading(false);
return;
}
if (currentMode === 'Nester') {
if (['belongsTo', 'hasOne'].includes(collectionField.type)) {
field.value = {};
} else if (['belongsToMany', 'hasMany'].includes(collectionField.type)) {
field.value = [{}];
}
}
setLoading(false);
return;
}
if (currentMode === 'Nester') {
if (['belongsTo', 'hasOne'].includes(collectionField.type)) {
field.value = {};
} else if (['belongsToMany', 'hasMany'].includes(collectionField.type)) {
field.value = [{}];
}
}
setLoading(false);
}, [currentMode, collectionField, field.value]);
}, [currentMode, collectionField, field.value]);
if (loading) {
return null;
}
if (loading) {
return null;
}
return collectionField ? (
<AssociationFieldContext.Provider
value={{ options: collectionField, field, allowMultiple, allowDissociate, currentMode }}
>
{props.children}
</AssociationFieldContext.Provider>
) : null;
});
return collectionField ? (
<AssociationFieldContext.Provider
value={{ options: collectionField, field, allowMultiple, allowDissociate, currentMode }}
>
{props.children}
</AssociationFieldContext.Provider>
) : null;
},
{ displayName: 'AssociationFieldProvider' },
);

View File

@ -11,41 +11,44 @@ export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
multiple?: boolean;
};
const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
const { fieldNames, objectValue = true } = props;
const field: any = useField();
const fieldSchema = useFieldSchema();
const service = useServiceOptions(props);
const isAllowAddNew = fieldSchema['x-add-new'];
const value = Array.isArray(props.value) ? props.value.filter(Boolean) : props.value;
const InternalAssociationSelect = observer(
(props: AssociationSelectProps) => {
const { fieldNames, objectValue = true } = props;
const field: any = useField();
const fieldSchema = useFieldSchema();
const service = useServiceOptions(props);
const isAllowAddNew = fieldSchema['x-add-new'];
const value = Array.isArray(props.value) ? props.value.filter(Boolean) : props.value;
return (
<div key={fieldSchema.name}>
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<RemoteSelect
style={{ width: '100%' }}
{...props}
objectValue={objectValue}
value={value}
service={service}
></RemoteSelect>
return (
<div key={fieldSchema.name}>
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<RemoteSelect
style={{ width: '100%' }}
{...props}
objectValue={objectValue}
value={value}
service={service}
></RemoteSelect>
{isAllowAddNew && (
<RecordProvider record={null}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</RecordProvider>
)}
</Input.Group>
</div>
);
});
{isAllowAddNew && (
<RecordProvider record={null}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</RecordProvider>
)}
</Input.Group>
</div>
);
},
{ displayName: 'InternalAssociationSelect' },
);
interface AssociationSelectInterface {
(props: any): React.ReactElement;

View File

@ -12,50 +12,56 @@ import { InternalSubTable } from './InternalSubTable';
import { CreateRecordAction } from './components/CreateRecordAction';
import { useAssociationFieldContext } from './hooks';
const EditableAssociationField = observer((props: any) => {
const { multiple } = props;
const field: Field = useField();
const form = useForm();
const fieldSchema = useFieldSchema();
const { options: collectionField, currentMode } = useAssociationFieldContext();
const EditableAssociationField = observer(
(props: any) => {
const { multiple } = props;
const field: Field = useField();
const form = useForm();
const fieldSchema = useFieldSchema();
const { options: collectionField, currentMode } = useAssociationFieldContext();
const useCreateActionProps = () => {
const { onClick } = useCAP();
const actionField: any = useField();
return {
async onClick() {
await onClick();
const { data } = actionField.data?.data?.data || {};
if (data) {
if (['m2m', 'o2m'].includes(collectionField?.interface) && multiple !== false) {
const values = form.getValuesIn(field.path) || [];
values.push(data);
form.setValuesIn(field.path, values);
field.onInput(values);
} else {
form.setValuesIn(field.path, data);
field.onInput(data);
const useCreateActionProps = () => {
const { onClick } = useCAP();
const actionField: any = useField();
return {
async onClick() {
await onClick();
const { data } = actionField.data?.data?.data || {};
if (data) {
if (['m2m', 'o2m'].includes(collectionField?.interface) && multiple !== false) {
const values = form.getValuesIn(field.path) || [];
values.push(data);
form.setValuesIn(field.path, values);
field.onInput(values);
} else {
form.setValuesIn(field.path, data);
field.onInput(data);
}
}
}
},
},
};
};
};
return (
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}>
{currentMode === 'Picker' && <InternalPicker {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'Select' && <AssociationSelect {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
</SchemaComponentOptions>
);
});
return (
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}>
{currentMode === 'Picker' && <InternalPicker {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'Select' && <AssociationSelect {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
</SchemaComponentOptions>
);
},
{ displayName: 'EditableAssociationField' },
);
export const Editable = observer((props) => {
return (
<AssociationFieldProvider>
<EditableAssociationField {...props} />
</AssociationFieldProvider>
);
});
export const Editable = observer(
(props) => {
return (
<AssociationFieldProvider>
<EditableAssociationField {...props} />
</AssociationFieldProvider>
);
},
{ displayName: 'Editable' },
);

View File

@ -60,138 +60,145 @@ const useTableSelectorProps = () => {
};
};
export const InternalPicker = observer((props: any) => {
const { value, multiple, onChange, quickUpload, selectFile, ...others } = props;
const field: any = useField();
const fieldNames = useFieldNames(props);
const [visibleSelector, setVisibleSelector] = useState(false);
const fieldSchema = useFieldSchema();
const insertSelector = useInsertSchema('Selector');
const { options: collectionField } = useAssociationFieldContext();
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const isAllowAddNew = fieldSchema['x-add-new'];
const [selectedRows, setSelectedRows] = useState([]);
const options = useMemo(() => {
if (value && Object.keys(value).length > 0) {
const opts = (Array.isArray(value) ? value : value ? [value] : []).filter(Boolean).map((option) => {
const label = option?.[fieldNames.label];
return {
...option,
[fieldNames.label]: getLabelFormatValue(compile(labelUiSchema), compile(label)),
};
});
return opts;
}
return [];
}, [value, fieldNames?.label]);
export const InternalPicker = observer(
(props: any) => {
const { value, multiple, onChange, quickUpload, selectFile, ...others } = props;
const field: any = useField();
const fieldNames = useFieldNames(props);
const [visibleSelector, setVisibleSelector] = useState(false);
const fieldSchema = useFieldSchema();
const insertSelector = useInsertSchema('Selector');
const { options: collectionField } = useAssociationFieldContext();
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const isAllowAddNew = fieldSchema['x-add-new'];
const [selectedRows, setSelectedRows] = useState([]);
const options = useMemo(() => {
if (value && Object.keys(value).length > 0) {
const opts = (Array.isArray(value) ? value : value ? [value] : []).filter(Boolean).map((option) => {
const label = option?.[fieldNames.label];
return {
...option,
[fieldNames.label]: getLabelFormatValue(compile(labelUiSchema), compile(label)),
};
});
return opts;
}
return [];
}, [value, fieldNames?.label]);
const pickerProps = {
size: 'small',
fieldNames,
multiple: multiple !== false && ['o2m', 'm2m'].includes(collectionField?.interface),
association: {
target: collectionField?.target,
},
options,
onChange: props?.onChange,
selectedRows,
setSelectedRows,
collectionField,
};
const getValue = () => {
if (multiple == null) return null;
return Array.isArray(value) ? value.filter(Boolean)?.map((v) => v?.[fieldNames.value]) : value?.[fieldNames.value];
};
const getFilter = () => {
const targetKey = collectionField?.targetKey || 'id';
const list = options.map((option) => option[targetKey]).filter(Boolean);
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
};
const usePickActionProps = () => {
const { setVisible } = useActionContext();
const { multiple, selectedRows, onChange, options, collectionField } = useContext(RecordPickerContext);
return {
onClick() {
if (multiple) {
onChange(unionBy(selectedRows, options, collectionField?.targetKey || 'id'));
} else {
onChange(selectedRows?.[0] || null);
}
setVisible(false);
const pickerProps = {
size: 'small',
fieldNames,
multiple: multiple !== false && ['o2m', 'm2m'].includes(collectionField?.interface),
association: {
target: collectionField?.target,
},
options,
onChange: props?.onChange,
selectedRows,
setSelectedRows,
collectionField,
};
};
return (
<>
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<div style={{ width: '100%' }}>
<Select
style={{ width: '100%' }}
{...others}
mode={multiple ? 'multiple' : props.mode}
fieldNames={fieldNames}
onDropdownVisibleChange={(open) => {
insertSelector(schema.Selector);
setVisibleSelector(true);
}}
allowClear
onChange={(changed: any) => {
if (!changed) {
const value = multiple ? [] : null;
onChange(value);
setSelectedRows(value);
} else if (Array.isArray(changed)) {
if (!changed.length) {
onChange([]);
setSelectedRows([]);
return;
}
const values = options?.filter((option) => changed.includes(option[fieldNames.value]));
onChange(values);
setSelectedRows(values);
}
}}
options={options}
value={getValue()}
open={false}
/>
</div>
{isAllowAddNew && (
<RecordProvider record={null}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
const getValue = () => {
if (multiple == null) return null;
return Array.isArray(value)
? value.filter(Boolean)?.map((v) => v?.[fieldNames.value])
: value?.[fieldNames.value];
};
const getFilter = () => {
const targetKey = collectionField?.targetKey || 'id';
const list = options.map((option) => option[targetKey]).filter(Boolean);
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
};
const usePickActionProps = () => {
const { setVisible } = useActionContext();
const { multiple, selectedRows, onChange, options, collectionField } = useContext(RecordPickerContext);
return {
onClick() {
if (multiple) {
onChange(unionBy(selectedRows, options, collectionField?.targetKey || 'id'));
} else {
onChange(selectedRows?.[0] || null);
}
setVisible(false);
},
};
};
return (
<>
<Input.Group compact style={{ display: 'flex', lineHeight: '32px' }}>
<div style={{ width: '100%' }}>
<Select
style={{ width: '100%' }}
{...others}
mode={multiple ? 'multiple' : props.mode}
fieldNames={fieldNames}
onDropdownVisibleChange={(open) => {
insertSelector(schema.Selector);
setVisibleSelector(true);
}}
allowClear
onChange={(changed: any) => {
if (!changed) {
const value = multiple ? [] : null;
onChange(value);
setSelectedRows(value);
} else if (Array.isArray(changed)) {
if (!changed.length) {
onChange([]);
setSelectedRows([]);
return;
}
const values = options?.filter((option) => changed.includes(option[fieldNames.value]));
onChange(values);
setSelectedRows(values);
}
}}
options={options}
value={getValue()}
open={false}
/>
</RecordProvider>
)}
</Input.Group>
<ActionContext.Provider value={{ openMode: 'drawer', visible: visibleSelector, setVisible: setVisibleSelector }}>
<RecordPickerProvider {...pickerProps}>
<CollectionProvider name={collectionField?.target}>
<FormProvider>
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
<SchemaComponentOptions scope={{ usePickActionProps, useTableSelectorProps }}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.Selector';
}}
/>
</SchemaComponentOptions>
</TableSelectorParamsProvider>
</FormProvider>
</CollectionProvider>
</RecordPickerProvider>
</ActionContext.Provider>
</>
);
});
</div>
{isAllowAddNew && (
<RecordProvider record={null}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</RecordProvider>
)}
</Input.Group>
<ActionContext.Provider
value={{ openMode: 'drawer', visible: visibleSelector, setVisible: setVisibleSelector }}
>
<RecordPickerProvider {...pickerProps}>
<CollectionProvider name={collectionField?.target}>
<FormProvider>
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
<SchemaComponentOptions scope={{ usePickActionProps, useTableSelectorProps }}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.Selector';
}}
/>
</SchemaComponentOptions>
</TableSelectorParamsProvider>
</FormProvider>
</CollectionProvider>
</RecordPickerProvider>
</ActionContext.Provider>
</>
);
},
{ displayName: 'InternalPicker' },
);

View File

@ -22,94 +22,97 @@ const toValue = (value, placeholder) => {
}
return value;
};
export const ReadPrettyInternalViewer: React.FC = observer((props: any) => {
const fieldSchema = useFieldSchema();
const recordCtx = useRecord();
const { enableLink } = fieldSchema['x-component-props'];
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
const field = useField();
const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false);
const insertViewer = useInsertSchema('Viewer');
const { options: collectionField } = useAssociationFieldContext();
const [record, setRecord] = useState({});
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const { snapshot } = useActionContext();
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
export const ReadPrettyInternalViewer: React.FC = observer(
(props: any) => {
const fieldSchema = useFieldSchema();
const recordCtx = useRecord();
const { enableLink } = fieldSchema['x-component-props'];
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
const field = useField();
const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false);
const insertViewer = useInsertSchema('Viewer');
const { options: collectionField } = useAssociationFieldContext();
const [record, setRecord] = useState({});
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const { snapshot } = useActionContext();
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
const text = getLabelFormatValue(compile(labelUiSchema), val, true);
return (
<Fragment key={`${record.id}_${index}`}>
<span>
{snapshot ? (
text
) : enableLink !== false ? (
<a
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
insertViewer(schema.Viewer);
setVisible(true);
setRecord(record);
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>
{text}
</a>
) : (
text
)}
</span>
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
</Fragment>
);
});
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
const text = getLabelFormatValue(compile(labelUiSchema), val, true);
return (
<Fragment key={`${record.id}_${index}`}>
<span>
{snapshot ? (
text
) : enableLink !== false ? (
<a
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
insertViewer(schema.Viewer);
setVisible(true);
setRecord(record);
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>
{text}
</a>
) : (
text
)}
</span>
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
</Fragment>
);
});
const renderWithoutTableFieldResourceProvider = () => (
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
basePath={field.address}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.Viewer';
}}
/>
</FormProvider>
</WithoutTableFieldResource.Provider>
);
const renderRecordProvider = () => {
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
return collectionFieldNames && collectionFieldNames.length > 2 ? (
<RecordProvider record={recordCtx[collectionFieldNames[1]]}>
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
</RecordProvider>
) : (
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
const renderWithoutTableFieldResourceProvider = () => (
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
basePath={field.address}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.Viewer';
}}
/>
</FormProvider>
</WithoutTableFieldResource.Provider>
);
};
return (
<div>
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
<CollectionProvider name={collectionField?.target ?? collectionField?.targetCollection}>
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
{renderRecords()}
</EllipsisWithTooltip>
<ActionContext.Provider
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField?.interface === 'snapshot' }}
>
{renderRecordProvider()}
</ActionContext.Provider>
</CollectionProvider>
</BlockAssociationContext.Provider>
</div>
);
});
const renderRecordProvider = () => {
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
return collectionFieldNames && collectionFieldNames.length > 2 ? (
<RecordProvider record={recordCtx[collectionFieldNames[1]]}>
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
</RecordProvider>
) : (
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
);
};
return (
<div>
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
<CollectionProvider name={collectionField?.target ?? collectionField?.targetCollection}>
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
{renderRecords()}
</EllipsisWithTooltip>
<ActionContext.Provider
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField?.interface === 'snapshot' }}
>
{renderRecordProvider()}
</ActionContext.Provider>
</CollectionProvider>
</BlockAssociationContext.Provider>
</div>
);
},
{ displayName: 'ReadPrettyInternalViewer' },
);

View File

@ -25,92 +25,95 @@ const ToOneNester = (props) => {
return <Card bordered={true}>{props.children}</Card>;
};
const ToManyNester = observer((props) => {
const fieldSchema = useFieldSchema();
const { options, field, allowMultiple, allowDissociate } = useAssociationFieldContext<ArrayField>();
const { t } = useTranslation();
return (field.value || []).length > 0 ? (
<Card
bordered={true}
style={{ position: 'relative' }}
className={css`
> .ant-card-body > .ant-divider:last-child {
display: none;
}
`}
>
{(field.value || []).map((value, index) => {
let allowed = allowDissociate;
if (!allowDissociate) {
allowed = !value?.[options.targetKey];
}
return (
<>
<div style={{ textAlign: 'right' }}>
{field.editable && allowMultiple && (
<Tooltip key={'add'} title={t('Add new')}>
<PlusOutlined
style={{ zIndex: 1000, marginRight: '10px', color: '#a8a3a3' }}
onClick={() => {
action(() => {
if (!Array.isArray(field.value)) {
field.value = [];
}
spliceArrayState(field as any, {
startIndex: index + 1,
insertCount: 1,
const ToManyNester = observer(
(props) => {
const fieldSchema = useFieldSchema();
const { options, field, allowMultiple, allowDissociate } = useAssociationFieldContext<ArrayField>();
const { t } = useTranslation();
return (field.value || []).length > 0 ? (
<Card
bordered={true}
style={{ position: 'relative' }}
className={css`
> .ant-card-body > .ant-divider:last-child {
display: none;
}
`}
>
{(field.value || []).map((value, index) => {
let allowed = allowDissociate;
if (!allowDissociate) {
allowed = !value?.[options.targetKey];
}
return (
<>
<div style={{ textAlign: 'right' }}>
{field.editable && allowMultiple && (
<Tooltip key={'add'} title={t('Add new')}>
<PlusOutlined
style={{ zIndex: 1000, marginRight: '10px', color: '#a8a3a3' }}
onClick={() => {
action(() => {
if (!Array.isArray(field.value)) {
field.value = [];
}
spliceArrayState(field as any, {
startIndex: index + 1,
insertCount: 1,
});
field.value.splice(index + 1, 0, {});
return field.onInput(field.value);
});
field.value.splice(index + 1, 0, {});
return field.onInput(field.value);
});
}}
/>
</Tooltip>
)}
{!field.readPretty && allowed && (
<Tooltip key={'remove'} title={t('Remove')}>
<CloseOutlined
style={{ zIndex: 1000, color: '#a8a3a3' }}
onClick={() => {
action(() => {
spliceArrayState(field as any, {
startIndex: index,
deleteCount: 1,
}}
/>
</Tooltip>
)}
{!field.readPretty && allowed && (
<Tooltip key={'remove'} title={t('Remove')}>
<CloseOutlined
style={{ zIndex: 1000, color: '#a8a3a3' }}
onClick={() => {
action(() => {
spliceArrayState(field as any, {
startIndex: index,
deleteCount: 1,
});
field.value.splice(index, 1);
return field.onInput(field.value);
});
field.value.splice(index, 1);
return field.onInput(field.value);
});
}}
/>
</Tooltip>
)}
</div>
<RecursionField onlyRenderProperties basePath={field.address.concat(index)} schema={fieldSchema} />
<Divider />
</>
);
})}
</Card>
) : (
<>
{field.editable && allowMultiple && (
<Tooltip key={'add'} title={t('Add new')}>
<Button
type={'default'}
className={css`
border: 1px solid #f0f0f0 !important;
box-shadow: none;
`}
block
icon={<PlusOutlined />}
onClick={() => {
const result = field.value;
result.push({});
field.value = result;
}}
></Button>
</Tooltip>
)}
</>
);
});
}}
/>
</Tooltip>
)}
</div>
<RecursionField onlyRenderProperties basePath={field.address.concat(index)} schema={fieldSchema} />
<Divider />
</>
);
})}
</Card>
) : (
<>
{field.editable && allowMultiple && (
<Tooltip key={'add'} title={t('Add new')}>
<Button
type={'default'}
className={css`
border: 1px solid #f0f0f0 !important;
box-shadow: none;
`}
block
icon={<PlusOutlined />}
onClick={() => {
const result = field.value;
result.push({});
field.value = result;
}}
></Button>
</Tooltip>
)}
</>
);
},
{ displayName: 'ToManyNester' },
);

View File

@ -7,23 +7,29 @@ import { InternalSubTable } from './InternalSubTable';
import { FileManageReadPretty } from './FileManager';
import { useAssociationFieldContext } from './hooks';
const ReadPrettyAssociationField = observer((props: any) => {
const { currentMode } = useAssociationFieldContext();
const ReadPrettyAssociationField = observer(
(props: any) => {
const { currentMode } = useAssociationFieldContext();
return (
<>
{['Select', 'Picker'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <FileManageReadPretty {...props} />}
</>
);
});
return (
<>
{['Select', 'Picker'].includes(currentMode) && <ReadPrettyInternalViewer {...props} />}
{currentMode === 'Nester' && <InternalNester {...props} />}
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
{currentMode === 'FileManager' && <FileManageReadPretty {...props} />}
</>
);
},
{ displayName: 'ReadPrettyAssociationField' },
);
export const ReadPretty = observer((props) => {
return (
<AssociationFieldProvider>
<ReadPrettyAssociationField {...props} />
</AssociationFieldProvider>
);
});
export const ReadPretty = observer(
(props) => {
return (
<AssociationFieldProvider>
<ReadPrettyAssociationField {...props} />
</AssociationFieldProvider>
);
},
{ displayName: 'ReadPretty' },
);

View File

@ -5,24 +5,27 @@ import React from 'react';
import { Table } from '../table-v2/Table';
import { useAssociationFieldContext } from './hooks';
export const SubTable: any = observer((props) => {
const { field } = useAssociationFieldContext<ArrayField>();
return (
<div>
<Table size={'small'} showIndex field={field} pagination={false} />
{field.editable && (
<Button
type={'dashed'}
block
style={{ marginTop: 12 }}
onClick={() => {
field.value = field.value || [];
field.value.push({});
}}
>
Add new
</Button>
)}
</div>
);
});
export const SubTable: any = observer(
(props) => {
const { field } = useAssociationFieldContext<ArrayField>();
return (
<div>
<Table size={'small'} showIndex field={field} pagination={false} />
{field.editable && (
<Button
type={'dashed'}
block
style={{ marginTop: 12 }}
onClick={() => {
field.value = field.value || [];
field.value.push({});
}}
>
Add new
</Button>
)}
</div>
);
},
{ displayName: 'SubTable' },
);

View File

@ -6,34 +6,37 @@ import { ActionContext, useActionContext } from '../../action';
import { useAssociationFieldContext, useInsertSchema } from '../hooks';
import schema from '../schema';
export const CreateRecordAction = observer((props) => {
const field: any = useField();
const fieldSchema = useFieldSchema();
const ctx = useActionContext();
const insertAddNewer = useInsertSchema('AddNewer');
const { options: collectionField } = useAssociationFieldContext();
const [visibleAddNewer, setVisibleAddNewer] = useState(false);
const [currentCollection, setCurrentCollection] = useState(collectionField?.target);
const addbuttonClick = (name) => {
insertAddNewer(schema.AddNewer);
setVisibleAddNewer(true);
setCurrentCollection(name);
};
return (
<CollectionProvider name={collectionField?.target}>
<CreateAction {...props} onClick={(arg) => addbuttonClick(arg)} />
<ActionContext.Provider value={{ ...ctx, visible: visibleAddNewer, setVisible: setVisibleAddNewer }}>
<CollectionProvider name={currentCollection}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.AddNewer';
}}
/>
</CollectionProvider>
</ActionContext.Provider>
</CollectionProvider>
);
});
export const CreateRecordAction = observer(
(props) => {
const field: any = useField();
const fieldSchema = useFieldSchema();
const ctx = useActionContext();
const insertAddNewer = useInsertSchema('AddNewer');
const { options: collectionField } = useAssociationFieldContext();
const [visibleAddNewer, setVisibleAddNewer] = useState(false);
const [currentCollection, setCurrentCollection] = useState(collectionField?.target);
const addbuttonClick = (name) => {
insertAddNewer(schema.AddNewer);
setVisibleAddNewer(true);
setCurrentCollection(name);
};
return (
<CollectionProvider name={collectionField?.target}>
<CreateAction {...props} onClick={(arg) => addbuttonClick(arg)} />
<ActionContext.Provider value={{ ...ctx, visible: visibleAddNewer, setVisible: setVisibleAddNewer }}>
<CollectionProvider name={currentCollection}>
<RecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'AssociationField.AddNewer';
}}
/>
</CollectionProvider>
</ActionContext.Provider>
</CollectionProvider>
);
},
{ displayName: 'CreateRecordAction' },
);

View File

@ -4,8 +4,11 @@ import { useFieldTitle } from '../../hooks';
import { RemoteSelect } from '../remote-select';
import useServiceOptions from './useServiceOptions';
export const ReadPretty = observer((props: any) => {
const service = useServiceOptions(props);
useFieldTitle();
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
});
export const ReadPretty = observer(
(props: any) => {
const service = useServiceOptions(props);
useFieldTitle();
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
},
{ displayName: 'ReadPretty' },
);

View File

@ -3,15 +3,18 @@ import { uid } from '@formily/shared';
import { BlockItem, DndContext, DragHandler, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const Block = observer((props) => {
const fieldSchema = useFieldSchema();
return (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.name}
<DragHandler />
</div>
);
});
const Block = observer(
(props) => {
const fieldSchema = useFieldSchema();
return (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.name}
<DragHandler />
</div>
);
},
{ displayName: 'Block' },
);
export default function App() {
return (

View File

@ -190,71 +190,74 @@ const CalendarRecordViewer = (props) => {
);
};
export const Calendar: any = observer((props: any) => {
const { dataSource, fieldNames, showLunar, fixedBlock } = useProps(props);
const [date, setDate] = useState<Date>(new Date());
const [view, setView] = useState<(typeof Weeks)[number]>('month');
const events = useEvents(dataSource, fieldNames, date, view);
const [visible, setVisible] = useState(false);
const [record, setRecord] = useState<any>({});
export const Calendar: any = observer(
(props: any) => {
const { dataSource, fieldNames, showLunar, fixedBlock } = useProps(props);
const [date, setDate] = useState<Date>(new Date());
const [view, setView] = useState<(typeof Weeks)[number]>('month');
const events = useEvents(dataSource, fieldNames, date, view);
const [visible, setVisible] = useState(false);
const [record, setRecord] = useState<any>({});
const components = useMemo(() => {
return {
toolbar: (props) => <Toolbar {...props} showLunar={showLunar}></Toolbar>,
week: {
header: (props) => <Header {...props} type="week" showLunar={showLunar}></Header>,
},
month: {
dateHeader: (props) => <Header {...props} showLunar={showLunar}></Header>,
},
};
}, [showLunar]);
const components = useMemo(() => {
return {
toolbar: (props) => <Toolbar {...props} showLunar={showLunar}></Toolbar>,
week: {
header: (props) => <Header {...props} type="week" showLunar={showLunar}></Header>,
},
month: {
dateHeader: (props) => <Header {...props} showLunar={showLunar}></Header>,
},
};
}, [showLunar]);
return (
<div style={{ height: fixedBlock ? '100%' : 700 }}>
<CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} />
<BigCalendar
popup
selectable
events={events}
view={view}
views={Weeks}
date={date}
step={60}
showMultiDayTimes
messages={messages}
onNavigate={setDate}
onView={setView}
onSelectSlot={(slotInfo) => {
console.log('onSelectSlot', slotInfo);
}}
onDoubleClickEvent={(event) => {
console.log('onDoubleClickEvent');
}}
onSelectEvent={(event) => {
const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
if (!record) {
return;
}
record.__event = { ...event, start: formatDate(moment(event.start)), end: formatDate(moment(event.end)) };
setRecord(record);
setVisible(true);
}}
formats={{
monthHeaderFormat: 'Y-M',
agendaDateFormat: 'M-DD',
dayHeaderFormat: 'Y-M-DD',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'Y-M', culture);
return (
<div style={{ height: fixedBlock ? '100%' : 700 }}>
<CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} />
<BigCalendar
popup
selectable
events={events}
view={view}
views={Weeks}
date={date}
step={60}
showMultiDayTimes
messages={messages}
onNavigate={setDate}
onView={setView}
onSelectSlot={(slotInfo) => {
console.log('onSelectSlot', slotInfo);
}}
onDoubleClickEvent={(event) => {
console.log('onDoubleClickEvent');
}}
onSelectEvent={(event) => {
const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
if (!record) {
return;
}
return `${local.format(start, 'Y-M', culture)} - ${local.format(end, 'Y-M', culture)}`;
},
}}
components={components}
localizer={localizer}
/>
</div>
);
});
record.__event = { ...event, start: formatDate(moment(event.start)), end: formatDate(moment(event.end)) };
setRecord(record);
setVisible(true);
}}
formats={{
monthHeaderFormat: 'Y-M',
agendaDateFormat: 'M-DD',
dayHeaderFormat: 'Y-M-DD',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'Y-M', culture);
}
return `${local.format(start, 'Y-M', culture)} - ${local.format(end, 'Y-M', culture)}`;
},
}}
components={components}
localizer={localizer}
/>
</div>
);
},
{ displayName: 'Calendar' },
);

View File

@ -11,61 +11,64 @@ import { DeleteEventContext } from './Calendar';
import { formatDate } from './utils';
const { Text } = Typography;
export const DeleteEvent = observer(() => {
const { visible, setVisible } = useActionContext();
const { exclude = [], cron, ...record } = useRecord();
const { close } = useContext(DeleteEventContext);
const startDate = formatDate(moment(record.__parent.__event.start));
const filterByTk = useFilterByTk();
const { resource, service, __parent } = useBlockRequestContext();
const [value, onChange] = useState(startDate);
const [loading, setLoading] = useState(false);
const onOk = async () => {
setLoading(true);
if (value === 'all' || !cron) {
await resource.destroy({
filterByTk,
});
} else {
await resource.update({
filterByTk,
values: {
exclude: (exclude || []).concat(value),
},
});
}
setLoading(false);
__parent?.service?.refresh?.();
service?.refresh?.();
setVisible?.(false, true);
close();
};
export const DeleteEvent = observer(
() => {
const { visible, setVisible } = useActionContext();
const { exclude = [], cron, ...record } = useRecord();
const { close } = useContext(DeleteEventContext);
const startDate = formatDate(moment(record.__parent.__event.start));
const filterByTk = useFilterByTk();
const { resource, service, __parent } = useBlockRequestContext();
const [value, onChange] = useState(startDate);
const [loading, setLoading] = useState(false);
const onOk = async () => {
setLoading(true);
if (value === 'all' || !cron) {
await resource.destroy({
filterByTk,
});
} else {
await resource.update({
filterByTk,
values: {
exclude: (exclude || []).concat(value),
},
});
}
setLoading(false);
__parent?.service?.refresh?.();
service?.refresh?.();
setVisible?.(false, true);
close();
};
const { t } = useTranslation();
return createPortal(
<Modal
title={cron ? t('Delete events') : null}
visible={visible}
onCancel={() => setVisible(false)}
onOk={() => onOk()}
confirmLoading={loading}
>
{cron ? (
<Radio.Group value={value} onChange={(event) => onChange(event.target.value)}>
<Space direction="vertical">
<Radio value={startDate}>{t('This event')}</Radio>
<Radio value={`${startDate}_after`}>{t('This and following events')}</Radio>
<Radio value="all">{t('All events')}</Radio>
</Space>
</Radio.Group>
) : (
<Text strong style={{ fontSize: '18px' }}>
{t('Delete this event?')}
</Text>
)}
</Modal>,
document.body,
);
});
const { t } = useTranslation();
return createPortal(
<Modal
title={cron ? t('Delete events') : null}
visible={visible}
onCancel={() => setVisible(false)}
onOk={() => onOk()}
confirmLoading={loading}
>
{cron ? (
<Radio.Group value={value} onChange={(event) => onChange(event.target.value)}>
<Space direction="vertical">
<Radio value={startDate}>{t('This event')}</Radio>
<Radio value={`${startDate}_after`}>{t('This and following events')}</Radio>
<Radio value="all">{t('All events')}</Radio>
</Space>
</Radio.Group>
) : (
<Text strong style={{ fontSize: '18px' }}>
{t('Delete this event?')}
</Text>
)}
</Modal>,
document.body,
);
},
{ displayName: 'DeleteEvent' },
);
export default DeleteEvent;

View File

@ -1,6 +1,9 @@
import { observer } from '@formily/react';
import React from 'react';
export const Event = observer((props) => {
return <>{props.children}</>;
});
export const Event = observer(
(props) => {
return <>{props.children}</>;
},
{ displayName: 'Event' },
);

View File

@ -6,14 +6,17 @@ import { navigate } from 'react-big-calendar/lib/utils/constants';
import { useDesignable } from '../../hooks';
import { CalendarToolbarContext } from './context';
export const Nav = observer((props) => {
const { DesignableBar } = useDesignable();
const { onNavigate } = useContext(CalendarToolbarContext);
return (
<div className="ant-btn-group">
<Button icon={<LeftOutlined />} onClick={() => onNavigate(navigate.PREVIOUS)}></Button>
<Button icon={<RightOutlined />} onClick={() => onNavigate(navigate.NEXT)}></Button>
<DesignableBar />
</div>
);
});
export const Nav = observer(
(props) => {
const { DesignableBar } = useDesignable();
const { onNavigate } = useContext(CalendarToolbarContext);
return (
<div className="ant-btn-group">
<Button icon={<LeftOutlined />} onClick={() => onNavigate(navigate.PREVIOUS)}></Button>
<Button icon={<RightOutlined />} onClick={() => onNavigate(navigate.NEXT)}></Button>
<DesignableBar />
</div>
);
},
{ displayName: 'Nav' },
);

View File

@ -4,22 +4,25 @@ import { useDesignable } from '../../hooks';
import { CalendarToolbarContext } from './context';
import { getLunarDay } from './utils';
export const Title = observer(() => {
const { DesignableBar } = useDesignable();
const { date, view, label, showLunar } = useContext(CalendarToolbarContext);
export const Title = observer(
() => {
const { DesignableBar } = useDesignable();
const { date, view, label, showLunar } = useContext(CalendarToolbarContext);
const lunarElement = useMemo(() => {
if (!showLunar || view !== 'day') {
return;
}
return <span>{getLunarDay(date)}</span>;
}, [view, date, showLunar]);
const lunarElement = useMemo(() => {
if (!showLunar || view !== 'day') {
return;
}
return <span>{getLunarDay(date)}</span>;
}, [view, date, showLunar]);
return (
<div className="ant-btn-group" style={{ fontSize: '1.75em', fontWeight: 300 }}>
<span>{label}</span>
<span style={{ marginLeft: '4px' }}>{lunarElement}</span>
<DesignableBar />
</div>
);
});
return (
<div className="ant-btn-group" style={{ fontSize: '1.75em', fontWeight: 300 }}>
<span>{label}</span>
<span style={{ marginLeft: '4px' }}>{lunarElement}</span>
<DesignableBar />
</div>
);
},
{ displayName: 'Title' },
);

View File

@ -6,18 +6,21 @@ import { useTranslation } from 'react-i18next';
import { useDesignable } from '../../hooks';
import { CalendarToolbarContext } from './context';
export const Today = observer((props) => {
const { DesignableBar } = useDesignable();
const { onNavigate } = useContext(CalendarToolbarContext);
const { t } = useTranslation();
return (
<Button
onClick={() => {
onNavigate(navigate.TODAY);
}}
>
{t('Today')}
<DesignableBar />
</Button>
);
});
export const Today = observer(
(props) => {
const { DesignableBar } = useDesignable();
const { onNavigate } = useContext(CalendarToolbarContext);
const { t } = useTranslation();
return (
<Button
onClick={() => {
onNavigate(navigate.TODAY);
}}
>
{t('Today')}
<DesignableBar />
</Button>
);
},
{ displayName: 'Today' },
);

View File

@ -4,22 +4,25 @@ import React, { useContext } from 'react';
import { useDesignable } from '../../hooks';
import { CalendarToolbarContext } from './context';
export const ViewSelect = observer((props) => {
const { DesignableBar } = useDesignable();
const {
views,
view,
onView,
localizer: { messages },
} = useContext(CalendarToolbarContext);
return (
<div className="ant-btn-group">
<Select value={view} onChange={onView}>
{views.map((name) => (
<Select.Option value={name}>{messages[name]}</Select.Option>
))}
</Select>
<DesignableBar />
</div>
);
});
export const ViewSelect = observer(
(props) => {
const { DesignableBar } = useDesignable();
const {
views,
view,
onView,
localizer: { messages },
} = useContext(CalendarToolbarContext);
return (
<div className="ant-btn-group">
<Select value={view} onChange={onView}>
{views.map((name) => (
<Select.Option value={name}>{messages[name]}</Select.Option>
))}
</Select>
<DesignableBar />
</div>
);
},
{ displayName: 'ViewSelect' },
);

View File

@ -58,6 +58,6 @@ export const CollectionSelect = connect(
))}
</div>
);
}),
}, { displayName: 'CollectionSelectObserver' }),
),
);

View File

@ -15,35 +15,38 @@ const useDef = (options) => {
return useRequest(() => Promise.resolve({ data: field.dataSource }), options);
};
export const Filter: any = observer((props: any) => {
const { useDataSource = useDef } = props;
const { options, dynamicComponent, className } = useProps(props);
const field = useField<ObjectFieldModel>();
const fieldSchema = useFieldSchema();
useDataSource({
onSuccess(data) {
field.dataSource = data?.data || [];
},
});
return (
<div className={className}>
<DatePickerProvider value={{ utc: false }}>
<FilterContext.Provider
value={{
field,
fieldSchema,
dynamicComponent,
options: options || field.dataSource || [],
disabled: props.disabled,
}}
>
<FilterGroup {...props} bordered={false} />
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
</FilterContext.Provider>
</DatePickerProvider>
</div>
);
});
export const Filter: any = observer(
(props: any) => {
const { useDataSource = useDef } = props;
const { options, dynamicComponent, className } = useProps(props);
const field = useField<ObjectFieldModel>();
const fieldSchema = useFieldSchema();
useDataSource({
onSuccess(data) {
field.dataSource = data?.data || [];
},
});
return (
<div className={className}>
<DatePickerProvider value={{ utc: false }}>
<FilterContext.Provider
value={{
field,
fieldSchema,
dynamicComponent,
options: options || field.dataSource || [],
disabled: props.disabled,
}}
>
<FilterGroup {...props} bordered={false} />
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
</FilterContext.Provider>
</DatePickerProvider>
</div>
);
},
{ displayName: 'Filter' },
);
Filter.SaveDefaultValue = SaveDefaultValue;

View File

@ -11,83 +11,86 @@ import { Action } from '../action';
export const FilterActionContext = createContext<any>(null);
export const FilterAction = observer((props: any) => {
const { t } = useTranslation();
const field = useField<Field>();
const [visible, setVisible] = useState(false);
const { designable, dn } = useDesignable();
const fieldSchema = useFieldSchema();
const form = useMemo<Form>(() => props.form || createForm(), []);
const { options, onSubmit, onReset, ...others } = useProps(props);
return (
<FilterActionContext.Provider value={{ field, fieldSchema, designable, dn }}>
<Popover
destroyTooltipOnHide
placement={'bottomLeft'}
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
trigger={'click'}
content={
<form>
<FormProvider form={form}>
<SchemaComponent
schema={{
type: 'object',
properties: {
filter: {
type: 'string',
enum: options || field.dataSource,
default: fieldSchema.default,
'x-component': 'Filter',
'x-component-props': {},
export const FilterAction = observer(
(props: any) => {
const { t } = useTranslation();
const field = useField<Field>();
const [visible, setVisible] = useState(false);
const { designable, dn } = useDesignable();
const fieldSchema = useFieldSchema();
const form = useMemo<Form>(() => props.form || createForm(), []);
const { options, onSubmit, onReset, ...others } = useProps(props);
return (
<FilterActionContext.Provider value={{ field, fieldSchema, designable, dn }}>
<Popover
destroyTooltipOnHide
placement={'bottomLeft'}
open={visible}
onOpenChange={(visible) => {
setVisible(visible);
}}
trigger={'click'}
content={
<form>
<FormProvider form={form}>
<SchemaComponent
schema={{
type: 'object',
properties: {
filter: {
type: 'string',
enum: options || field.dataSource,
default: fieldSchema.default,
'x-component': 'Filter',
'x-component-props': {},
},
},
},
}}
/>
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
<Space>
<SaveConditions />
<Button
onClick={async () => {
await form.reset();
onReset?.(form.values);
field.title = t('Filter');
setVisible(false);
}}
>
{t('Reset')}
</Button>
<Button
type={'primary'}
htmlType={'submit'}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSubmit?.(form.values);
setVisible(false);
}}
>
{t('Submit')}
</Button>
</Space>
</div>
</FormProvider>
</form>
}
>
<Action {...others} title={field.title} />
</Popover>
</FilterActionContext.Provider>
);
});
}}
/>
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
<Space>
<SaveConditions />
<Button
onClick={async () => {
await form.reset();
onReset?.(form.values);
field.title = t('Filter');
setVisible(false);
}}
>
{t('Reset')}
</Button>
<Button
type={'primary'}
htmlType={'submit'}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSubmit?.(form.values);
setVisible(false);
}}
>
{t('Submit')}
</Button>
</Space>
</div>
</FormProvider>
</form>
}
>
<Action {...others} title={field.title} />
</Popover>
</FilterActionContext.Provider>
);
},
{ displayName: 'FilterAction' },
);
const SaveConditions = () => {
const { fieldSchema, field, designable, dn } = useContext(FilterActionContext);

View File

@ -9,56 +9,59 @@ import { RemoveConditionContext } from './context';
import { DynamicComponent } from './DynamicComponent';
import { useValues } from './useValues';
export const FilterItem = observer((props: any) => {
const { t } = useTranslation();
const compile = useCompile();
const remove = useContext(RemoveConditionContext);
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
return (
<div style={{ marginBottom: 8 }}>
<Space>
<Cascader
className={css`
width: 160px;
`}
fieldNames={{
label: 'title',
value: 'name',
children: 'children',
}}
changeOnSelect={false}
value={dataIndex}
options={compile(fields)}
onChange={(value) => {
setDataIndex(value);
}}
placeholder={t('Select Field')}
/>
<Select
className={css`
min-width: 110px;
`}
value={operator?.value}
options={compile(operators)}
onChange={(value) => {
setOperator(value);
}}
placeholder={t('Comparision')}
/>
{!operator?.noValue &&
React.createElement(DynamicComponent, {
value: value ? value : undefined,
schema,
onChange(value) {
setValue(value);
},
})}
{!props.disabled && (
<a>
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
</a>
)}
</Space>
</div>
);
});
export const FilterItem = observer(
(props: any) => {
const { t } = useTranslation();
const compile = useCompile();
const remove = useContext(RemoveConditionContext);
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
return (
<div style={{ marginBottom: 8 }}>
<Space>
<Cascader
className={css`
width: 160px;
`}
fieldNames={{
label: 'title',
value: 'name',
children: 'children',
}}
changeOnSelect={false}
value={dataIndex}
options={compile(fields)}
onChange={(value) => {
setDataIndex(value);
}}
placeholder={t('Select Field')}
/>
<Select
className={css`
min-width: 110px;
`}
value={operator?.value}
options={compile(operators)}
onChange={(value) => {
setOperator(value);
}}
placeholder={t('Comparision')}
/>
{!operator?.noValue &&
React.createElement(DynamicComponent, {
value: value ? value : undefined,
schema,
onChange(value) {
setValue(value);
},
})}
{!props.disabled && (
<a>
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
</a>
)}
</Space>
</div>
);
},
{ displayName: 'FilterItem' },
);

View File

@ -5,17 +5,20 @@ import { RemoveConditionContext } from './context';
import { FilterGroup } from './FilterGroup';
import { FilterItem } from './FilterItem';
export const FilterItems = observer((props) => {
const field = useField<ArrayFieldModel>();
return (
<div>
{field?.value?.map((item, index) => {
return (
<RemoveConditionContext.Provider key={index} value={() => field.remove(index)}>
<ObjectField name={index} component={[item.$and || item.$or ? FilterGroup : FilterItem]} />
</RemoveConditionContext.Provider>
);
})}
</div>
);
});
export const FilterItems = observer(
(props) => {
const field = useField<ArrayFieldModel>();
return (
<div>
{field?.value?.map((item, index) => {
return (
<RemoveConditionContext.Provider key={index} value={() => field.remove(index)}>
<ObjectField name={index} component={[item.$and || item.$or ? FilterGroup : FilterItem]} />
</RemoveConditionContext.Provider>
);
})}
</div>
);
},
{ displayName: 'FilterItems' },
);

View File

@ -53,61 +53,64 @@ const divWrap = (schema: ISchema) => {
};
};
export const FormItem: any = observer((props: any) => {
useEnsureOperatorsValid();
export const FormItem: any = observer(
(props: any) => {
useEnsureOperatorsValid();
const field = useField<Field>();
const ctx = useContext(BlockRequestContext);
const schema = useFieldSchema();
const variablesCtx = useVariablesCtx();
const field = useField<Field>();
const ctx = useContext(BlockRequestContext);
const schema = useFieldSchema();
const variablesCtx = useVariablesCtx();
useEffect(() => {
if (ctx?.block === 'form') {
ctx.field.data = ctx.field.data || {};
ctx.field.data.activeFields = ctx.field.data.activeFields || new Set();
ctx.field.data.activeFields.add(schema.name);
// 如果默认值是一个变量,则需要解析之后再显示出来
if (isVariable(schema?.default)) {
field.setInitialValue?.(parseVariables(schema.default, variablesCtx));
useEffect(() => {
if (ctx?.block === 'form') {
ctx.field.data = ctx.field.data || {};
ctx.field.data.activeFields = ctx.field.data.activeFields || new Set();
ctx.field.data.activeFields.add(schema.name);
// 如果默认值是一个变量,则需要解析之后再显示出来
if (isVariable(schema?.default)) {
field.setInitialValue?.(parseVariables(schema.default, variablesCtx));
}
}
}
}, []);
const showTitle = schema['x-decorator-props']?.showTitle ?? true;
return (
<ACLCollectionFieldProvider>
<BlockItem className={'nb-form-item'}>
<Item
className={cx(
css`
& .ant-space {
flex-wrap: wrap;
}
`,
{
[css`
> .ant-formily-item-label {
display: none;
}, []);
const showTitle = schema['x-decorator-props']?.showTitle ?? true;
return (
<ACLCollectionFieldProvider>
<BlockItem className={'nb-form-item'}>
<Item
className={cx(
css`
& .ant-space {
flex-wrap: wrap;
}
`]: showTitle === false,
},
)}
{...props}
extra={
typeof field.description === 'string' ? (
<div
dangerouslySetInnerHTML={{
__html: HTMLEncode(field.description).split('\n').join('<br/>'),
}}
/>
) : (
field.description
)
}
/>
</BlockItem>
</ACLCollectionFieldProvider>
);
});
`,
{
[css`
> .ant-formily-item-label {
display: none;
}
`]: showTitle === false,
},
)}
{...props}
extra={
typeof field.description === 'string' ? (
<div
dangerouslySetInnerHTML={{
__html: HTMLEncode(field.description).split('\n').join('<br/>'),
}}
/>
) : (
field.description
)
}
/>
</BlockItem>
</ACLCollectionFieldProvider>
);
},
{ displayName: 'FormItem' },
);
FormItem.Designer = function Designer() {
const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager();

View File

@ -170,21 +170,24 @@ export const Form: React.FC<FormProps> & {
FilterDesigner?: any;
ReadPrettyDesigner?: any;
Templates?: any;
} = observer((props) => {
const field = useField<Field>();
const { form, disabled, ...others } = useProps(props);
const formDisabled = disabled || field.disabled;
return (
<ConfigProvider componentDisabled={formDisabled}>
<form>
<Spin spinning={field.loading || false}>
{form ? (
<WithForm form={form} {...others} disabled={formDisabled} />
) : (
<WithoutForm {...others} disabled={formDisabled} />
)}
</Spin>
</form>
</ConfigProvider>
);
});
} = observer(
(props) => {
const field = useField<Field>();
const { form, disabled, ...others } = useProps(props);
const formDisabled = disabled || field.disabled;
return (
<ConfigProvider componentDisabled={formDisabled}>
<form>
<Spin spinning={field.loading || false}>
{form ? (
<WithForm form={form} {...others} disabled={formDisabled} />
) : (
<WithoutForm {...others} disabled={formDisabled} />
)}
</Spin>
</form>
</ConfigProvider>
);
},
{ displayName: 'Form' },
);

View File

@ -4,21 +4,24 @@ import { useFormBlockContext } from '../../../block-provider';
import { useCollection } from '../../../collection-manager';
import { useCompile } from '../../hooks';
export const FormField: any = observer((props) => {
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const field = useField();
const collectionField = getField(fieldSchema.name);
const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => {
if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title);
}
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []);
return <div>{props.children}</div>;
});
export const FormField: any = observer(
(props) => {
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const field = useField();
const collectionField = getField(fieldSchema.name);
const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => {
if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title);
}
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []);
return <div>{props.children}</div>;
},
{ displayName: 'FormField' },
);

View File

@ -83,34 +83,37 @@ const useDefaultValues = (opts: any = {}, props: FormProps = {}) => {
const FormBlockContext = createContext<any>(null);
export const Form: React.FC<FormProps> & { Designer?: any } = observer((props) => {
const { request, effects, initialValue, useValues = useDefaultValues, ...others } = props;
const fieldSchema = useFieldSchema();
const field = useField();
const form = useMemo(() => createForm({ effects }), []);
const result = useValues(
{
uid: fieldSchema['x-uid'],
async onSuccess(data) {
await form.reset();
form.setValues(data?.data);
export const Form: React.FC<FormProps> & { Designer?: any } = observer(
(props) => {
const { request, effects, initialValue, useValues = useDefaultValues, ...others } = props;
const fieldSchema = useFieldSchema();
const field = useField();
const form = useMemo(() => createForm({ effects }), []);
const result = useValues(
{
uid: fieldSchema['x-uid'],
async onSuccess(data) {
await form.reset();
form.setValues(data?.data);
},
},
},
props,
);
const parent = useContext(FormBlockContext);
return (
<FormBlockContext.Provider value={{ parent, form, result, field, fieldSchema }}>
<Spin spinning={result?.loading || false}>
{fieldSchema['x-decorator'] === 'Form' ? (
<FormDecorator form={form} {...others} />
) : (
<FormComponent form={form} {...others} />
)}
</Spin>
</FormBlockContext.Provider>
);
});
props,
);
const parent = useContext(FormBlockContext);
return (
<FormBlockContext.Provider value={{ parent, form, result, field, fieldSchema }}>
<Spin spinning={result?.loading || false}>
{fieldSchema['x-decorator'] === 'Form' ? (
<FormDecorator form={form} {...others} />
) : (
<FormComponent form={form} {...others} />
)}
</Spin>
</FormBlockContext.Provider>
);
},
{ displayName: 'Form' },
);
Form.Designer = () => {
const { name, title } = useCollection();

View File

@ -49,10 +49,13 @@ export default observer(() => {
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
const Output = observer(
() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
},
{ displayName: 'Output' },
);
return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>

View File

@ -38,10 +38,13 @@ export default observer(() => {
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
const Output = observer(
() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
},
{ displayName: 'Output' },
);
const useSubmit = () => {
const form = useForm();

View File

@ -38,10 +38,13 @@ export default observer(() => {
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
const Output = observer(
() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
},
{ displayName: 'Output' },
);
const useSubmit = () => {
const form = useForm();

View File

@ -54,10 +54,13 @@ export default observer(() => {
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
const Output = observer(
() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
},
{ displayName: 'Output' },
);
return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Output, Form, Action, Input, FormItem }}>

View File

@ -69,10 +69,13 @@ export default observer(() => {
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
const Output = observer(
() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
},
{ displayName: 'Output' },
);
const useSubmit = () => {
const form = useForm();

View File

@ -39,10 +39,13 @@ export default observer(() => {
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
const Output = observer(
() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
},
{ displayName: 'Output' },
);
const useSubmit = () => {
const form = useForm();

View File

@ -36,10 +36,13 @@ const schema: ISchema = {
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
const Output = observer(
() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
},
{ displayName: 'Output' },
);
const useSubmit = () => {
const form = useForm();

View File

@ -127,51 +127,54 @@ export const G2PlotRenderer = forwardRef(function <O = any>(props: ReactG2PlotPr
return <div className={cls(['g2plot', className])} ref={containerRef} />;
});
export const G2Plot: any = observer((props: any) => {
const { plot, config } = props;
const field = useField<Field>();
const { t } = useTranslation();
const api = useAPIClient();
useEffect(() => {
field.data = field.data || {};
field.data.loading = true;
const fn = config?.data;
if (typeof fn === 'function') {
const result = fn.bind({ api })();
if (result?.then) {
result.then((data) => {
if (Array.isArray(data)) {
field.componentProps.config.data = data;
}
export const G2Plot: any = observer(
(props: any) => {
const { plot, config } = props;
const field = useField<Field>();
const { t } = useTranslation();
const api = useAPIClient();
useEffect(() => {
field.data = field.data || {};
field.data.loading = true;
const fn = config?.data;
if (typeof fn === 'function') {
const result = fn.bind({ api })();
if (result?.then) {
result.then((data) => {
if (Array.isArray(data)) {
field.componentProps.config.data = data;
}
field.data.loading = false;
});
} else {
field.data.loading = false;
});
}
} else {
field.data.loading = false;
}
} else {
field.data.loading = false;
}
}, []);
}, []);
if (!plot || !config) {
return <div style={{ opacity: 0.3 }}>{t('In configuration')}...</div>;
}
if (field?.data?.loading !== false) {
return <Spin />;
}
return (
<div>
{field.title && <h2>{field.title}</h2>}
<G2PlotRenderer
plot={plots[plot]}
config={{
...config,
data: Array.isArray(config?.data) ? config.data : [],
}}
/>
</div>
);
});
if (!plot || !config) {
return <div style={{ opacity: 0.3 }}>{t('In configuration')}...</div>;
}
if (field?.data?.loading !== false) {
return <Spin />;
}
return (
<div>
{field.title && <h2>{field.title}</h2>}
<G2PlotRenderer
plot={plots[plot]}
config={{
...config,
data: Array.isArray(config?.data) ? config.data : [],
}}
/>
</div>
);
},
{ displayName: 'G2Plot' },
);
G2Plot.Designer = G2PlotDesigner;
G2Plot.plots = plots;

View File

@ -1,6 +1,9 @@
import { observer } from '@formily/react';
import React from 'react';
export const Event = observer((props) => {
return <>{props.children}</>;
});
export const Event = observer(
(props) => {
return <>{props.children}</>;
},
{ displayName: 'Event' },
);

View File

@ -2,12 +2,15 @@ import { observer, useFieldSchema } from '@formily/react';
import React from 'react';
import { DragHandler } from '../../';
export const Block = observer((props) => {
const fieldSchema = useFieldSchema();
return (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.title}
<DragHandler />
</div>
);
});
export const Block = observer(
(props) => {
const fieldSchema = useFieldSchema();
return (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.title}
<DragHandler />
</div>
);
},
{ displayName: 'Block' },
);

View File

@ -330,45 +330,108 @@ export const useGridRowContext = () => {
return useContext(GridRowContext);
};
export const Grid: any = observer((props: any) => {
const gridRef = useRef(null);
const field = useField();
const fieldSchema = useFieldSchema();
const { render, InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
const addr = field.address.toString();
const rows = useRowProperties();
const { setPrintContent } = useFormBlockContext();
export const Grid: any = observer(
(props: any) => {
const gridRef = useRef(null);
const field = useField();
const fieldSchema = useFieldSchema();
const { render, InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
const addr = field.address.toString();
const rows = useRowProperties();
const { setPrintContent } = useFormBlockContext();
useEffect(() => {
gridRef.current && setPrintContent?.(gridRef.current);
}, [gridRef.current]);
useEffect(() => {
gridRef.current && setPrintContent?.(gridRef.current);
}, [gridRef.current]);
return (
<GridContext.Provider value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent }}>
<div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
<DndWrapper dndContext={props.dndContext}>
<RowDivider
rows={rows}
return (
<GridContext.Provider
value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent }}
>
<div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
<DndWrapper dndContext={props.dndContext}>
<RowDivider
rows={rows}
first
id={`${addr}_0`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
insertAdjacent: 'afterBegin',
schema: fieldSchema,
}}
/>
{rows.map((schema, index) => {
return (
<React.Fragment key={index}>
<RecursionField name={schema.name} schema={schema} />
<RowDivider
rows={rows}
index={index}
id={`${addr}_${index + 1}`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
insertAdjacent: 'afterEnd',
schema,
}}
/>
</React.Fragment>
);
})}
</DndWrapper>
<InitializerComponent />
</div>
</GridContext.Provider>
);
},
{ displayName: 'Grid' },
);
Grid.Row = observer(
() => {
const field = useField();
const fieldSchema = useFieldSchema();
const addr = field.address.toString();
const cols = useColProperties();
return (
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
<div
className={cls(
'nb-grid-row',
css`
overflow-x: hidden;
margin: 0 calc(-1 * var(--nb-spacing));
display: flex;
position: relative;
/* z-index: 0; */
`,
)}
>
<ColDivider
cols={cols}
first
id={`${addr}_0`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterBegin',
schema: fieldSchema,
}}
/>
{rows.map((schema, index) => {
{cols.map((schema, index) => {
return (
<React.Fragment key={index}>
<RecursionField name={schema.name} schema={schema} />
<RowDivider
rows={rows}
<ColDivider
cols={cols}
index={index}
last={index === cols.length - 1}
id={`${addr}_${index + 1}`}
data={{
breakRemoveOn: breakRemoveOnGrid,
wrapSchema: wrapRowSchema,
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterEnd',
schema,
}}
@ -376,95 +439,43 @@ export const Grid: any = observer((props: any) => {
</React.Fragment>
);
})}
</DndWrapper>
<InitializerComponent />
</div>
</GridContext.Provider>
);
});
</div>
</GridRowContext.Provider>
);
},
{ displayName: 'Grid.Row' },
);
Grid.Row = observer(() => {
const field = useField();
const fieldSchema = useFieldSchema();
const addr = field.address.toString();
const cols = useColProperties();
Grid.Col = observer(
(props: any) => {
const { cols = [] } = useContext(GridRowContext);
const schema = useFieldSchema();
const field = useField();
return (
<GridRowContext.Provider value={{ schema: fieldSchema, cols }}>
<div
className={cls(
'nb-grid-row',
css`
overflow-x: hidden;
margin: 0 calc(-1 * var(--nb-spacing));
display: flex;
position: relative;
/* z-index: 0; */
`,
)}
>
<ColDivider
cols={cols}
first
id={`${addr}_0`}
data={{
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterBegin',
schema: fieldSchema,
}}
/>
{cols.map((schema, index) => {
return (
<React.Fragment key={index}>
<RecursionField name={schema.name} schema={schema} />
<ColDivider
cols={cols}
index={index}
last={index === cols.length - 1}
id={`${addr}_${index + 1}`}
data={{
breakRemoveOn: breakRemoveOnRow,
wrapSchema: wrapColSchema,
insertAdjacent: 'afterEnd',
schema,
}}
/>
</React.Fragment>
);
})}
</div>
</GridRowContext.Provider>
);
});
const width = useMemo(() => {
let width = '';
if (cols?.length) {
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
width = `calc(${w}% - var(--nb-spacing) * ${(cols.length + 1) / cols.length})`;
}
return width;
}, [cols?.length, schema?.['x-component-props']?.['width']]);
Grid.Col = observer((props: any) => {
const { cols = [] } = useContext(GridRowContext);
const schema = useFieldSchema();
const field = useField();
const width = useMemo(() => {
let width = '';
if (cols?.length) {
const w = schema?.['x-component-props']?.['width'] || 100 / cols.length;
width = `calc(${w}% - var(--nb-spacing) * ${(cols.length + 1) / cols.length})`;
}
return width;
}, [cols?.length, schema?.['x-component-props']?.['width']]);
const { setNodeRef } = useDroppable({
id: field.address.toString(),
data: {
insertAdjacent: 'beforeEnd',
schema,
wrapSchema: (s) => s,
},
});
return (
<GridColContext.Provider value={{ cols, schema }}>
<div ref={setNodeRef} style={{ width }} className={cls('nb-grid-col')}>
{props.children}
</div>
</GridColContext.Provider>
);
});
const { setNodeRef } = useDroppable({
id: field.address.toString(),
data: {
insertAdjacent: 'beforeEnd',
schema,
wrapSchema: (s) => s,
},
});
return (
<GridColContext.Provider value={{ cols, schema }}>
<div ref={setNodeRef} style={{ width }} className={cls('nb-grid-col')}>
{props.children}
</div>
</GridColContext.Provider>
);
},
{ displayName: 'Grid.Col' },
);

View File

@ -2,15 +2,18 @@ import { observer, useFieldSchema } from '@formily/react';
import { BlockItem, DragHandler, Grid, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const Block = observer((props) => {
const fieldSchema = useFieldSchema();
return (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.title}
<DragHandler />
</div>
);
});
const Block = observer(
(props) => {
const fieldSchema = useFieldSchema();
return (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.title}
<DragHandler />
</div>
);
},
{ displayName: 'Block' },
);
const schema = {
_isJSONSchemaObject: true,

View File

@ -10,98 +10,101 @@ import { RecordProvider } from '../../../record-provider';
import { SchemaComponentOptions } from '../../core/SchemaComponentOptions';
import { KanbanCardContext } from './context';
export const KanbanCard: any = observer((props: any) => {
const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } =
useContext(KanbanCardContext);
const fieldSchema = useFieldSchema();
const [visible, setVisible] = useState(false);
const labelDisabled = fieldSchema['x-label-disabled'];
export const KanbanCard: any = observer(
(props: any) => {
const { setDisableCardDrag, cardViewerSchema, card, cardField, columnIndex, cardIndex } =
useContext(KanbanCardContext);
const fieldSchema = useFieldSchema();
const [visible, setVisible] = useState(false);
const labelDisabled = fieldSchema['x-label-disabled'];
return (
<SchemaComponentOptions components={{}} scope={{}}>
<Card
onClick={(e) => {
setVisible(true);
}}
bordered={false}
hoverable
style={{ cursor: 'pointer', overflow: 'hidden' }}
// bodyStyle={{ paddingBottom: 0 }}
className={cls(
css`
.ant-card-body {
padding: 16px;
}
.nb-row-divider {
height: 16px;
margin-top: -16px;
&:last-child {
margin-top: 0;
return (
<SchemaComponentOptions components={{}} scope={{}}>
<Card
onClick={(e) => {
setVisible(true);
}}
bordered={false}
hoverable
style={{ cursor: 'pointer', overflow: 'hidden' }}
// bodyStyle={{ paddingBottom: 0 }}
className={cls(
css`
.ant-card-body {
padding: 16px;
}
}
.ant-description-input {
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
}
.ant-description-textarea {
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
}
.ant-formily-item {
margin-bottom: 12px;
}
.nb-grid-row:last-of-type {
.nb-grid-col {
.nb-form-item:last-of-type {
.ant-formily-item {
margin-bottom: 0;
.nb-row-divider {
height: 16px;
margin-top: -16px;
&:last-child {
margin-top: 0;
}
}
.ant-description-input {
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
}
.ant-description-textarea {
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
}
.ant-formily-item {
margin-bottom: 12px;
}
.nb-grid-row:last-of-type {
.nb-grid-col {
.nb-form-item:last-of-type {
.ant-formily-item {
margin-bottom: 0;
}
}
}
}
}
`,
{
'kanban-no-label': labelDisabled,
},
`,
{
'kanban-no-label': labelDisabled,
},
)}
>
<DndContext
onDragStart={() => {
setDisableCardDrag(true);
}}
onDragEnd={() => {
setDisableCardDrag(false);
}}
>
<FormLayout layout={'vertical'}>
<RecursionField
basePath={cardField.address.concat(`${columnIndex}.cards.${cardIndex}`)}
schema={fieldSchema}
onlyRenderProperties
/>
</FormLayout>
</DndContext>
</Card>
{cardViewerSchema && (
<ActionContext.Provider
value={{
openMode: fieldSchema['x-component-props']?.['openMode'] || 'drawer',
openSize: fieldSchema['x-component-props']?.['openSize'],
visible,
setVisible,
}}
>
<RecordProvider record={card}>
<RecursionField
basePath={cardField.address.concat(`${columnIndex}.cardViewer.${cardIndex}`)}
schema={cardViewerSchema}
onlyRenderProperties
/>
</RecordProvider>
</ActionContext.Provider>
)}
>
<DndContext
onDragStart={() => {
setDisableCardDrag(true);
}}
onDragEnd={() => {
setDisableCardDrag(false);
}}
>
<FormLayout layout={'vertical'}>
<RecursionField
basePath={cardField.address.concat(`${columnIndex}.cards.${cardIndex}`)}
schema={fieldSchema}
onlyRenderProperties
/>
</FormLayout>
</DndContext>
</Card>
{cardViewerSchema && (
<ActionContext.Provider
value={{
openMode: fieldSchema['x-component-props']?.['openMode'] || 'drawer',
openSize: fieldSchema['x-component-props']?.['openSize'],
visible,
setVisible,
}}
>
<RecordProvider record={card}>
<RecursionField
basePath={cardField.address.concat(`${columnIndex}.cardViewer.${cardIndex}`)}
schema={cardViewerSchema}
onlyRenderProperties
/>
</RecordProvider>
</ActionContext.Provider>
)}
</SchemaComponentOptions>
);
});
</SchemaComponentOptions>
);
},
{ displayName: 'KanbanCard' },
);

View File

@ -1,5 +1,8 @@
import { observer } from '@formily/react';
export const KanbanCardViewer: any = observer((props: any) => {
return props.children;
});
export const KanbanCardViewer: any = observer(
(props: any) => {
return props.children;
},
{ displayName: 'KanbanCardViewer' },
);

View File

@ -55,103 +55,106 @@ export const toColumns = (groupField: any, dataSource: Array<any> = []) => {
return Object.values(columns);
};
export const Kanban: any = observer((props: any) => {
const { groupField, onCardDragEnd, ...restProps } = useProps(props);
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const [disableCardDrag, setDisableCardDrag] = useState(false);
const schemas = useMemo(
() =>
fieldSchema.reduceProperties(
(buf, current) => {
if (current['x-component'].endsWith('.Card')) {
buf.card = current;
} else if (current['x-component'].endsWith('.CardAdder')) {
buf.cardAdder = current;
} else if (current['x-component'].endsWith('.CardViewer')) {
buf.cardViewer = current;
}
return buf;
},
{ card: null, cardAdder: null, cardViewer: null },
),
[],
);
const handleCardRemove = (card, column) => {
const updatedBoard = Board.removeCard({ columns: field.value }, column, card);
field.value = updatedBoard.columns;
};
const handleCardDragEnd = (card, fromColumn, toColumn) => {
onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn);
const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn);
field.value = updatedBoard.columns;
};
export const Kanban: any = observer(
(props: any) => {
const { groupField, onCardDragEnd, ...restProps } = useProps(props);
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const [disableCardDrag, setDisableCardDrag] = useState(false);
const schemas = useMemo(
() =>
fieldSchema.reduceProperties(
(buf, current) => {
if (current['x-component'].endsWith('.Card')) {
buf.card = current;
} else if (current['x-component'].endsWith('.CardAdder')) {
buf.cardAdder = current;
} else if (current['x-component'].endsWith('.CardViewer')) {
buf.cardViewer = current;
}
return buf;
},
{ card: null, cardAdder: null, cardViewer: null },
),
[],
);
const handleCardRemove = (card, column) => {
const updatedBoard = Board.removeCard({ columns: field.value }, column, card);
field.value = updatedBoard.columns;
};
const handleCardDragEnd = (card, fromColumn, toColumn) => {
onCardDragEnd?.({ columns: field.value, groupField }, fromColumn, toColumn);
const updatedBoard = Board.moveCard({ columns: field.value }, fromColumn, toColumn);
field.value = updatedBoard.columns;
};
return (
<Spin
wrapperClassName={css`
overflow: hidden;
height: 100%;
> .ant-spin-container {
return (
<Spin
wrapperClassName={css`
overflow: hidden;
height: 100%;
}
`}
spinning={field.loading || false}
>
<Board
{...restProps}
allowAddCard={!!schemas.cardAdder}
disableColumnDrag
cardAdderPosition={'bottom'}
disableCardDrag={restProps.disableCardDrag || disableCardDrag}
onCardRemove={handleCardRemove}
onCardDragEnd={handleCardDragEnd}
renderColumnHeader={({ title, color }) => (
<div className={'react-kanban-column-header'}>
<Tag color={color}>{title}</Tag>
</div>
)}
renderCard={(card, { column, dragging }) => {
const columnIndex = field.value?.indexOf(column);
const cardIndex = column?.cards?.indexOf(card);
return (
schemas.card && (
<RecordProvider record={card}>
<KanbanCardContext.Provider
value={{
setDisableCardDrag,
cardViewerSchema: schemas.cardViewer,
cardField: field,
card,
column,
dragging,
columnIndex,
cardIndex,
}}
>
<RecursionField name={schemas.card.name} schema={schemas.card} />
</KanbanCardContext.Provider>
</RecordProvider>
)
);
}}
renderCardAdder={({ column }) => {
if (!schemas.cardAdder) {
return null;
> .ant-spin-container {
height: 100%;
}
return (
<KanbanColumnContext.Provider value={{ column, groupField }}>
<SchemaComponentOptions scope={{ useCreateActionProps }}>
<RecursionField name={schemas.cardAdder.name} schema={schemas.cardAdder} />
</SchemaComponentOptions>
</KanbanColumnContext.Provider>
);
}}
`}
spinning={field.loading || false}
>
{{
columns: field.value || [],
}}
</Board>
</Spin>
);
});
<Board
{...restProps}
allowAddCard={!!schemas.cardAdder}
disableColumnDrag
cardAdderPosition={'bottom'}
disableCardDrag={restProps.disableCardDrag || disableCardDrag}
onCardRemove={handleCardRemove}
onCardDragEnd={handleCardDragEnd}
renderColumnHeader={({ title, color }) => (
<div className={'react-kanban-column-header'}>
<Tag color={color}>{title}</Tag>
</div>
)}
renderCard={(card, { column, dragging }) => {
const columnIndex = field.value?.indexOf(column);
const cardIndex = column?.cards?.indexOf(card);
return (
schemas.card && (
<RecordProvider record={card}>
<KanbanCardContext.Provider
value={{
setDisableCardDrag,
cardViewerSchema: schemas.cardViewer,
cardField: field,
card,
column,
dragging,
columnIndex,
cardIndex,
}}
>
<RecursionField name={schemas.card.name} schema={schemas.card} />
</KanbanCardContext.Provider>
</RecordProvider>
)
);
}}
renderCardAdder={({ column }) => {
if (!schemas.cardAdder) {
return null;
}
return (
<KanbanColumnContext.Provider value={{ column, groupField }}>
<SchemaComponentOptions scope={{ useCreateActionProps }}>
<RecursionField name={schemas.cardAdder.name} schema={schemas.cardAdder} />
</SchemaComponentOptions>
</KanbanColumnContext.Provider>
);
}}
>
{{
columns: field.value || [],
}}
</Board>
</Spin>
);
},
{ displayName: 'Kanban' },
);

View File

@ -41,44 +41,47 @@ const MarkdownEditor = (props: any) => {
);
};
export const MarkdownVoid: any = observer((props: any) => {
const { content, className } = props;
const field = useField();
const schema = useFieldSchema();
const { dn } = useDesignable();
const { onSave, onCancel } = props;
const { html, loading } = useParseMarkdown(content);
if (loading) {
return <Spin />;
}
return field?.editable ? (
<MarkdownEditor
{...props}
className
defaultValue={content}
onCancel={() => {
field.editable = false;
onCancel?.();
}}
onSubmit={async (value) => {
field.editable = false;
schema['x-component-props'] ?? (schema['x-component-props'] = {});
schema['x-component-props']['content'] = value;
field.componentProps.content = value;
onSave?.(schema);
dn.emit('patch', {
schema: {
'x-uid': schema['x-uid'],
'x-component-props': {
content: value,
export const MarkdownVoid: any = observer(
(props: any) => {
const { content, className } = props;
const field = useField();
const schema = useFieldSchema();
const { dn } = useDesignable();
const { onSave, onCancel } = props;
const { html, loading } = useParseMarkdown(content);
if (loading) {
return <Spin />;
}
return field?.editable ? (
<MarkdownEditor
{...props}
className
defaultValue={content}
onCancel={() => {
field.editable = false;
onCancel?.();
}}
onSubmit={async (value) => {
field.editable = false;
schema['x-component-props'] ?? (schema['x-component-props'] = {});
schema['x-component-props']['content'] = value;
field.componentProps.content = value;
onSave?.(schema);
dn.emit('patch', {
schema: {
'x-uid': schema['x-uid'],
'x-component-props': {
content: value,
},
},
},
});
}}
/>
) : (
<div className={cls(['nb-markdown', className])} dangerouslySetInnerHTML={{ __html: html }} />
);
});
});
}}
/>
) : (
<div className={cls(['nb-markdown', className])} dangerouslySetInnerHTML={{ __html: html }} />
);
},
{ displayName: 'MarkdownVoid' },
);
MarkdownVoid.Designer = MarkdownVoidDesigner;

View File

@ -23,24 +23,27 @@ const schema = {
},
};
const Editable = observer((props: any) => {
const filed = useField<any>();
if (filed.editable) {
return props.children;
}
return (
<div>
<Button
onClick={() => {
filed.editable = true;
}}
>
</Button>
<div>{props.children}</div>
</div>
);
});
const Editable = observer(
(props: any) => {
const filed = useField<any>();
if (filed.editable) {
return props.children;
}
return (
<div>
<Button
onClick={() => {
filed.editable = true;
}}
>
</Button>
<div>{props.children}</div>
</div>
);
},
{ displayName: 'Editable' },
);
export default () => {
return (

View File

@ -136,148 +136,149 @@ const useSideMenuRef = () => {
const MenuItemDesignerContext = createContext(null);
export const Menu: ComposedMenu = observer((props) => {
const {
onSelect,
mode,
selectedUid,
defaultSelectedUid,
sideMenuRefScopeKey,
defaultSelectedKeys: dSelectedKeys,
defaultOpenKeys: dOpenKeys,
...others
} = useProps(props);
const { t } = useTranslation();
const Designer = useDesigner();
const schema = useFieldSchema();
const { refresh } = useDesignable();
const api = useAPIClient();
const { render } = useSchemaInitializer(schema['x-initializer']);
const sideMenuRef = useSideMenuRef();
const [defaultSelectedKeys, setDefaultSelectedKeys] = useState(() => {
if (dSelectedKeys) {
return dSelectedKeys;
}
if (defaultSelectedUid) {
return findKeysByUid(schema, defaultSelectedUid);
}
return [];
});
const [loading, setLoading] = useState(false);
const [defaultOpenKeys, setDefaultOpenKeys] = useState(() => {
if (['inline', 'mix'].includes(mode)) {
return dOpenKeys || defaultSelectedKeys;
}
return dOpenKeys;
});
const [sideMenuSchema, setSideMenuSchema] = useState<Schema>(() => {
const key = defaultSelectedKeys?.[0] || null;
if (mode === 'mix' && key) {
const s = schema.properties?.[key];
if (s['x-component'] === 'Menu.SubMenu') {
return s;
export const Menu: ComposedMenu = observer(
(props) => {
const {
onSelect,
mode,
selectedUid,
defaultSelectedUid,
sideMenuRefScopeKey,
defaultSelectedKeys: dSelectedKeys,
defaultOpenKeys: dOpenKeys,
...others
} = useProps(props);
const { t } = useTranslation();
const Designer = useDesigner();
const schema = useFieldSchema();
const { refresh } = useDesignable();
const api = useAPIClient();
const { render } = useSchemaInitializer(schema['x-initializer']);
const sideMenuRef = useSideMenuRef();
const [defaultSelectedKeys, setDefaultSelectedKeys] = useState(() => {
if (dSelectedKeys) {
return dSelectedKeys;
}
}
return null;
});
useEffect(() => {
const keys = findKeysByUid(schema, selectedUid);
setDefaultSelectedKeys(keys);
if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(dOpenKeys || keys);
}
const key = keys?.[0] || null;
if (mode === 'mix') {
if (key) {
if (defaultSelectedUid) {
return findKeysByUid(schema, defaultSelectedUid);
}
return [];
});
const [loading, setLoading] = useState(false);
const [defaultOpenKeys, setDefaultOpenKeys] = useState(() => {
if (['inline', 'mix'].includes(mode)) {
return dOpenKeys || defaultSelectedKeys;
}
return dOpenKeys;
});
const [sideMenuSchema, setSideMenuSchema] = useState<Schema>(() => {
const key = defaultSelectedKeys?.[0] || null;
if (mode === 'mix' && key) {
const s = schema.properties?.[key];
if (s['x-component'] === 'Menu.SubMenu') {
setSideMenuSchema(s);
return s;
}
} else {
setSideMenuSchema(null);
}
}
}, [selectedUid]);
useEffect(() => {
if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(defaultSelectedKeys);
}
}, [defaultSelectedKeys]);
useEffect(() => {
const sideMenuElement = sideMenuRef?.current as HTMLElement;
if (!sideMenuElement) {
return;
}
sideMenuElement.style.display = sideMenuSchema?.['x-component'] === 'Menu.SubMenu' ? 'block' : 'none';
}, [sideMenuSchema?.name, sideMenuRef]);
const { designable } = useDesignable();
return (
<DndContext>
<MenuItemDesignerContext.Provider value={Designer}>
<MenuModeContext.Provider value={mode}>
<AntdMenu
{...others}
style={
{
// width: mode === 'mix' ? '100%' : undefined,
}
}
className={css`
.ant-menu-item:hover {
> .ant-menu-title-content > div {
.general-schema-designer {
display: block;
}
return null;
});
useEffect(() => {
const keys = findKeysByUid(schema, selectedUid);
setDefaultSelectedKeys(keys);
if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(dOpenKeys || keys);
}
const key = keys?.[0] || null;
if (mode === 'mix') {
if (key) {
const s = schema.properties?.[key];
if (s['x-component'] === 'Menu.SubMenu') {
setSideMenuSchema(s);
}
} else {
setSideMenuSchema(null);
}
}
}, [selectedUid]);
useEffect(() => {
if (['inline', 'mix'].includes(mode)) {
setDefaultOpenKeys(defaultSelectedKeys);
}
}, [defaultSelectedKeys]);
useEffect(() => {
const sideMenuElement = sideMenuRef?.current as HTMLElement;
if (!sideMenuElement) {
return;
}
sideMenuElement.style.display = sideMenuSchema?.['x-component'] === 'Menu.SubMenu' ? 'block' : 'none';
}, [sideMenuSchema?.name, sideMenuRef]);
const { designable } = useDesignable();
return (
<DndContext>
<MenuItemDesignerContext.Provider value={Designer}>
<MenuModeContext.Provider value={mode}>
<AntdMenu
{...others}
style={
{
// width: mode === 'mix' ? '100%' : undefined,
}
}
`}
onSelect={(info: any) => {
const s = schema.properties[info.key];
if (mode === 'mix') {
setSideMenuSchema(s);
if (s['x-component'] !== 'Menu.SubMenu') {
onSelect && onSelect(info);
} else {
const menuItemSchema = findMenuItem(s);
if (!menuItemSchema) {
return;
className={css`
.ant-menu-item:hover {
> .ant-menu-title-content > div {
.general-schema-designer {
display: block;
}
}
// TODO
setLoading(true);
const keys = findKeysByUid(schema, menuItemSchema['x-uid']);
setDefaultSelectedKeys(keys);
setTimeout(() => {
setLoading(false);
}, 100);
onSelect &&
onSelect({
key: menuItemSchema.name,
item: {
props: {
schema: menuItemSchema,
}
`}
onSelect={(info: any) => {
const s = schema.properties[info.key];
if (mode === 'mix') {
setSideMenuSchema(s);
if (s['x-component'] !== 'Menu.SubMenu') {
onSelect && onSelect(info);
} else {
const menuItemSchema = findMenuItem(s);
if (!menuItemSchema) {
return;
}
// TODO
setLoading(true);
const keys = findKeysByUid(schema, menuItemSchema['x-uid']);
setDefaultSelectedKeys(keys);
setTimeout(() => {
setLoading(false);
}, 100);
onSelect &&
onSelect({
key: menuItemSchema.name,
item: {
props: {
schema: menuItemSchema,
},
},
},
});
});
}
} else {
onSelect && onSelect(info);
}
} else {
onSelect && onSelect(info);
}
}}
mode={mode === 'mix' ? 'horizontal' : mode}
defaultOpenKeys={defaultOpenKeys}
defaultSelectedKeys={defaultSelectedKeys}
selectedKeys={defaultSelectedKeys}
>
{designable && (
<AntdMenu.Item disabled key="x-designer-button" style={{ padding: '0 8px', order: 9999 }}>
{render({ style: { background: 'none' } })}
</AntdMenu.Item>
)}
{props.children}
</AntdMenu>
{loading
? null
: mode === 'mix' &&
}}
mode={mode === 'mix' ? 'horizontal' : mode}
defaultOpenKeys={defaultOpenKeys}
defaultSelectedKeys={defaultSelectedKeys}
selectedKeys={defaultSelectedKeys}
>
{designable && (
<AntdMenu.Item disabled key="x-designer-button" style={{ padding: '0 8px', order: 9999 }}>
{render({ style: { background: 'none' } })}
</AntdMenu.Item>
)}
{props.children}
</AntdMenu>
{loading
? null
: mode === 'mix' &&
sideMenuSchema?.['x-component'] === 'Menu.SubMenu' &&
sideMenuRef?.current?.firstChild &&
createPortal(
@ -290,37 +291,37 @@ export const Menu: ComposedMenu = observer((props) => {
onSelect && onSelect(info);
}}
className={css`
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.ant-menu-item {
> .ant-menu-title-content {
margin-left: -24px;
margin-right: -16px;
padding: 0 16px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.ant-menu-item {
> .ant-menu-title-content {
margin-left: -24px;
margin-right: -16px;
padding: 0 16px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
}
}
}
}
}
.ant-menu-submenu-title {
.ant-menu-title-content {
margin-left: -24px;
margin-right: -34px;
padding: 0 34px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
}
> span.anticon {
margin-right: 10px;
.ant-menu-submenu-title {
.ant-menu-title-content {
margin-left: -24px;
margin-right: -34px;
padding: 0 34px 0 24px;
> div {
> .general-schema-designer {
right: 6px !important;
}
> span.anticon {
margin-right: 10px;
}
}
}
}
}
`}
`}
>
<RecursionField schema={sideMenuSchema} onlyRenderProperties />
{render({
@ -340,117 +341,128 @@ export const Menu: ComposedMenu = observer((props) => {
</MenuModeContext.Provider>,
sideMenuRef.current.firstChild,
)}
</MenuModeContext.Provider>
</MenuItemDesignerContext.Provider>
</DndContext>
);
});
</MenuModeContext.Provider>
</MenuItemDesignerContext.Provider>
</DndContext>
);
},
{ displayName: 'Menu' },
);
Menu.Item = observer((props) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const field = useField();
const Designer = useContext(MenuItemDesignerContext);
return (
<AntdMenu.Item
{...others}
className={css`
:active {
background: inherit;
}
`}
key={schema.name}
eventKey={schema.name}
schema={schema}
>
<SortableItem className={designerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} />
<span
className={css`
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
width: 100%;
vertical-align: middle;
`}
>
{field.title}
</span>
<Designer />
</SortableItem>
</AntdMenu.Item>
);
});
Menu.URL = observer((props) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const field = useField();
const Designer = useContext(MenuItemDesignerContext);
return (
<AntdMenu.Item
{...others}
className={css`
:active {
background: inherit;
}
`}
key={schema.name}
eventKey={schema.name}
schema={schema}
onClick={() => {
window.open(props.href, '_blank');
}}
>
<SortableItem className={designerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} />
<span
className={css`
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
width: 100%;
vertical-align: middle;
`}
>
{field.title}
</span>
<Designer />
</SortableItem>
</AntdMenu.Item>
);
});
Menu.SubMenu = observer((props) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const field = useField();
const mode = useContext(MenuModeContext);
const Designer = useContext(MenuItemDesignerContext);
if (mode === 'mix') {
return <Menu.Item {...props} />;
}
return (
<AntdMenu.SubMenu
{...others}
className={css`
:active {
background: inherit;
}
`}
key={schema.name}
eventKey={schema.name}
title={
<SortableItem className={subMenuDesignerCss} removeParentsIfNoChildren={false}>
Menu.Item = observer(
(props) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const field = useField();
const Designer = useContext(MenuItemDesignerContext);
return (
<AntdMenu.Item
{...others}
className={css`
:active {
background: inherit;
}
`}
key={schema.name}
eventKey={schema.name}
schema={schema}
>
<SortableItem className={designerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} />
{field.title}
<span
className={css`
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
width: 100%;
vertical-align: middle;
`}
>
{field.title}
</span>
<Designer />
</SortableItem>
}
>
<RecursionField schema={schema} onlyRenderProperties />
</AntdMenu.SubMenu>
);
});
</AntdMenu.Item>
);
},
{ displayName: 'MenuItem' },
);
Menu.URL = observer(
(props) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const field = useField();
const Designer = useContext(MenuItemDesignerContext);
return (
<AntdMenu.Item
{...others}
className={css`
:active {
background: inherit;
}
`}
key={schema.name}
eventKey={schema.name}
schema={schema}
onClick={() => {
window.open(props.href, '_blank');
}}
>
<SortableItem className={designerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} />
<span
className={css`
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
width: 100%;
vertical-align: middle;
`}
>
{field.title}
</span>
<Designer />
</SortableItem>
</AntdMenu.Item>
);
},
{ displayName: 'MenuURL' },
);
Menu.SubMenu = observer(
(props) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const field = useField();
const mode = useContext(MenuModeContext);
const Designer = useContext(MenuItemDesignerContext);
if (mode === 'mix') {
return <Menu.Item {...props} />;
}
return (
<AntdMenu.SubMenu
{...others}
className={css`
:active {
background: inherit;
}
`}
key={schema.name}
eventKey={schema.name}
title={
<SortableItem className={subMenuDesignerCss} removeParentsIfNoChildren={false}>
<Icon type={icon} />
{field.title}
<Designer />
</SortableItem>
}
>
<RecursionField schema={schema} onlyRenderProperties />
</AntdMenu.SubMenu>
);
},
{ displayName: 'MenuSubMenu' },
);
Menu.Designer = MenuDesigner;

View File

@ -3,20 +3,23 @@ import { Pagination as AntdPagination } from 'antd';
import React, { KeyboardEventHandler } from 'react';
import { useProps } from '../../hooks/useProps';
export const Pagination = observer((props: any) => {
const { hidden, ...others } = useProps(props);
if (hidden) {
return null;
}
const onKeypress: KeyboardEventHandler<HTMLDivElement> = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
export const Pagination = observer(
(props: any) => {
const { hidden, ...others } = useProps(props);
if (hidden) {
return null;
}
};
const onKeypress: KeyboardEventHandler<HTMLDivElement> = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
}
};
return (
<div onKeyPress={onKeypress}>
<AntdPagination {...others} />
</div>
);
});
return (
<div onKeyPress={onKeypress}>
<AntdPagination {...others} />
</div>
);
},
{ displayName: 'Pagination' },
);

View File

@ -25,98 +25,101 @@ const toValue = (value, placeholder) => {
return value;
};
export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
const { ellipsis } = props;
const fieldSchema = useFieldSchema();
const recordCtx = useRecord();
const { getCollectionJoinField } = useCollectionManager();
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
// const field = useField<Field>();
const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false);
const { getField } = useCollection();
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
const [record, setRecord] = useState({});
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const showFilePicker = isShowFilePicker(labelUiSchema);
const { snapshot } = useActionContext();
const isTagsMode = fieldSchema['x-component-props']?.mode === 'tags';
export const ReadPrettyRecordPicker: React.FC = observer(
(props: any) => {
const { ellipsis } = props;
const fieldSchema = useFieldSchema();
const recordCtx = useRecord();
const { getCollectionJoinField } = useCollectionManager();
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
// const field = useField<Field>();
const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false);
const { getField } = useCollection();
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
const [record, setRecord] = useState({});
const compile = useCompile();
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const showFilePicker = isShowFilePicker(labelUiSchema);
const { snapshot } = useActionContext();
const isTagsMode = fieldSchema['x-component-props']?.mode === 'tags';
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
if (showFilePicker) {
return collectionField ? <Preview {...props} /> : null;
}
if (showFilePicker) {
return collectionField ? <Preview {...props} /> : null;
}
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
const text = getLabelFormatValue(labelUiSchema, val, true);
return (
<Fragment key={`${record.id}_${index}`}>
<span>
{snapshot || isTagsMode ? (
text
) : (
<a
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setVisible(true);
setRecord(record);
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>
{text}
</a>
)}
</span>
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
</Fragment>
);
});
const renderRecords = () =>
toArr(props.value).map((record, index, arr) => {
const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A');
const text = getLabelFormatValue(labelUiSchema, val, true);
return (
<Fragment key={`${record.id}_${index}`}>
<span>
{snapshot || isTagsMode ? (
text
) : (
<a
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setVisible(true);
setRecord(record);
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
}}
>
{text}
</a>
)}
</span>
{index < arr.length - 1 ? <span style={{ marginRight: 4, color: '#aaa' }}>,</span> : null}
</Fragment>
);
});
const renderWithoutTableFieldResourceProvider = () => (
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === 'RecordPicker.Viewer';
}}
/>
</FormProvider>
</WithoutTableFieldResource.Provider>
);
const renderRecordProvider = () => {
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
return collectionFieldNames && collectionFieldNames.length > 2 ? (
<RecordProvider record={recordCtx[collectionFieldNames[1]]}>
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
</RecordProvider>
) : (
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
const renderWithoutTableFieldResourceProvider = () => (
<WithoutTableFieldResource.Provider value={true}>
<FormProvider>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => {
return s['x-component'] === 'RecordPicker.Viewer';
}}
/>
</FormProvider>
</WithoutTableFieldResource.Provider>
);
};
return collectionField ? (
<div>
<BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}>
<CollectionProvider name={collectionField.target ?? collectionField.targetCollection}>
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
{renderRecords()}
</EllipsisWithTooltip>
<ActionContext.Provider
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
>
{renderRecordProvider()}
</ActionContext.Provider>
</CollectionProvider>
</BlockAssociationContext.Provider>
</div>
) : null;
});
const renderRecordProvider = () => {
const collectionFieldNames = fieldSchema?.['x-collection-field']?.split('.');
return collectionFieldNames && collectionFieldNames.length > 2 ? (
<RecordProvider record={recordCtx[collectionFieldNames[1]]}>
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
</RecordProvider>
) : (
<RecordProvider record={record}>{renderWithoutTableFieldResourceProvider()}</RecordProvider>
);
};
return collectionField ? (
<div>
<BlockAssociationContext.Provider value={`${collectionField.collectionName}.${collectionField.name}`}>
<CollectionProvider name={collectionField.target ?? collectionField.targetCollection}>
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
{renderRecords()}
</EllipsisWithTooltip>
<ActionContext.Provider
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
>
{renderRecordProvider()}
</ActionContext.Provider>
</CollectionProvider>
</BlockAssociationContext.Provider>
</div>
) : null;
},
{ displayName: 'ReadPrettyRecordPicker' },
);

View File

@ -6,19 +6,20 @@ import { useRequest } from '../../../api-client';
import { useRecord } from '../../../record-provider';
import { useActionContext } from '../action';
export const ReadPretty = observer((props: any) => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const field = useField<any>();
const fieldSchema = useFieldSchema();
const record = useRecord();
const { snapshot } = useActionContext();
export const ReadPretty = observer(
(props: any) => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const field = useField<any>();
const fieldSchema = useFieldSchema();
const record = useRecord();
const { snapshot } = useActionContext();
const { data } = useRequest(
snapshot
? async () => ({
const { data } = useRequest(
snapshot
? async () => ({
data: record[fieldSchema.name],
})
: {
: {
action: 'list',
...props.service,
params: {
@ -30,10 +31,12 @@ export const ReadPretty = observer((props: any) => {
},
},
},
{
refreshDeps: [props.service, field.value],
},
);
{
refreshDeps: [props.service, field.value],
},
);
return <Select.ReadPretty {...props} options={data?.data}></Select.ReadPretty>;
});
return <Select.ReadPretty {...props} options={data?.data}></Select.ReadPretty>;
},
{ displayName: 'ReadPretty' },
);

View File

@ -12,29 +12,32 @@ type Composed = {
Object?: React.FC<any>;
};
export const ReadPretty = observer((props: any) => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const field = useField<any>();
const compile = useCompile();
export const ReadPretty = observer(
(props: any) => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const field = useField<any>();
const compile = useCompile();
if (!isValid(props.value)) {
return <div />;
}
if (isArrayField(field) && field?.value?.length === 0) {
return <div />;
}
const dataSource = field.dataSource || props.options || [];
const options = getCurrentOptions(field.value, dataSource, fieldNames);
if (!isValid(props.value)) {
return <div />;
}
if (isArrayField(field) && field?.value?.length === 0) {
return <div />;
}
const dataSource = field.dataSource || props.options || [];
const options = getCurrentOptions(field.value, dataSource, fieldNames);
return (
<div>
<EllipsisWithTooltip ellipsis={props.ellipsis}>
{options.map((option, key) => (
<Tag key={key} color={option[fieldNames.color]} icon={option.icon}>
{compile(option[fieldNames.label])}
</Tag>
))}
</EllipsisWithTooltip>
</div>
);
});
return (
<div>
<EllipsisWithTooltip ellipsis={props.ellipsis}>
{options.map((option, key) => (
<Tag key={key} color={option[fieldNames.color]} icon={option.icon}>
{compile(option[fieldNames.label])}
</Tag>
))}
</EllipsisWithTooltip>
</div>
);
},
{ displayName: 'ReadPretty' },
);

View File

@ -42,12 +42,15 @@ export const designerCss = css`
}
`;
export const TableColumnActionBar = observer((props) => {
const Designer = useDesigner();
return (
<SortableItem className={designerCss}>
<Designer />
{props.children}
</SortableItem>
);
});
export const TableColumnActionBar = observer(
(props) => {
const Designer = useDesigner();
return (
<SortableItem className={designerCss}>
<Designer />
{props.children}
</SortableItem>
);
},
{ displayName: 'TableColumnActionBar' },
);

View File

@ -163,164 +163,165 @@ const useValidator = (validator: (value: any) => string) => {
}, []);
};
export const Table: any = observer((props: any) => {
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
const {
dragSort = false,
showIndex = true,
onRowSelectionChange,
onChange: onTableChange,
rowSelection,
rowKey,
required,
onExpand,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
const columns = useTableColumns(others);
const schema = useFieldSchema();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const { expandFlag } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
const paginationProps = usePaginationProps(pagination1, pagination2);
const requiredValidator = field.required || required;
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
const [expandedKeys, setExpandesKeys] = useState([]);
const [allIncludesChildren, setAllIncludesChildren] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]);
export const Table: any = observer(
(props: any) => {
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
const {
dragSort = false,
showIndex = true,
onRowSelectionChange,
onChange: onTableChange,
rowSelection,
rowKey,
required,
onExpand,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
const columns = useTableColumns(others);
const schema = useFieldSchema();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const { expandFlag } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => { }));
const paginationProps = usePaginationProps(pagination1, pagination2);
const requiredValidator = field.required || required;
const { treeTable } = schema?.parent?.['x-decorator-props'] || {};
const [expandedKeys, setExpandesKeys] = useState([]);
const [allIncludesChildren, setAllIncludesChildren] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]);
let onRow = null,
highlightRow = '';
let onRow = null,
highlightRow = '';
if (onClickRow) {
onRow = (record) => {
return {
onClick: () => onClickRow(record, setSelectedRow, selectedRow),
if (onClickRow) {
onRow = (record) => {
return {
onClick: () => onClickRow(record, setSelectedRow, selectedRow),
};
};
};
highlightRow = css`
& > td {
background-color: #caedff !important;
}
&:hover > td {
background-color: #caedff !important;
}
`;
}
useEffect(() => {
field.setValidator((value) => {
if (requiredValidator) {
return Array.isArray(value) && value.length > 0 ? null : 'The field value is required';
}
return;
});
}, [requiredValidator]);
useEffect(() => {
if (treeTable !== false) {
const keys = getIdsWithChildren(field.value?.slice?.());
setAllIncludesChildren(keys);
highlightRow = css`
& > td {
background-color: #caedff !important;
}
&:hover > td {
background-color: #caedff !important;
}
`;
}
}, [field.value]);
useEffect(() => {
if (expandFlag) {
setExpandesKeys(allIncludesChildren);
} else {
setExpandesKeys([]);
}
}, [expandFlag, allIncludesChildren]);
const components = useMemo(() => {
return {
header: {
wrapper: (props) => {
return (
<DndContext>
<thead {...props} />
</DndContext>
);
useEffect(() => {
field.setValidator((value) => {
if (requiredValidator) {
return Array.isArray(value) && value.length > 0 ? null : 'The field value is required';
}
return;
});
}, [requiredValidator]);
useEffect(() => {
if (treeTable !== false) {
const keys = getIdsWithChildren(field.value?.slice?.());
setAllIncludesChildren(keys);
}
}, [field.value]);
useEffect(() => {
if (expandFlag) {
setExpandesKeys(allIncludesChildren);
} else {
setExpandesKeys([]);
}
}, [expandFlag, allIncludesChildren]);
const components = useMemo(() => {
return {
header: {
wrapper: (props) => {
return (
<DndContext>
<thead {...props} />
</DndContext>
);
},
cell: (props) => {
return (
<th
{...props}
className={cls(
props.className,
css`
max-width: 300px;
white-space: nowrap;
&:hover .general-schema-designer {
display: block;
}
`,
)}
/>
);
},
},
cell: (props) => {
return (
<th
body: {
wrapper: (props) => {
return (
<DndContext
onDragEnd={(e) => {
if (!e.active || !e.over) {
console.warn('move cancel');
return;
}
const fromIndex = e.active?.data.current?.sortable?.index;
const toIndex = e.over?.data.current?.sortable?.index;
const from = field.value[fromIndex];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
onRowDragEnd({ fromIndex, toIndex, from, to });
}}
>
<tbody {...props} />
</DndContext>
);
},
row: (props) => {
return <SortableRow {...props}></SortableRow>;
},
cell: (props) => (
<td
{...props}
className={cls(
className={classNames(
props.className,
css`
max-width: 300px;
white-space: nowrap;
&:hover .general-schema-designer {
display: block;
.nb-read-pretty-input-number {
text-align: right;
}
`,
)}
/>
);
),
},
},
body: {
wrapper: (props) => {
return (
<DndContext
onDragEnd={(e) => {
if (!e.active || !e.over) {
console.warn('move cancel');
return;
}
};
}, [field, onRowDragEnd, dragSort]);
const fromIndex = e.active?.data.current?.sortable?.index;
const toIndex = e.over?.data.current?.sortable?.index;
const from = field.value[fromIndex];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
onRowDragEnd({ fromIndex, toIndex, from, to });
}}
>
<tbody {...props} />
</DndContext>
);
},
row: (props) => {
return <SortableRow {...props}></SortableRow>;
},
cell: (props) => (
<td
{...props}
className={classNames(
props.className,
css`
max-width: 300px;
white-space: nowrap;
.nb-read-pretty-input-number {
text-align: right;
}
`,
)}
/>
),
},
const defaultRowKey = (record: any) => {
return field.value?.indexOf?.(record);
};
}, [field, onRowDragEnd, dragSort]);
const defaultRowKey = (record: any) => {
return field.value?.indexOf?.(record);
};
const getRowKey = (record: any) => {
if (typeof rowKey === 'string') {
return record[rowKey]?.toString();
} else {
return (rowKey ?? defaultRowKey)(record)?.toString();
}
};
const getRowKey = (record: any) => {
if (typeof rowKey === 'string') {
return record[rowKey]?.toString();
} else {
return (rowKey ?? defaultRowKey)(record)?.toString();
}
};
const restProps = {
rowSelection: rowSelection
? {
const restProps = {
rowSelection: rowSelection
? {
type: 'checkbox',
selectedRowKeys: selectedRowKeys,
onChange(selectedRowKeys: any[], selectedRows: any[]) {
@ -348,40 +349,40 @@ export const Table: any = observer((props: any) => {
className={classNames(
checked ? 'checked' : null,
css`
position: relative;
display: flex;
float: left;
align-items: center;
justify-content: space-evenly;
padding-right: 8px;
.nb-table-index {
opacity: 0;
}
&:not(.checked) {
.nb-table-index {
opacity: 1;
}
}
&:hover {
position: relative;
display: flex;
float: left;
align-items: center;
justify-content: space-evenly;
padding-right: 8px;
.nb-table-index {
opacity: 0;
}
.nb-origin-node {
display: block;
&:not(.checked) {
.nb-table-index {
opacity: 1;
}
}
}
`,
&:hover {
.nb-table-index {
opacity: 0;
}
.nb-origin-node {
display: block;
}
}
`,
)}
>
<div
className={classNames(
checked ? 'checked' : null,
css`
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
`,
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
`,
)}
>
{dragSort && <SortHandle id={getRowKey(record)} />}
@ -392,13 +393,13 @@ export const Table: any = observer((props: any) => {
'nb-origin-node',
checked ? 'checked' : null,
css`
position: absolute;
right: 50%;
transform: translateX(50%);
&:not(.checked) {
display: none;
}
`,
position: absolute;
right: 50%;
transform: translateX(50%);
&:not(.checked) {
display: none;
}
`,
)}
>
{originNode}
@ -408,94 +409,96 @@ export const Table: any = observer((props: any) => {
},
...rowSelection,
}
: undefined,
};
: undefined,
};
const SortableWrapper = useCallback<React.FC>(
({ children }) => {
return dragSort
? React.createElement(SortableContext, {
const SortableWrapper = useCallback<React.FC>(
({ children }) => {
return dragSort
? React.createElement(SortableContext, {
items: field.value.map(getRowKey),
children: children,
})
: React.createElement(React.Fragment, {
: React.createElement(React.Fragment, {
children,
});
},
[field, dragSort],
);
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
},
[field, dragSort],
);
const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const scroll = useMemo(() => {
return fixedBlock
? {
const scroll = useMemo(() => {
return fixedBlock
? {
x: 'max-content',
y: tableHeight,
}
: {
: {
x: 'max-content',
};
}, [fixedBlock, tableHeight]);
}, [fixedBlock, tableHeight]);
return (
<div
className={css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
return (
<div
className={css`
height: 100%;
.ant-spin-nested-loading {
overflow: hidden;
.ant-table-wrapper {
height: 100%;
.ant-spin-container {
.ant-spin-nested-loading {
height: 100%;
display: flex;
flex-direction: column;
.ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
}
}
}
}
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<SortableWrapper>
<AntdTable
ref={tableSizeRefCallback}
rowKey={rowKey ?? defaultRowKey}
dataSource={field?.value?.slice?.()}
{...others}
{...restProps}
pagination={paginationProps}
components={components}
onChange={(pagination, filters, sorter, extra) => {
onTableChange?.(pagination, filters, sorter, extra);
}}
onRow={onRow}
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
tableLayout={'auto'}
scroll={scroll}
columns={columns}
expandable={{
onExpand: (flag, record) => {
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
setExpandesKeys(newKeys);
onExpand?.(flag, record);
},
expandedRowKeys: expandedKeys,
}}
/>
</SortableWrapper>
{field.errors.length > 0 && (
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
{field.errors.map((error) => {
return error.messages.map((message) => <div>{message}</div>);
})}
</div>
)}
</div>
);
});
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<SortableWrapper>
<AntdTable
ref={tableSizeRefCallback}
rowKey={rowKey ?? defaultRowKey}
dataSource={field?.value?.slice?.()}
{...others}
{...restProps}
pagination={paginationProps}
components={components}
onChange={(pagination, filters, sorter, extra) => {
onTableChange?.(pagination, filters, sorter, extra);
}}
onRow={onRow}
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
tableLayout={'auto'}
scroll={scroll}
columns={columns}
expandable={{
onExpand: (flag, record) => {
const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i);
setExpandesKeys(newKeys);
onExpand?.(flag, record);
},
expandedRowKeys: expandedKeys,
}}
/>
</SortableWrapper>
{field.errors.length > 0 && (
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
{field.errors.map((error) => {
return error.messages.map((message) => <div>{message}</div>);
})}
</div>
)}
</div>
);
},
{ displayName: 'Table' },
);

View File

@ -6,36 +6,42 @@ import { useCollection } from '../../../collection-manager';
import { useCompile } from '../../hooks';
import { ActionBar } from '../action';
export const TableField: any = observer((props) => {
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const field = useField<Field>();
const collectionField = getField(fieldSchema.name);
const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => {
if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title);
}
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []);
useEffect(() => {
field.decoratorProps.asterisk = fieldSchema.required;
}, [fieldSchema.required]);
return <div>{props.children}</div>;
});
export const TableField: any = observer(
(props) => {
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const field = useField<Field>();
const collectionField = getField(fieldSchema.name);
const compile = useCompile();
const ctx = useFormBlockContext();
useEffect(() => {
if (!field.title) {
field.title = compile(collectionField?.uiSchema?.title);
}
if (ctx?.field) {
ctx.field.added = ctx.field.added || new Set();
ctx.field.added.add(fieldSchema.name);
}
}, []);
useEffect(() => {
field.decoratorProps.asterisk = fieldSchema.required;
}, [fieldSchema.required]);
return <div>{props.children}</div>;
},
{ displayName: 'TableField' },
);
TableField.ActionBar = observer((props) => {
const form = useForm();
if (form.readPretty) {
return null;
}
return (
<div style={{ marginBottom: 8, marginTop: -32 }}>
<ActionBar {...props} />
</div>
);
});
TableField.ActionBar = observer(
(props) => {
const form = useForm();
if (form.readPretty) {
return null;
}
return (
<div style={{ marginBottom: 8, marginTop: -32 }}>
<ActionBar {...props} />
</div>
);
},
{ displayName: 'TableField.ActionBar' },
);

View File

@ -4,27 +4,30 @@ import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import { Table, TableColumnType } from 'antd';
import React from 'react';
const ArrayTable = observer((props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const ArrayTable = observer(
(props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
return <Table {...props} rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
return <Table {...props} rowKey={rowKey} columns={columns} dataSource={field.value} />;
},
{ displayName: 'ArrayTable' },
);
const Value = connect((props) => {
return <li>value: {props.value}</li>;

View File

@ -151,28 +151,29 @@ const useDefAction = () => {
};
};
export const TableArray: React.FC<any> = observer((props) => {
const field = useField<ArrayField>();
const columns = useTableColumns();
const {
dragSort = false,
showIndex = true,
useSelectedRowKeys = useDef,
useDataSource = useDefDataSource,
useAction = useDefAction,
onChange,
...others
} = props;
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
useDataSource({
onSuccess(data) {
field.value = data?.data || [];
},
});
const { move } = useAction();
const restProps = {
rowSelection: props.rowSelection
? {
export const TableArray: React.FC<any> = observer(
(props) => {
const field = useField<ArrayField>();
const columns = useTableColumns();
const {
dragSort = false,
showIndex = true,
useSelectedRowKeys = useDef,
useDataSource = useDefDataSource,
useAction = useDefAction,
onChange,
...others
} = props;
const [selectedRowKeys, setSelectedRowKeys] = useSelectedRowKeys();
useDataSource({
onSuccess(data) {
field.value = data?.data || [];
},
});
const { move } = useAction();
const restProps = {
rowSelection: props.rowSelection
? {
type: 'checkbox',
selectedRowKeys,
onChange(selectedRowKeys: any[]) {
@ -189,39 +190,39 @@ export const TableArray: React.FC<any> = observer((props) => {
className={classNames(
checked ? 'checked' : null,
css`
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
padding-right: 8px;
.nb-table-index {
opacity: 0;
}
&:not(.checked) {
.nb-table-index {
opacity: 1;
}
}
&:hover {
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
padding-right: 8px;
.nb-table-index {
opacity: 0;
}
.nb-origin-node {
display: block;
&:not(.checked) {
.nb-table-index {
opacity: 1;
}
}
}
`,
&:hover {
.nb-table-index {
opacity: 0;
}
.nb-origin-node {
display: block;
}
}
`,
)}
>
<div
className={classNames(
checked ? 'checked' : null,
css`
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
`,
position: relative;
display: flex;
align-items: center;
justify-content: space-evenly;
`,
)}
>
{dragSort && <SortHandle />}
@ -232,13 +233,13 @@ export const TableArray: React.FC<any> = observer((props) => {
'nb-origin-node',
checked ? 'checked' : null,
css`
position: absolute;
right: 50%;
transform: translateX(50%);
&:not(.checked) {
display: none;
}
`,
position: absolute;
right: 50%;
transform: translateX(50%);
&:not(.checked) {
display: none;
}
`,
)}
>
{originNode}
@ -248,43 +249,45 @@ export const TableArray: React.FC<any> = observer((props) => {
},
...props.rowSelection,
}
: undefined,
};
: undefined,
};
const defaultRowKey = (record: any) => {
return field.value?.indexOf?.(record);
};
const defaultRowKey = (record: any) => {
return field.value?.indexOf?.(record);
};
return (
<div
className={css`
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<ReactDragListView
handleSelector={'.drag-handle'}
onDragEnd={async (fromIndex, toIndex) => {
const from = field.value[fromIndex];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
await move(from, to);
}}
lineClassName={css`
border-bottom: 2px solid rgba(241, 139, 98, 0.6) !important;
return (
<div
className={css`
.ant-table {
overflow-x: auto;
overflow-y: hidden;
}
`}
>
<Table
rowKey={defaultRowKey}
{...others}
{...restProps}
components={components}
columns={columns}
dataSource={field?.value?.slice?.()}
/>
</ReactDragListView>
</div>
);
});
<ReactDragListView
handleSelector={'.drag-handle'}
onDragEnd={async (fromIndex, toIndex) => {
const from = field.value[fromIndex];
const to = field.value[toIndex];
field.move(fromIndex, toIndex);
await move(from, to);
}}
lineClassName={css`
border-bottom: 2px solid rgba(241, 139, 98, 0.6) !important;
`}
>
<Table
rowKey={defaultRowKey}
{...others}
{...restProps}
components={components}
columns={columns}
dataSource={field?.value?.slice?.()}
/>
</ReactDragListView>
</div>
);
},
{ displayName: 'TableArray' },
);

View File

@ -42,12 +42,15 @@ export const designerCss = css`
}
`;
export const TableColumnActionBar = observer((props) => {
const Designer = useDesigner();
return (
<SortableItem className={designerCss}>
<Designer />
{props.children}
</SortableItem>
);
});
export const TableColumnActionBar = observer(
(props) => {
const Designer = useDesigner();
return (
<SortableItem className={designerCss}>
<Designer />
{props.children}
</SortableItem>
);
},
{ displayName: 'TableColumnActionBar' },
);

View File

@ -9,20 +9,23 @@ type Props = TableProps<any> & { value?: any; onChange?: any; objectValue?: bool
const toArr = (value: any) => (isArr(value) ? value : isValid(value) ? [value] : []);
export const TableRowSelection = observer((props: Props) => {
const { rowKey = 'id', objectValue } = props;
const field = useField<Field>();
const rowSelection: any = {
type: 'checkbox',
...props.rowSelection,
selectedRowKeys: toArr(field.value).map((val) => (typeof val === 'object' ? val[rowKey as any] : val)),
onChange(selectedRowKeys: any[], selectedRows?: any) {
if (rowSelection.type === 'checkbox') {
props.onChange(objectValue ? selectedRows : selectedRowKeys);
} else {
props.onChange([...(objectValue ? selectedRows : selectedRowKeys)].shift());
}
},
};
return <TableVoid {...props} rowSelection={rowSelection} />;
});
export const TableRowSelection = observer(
(props: Props) => {
const { rowKey = 'id', objectValue } = props;
const field = useField<Field>();
const rowSelection: any = {
type: 'checkbox',
...props.rowSelection,
selectedRowKeys: toArr(field.value).map((val) => (typeof val === 'object' ? val[rowKey as any] : val)),
onChange(selectedRowKeys: any[], selectedRows?: any) {
if (rowSelection.type === 'checkbox') {
props.onChange(objectValue ? selectedRows : selectedRowKeys);
} else {
props.onChange([...(objectValue ? selectedRows : selectedRowKeys)].shift());
}
},
};
return <TableVoid {...props} rowSelection={rowSelection} />;
},
{ displayName: 'TableRowSelection' },
);

View File

@ -80,45 +80,48 @@ const useDefSelectedRowKeys = () => {
return [result?.state?.selectedRowKeys, (selectedRowKeys) => result?.setState?.({ selectedRowKeys })];
};
export const TableVoid: React.FC<TableVoidProps> = observer((props) => {
const { rowKey = 'id', useDataSource = useDef, useSelectedRowKeys = useDefSelectedRowKeys } = props;
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
const result = useDataSource(
{
uid: fieldSchema['x-uid'],
onSuccess(data) {
form.setValues({
[fieldSchema.name]: data?.data,
});
if (field?.componentProps?.pagination === false) {
return;
}
field.componentProps.pagination = field.componentProps.pagination || {};
if (data?.meta?.count) {
field.componentProps.pagination.total = data?.meta?.count;
}
field.componentProps.pagination.current = data?.meta?.page || 1;
field.componentProps.pagination.pageSize = data?.meta?.pageSize || 10;
export const TableVoid: React.FC<TableVoidProps> = observer(
(props) => {
const { rowKey = 'id', useDataSource = useDef, useSelectedRowKeys = useDefSelectedRowKeys } = props;
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
const f = useAttach(form.createArrayField({ ...field.props, basePath: '' }));
const result = useDataSource(
{
uid: fieldSchema['x-uid'],
onSuccess(data) {
form.setValues({
[fieldSchema.name]: data?.data,
});
if (field?.componentProps?.pagination === false) {
return;
}
field.componentProps.pagination = field.componentProps.pagination || {};
if (data?.meta?.count) {
field.componentProps.pagination.total = data?.meta?.count;
}
field.componentProps.pagination.current = data?.meta?.page || 1;
field.componentProps.pagination.pageSize = data?.meta?.pageSize || 10;
},
},
},
props,
);
return (
<AsyncDataProvider value={result}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<TableArray
{...props}
rowKey={rowKey}
useSelectedRowKeys={useSelectedRowKeys}
loading={result?.loading}
pagination={usePaginationProps(props, result)}
/>
</FieldContext.Provider>
</FormContext.Provider>
</AsyncDataProvider>
);
});
props,
);
return (
<AsyncDataProvider value={result}>
<FormContext.Provider value={form}>
<FieldContext.Provider value={f}>
<TableArray
{...props}
rowKey={rowKey}
useSelectedRowKeys={useSelectedRowKeys}
loading={result?.loading}
pagination={usePaginationProps(props, result)}
/>
</FieldContext.Provider>
</FormContext.Provider>
</AsyncDataProvider>
);
},
{ displayName: 'TableVoid' },
);

View File

@ -184,40 +184,43 @@ const schema: ISchema = {
},
};
const DataSourceProvider = observer((props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [dataSource, setDataSource] = useState([]);
const service = useRequest(
() => {
return Promise.resolve({
data: [
{ id: 1, name: 'Name1' },
{ id: 2, name: 'Name2' },
{ id: 3, name: 'Name3' },
],
});
},
{
onSuccess(data) {
setDataSource(data.data);
const DataSourceProvider = observer(
(props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [dataSource, setDataSource] = useState([]);
const service = useRequest(
() => {
return Promise.resolve({
data: [
{ id: 1, name: 'Name1' },
{ id: 2, name: 'Name2' },
{ id: 3, name: 'Name3' },
],
});
},
},
);
console.log('dataSource1', dataSource);
return (
<DataSourceContext.Provider
value={{
service,
dataSource,
setDataSource,
selectedRowKeys,
setSelectedRowKeys,
}}
>
{props.children}
</DataSourceContext.Provider>
);
});
{
onSuccess(data) {
setDataSource(data.data);
},
},
);
console.log('dataSource1', dataSource);
return (
<DataSourceContext.Provider
value={{
service,
dataSource,
setDataSource,
selectedRowKeys,
setSelectedRowKeys,
}}
>
{props.children}
</DataSourceContext.Provider>
);
},
{ displayName: 'DataSourceProvider' },
);
export default () => {
return (

View File

@ -9,28 +9,31 @@ import { DndContext, SortableItem } from '../../common';
import { useDesigner } from '../../hooks/useDesigner';
import { TabsDesigner } from './Tabs.Designer';
export const Tabs: any = observer((props: TabsProps) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
return (
<DndContext>
<AntdTabs
style={props.style}
tabBarExtraContent={{
right: render(),
}}
>
{fieldSchema.mapProperties((schema, key) => {
return (
<AntdTabs.TabPane tab={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}>
<RecursionField name={key} schema={schema} onlyRenderProperties />
</AntdTabs.TabPane>
);
})}
</AntdTabs>
</DndContext>
);
});
export const Tabs: any = observer(
(props: TabsProps) => {
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
return (
<DndContext>
<AntdTabs
style={props.style}
tabBarExtraContent={{
right: render(),
}}
>
{fieldSchema.mapProperties((schema, key) => {
return (
<AntdTabs.TabPane tab={<RecursionField name={key} schema={schema} onlyRenderSelf />} key={key}>
<RecursionField name={key} schema={schema} onlyRenderProperties />
</AntdTabs.TabPane>
);
})}
</AntdTabs>
</DndContext>
);
},
{ displayName: 'Tabs' },
);
const designerCss = css`
position: relative;
@ -79,15 +82,18 @@ const designerCss = css`
}
`;
Tabs.TabPane = observer((props: TabPaneProps & { icon?: any }) => {
const Designer = useDesigner();
const field = useField();
return (
<SortableItem className={classNames('nb-action-link', designerCss, props.className)}>
{props.icon && <Icon style={{ marginRight: 2 }} type={props.icon} />} {props.tab || field.title}
<Designer />
</SortableItem>
);
});
Tabs.TabPane = observer(
(props: TabPaneProps & { icon?: any }) => {
const Designer = useDesigner();
const field = useField();
return (
<SortableItem className={classNames('nb-action-link', designerCss, props.className)}>
{props.icon && <Icon style={{ marginRight: 2 }} type={props.icon} />} {props.tab || field.title}
<Designer />
</SortableItem>
);
},
{ displayName: 'TabPane' },
);
Tabs.Designer = TabsDesigner;

Some files were not shown because too many files have changed in this diff Show More