Merge branch 'next' into develop

This commit is contained in:
Zeke Zhang 2024-08-28 14:57:46 +08:00
commit f03ac173d0
51 changed files with 760 additions and 384 deletions

View File

@ -34,8 +34,7 @@ jobs:
shell: bash
run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
APP_TOKEN=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v3

View File

@ -30,8 +30,7 @@ jobs:
shell: bash
run: |
APP_TOKEN=${{ steps.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$APP_TOKEN" | openssl enc -aes-256-cbc -pbkdf2 -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
ENCRYPTED_SECRET=$(echo -n "$BINARY_ENCRYPTED_SECRET" | base64 -w 0);
ENCRYPTED_SECRET=$(echo -n "$APP_TOKEN" | openssl enc -aes-256-cbc -pbkdf2 -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}" | base64 -w 0);
echo "token=$ENCRYPTED_SECRET" >> $GITHUB_OUTPUT
- name: Get GitHub App User ID
id: get-user-id

View File

@ -34,8 +34,7 @@ jobs:
shell: bash
run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
APP_TOKEN=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4

View File

@ -30,8 +30,7 @@ jobs:
shell: bash
run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
APP_TOKEN=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4
@ -60,8 +59,7 @@ jobs:
shell: bash
run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
APP_TOKEN=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4

View File

@ -21,8 +21,7 @@ jobs:
shell: bash
run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
APP_TOKEN=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4

View File

@ -23,8 +23,7 @@ jobs:
shell: bash
run: |
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
APP_TOKEN=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v3

View File

@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
## [v1.3.4-beta](https://github.com/nocobase/nocobase/compare/v1.3.3-beta...v1.3.4-beta) - 2024-08-27
### Merged
- refactor: set remainsTheSame as the default value for field editing in bulk editing action [`#5124`](https://github.com/nocobase/nocobase/pull/5124)
### Commits
- chore(versions): 😊 publish v1.3.4-beta [`a011748`](https://github.com/nocobase/nocobase/commit/a0117480e037e48a23f59921110003047a1a174b)
- chore: update changelog [`3403e8d`](https://github.com/nocobase/nocobase/commit/3403e8d76684950d6962a6110a4440eb95856a35)
## [v1.3.3-beta](https://github.com/nocobase/nocobase/compare/v1.3.2-beta...v1.3.3-beta) - 2024-08-27
### Merged

View File

@ -141,6 +141,7 @@ export const useDetailsBlockProps = () => {
ctx.form
.reset()
.then(() => {
ctx.form.setInitialValues(data || {});
ctx.form.setValues(data || {});
})
.catch(console.error);

View File

@ -87,7 +87,7 @@ const InternalFormBlockProvider = (props) => {
updateAssociationValues,
]);
if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) {
if (service.loading && Object.keys(form?.initialValues || {})?.length === 0 && action) {
return <Spin />;
}
@ -151,9 +151,10 @@ export const useFormBlockContext = () => {
/**
* @internal
*/
export const useFormBlockProps = (shouldClearInitialValues = false) => {
export const useFormBlockProps = () => {
const ctx = useFormBlockContext();
const treeParentRecord = useTreeParentRecord();
useEffect(() => {
if (treeParentRecord) {
ctx.form?.query('parent').take((field) => {
@ -164,19 +165,14 @@ export const useFormBlockProps = (shouldClearInitialValues = false) => {
});
useEffect(() => {
if (!ctx?.service?.loading) {
const form: Form = ctx.form;
if (form) {
// form 字段中可能一开始就存在一些默认值(比如设置了字段默认值的模板区块)。在编辑表单中,
// 这些默认值是不需要的需要清除掉不然会导致一些问题。比如https://github.com/nocobase/nocobase/issues/4868
if (shouldClearInitialValues === true) {
form.initialValues = {};
form.reset();
}
form.setInitialValues(ctx.service?.data?.data);
}
const form: Form = ctx.form;
if (!form || ctx.service?.loading) {
return;
}
}, [ctx?.service?.loading]);
form.setInitialValues(ctx.service?.data?.data);
}, [ctx.form, ctx.service?.data?.data, ctx.service?.loading]);
return {
form: ctx.form,
};

View File

@ -24,7 +24,7 @@ describe('parseVariablesAndChangeParamsToQueryString', () => {
{ name: 'param3', value: 'value3' },
];
const variables: any = {
parseVariable: vi.fn().mockResolvedValue('parsedValue'),
parseVariable: vi.fn().mockResolvedValue({ value: 'parsedValue' }),
};
const localVariables: any = [
{ name: '$var1', ctx: { value: 'localValue1' } },

View File

@ -163,9 +163,9 @@ export function useCollectValuesToSubmit() {
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
@ -324,9 +324,9 @@ export const useAssociationCreateActionProps = () => {
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
@ -582,9 +582,9 @@ export const useCustomizeUpdateActionProps = () => {
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
@ -680,9 +680,9 @@ export const useCustomizeBulkUpdateActionProps = () => {
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
@ -888,9 +888,9 @@ export const useUpdateActionProps = () => {
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
@ -1661,8 +1661,8 @@ export async function parseVariablesAndChangeParamsToQueryString({
searchParams.map(async ({ name, value }) => {
if (typeof value === 'string') {
if (isVariable(value)) {
const result = await variables.parseVariable(value, localVariables);
return { name, value: result };
const { value: parsedValue } = (await variables.parseVariable(value, localVariables)) || {};
return { name, value: parsedValue };
}
const result = await replaceVariableValue(value, variables, localVariables);
return { name, value: result };

View File

@ -26,7 +26,10 @@ test.describe('bulk edit form', () => {
// 2. 打开弹窗,显示出批量编辑表单
await page.getByLabel('action-Action-Bulk edit-').click();
// 默认为 "Changed to" 模式,此时应该显示字段是必填的
await page.getByLabel('block-item-BulkEditField-').click();
await page.getByRole('option', { name: 'Changed to' }).click();
// "Changed to" 模式,此时应该显示字段是必填的
await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible();
// 3. 输入值,点击提交
@ -45,7 +48,10 @@ test.describe('bulk edit form', () => {
// 1. 打开弹窗,显示出批量编辑表单
await page.getByLabel('action-Action-Bulk edit-').click();
// 默认为 "Changed to" 模式,此时应该显示字段是必填的
await page.getByLabel('block-item-BulkEditField-').click();
await page.getByRole('option', { name: 'Changed to' }).click();
// "Changed to" 模式,此时应该显示字段是必填的
await expect(page.getByLabel('block-item-BulkEditField-').getByText('*')).toBeVisible();
// 2. 切换为其它模式,此时应该不显示字段是必填的

View File

@ -10,5 +10,5 @@
import { useFormBlockProps } from '../../../../../block-provider/FormBlockProvider';
export function useEditFormBlockProps() {
return useFormBlockProps(true);
return useFormBlockProps();
}

View File

@ -17,7 +17,10 @@ import { PopupVisibleProvider, PopupVisibleProviderContext } from '../../schema-
* @param props
* @returns
*/
export const PopupContextProvider: React.FC = (props) => {
export const PopupContextProvider: React.FC<{
visible?: boolean;
setVisible?: (visible: boolean) => void;
}> = (props) => {
const [visible, setVisible] = useState(false);
const { visible: visibleWithURL, setVisible: setVisibleWithURL } = useContext(PopupVisibleProviderContext) || {
visible: false,
@ -26,10 +29,11 @@ export const PopupContextProvider: React.FC = (props) => {
const fieldSchema = useFieldSchema();
const _setVisible = useCallback(
(value: boolean): void => {
props.setVisible?.(value);
setVisible?.(value);
setVisibleWithURL?.(value);
},
[setVisibleWithURL],
[props, setVisibleWithURL],
);
const openMode = fieldSchema['x-component-props']?.['openMode'] || 'drawer';
const openSize = fieldSchema['x-component-props']?.['openSize'];
@ -37,7 +41,7 @@ export const PopupContextProvider: React.FC = (props) => {
return (
<PopupVisibleProvider visible={false}>
<ActionContextProvider
visible={visible || visibleWithURL}
visible={props.visible || visible || visibleWithURL}
setVisible={_setVisible}
openMode={openMode}
openSize={openSize}

View File

@ -9,6 +9,7 @@
import { Field } from '@formily/core';
import { useField, useFieldSchema, useForm } from '@formily/react';
import { untracked } from '@formily/reactive';
import { nextTick } from '@nocobase/utils/client';
import _ from 'lodash';
import { useEffect, useMemo, useRef } from 'react';
@ -20,7 +21,6 @@ import { useVariables } from '../../../../variables';
import { transformVariableValue } from '../../../../variables/utils/transformVariableValue';
import { useSubFormValue } from '../../association-field/hooks';
import { isDisplayField } from '../utils';
import { untracked } from '@formily/reactive';
/**
* Form
@ -97,7 +97,7 @@ const useLazyLoadDisplayAssociationFieldsOfForm = () => {
variables
.parseVariable(variableString, formVariable, { appends })
.then((value) => {
.then(({ value }) => {
nextTick(() => {
const result = transformVariableValue(value, { targetCollectionField: collectionFieldRef.current });
// fix https://nocobase.height.app/T-2608

View File

@ -14,6 +14,7 @@ import { getValuesByPath } from '@nocobase/utils/client';
import _ from 'lodash';
import { useCallback, useEffect } from 'react';
import { useRecordIndex } from '../../../../../src/record-provider';
import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider';
import { useCollection_deprecated } from '../../../../collection-manager';
import { useCollectionRecord } from '../../../../data-source/collection-record/CollectionRecordProvider';
import { useFlag } from '../../../../flag-provider';
@ -39,6 +40,7 @@ const useParseDefaultValue = () => {
const { getField } = useCollection_deprecated();
const { isSpecialCase, setDefaultValue } = useSpecialCase();
const index = useRecordIndex();
const { type, form } = useFormBlockContext();
/**
* name: $user
@ -55,6 +57,13 @@ const useParseDefaultValue = () => {
);
useEffect(() => {
// fix https://github.com/nocobase/nocobase/issues/4868
// fix http://localhost:12000/admin/ugmnj2ycfgg/popups/1qlw5c38t3b/puid/dz42x7ffr7i/filterbytk/182
// to clear the default value of the field
if (type === 'update' && fieldSchema.default && field.form === form) {
field.setValue?.(record?.data?.[fieldSchema.name]);
}
if (
fieldSchema.default == null ||
isInSetDefaultValueDialog ||
@ -86,7 +95,18 @@ const useParseDefaultValue = () => {
}
}
const value = transformVariableValue(await variables.parseVariable(fieldSchema.default, localVariables), {
const { value: parsedValue, collectionName: collectionNameOfVariable } = await variables.parseVariable(
fieldSchema.default,
localVariables,
);
// fix https://tasks.aliyun.nocobase.com/admin/ugmnj2ycfgg/popups/1qlw5c38t3b/puid/dz42x7ffr7i/filterbytk/199
if (collectionField.target && collectionField.target !== collectionNameOfVariable) {
field.loading = false;
return;
}
const value = transformVariableValue(parsedValue, {
targetCollectionField: collectionField,
});
@ -154,7 +174,7 @@ const useParseDefaultValue = () => {
// 解决子表格(或子表单)中新增一行数据时,默认值不生效的问题
field.setValue(fieldSchema.default);
}
}, [fieldSchema.default, localVariables]);
}, [fieldSchema.default, localVariables, type]);
};
export default useParseDefaultValue;

View File

@ -156,14 +156,14 @@ export async function replaceVariables(
}
const waitForParsing = value.match(REGEX_OF_VARIABLE_IN_EXPRESSION)?.map(async (item) => {
const result = await variables.parseVariable(item, localVariables);
const { value: parsedValue } = await variables.parseVariable(item, localVariables);
// 在开头加 `_` 是为了保证 id 不能以数字开头,否则在解析表达式的时候(不是解析变量)会报错
const id = `_${uid()}`;
scope[id] = result;
scope[id] = parsedValue;
store[item] = id;
return result;
return parsedValue;
});
if (waitForParsing) {

View File

@ -33,7 +33,7 @@ interface PopupsVisibleProviderProps {
setVisible?: (value: boolean) => void;
}
interface PopupProps {
export interface PopupProps {
params: PopupParams;
context: PopupContext;
/**

View File

@ -7,13 +7,31 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useCallback } from 'react';
import React, { FC, useCallback, useMemo } from 'react';
const PopupSettingsContext = React.createContext({
enableURL: true,
});
export const PopupSettingsProvider: FC<{
/**
* @default true
*/
enableURL?: boolean;
}> = (props) => {
const { enableURL = true } = props;
const value = useMemo(() => ({ enableURL }), [enableURL]);
return <PopupSettingsContext.Provider value={value}>{props.children}</PopupSettingsContext.Provider>;
};
/**
* Hook for accessing the popup settings.
* @returns The popup settings.
*/
export const usePopupSettings = () => {
const { enableURL } = React.useContext(PopupSettingsContext);
const isPopupVisibleControlledByURL = useCallback(() => {
const pathname = window.location.pathname;
const hash = window.location.hash;
@ -21,8 +39,8 @@ export const usePopupSettings = () => {
const isNewMobileMode = pathname?.includes('/m/');
const isPCMode = pathname?.includes('/admin/');
return (isPCMode || isNewMobileMode) && !isOldMobileMode;
}, []);
return (isPCMode || isNewMobileMode) && !isOldMobileMode && enableURL;
}, [enableURL]);
return {
/** 弹窗窗口的显隐是否由 URL 控制 */

View File

@ -15,3 +15,4 @@ export * from './Page.Settings';
export { PagePopups } from './PagePopups';
export { storePopupContext } from './pagePopupUtils';
export * from './PageTab.Settings';
export { PopupSettingsProvider } from './PopupSettingsProvider';

View File

@ -127,7 +127,16 @@ export const getPopupPathFromParams = (params: PopupParams) => {
* Note: use this hook in a plugin is not recommended
* @returns
*/
export const usePopupUtils = () => {
export const usePopupUtils = (
options: {
/**
* when the popup does not support opening via URL, you can control the display status of the popup through this method
* @param visible
* @returns
*/
setVisible?: (visible: boolean) => void;
} = {},
) => {
const navigate = useNavigateNoUpdate();
const location = useLocationNoUpdate();
const fieldSchema = useFieldSchema();
@ -141,14 +150,16 @@ export const usePopupUtils = () => {
const { params: popupParams } = useCurrentPopupContext();
const service = useDataBlockRequest();
const { isPopupVisibleControlledByURL } = usePopupSettings();
const { setVisible: setVisibleFromAction } = useContext(ActionContext);
const { setVisible: _setVisibleFromAction } = useContext(ActionContext);
const { updatePopupContext } = usePopupContextInActionOrAssociationField();
const currentPopupContext = useCurrentPopupContext();
const getSourceId = useCallback(
(_parentRecordData?: Record<string, any>) =>
(_parentRecordData || parentRecord?.data)?.[cm.getSourceKeyByAssociation(association)],
[parentRecord, association],
);
const currentPopupUidWithoutOpened = fieldSchema?.['x-uid'];
const setVisibleFromAction = options.setVisible || _setVisibleFromAction;
const getNewPathname = useCallback(
({
@ -199,6 +210,7 @@ export const usePopupUtils = () => {
parentRecordData,
collectionNameUsedInURL,
popupUidUsedInURL,
customActionSchema,
}: {
recordData?: Record<string, any>;
parentRecordData?: Record<string, any>;
@ -206,11 +218,13 @@ export const usePopupUtils = () => {
collectionNameUsedInURL?: string;
/** if this value exists, it will be saved in the URL */
popupUidUsedInURL?: string;
customActionSchema?: ISchema;
} = {}) => {
if (!isPopupVisibleControlledByURL()) {
return setVisibleFromAction?.(true);
}
const currentPopupUidWithoutOpened = customActionSchema?.['x-uid'] || fieldSchema?.['x-uid'];
const sourceId = getSourceId(parentRecordData);
recordData = recordData || record?.data;
@ -227,7 +241,7 @@ export const usePopupUtils = () => {
}
storePopupContext(currentPopupUidWithoutOpened, {
schema: fieldSchema,
schema: customActionSchema || fieldSchema,
record: new CollectionRecord({ isNew: false, data: recordData }),
parentRecord: parentRecordData ? new CollectionRecord({ isNew: false, data: parentRecordData }) : parentRecord,
service,
@ -237,7 +251,7 @@ export const usePopupUtils = () => {
sourceId,
});
updatePopupContext(getPopupContext());
updatePopupContext(getPopupContext(), customActionSchema);
navigate(withSearchParams(`${url}${pathname}`));
},
@ -256,7 +270,6 @@ export const usePopupUtils = () => {
isPopupVisibleControlledByURL,
getSourceId,
getPopupContext,
currentPopupUidWithoutOpened,
],
);
@ -317,6 +330,7 @@ export const usePopupUtils = () => {
closePopup,
savePopupSchemaToSchema,
getPopupSchemaFromSchema,
context: currentPopupContext,
/**
* @deprecated
* TODO: remove this

View File

@ -29,18 +29,19 @@ export const usePopupContextInActionOrAssociationField = () => {
const { dn } = useDesignable();
const updatePopupContext = useCallback(
(context: PopupContext) => {
(context: PopupContext, customSchema?: ISchema) => {
customSchema = customSchema || fieldSchema;
context = _.omitBy(context, _.isNil) as PopupContext;
if (_.isEqual(context, getPopupContextFromActionOrAssociationFieldSchema(fieldSchema))) {
if (_.isEqual(context, getPopupContextFromActionOrAssociationFieldSchema(customSchema))) {
return;
}
fieldSchema[CONTEXT_SCHEMA_KEY] = context;
customSchema[CONTEXT_SCHEMA_KEY] = context;
return dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-uid': customSchema['x-uid'],
[CONTEXT_SCHEMA_KEY]: context,
},
});

View File

@ -7,13 +7,13 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import _, { every, findIndex, some } from 'lodash';
import Handlebars from 'handlebars';
import _, { every, findIndex, some } from 'lodash';
import { replaceVariableValue } from '../../../block-provider/hooks';
import { VariableOption, VariablesContextType } from '../../../variables/types';
import { isVariable } from '../../../variables/utils/isVariable';
import { transformVariableValue } from '../../../variables/utils/transformVariableValue';
import { getJsonLogic } from '../../common/utils/logic';
import { replaceVariableValue } from '../../../block-provider/hooks';
type VariablesCtx = {
/** 当前登录的用户 */
@ -102,12 +102,14 @@ export const conditionAnalyses = async ({
}
const targetVariableName = targetFieldToVariableString(getTargetField(condition));
const targetValue = variables.parseVariable(targetVariableName, localVariables, {
doNotRequest: true,
});
const targetValue = variables
.parseVariable(targetVariableName, localVariables, {
doNotRequest: true,
})
.then(({ value }) => value);
const parsingResult = isVariable(jsonlogic?.value)
? [variables.parseVariable(jsonlogic?.value, localVariables), targetValue]
? [variables.parseVariable(jsonlogic?.value, localVariables).then(({ value }) => value), targetValue]
: [jsonlogic?.value, targetValue];
try {

View File

@ -55,7 +55,7 @@ const useParseDataScopeFilter = ({ exclude = defaultExclude }: Props = {}) => {
if (exclude.includes(getVariableName(value))) {
return value;
}
const result = variables?.parseVariable(value, localVariables);
const result = variables?.parseVariable(value, localVariables).then(({ value }) => value);
return result;
},
});

View File

@ -69,7 +69,7 @@ const VariablesProvider = ({ children }) => {
* 2. `key` `key` api `ctx`
* 3. `key` `key`
*/
const getValue = useCallback(
const getResult = useCallback(
async (
variablePath: string,
localVariables?: VariableOption[],
@ -87,13 +87,23 @@ const VariablesProvider = ({ children }) => {
const { fieldPath, dataSource, variableOption } = getFieldPath(variableName, _variableToCollectionName);
let collectionName = fieldPath;
const { fieldPath: fieldPathOfVariable } = getFieldPath(variablePath, _variableToCollectionName);
const collectionNameOfVariable =
list.length === 1
? variableOption.collectionName
: getCollectionJoinField(fieldPathOfVariable, dataSource)?.target;
if (!(variableName in current)) {
throw new Error(`VariablesProvider: ${variableName} is not found`);
}
for (let index = 0; index < list.length; index++) {
if (current == null) {
return current === undefined ? variableOption.defaultValue : current;
return {
value: current === undefined ? variableOption.defaultValue : current,
dataSource,
collectionName: collectionNameOfVariable,
};
}
const key = list[index];
@ -171,8 +181,12 @@ const VariablesProvider = ({ children }) => {
}
}
const result = compile(_.isFunction(current) ? current() : current);
return result === undefined ? variableOption.defaultValue : result;
const _value = compile(_.isFunction(current) ? current() : current);
return {
value: _value === undefined ? variableOption.defaultValue : _value,
dataSource,
collectionName: collectionNameOfVariable,
};
},
[getCollectionJoinField],
);
@ -248,11 +262,14 @@ const VariablesProvider = ({ children }) => {
}
const path = getPath(str);
const value = await getValue(path, localVariables as VariableOption[], options);
const result = await getResult(path, localVariables as VariableOption[], options);
return uniq(filterEmptyValues(value));
return {
...result,
value: uniq(filterEmptyValues(result.value)),
};
},
[getValue],
[getResult],
);
const getCollectionField = useCallback(

View File

@ -276,7 +276,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.nickname }}')).toBe('test');
expect(await result.current.parseVariable('{{ $user.nickname }}').then(({ value }) => value)).toBe('test');
});
});
@ -286,7 +286,9 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.nickname }}')).toBe('from request');
expect(await result.current.parseVariable('{{ $user.nickname }}').then(({ value }) => value)).toBe(
'from request',
);
});
});
@ -296,7 +298,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.belongsToField }}')).toEqual({
expect(await result.current.parseVariable('{{ $user.belongsToField }}').then(({ value }) => value)).toEqual({
id: 0,
name: '$user.belongsToField',
});
@ -309,9 +311,11 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.belongsToField }}', undefined, { doNotRequest: true })).toBe(
null,
);
expect(
await result.current
.parseVariable('{{ $user.belongsToField }}', undefined, { doNotRequest: true })
.then(({ value }) => value),
).toBe(null);
});
});
@ -321,7 +325,9 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.belongsToField.name }}')).toBe('$user.belongsToField');
expect(await result.current.parseVariable('{{ $user.belongsToField.name }}').then(({ value }) => value)).toBe(
'$user.belongsToField',
);
});
});
@ -331,7 +337,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.hasManyField }}')).toEqual([
expect(await result.current.parseVariable('{{ $user.hasManyField }}').then(({ value }) => value)).toEqual([
{
id: 0,
name: '$user.hasManyField',
@ -340,7 +346,9 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.hasManyField.name }}')).toEqual(['$user.hasManyField']);
expect(await result.current.parseVariable('{{ $user.hasManyField.name }}').then(({ value }) => value)).toEqual([
'$user.hasManyField',
]);
});
});
@ -350,7 +358,9 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.hasManyField.hasManyField }}')).toEqual([
expect(
await result.current.parseVariable('{{ $user.hasManyField.hasManyField }}').then(({ value }) => value),
).toEqual([
{
id: 0,
name: '$user.hasManyField.hasManyField',
@ -359,15 +369,34 @@ describe('useVariables', () => {
});
});
it('$user.hasManyField', async () => {
const { result } = renderHook(() => useVariables(), {
wrapper: Providers,
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.hasManyField }}')).toEqual({
collectionName: 'test',
dataSource: 'main',
value: [
{
id: 0,
name: '$user.hasManyField',
},
],
});
});
});
it('$user.hasManyField.hasManyField.name', async () => {
const { result } = renderHook(() => useVariables(), {
wrapper: Providers,
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.hasManyField.hasManyField.name }}')).toEqual([
'$user.hasManyField.hasManyField',
]);
expect(
await result.current.parseVariable('{{ $user.hasManyField.hasManyField.name }}').then(({ value }) => value),
).toEqual(['$user.hasManyField.hasManyField']);
});
});
@ -393,7 +422,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.hasManyField }}')).toEqual([
expect(await result.current.parseVariable('{{ $user.hasManyField }}').then(({ value }) => value)).toEqual([
{
id: 0,
name: '$user.hasManyField',
@ -402,7 +431,9 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.hasManyField.hasManyField }}')).toEqual([
expect(
await result.current.parseVariable('{{ $user.hasManyField.hasManyField }}').then(({ value }) => value),
).toEqual([
{
id: 0,
name: '$user.hasManyField.hasManyField',
@ -417,7 +448,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $user.belongsToManyField }}')).toEqual([
expect(await result.current.parseVariable('{{ $user.belongsToManyField }}').then(({ value }) => value)).toEqual([
{
id: 0,
name: '$user.belongsToManyField',
@ -608,7 +639,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $new.name }}')).toBe('new variable');
expect(await result.current.parseVariable('{{ $new.name }}').then(({ value }) => value)).toBe('new variable');
});
});
@ -627,7 +658,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $new.noExist }}')).toBe(null);
expect(await result.current.parseVariable('{{ $new.noExist }}').then(({ value }) => value)).toBe(null);
});
});
@ -647,7 +678,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $new.noExist }}')).toBe('default value');
expect(await result.current.parseVariable('{{ $new.noExist }}').then(({ value }) => value)).toBe('default value');
});
});
@ -667,7 +698,7 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $new.noExist }}')).toBe(undefined);
expect(await result.current.parseVariable('{{ $new.noExist }}').then(({ value }) => value)).toBe(undefined);
});
});
@ -686,8 +717,30 @@ describe('useVariables', () => {
ctx: {
name: 'local variable',
},
collectionName: 'local',
dataSource: 'local',
}),
).toBe('local variable');
).toEqual({
value: 'local variable',
dataSource: 'local',
});
expect(
await result.current.parseVariable('{{ $local }}', {
name: '$local',
ctx: {
name: 'local variable',
},
collectionName: 'local',
dataSource: 'local',
}),
).toEqual({
value: {
name: 'local variable',
},
collectionName: 'local',
dataSource: 'local',
});
// 由于 $local 是一个局部变量,所以不会被缓存到 ctx 中
expect(result.current.getVariable('$local')).toBe(null);
@ -703,14 +756,16 @@ describe('useVariables', () => {
});
expect(
await result.current.parseVariable('{{ $local.name }}', [
{
name: '$local',
ctx: {
name: 'local variable',
await result.current
.parseVariable('{{ $local.name }}', [
{
name: '$local',
ctx: {
name: 'local variable',
},
},
},
]),
])
.then(({ value }) => value),
).toBe('local variable');
// 由于 $local 是一个局部变量,所以不会被缓存到 ctx 中
@ -764,7 +819,9 @@ describe('useVariables', () => {
});
await waitFor(async () => {
expect(await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}')).toEqual({
expect(
await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}').then(({ value }) => value),
).toEqual({
id: 0,
name: '$some.belongsToField.belongsToField',
});
@ -783,7 +840,9 @@ describe('useVariables', () => {
await waitFor(async () => {
// 只有解析后的值是 undefined 才会使用默认值
expect(await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}')).toBe(null);
expect(
await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}').then(({ value }) => value),
).toBe(null);
});
// 会覆盖之前的 $some
@ -798,7 +857,9 @@ describe('useVariables', () => {
await waitFor(async () => {
// 解析后的值是 undefined 所以会返回上面设置的默认值
expect(await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}')).toBe('default value');
expect(
await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}').then(({ value }) => value),
).toBe('default value');
});
});

View File

@ -47,7 +47,14 @@ export interface VariablesContextType {
/** do not request when the association field is empty */
doNotRequest?: boolean;
},
) => Promise<any>;
) => Promise<{
value: any;
/**
*
*/
collectionName?: string;
dataSource?: string;
}>;
/**
*
* @param variableOption

View File

@ -0,0 +1,19 @@
/**
* 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 { ACLActionProvider, PopupSettingsProvider } from '@nocobase/client';
import React, { FC } from 'react';
export const BulkEditActionDecorator: FC = (props) => {
return (
<PopupSettingsProvider enableURL={false}>
<ACLActionProvider>{props.children}</ACLActionProvider>
</PopupSettingsProvider>
);
};

View File

@ -95,7 +95,7 @@ export const BulkEditField = (props: any) => {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const field = useField<Field>();
const [type, setType] = useState<number>(BulkEditFormItemValueType.ChangedTo);
const [type, setType] = useState<number>(BulkEditFormItemValueType.RemainsTheSame);
const [value, setValue] = useState(null);
const { getField } = useCollection_deprecated();
const collectionField = getField(fieldSchema.name) || {};

View File

@ -9,6 +9,7 @@
import { Plugin, useActionAvailable } from '@nocobase/client';
import { bulkEditActionSettings, deprecatedBulkEditActionSettings } from './BulkEditAction.Settings';
import { BulkEditActionDecorator } from './BulkEditActionDecorator';
import { BulkEditActionInitializer } from './BulkEditActionInitializer';
import {
BulkEditBlockInitializers_deprecated,
@ -25,7 +26,7 @@ import { BulkEditField } from './component/BulkEditField';
import { useCustomizeBulkEditActionProps } from './utils';
export class PluginActionBulkEditClient extends Plugin {
async load() {
this.app.addComponents({ BulkEditField });
this.app.addComponents({ BulkEditField, BulkEditActionDecorator });
this.app.addScopes({ useCustomizeBulkEditActionProps });
this.app.schemaSettingsManager.add(deprecatedBulkEditActionSettings);
this.app.schemaSettingsManager.add(bulkEditActionSettings);
@ -45,7 +46,7 @@ export class PluginActionBulkEditClient extends Plugin {
Component: BulkEditActionInitializer,
schema: {
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-decorator': 'BulkEditActionDecorator',
'x-action': 'customize:bulkEdit',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:bulkEdit',

View File

@ -64,7 +64,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
const result = await variables?.parseVariable(value, localVariables).then(({ value }) => value);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
}

View File

@ -13,6 +13,7 @@ import {
ActionContextProvider,
CollectionProvider_deprecated,
FormBlockContext,
PopupSettingsProvider,
RecordProvider,
fetchTemplateData,
useACLActionParamsContext,
@ -203,7 +204,9 @@ export const DuplicateAction = observer(
{/* 这里的 record 就是弹窗中创建表单的 sourceRecord */}
<RecordProvider record={{ ...parentRecordData, __collection: duplicateCollection || __collection }}>
<ActionContextProvider value={{ ...ctx, visible, setVisible }}>
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
<PopupSettingsProvider enableURL={false}>
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
</PopupSettingsProvider>
</ActionContextProvider>
</RecordProvider>
</CollectionProvider_deprecated>

