Zeke Zhang 05cf9986b0
feat: enable direct dialog opening via URL and support for page mode (#4706)
* refactor: optimize page tabs routing

* test: add e2e test for page tabs

* feat: add popup routing

* fix: resolve nested issue

* refactor: rename file utils to pagePopupUtils

* perf: enhance animation and overall performance

* fix: fix filterByTK

* fix(sourceId): resolve error when sourceId is undefined

* fix: fix List and GridCard

* fix: fix params not fresh

* fix: fix parent record

* fix: resolve the issue on block data not refreshing after popup closure

* feat: bind tab with URL in popups

* feat(sub-page): enable popup to open in page mode

* chore: optimize

* feat: support association fields

* fix: address the issue of no data in associaiton field

* fix: resolve the issue with opening nested dialog in association field

* fix: fix the issue of dialog content not refreshing

* perf: use useNavigateNoUpdate to replace useNavigate

* perf: enhance popups performance by avoiding unnecessary rendering

* fix: fix tab page

* fix: fix bulk edit action

* chore: fix unit test

* chore: fix unit tests

* fix: fix bug to pass e2e tests

* chore: fix build

* fix: fix bugs to pass e2e tests

* chore: avoid crashing

* chore: make e2e tests pass

* chore: make e2e tests pass

* chore: fix unit tests

* fix(multi-app): fix known issues

* fix(Duplicate): should no page mode

* chore: fix build

* fix(mobile): fix known issues

* fix: fix open mode of Add new

* refactor: rename 'popupUid' to 'popupuid'

* refactor: rename 'subPageUid' tp 'subpageuid'

* refactor(subpage): simplify configuration of router

* fix(variable): refresh data after value change

* test: add e2e test for sub page

* refactor: refactor and add tests

* fix: fix association field

* refactor(subPage): avoid blank page occurrences

* chore: fix unit tests

* fix: correct first-click context setting for association fields

* refactor: use Action's uid for subpage

* refactor: rename x-nb-popup-context to x-action-context and move it to Action schema

* feat: add context during the creation of actions

* chore: fix build

* chore: make e2e tests pass

* fix(addChild): fix context of Add child

* fix: avoid loss or query string

* fix: avoid including 'popups' in the path

* fix: resolve issue with popup variables and add tests

* chore(e2e): fix e2e test

* fix(sideMenu): resolve the disappearing sidebar issue and add tests

* chore(e2e): fix e2e test

* fix: should refresh block data after mutiple popups closed

* chore: fix e2e test

* fix(associationField): fix wrong context

* fix: address issue with special characters
2024-06-30 23:25:01 +08:00

404 lines
13 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { onFieldInputValueChange } from '@formily/core';
import { ISchema, connect, mapProps, useField, useFieldSchema, useForm } from '@formily/react';
import {
ActionDesigner,
SchemaSettingOpenModeSchemaItems,
SchemaSettings,
SchemaSettingsItemType,
SchemaSettingsLinkageRules,
SchemaSettingsModalItem,
useCollectionState,
useCollection_deprecated,
useDesignable,
useRecord,
useSchemaToolbar,
useSyncFromForm,
} from '@nocobase/client';
import { Tree as AntdTree } from 'antd';
import { cloneDeep } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
const Tree = connect(
AntdTree,
mapProps((props, field: any) => {
useEffect(() => {
field.value = props.defaultCheckedKeys || [];
}, []);
const [checkedKeys, setCheckedKeys] = useState(props.defaultCheckedKeys || []);
const onCheck = (checkedKeys) => {
setCheckedKeys(checkedKeys);
field.value = checkedKeys;
};
field.onCheck = onCheck;
const form = useForm();
return {
...props,
checkedKeys,
onCheck,
treeData: props?.treeData.map((v: any) => {
if (form.values.duplicateMode === 'quickDulicate') {
const children = v?.children?.map((k) => {
return {
...k,
disabled: false,
};
});
return {
...v,
disabled: false,
children,
};
}
return v;
}),
};
}),
);
const getAllkeys = (data, result) => {
for (let i = 0; i < data?.length; i++) {
const { children, ...rest } = data[i];
result.push(rest.key);
if (children) {
getAllkeys(children, result);
}
}
return result;
};
const findFormBlock = (schema) => {
const formSchema = schema.reduceProperties((_, s) => {
if (s['x-decorator'] === 'FormBlockProvider') {
return s;
} else {
return findFormBlock(s);
}
}, null);
return formSchema;
};
function DuplicationMode() {
const { dn } = useDesignable();
const { t } = useTranslation();
const field = useField();
const fieldSchema = useFieldSchema();
const { name } = useCollection_deprecated();
const { collectionList, getEnableFieldTree, getOnLoadData, getOnCheck } = useCollectionState(name);
const duplicateValues = cloneDeep(fieldSchema['x-component-props']?.duplicateFields || []);
const record = useRecord();
const syncCallBack = useCallback((treeData, selectFields, form) => {
form.query('duplicateFields').take((f) => {
f.componentProps.treeData = treeData;
f.componentProps.defaultCheckedKeys = selectFields;
f.setInitialValue(selectFields);
f?.onCheck(selectFields);
form.setValues({ ...form.values, treeData });
});
}, []);
const useSelectAllFields = (form) => {
return {
async run() {
form.query('duplicateFields').take((f) => {
const selectFields = getAllkeys(f.componentProps.treeData, []);
f.componentProps.defaultCheckedKeys = selectFields;
f.setInitialValue(selectFields);
f?.onCheck?.(selectFields);
});
},
};
};
const useUnSelectAllFields = (form) => {
return {
async run() {
form.query('duplicateFields').take((f) => {
f.componentProps.defaultCheckedKeys = [];
f.setInitialValue([]);
f?.onCheck([]);
});
},
};
};
return (
<SchemaSettingsModalItem
title={t('Duplicate mode')}
components={{ Tree }}
scope={{
getEnableFieldTree,
collectionName: fieldSchema['x-component-props']?.duplicateCollection || record?.__collection || name,
currentCollection: record?.__collection || name,
getOnLoadData,
getOnCheck,
treeData: fieldSchema['x-component-props']?.treeData,
duplicateValues,
onFieldInputValueChange,
}}
schema={
{
type: 'object',
title: t('Duplicate mode'),
properties: {
duplicateMode: {
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
title: t('Duplicate mode'),
default: fieldSchema['x-component-props']?.duplicateMode || 'quickDulicate',
enum: [
{ value: 'quickDulicate', label: '{{t("Direct duplicate")}}' },
{ value: 'continueduplicate', label: '{{t("Copy into the form and continue to fill in")}}' },
],
},
collection: {
type: 'string',
title: '{{ t("Target collection") }}',
required: true,
description: t('If collection inherits, choose inherited collections as templates'),
default: '{{ collectionName }}',
'x-display': collectionList.length > 1 ? 'visible' : 'hidden',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
options: collectionList,
},
'x-reactions': [
{
dependencies: ['.duplicateMode'],
fulfill: {
state: {
disabled: `{{ $deps[0]==="quickDulicate" }}`,
value: `{{ $deps[0]==="quickDulicate"? currentCollection:collectionName }}`,
},
},
},
],
},
syncFromForm: {
type: 'void',
title: '{{ t("Sync from form fields") }}',
'x-component': 'Action.Link',
'x-component-props': {
type: 'primary',
style: { float: 'right', position: 'relative', zIndex: 1200 },
useAction: () => {
const formSchema = useMemo(() => findFormBlock(fieldSchema), [fieldSchema]);
return useSyncFromForm(
formSchema,
fieldSchema['x-component-props']?.duplicateCollection || record?.__collection || name,
syncCallBack,
);
},
},
'x-reactions': [
{
dependencies: ['.duplicateMode'],
fulfill: {
state: {
visible: `{{ $deps[0]!=="quickDulicate" }}`,
},
},
},
],
},
selectAll: {
type: 'void',
title: '{{ t("Select all") }}',
'x-component': 'Action.Link',
'x-reactions': [
{
dependencies: ['.duplicateMode'],
fulfill: {
state: {
visible: `{{ $deps[0]==="quickDulicate"}}`,
},
},
},
],
'x-component-props': {
type: 'primary',
style: { float: 'right', position: 'relative', zIndex: 1200 },
useAction: () => {
const from = useForm();
return useSelectAllFields(from);
},
},
},
unselectAll: {
type: 'void',
title: '{{ t("UnSelect all") }}',
'x-component': 'Action.Link',
'x-reactions': [
{
dependencies: ['.duplicateMode', '.duplicateFields'],
fulfill: {
state: {
visible: `{{ $deps[0]==="quickDulicate"&&$form.getValuesIn('duplicateFields').length>0 }}`,
},
},
},
],
'x-component-props': {
type: 'primary',
style: { float: 'right', position: 'relative', zIndex: 1200, marginRight: '10px' },
useAction: () => {
const from = useForm();
return useUnSelectAllFields(from);
},
},
},
duplicateFields: {
type: 'array',
title: '{{ t("Data fields") }}',
required: true,
description: t('Only the selected fields will be used as the initialization data for the form'),
'x-decorator': 'FormItem',
'x-component': Tree,
'x-component-props': {
defaultCheckedKeys: duplicateValues,
treeData: [],
checkable: true,
checkStrictly: true,
selectable: false,
loadData: '{{ getOnLoadData($self) }}',
onCheck: '{{ getOnCheck($self) }}',
rootStyle: {
padding: '8px 0',
border: '1px solid #d9d9d9',
borderRadius: '2px',
maxHeight: '30vh',
overflow: 'auto',
margin: '2px 0',
},
},
'x-reactions': [
{
dependencies: ['.collection', '.duplicateMode'],
fulfill: {
state: {
disabled: '{{ !$deps[0] }}',
componentProps: {
treeData: '{{ getEnableFieldTree($deps[0], $self,treeData) }}',
},
},
},
},
],
},
},
} as ISchema
}
onSubmit={({ duplicateMode, collection, duplicateFields, treeData }) => {
const fields = Array.isArray(duplicateFields) ? duplicateFields : duplicateFields.checked || [];
field.componentProps.duplicateMode = duplicateMode;
field.componentProps.duplicateFields = fields;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].duplicateMode = duplicateMode;
fieldSchema['x-component-props'].duplicateFields = fields;
fieldSchema['x-component-props'].duplicateCollection = collection;
fieldSchema['x-component-props'].treeData = treeData || field.componentProps?.treeData;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': {
...fieldSchema['x-component-props'],
},
},
});
dn.refresh();
}}
/>
);
}
const schemaSettingsItems: SchemaSettingsItemType[] = [
{
name: 'Customize',
Component: (props): any => {
return props.children;
},
children: [
{
name: 'editButton',
Component: ActionDesigner.ButtonEditor,
useComponentProps() {
const { buttonEditorProps } = useSchemaToolbar();
return buttonEditorProps;
},
},
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
collectionName: name,
};
},
},
{
name: 'duplicationMode',
Component: DuplicationMode,
useVisible() {
const fieldSchema = useFieldSchema();
const isDuplicateAction = fieldSchema['x-action'] === 'duplicate';
return isDuplicateAction;
},
},
{
name: 'openMode',
Component: SchemaSettingOpenModeSchemaItems,
useComponentProps() {
const { t } = useTranslation();
const modeOptions = useMemo(() => {
return [
{ label: t('Drawer'), value: 'drawer' },
{ label: t('Dialog'), value: 'modal' },
];
}, [t]);
return {
openMode: true,
openSize: true,
modeOptions,
};
},
},
{
name: 'remove',
sort: 100,
Component: ActionDesigner.RemoveButton as any,
useComponentProps() {
const { removeButtonProps } = useSchemaToolbar();
return removeButtonProps;
},
},
],
},
];
/**
* @deprecated
* 用于兼容之前的 name
*/
const deprecatedDuplicateActionSettings = new SchemaSettings({
name: 'ActionSettings:duplicate',
items: schemaSettingsItems,
});
const duplicateActionSettings = new SchemaSettings({
name: 'actionSettings:duplicate',
items: schemaSettingsItems,
});
export { deprecatedDuplicateActionSettings, duplicateActionSettings };