mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
Merge branch 'main' into fix/sub-table-appends
This commit is contained in:
commit
bfb60729b0
19
.github/PULL_REQUEST_TEMPLATE/bug_fix.md
vendored
Normal file
19
.github/PULL_REQUEST_TEMPLATE/bug_fix.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# Description (Bug描述)
|
||||
|
||||
## Steps to reproduce (复现步骤)
|
||||
Clear steps to reproduce the bug.
|
||||
|
||||
## Expected behavior (预期行为)
|
||||
Describe what the expected behavior should be when the code is executed without the bug.
|
||||
|
||||
## Actual behavior (实际行为)
|
||||
Describe what actually happens when the code is executed with the bug.
|
||||
|
||||
## Related issues (相关issue)
|
||||
Include any related issues or previous bug reports related to this bug.
|
||||
|
||||
# Reason (原因)
|
||||
Explain what caused the bug to occur.
|
||||
|
||||
# Solution (解决方案)
|
||||
Describe solution to the bug clearly and consciously.
|
20
.github/PULL_REQUEST_TEMPLATE/feature.md
vendored
Normal file
20
.github/PULL_REQUEST_TEMPLATE/feature.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
# Description (需求描述)
|
||||
Describe the new feature or modification to an existing feature clearly and consciously.
|
||||
|
||||
# Motivation (需求背景)
|
||||
Explain the reason for adding or modifying this feature.
|
||||
|
||||
# Key changes (关键改动)
|
||||
Provide a technically detailed description of the key changes made.
|
||||
- Frontend (前端)
|
||||
- Backend (后端)
|
||||
|
||||
# Test plan (测试计划)
|
||||
## Suggestions (测试建议)
|
||||
Provide any suggestions or recommendations for improvements in the testing plan.
|
||||
|
||||
## Underlying risk (潜在风险)
|
||||
Identify any potential risks or issues that may arise from the new feature or modification.
|
||||
|
||||
# Showcase (结果展示)
|
||||
Including any screenshots of the new feature or modification.
|
23
.github/pull_request_template.md
vendored
Normal file
23
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
> **Note**
|
||||
> This is a template for submitting a new feature. Use the bug fix template if you're submitting a bug fix pull request by adding `template=bug_fix.md` to your pull request URL.
|
||||
|
||||
# Description (需求描述)
|
||||
Describe the new feature or modification to an existing feature clearly and consciously.
|
||||
|
||||
# Motivation (需求背景)
|
||||
Explain the reason for adding or modifying this feature.
|
||||
|
||||
# Key changes (关键改动)
|
||||
Provide a technically detailed description of the key changes made.
|
||||
- Frontend (前端)
|
||||
- Backend (后端)
|
||||
|
||||
# Test plan (测试计划)
|
||||
## Suggestions (测试建议)
|
||||
Provide any suggestions or recommendations for improvements in the testing plan.
|
||||
|
||||
## Underlying risk (潜在风险)
|
||||
Identify any potential risks or issues that may arise from the new feature or modification.
|
||||
|
||||
# Showcase (结果展示)
|
||||
Including any screenshots of the new feature or modification.
|
@ -137,7 +137,7 @@ const getIgnoreScope = (options: any = {}) => {
|
||||
|
||||
const useAllowedActions = () => {
|
||||
const result = useBlockRequestContext() || { service: useResourceActionContext() };
|
||||
return result?.service?.data?.meta?.allowedActions;
|
||||
return result?.allowedActions ?? result?.service?.data?.meta?.allowedActions;
|
||||
};
|
||||
|
||||
const useResourceName = () => {
|
||||
|
@ -2,9 +2,10 @@ import { css } from '@emotion/css';
|
||||
import { Field } from '@formily/core';
|
||||
import { RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { useRequest } from 'ahooks';
|
||||
import merge from 'deepmerge';
|
||||
import { Col, Row } from 'antd';
|
||||
import template from 'lodash/template';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ACLCollectionProvider,
|
||||
@ -19,6 +20,7 @@ import { CollectionProvider, useCollection, useCollectionManager } from '../coll
|
||||
import { FilterBlockRecord } from '../filter-provider/FilterProvider';
|
||||
import { useRecordIndex } from '../record-provider';
|
||||
import { SharedFilterProvider } from './SharedFilterProvider';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const BlockResourceContext = createContext(null);
|
||||
export const BlockAssociationContext = createContext(null);
|
||||
@ -91,17 +93,19 @@ export const useResourceAction = (props, opts = {}) => {
|
||||
/**
|
||||
* fieldName: 来自 TableFieldProvider
|
||||
*/
|
||||
const { resource, action, fieldName: tableFieldName } = props;
|
||||
const { resource, action, fieldName: tableFieldName, runWhenParamsChanged = false } = props;
|
||||
const { fields } = useCollection();
|
||||
const appends = fields?.filter((field) => field.target).map((field) => field.name);
|
||||
const params = useActionParams(props);
|
||||
const api = useAPIClient();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { snapshot } = useActionContext();
|
||||
const record = useRecord();
|
||||
|
||||
if (!Object.keys(params).includes('appends') && appends?.length) {
|
||||
params['appends'] = appends;
|
||||
if (!Reflect.has(params, 'appends')) {
|
||||
const appends = fields?.filter((field) => field.target).map((field) => field.name);
|
||||
if (appends?.length) {
|
||||
params['appends'] = appends;
|
||||
}
|
||||
}
|
||||
const result = useRequest(
|
||||
snapshot
|
||||
@ -112,10 +116,7 @@ export const useResourceAction = (props, opts = {}) => {
|
||||
if (!action) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
const actionParams = { ...opts };
|
||||
if (params.appends) {
|
||||
actionParams.appends = params.appends;
|
||||
}
|
||||
const actionParams = { ...params, ...opts };
|
||||
return resource[action](actionParams).then((res) => res.data);
|
||||
},
|
||||
{
|
||||
@ -127,9 +128,22 @@ export const useResourceAction = (props, opts = {}) => {
|
||||
}
|
||||
},
|
||||
defaultParams: [params],
|
||||
refreshDeps: [JSON.stringify(params.appends)],
|
||||
refreshDeps: [runWhenParamsChanged ? JSON.stringify(params.appends) : null],
|
||||
},
|
||||
);
|
||||
|
||||
// automatic run service when params has changed
|
||||
const firstRun = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!runWhenParamsChanged) {
|
||||
return;
|
||||
}
|
||||
if (firstRun.current) {
|
||||
result?.run({ ...result?.params?.[0], ...params });
|
||||
}
|
||||
firstRun.current = true;
|
||||
}, [JSON.stringify(params), runWhenParamsChanged]);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -147,15 +161,29 @@ export const MaybeCollectionProvider = (props) => {
|
||||
const BlockRequestProvider = (props) => {
|
||||
const field = useField();
|
||||
const resource = useBlockResource();
|
||||
const [allowedActions, setAllowedActions] = useState({});
|
||||
|
||||
const service = useResourceAction(
|
||||
{ ...props, resource },
|
||||
{
|
||||
...props.requestOptions,
|
||||
},
|
||||
);
|
||||
|
||||
// Infinite scroll support
|
||||
const serviceAllowedActions = (service?.data as any)?.meta?.allowedActions;
|
||||
useEffect(() => {
|
||||
if (!serviceAllowedActions) return;
|
||||
setAllowedActions((last) => {
|
||||
return merge(last, serviceAllowedActions ?? {});
|
||||
});
|
||||
}, [serviceAllowedActions]);
|
||||
|
||||
const __parent = useContext(BlockRequestContext);
|
||||
return (
|
||||
<BlockRequestContext.Provider value={{ block: props.block, props, field, service, resource, __parent }}>
|
||||
<BlockRequestContext.Provider
|
||||
value={{ allowedActions, block: props.block, props, field, service, resource, __parent }}
|
||||
>
|
||||
{props.children}
|
||||
</BlockRequestContext.Provider>
|
||||
);
|
||||
|
@ -1077,7 +1077,7 @@ export const useAssociationNames = (collection) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const associationValues = [];
|
||||
const formSchema = fieldSchema.reduceProperties((buf, schema) => {
|
||||
if (['FormV2', 'Details'].includes(schema['x-component'])) {
|
||||
if (['FormV2', 'Details', 'List', 'GridCard'].includes(schema['x-component'])) {
|
||||
return schema;
|
||||
}
|
||||
return buf;
|
||||
|
@ -69,7 +69,7 @@ const InternalField: React.FC = (props: Props) => {
|
||||
if (!uiSchema) {
|
||||
return null;
|
||||
}
|
||||
return React.createElement(Component, props, props.children);
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
export const InternalFallbackField = () => {
|
||||
|
@ -4,9 +4,9 @@ import { useCollectionManager } from '.';
|
||||
import { useCompile } from '../../schema-component';
|
||||
|
||||
export function useCollectionDataSource(filter?: Function) {
|
||||
const compile = useCompile();
|
||||
const { collections = [] } = useCollectionManager();
|
||||
return (field: any) => {
|
||||
const compile = useCompile();
|
||||
const { collections = [] } = useCollectionManager();
|
||||
action.bound((data: any) => {
|
||||
const filtered = typeof filter === 'function' ? data.filter(filter) : data;
|
||||
field.dataSource = filtered.map((item) => ({
|
||||
|
@ -24,6 +24,7 @@ export * from './schema-component';
|
||||
export * from './schema-initializer';
|
||||
export * from './schema-settings';
|
||||
export * from './schema-templates';
|
||||
export * from './schema-items';
|
||||
export * from './settings-form';
|
||||
export * from './system-settings';
|
||||
export * from './user';
|
||||
|
@ -104,6 +104,17 @@ export default {
|
||||
"Table": "Table",
|
||||
"Table OID(Inheritance)":"Table OID(Inheritance)",
|
||||
"Form": "Form",
|
||||
"List": "List",
|
||||
"Grid Card": "Grid Card",
|
||||
"pixels": "pixels",
|
||||
"Screen size": "Screen size",
|
||||
"Display title": "Display title",
|
||||
'Set the count of columns displayed in a row': "Set the count of columns displayed in a row",
|
||||
'Column': 'Column',
|
||||
'Phone device': 'Phone device',
|
||||
'Tablet device': 'Tablet device',
|
||||
'Desktop device': 'Desktop device',
|
||||
'Large screen device': 'Large screen device',
|
||||
"Collapse": "Collapse",
|
||||
"Select data source": "Select data source",
|
||||
"Calendar": "Calendar",
|
||||
|
@ -120,6 +120,17 @@ export default {
|
||||
"Filter blocks": "筛选区块",
|
||||
"Table": "表格",
|
||||
"Form": "表单",
|
||||
"List": "列表",
|
||||
"Grid Card": "网格卡片",
|
||||
"Screen size": "屏幕尺寸",
|
||||
"pixels": "像素",
|
||||
"Display title": "显示标题",
|
||||
'Set the count of columns displayed in a row': "设置一行展示的列数",
|
||||
'Column': '列',
|
||||
'Phone device': '手机设备',
|
||||
'Tablet device': '平板设备',
|
||||
'Desktop device': '电脑设备',
|
||||
'Large screen device': '大屏幕设备',
|
||||
"Table OID(Inheritance)":"数据表 OID(继承)",
|
||||
"Collapse": "折叠面板",
|
||||
"Select data source": "选择数据源",
|
||||
@ -217,7 +228,6 @@ export default {
|
||||
"Edit collection": "编辑数据表",
|
||||
"Configure fields": "配置字段",
|
||||
"Configure columns": "配置字段",
|
||||
"Please select the records you want to delete": "请选择要删除的记录",
|
||||
"Edit field": "编辑字段",
|
||||
"Override": "重写",
|
||||
"Override field": "重写字段",
|
||||
|
@ -12,6 +12,10 @@ export const RecordProvider: React.FC<{ record: any; parent?: any }> = (props) =
|
||||
return <RecordContext.Provider value={value}>{children}</RecordContext.Provider>;
|
||||
};
|
||||
|
||||
export const RecordSimpleProvider: React.FC<{ value: Record<string, any>; children: React.ReactNode }> = (props) => {
|
||||
return <RecordContext.Provider {...props} />;
|
||||
};
|
||||
|
||||
export const RecordIndexProvider: React.FC<{ index: any }> = (props) => {
|
||||
const { index, children } = props;
|
||||
return <RecordIndexContext.Provider value={index}>{children}</RecordIndexContext.Provider>;
|
||||
|
@ -44,7 +44,7 @@ export const ActionDesigner = (props) => {
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
const isAction = useLinkageAction()
|
||||
const isAction = useLinkageAction();
|
||||
const isPopupAction = ['create', 'update', 'view', 'customize:popup'].includes(fieldSchema['x-action'] || '');
|
||||
const isUpdateModePopupAction = ['customize:bulkUpdate', 'customize:bulkEdit'].includes(fieldSchema['x-action']);
|
||||
const [initialSchema, setInitialSchema] = useState<ISchema>();
|
||||
@ -52,6 +52,7 @@ export const ActionDesigner = (props) => {
|
||||
const isLinkageAction = linkageAction || isAction;
|
||||
const isChildCollectionAction = getChildrenCollections(name).length > 0 && fieldSchema['x-action'] === 'create';
|
||||
const isLink = fieldSchema['x-component'] === 'Action.Link';
|
||||
const isDelete = fieldSchema?.parent['x-component'] === 'CollectionField';
|
||||
useEffect(() => {
|
||||
const schemaUid = uid();
|
||||
const schema: ISchema = {
|
||||
@ -329,16 +330,19 @@ export const ActionDesigner = (props) => {
|
||||
)}
|
||||
|
||||
{isChildCollectionAction && <SchemaSettings.EnableChildCollections collectionName={name} />}
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={(s) => {
|
||||
return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar');
|
||||
}}
|
||||
confirm={{
|
||||
title: t('Delete action'),
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isDelete && [
|
||||
<SchemaSettings.Divider />,
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={(s) => {
|
||||
return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar');
|
||||
}}
|
||||
confirm={{
|
||||
title: t('Delete action'),
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
</MenuGroup>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
|
@ -1,30 +1,34 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { observer, RecursionField, useFieldSchema } from '@formily/react';
|
||||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { useSchemaInitializer } from '../../../schema-initializer';
|
||||
import { DndContext } from '../../common';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useDesignable, useProps } from '../../hooks';
|
||||
|
||||
export const ActionBar = observer((props: any) => {
|
||||
const { layout = 'tow-columns', style, ...others } = props;
|
||||
const { layout = 'tow-columns', style, spaceProps, ...others } = useProps(props);
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||
const { InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||
const { designable } = useDesignable();
|
||||
if (layout === 'one-column') {
|
||||
return (
|
||||
<DndContext>
|
||||
<div style={{ display: 'flex', ...style }} {...others}>
|
||||
<div
|
||||
style={{ display: 'flex', alignItems: 'center', ...style }}
|
||||
{...others}
|
||||
className={cx(others.className, 'nb-action-bar')}
|
||||
>
|
||||
{props.children && (
|
||||
<div style={{ marginRight: 8 }}>
|
||||
<Space>
|
||||
<Space {...spaceProps}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
return <RecursionField key={key} name={key} schema={schema} />;
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
{render()}
|
||||
<InitializerComponent />
|
||||
</div>
|
||||
</DndContext>
|
||||
);
|
||||
@ -45,6 +49,7 @@ export const ActionBar = observer((props: any) => {
|
||||
}
|
||||
}
|
||||
{...others}
|
||||
className={cx(others.className, 'nb-action-bar')}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
@ -55,7 +60,7 @@ export const ActionBar = observer((props: any) => {
|
||||
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}
|
||||
>
|
||||
<DndContext>
|
||||
<Space>
|
||||
<Space {...spaceProps}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
if (schema['x-align'] !== 'left') {
|
||||
return null;
|
||||
@ -63,7 +68,7 @@ export const ActionBar = observer((props: any) => {
|
||||
return <RecursionField key={key} name={key} schema={schema} />;
|
||||
})}
|
||||
</Space>
|
||||
<Space>
|
||||
<Space {...spaceProps}>
|
||||
{fieldSchema.mapProperties((schema, key) => {
|
||||
if (schema['x-align'] === 'left') {
|
||||
return null;
|
||||
@ -73,7 +78,7 @@ export const ActionBar = observer((props: any) => {
|
||||
</Space>
|
||||
</DndContext>
|
||||
</div>
|
||||
{render()}
|
||||
<InitializerComponent />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,15 +1,10 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { RecursionField, connect, mapProps, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { Button, Input } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CollectionProvider } from '../../../collection-manager';
|
||||
import { Input } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useFieldTitle } from '../../hooks';
|
||||
import { ActionContext } from '../action';
|
||||
import { useAssociationFieldContext } from './hooks';
|
||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||
import useServiceOptions, { useInsertSchema } from './hooks';
|
||||
import schema from './schema';
|
||||
import useServiceOptions from './hooks';
|
||||
|
||||
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
||||
action?: string;
|
||||
@ -20,13 +15,8 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
|
||||
const { fieldNames, objectValue = true } = props;
|
||||
const field: any = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const [visibleAddNewer, setVisibleAddNewer] = useState(false);
|
||||
const service = useServiceOptions(props);
|
||||
const { options: collectionField } = useAssociationFieldContext();
|
||||
const isFilterForm = fieldSchema['x-designer'] === 'FormItem.FilterFormDesigner';
|
||||
const isAllowAddNew = fieldSchema['x-add-new'];
|
||||
const insertAddNewer = useInsertSchema('AddNewer');
|
||||
const { t } = useTranslation();
|
||||
const normalizeValues = useCallback(
|
||||
(obj) => {
|
||||
if (!objectValue && typeof obj === 'object') {
|
||||
@ -36,7 +26,6 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
|
||||
},
|
||||
[objectValue, fieldNames?.value],
|
||||
);
|
||||
|
||||
const value = useMemo(() => {
|
||||
if (props.value === undefined || props.value === null || !Object.keys(props.value).length) {
|
||||
return;
|
||||
@ -60,31 +49,18 @@ const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
|
||||
value={value}
|
||||
service={service}
|
||||
></RemoteSelect>
|
||||
{isAllowAddNew && !field.readPretty && !isFilterForm && (
|
||||
<Button
|
||||
type={'default'}
|
||||
onClick={() => {
|
||||
insertAddNewer(schema.AddNewer);
|
||||
setVisibleAddNewer(true);
|
||||
}}
|
||||
>
|
||||
{t('Add new')}
|
||||
</Button>
|
||||
)}
|
||||
</Input.Group>
|
||||
|
||||
<ActionContext.Provider value={{ openMode: 'drawer', visible: visibleAddNewer, setVisible: setVisibleAddNewer }}>
|
||||
<CollectionProvider name={collectionField.target}>
|
||||
{isAllowAddNew && (
|
||||
<RecursionField
|
||||
onlyRenderProperties
|
||||
basePath={field.address}
|
||||
schema={fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'AssociationField.AddNewer';
|
||||
return s['x-component'] === 'Action';
|
||||
}}
|
||||
/>
|
||||
</CollectionProvider>
|
||||
</ActionContext.Provider>
|
||||
)}
|
||||
</Input.Group>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import { SchemaComponentOptions } from '../../';
|
||||
import { InternalSubTable } from './InternalSubTable';
|
||||
import { InternalFileManager } from './FileManager';
|
||||
import { useAssociationFieldContext } from './hooks';
|
||||
import {CreateRecordAction} from './components/CreateRecordAction'
|
||||
|
||||
const EditableAssociationField = observer((props: any) => {
|
||||
const { multiple } = props;
|
||||
@ -44,7 +45,7 @@ const EditableAssociationField = observer((props: any) => {
|
||||
};
|
||||
};
|
||||
return (
|
||||
<SchemaComponentOptions scope={{ useCreateActionProps }}>
|
||||
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{CreateRecordAction}}>
|
||||
{currentMode === 'Picker' && <InternalPicker {...props} />}
|
||||
{currentMode === 'Nester' && <InternalNester {...props} />}
|
||||
{currentMode === 'Select' && <AssociationSelect {...props} />}
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
TableSelectorParamsProvider,
|
||||
useTableSelectorProps as useTsp,
|
||||
} from '../../../block-provider/TableSelectorProvider';
|
||||
import { CollectionProvider, useCollection, useCollectionManager } from '../../../collection-manager';
|
||||
import { CollectionProvider } from '../../../collection-manager';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContext } from '../action';
|
||||
import { useFieldNames, useInsertSchema } from './hooks';
|
||||
@ -65,17 +65,10 @@ export const InternalPicker = observer((props: any) => {
|
||||
const { value, multiple, onChange, quickUpload, selectFile, ...others } = props;
|
||||
const field: any = useField();
|
||||
const fieldNames = useFieldNames(props);
|
||||
const [visibleAddNewer, setVisibleAddNewer] = useState(false);
|
||||
const [visibleSelector, setVisibleSelector] = useState(false);
|
||||
const fieldSchema = useFieldSchema();
|
||||
const insertAddNewer = useInsertSchema('AddNewer');
|
||||
const insertSelector = useInsertSchema('Selector');
|
||||
const { t } = useTranslation();
|
||||
const { options: collectionField } = useAssociationFieldContext();
|
||||
const addbuttonClick = () => {
|
||||
insertAddNewer(schema.AddNewer);
|
||||
setVisibleAddNewer(true);
|
||||
};
|
||||
const compile = useCompile();
|
||||
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
|
||||
const isAllowAddNew = fieldSchema['x-add-new'];
|
||||
@ -168,29 +161,16 @@ export const InternalPicker = observer((props: any) => {
|
||||
/>
|
||||
</div>
|
||||
{isAllowAddNew && (
|
||||
<Button
|
||||
style={{ width: 'auto' }}
|
||||
type={'default'}
|
||||
onClick={() => {
|
||||
addbuttonClick();
|
||||
}}
|
||||
>
|
||||
{t('Add new')}
|
||||
</Button>
|
||||
)}
|
||||
</Input.Group>
|
||||
<ActionContext.Provider value={{ openMode: 'drawer', visible: visibleAddNewer, setVisible: setVisibleAddNewer }}>
|
||||
<CollectionProvider name={collectionField.target}>
|
||||
<RecursionField
|
||||
onlyRenderProperties
|
||||
basePath={field.address}
|
||||
schema={fieldSchema}
|
||||
filterProperties={(s) => {
|
||||
return s['x-component'] === 'AssociationField.AddNewer';
|
||||
return s['x-component'] === 'Action';
|
||||
}}
|
||||
/>
|
||||
</CollectionProvider>
|
||||
</ActionContext.Provider>
|
||||
)}
|
||||
</Input.Group>
|
||||
<ActionContext.Provider value={{ openMode: 'drawer', visible: visibleSelector, setVisible: setVisibleSelector }}>
|
||||
<RecordPickerProvider {...pickerProps}>
|
||||
<CollectionProvider name={collectionField.target}>
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { useState } from 'react';
|
||||
import { CollectionProvider } from '../../../../collection-manager';
|
||||
import { ActionContext, useActionContext } from '../../action';
|
||||
import { useAssociationFieldContext } from '../hooks';
|
||||
import { useInsertSchema } from '../hooks';
|
||||
import { CreateAction } from '../../../../schema-initializer/components';
|
||||
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>
|
||||
);
|
||||
});
|
@ -15,6 +15,7 @@ import {
|
||||
} from '../../../collection-manager';
|
||||
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
|
||||
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
|
||||
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
|
||||
import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
|
||||
@ -22,6 +23,7 @@ import { defaultFieldNames } from '../select';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
import { ReadPretty } from './ReadPretty';
|
||||
import useServiceOptions from './useServiceOptions';
|
||||
import { GeneralSchemaItems } from '../../../schema-items';
|
||||
|
||||
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
||||
action?: string;
|
||||
@ -102,6 +104,8 @@ AssociationSelect.Designer = function Designer() {
|
||||
const tk = useFilterByTk();
|
||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const IsShowMultipleSwitch = useIsShowMultipleSwitch();
|
||||
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const fieldComponentOptions = useFieldComponentOptions();
|
||||
const isSubFormAssociationField = field.address.segments.includes('__form_grid');
|
||||
@ -158,124 +162,7 @@ AssociationSelect.Designer = function Designer() {
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
{collectionField && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-field-title"
|
||||
title={t('Edit field title')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit field title'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Field title'),
|
||||
default: field?.title,
|
||||
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ title }) => {
|
||||
if (title) {
|
||||
field.title = title;
|
||||
fieldSchema.title = title;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
title: fieldSchema.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-description"
|
||||
title={t('Edit description')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
description: {
|
||||
// title: t('Description'),
|
||||
default: field?.description,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ description }) => {
|
||||
field.description = description;
|
||||
fieldSchema.description = description;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
description: fieldSchema.description,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-tooltip"
|
||||
title={t('Edit tooltip')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
tooltip: {
|
||||
default: fieldSchema?.['x-decorator-props']?.tooltip,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ tooltip }) => {
|
||||
field.decoratorProps.tooltip = tooltip;
|
||||
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
||||
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!field.readPretty && (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="required"
|
||||
title={t('Required')}
|
||||
checked={fieldSchema.required as boolean}
|
||||
onChange={(required) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
field.required = required;
|
||||
fieldSchema['required'] = required;
|
||||
schema['required'] = required;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<GeneralSchemaItems />
|
||||
{form && !form?.readPretty && validateSchema && (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set validation rules')}
|
||||
@ -495,36 +382,31 @@ AssociationSelect.Designer = function Designer() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{form &&
|
||||
!form?.readPretty &&
|
||||
['o2m', 'm2m'].includes(collectionField.interface) &&
|
||||
fieldSchema['x-component'] !== 'TableField' && (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="multiple"
|
||||
title={t('Allow multiple')}
|
||||
checked={
|
||||
fieldSchema['x-component-props']?.multiple === undefined
|
||||
? true
|
||||
: fieldSchema['x-component-props'].multiple
|
||||
}
|
||||
onChange={(value) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
field.componentProps = field.componentProps || {};
|
||||
{IsShowMultipleSwitch() ? (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="multiple"
|
||||
title={t('Allow multiple')}
|
||||
checked={
|
||||
fieldSchema['x-component-props']?.multiple === undefined ? true : fieldSchema['x-component-props'].multiple
|
||||
}
|
||||
onChange={(value) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
field.componentProps = field.componentProps || {};
|
||||
|
||||
fieldSchema['x-component-props'].multiple = value;
|
||||
field.componentProps.multiple = value;
|
||||
fieldSchema['x-component-props'].multiple = value;
|
||||
field.componentProps.multiple = value;
|
||||
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the data scope')}
|
||||
schema={
|
||||
@ -797,105 +679,7 @@ AssociationSelect.FilterDesigner = function FilterDesigner() {
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
{collectionField && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-field-title"
|
||||
title={t('Edit field title')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit field title'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Field title'),
|
||||
default: field?.title,
|
||||
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ title }) => {
|
||||
if (title) {
|
||||
field.title = title;
|
||||
fieldSchema.title = title;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
title: fieldSchema.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-description"
|
||||
title={t('Edit description')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
description: {
|
||||
// title: t('Description'),
|
||||
default: field?.description,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ description }) => {
|
||||
field.description = description;
|
||||
fieldSchema.description = description;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
description: fieldSchema.description,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-tooltip"
|
||||
title={t('Edit tooltip')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
tooltip: {
|
||||
default: fieldSchema?.['x-decorator-props']?.tooltip,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ tooltip }) => {
|
||||
field.decoratorProps.tooltip = tooltip;
|
||||
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
||||
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<GeneralSchemaItems required={false} />
|
||||
{form && !form?.readPretty && validateSchema && (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set validation rules')}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { ArrayCollapse, ArrayItems, FormLayout, FormItem as Item } from '@formily/antd';
|
||||
import { Field } from '@formily/core';
|
||||
import { ISchema, Schema, observer, useField, useFieldSchema } from '@formily/react';
|
||||
@ -8,7 +8,7 @@ import moment from 'moment';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
|
||||
import { BlockRequestContext, useFilterByTk, useFormBlockContext } from '../../../block-provider';
|
||||
import { BlockRequestContext, useFormBlockContext } from '../../../block-provider';
|
||||
import {
|
||||
Collection,
|
||||
CollectionFieldOptions,
|
||||
@ -18,8 +18,10 @@ import {
|
||||
useSortFields,
|
||||
} from '../../../collection-manager';
|
||||
import { isTitleField } from '../../../collection-manager/Configuration/CollectionFields';
|
||||
import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems';
|
||||
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
|
||||
import { VariableInput } from '../../../schema-settings/VariableInput/VariableInput';
|
||||
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
|
||||
import { isVariable, parseVariables, useVariablesCtx } from '../../common/utils/uitls';
|
||||
import { SchemaComponent } from '../../core';
|
||||
import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks';
|
||||
@ -69,15 +71,25 @@ export const FormItem: any = observer((props: any) => {
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
const { showTitle = true } = props;
|
||||
return (
|
||||
<ACLCollectionFieldProvider>
|
||||
<BlockItem className={'nb-form-item'}>
|
||||
<Item
|
||||
className={`${css`
|
||||
& .ant-space {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`}`}
|
||||
className={cx(
|
||||
css`
|
||||
& .ant-space {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`,
|
||||
{
|
||||
[css`
|
||||
& .ant-formily-item-label {
|
||||
display: none;
|
||||
}
|
||||
`]: showTitle === false,
|
||||
},
|
||||
)}
|
||||
{...props}
|
||||
extra={
|
||||
typeof field.description === 'string' ? (
|
||||
@ -99,7 +111,6 @@ export const FormItem: any = observer((props: any) => {
|
||||
FormItem.Designer = function Designer() {
|
||||
const { getCollectionFields, getInterface, getCollectionJoinField, getCollection } = useCollectionManager();
|
||||
const { getField } = useCollection();
|
||||
const tk = useFilterByTk();
|
||||
const { form } = useFormBlockContext();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -107,6 +118,8 @@ FormItem.Designer = function Designer() {
|
||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const variablesCtx = useVariablesCtx();
|
||||
const IsShowMultipleSwitch = useIsShowMultipleSwitch();
|
||||
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
const targetCollection = getCollection(collectionField?.target);
|
||||
const interfaceConfig = getInterface(collectionField?.interface);
|
||||
@ -133,6 +146,7 @@ FormItem.Designer = function Designer() {
|
||||
value: field?.name,
|
||||
label: compile(field?.uiSchema?.title) || field?.name,
|
||||
}));
|
||||
|
||||
let readOnlyMode = 'editable';
|
||||
if (fieldSchema['x-disabled'] === true) {
|
||||
readOnlyMode = 'readonly';
|
||||
@ -160,124 +174,7 @@ FormItem.Designer = function Designer() {
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
{collectionField && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-field-title"
|
||||
title={t('Edit field title')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit field title'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Field title'),
|
||||
default: field?.title,
|
||||
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ title }) => {
|
||||
if (title) {
|
||||
field.title = title;
|
||||
fieldSchema.title = title;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
title: fieldSchema.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-description"
|
||||
title={t('Edit description')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
description: {
|
||||
// title: t('Description'),
|
||||
default: field?.description,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ description }) => {
|
||||
field.description = description;
|
||||
fieldSchema.description = description;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
description: fieldSchema.description,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-tooltip"
|
||||
title={t('Edit tooltip')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
tooltip: {
|
||||
default: fieldSchema?.['x-decorator-props']?.tooltip,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ tooltip }) => {
|
||||
field.decoratorProps.tooltip = tooltip;
|
||||
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
||||
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!field.readPretty && fieldSchema['x-component'] !== 'FormField' && (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="required"
|
||||
title={t('Required')}
|
||||
checked={fieldSchema.required as boolean}
|
||||
onChange={(required) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
field.required = required;
|
||||
fieldSchema['required'] = required;
|
||||
schema['required'] = required;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<GeneralSchemaItems />
|
||||
{!form?.readPretty && isFileField ? (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="quick-upload"
|
||||
@ -692,6 +589,28 @@ FormItem.Designer = function Designer() {
|
||||
title={t('Allow add new data')}
|
||||
checked={fieldSchema['x-add-new'] as boolean}
|
||||
onChange={(allowAddNew) => {
|
||||
const hasAddNew = fieldSchema.reduceProperties((buf, schema) => {
|
||||
if (schema['x-component'] === 'Action') {
|
||||
return schema;
|
||||
}
|
||||
return buf;
|
||||
}, null);
|
||||
|
||||
if (!hasAddNew) {
|
||||
const addNewActionschema = {
|
||||
'x-action': 'create',
|
||||
title: "{{t('Add new')}}",
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-component': 'Action',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
type: 'default',
|
||||
component: 'CreateRecordAction',
|
||||
},
|
||||
};
|
||||
insertAdjacent('afterBegin', addNewActionschema);
|
||||
}
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
@ -705,36 +624,31 @@ FormItem.Designer = function Designer() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{form &&
|
||||
!form?.readPretty &&
|
||||
['o2m', 'm2m'].includes(collectionField?.interface) &&
|
||||
fieldSchema['x-component'] !== 'TableField' && (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="multiple"
|
||||
title={t('Allow multiple')}
|
||||
checked={
|
||||
fieldSchema['x-component-props']?.multiple === undefined
|
||||
? true
|
||||
: fieldSchema['x-component-props'].multiple
|
||||
}
|
||||
onChange={(value) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
field.componentProps = field.componentProps || {};
|
||||
{IsShowMultipleSwitch() ? (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="multiple"
|
||||
title={t('Allow multiple')}
|
||||
checked={
|
||||
fieldSchema['x-component-props']?.multiple === undefined ? true : fieldSchema['x-component-props'].multiple
|
||||
}
|
||||
onChange={(value) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||
field.componentProps = field.componentProps || {};
|
||||
|
||||
fieldSchema['x-component-props'].multiple = value;
|
||||
field.componentProps.multiple = value;
|
||||
fieldSchema['x-component-props'].multiple = value;
|
||||
field.componentProps.multiple = value;
|
||||
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{field.readPretty && options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && !isFileField && (
|
||||
<SchemaSettings.SwitchItem
|
||||
title={t('Enable link')}
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { FormContext, useField, useForm } from '@formily/react';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { BlockProvider, useBlockRequestContext } from '../../../block-provider';
|
||||
import { FormLayout } from '@formily/antd';
|
||||
import { css } from '@emotion/css';
|
||||
import { useAssociationNames } from '../../../block-provider/hooks';
|
||||
|
||||
export const GridCardBlockContext = createContext<any>({});
|
||||
|
||||
const InternalGridCardBlockProvider = (props) => {
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
const field = useField();
|
||||
const form = useMemo(() => {
|
||||
return createForm({
|
||||
readPretty: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!service?.loading) {
|
||||
form.setValuesIn(field.address.concat('list').toString(), service?.data?.data);
|
||||
}
|
||||
}, [service?.data?.data, service?.loading]);
|
||||
|
||||
return (
|
||||
<GridCardBlockContext.Provider
|
||||
value={{
|
||||
service,
|
||||
resource,
|
||||
columnCount: props.columnCount,
|
||||
}}
|
||||
>
|
||||
<FormContext.Provider value={form}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
<div
|
||||
className={css`
|
||||
& > .nb-block-item {
|
||||
margin-bottom: var(--nb-spacing);
|
||||
& > .nb-action-bar:has(:first-child:not(:empty)) {
|
||||
padding: var(--nb-spacing);
|
||||
background: #fff;
|
||||
}
|
||||
.ant-list-pagination {
|
||||
padding: var(--nb-spacing);
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</FormLayout>
|
||||
</FormContext.Provider>
|
||||
</GridCardBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const GridCardBlockProvider = (props) => {
|
||||
const params = { ...props.params };
|
||||
const { collection } = props;
|
||||
const { appends } = useAssociationNames(collection);
|
||||
if (!Object.keys(params).includes('appends')) {
|
||||
params['appends'] = appends;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockProvider {...props} params={params}>
|
||||
<InternalGridCardBlockProvider {...props} />
|
||||
</BlockProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useGridCardBlockContext = () => {
|
||||
return useContext(GridCardBlockContext);
|
||||
};
|
||||
|
||||
export const useGridCardItemProps = () => {
|
||||
return {};
|
||||
};
|
@ -0,0 +1,245 @@
|
||||
import { useFieldSchema, useField, ISchema } from '@formily/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { Slider } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options';
|
||||
Slider;
|
||||
|
||||
const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => {
|
||||
obj[cur] = cur;
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
export const GridCardDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
const template = useSchemaTemplate();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const dataSource = useCollectionFilterOptions(name);
|
||||
const { dn } = useDesignable();
|
||||
const sortFields = useSortFields(name);
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const columnCount = field.decoratorProps.columnCount || defaultColumnCount;
|
||||
|
||||
const columnCountSchema = useMemo(() => {
|
||||
return {
|
||||
'x-component': 'Slider',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
min: 1,
|
||||
max: 24,
|
||||
marks: columnCountMarks,
|
||||
tooltip: {
|
||||
formatter: (value) => `${value}${t('Column')}`,
|
||||
},
|
||||
step: null,
|
||||
},
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
const columnCountProperties = useMemo(() => {
|
||||
return gridSizes.reduce((o, k) => {
|
||||
o[k] = {
|
||||
...columnCountSchema,
|
||||
title: t(screenSizeTitleMaps[k]),
|
||||
description: `${t('Screen size')} ${screenSizeMaps[k]} ${t('pixels')}`,
|
||||
};
|
||||
return o;
|
||||
}, {});
|
||||
}, [columnCountSchema, t]);
|
||||
|
||||
const sort = defaultSort?.map((item: string) => {
|
||||
return item.startsWith('-')
|
||||
? {
|
||||
field: item.substring(1),
|
||||
direction: 'desc',
|
||||
}
|
||||
: {
|
||||
field: item,
|
||||
direction: 'asc',
|
||||
};
|
||||
});
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaComponentOptions components={{ Slider }}>
|
||||
<SchemaSettings.BlockTitleItem />
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the count of columns displayed in a row')}
|
||||
initialValues={columnCount}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set the count of columns displayed in a row'),
|
||||
properties: columnCountProperties,
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={(columnCount) => {
|
||||
_.set(fieldSchema, 'x-decorator-props.columnCount', columnCount);
|
||||
field.decoratorProps.columnCount = columnCount;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the data scope')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set the data scope'),
|
||||
properties: {
|
||||
filter: {
|
||||
default: defaultFilter,
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
_.set(fieldSchema, 'x-decorator-props.params.filter', filter);
|
||||
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params };
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set default sorting rules')}
|
||||
components={{ ArrayItems }}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set default sorting rules'),
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'array',
|
||||
default: sort,
|
||||
'x-component': 'ArrayItems',
|
||||
'x-decorator': 'FormItem',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.SortHandle',
|
||||
},
|
||||
field: {
|
||||
type: 'string',
|
||||
enum: sortFields,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
width: 260,
|
||||
},
|
||||
},
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {
|
||||
optionType: 'button',
|
||||
},
|
||||
enum: [
|
||||
{
|
||||
label: t('ASC'),
|
||||
value: 'asc',
|
||||
},
|
||||
{
|
||||
label: t('DESC'),
|
||||
value: 'desc',
|
||||
},
|
||||
],
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: t('Add sort field'),
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ sort }) => {
|
||||
const sortArr = sort.map((item) => {
|
||||
return item.direction === 'desc' ? `-${item.field}` : item.field;
|
||||
});
|
||||
|
||||
_.set(fieldSchema, 'x-decorator-props.params.sort', sortArr);
|
||||
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params };
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.SelectItem
|
||||
title={t('Records per page')}
|
||||
value={field.decoratorProps?.params?.pageSize || 20}
|
||||
options={pageSizeOptions.map((v) => ({ value: v }))}
|
||||
onChange={(pageSize) => {
|
||||
_.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize);
|
||||
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.Template componentName={'GridCard'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</SchemaComponentOptions>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
import { css } from '@emotion/css';
|
||||
import { useField } from '@formily/react';
|
||||
import { ObjectField } from '@formily/core';
|
||||
import { RecordSimpleProvider } from '../../../record-provider';
|
||||
|
||||
const itemCss = css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export const GridCardItem = (props) => {
|
||||
const field = useField<ObjectField>();
|
||||
return (
|
||||
<Card
|
||||
className={css`
|
||||
&,
|
||||
& .ant-card-body {
|
||||
height: 100%;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className={itemCss}>
|
||||
<RecordSimpleProvider value={field.value}>{props.children}</RecordSimpleProvider>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
@ -0,0 +1,150 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { List as AntdList, PaginationProps, Col } from 'antd';
|
||||
import { useGridCardActionBarProps } from './hooks';
|
||||
import { SortableItem } from '../../common';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { useDesigner } from '../../hooks';
|
||||
import { GridCardItem } from './GridCard.Item';
|
||||
import { useGridCardBlockContext, useGridCardItemProps, GridCardBlockProvider } from './GridCard.Decorator';
|
||||
import { GridCardDesigner } from './GridCard.Designer';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { defaultColumnCount, pageSizeOptions } from './options';
|
||||
|
||||
const rowGutter = {
|
||||
md: 16,
|
||||
sm: 8,
|
||||
xs: 8,
|
||||
};
|
||||
|
||||
const designerCss = css`
|
||||
width: 100%;
|
||||
&:hover {
|
||||
> .general-schema-designer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> .general-schema-designer {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
background: rgba(241, 139, 98, 0.06);
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
> .general-schema-designer-icons {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
line-height: 16px;
|
||||
pointer-events: all;
|
||||
.ant-space-item {
|
||||
background-color: #f18b62;
|
||||
color: #fff;
|
||||
line-height: 16px;
|
||||
width: 16px;
|
||||
padding-left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const InternalGridCard = (props) => {
|
||||
const { service, columnCount = defaultColumnCount } = useGridCardBlockContext();
|
||||
const { run, params } = service;
|
||||
const meta = service?.data?.meta;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField<ArrayField>();
|
||||
const Designer = useDesigner();
|
||||
const [schemaMap] = useState(new Map());
|
||||
const getSchema = useCallback(
|
||||
(key) => {
|
||||
if (!schemaMap.has(key)) {
|
||||
schemaMap.set(
|
||||
key,
|
||||
new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
[key]: fieldSchema.properties['item'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return schemaMap.get(key);
|
||||
},
|
||||
[fieldSchema.properties, schemaMap],
|
||||
);
|
||||
|
||||
const onPaginationChange: PaginationProps['onChange'] = useCallback(
|
||||
(page, pageSize) => {
|
||||
run({
|
||||
...params?.[0],
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
});
|
||||
},
|
||||
[run, params],
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
scope={{
|
||||
useGridCardItemProps,
|
||||
useGridCardActionBarProps,
|
||||
}}
|
||||
>
|
||||
<SortableItem className={cx('nb-card-list', designerCss)}>
|
||||
<AntdList
|
||||
pagination={
|
||||
!meta || meta.count <= meta.pageSize
|
||||
? false
|
||||
: {
|
||||
onChange: onPaginationChange,
|
||||
total: meta?.count || 0,
|
||||
pageSize: meta?.pageSize || 10,
|
||||
current: meta?.page || 1,
|
||||
pageSizeOptions,
|
||||
}
|
||||
}
|
||||
dataSource={field.value}
|
||||
grid={{
|
||||
...columnCount,
|
||||
sm: columnCount.xs,
|
||||
xl: columnCount.lg,
|
||||
gutter: [rowGutter, rowGutter],
|
||||
}}
|
||||
renderItem={(item, index) => {
|
||||
return (
|
||||
<Col style={{ height: '100%' }}>
|
||||
<RecursionField
|
||||
key={index}
|
||||
basePath={field.address}
|
||||
name={index}
|
||||
onlyRenderProperties
|
||||
schema={getSchema(index)}
|
||||
></RecursionField>
|
||||
</Col>
|
||||
);
|
||||
}}
|
||||
loading={service?.loading}
|
||||
/>
|
||||
<Designer />
|
||||
</SortableItem>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
};
|
||||
|
||||
export const GridCard = InternalGridCard as typeof InternalGridCard & {
|
||||
Item: typeof GridCardItem;
|
||||
Designer: typeof GridCardDesigner;
|
||||
Decorator: typeof GridCardBlockProvider;
|
||||
};
|
||||
|
||||
GridCard.Item = GridCardItem;
|
||||
GridCard.Designer = GridCardDesigner;
|
||||
GridCard.Decorator = GridCardBlockProvider;
|
@ -0,0 +1,12 @@
|
||||
import { SpaceProps } from 'antd';
|
||||
|
||||
const spaceProps: SpaceProps = {
|
||||
size: ['large', 'small'],
|
||||
wrap: true,
|
||||
};
|
||||
|
||||
export const useGridCardActionBarProps = () => {
|
||||
return {
|
||||
spaceProps,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './GridCard';
|
@ -0,0 +1,21 @@
|
||||
export const pageSizeOptions = [12, 24, 60, 96, 120];
|
||||
export const gridSizes = ['xs', 'md', 'lg', 'xxl'];
|
||||
export const screenSizeTitleMaps = {
|
||||
xs: 'Phone device',
|
||||
md: 'Tablet device',
|
||||
lg: 'Desktop device',
|
||||
xxl: 'Large screen device',
|
||||
};
|
||||
export const screenSizeMaps = {
|
||||
xs: '< 768',
|
||||
md: '≥ 768',
|
||||
lg: '≥ 992',
|
||||
xxl: '≥ 1600',
|
||||
};
|
||||
|
||||
export const defaultColumnCount = {
|
||||
xs: 1,
|
||||
md: 2,
|
||||
lg: 3,
|
||||
xxl: 4,
|
||||
};
|
@ -3,7 +3,7 @@ import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import cls from 'classnames';
|
||||
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDesignable, useFormBlockContext, useSchemaInitializer } from '../../../';
|
||||
import { DndContext } from '../../common/dnd-context';
|
||||
|
||||
@ -293,22 +293,26 @@ const wrapColSchema = (schema: Schema) => {
|
||||
|
||||
const useRowProperties = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return fieldSchema.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === 'Grid.Row' && !s['x-hidden']) {
|
||||
buf.push(s);
|
||||
}
|
||||
return buf;
|
||||
}, []);
|
||||
return useMemo(() => {
|
||||
return fieldSchema.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === 'Grid.Row' && !s['x-hidden']) {
|
||||
buf.push(s);
|
||||
}
|
||||
return buf;
|
||||
}, []);
|
||||
}, [Object.keys(fieldSchema.properties || {}).join(',')]);
|
||||
};
|
||||
|
||||
const useColProperties = () => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
return fieldSchema.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === 'Grid.Col' && !s['x-hidden']) {
|
||||
buf.push(s);
|
||||
}
|
||||
return buf;
|
||||
}, []);
|
||||
return useMemo(() => {
|
||||
return fieldSchema.reduceProperties((buf, s) => {
|
||||
if (s['x-component'] === 'Grid.Col' && !s['x-hidden']) {
|
||||
buf.push(s);
|
||||
}
|
||||
return buf;
|
||||
}, []);
|
||||
}, [Object.keys(fieldSchema.properties || {}).join(',')]);
|
||||
};
|
||||
|
||||
const DndWrapper = (props) => {
|
||||
@ -330,7 +334,7 @@ export const Grid: any = observer((props: any) => {
|
||||
const gridRef = useRef(null);
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||
const { render, InitializerComponent } = useSchemaInitializer(fieldSchema['x-initializer']);
|
||||
const addr = field.address.toString();
|
||||
const rows = useRowProperties();
|
||||
const { setPrintContent } = useFormBlockContext();
|
||||
@ -338,8 +342,9 @@ export const Grid: any = observer((props: any) => {
|
||||
useEffect(() => {
|
||||
gridRef.current && setPrintContent?.(gridRef.current);
|
||||
}, [gridRef.current]);
|
||||
|
||||
return (
|
||||
<GridContext.Provider value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render }}>
|
||||
<GridContext.Provider value={{ ref: gridRef, fieldSchema, renderSchemaInitializer: render, InitializerComponent }}>
|
||||
<div className={'nb-grid'} style={{ position: 'relative' }} ref={gridRef}>
|
||||
<DndWrapper dndContext={props.dndContext}>
|
||||
<RowDivider
|
||||
@ -355,7 +360,7 @@ export const Grid: any = observer((props: any) => {
|
||||
/>
|
||||
{rows.map((schema, index) => {
|
||||
return (
|
||||
<React.Fragment key={schema.name}>
|
||||
<React.Fragment key={index}>
|
||||
<RecursionField name={schema.name} schema={schema} />
|
||||
<RowDivider
|
||||
rows={rows}
|
||||
@ -372,7 +377,7 @@ export const Grid: any = observer((props: any) => {
|
||||
);
|
||||
})}
|
||||
</DndWrapper>
|
||||
{render()}
|
||||
<InitializerComponent />
|
||||
</div>
|
||||
</GridContext.Provider>
|
||||
);
|
||||
@ -411,7 +416,7 @@ Grid.Row = observer(() => {
|
||||
/>
|
||||
{cols.map((schema, index) => {
|
||||
return (
|
||||
<React.Fragment key={schema.name}>
|
||||
<React.Fragment key={index}>
|
||||
<RecursionField name={schema.name} schema={schema} />
|
||||
<ColDivider
|
||||
cols={cols}
|
||||
@ -437,11 +442,16 @@ Grid.Col = observer((props: any) => {
|
||||
const { cols = [] } = useContext(GridRowContext);
|
||||
const schema = useFieldSchema();
|
||||
const field = useField();
|
||||
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})`;
|
||||
}
|
||||
|
||||
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: {
|
||||
|
@ -19,10 +19,12 @@ export * from './form-v2';
|
||||
export * from './g2plot';
|
||||
export * from './gantt';
|
||||
export * from './grid';
|
||||
export * from './grid-card';
|
||||
export * from './icon-picker';
|
||||
export * from './input';
|
||||
export * from './input-number';
|
||||
export * from './kanban';
|
||||
export * from './list';
|
||||
export * from './markdown';
|
||||
export * from './menu';
|
||||
export * from './page';
|
||||
|
@ -0,0 +1,61 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { FormContext, useField } from '@formily/react';
|
||||
import React, { createContext, useContext, useEffect, useMemo } from 'react';
|
||||
import { FormLayout } from '@formily/antd';
|
||||
import { useAssociationNames } from '../../../block-provider/hooks';
|
||||
import { BlockProvider, useBlockRequestContext } from '../../../block-provider';
|
||||
|
||||
export const ListBlockContext = createContext<any>({});
|
||||
|
||||
const InternalListBlockProvider = (props) => {
|
||||
const { resource, service } = useBlockRequestContext();
|
||||
|
||||
const field = useField();
|
||||
const form = useMemo(() => {
|
||||
return createForm({
|
||||
readPretty: true,
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!service?.loading) {
|
||||
form.setValuesIn(field.address.concat('list').toString(), service?.data?.data);
|
||||
}
|
||||
}, [service?.data?.data, service?.loading]);
|
||||
|
||||
return (
|
||||
<ListBlockContext.Provider
|
||||
value={{
|
||||
service,
|
||||
resource,
|
||||
}}
|
||||
>
|
||||
<FormContext.Provider value={form}>
|
||||
<FormLayout layout={'vertical'}>{props.children}</FormLayout>
|
||||
</FormContext.Provider>
|
||||
</ListBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListBlockProvider = (props) => {
|
||||
const params = { ...props.params };
|
||||
const { collection } = props;
|
||||
const { appends } = useAssociationNames(collection);
|
||||
if (!Object.keys(params).includes('appends')) {
|
||||
params['appends'] = appends;
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockProvider {...props} params={params}>
|
||||
<InternalListBlockProvider {...props} />
|
||||
</BlockProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useListBlockContext = () => {
|
||||
return useContext(ListBlockContext);
|
||||
};
|
||||
|
||||
export const useListItemProps = () => {
|
||||
return {};
|
||||
};
|
@ -0,0 +1,192 @@
|
||||
import { useFieldSchema, useField, ISchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { useListBlockContext } from './List.Decorator';
|
||||
import _ from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
|
||||
|
||||
export const ListDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
const template = useSchemaTemplate();
|
||||
const { t } = useTranslation();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const dataSource = useCollectionFilterOptions(name);
|
||||
const { service } = useListBlockContext();
|
||||
const { dn } = useDesignable();
|
||||
const sortFields = useSortFields(name);
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const sort = defaultSort?.map((item: string) => {
|
||||
return item.startsWith('-')
|
||||
? {
|
||||
field: item.substring(1),
|
||||
direction: 'desc',
|
||||
}
|
||||
: {
|
||||
field: item,
|
||||
direction: 'asc',
|
||||
};
|
||||
});
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.BlockTitleItem />
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the data scope')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set the data scope'),
|
||||
properties: {
|
||||
filter: {
|
||||
default: defaultFilter,
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
_.set(fieldSchema, 'x-decorator-props.params.filter', filter);
|
||||
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set default sorting rules')}
|
||||
components={{ ArrayItems }}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set default sorting rules'),
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'array',
|
||||
default: sort,
|
||||
'x-component': 'ArrayItems',
|
||||
'x-decorator': 'FormItem',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
properties: {
|
||||
sort: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.SortHandle',
|
||||
},
|
||||
field: {
|
||||
type: 'string',
|
||||
enum: sortFields,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
width: 260,
|
||||
},
|
||||
},
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {
|
||||
optionType: 'button',
|
||||
},
|
||||
enum: [
|
||||
{
|
||||
label: t('ASC'),
|
||||
value: 'asc',
|
||||
},
|
||||
{
|
||||
label: t('DESC'),
|
||||
value: 'desc',
|
||||
},
|
||||
],
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: t('Add sort field'),
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ sort }) => {
|
||||
const sortArr = sort.map((item) => {
|
||||
return item.direction === 'desc' ? `-${item.field}` : item.field;
|
||||
});
|
||||
|
||||
_.set(fieldSchema, 'x-decorator-props.params.sort', sortArr);
|
||||
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.SelectItem
|
||||
title={t('Records per page')}
|
||||
value={field.decoratorProps?.params?.pageSize || 20}
|
||||
options={[
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 },
|
||||
{ label: '80', value: 80 },
|
||||
{ label: '100', value: 100 },
|
||||
]}
|
||||
onChange={(pageSize) => {
|
||||
_.set(fieldSchema, 'x-decorator-props.params.pageSize', pageSize);
|
||||
field.decoratorProps.params = { ...fieldSchema['x-decorator-props'].params, page: 1 };
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.Template componentName={'List'} collectionName={name} resourceName={defaultResource} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { useField } from '@formily/react';
|
||||
import { ObjectField } from '@formily/core';
|
||||
import { RecordSimpleProvider } from '../../../record-provider';
|
||||
|
||||
const itemCss = css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
`;
|
||||
|
||||
export const ListItem = (props) => {
|
||||
const field = useField<ObjectField>();
|
||||
return (
|
||||
<div className={itemCss}>
|
||||
<RecordSimpleProvider value={field.value}>{props.children}</RecordSimpleProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
135
packages/core/client/src/schema-component/antd/list/List.tsx
Normal file
135
packages/core/client/src/schema-component/antd/list/List.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import { ListDesigner } from './List.Designer';
|
||||
import { ListBlockProvider, useListBlockContext, useListItemProps } from './List.Decorator';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { List as AntdList, PaginationProps } from 'antd';
|
||||
import { useListActionBarProps } from './hooks';
|
||||
import { SortableItem } from '../../common';
|
||||
import { SchemaComponentOptions } from '../../core';
|
||||
import { useDesigner } from '../../hooks';
|
||||
import { ListItem } from './List.Item';
|
||||
import { ArrayField } from '@formily/core';
|
||||
|
||||
const designerCss = css`
|
||||
width: 100%;
|
||||
margin-bottom: var(--nb-spacing);
|
||||
&:hover {
|
||||
> .general-schema-designer {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> .general-schema-designer {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
background: rgba(241, 139, 98, 0.06);
|
||||
border: 0;
|
||||
pointer-events: none;
|
||||
> .general-schema-designer-icons {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
line-height: 16px;
|
||||
pointer-events: all;
|
||||
.ant-space-item {
|
||||
background-color: #f18b62;
|
||||
color: #fff;
|
||||
line-height: 16px;
|
||||
width: 16px;
|
||||
padding-left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const InternalList = (props) => {
|
||||
const { service } = useListBlockContext();
|
||||
const { run, params } = service;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const Designer = useDesigner();
|
||||
const meta = service?.data?.meta;
|
||||
const field = useField<ArrayField>();
|
||||
const [schemaMap] = useState(new Map());
|
||||
const getSchema = useCallback(
|
||||
(key) => {
|
||||
if (!schemaMap.has(key)) {
|
||||
schemaMap.set(
|
||||
key,
|
||||
new Schema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
[key]: fieldSchema.properties['item'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
return schemaMap.get(key);
|
||||
},
|
||||
[fieldSchema.properties, schemaMap],
|
||||
);
|
||||
|
||||
const onPaginationChange: PaginationProps['onChange'] = useCallback(
|
||||
(page, pageSize) => {
|
||||
run({
|
||||
...params?.[0],
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
});
|
||||
},
|
||||
[run, params],
|
||||
);
|
||||
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
scope={{
|
||||
useListItemProps,
|
||||
useListActionBarProps,
|
||||
}}
|
||||
>
|
||||
<SortableItem className={cx('nb-list', designerCss)}>
|
||||
<AntdList
|
||||
pagination={
|
||||
!meta || meta.count <= meta.pageSize
|
||||
? false
|
||||
: {
|
||||
onChange: onPaginationChange,
|
||||
total: meta?.count || 0,
|
||||
pageSize: meta?.pageSize || 10,
|
||||
current: meta?.page || 1,
|
||||
}
|
||||
}
|
||||
loading={service?.loading}
|
||||
>
|
||||
{field.value?.map((item, index) => {
|
||||
return (
|
||||
<RecursionField
|
||||
basePath={field.address}
|
||||
key={index}
|
||||
name={index}
|
||||
onlyRenderProperties
|
||||
schema={getSchema(index)}
|
||||
></RecursionField>
|
||||
);
|
||||
})}
|
||||
</AntdList>
|
||||
<Designer />
|
||||
</SortableItem>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = InternalList as typeof InternalList & {
|
||||
Item: typeof ListItem;
|
||||
Designer: typeof ListDesigner;
|
||||
Decorator: typeof ListBlockProvider;
|
||||
};
|
||||
|
||||
List.Item = ListItem;
|
||||
List.Designer = ListDesigner;
|
||||
List.Decorator = ListBlockProvider;
|
12
packages/core/client/src/schema-component/antd/list/hooks.ts
Normal file
12
packages/core/client/src/schema-component/antd/list/hooks.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { SpaceProps } from 'antd';
|
||||
|
||||
const spaceProps: SpaceProps = {
|
||||
size: ['large', 'small'],
|
||||
wrap: true,
|
||||
};
|
||||
|
||||
export const useListActionBarProps = () => {
|
||||
return {
|
||||
spaceProps,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './List';
|
@ -97,11 +97,14 @@ const ConstantTypes = {
|
||||
},
|
||||
};
|
||||
|
||||
function getTypedConstantOption(type) {
|
||||
function getTypedConstantOption(type: string, types?: true | string[]) {
|
||||
const allTypes = Object.values(ConstantTypes);
|
||||
return {
|
||||
value: '',
|
||||
label: '{{t("Constant")}}',
|
||||
children: Object.values(ConstantTypes),
|
||||
children: types
|
||||
? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true)
|
||||
: allTypes,
|
||||
component: ConstantTypes[type]?.component,
|
||||
};
|
||||
}
|
||||
@ -129,7 +132,7 @@ export function Input(props) {
|
||||
label: '{{t("Constant")}}',
|
||||
}
|
||||
: useTypedConstant
|
||||
? getTypedConstantOption(type)
|
||||
? getTypedConstantOption(type, useTypedConstant)
|
||||
: {
|
||||
value: '',
|
||||
label: '{{t("Null")}}',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { isPlainObj } from '@formily/shared';
|
||||
import get from 'lodash/get';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
|
||||
import { SchemaComponentOptions } from '../schema-component';
|
||||
import { SchemaInitializer } from './SchemaInitializer';
|
||||
import * as globals from './buttons';
|
||||
@ -22,20 +22,32 @@ export const useSchemaInitializer = (name: string, props = {}) => {
|
||||
return component && React.createElement(component, props);
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
return { exists: false, render: (props?: any) => render(null) };
|
||||
}
|
||||
|
||||
const initializer = get(initializers, name || fieldSchema?.['x-initializer']);
|
||||
const initializerPropsRef = useRef({});
|
||||
const initializer = name ? get(initializers, name || fieldSchema?.['x-initializer']) : null;
|
||||
const initializerProps = { ...props, ...fieldSchema?.['x-initializer-props'] };
|
||||
initializerPropsRef.current = initializerProps;
|
||||
|
||||
const InitializerComponent = useMemo(() => {
|
||||
let C = React.Fragment;
|
||||
if (!initializer) {
|
||||
return C;
|
||||
} else if (isPlainObj(initializer)) {
|
||||
C = (initializer as any).component || SchemaInitializer.Button;
|
||||
return (p) => <C {...initializer} {...initializerPropsRef.current} {...p} />;
|
||||
} else {
|
||||
C = initializer;
|
||||
return (p) => <C {...initializerPropsRef.current} {...p} />;
|
||||
}
|
||||
}, [initializer]);
|
||||
|
||||
if (!initializer) {
|
||||
return { exists: false, render: (props?: any) => render(null) };
|
||||
return { exists: false, InitializerComponent, render: (props?: any) => render(null) };
|
||||
}
|
||||
|
||||
if (isPlainObj(initializer)) {
|
||||
return {
|
||||
exists: true,
|
||||
InitializerComponent,
|
||||
render: (props?: any) => {
|
||||
const component = (initializer as any).component || SchemaInitializer.Button;
|
||||
return render(component, { ...initializer, ...initializerProps, ...props });
|
||||
@ -45,6 +57,7 @@ export const useSchemaInitializer = (name: string, props = {}) => {
|
||||
|
||||
return {
|
||||
exists: true,
|
||||
InitializerComponent,
|
||||
render: (props?: any) => render(initializer, { ...initializerProps, ...props }),
|
||||
};
|
||||
};
|
||||
|
@ -18,6 +18,18 @@ export const BlockInitializers = {
|
||||
title: '{{t("Table")}}',
|
||||
component: 'TableBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'List',
|
||||
type: 'item',
|
||||
title: '{{t("List")}}',
|
||||
component: 'ListBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'GridCard',
|
||||
type: 'item',
|
||||
title: '{{t("Grid Card")}}',
|
||||
component: 'GridCardBlockInitializer',
|
||||
},
|
||||
{
|
||||
key: 'form',
|
||||
type: 'item',
|
||||
|
@ -0,0 +1,299 @@
|
||||
import { useFieldSchema, Schema } from '@formily/react';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
|
||||
// 表单的操作配置
|
||||
export const GridCardActionInitializers = {
|
||||
title: "{{t('Configure actions')}}",
|
||||
icon: 'SettingOutlined',
|
||||
style: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: "{{t('Enable actions')}}",
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Filter')}}",
|
||||
component: 'FilterActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'left',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Add new')}}",
|
||||
component: 'CreateActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' && collection.template !== 'file';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Refresh')}}",
|
||||
component: 'RefreshActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Import')}}",
|
||||
component: 'ImportActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Export')}}",
|
||||
component: 'ExportActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// type: 'divider',
|
||||
// visible: () => {
|
||||
// const collection = useCollection();
|
||||
// return (collection as any).template !== 'view';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// type: 'subMenu',
|
||||
// title: '{{t("Customize")}}',
|
||||
// children: [
|
||||
// {
|
||||
// type: 'item',
|
||||
// title: '{{t("Bulk update")}}',
|
||||
// component: 'CustomizeActionInitializer',
|
||||
// schema: {
|
||||
// type: 'void',
|
||||
// title: '{{ t("Bulk update") }}',
|
||||
// 'x-component': 'Action',
|
||||
// 'x-align': 'right',
|
||||
// 'x-acl-action': 'update',
|
||||
// 'x-decorator': 'ACLActionProvider',
|
||||
// 'x-acl-action-props': {
|
||||
// skipScopeCheck: true,
|
||||
// },
|
||||
// 'x-action': 'customize:bulkUpdate',
|
||||
// 'x-designer': 'Action.Designer',
|
||||
// 'x-action-settings': {
|
||||
// assignedValues: {},
|
||||
// updateMode: 'selected',
|
||||
// onSuccess: {
|
||||
// manualClose: true,
|
||||
// redirecting: false,
|
||||
// successMessage: '{{t("Updated successfully")}}',
|
||||
// },
|
||||
// },
|
||||
// 'x-component-props': {
|
||||
// icon: 'EditOutlined',
|
||||
// useProps: '{{ useCustomizeBulkUpdateActionProps }}',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// type: 'item',
|
||||
// title: '{{t("Bulk edit")}}',
|
||||
// component: 'CustomizeBulkEditActionInitializer',
|
||||
// schema: {
|
||||
// 'x-align': 'right',
|
||||
// 'x-decorator': 'ACLActionProvider',
|
||||
// 'x-acl-action': 'update',
|
||||
// 'x-acl-action-props': {
|
||||
// skipScopeCheck: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// visible: () => {
|
||||
// const collection = useCollection();
|
||||
// return (collection as any).template !== 'view';
|
||||
// },
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
export const GridCardItemActionInitializers = {
|
||||
title: '{{t("Configure actions")}}',
|
||||
icon: 'SettingOutlined',
|
||||
items: [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Enable actions")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("View")}}',
|
||||
component: 'ViewActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'view',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Edit")}}',
|
||||
component: 'UpdateActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'update',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Delete")}}',
|
||||
component: 'DestroyActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'destroy',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'subMenu',
|
||||
title: '{{t("Customize")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Popup")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
type: 'void',
|
||||
title: '{{ t("Popup") }}',
|
||||
'x-action': 'customize:popup',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{ t("Popup") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializers',
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'RecordBlockInitializers',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Update record")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Update record") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'customize:update',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action': 'update',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
assignedValues: {},
|
||||
onSuccess: {
|
||||
manualClose: true,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Updated successfully")}}',
|
||||
},
|
||||
},
|
||||
'x-component-props': {
|
||||
useProps: '{{ useCustomizeUpdateActionProps }}',
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Custom request") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'customize:table:request',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
requestSettings: {},
|
||||
onSuccess: {
|
||||
manualClose: false,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Request success")}}',
|
||||
},
|
||||
},
|
||||
'x-component-props': {
|
||||
useProps: '{{ useCustomizeRequestActionProps }}',
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
@ -0,0 +1,299 @@
|
||||
import { useFieldSchema, Schema } from '@formily/react';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
|
||||
// 表单的操作配置
|
||||
export const ListActionInitializers = {
|
||||
title: "{{t('Configure actions')}}",
|
||||
icon: 'SettingOutlined',
|
||||
style: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: "{{t('Enable actions')}}",
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Filter')}}",
|
||||
component: 'FilterActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'left',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Add new')}}",
|
||||
component: 'CreateActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return collection.template !== 'view' && collection.template !== 'file';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Refresh')}}",
|
||||
component: 'RefreshActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Import')}}",
|
||||
component: 'ImportActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Export')}}",
|
||||
component: 'ExportActionInitializer',
|
||||
schema: {
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// type: 'divider',
|
||||
// visible: () => {
|
||||
// const collection = useCollection();
|
||||
// return (collection as any).template !== 'view';
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// type: 'subMenu',
|
||||
// title: '{{t("Customize")}}',
|
||||
// children: [
|
||||
// {
|
||||
// type: 'item',
|
||||
// title: '{{t("Bulk update")}}',
|
||||
// component: 'CustomizeActionInitializer',
|
||||
// schema: {
|
||||
// type: 'void',
|
||||
// title: '{{ t("Bulk update") }}',
|
||||
// 'x-component': 'Action',
|
||||
// 'x-align': 'right',
|
||||
// 'x-acl-action': 'update',
|
||||
// 'x-decorator': 'ACLActionProvider',
|
||||
// 'x-acl-action-props': {
|
||||
// skipScopeCheck: true,
|
||||
// },
|
||||
// 'x-action': 'customize:bulkUpdate',
|
||||
// 'x-designer': 'Action.Designer',
|
||||
// 'x-action-settings': {
|
||||
// assignedValues: {},
|
||||
// updateMode: 'selected',
|
||||
// onSuccess: {
|
||||
// manualClose: true,
|
||||
// redirecting: false,
|
||||
// successMessage: '{{t("Updated successfully")}}',
|
||||
// },
|
||||
// },
|
||||
// 'x-component-props': {
|
||||
// icon: 'EditOutlined',
|
||||
// useProps: '{{ useCustomizeBulkUpdateActionProps }}',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// type: 'item',
|
||||
// title: '{{t("Bulk edit")}}',
|
||||
// component: 'CustomizeBulkEditActionInitializer',
|
||||
// schema: {
|
||||
// 'x-align': 'right',
|
||||
// 'x-decorator': 'ACLActionProvider',
|
||||
// 'x-acl-action': 'update',
|
||||
// 'x-acl-action-props': {
|
||||
// skipScopeCheck: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// visible: () => {
|
||||
// const collection = useCollection();
|
||||
// return (collection as any).template !== 'view';
|
||||
// },
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
export const ListItemActionInitializers = {
|
||||
title: '{{t("Configure actions")}}',
|
||||
icon: 'SettingOutlined',
|
||||
items: [
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: '{{t("Enable actions")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("View")}}',
|
||||
component: 'ViewActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'view',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Edit")}}',
|
||||
component: 'UpdateActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'update',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Delete")}}',
|
||||
component: 'DestroyActionInitializer',
|
||||
schema: {
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'destroy',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-align': 'left',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'subMenu',
|
||||
title: '{{t("Customize")}}',
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Popup")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
type: 'void',
|
||||
title: '{{ t("Popup") }}',
|
||||
'x-action': 'customize:popup',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
type: 'void',
|
||||
title: '{{ t("Popup") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'TabPaneInitializers',
|
||||
properties: {
|
||||
tab1: {
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'RecordBlockInitializers',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Update record")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Update record") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'customize:update',
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-acl-action': 'update',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
assignedValues: {},
|
||||
onSuccess: {
|
||||
manualClose: true,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Updated successfully")}}',
|
||||
},
|
||||
},
|
||||
'x-component-props': {
|
||||
useProps: '{{ useCustomizeUpdateActionProps }}',
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
title: '{{t("Custom request")}}',
|
||||
component: 'CustomizeActionInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Custom request") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-action': 'customize:table:request',
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-action-settings': {
|
||||
requestSettings: {},
|
||||
onSuccess: {
|
||||
manualClose: false,
|
||||
redirecting: false,
|
||||
successMessage: '{{t("Request success")}}',
|
||||
},
|
||||
},
|
||||
'x-component-props': {
|
||||
useProps: '{{ useCustomizeRequestActionProps }}',
|
||||
},
|
||||
},
|
||||
visible: () => {
|
||||
const collection = useCollection();
|
||||
return (collection as any).template !== 'view';
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
@ -33,7 +33,7 @@ export const TableColumnInitializers = (props: any) => {
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
|
||||
children: Object.values(inherit)[0],
|
||||
children: Object.values(inherit)[0].filter((v)=>!v?.field?.isForeignKey),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -21,5 +21,8 @@ export * from './TableColumnInitializers';
|
||||
export * from './TableSelectorInitializers';
|
||||
export * from './TabPaneInitializers';
|
||||
export * from './GanttActionInitializers';
|
||||
export * from './DetailsActionInitializers';
|
||||
export * from './ListActionInitializers';
|
||||
export * from './GridCardActionInitializers';
|
||||
// association filter
|
||||
export * from '../../schema-component/antd/association-filter/AssociationFilter';
|
||||
|
@ -3,7 +3,6 @@ import { css } from '@emotion/css';
|
||||
import { RecursionField, observer, useField, useFieldSchema } from '@formily/react';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDesignable } from '../../';
|
||||
import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
|
||||
import { CollectionProvider, useCollection, useCollectionManager } from '../../collection-manager';
|
||||
@ -85,11 +84,48 @@ export const CreateRecordAction = observer((props: any) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const collection = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field: any = useField();
|
||||
const [currentCollection, setCurrentCollection] = useState(collection.name);
|
||||
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
||||
const values = useRecord();
|
||||
const ctx = useActionContext();
|
||||
useEffect(() => {
|
||||
field.linkageProperty = {};
|
||||
linkageRules
|
||||
.filter((k) => !k.disabled)
|
||||
.map((v) => {
|
||||
return v.actions?.map((h) => {
|
||||
linkageAction(h.operator, field, v.condition, values);
|
||||
});
|
||||
});
|
||||
}, [linkageRules, values]);
|
||||
return (
|
||||
<div className={actionDesignerCss}>
|
||||
<ActionContext.Provider value={{ ...ctx, visible, setVisible }}>
|
||||
<CreateAction
|
||||
{...props}
|
||||
onClick={(name) => {
|
||||
setVisible(true);
|
||||
setCurrentCollection(name);
|
||||
}}
|
||||
/>
|
||||
<CollectionProvider name={currentCollection}>
|
||||
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
|
||||
</CollectionProvider>
|
||||
</ActionContext.Provider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const CreateAction = observer((props: any) => {
|
||||
const { onClick } = props;
|
||||
const collection = useCollection();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const enableChildren = fieldSchema['x-enable-children'] || [];
|
||||
const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current'];
|
||||
const field: any = useField();
|
||||
const { t } = useTranslation();
|
||||
const componentType = field.componentProps.type || 'primary';
|
||||
console.log(componentType)
|
||||
const { getChildrenCollections } = useCollectionManager();
|
||||
const totalChildCollections = getChildrenCollections(collection.name);
|
||||
const inheritsCollections = enableChildren
|
||||
@ -109,10 +145,8 @@ export const CreateRecordAction = observer((props: any) => {
|
||||
.filter((v) => {
|
||||
return v && actionAclCheck(`${v.name}:create`);
|
||||
});
|
||||
const [currentCollection, setCurrentCollection] = useState(collection.name);
|
||||
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
|
||||
const values = useRecord();
|
||||
const ctx = useActionContext();
|
||||
const compile = useCompile();
|
||||
const { designable } = useDesignable();
|
||||
const icon = props.icon || <PlusOutlined />;
|
||||
@ -123,8 +157,7 @@ export const CreateRecordAction = observer((props: any) => {
|
||||
<Menu.Item
|
||||
key={option.name}
|
||||
onClick={(info) => {
|
||||
setVisible(true);
|
||||
setCurrentCollection(option.name);
|
||||
onClick?.(option.name);
|
||||
}}
|
||||
>
|
||||
{compile(option.title)}
|
||||
@ -145,56 +178,49 @@ export const CreateRecordAction = observer((props: any) => {
|
||||
}, [linkageRules, values]);
|
||||
return (
|
||||
<div className={actionDesignerCss}>
|
||||
<ActionContext.Provider value={{ ...ctx, visible, setVisible }}>
|
||||
{inheritsCollections?.length > 0 ? (
|
||||
allowAddToCurrent === undefined || allowAddToCurrent ? (
|
||||
<Dropdown.Button
|
||||
type={componentType}
|
||||
icon={<DownOutlined />}
|
||||
buttonsRender={([leftButton, rightButton]) => [
|
||||
leftButton,
|
||||
React.cloneElement(rightButton as React.ReactElement<any, string>, { loading: false }),
|
||||
]}
|
||||
overlay={menu}
|
||||
onClick={(info) => {
|
||||
setVisible(true);
|
||||
setCurrentCollection(collection.name);
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{props.children}
|
||||
</Dropdown.Button>
|
||||
) : (
|
||||
<Dropdown overlay={menu}>
|
||||
{
|
||||
<Button icon={icon} type={'primary'}>
|
||||
{props.children} <DownOutlined />
|
||||
</Button>
|
||||
}
|
||||
</Dropdown>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
{inheritsCollections?.length > 0 ? (
|
||||
allowAddToCurrent === undefined || allowAddToCurrent ? (
|
||||
<Dropdown.Button
|
||||
type={componentType}
|
||||
disabled={field.disabled}
|
||||
danger={componentType === 'danger'}
|
||||
icon={icon}
|
||||
icon={<DownOutlined />}
|
||||
buttonsRender={([leftButton, rightButton]) => [
|
||||
leftButton,
|
||||
React.cloneElement(rightButton as React.ReactElement<any, string>, { loading: false }),
|
||||
]}
|
||||
overlay={menu}
|
||||
onClick={(info) => {
|
||||
setVisible(true);
|
||||
setCurrentCollection(collection.name);
|
||||
}}
|
||||
style={{
|
||||
display: !designable && field?.data?.hidden && 'none',
|
||||
opacity: designable && field?.data?.hidden && 0.1,
|
||||
onClick?.(collection.name);
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{props.children}
|
||||
</Button>
|
||||
)}
|
||||
<CollectionProvider name={currentCollection}>
|
||||
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
|
||||
</CollectionProvider>
|
||||
</ActionContext.Provider>
|
||||
</Dropdown.Button>
|
||||
) : (
|
||||
<Dropdown overlay={menu}>
|
||||
{
|
||||
<Button icon={icon} type={componentType}>
|
||||
{props.children} <DownOutlined />
|
||||
</Button>
|
||||
}
|
||||
</Dropdown>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
type={componentType}
|
||||
disabled={field.disabled}
|
||||
danger={componentType === 'danger'}
|
||||
icon={icon}
|
||||
onClick={(info) => {
|
||||
onClick?.(collection.name);
|
||||
}}
|
||||
style={{
|
||||
display: !designable && field?.data?.hidden && 'none',
|
||||
opacity: designable && field?.data?.hidden && 0.1,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { OrderedListOutlined } from '@ant-design/icons';
|
||||
import { createGridCardBlockSchema, createListBlockSchema } from '../utils';
|
||||
import { DataBlockInitializer } from './DataBlockInitializer';
|
||||
import { useCollectionManager } from '../../collection-manager';
|
||||
|
||||
export const GridCardBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { getCollection } = useCollectionManager();
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
{...props}
|
||||
icon={<OrderedListOutlined />}
|
||||
componentType={'GridCard'}
|
||||
onCreateBlockSchema={async ({ item }) => {
|
||||
const collection = getCollection(item.name);
|
||||
const schema = createGridCardBlockSchema({
|
||||
collection: item.name,
|
||||
rowKey: collection.filterTargetKey || 'id',
|
||||
});
|
||||
insert(schema);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -3,6 +3,8 @@ import React from 'react';
|
||||
|
||||
import { SchemaInitializer } from '..';
|
||||
import { useCurrentSchema } from '../utils';
|
||||
import { useBlockRequestContext } from '../../block-provider';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
|
||||
export const InitializerWithSwitch = (props) => {
|
||||
const { type, schema, item, insert, remove: passInRemove } = props;
|
||||
|
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { OrderedListOutlined } from '@ant-design/icons';
|
||||
import { createListBlockSchema } from '../utils';
|
||||
import { DataBlockInitializer } from './DataBlockInitializer';
|
||||
import { useCollectionManager } from '../../collection-manager';
|
||||
|
||||
export const ListBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { getCollection } = useCollectionManager();
|
||||
return (
|
||||
<DataBlockInitializer
|
||||
{...props}
|
||||
icon={<OrderedListOutlined />}
|
||||
componentType={'List'}
|
||||
onCreateBlockSchema={async ({ item }) => {
|
||||
const collection = getCollection(item.name);
|
||||
const schema = createListBlockSchema({
|
||||
collection: item.name,
|
||||
rowKey: collection.filterTargetKey || 'id',
|
||||
});
|
||||
insert(schema);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -29,8 +29,10 @@ export * from './FilterFormBlockInitializer';
|
||||
export * from './FormBlockInitializer';
|
||||
export * from './G2PlotInitializer';
|
||||
export * from './GanttBlockInitializer';
|
||||
export * from './GridCardBlockInitializer';
|
||||
export * from './InitializerWithSwitch';
|
||||
export * from './KanbanBlockInitializer';
|
||||
export * from './ListBlockInitializer';
|
||||
export * from './MarkdownBlockInitializer';
|
||||
export * from './PrintActionInitializer';
|
||||
export * from './RecordAssociationBlockInitializer';
|
||||
@ -41,6 +43,7 @@ export * from './RecordFormBlockInitializer';
|
||||
export * from './RecordReadPrettyAssociationFormBlockInitializer';
|
||||
export * from './RecordReadPrettyFormBlockInitializer';
|
||||
export * from './RefreshActionInitializer';
|
||||
export * from './SelectActionInitializer';
|
||||
export * from './SubmitActionInitializer';
|
||||
export * from './TableActionColumnInitializer';
|
||||
export * from './TableBlockInitializer';
|
||||
@ -49,4 +52,3 @@ export * from './TableSelectorInitializer';
|
||||
export * from './UpdateActionInitializer';
|
||||
export * from './UpdateSubmitActionInitializer';
|
||||
export * from './ViewActionInitializer';
|
||||
export * from './SelectActionInitializer';
|
||||
|
@ -950,6 +950,177 @@ export const createDetailsBlockSchema = (options) => {
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const createListBlockSchema = (options) => {
|
||||
const {
|
||||
formItemInitializers = 'ReadPrettyFormItemInitializers',
|
||||
actionInitializers = 'ListActionInitializers',
|
||||
itemActionInitializers = 'ListItemActionInitializers',
|
||||
collection,
|
||||
association,
|
||||
resource,
|
||||
template,
|
||||
...others
|
||||
} = options;
|
||||
const resourceName = resource || association || collection;
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-acl-action': `${resourceName}:view`,
|
||||
'x-decorator': 'List.Decorator',
|
||||
'x-decorator-props': {
|
||||
resource: resourceName,
|
||||
collection,
|
||||
association,
|
||||
readPretty: true,
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 10,
|
||||
},
|
||||
runWhenParamsChanged: true,
|
||||
...others,
|
||||
},
|
||||
'x-component': 'CardItem',
|
||||
'x-designer': 'List.Designer',
|
||||
properties: {
|
||||
actionBar: {
|
||||
type: 'void',
|
||||
'x-initializer': actionInitializers,
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
list: {
|
||||
type: 'array',
|
||||
'x-component': 'List',
|
||||
'x-component-props': {
|
||||
props: '{{ useListBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
item: {
|
||||
type: 'object',
|
||||
'x-component': 'List.Item',
|
||||
'x-read-pretty': true,
|
||||
'x-component-props': {
|
||||
useProps: '{{ useListItemProps }}',
|
||||
},
|
||||
properties: {
|
||||
grid: template || {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': formItemInitializers,
|
||||
'x-initializer-props': {
|
||||
useProps: '{{ useListItemInitializerProps }}',
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
actionBar: {
|
||||
type: 'void',
|
||||
'x-align': 'left',
|
||||
'x-initializer': itemActionInitializers,
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useListActionBarProps }}',
|
||||
layout: 'one-column',
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const createGridCardBlockSchema = (options) => {
|
||||
const {
|
||||
formItemInitializers = 'ReadPrettyFormItemInitializers',
|
||||
actionInitializers = 'GridCardActionInitializers',
|
||||
itemActionInitializers = 'GridCardItemActionInitializers',
|
||||
collection,
|
||||
association,
|
||||
resource,
|
||||
template,
|
||||
...others
|
||||
} = options;
|
||||
const resourceName = resource || association || collection;
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-acl-action': `${resourceName}:view`,
|
||||
'x-decorator': 'GridCard.Decorator',
|
||||
'x-decorator-props': {
|
||||
resource: resourceName,
|
||||
collection,
|
||||
association,
|
||||
readPretty: true,
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 12,
|
||||
},
|
||||
runWhenParamsChanged: true,
|
||||
...others,
|
||||
},
|
||||
'x-component': 'BlockItem',
|
||||
'x-designer': 'GridCard.Designer',
|
||||
properties: {
|
||||
actionBar: {
|
||||
type: 'void',
|
||||
'x-initializer': actionInitializers,
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
list: {
|
||||
type: 'array',
|
||||
'x-component': 'GridCard',
|
||||
'x-component-props': {
|
||||
props: '{{ useGridCardBlockProps }}',
|
||||
},
|
||||
properties: {
|
||||
item: {
|
||||
type: 'object',
|
||||
'x-component': 'GridCard.Item',
|
||||
'x-read-pretty': true,
|
||||
'x-component-props': {
|
||||
useProps: '{{ useGridCardItemProps }}',
|
||||
},
|
||||
properties: {
|
||||
grid: template || {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': formItemInitializers,
|
||||
'x-initializer-props': {
|
||||
useProps: '{{ useGridCardItemInitializerProps }}',
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
actionBar: {
|
||||
type: 'void',
|
||||
'x-align': 'left',
|
||||
'x-initializer': itemActionInitializers,
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
useProps: '{{ useGridCardActionBarProps }}',
|
||||
layout: 'one-column',
|
||||
},
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return schema;
|
||||
};
|
||||
export const createFormBlockSchema = (options) => {
|
||||
const {
|
||||
formItemInitializers = 'FormItemInitializers',
|
||||
@ -1177,7 +1348,7 @@ export const createTableBlockSchema = (options) => {
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
@ -1276,7 +1447,7 @@ export const createTableSelectorSchema = (options) => {
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
@ -1546,7 +1717,7 @@ export const createKanbanBlockSchema = (options) => {
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
properties: {},
|
||||
|
161
packages/core/client/src/schema-items/GeneralSchemaItems.tsx
Normal file
161
packages/core/client/src/schema-items/GeneralSchemaItems.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
import { useCollectionManager, useCollection } from '../collection-manager';
|
||||
import { useDesignable } from '../schema-component';
|
||||
import { SchemaSettings } from '../schema-settings';
|
||||
|
||||
export const GeneralSchemaItems: React.FC<{
|
||||
required?: boolean;
|
||||
}> = (props) => {
|
||||
const { required = true } = props;
|
||||
const { getCollectionJoinField } = useCollectionManager();
|
||||
const { getField } = useCollection();
|
||||
const field = useField<Field>();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const { dn, refresh } = useDesignable();
|
||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||
|
||||
return (
|
||||
<>
|
||||
{collectionField && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-field-title"
|
||||
title={t('Edit field title')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit field title'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Field title'),
|
||||
default: field?.title,
|
||||
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ title }) => {
|
||||
if (title) {
|
||||
field.title = title;
|
||||
fieldSchema.title = title;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
title: fieldSchema.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SchemaSettings.SwitchItem
|
||||
checked={field.decoratorProps.showTitle ?? true}
|
||||
title={t('Display title')}
|
||||
onChange={(checked) => {
|
||||
field.decoratorProps.showTitle = checked;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-decorator-props': {
|
||||
...fieldSchema['x-decorator-props'],
|
||||
showTitle: checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
></SchemaSettings.SwitchItem>
|
||||
{!field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-description"
|
||||
title={t('Edit description')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
description: {
|
||||
// title: t('Description'),
|
||||
default: field?.description,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ description }) => {
|
||||
field.description = description;
|
||||
fieldSchema.description = description;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
description: fieldSchema.description,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-tooltip"
|
||||
title={t('Edit tooltip')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
tooltip: {
|
||||
default: fieldSchema?.['x-decorator-props']?.tooltip,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ tooltip }) => {
|
||||
field.decoratorProps.tooltip = tooltip;
|
||||
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
||||
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!field.readPretty && fieldSchema['x-component'] !== 'FormField' && required && (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="required"
|
||||
title={t('Required')}
|
||||
checked={fieldSchema.required as boolean}
|
||||
onChange={(required) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
field.required = required;
|
||||
fieldSchema['required'] = required;
|
||||
schema['required'] = required;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1 +1,2 @@
|
||||
export * from './OpenModeSchemaItems';
|
||||
export * from './GeneralSchemaItems';
|
||||
|
@ -3,7 +3,7 @@ import { css } from '@emotion/css';
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { Space } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component';
|
||||
import { gridRowColWrap } from '../schema-initializer/utils';
|
||||
@ -43,14 +43,24 @@ export const GeneralSchemaDesigner = (props: any) => {
|
||||
field,
|
||||
fieldSchema,
|
||||
};
|
||||
if (!designable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rowCtx = useGridRowContext();
|
||||
const ctx = useGridContext();
|
||||
const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName)
|
||||
? `${template?.name} ${t('(Fields only)')}`
|
||||
: template?.name;
|
||||
const initializerProps = useMemo(() => {
|
||||
return {
|
||||
insertPosition: 'afterEnd',
|
||||
wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap,
|
||||
component: <PlusOutlined style={{ cursor: 'pointer', fontSize: 14 }} />,
|
||||
};
|
||||
}, [rowCtx?.cols?.length]);
|
||||
|
||||
if (!designable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'general-schema-designer'}>
|
||||
{title && (
|
||||
@ -73,11 +83,11 @@ export const GeneralSchemaDesigner = (props: any) => {
|
||||
</DragHandler>
|
||||
)}
|
||||
{!disableInitializer &&
|
||||
ctx?.renderSchemaInitializer?.({
|
||||
insertPosition: 'afterEnd',
|
||||
wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap,
|
||||
component: <PlusOutlined style={{ cursor: 'pointer', fontSize: 14 }} />,
|
||||
})}
|
||||
(ctx?.InitializerComponent ? (
|
||||
<ctx.InitializerComponent {...initializerProps} />
|
||||
) : (
|
||||
ctx?.renderSchemaInitializer?.(initializerProps)
|
||||
))}
|
||||
<SchemaSettings title={<MenuOutlined style={{ cursor: 'pointer', fontSize: 12 }} />} {...schemaSettingsProps}>
|
||||
{props.children}
|
||||
</SchemaSettings>
|
||||
|
@ -1111,13 +1111,13 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop
|
||||
fieldSchema['x-enable-children'] = enableChildren;
|
||||
fieldSchema['x-allow-add-to-current'] = v.allowAddToCurrent;
|
||||
fieldSchema['x-component-props'] = {
|
||||
openMode: 'drawer',
|
||||
...fieldSchema['x-component-props'],
|
||||
component: 'CreateRecordAction',
|
||||
};
|
||||
schema['x-enable-children'] = enableChildren;
|
||||
schema['x-allow-add-to-current'] = v.allowAddToCurrent;
|
||||
schema['x-component-props'] = {
|
||||
openMode: 'drawer',
|
||||
...fieldSchema['x-component-props'],
|
||||
component: 'CreateRecordAction',
|
||||
};
|
||||
dn.emit('patch', {
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { useCollectionManager } from '../../collection-manager';
|
||||
|
||||
/**
|
||||
* 是否显示 `允许多选` 开关
|
||||
*/
|
||||
export function useIsShowMultipleSwitch() {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { getCollectionField } = useCollectionManager();
|
||||
|
||||
const collectionField = getCollectionField(fieldSchema['x-collection-field']);
|
||||
const uiSchema = collectionField?.uiSchema || fieldSchema;
|
||||
const hasMultiple = uiSchema['x-component-props']?.multiple === true;
|
||||
|
||||
return function IsShowMultipleSwitch() {
|
||||
return !field.readPretty && fieldSchema['x-component'] !== 'TableField' && hasMultiple;
|
||||
};
|
||||
}
|
@ -2,6 +2,7 @@ import { Field } from '@formily/core';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
GeneralSchemaItems,
|
||||
SchemaSettings,
|
||||
isPatternDisabled,
|
||||
useCollection,
|
||||
@ -40,122 +41,7 @@ const Designer = () => {
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-field-title"
|
||||
title={t('Edit field title')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit field title'),
|
||||
properties: {
|
||||
title: {
|
||||
title: t('Field title'),
|
||||
default: field?.title,
|
||||
description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ title }) => {
|
||||
if (title) {
|
||||
field.title = title;
|
||||
fieldSchema.title = title;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
title: fieldSchema.title,
|
||||
},
|
||||
});
|
||||
}
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
{!field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-description"
|
||||
title={t('Edit description')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
description: {
|
||||
// title: t('Description'),
|
||||
default: field?.description,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ description }) => {
|
||||
field.description = description;
|
||||
fieldSchema.description = description;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
description: fieldSchema.description,
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{field.readPretty && (
|
||||
<SchemaSettings.ModalItem
|
||||
key="edit-tooltip"
|
||||
title={t('Edit tooltip')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Edit description'),
|
||||
properties: {
|
||||
tooltip: {
|
||||
default: fieldSchema?.['x-decorator-props']?.tooltip,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ tooltip }) => {
|
||||
field.decoratorProps.tooltip = tooltip;
|
||||
fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {};
|
||||
fieldSchema['x-decorator-props']['tooltip'] = tooltip;
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
'x-uid': fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
dn.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!field.readPretty && (
|
||||
<SchemaSettings.SwitchItem
|
||||
key="required"
|
||||
title={t('Required')}
|
||||
checked={fieldSchema.required as boolean}
|
||||
onChange={(required) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
};
|
||||
field.required = required;
|
||||
fieldSchema['required'] = required;
|
||||
schema['required'] = required;
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<GeneralSchemaItems />
|
||||
{form && !form?.readPretty && !isPatternDisabled(fieldSchema) && (
|
||||
<SchemaSettings.SelectItem
|
||||
key="pattern"
|
||||
|
24
packages/plugins/workflow/src/client/CanvasContent.tsx
Normal file
24
packages/plugins/workflow/src/client/CanvasContent.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { Tag } from 'antd';
|
||||
import { cx } from '@emotion/css';
|
||||
|
||||
import { Branch } from './Branch';
|
||||
import { lang } from './locale';
|
||||
import { branchBlockClass, nodeCardClass, nodeMetaClass } from './style';
|
||||
import { TriggerConfig } from './triggers';
|
||||
|
||||
export function CanvasContent({ entry }) {
|
||||
return (
|
||||
<div className="workflow-canvas">
|
||||
<TriggerConfig />
|
||||
<div className={branchBlockClass}>
|
||||
<Branch entry={entry} />
|
||||
</div>
|
||||
<div className={cx('end', nodeCardClass)}>
|
||||
<div className={cx(nodeMetaClass)}>
|
||||
<Tag color="#333">{lang('End')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,35 +1,117 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Tag, Breadcrumb } from 'antd';
|
||||
import { cx } from '@emotion/css';
|
||||
import { css } from '@emotion/css';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useCompile, useDocumentTitle, useResourceActionContext } from '@nocobase/client';
|
||||
import {
|
||||
ActionContext,
|
||||
SchemaComponent,
|
||||
useCompile,
|
||||
useDocumentTitle,
|
||||
useResourceActionContext,
|
||||
} from '@nocobase/client';
|
||||
import { str2moment } from '@nocobase/utils/client';
|
||||
|
||||
import { FlowContext } from './FlowContext';
|
||||
import { branchBlockClass, nodeCardClass, nodeMetaClass } from './style';
|
||||
import { TriggerConfig } from './triggers';
|
||||
import { Branch } from './Branch';
|
||||
import { ExecutionStatusOptionsMap } from './constants';
|
||||
import { lang } from './locale';
|
||||
import { FlowContext, useFlowContext } from './FlowContext';
|
||||
import { nodeTitleClass } from './style';
|
||||
import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants';
|
||||
import { lang, NAMESPACE } from './locale';
|
||||
import { linkNodes } from './utils';
|
||||
import { instructions } from './nodes';
|
||||
import { CanvasContent } from './CanvasContent';
|
||||
|
||||
function attachJobs(nodes, jobs: any[] = []): void {
|
||||
const nodesMap = new Map();
|
||||
nodes.forEach((item) => nodesMap.set(item.id, item));
|
||||
const jobsMap = new Map();
|
||||
jobs.forEach((item) => jobsMap.set(item.nodeId, item));
|
||||
for (const node of nodesMap.values()) {
|
||||
if (jobsMap.has(node.id)) {
|
||||
node.job = jobsMap.get(node.id);
|
||||
}
|
||||
}
|
||||
nodes.forEach((item) => {
|
||||
item.jobs = [];
|
||||
nodesMap.set(item.id, item);
|
||||
});
|
||||
jobs.forEach((item) => {
|
||||
const node = nodesMap.get(item.nodeId);
|
||||
node.jobs.push(item);
|
||||
item.node = {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
type: node.type,
|
||||
};
|
||||
});
|
||||
nodes.forEach((item) => {
|
||||
item.jobs = item.jobs.sort((a, b) => a.id - b.id);
|
||||
});
|
||||
}
|
||||
|
||||
function JobModal() {
|
||||
const compile = useCompile();
|
||||
const { viewJob: job, setViewJob } = useFlowContext();
|
||||
const { node = {} } = job ?? {};
|
||||
const instruction = instructions.get(node.type);
|
||||
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible: Boolean(job), setVisible: setViewJob }}>
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
[`${job?.id}-modal`]: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
initialValue: job,
|
||||
},
|
||||
'x-component': 'Action.Modal',
|
||||
title: (
|
||||
<div className={nodeTitleClass}>
|
||||
<Tag>{compile(instruction?.title)}</Tag>
|
||||
<strong>{node.title}</strong>
|
||||
<span className="workflow-node-id">#{node.id}</span>
|
||||
</div>
|
||||
),
|
||||
properties: {
|
||||
status: {
|
||||
type: 'number',
|
||||
title: `{{t("Status", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: JobStatusOptions,
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
title: `{{t("Executed at", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
result: {
|
||||
type: 'object',
|
||||
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.JSON',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
padding: 1em;
|
||||
background-color: #eee;
|
||||
`,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExecutionCanvas() {
|
||||
const compile = useCompile();
|
||||
const { data, loading } = useResourceActionContext();
|
||||
const { setTitle } = useDocumentTitle();
|
||||
const [viewJob, setViewJob] = useState(null);
|
||||
useEffect(() => {
|
||||
const { workflow } = data?.data ?? {};
|
||||
setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
|
||||
@ -58,6 +140,8 @@ export function ExecutionCanvas() {
|
||||
workflow: workflow.type ? workflow : null,
|
||||
nodes,
|
||||
execution,
|
||||
viewJob,
|
||||
setViewJob
|
||||
}}
|
||||
>
|
||||
<div className="workflow-toolbar">
|
||||
@ -79,17 +163,8 @@ export function ExecutionCanvas() {
|
||||
<time>{str2moment(execution.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||
</aside>
|
||||
</div>
|
||||
<div className="workflow-canvas">
|
||||
<TriggerConfig />
|
||||
<div className={branchBlockClass}>
|
||||
<Branch entry={entry} />
|
||||
</div>
|
||||
<div className={cx(nodeCardClass)}>
|
||||
<div className={cx(nodeMetaClass)}>
|
||||
<Tag color="#333">{lang('End')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CanvasContent entry={entry} />
|
||||
<JobModal />
|
||||
</FlowContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -16,13 +16,12 @@ import {
|
||||
} from '@nocobase/client';
|
||||
|
||||
import { FlowContext, useFlowContext } from './FlowContext';
|
||||
import { branchBlockClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style';
|
||||
import { TriggerConfig } from './triggers';
|
||||
import { Branch } from './Branch';
|
||||
import { workflowVersionDropdownClass } from './style';
|
||||
import { executionSchema } from './schemas/executions';
|
||||
import { ExecutionLink } from './ExecutionLink';
|
||||
import { lang } from './locale';
|
||||
import { linkNodes } from './utils';
|
||||
import { CanvasContent } from './CanvasContent';
|
||||
|
||||
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
||||
const { workflow } = useFlowContext();
|
||||
@ -214,17 +213,7 @@ export function WorkflowCanvas() {
|
||||
</ActionContext.Provider>
|
||||
</aside>
|
||||
</div>
|
||||
<div className="workflow-canvas">
|
||||
<TriggerConfig />
|
||||
<div className={branchBlockClass}>
|
||||
<Branch entry={entry} />
|
||||
</div>
|
||||
<div className={cx('end', nodeCardClass)}>
|
||||
<div className={cx(nodeMetaClass)}>
|
||||
<Tag color="#333">{lang('End')}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CanvasContent entry={entry} />
|
||||
</FlowContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export default observer(({ value, disabled, onChange }: any) => {
|
||||
!['formula'].includes(field.type),
|
||||
);
|
||||
|
||||
const unassignedFields = fields.filter((field) => !(field.name in value));
|
||||
const unassignedFields = fields.filter((field) => !value || !(field.name in value));
|
||||
const scope = useWorkflowVariableOptions();
|
||||
const mergedDisabled = disabled || form.disabled;
|
||||
|
||||
@ -69,7 +69,7 @@ export default observer(({ value, disabled, onChange }: any) => {
|
||||
{fields.length ? (
|
||||
<CollectionProvider collection={getCollection(collectionName)}>
|
||||
{fields
|
||||
.filter((field) => field.name in value)
|
||||
.filter((field) => value && field.name in value)
|
||||
.map((field) => {
|
||||
// constant for associations to use Input, others to use CollectionField
|
||||
// dynamic values only support belongsTo/hasOne association, other association type should disable
|
||||
|
@ -116,6 +116,15 @@ export default {
|
||||
'Continue after all branches succeeded': '全部分支都成功后才能继续',
|
||||
'Continue after any branch succeeded': '任意分支成功后就继续',
|
||||
'Continue after any branch succeeded, or exit after any branch failed': '任意分支成功继续,或失败后退出',
|
||||
Loop: '循环',
|
||||
'Loop target': '循环对象',
|
||||
'Loop index': '当前索引',
|
||||
'Loop length': '循环长度',
|
||||
'Loop will cause performance issue based on the quantity, please use with caution.':
|
||||
'循环次数过高可能引起性能问题,请谨慎使用。',
|
||||
'Scope variables': '局域变量',
|
||||
'Single number will be treated as times, single string will be treated as chars, other non-array value will be turned into a single item array.':
|
||||
'单一数字值将被视为次数,单一字符串值将被视为字符数组,其他非数组值将被转换为值数组。',
|
||||
Delay: '延时',
|
||||
Duration: '时长',
|
||||
'End Status': '到时状态',
|
||||
|
@ -170,7 +170,7 @@ export default {
|
||||
RadioWithTooltip,
|
||||
DynamicConfig,
|
||||
},
|
||||
getOptions(config, types) {
|
||||
useVariables(current, types) {
|
||||
if (
|
||||
types &&
|
||||
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
||||
|
@ -40,7 +40,7 @@ export default {
|
||||
CollectionFieldset,
|
||||
FieldsSelect,
|
||||
},
|
||||
getOptions(config, types) {
|
||||
useVariables({ config }, types) {
|
||||
return useCollectionFieldOptions({
|
||||
collection: config.collection,
|
||||
types,
|
||||
|
@ -2,10 +2,10 @@ import React, { useState, useContext } from 'react';
|
||||
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { Button, message, Modal, Tag, Alert, Input } from 'antd';
|
||||
import { Button, message, Modal, Tag, Alert, Input, Dropdown } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Registry, parse } from '@nocobase/utils/client';
|
||||
import { Registry, parse, str2moment } from '@nocobase/utils/client';
|
||||
import {
|
||||
ActionContext,
|
||||
SchemaComponent,
|
||||
@ -17,13 +17,14 @@ import {
|
||||
useResourceActionContext,
|
||||
} from '@nocobase/client';
|
||||
|
||||
import { nodeBlockClass, nodeCardClass, nodeClass, nodeMetaClass, nodeTitleClass } from '../style';
|
||||
import { nodeBlockClass, nodeCardClass, nodeClass, nodeJobButtonClass, nodeMetaClass, nodeTitleClass } from '../style';
|
||||
import { AddButton } from '../AddButton';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
|
||||
import calculation from './calculation';
|
||||
import condition from './condition';
|
||||
import parallel from './parallel';
|
||||
import loop from './loop';
|
||||
import delay from './delay';
|
||||
|
||||
import manual from './manual';
|
||||
@ -32,8 +33,8 @@ import query from './query';
|
||||
import create from './create';
|
||||
import update from './update';
|
||||
import destroy from './destroy';
|
||||
import { JobStatusOptions, JobStatusOptionsMap } from '../constants';
|
||||
import { lang, NAMESPACE } from '../locale';
|
||||
import { JobStatusOptionsMap } from '../constants';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
import request from './request';
|
||||
import { VariableOptions } from '../variable';
|
||||
|
||||
@ -48,16 +49,18 @@ export interface Instruction {
|
||||
components?: { [key: string]: any };
|
||||
render?(props): React.ReactNode;
|
||||
endding?: boolean;
|
||||
getOptions?(config, types?): VariableOptions;
|
||||
useVariables?(node, types?): VariableOptions;
|
||||
useScopeVariables?(node, types?): VariableOptions;
|
||||
useInitializers?(node): SchemaInitializerItemOptions | null;
|
||||
initializers?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export const instructions = new Registry<Instruction>();
|
||||
|
||||
instructions.register('calculation', calculation);
|
||||
instructions.register('condition', condition);
|
||||
instructions.register('parallel', parallel);
|
||||
instructions.register('calculation', calculation);
|
||||
instructions.register('loop', loop);
|
||||
instructions.register('delay', delay);
|
||||
|
||||
instructions.register('manual', manual);
|
||||
@ -112,6 +115,21 @@ export function useAvailableUpstreams(node) {
|
||||
return stack;
|
||||
}
|
||||
|
||||
export function useUpstreamScopes(node) {
|
||||
const stack: any[] = [];
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (let current = node; current; current = current.upstream) {
|
||||
if (current.upstream && current.branchIndex != null) {
|
||||
stack.push(current.upstream);
|
||||
}
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
export function Node({ data }) {
|
||||
const instruction = instructions.get(data.type);
|
||||
|
||||
@ -212,18 +230,28 @@ export function RemoveButton() {
|
||||
);
|
||||
}
|
||||
|
||||
function InnerJobButton({ job, ...props }) {
|
||||
const { icon, color } = JobStatusOptionsMap[job.status];
|
||||
|
||||
return (
|
||||
<Button {...props} shape="circle" className={cx(nodeJobButtonClass, 'workflow-node-job-button')}>
|
||||
<Tag color={color}>{icon}</Tag>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function JobButton() {
|
||||
const compile = useCompile();
|
||||
const { execution } = useFlowContext();
|
||||
const { id, type, title, job } = useNodeContext() ?? {};
|
||||
const { execution, setViewJob } = useFlowContext();
|
||||
const { jobs } = useNodeContext() ?? {};
|
||||
if (!execution) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
if (!jobs.length) {
|
||||
return (
|
||||
<span
|
||||
className={cx(
|
||||
nodeJobButtonClass,
|
||||
'workflow-node-job-button',
|
||||
css`
|
||||
border: 2px solid #d9d9d9;
|
||||
@ -235,87 +263,45 @@ export function JobButton() {
|
||||
);
|
||||
}
|
||||
|
||||
const instruction = instructions.get(type);
|
||||
const { value, icon, color } = JobStatusOptionsMap[job.status];
|
||||
function onOpenJob({ key }) {
|
||||
const job = jobs.find((item) => item.id == key);
|
||||
setViewJob(job);
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
properties: {
|
||||
[`${job.id}-button`]: {
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
title: <Tag color={color}>{icon}</Tag>,
|
||||
shape: 'circle',
|
||||
className: [
|
||||
'workflow-node-job-button',
|
||||
css`
|
||||
.ant-tag {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
line-height: 18px;
|
||||
margin-right: 0;
|
||||
border-radius: 50%;
|
||||
return jobs.length > 1 ? (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: jobs.map((job) => {
|
||||
const { icon, color } = JobStatusOptionsMap[job.status];
|
||||
return {
|
||||
key: job.id,
|
||||
label: (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
|
||||
time {
|
||||
color: #999;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
`,
|
||||
],
|
||||
},
|
||||
properties: {
|
||||
[`${job.id}-modal`]: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
initialValue: job,
|
||||
},
|
||||
'x-component': 'Action.Modal',
|
||||
title: (
|
||||
<div className={cx(nodeTitleClass)}>
|
||||
<Tag>{compile(instruction.title)}</Tag>
|
||||
<strong>{title}</strong>
|
||||
<span className="workflow-node-id">#{id}</span>
|
||||
</div>
|
||||
),
|
||||
properties: {
|
||||
status: {
|
||||
type: 'number',
|
||||
title: `{{t("Status", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
enum: JobStatusOptions,
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
title: `{{t("Executed at", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
showTime: true,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
result: {
|
||||
type: 'object',
|
||||
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.JSON',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
padding: 1em;
|
||||
background-color: #eee;
|
||||
`,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`}
|
||||
>
|
||||
<span className={cx(nodeJobButtonClass, 'workflow-node-job-button')}>
|
||||
<Tag color={color}>{icon}</Tag>
|
||||
</span>
|
||||
<time>{str2moment(job.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
onClick: onOpenJob,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<InnerJobButton job={jobs[jobs.length - 1]} />
|
||||
</Dropdown>
|
||||
) : (
|
||||
<InnerJobButton job={jobs[0]} onClick={() => setViewJob(jobs[0])} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -353,7 +339,7 @@ export function NodeDefaultView(props) {
|
||||
return;
|
||||
}
|
||||
const whiteSet = new Set(['workflow-node-meta', 'workflow-node-config-button', 'ant-input-disabled']);
|
||||
for (let el = ev.target; el && el !== ev.currentTarget; el = el.parentNode) {
|
||||
for (let el = ev.target; el && el !== ev.currentTarget && el !== document.documentElement; el = el.parentNode) {
|
||||
if ((Array.from(el.classList) as string[]).some((name: string) => whiteSet.has(name))) {
|
||||
setEditingConfig(true);
|
||||
ev.stopPropagation();
|
||||
@ -445,7 +431,6 @@ export function NodeDefaultView(props) {
|
||||
min-width: 6em;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper {
|
||||
&:not(.full-width) {
|
||||
.ant-input {
|
||||
|
162
packages/plugins/workflow/src/client/nodes/loop.tsx
Normal file
162
packages/plugins/workflow/src/client/nodes/loop.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import { Alert } from 'antd';
|
||||
import { ArrowUpOutlined } from '@ant-design/icons';
|
||||
import { cx, css } from '@emotion/css';
|
||||
|
||||
import { useCompile } from '@nocobase/client';
|
||||
|
||||
import { NodeDefaultView } from '.';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import { lang, NAMESPACE } from '../locale';
|
||||
import { useWorkflowVariableOptions, VariableOption, VariableTypes } from '../variable';
|
||||
import { addButtonClass, branchBlockClass, branchClass, nodeSubtreeClass } from '../style';
|
||||
import { Branch } from '../Branch';
|
||||
|
||||
function findOption(options: VariableOption[], paths: string[]) {
|
||||
let current = options;
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
const option = current.find((item) => item.value === path);
|
||||
if (!option) {
|
||||
return null;
|
||||
}
|
||||
if (option.children) {
|
||||
current = option.children;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
export default {
|
||||
title: `{{t("Loop", { ns: "${NAMESPACE}" })}}`,
|
||||
type: 'loop',
|
||||
group: 'control',
|
||||
fieldset: {
|
||||
warning: {
|
||||
type: 'void',
|
||||
'x-component': Alert,
|
||||
'x-component-props': {
|
||||
type: 'warning',
|
||||
showIcon: true,
|
||||
message: `{{t("Loop will cause performance issue based on the quantity, please use with caution.", { ns: "${NAMESPACE}" })}}`,
|
||||
className: css`
|
||||
width: 100%;
|
||||
font-size: 85%;
|
||||
margin-bottom: 2em;
|
||||
`,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
title: `{{t("Loop target", { ns: "${NAMESPACE}" })}}`,
|
||||
description: `{{t("Single number will be treated as times, single string will be treated as chars, other non-array value will be turned into a single item array.", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableOptions}}',
|
||||
useTypedConstant: ['string', 'number', 'null'],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
view: {},
|
||||
render: function Renderer(data) {
|
||||
const { nodes } = useFlowContext();
|
||||
const entry = nodes.find((node) => node.upstreamId === data.id && node.branchIndex != null);
|
||||
|
||||
return (
|
||||
<NodeDefaultView data={data}>
|
||||
<div className={cx(nodeSubtreeClass)}>
|
||||
<div
|
||||
className={cx(
|
||||
branchBlockClass,
|
||||
css`
|
||||
padding-left: 20em;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<Branch from={data} entry={entry} branchIndex={entry?.branchIndex ?? 0} />
|
||||
|
||||
<div className={cx(branchClass)}>
|
||||
<div className="workflow-branch-lines" />
|
||||
<div
|
||||
className={cx(
|
||||
addButtonClass,
|
||||
css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 2em;
|
||||
height: 6em;
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<ArrowUpOutlined
|
||||
className={css`
|
||||
background-color: #f0f2f5;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css`
|
||||
position: relative;
|
||||
height: 2em;
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</NodeDefaultView>
|
||||
);
|
||||
},
|
||||
scope: {
|
||||
useWorkflowVariableOptions,
|
||||
},
|
||||
components: {},
|
||||
useScopeVariables(node, types) {
|
||||
const compile = useCompile();
|
||||
const { target } = node.config;
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// const { workflow } = useFlowContext();
|
||||
// const current = useNodeContext();
|
||||
// const upstreams = useAvailableUpstreams(current);
|
||||
// find target data model by path described in `config.target`
|
||||
// 1. get options from $context/$jobsMapByNodeId
|
||||
// 2. route to sub-options and use as loop target options
|
||||
const targetOption: VariableOption = { key: 'item', value: 'item', label: lang('Loop target') };
|
||||
|
||||
if (typeof target === 'string' && target.startsWith('{{') && target.endsWith('}}')) {
|
||||
const paths = target
|
||||
.slice(2, -2)
|
||||
.split('.')
|
||||
.map((path) => path.trim());
|
||||
|
||||
const options = VariableTypes.filter((item) => ['$context', '$jobsMapByNodeId'].includes(item.value)).map(
|
||||
(item: any) => {
|
||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(node, types).filter(Boolean) : null;
|
||||
return {
|
||||
label: compile(item.title),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(opts),
|
||||
disabled: opts && !opts.length,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
targetOption.children = findOption(options, paths);
|
||||
}
|
||||
|
||||
return [
|
||||
targetOption,
|
||||
{ key: 'index', value: 'index', label: lang('Loop index') },
|
||||
{ key: 'length', value: 'length', label: lang('Loop length') },
|
||||
];
|
||||
},
|
||||
};
|
@ -86,7 +86,7 @@ export default {
|
||||
ModeConfig,
|
||||
AssigneesSelect,
|
||||
},
|
||||
getOptions(config, types) {
|
||||
useVariables({ config }, types) {
|
||||
const formKeys = Object.keys(config.forms ?? {});
|
||||
if (!formKeys.length) {
|
||||
return null;
|
||||
|
@ -14,15 +14,12 @@ export default {
|
||||
group: 'collection',
|
||||
fieldset: {
|
||||
collection,
|
||||
// multiple: {
|
||||
// type: 'boolean',
|
||||
// title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`,
|
||||
// 'x-decorator': 'FormItem',
|
||||
// 'x-component': 'Checkbox',
|
||||
// 'x-component-props': {
|
||||
// disabled: true
|
||||
// }
|
||||
// },
|
||||
multiple: {
|
||||
type: 'boolean',
|
||||
title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -45,7 +42,7 @@ export default {
|
||||
FilterDynamicComponent,
|
||||
FieldsSelect,
|
||||
},
|
||||
getOptions(config, types) {
|
||||
useVariables({ config }, types) {
|
||||
return useCollectionFieldOptions({
|
||||
collection: config.collection,
|
||||
types,
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export const workflowPageClass = css`
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.workflow-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
@ -28,8 +35,9 @@ export const workflowPageClass = css`
|
||||
}
|
||||
|
||||
.workflow-canvas {
|
||||
width: min-content;
|
||||
min-width: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@ -95,11 +103,11 @@ export const branchClass = css`
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
min-width: 20em;
|
||||
padding: 0 2em;
|
||||
|
||||
.workflow-node-list {
|
||||
flex-grow: 1;
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.workflow-branch-lines {
|
||||
@ -181,12 +189,8 @@ export const nodeCardClass = css`
|
||||
box-shadow: 0 0.25em 1em rgba(0, 100, 200, 0.25);
|
||||
}
|
||||
|
||||
.workflow-node-remove-button,
|
||||
.workflow-node-job-button {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.workflow-node-remove-button {
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
color: #999;
|
||||
@ -202,21 +206,10 @@ export const nodeCardClass = css`
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-node-job-button {
|
||||
display: flex;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
min-width: 1.25rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
|
||||
&[type='button'] {
|
||||
border: none;
|
||||
}
|
||||
> .workflow-node-job-button {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
@ -251,6 +244,30 @@ export const nodeCardClass = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export const nodeJobButtonClass = css`
|
||||
display: flex;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
min-width: 1.25rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
|
||||
&[type='button'] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
line-height: 18px;
|
||||
margin-right: 0;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const nodeHeaderClass = css`
|
||||
position: relative;
|
||||
`;
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
useResourceActionContext,
|
||||
} from '@nocobase/client';
|
||||
|
||||
import { nodeCardClass, nodeMetaClass, nodeTitleClass } from '../style';
|
||||
import { nodeCardClass, nodeJobButtonClass, nodeMetaClass, nodeTitleClass } from '../style';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import collection from './collection';
|
||||
import schedule from './schedule/';
|
||||
@ -86,7 +86,7 @@ function TriggerExecution() {
|
||||
'x-component-props': {
|
||||
title: <InfoOutlined />,
|
||||
shape: 'circle',
|
||||
className: 'workflow-node-job-button',
|
||||
className: cx(nodeJobButtonClass, 'workflow-node-job-button'),
|
||||
type: 'primary',
|
||||
},
|
||||
properties: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useCollectionManager, useCompile } from '@nocobase/client';
|
||||
import { useFlowContext } from './FlowContext';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { instructions, useAvailableUpstreams, useNodeContext } from './nodes';
|
||||
import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes';
|
||||
import { triggers } from './triggers';
|
||||
|
||||
export type VariableOption = {
|
||||
@ -13,17 +13,37 @@ export type VariableOption = {
|
||||
|
||||
export type VariableOptions = VariableOption[] | null;
|
||||
|
||||
const VariableTypes = [
|
||||
export const VariableTypes = [
|
||||
{
|
||||
title: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$scopes',
|
||||
useOptions(current, types) {
|
||||
const scopes = useUpstreamScopes(current);
|
||||
const options: VariableOption[] = [];
|
||||
scopes.forEach((node) => {
|
||||
const instruction = instructions.get(node.type);
|
||||
const subOptions = instruction.useScopeVariables?.(node, types);
|
||||
if (subOptions) {
|
||||
options.push({
|
||||
key: node.id.toString(),
|
||||
value: node.id.toString(),
|
||||
label: node.title ?? `#${node.id}`,
|
||||
children: subOptions,
|
||||
});
|
||||
}
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$jobsMapByNodeId',
|
||||
options(types) {
|
||||
const current = useNodeContext();
|
||||
useOptions(current, types) {
|
||||
const upstreams = useAvailableUpstreams(current);
|
||||
const options: VariableOption[] = [];
|
||||
upstreams.forEach((node) => {
|
||||
const instruction = instructions.get(node.type);
|
||||
const subOptions = instruction.getOptions?.(node.config, types);
|
||||
const subOptions = instruction.useVariables?.(node, types);
|
||||
if (subOptions) {
|
||||
options.push({
|
||||
key: node.id.toString(),
|
||||
@ -39,7 +59,7 @@ const VariableTypes = [
|
||||
{
|
||||
title: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$context',
|
||||
options(types) {
|
||||
useOptions(current, types) {
|
||||
const { workflow } = useFlowContext();
|
||||
const trigger = triggers.get(workflow.type);
|
||||
return trigger?.getOptions?.(workflow.config, types) ?? null;
|
||||
@ -48,7 +68,7 @@ const VariableTypes = [
|
||||
{
|
||||
title: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$system',
|
||||
options(types) {
|
||||
useOptions(current, types) {
|
||||
return [
|
||||
...(!types || types.includes('date')
|
||||
? [
|
||||
@ -137,8 +157,9 @@ export function filterTypedFields(fields, types, depth = 1) {
|
||||
|
||||
export function useWorkflowVariableOptions(types?) {
|
||||
const compile = useCompile();
|
||||
const current = useNodeContext();
|
||||
const options = VariableTypes.map((item: any) => {
|
||||
const opts = typeof item.options === 'function' ? item.options(types).filter(Boolean) : item.options;
|
||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(current, types).filter(Boolean) : null;
|
||||
return {
|
||||
label: compile(item.title),
|
||||
value: item.value,
|
||||
@ -214,8 +235,9 @@ function useNormalizedFields(collectionName) {
|
||||
export function useCollectionFieldOptions(options): VariableOption[] {
|
||||
const { fields, collection, types, depth = 1 } = options;
|
||||
const compile = useCompile();
|
||||
const normalizedFields = fields ?? useNormalizedFields(collection);
|
||||
const result: VariableOption[] = filterTypedFields(normalizedFields, types, depth)
|
||||
const normalizedFields = useNormalizedFields(collection);
|
||||
const computedFields = fields ?? normalizedFields;
|
||||
const result: VariableOption[] = filterTypedFields(computedFields, types, depth)
|
||||
.filter((field) => !isAssociationField(field) || depth)
|
||||
.map((field) => {
|
||||
const label = compile(field.uiSchema?.title || field.name);
|
||||
|
@ -5,6 +5,7 @@ import { parse } from '@nocobase/utils';
|
||||
import { Transaction, Transactionable } from 'sequelize';
|
||||
import Plugin from '.';
|
||||
import { EXECUTION_STATUS, JOB_STATUS } from './constants';
|
||||
import { Runner } from './instructions';
|
||||
import ExecutionModel from './models/Execution';
|
||||
import FlowNodeModel from './models/FlowNode';
|
||||
import JobModel from './models/Job';
|
||||
@ -131,7 +132,7 @@ export default class Processor {
|
||||
}
|
||||
}
|
||||
|
||||
private async exec(instruction: Function, node: FlowNodeModel, prevJob) {
|
||||
private async exec(instruction: Runner, node: FlowNodeModel, prevJob) {
|
||||
let job;
|
||||
try {
|
||||
// call instruction to get result and status
|
||||
@ -174,7 +175,7 @@ export default class Processor {
|
||||
|
||||
if (savedJob.status === JOB_STATUS.RESOLVED && node.downstream) {
|
||||
// run next node
|
||||
this.logger.debug(`run next node (${node.id})`);
|
||||
this.logger.debug(`run next node (${node.downstreamId})`);
|
||||
return this.run(node.downstream, savedJob);
|
||||
}
|
||||
|
||||
@ -194,7 +195,7 @@ export default class Processor {
|
||||
|
||||
// parent node should take over the control
|
||||
public async end(node, job) {
|
||||
this.logger.debug(`branch ended at node (${node.id})})`);
|
||||
this.logger.debug(`branch ended at node (${node.id})`);
|
||||
const parentNode = this.findBranchParentNode(node);
|
||||
// no parent, means on main flow
|
||||
if (parentNode) {
|
||||
@ -289,6 +290,15 @@ export default class Processor {
|
||||
return null;
|
||||
}
|
||||
|
||||
findBranchEndNode(node: FlowNodeModel): FlowNodeModel | null {
|
||||
for (let n = node; n; n = n.downstream) {
|
||||
if (!n.downstream) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
findBranchParentJob(job: JobModel, node: FlowNodeModel): JobModel | null {
|
||||
for (let j: JobModel | undefined = job; j; j = this.jobsMap.get(j.upstreamId)) {
|
||||
if (j.nodeId === node.id) {
|
||||
@ -298,6 +308,18 @@ export default class Processor {
|
||||
return null;
|
||||
}
|
||||
|
||||
findBranchLastJob(node: FlowNodeModel): JobModel | null {
|
||||
for (let n = this.findBranchEndNode(node); n && n !== node.upstream; n = n.upstream) {
|
||||
const jobs = Array.from(this.jobsMap.values())
|
||||
.filter((item) => item.nodeId === n.id)
|
||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
if (jobs.length) {
|
||||
return jobs[jobs.length - 1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getScope(node?) {
|
||||
const systemFns = {};
|
||||
const scope = {
|
||||
@ -308,10 +330,21 @@ export default class Processor {
|
||||
systemFns[name] = fn.bind(scope);
|
||||
}
|
||||
|
||||
const $scopes = {};
|
||||
if (node) {
|
||||
for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) {
|
||||
const instruction = this.options.plugin.instructions.get(n.type);
|
||||
if (typeof instruction.getScope === 'function') {
|
||||
$scopes[n.id] = instruction.getScope(n, this.jobsMapByNodeId[n.id], this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
$context: this.execution.context,
|
||||
$jobsMapByNodeId: this.jobsMapByNodeId,
|
||||
$system: systemFns,
|
||||
$scopes,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,10 @@ export default {
|
||||
type: 'belongsTo',
|
||||
name: 'post',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'content',
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'status',
|
||||
|
@ -0,0 +1,331 @@
|
||||
import Database from '@nocobase/database';
|
||||
import { Application } from '@nocobase/server';
|
||||
import { getApp, sleep } from '..';
|
||||
import { EXECUTION_STATUS, JOB_STATUS } from '../../constants';
|
||||
|
||||
describe('workflow > instructions > loop', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let PostRepo;
|
||||
let WorkflowModel;
|
||||
let workflow;
|
||||
let plugin;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
plugin = app.pm.get('workflow');
|
||||
|
||||
db = app.db;
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => app.stop());
|
||||
|
||||
describe('branch', () => {
|
||||
it('no branch just pass', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n2);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(jobs.length).toBe(2);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(jobs[0].result).toBe(0);
|
||||
});
|
||||
|
||||
it('should exit when branch meets error', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
config: {
|
||||
target: 2,
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'error',
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const n3 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n3);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.ERROR);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(jobs.length).toBe(2);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.ERROR);
|
||||
expect(jobs[0].result).toBe(0);
|
||||
expect(jobs[1].status).toBe(JOB_STATUS.ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config', () => {
|
||||
it('no target just pass', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const n3 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n3);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(jobs.length).toBe(2);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(jobs[0].result).toBe(0);
|
||||
});
|
||||
|
||||
it('null target just pass', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
config: {
|
||||
target: null,
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const n3 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n3);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(jobs.length).toBe(2);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(jobs[0].result).toBe(0);
|
||||
});
|
||||
|
||||
it('empty array just pass', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
config: {
|
||||
target: [],
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const n3 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n3);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(jobs.length).toBe(2);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(jobs[0].result).toBe(0);
|
||||
});
|
||||
|
||||
it('target is number, cycle number times', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
config: {
|
||||
target: 2.5,
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const n3 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n3);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
|
||||
expect(jobs.length).toBe(4);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(jobs[0].result).toBe(2);
|
||||
});
|
||||
|
||||
it('target is no array, set as an array', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
config: {
|
||||
target: {},
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const n3 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n3);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
|
||||
expect(jobs.length).toBe(3);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(jobs[0].result).toBe(1);
|
||||
});
|
||||
|
||||
it('multiple targets', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
config: {
|
||||
target: [1, 2],
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const n3 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n3);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
|
||||
expect(jobs.length).toBe(4);
|
||||
expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED);
|
||||
expect(jobs[0].result).toBe(2);
|
||||
expect(jobs.filter((j) => j.nodeId === n2.id).length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scope variable', () => {
|
||||
it('item.key', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'loop',
|
||||
config: {
|
||||
target: '{{$context.data.comments}}',
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'calculation',
|
||||
config: {
|
||||
engine: 'formula.js',
|
||||
expression: `{{$scopes.${n1.id}.item.content}}`,
|
||||
},
|
||||
upstreamId: n1.id,
|
||||
branchIndex: 0,
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
comments: [{ content: 'c1' }, { content: 'c2' }],
|
||||
},
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(jobs.length).toBe(3);
|
||||
expect(jobs[1].result).toBe('c1');
|
||||
expect(jobs[2].result).toBe('c2');
|
||||
});
|
||||
});
|
||||
});
|
@ -80,12 +80,12 @@ function migrateConfig(config, oldToNew) {
|
||||
case 'array':
|
||||
return value.map((item) => migrate(item));
|
||||
case 'string':
|
||||
return value.replace(/(\{\{\$jobsMapByNodeId\.)([\w-]+)/g, (_, jobVar, oldNodeId) => {
|
||||
return value.replace(/({{\$jobsMapByNodeId|{{\$scopes)\.([\w-]+)/g, (_, jobVar, oldNodeId) => {
|
||||
const newNode = oldToNew.get(Number.parseInt(oldNodeId, 10));
|
||||
if (!newNode) {
|
||||
throw new Error('node configurated for result is not existed');
|
||||
}
|
||||
return `{{$jobsMapByNodeId.${newNode.id}`;
|
||||
return `${jobVar}.${newNode.id}`;
|
||||
});
|
||||
default:
|
||||
return value;
|
||||
|
@ -15,7 +15,7 @@ export default {
|
||||
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
||||
const { dynamic = false } = <CalculationConfig>node.config || {};
|
||||
let { engine = 'math.js', expression = '' } = node.config;
|
||||
let scope = processor.getScope();
|
||||
let scope = processor.getScope(node);
|
||||
if (dynamic) {
|
||||
const parsed = parse(dynamic)(scope) ?? {};
|
||||
engine = parsed.engine;
|
||||
|
@ -122,7 +122,7 @@ export default {
|
||||
try {
|
||||
result = evaluator
|
||||
? evaluator(expression, processor.getScope())
|
||||
: logicCalculate(processor.getParsedValue(calculation));
|
||||
: logicCalculate(processor.getParsedValue(calculation, node));
|
||||
} catch (e) {
|
||||
return {
|
||||
result: e.toString(),
|
||||
|
@ -6,7 +6,7 @@ export default {
|
||||
const { collection, params: { appends = [], ...params } = {} } = node.config;
|
||||
|
||||
const { repository, model } = (<typeof FlowNodeModel>node.constructor).database.getCollection(collection);
|
||||
const options = processor.getParsedValue(params);
|
||||
const options = processor.getParsedValue(params, node);
|
||||
const result = await repository.create({
|
||||
...options,
|
||||
context: {
|
||||
|
@ -6,7 +6,7 @@ export default {
|
||||
const { collection, params = {} } = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const options = processor.getParsedValue(params);
|
||||
const options = processor.getParsedValue(params, node);
|
||||
const result = await repo.destroy({
|
||||
...options,
|
||||
context: {
|
||||
|
@ -24,6 +24,8 @@ export interface Instruction {
|
||||
|
||||
// for start node in main flow (or branch) to resume when manual sub branch triggered
|
||||
resume?: Runner;
|
||||
|
||||
getScope?: (node: FlowNodeModel, job: any, processor: Processor) => any;
|
||||
}
|
||||
|
||||
type InstructionConstructor<T> = { new (p: Plugin): T };
|
||||
@ -35,6 +37,7 @@ export default function <T extends Instruction>(plugin, more: { [key: string]: T
|
||||
'calculation',
|
||||
'condition',
|
||||
'parallel',
|
||||
'loop',
|
||||
'delay',
|
||||
'manual',
|
||||
'query',
|
||||
|
100
packages/plugins/workflow/src/server/instructions/loop.ts
Normal file
100
packages/plugins/workflow/src/server/instructions/loop.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import FlowNodeModel from '../models/FlowNode';
|
||||
import JobModel from '../models/Job';
|
||||
import Processor from '../Processor';
|
||||
import { JOB_STATUS } from '../constants';
|
||||
|
||||
function getTargetLength(target) {
|
||||
let length = 0;
|
||||
if (typeof target === 'number') {
|
||||
if (target < 0) {
|
||||
throw new Error('Loop target in number type must be greater than 0');
|
||||
}
|
||||
length = Math.floor(target);
|
||||
} else {
|
||||
const targets = (Array.isArray(target) ? target : [target]).filter((t) => t != null);
|
||||
length = targets.length;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
export default {
|
||||
async run(node: FlowNodeModel, prevJob: JobModel, processor: Processor) {
|
||||
const [branch] = processor.getBranches(node);
|
||||
const target = processor.getParsedValue(node.config.target, node);
|
||||
const length = getTargetLength(target);
|
||||
|
||||
if (!branch || !length) {
|
||||
return {
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
result: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const job = await processor.saveJob({
|
||||
status: JOB_STATUS.PENDING,
|
||||
// save loop index
|
||||
result: 0,
|
||||
nodeId: node.id,
|
||||
upstreamId: prevJob?.id ?? null,
|
||||
});
|
||||
|
||||
// TODO: add loop scope to stack
|
||||
// processor.stack.push({
|
||||
// label: node.title,
|
||||
// value: node.id
|
||||
// });
|
||||
|
||||
await processor.run(branch, job);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
async resume(node: FlowNodeModel, branchJob, processor: Processor) {
|
||||
const job = processor.findBranchParentJob(branchJob, node) as JobModel;
|
||||
const loop = processor.nodesMap.get(job.nodeId);
|
||||
const [branch] = processor.getBranches(node);
|
||||
|
||||
const { result, status } = job;
|
||||
// if loop has been done (resolved / rejected), do not care newly executed branch jobs.
|
||||
if (status !== JOB_STATUS.PENDING) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextIndex = result + 1;
|
||||
|
||||
const target = processor.getParsedValue(loop.config.target, node);
|
||||
// branchJob.status === JOB_STATUS.RESOLVED means branchJob is done, try next loop or exit as resolved
|
||||
if (branchJob.status > JOB_STATUS.PENDING) {
|
||||
job.set({ result: nextIndex });
|
||||
|
||||
const length = getTargetLength(target);
|
||||
if (nextIndex < length) {
|
||||
await processor.saveJob(job);
|
||||
await processor.run(branch, job);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// branchJob.status < JOB_STATUS.PENDING means branchJob is rejected, any rejection should cause loop rejected
|
||||
job.set({
|
||||
status: branchJob.status,
|
||||
});
|
||||
|
||||
return job;
|
||||
},
|
||||
|
||||
getScope(node, index, processor) {
|
||||
const target = processor.getParsedValue(node.config.target, node);
|
||||
const targets = (Array.isArray(target) ? target : [target]).filter((t) => t != null);
|
||||
const length = getTargetLength(target);
|
||||
const item = typeof target === 'number' ? index : targets[index];
|
||||
|
||||
const result = {
|
||||
item,
|
||||
index,
|
||||
length,
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
@ -75,16 +75,19 @@ export default {
|
||||
const { mode = PARALLEL_MODE.ALL } = node.config;
|
||||
await branches.reduce(
|
||||
(promise: Promise<any>, branch, i) =>
|
||||
promise.then((previous) => {
|
||||
promise.then(async (previous) => {
|
||||
if (i && !Modes[mode].next(previous)) {
|
||||
return Promise.resolve(previous);
|
||||
return previous;
|
||||
}
|
||||
return processor.run(branch, job);
|
||||
await processor.run(branch, job);
|
||||
|
||||
// find last job of the branch
|
||||
return processor.findBranchLastJob(branch);
|
||||
}),
|
||||
Promise.resolve(),
|
||||
);
|
||||
|
||||
return processor.end(node, job);
|
||||
return null;
|
||||
},
|
||||
|
||||
async resume(node: FlowNodeModel, branchJob, processor: Processor) {
|
||||
|
@ -7,12 +7,9 @@ export default {
|
||||
const { collection, multiple, params = {}, failOnEmpty = false } = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const options = processor.getParsedValue(params);
|
||||
const options = processor.getParsedValue(params, node);
|
||||
const result = await (multiple ? repo.find : repo.findOne).call(repo, {
|
||||
...options,
|
||||
context: {
|
||||
executionId: processor.execution.id,
|
||||
},
|
||||
transaction: processor.transaction,
|
||||
});
|
||||
|
||||
|
@ -51,7 +51,7 @@ export default class implements Instruction {
|
||||
nodeId: node.id,
|
||||
});
|
||||
|
||||
const config = processor.getParsedValue(node.config) as RequestConfig;
|
||||
const config = processor.getParsedValue(node.config, node) as RequestConfig;
|
||||
|
||||
request(config)
|
||||
.then((response) => {
|
||||
|
@ -7,7 +7,7 @@ export default {
|
||||
const { collection, multiple = false, params = {} } = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const options = processor.getParsedValue(params);
|
||||
const options = processor.getParsedValue(params, node);
|
||||
const result = await repo.update({
|
||||
...options,
|
||||
context: {
|
||||
|
223
yarn.lock
223
yarn.lock
@ -6680,56 +6680,6 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
|
||||
integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==
|
||||
|
||||
antd@4.22.8:
|
||||
version "4.22.8"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-4.22.8.tgz#e2f446932815a522a8aa3d285a8a9bdcb3d0fa9f"
|
||||
integrity sha512-mqHuCg9itZX+z6wk+mvRBcfz/U9iiIXS4LoNkyo8X/UBgdN8CoetFmrdvA1UQy1BuWa0/n62LiS1LatdvoTuHw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^6.0.0"
|
||||
"@ant-design/icons" "^4.7.0"
|
||||
"@ant-design/react-slick" "~0.29.1"
|
||||
"@babel/runtime" "^7.18.3"
|
||||
"@ctrl/tinycolor" "^3.4.0"
|
||||
classnames "^2.2.6"
|
||||
copy-to-clipboard "^3.2.0"
|
||||
lodash "^4.17.21"
|
||||
memoize-one "^6.0.0"
|
||||
moment "^2.29.2"
|
||||
rc-cascader "~3.6.0"
|
||||
rc-checkbox "~2.3.0"
|
||||
rc-collapse "~3.3.0"
|
||||
rc-dialog "~8.9.0"
|
||||
rc-drawer "~5.1.0"
|
||||
rc-dropdown "~4.0.0"
|
||||
rc-field-form "~1.27.0"
|
||||
rc-image "~5.7.0"
|
||||
rc-input "~0.0.1-alpha.5"
|
||||
rc-input-number "~7.3.5"
|
||||
rc-mentions "~1.9.1"
|
||||
rc-menu "~9.6.3"
|
||||
rc-motion "^2.6.1"
|
||||
rc-notification "~4.6.0"
|
||||
rc-pagination "~3.1.17"
|
||||
rc-picker "~2.6.10"
|
||||
rc-progress "~3.3.2"
|
||||
rc-rate "~2.9.0"
|
||||
rc-resize-observer "^1.2.0"
|
||||
rc-segmented "~2.1.0"
|
||||
rc-select "~14.1.1"
|
||||
rc-slider "~10.0.0"
|
||||
rc-steps "~4.1.0"
|
||||
rc-switch "~3.2.0"
|
||||
rc-table "~7.25.3"
|
||||
rc-tabs "~11.16.0"
|
||||
rc-textarea "~0.3.0"
|
||||
rc-tooltip "~5.2.0"
|
||||
rc-tree "~5.6.5"
|
||||
rc-tree-select "~5.4.0"
|
||||
rc-trigger "^5.2.10"
|
||||
rc-upload "~4.3.0"
|
||||
rc-util "^5.22.5"
|
||||
scroll-into-view-if-needed "^2.2.25"
|
||||
|
||||
antd@^4.24.8:
|
||||
version "4.24.8"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-4.24.8.tgz#22f34de6857556868780dfa5fe7b374b0b71b517"
|
||||
@ -16681,11 +16631,6 @@ memoize-one@^5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||
|
||||
memoize-one@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
|
||||
|
||||
memory-fs@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
|
||||
@ -20256,18 +20201,6 @@ rc-align@^4.0.0:
|
||||
rc-util "^5.26.0"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
|
||||
rc-cascader@~3.6.0:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.6.2.tgz#2b5c108807234898cd9a0366d0626f786b7b5622"
|
||||
integrity sha512-sf2otpazlROTzkD3nZVfIzXmfBLiEOBTXA5wxozGXBpS902McDpvF0bdcYBu5hN+rviEAm6Mh9cLXNQ1Ty8wKQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
array-tree-filter "^2.1.0"
|
||||
classnames "^2.3.1"
|
||||
rc-select "~14.1.0"
|
||||
rc-tree "~5.6.3"
|
||||
rc-util "^5.6.1"
|
||||
|
||||
rc-cascader@~3.7.0:
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.7.2.tgz#447f2725add7953dee205d1cf59f58a8317bf5f7"
|
||||
@ -20288,17 +20221,6 @@ rc-checkbox@~2.3.0:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
|
||||
rc-collapse@~3.3.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-3.3.1.tgz#fc66d4c9cfeaf41e932b2de6da2d454874aee55a"
|
||||
integrity sha512-cOJfcSe3R8vocrF8T+PgaHDrgeA1tX+lwfhwSj60NX9QVRidsILIbRNDLD6nAzmcvVC5PWiIRiR4S1OobxdhCg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "2.x"
|
||||
rc-motion "^2.3.4"
|
||||
rc-util "^5.2.1"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-collapse@~3.4.2:
|
||||
version "3.4.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-3.4.2.tgz#1310be7ad4cd0dcfc622c45f6c3b5ffdee403ad7"
|
||||
@ -20310,16 +20232,6 @@ rc-collapse@~3.4.2:
|
||||
rc-util "^5.2.1"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-dialog@~8.9.0:
|
||||
version "8.9.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-8.9.0.tgz#04dc39522f0321ed2e06018d4a7e02a4c32bd3ea"
|
||||
integrity sha512-Cp0tbJnrvPchJfnwIvOMWmJ4yjX3HWFatO6oBFD1jx8QkgsQCR0p8nUWAKdd3seLJhEC39/v56kZaEjwp9muoQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.6"
|
||||
rc-motion "^2.3.0"
|
||||
rc-util "^5.21.0"
|
||||
|
||||
rc-dialog@~9.0.0, rc-dialog@~9.0.2:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-9.0.2.tgz#aadfebdeba145f256c1fac9b9f509f893cdbb5b8"
|
||||
@ -20331,16 +20243,6 @@ rc-dialog@~9.0.0, rc-dialog@~9.0.2:
|
||||
rc-motion "^2.3.0"
|
||||
rc-util "^5.21.0"
|
||||
|
||||
rc-drawer@~5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-5.1.0.tgz#c1b8a46e5c064ba46a16233fbcfb1ccec6a73c10"
|
||||
integrity sha512-pU3Tsn99pxGdYowXehzZbdDVE+4lDXSGb7p8vA9mSmr569oc2Izh4Zw5vLKSe/Xxn2p5MSNbLVqD4tz+pK6SOw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.6"
|
||||
rc-motion "^2.6.1"
|
||||
rc-util "^5.21.2"
|
||||
|
||||
rc-drawer@~6.1.0:
|
||||
version "6.1.5"
|
||||
resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-6.1.5.tgz#c4137b944c16b7c179d0dba6f06ebe54f9311ec8"
|
||||
@ -20383,17 +20285,7 @@ rc-image@~5.13.0:
|
||||
rc-motion "^2.6.2"
|
||||
rc-util "^5.0.6"
|
||||
|
||||
rc-image@~5.7.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-5.7.1.tgz#678dc014845954c30237808c00c7b12e5f2a0b07"
|
||||
integrity sha512-QyMfdhoUfb5W14plqXSisaYwpdstcLYnB0MjX5ccIK2rydQM9sDPuekQWu500DDGR2dBaIF5vx9XbWkNFK17Fg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.2"
|
||||
classnames "^2.2.6"
|
||||
rc-dialog "~8.9.0"
|
||||
rc-util "^5.0.6"
|
||||
|
||||
rc-input-number@~7.3.5, rc-input-number@~7.3.9:
|
||||
rc-input-number@~7.3.9:
|
||||
version "7.3.11"
|
||||
resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-7.3.11.tgz#c7089705a220e1a59ba974fabf89693e00dd2442"
|
||||
integrity sha512-aMWPEjFeles6PQnMqP5eWpxzsvHm9rh1jQOWXExUEIxhX62Fyl/ptifLHOn17+waDG1T/YUb6flfJbvwRhHrbA==
|
||||
@ -20402,15 +20294,6 @@ rc-input-number@~7.3.5, rc-input-number@~7.3.9:
|
||||
classnames "^2.2.5"
|
||||
rc-util "^5.23.0"
|
||||
|
||||
rc-input@~0.0.1-alpha.5:
|
||||
version "0.0.1-alpha.7"
|
||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-0.0.1-alpha.7.tgz#53e3f13871275c21d92b51f80b698f389ad45dd3"
|
||||
integrity sha512-eozaqpCYWSY5LBMwlHgC01GArkVEP+XlJ84OMvdkwUnJBSv83Yxa15pZpn7vACAj84uDC4xOA2CoFdbLuqB08Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.1"
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.18.1"
|
||||
|
||||
rc-input@~0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-0.1.4.tgz#45cb4ba209ae6cc835a2acb8629d4f8f0cb347e0"
|
||||
@ -20432,19 +20315,7 @@ rc-mentions@~1.13.1:
|
||||
rc-trigger "^5.0.4"
|
||||
rc-util "^5.22.5"
|
||||
|
||||
rc-mentions@~1.9.1:
|
||||
version "1.9.2"
|
||||
resolved "https://registry.yarnpkg.com/rc-mentions/-/rc-mentions-1.9.2.tgz#f264ebc4ec734dad9edc8e078b65ab3586d94a7b"
|
||||
integrity sha512-uxb/lzNnEGmvraKWNGE6KXMVXvt8RQv9XW8R0Dqi3hYsyPiAZeHRCHQKdLARuk5YBhFhZ6ga55D/8XuY367g3g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.6"
|
||||
rc-menu "~9.6.0"
|
||||
rc-textarea "^0.3.0"
|
||||
rc-trigger "^5.0.4"
|
||||
rc-util "^5.22.5"
|
||||
|
||||
rc-menu@~9.6.0, rc-menu@~9.6.3:
|
||||
rc-menu@~9.6.0:
|
||||
version "9.6.4"
|
||||
resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-9.6.4.tgz#033e7b8848c17a09a81b68b8d4c3fa457605f4f6"
|
||||
integrity sha512-6DiNAjxjVIPLZXHffXxxcyE15d4isRL7iQ1ru4MqYDH2Cqc5bW96wZOdMydFtGLyDdnmEQ9jVvdCE9yliGvzkw==
|
||||
@ -20498,14 +20369,6 @@ rc-overflow@^1.0.0, rc-overflow@^1.2.0, rc-overflow@^1.2.8:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.19.2"
|
||||
|
||||
rc-pagination@~3.1.17:
|
||||
version "3.1.17"
|
||||
resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.17.tgz#91e690aa894806e344cea88ea4a16d244194a1bd"
|
||||
integrity sha512-/BQ5UxcBnW28vFAcP2hfh+Xg15W0QZn8TWYwdCApchMH1H0CxiaUUcULP8uXcFM1TygcdKWdt3JqsL9cTAfdkQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
|
||||
rc-pagination@~3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.2.0.tgz#4f2fdba9fdac0f48e5c9fb1141973818138af7e1"
|
||||
@ -20514,20 +20377,6 @@ rc-pagination@~3.2.0:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
|
||||
rc-picker@~2.6.10:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.6.11.tgz#d4a55e46480517cd1bfea5f5acd28b1d6be232d2"
|
||||
integrity sha512-INJ7ULu+Kj4UgqbcqE8Q+QpMw55xFf9kkyLBHJFk0ihjJpAV4glialRfqHE7k4KX2BWYPQfpILwhwR14x2EiRQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
date-fns "2.x"
|
||||
dayjs "1.x"
|
||||
moment "^2.24.0"
|
||||
rc-trigger "^5.0.4"
|
||||
rc-util "^5.4.0"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-picker@~2.7.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.7.0.tgz#3c19881da27a0c5ee4c7e7504e21b552bd43a94c"
|
||||
@ -20542,15 +20391,6 @@ rc-picker@~2.7.0:
|
||||
rc-util "^5.4.0"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-progress@~3.3.2:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-3.3.3.tgz#eb9bffbacab1534f2542f9f6861ce772254362b1"
|
||||
integrity sha512-MDVNVHzGanYtRy2KKraEaWeZLri2ZHWIRyaE1a9MQ2MuJ09m+Wxj5cfcaoaR6z5iRpHpA59YeUxAlpML8N4PJw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.6"
|
||||
rc-util "^5.16.1"
|
||||
|
||||
rc-progress@~3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-3.4.1.tgz#a9ffe099e88a4fc03afb09d8603162bf0760d743"
|
||||
@ -20589,7 +20429,7 @@ rc-segmented@~2.1.0:
|
||||
rc-motion "^2.4.4"
|
||||
rc-util "^5.17.0"
|
||||
|
||||
rc-select@~14.1.0, rc-select@~14.1.1, rc-select@~14.1.13:
|
||||
rc-select@~14.1.0, rc-select@~14.1.13:
|
||||
version "14.1.17"
|
||||
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.1.17.tgz#e623eabeaa0dd117d5a63354e6ddaaa118abc5ee"
|
||||
integrity sha512-6qQhMqtoUkkboRqXKKFRR5Nu1mrnw2mC1uxIBIczg7aiJ94qCZBg4Ww8OLT9f4xdyCgbFSGh6r3yB9EBsjoHGA==
|
||||
@ -20612,15 +20452,6 @@ rc-slider@~10.0.0:
|
||||
rc-util "^5.18.1"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-steps@~4.1.0:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-4.1.4.tgz#0ba82db202d59ca52d0693dc9880dd145b19dc23"
|
||||
integrity sha512-qoCqKZWSpkh/b03ASGx1WhpKnuZcRWmvuW+ZUu4mvMdfvFzVxblTwUM+9aBd0mlEUFmt6GW8FXhMpHkK3Uzp3w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.2"
|
||||
classnames "^2.2.3"
|
||||
rc-util "^5.0.1"
|
||||
|
||||
rc-steps@~5.0.0-alpha.2:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-5.0.0.tgz#2e2403f2dd69eb3966d65f461f7e3a8ee1ef69fe"
|
||||
@ -20639,17 +20470,6 @@ rc-switch@~3.2.0:
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.0.1"
|
||||
|
||||
rc-table@~7.25.3:
|
||||
version "7.25.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.25.3.tgz#a2941d4fde4c181e687e97a294faca8e4122e26d"
|
||||
integrity sha512-McsLJ2rg8EEpRBRYN4Pf9gT7ZNYnjvF9zrBpUBBbUX/fxk+eGi5ff1iPIhMyiHsH71/BmTUzX9nc9XqupD0nMg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.5"
|
||||
rc-resize-observer "^1.1.0"
|
||||
rc-util "^5.22.5"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-table@~7.26.0:
|
||||
version "7.26.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.26.0.tgz#9d517e7fa512e7571fdcc453eb1bf19edfac6fbc"
|
||||
@ -20661,7 +20481,7 @@ rc-table@~7.26.0:
|
||||
rc-util "^5.22.5"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-tabs@^11.7.1, rc-tabs@~11.16.0:
|
||||
rc-tabs@^11.7.1:
|
||||
version "11.16.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-11.16.1.tgz#7c57b6a092d9d0e2df54413b0319f195c27214a9"
|
||||
integrity sha512-bR7Dap23YyfzZQwtKomhiFEFzZuE7WaKWo+ypNRSGB9PDKSc6tM12VP8LWYkvmmQHthgwP0WRN8nFbSJWuqLYw==
|
||||
@ -20686,17 +20506,6 @@ rc-tabs@~12.5.6:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.16.0"
|
||||
|
||||
rc-textarea@^0.3.0, rc-textarea@~0.3.0:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-0.3.7.tgz#987142891efdedb774883c07e2f51b318fde5a11"
|
||||
integrity sha512-yCdZ6binKmAQB13hc/oehh0E/QRwoPP1pjF21aHBxlgXO3RzPF6dUu4LG2R4FZ1zx/fQd2L1faktulrXOM/2rw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.1"
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.7.0"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
rc-textarea@^0.4.0, rc-textarea@~0.4.5:
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-0.4.7.tgz#627f662d46f99e0059d1c1ebc8db40c65339fe90"
|
||||
@ -20717,17 +20526,6 @@ rc-tooltip@~5.2.0:
|
||||
classnames "^2.3.1"
|
||||
rc-trigger "^5.0.0"
|
||||
|
||||
rc-tree-select@~5.4.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-5.4.1.tgz#b97b9c6adcabc7415d25cfd40d18058b0c57bec2"
|
||||
integrity sha512-xhXnKP8Stu2Q7wTcjJaSzSOLd4wmFtUZOwmy1cioaWyPbpiKlYdnALXA/9U49HOaV3KFXdRHE9Yi0KYED7yOAQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "2.x"
|
||||
rc-select "~14.1.0"
|
||||
rc-tree "~5.6.1"
|
||||
rc-util "^5.16.1"
|
||||
|
||||
rc-tree-select@~5.5.0:
|
||||
version "5.5.5"
|
||||
resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-5.5.5.tgz#d28b3b45da1e820cd21762ba0ee93c19429bb369"
|
||||
@ -20750,17 +20548,6 @@ rc-tree@^5.2.0, rc-tree@~5.7.0:
|
||||
rc-util "^5.16.1"
|
||||
rc-virtual-list "^3.4.8"
|
||||
|
||||
rc-tree@~5.6.1, rc-tree@~5.6.3, rc-tree@~5.6.5:
|
||||
version "5.6.9"
|
||||
resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-5.6.9.tgz#b73290a6dcad65e4ed5d8dc21cb198b30316404b"
|
||||
integrity sha512-si8aGuWQ2/sh2Ibk+WdUdDeAxoviT/+kDY+NLtJ+RhqfySqPFqWM5uHTwgFRrWUvKCqEeE/PjCYuuhHrK7Y7+A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "2.x"
|
||||
rc-motion "^2.0.1"
|
||||
rc-util "^5.16.1"
|
||||
rc-virtual-list "^3.4.8"
|
||||
|
||||
rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2, rc-trigger@^5.2.10, rc-trigger@^5.3.1:
|
||||
version "5.3.4"
|
||||
resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.3.4.tgz#6b4b26e32825677c837d1eb4d7085035eecf9a61"
|
||||
@ -20781,7 +20568,7 @@ rc-upload@~4.3.0:
|
||||
classnames "^2.2.5"
|
||||
rc-util "^5.2.0"
|
||||
|
||||
rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.12.0, rc-util@^5.15.0, rc-util@^5.16.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.21.2, rc-util@^5.22.5, rc-util@^5.23.0, rc-util@^5.24.4, rc-util@^5.26.0, rc-util@^5.27.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.6.1, rc-util@^5.7.0, rc-util@^5.8.0, rc-util@^5.9.4:
|
||||
rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.12.0, rc-util@^5.15.0, rc-util@^5.16.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.21.2, rc-util@^5.22.5, rc-util@^5.23.0, rc-util@^5.24.4, rc-util@^5.26.0, rc-util@^5.27.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.6.1, rc-util@^5.8.0, rc-util@^5.9.4:
|
||||
version "5.29.3"
|
||||
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.29.3.tgz#dc02b7b2103468e9fdf14e0daa58584f47898e37"
|
||||
integrity sha512-wX6ZwQTzY2v7phJBquN4mSEIFR0E0qumlENx0zjENtDvoVSq2s7cR95UidKRO1hOHfDsecsfM9D1gO4Kebs7fA==
|
||||
|
Loading…
x
Reference in New Issue
Block a user