View File

@ -0,0 +1,15 @@
/**
* 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 { ACLActionProvider } from '@nocobase/client';
import React, { FC } from 'react';
export const DuplicateActionDecorator: FC = (props) => {
return <ACLActionProvider>{props.children}</ACLActionProvider>;
};

View File

@ -19,7 +19,7 @@ export const DuplicateActionInitializer = (props) => {
'x-acl-action': 'create',
title: '{{ t("Duplicate") }}',
'x-component': 'Action.Link',
'x-decorator': 'ACLActionProvider',
'x-decorator': 'DuplicateActionDecorator',
'x-component-props': {
openMode: defaultOpenMode,
component: 'DuplicateAction',

View File

@ -10,6 +10,7 @@
import { Plugin, useActionAvailable } from '@nocobase/client';
import { DuplicateAction } from './DuplicateAction';
import { deprecatedDuplicateActionSettings, duplicateActionSettings } from './DuplicateAction.Settings';
import { DuplicateActionDecorator } from './DuplicateActionDecorator';
import { DuplicateActionInitializer } from './DuplicateActionInitializer';
import { DuplicatePluginProvider } from './DuplicatePluginProvider';
@ -19,6 +20,7 @@ export class PluginActionDuplicateClient extends Plugin {
this.app.addComponents({
DuplicateActionInitializer,
DuplicateAction,
DuplicateActionDecorator,
});
this.app.schemaSettingsManager.add(deprecatedDuplicateActionSettings);
this.app.schemaSettingsManager.add(duplicateActionSettings);
@ -31,7 +33,7 @@ export class PluginActionDuplicateClient extends Plugin {
'x-action': 'duplicate',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:duplicate',
'x-decorator': 'ACLActionProvider',
'x-decorator': 'DuplicateActionDecorator',
'x-component-props': {
type: 'primary',
},

View File

@ -10,12 +10,12 @@
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { RecursionField, Schema, observer, useFieldSchema } from '@formily/react';
import {
ActionContextProvider,
PopupContextProvider,
RecordProvider,
VariablePopupRecordProvider,
getLabelFormatValue,
useCollection,
useCollectionParentRecordData,
usePopupUtils,
useProps,
withDynamicSchemaProps,
} from '@nocobase/client';
@ -23,10 +23,11 @@ import { parseExpression } from 'cron-parser';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import get from 'lodash/get';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import { Calendar as BigCalendar, View, dayjsLocalizer } from 'react-big-calendar';
import * as dates from 'react-big-calendar/lib/utils/dates';
import { i18nt, useTranslation } from '../../locale';
import { CalendarRecordViewer, findEventSchema } from './CalendarRecordViewer';
import Header from './components/Header';
import { CalendarToolbarContext } from './context';
import GlobalStyle from './global.style';
@ -160,54 +161,24 @@ const useEvents = (dataSource: any, fieldNames: any, date: Date, view: (typeof W
}, [dataSource, fieldNames.start, fieldNames.end, fieldNames.id, fieldNames.title, date, view, t]);
};
const CalendarRecordViewer = (props) => {
const { visible, setVisible, record } = props;
const { t } = useTranslation();
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const eventSchema: Schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current['x-component'].endsWith('.Event')) {
return current;
}
return buf;
}, null),
[],
);
const close = useCallback(() => {
setVisible(false);
}, []);
return (
eventSchema && (
<DeleteEventContext.Provider value={{ close }}>
<ActionContextProvider value={{ visible, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={eventSchema} name={eventSchema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
</DeleteEventContext.Provider>
)
);
};
export const Calendar: any = withDynamicSchemaProps(
observer(
(props: any) => {
const [visible, setVisible] = useState(false);
const { openPopup } = usePopupUtils({
setVisible,
});
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { dataSource, fieldNames, showLunar } = useProps(props);
const height = useCalenderHeight();
const [date, setDate] = useState<Date>(new Date());
const [view, setView] = useState<View>('month');
const events = useEvents(dataSource, fieldNames, date, view);
const [visible, setVisible] = useState(false);
const [record, setRecord] = useState<any>({});
const { wrapSSR, hashId, componentCls: containerClassName } = useStyle();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const components = useMemo(() => {
return {
@ -247,50 +218,57 @@ export const Calendar: any = withDynamicSchemaProps(
};
return wrapSSR(
<div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}>
<GlobalStyle />
<CalendarRecordViewer visible={visible} setVisible={setVisible} record={record} />
<BigCalendar
popup
selectable
events={events}
view={view}
views={Weeks}
date={date}
step={60}
showMultiDayTimes
messages={messages}
onNavigate={setDate}
onView={setView}
onSelectSlot={(slotInfo) => {
console.log('onSelectSlot', slotInfo);
}}
onDoubleClickEvent={() => {
console.log('onDoubleClickEvent');
}}
onSelectEvent={(event) => {
const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
if (!record) {
return;
}
record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) };
setRecord(record);
setVisible(true);
}}
formats={{
monthHeaderFormat: 'YYYY-M',
agendaDateFormat: 'M-DD',
dayHeaderFormat: 'YYYY-M-DD',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'YYYY-M', culture);
<PopupContextProvider visible={visible} setVisible={setVisible}>
<GlobalStyle />
<RecordProvider record={record} parent={parentRecordData}>
<CalendarRecordViewer />
</RecordProvider>
<BigCalendar
popup
selectable
events={events}
view={view}
views={Weeks}
date={date}
step={60}
showMultiDayTimes
messages={messages}
onNavigate={setDate}
onView={setView}
onSelectSlot={(slotInfo) => {
console.log('onSelectSlot', slotInfo);
}}
onDoubleClickEvent={() => {
console.log('onDoubleClickEvent');
}}
onSelectEvent={(event) => {
const record = dataSource?.find((item) => item[fieldNames.id] === event.id);
if (!record) {
return;
}
return `${local.format(start, 'YYYY-M', culture)} - ${local.format(end, 'YYYY-M', culture)}`;
},
}}
components={components}
localizer={localizer}
/>
record.__event = { ...event, start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)) };
setRecord(record);
openPopup({
recordData: record,
customActionSchema: findEventSchema(fieldSchema),
});
}}
formats={{
monthHeaderFormat: 'YYYY-M',
agendaDateFormat: 'M-DD',
dayHeaderFormat: 'YYYY-M-DD',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'YYYY-M', culture);
}
return `${local.format(start, 'YYYY-M', culture)} - ${local.format(end, 'YYYY-M', culture)}`;
},
}}
components={components}
localizer={localizer}
/>
</PopupContextProvider>
</div>,
);
},

View File

@ -0,0 +1,35 @@
/**
* 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 { RecursionField, Schema, useFieldSchema } from '@formily/react';
import React, { FC, useMemo } from 'react';
export const CalendarRecordViewer: FC = (props) => {
const fieldSchema = useFieldSchema();
const eventSchema: Schema = useMemo(() => findEventSchema(fieldSchema), [fieldSchema]);
if (!eventSchema) {
return null;
}
return <RecursionField schema={eventSchema} name={eventSchema.name} />;
};
export function findEventSchema(schema: Schema) {
if (schema['x-component'].endsWith('.Event')) {
return schema;
}
return schema.reduceProperties((buf, current) => {
if (current['x-component'].endsWith('.Event')) {
return current;
}
return buf;
}, null);
}

View File

@ -8,11 +8,35 @@
*/
import { observer } from '@formily/react';
import React from 'react';
import {
PopupContextProvider,
useActionContext,
useCollection,
useCollectionRecordData,
VariablePopupRecordProvider,
} from '@nocobase/client';
import React, { useCallback } from 'react';
import { DeleteEventContext } from './Calendar';
export const Event = observer(
(props) => {
return <>{props.children}</>;
const { visible, setVisible } = useActionContext();
const recordData = useCollectionRecordData();
const collection = useCollection();
const close = useCallback(() => {
setVisible(false);
}, [setVisible]);
return (
<PopupContextProvider visible={visible} setVisible={setVisible}>
<DeleteEventContext.Provider value={{ close }}>
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
{props.children}
</VariablePopupRecordProvider>
</DeleteEventContext.Provider>
</PopupContextProvider>
);
},
{ displayName: 'Event' },
);

View File

@ -7,10 +7,10 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Schema } from '@formily/react';
import {
Collection,
CollectionFieldInterfaceManager,
CollectionFieldOptions,
CollectionManager,
SchemaInitializerItemType,
i18n,
@ -19,18 +19,16 @@ import {
useDataSourceManager,
useVariables,
} from '@nocobase/client';
import { flatten, parse, unflatten } from '@nocobase/utils/client';
import { useMemoizedFn } from 'ahooks';
import deepmerge from 'deepmerge';
import { default as _, default as lodash } from 'lodash';
import { useCallback, useContext, useMemo } from 'react';
import { ChartDataContext } from '../block/ChartDataProvider';
import { Schema } from '@formily/react';
import { useChartsTranslation } from '../locale';
import { ChartFilterContext } from '../filter/FilterProvider';
import { useMemoizedFn } from 'ahooks';
import { flatten, parse, unflatten } from '@nocobase/utils/client';
import lodash from 'lodash';
import { getFormulaComponent, getValuesByPath } from '../utils';
import deepmerge from 'deepmerge';
import { findSchema, getFilterFieldPrefix, parseFilterFieldName } from '../filter/utils';
import _ from 'lodash';
import { useChartsTranslation } from '../locale';
import { getFormulaComponent, getValuesByPath } from '../utils';
export const useCustomFieldInterface = () => {
const { getInterface } = useCollectionManager_deprecated();
@ -420,7 +418,7 @@ export const useChartFilter = () => {
if (['$user', '$date', '$nDate', '$nRole', '$nFilter'].some((n) => value.includes(n))) {
return value;
}
const result = variables?.parseVariable(value);
const result = variables?.parseVariable(value).then(({ value }) => value);
return result;
},
});

View File

@ -8,11 +8,28 @@
*/
import { observer } from '@formily/react';
import {
PopupContextProvider,
useActionContext,
useCollection,
useCollectionRecordData,
VariablePopupRecordProvider,
} from '@nocobase/client';
import React from 'react';
export const Event = observer(
(props) => {
return <>{props.children}</>;
const { visible, setVisible } = useActionContext();
const recordData = useCollectionRecordData();
const collection = useCollection();
return (
<PopupContextProvider visible={visible} setVisible={setVisible}>
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
{props.children}
</VariablePopupRecordProvider>
</PopupContextProvider>
);
},
{ displayName: 'Event' },
);

View File

@ -0,0 +1,23 @@
/**
* 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 { RecursionField, useFieldSchema } from '@formily/react';
import { Schema } from '@nocobase/utils';
import React, { FC } from 'react';
export const GanttRecordViewer: FC = (props) => {
const fieldSchema = useFieldSchema();
const eventSchema: Schema = fieldSchema.properties.detail;
if (!eventSchema) {
return null;
}
return <RecursionField schema={eventSchema} name={eventSchema.name} />;
};

View File

@ -8,17 +8,16 @@
*/
import { css, cx } from '@emotion/css';
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
import { RecursionField, useFieldSchema } from '@formily/react';
import {
ActionContextProvider,
PopupContextProvider,
RecordProvider,
VariablePopupRecordProvider,
useAPIClient,
useBlockRequestContext,
useCollection,
useCollectionParentRecordData,
useCurrentAppInfo,
useDesignable,
usePopupUtils,
useProps,
useTableBlockContext,
useToken,
@ -26,7 +25,7 @@ import {
} from '@nocobase/client';
import { Spin, message } from 'antd';
import { debounce } from 'lodash';
import React, { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { SyntheticEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGanttBlockContext } from '../../GanttBlockProvider';
import { convertToBarTasks } from '../../helpers/bar-helper';
@ -41,6 +40,7 @@ import { GridProps } from '../grid/grid';
import { HorizontalScroll } from '../other/horizontal-scroll';
import { StandardTooltipContent, Tooltip } from '../other/tooltip';
import { VerticalScroll } from '../other/vertical-scroll';
import { GanttRecordViewer } from './GanttRecordViewer';
import useStyles from './style';
import { TaskGantt } from './task-gantt';
import { TaskGanttContentProps } from './task-gantt-content';
@ -49,34 +49,7 @@ const getColumnWidth = (dataSetLength: any, clientWidth: any) => {
const columnWidth = clientWidth / dataSetLength > 50 ? Math.floor(clientWidth / dataSetLength) + 20 : 50;
return columnWidth;
};
export const DeleteEventContext = React.createContext({
close: () => {},
});
const GanttRecordViewer = (props) => {
const { visible, setVisible, record } = props;
const { t } = useTranslation();
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const eventSchema: Schema = fieldSchema.properties.detail;
const close = useCallback(() => {
setVisible(false);
}, []);
return (
eventSchema && (
<DeleteEventContext.Provider value={{ close }}>
<ActionContextProvider value={{ visible, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={eventSchema} name={eventSchema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
</DeleteEventContext.Provider>
)
);
};
const debounceHandleTaskChange = debounce(async (task: Task, resource, fieldNames, service, t) => {
await resource.update({
filterByTk: task.id,
@ -160,7 +133,11 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
return { viewMode, dates: seedDates(startDate, endDate, viewMode) };
});
const [visible, setVisible] = useState(false);
const { openPopup } = usePopupUtils({
setVisible,
});
const [record, setRecord] = useState<any>({});
const parentRecordData = useCollectionParentRecordData();
const [currentViewDate, setCurrentViewDate] = useState<Date | undefined>(undefined);
const [taskListWidth, setTaskListWidth] = useState(0);
const [svgContainerWidth, setSvgContainerWidth] = useState(0);
@ -473,7 +450,10 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
return;
}
setRecord(recordData);
setVisible(true);
openPopup({
recordData,
customActionSchema: fieldSchema.properties.detail,
});
};
const gridProps: GridProps = {
columnWidth,
@ -536,55 +516,59 @@ export const Gantt: any = withDynamicSchemaProps((props: any) => {
`)}
ref={ganttRef}
>
<GanttRecordViewer visible={visible} setVisible={setVisible} record={record} />
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
<RecursionField name={'table'} schema={fieldSchema.properties.table} />
<div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}>
<TaskGantt
gridProps={gridProps}
calendarProps={calendarProps}
barProps={barProps}
ganttHeight={ganttHeight}
scrollY={scrollY}
scrollX={scrollX}
ref={verticalGanttContainerRef}
/>
{ganttEvent.changedTask && (
<Tooltip
arrowIndent={arrowIndent}
rowHeight={rowHeight}
svgContainerHeight={svgContainerHeight}
svgContainerWidth={svgContainerWidth}
fontFamily={fontFamily}
fontSize={fontSize}
scrollX={scrollX}
<PopupContextProvider visible={visible} setVisible={setVisible}>
<RecordProvider record={record} parent={parentRecordData}>
<GanttRecordViewer />
</RecordProvider>
<RecursionField name={'anctionBar'} schema={fieldSchema.properties.toolBar} />
<RecursionField name={'table'} schema={fieldSchema.properties.table} />
<div className={styles.wrapper} onKeyDown={handleKeyDown} tabIndex={0} ref={wrapperRef}>
<TaskGantt
gridProps={gridProps}
calendarProps={calendarProps}
barProps={barProps}
ganttHeight={ganttHeight}
scrollY={scrollY}
task={ganttEvent.changedTask}
scrollX={scrollX}
ref={verticalGanttContainerRef}
/>
{ganttEvent.changedTask && (
<Tooltip
arrowIndent={arrowIndent}
rowHeight={rowHeight}
svgContainerHeight={svgContainerHeight}
svgContainerWidth={svgContainerWidth}
fontFamily={fontFamily}
fontSize={fontSize}
scrollX={scrollX}
scrollY={scrollY}
task={ganttEvent.changedTask}
headerHeight={headerHeight}
taskListWidth={taskListWidth}
TooltipContent={TooltipContent}
rtl={rtl}
svgWidth={svgWidth}
/>
)}
<VerticalScroll
ganttFullHeight={ganttFullHeight}
ganttHeight={ganttHeight}
headerHeight={headerHeight}
taskListWidth={taskListWidth}
TooltipContent={TooltipContent}
scroll={scrollY}
onScroll={handleScrollY}
rtl={rtl}
svgWidth={svgWidth}
/>
)}
<VerticalScroll
ganttFullHeight={ganttFullHeight}
ganttHeight={ganttHeight}
headerHeight={headerHeight}
scroll={scrollY}
onScroll={handleScrollY}
rtl={rtl}
/>
<Spin spinning={loading} style={{ visibility: 'hidden' }}>
<HorizontalScroll
svgWidth={svgWidth}
taskListWidth={taskListWidth}
scroll={scrollX}
rtl={rtl}
onScroll={handleScrollX}
/>
</Spin>
</div>
<Spin spinning={loading} style={{ visibility: 'hidden' }}>
<HorizontalScroll
svgWidth={svgWidth}
taskListWidth={taskListWidth}
scroll={scrollX}
rtl={rtl}
onScroll={handleScrollX}
/>
</Spin>
</div>
</PopupContextProvider>
</div>
);
});

View File

@ -8,15 +8,29 @@
*/
import {
PopupContextProvider,
useCollection_deprecated,
useCollectionManager_deprecated,
usePopupUtils,
useProps,
withDynamicSchemaProps,
} from '@nocobase/client';
import React, { useMemo } from 'react';
import { MapBlockComponent } from '../components';
import { MapBlockDrawer } from '../components/MapBlockDrawer';
export const MapBlock = withDynamicSchemaProps((props) => {
const { context } = usePopupUtils();
// only render the popup
if (context.currentLevel) {
return (
<PopupContextProvider>
<MapBlockDrawer />
</PopupContextProvider>
);
}
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { fieldNames } = useProps(props);

View File

@ -8,11 +8,8 @@
*/
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
import { RecursionField, useFieldSchema } from '@formily/react';
import {
ActionContextProvider,
RecordProvider,
VariablePopupRecordProvider,
css,
getLabelFormatValue,
useCollection,
@ -21,14 +18,16 @@ import {
useCollection_deprecated,
useCompile,
useFilterAPI,
usePopupUtils,
useProps,
} from '@nocobase/client';
import { useMemoizedFn } from 'ahooks';
import { Button, Space } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { defaultImage, selectedImage } from '../../constants';
import { useMapTranslation } from '../../locale';
import { getSource } from '../../utils';
import { MapBlockDrawer } from '../MapBlockDrawer';
import { AMapComponent, AMapForwardedRefProps } from './Map';
export const AMapBlock = (props) => {
@ -50,6 +49,9 @@ export const AMapBlock = (props) => {
const selectingModeRef = useRef(selectingMode);
selectingModeRef.current = selectingMode;
const { fields } = useCollection();
const parentRecordData = useCollectionParentRecordData();
const { openPopup } = usePopupUtils();
const labelUiSchema = fields.find((v) => v.name === fieldNames?.marker)?.uiSchema;
const setOverlayOptions = (overlay: AMap.Polygon | AMap.Marker, state?: boolean) => {
const extData = overlay.getExtData();
@ -199,6 +201,9 @@ export const AMapBlock = (props) => {
if (data) {
setRecord(data);
openPopup({
recordData: data,
});
}
};
o.on('click', onClick);
@ -244,7 +249,17 @@ export const AMapBlock = (props) => {
});
events.forEach((e) => e());
};
}, [dataSource, isMapInitialization, fieldNames, name, primaryKey, collectionField.type, isConnected, lineSort]);
}, [
dataSource,
isMapInitialization,
fieldNames,
name,
primaryKey,
collectionField.type,
isConnected,
lineSort,
openPopup,
]);
useEffect(() => {
setTimeout(() => {
@ -307,7 +322,9 @@ export const AMapBlock = (props) => {
</Space>
) : null}
</div>
<MapBlockDrawer record={record} setVisible={setRecord} />
<RecordProvider record={record} parent={parentRecordData}>
<MapBlockDrawer />
</RecordProvider>
<AMapComponent
{...collectionField?.uiSchema?.['x-component-props']}
ref={mapRefCallback}
@ -324,35 +341,6 @@ export const AMapBlock = (props) => {
);
};
const MapBlockDrawer = (props) => {
const { setVisible, record } = props;
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current.name === 'drawer') {
return current;
}
return buf;
}, null),
[fieldSchema],
);
return (
schema && (
<ActionContextProvider value={{ visible: !!record, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={schema} name={schema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
)
);
};
function clearSelected(marker: AMap.Marker | AMap.Polygon | AMap.Polyline | AMap.Circle) {
if ((marker as AMap.Marker).dom) {
(marker as AMap.Marker).dom.style.filter = 'none';

View File

@ -8,11 +8,8 @@
*/
import { CheckOutlined, EnvironmentOutlined, ExpandOutlined } from '@ant-design/icons';
import { RecursionField, Schema, useFieldSchema } from '@formily/react';
import {
ActionContextProvider,
RecordProvider,
VariablePopupRecordProvider,
css,
getLabelFormatValue,
useCollection,
@ -21,14 +18,16 @@ import {
useCollection_deprecated,
useCompile,
useFilterAPI,
usePopupUtils,
useProps,
} from '@nocobase/client';
import { useMemoizedFn } from 'ahooks';
import { Button, Space } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { defaultImage, selectedImage } from '../../constants';
import { useMapTranslation } from '../../locale';
import { getSource } from '../../utils';
import { MapBlockDrawer } from '../MapBlockDrawer';
import { GoogleMapForwardedRefProps, GoogleMapsComponent, OverlayOptions } from './Map';
import { getIcon } from './utils';
@ -69,8 +68,10 @@ export const GoogleMapsBlock = (props) => {
const overlaysRef = useRef<google.maps.MVCObject[]>([]);
selectingModeRef.current = selectingMode;
const { fields } = useCollection();
const parentRecordData = useCollectionParentRecordData();
const labelUiSchema = fields.find((v) => v.name === fieldNames?.marker)?.uiSchema;
const { getCollectionJoinField } = useCollectionManager_deprecated();
const { openPopup } = usePopupUtils();
const setOverlayOptions = (overlay: google.maps.MVCObject, state?: boolean) => {
const selected = typeof state !== 'undefined' ? !state : overlay.get(OVERLAY_SELECtED);
@ -234,6 +235,9 @@ export const GoogleMapsBlock = (props) => {
if (data) {
setRecord(data);
openPopup({
recordData: data,
});
}
};
o.addListener('click', onClick);
@ -291,7 +295,7 @@ export const GoogleMapsBlock = (props) => {
});
events.forEach((e) => e());
};
}, [dataSource, isMapInitialization, markerName, collectionField.type, isConnected]);
}, [dataSource, isMapInitialization, markerName, collectionField.type, isConnected, openPopup]);
useEffect(() => {
setTimeout(() => {
@ -354,7 +358,9 @@ export const GoogleMapsBlock = (props) => {
) : null}
</Space>
</div>
<MapBlockDrawer record={record} setVisible={setRecord} />
<RecordProvider record={record} parent={parentRecordData}>
<MapBlockDrawer />
</RecordProvider>
</>
)}
<GoogleMapsComponent
@ -373,35 +379,6 @@ export const GoogleMapsBlock = (props) => {
);
};
const MapBlockDrawer = (props) => {
const { setVisible, record } = props;
const collection = useCollection();
const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema();
const schema: Schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current.name === 'drawer') {
return current;
}
return buf;
}, null),
[fieldSchema],
);
return (
schema && (
<ActionContextProvider value={{ visible: !!record, setVisible }}>
<RecordProvider record={record} parent={parentRecordData}>
<VariablePopupRecordProvider recordData={record} collection={collection}>
<RecursionField schema={schema} name={schema.name} />
</VariablePopupRecordProvider>
</RecordProvider>
</ActionContextProvider>
)
);
};
function clearSelected(target: google.maps.Polygon) {
if (target instanceof google.maps.Marker) {
return target.setIcon(getIcon(defaultImage));

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { PopupContextProvider } from '@nocobase/client';
import React, { useMemo } from 'react';
import { useMapTranslation } from '../locale';
import { AMapBlock } from './AMap';
@ -29,5 +30,9 @@ export const MapBlockComponent: React.FC<any> = (props) => {
return <div>{t(`The ${mapType} cannot found`)}</div>;
}
return <Component {...props} />;
return (
<PopupContextProvider>
<Component {...props} />
</PopupContextProvider>
);
};

View File

@ -0,0 +1,38 @@
/**
* 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 { RecursionField, useFieldSchema } from '@formily/react';
import { useCollection, useCollectionRecordData, VariablePopupRecordProvider } from '@nocobase/client';
import React, { FC, useMemo } from 'react';
export const MapBlockDrawer: FC = (props) => {
const recordData = useCollectionRecordData();
const collection = useCollection();
const fieldSchema = useFieldSchema();
const schema = useMemo(
() =>
fieldSchema.reduceProperties((buf, current) => {
if (current.name === 'drawer') {
return current;
}
return buf;
}, null),
[fieldSchema],
);
if (!schema) {
return null;
}
return (
<VariablePopupRecordProvider recordData={recordData} collection={collection}>
<RecursionField schema={schema} name={schema.name} />
</VariablePopupRecordProvider>
);
};

View File

@ -0,0 +1,36 @@
/**
* 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 { devices, expect, test } from '@nocobase/test/e2e';
test.use({
...devices['Galaxy S9+'],
});
test.describe('redirect to other page from mobile', () => {
test('redirect to signin page', async ({ page }) => {
const baseURL = process.env.APP_BASE_URL || `http://localhost:${process.env.APP_PORT || 20000}`;
await page.goto('/m/signin');
await page.waitForURL(`${baseURL}/signin`);
expect(page.url()).toBe(`${baseURL}/signin`);
// do not redirect to mobile page
await page.waitForTimeout(5000);
expect(page.url()).toBe(`${baseURL}/signin`);
});
test('redirect to admin page', async ({ page }) => {
const baseURL = process.env.APP_BASE_URL || `http://localhost:${process.env.APP_PORT || 20000}`;
await page.goto('/m/admin/settings/@nocobase/plugin-api-keys');
await page.waitForURL(`${baseURL}/admin/settings/@nocobase/plugin-api-keys`);
expect(page.url()).toBe(`${baseURL}/admin/settings/@nocobase/plugin-api-keys`);
});
});

View File

@ -179,22 +179,26 @@ export class PluginMobileClient extends Plugin {
Component: 'MobileHomePage',
});
// 跳转到主应用的登录页
// redirect to main app signin page
// e.g. /m/signin => /signin
this.mobileRouter.add('signin', {
path: '/signin',
Component: () => {
window.location.href = window.location.href
.replace(this.mobilePath, '')
.replace(this.mobilePath + '/', '/')
.replace('redirect=', `redirect=${this.mobilePath}`);
return null;
},
});
// 跳转到主应用的页面
// redirect to main app admin page
// e.g. /m/admin/xxx => /admin/xxx
this.mobileRouter.add('admin', {
path: `/admin/*`,
Component: () => {
window.location.replace(window.location.href.replace(this.mobilePath, ''));
if (window.location.pathname.startsWith(this.mobilePath + '/')) {
window.location.replace(window.location.href.replace(this.mobilePath + '/', '/'));
}
return null;
},
});

View File

@ -13,9 +13,9 @@ import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { message } from 'antd';
import { useActionContext, useRecord, useResourceActionContext, useResourceContext } from '@nocobase/client';
import { useActionContext, useResourceActionContext, useResourceContext } from '@nocobase/client';
import { ExecutionStatusOptions } from '../constants';
import { ExecutionStatusOptions, EXECUTION_STATUS } from '../constants';
import { NAMESPACE } from '../locale';
import { getWorkflowDetailPath } from '../utils';
@ -116,6 +116,19 @@ export const executionSchema = {
icon: 'ReloadOutlined',
},
},
delete: {
type: 'void',
title: '{{t("Delete")}}',
'x-component': 'Action',
'x-component-props': {
icon: 'DeleteOutlined',
useAction: '{{ cm.useBulkDestroyAction }}',
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
},
},
clear: {
type: 'void',
title: '{{t("Clear")}}',
@ -148,6 +161,9 @@ export const executionSchema = {
'x-component': 'Table.Void',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useDataSource: '{{ cm.useDataSourceFromRAC }}',
},
properties: {
@ -220,6 +236,28 @@ export const executionSchema = {
type: 'void',
'x-component': 'ExecutionLink',
},
delete: {
type: 'void',
title: '{{ t("Delete") }}',
'x-component': 'Action.Link',
'x-component-props': {
confirm: {
title: "{{t('Delete record')}}",
content: "{{t('Are you sure you want to delete it?')}}",
},
useAction: '{{ cm.useDestroyActionAndRefreshCM }}',
},
'x-reactions': [
{
dependencies: ['..status'],
fulfill: {
state: {
visible: `{{ $deps[0] !== ${EXECUTION_STATUS.STARTED} }}`,
},
},
},
],
},
},
},
},

View File

@ -13231,9 +13231,9 @@ eslint-plugin-import@^2.27.5:
tsconfig-paths "^3.15.0"
eslint-plugin-jest-dom@^5.0.1:
version "5.1.0"
resolved "https://registry.npmmirror.com/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.1.0.tgz#b285cd1cc71c084c8ee897f0f85599758d7cb933"
integrity sha512-JIXZp+E/h/aGlP/rQc4tuOejiHlZXg65qw8JAJMIJA5VsdjOkss/SYcRSqBrQuEOytEM8JvngUjcz31d1RrCrA==
version "5.4.0"
resolved "https://registry.npmmirror.com/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.4.0.tgz#03a5ea600f8af63f4fcd5de49ae83dc0e6aca325"
integrity sha512-yBqvFsnpS5Sybjoq61cJiUsenRkC9K32hYQBFS9doBR7nbQZZ5FyO+X7MlmfM1C48Ejx/qTuOCgukDUNyzKZ7A==
dependencies:
"@babel/runtime" "^7.16.3"
requireindex "^1.2.0"
@ -17016,7 +17016,7 @@ json5@^1.0.1, json5@^1.0.2:
dependencies:
minimist "^1.2.0"
json5@^2.1.2, json5@^2.2.3:
json5@^2.1.2, json5@^2.2.2, json5@^2.2.3:
version "2.2.3"
resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
@ -24512,7 +24512,7 @@ string-convert@^0.2.0:
resolved "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -24530,6 +24530,15 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@ -24625,7 +24634,7 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -24653,6 +24662,13 @@ strip-ansi@^5.1.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@ -26965,7 +26981,7 @@ wordwrap@^1.0.0:
resolved "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -26991,6 +27007,15 @@ wrap-ansi@^6.0.1:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.0.1, wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"