Merge branch 'next' into mobile-subpage

This commit is contained in:
Zeke Zhang 2024-07-29 11:28:36 +08:00
commit 9d6d18ad8a
27 changed files with 294 additions and 77 deletions

View File

@ -7,6 +7,24 @@ 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.2.25-alpha](https://github.com/nocobase/nocobase/compare/v1.2.24-alpha...v1.2.25-alpha) - 2024-07-27
### Merged
- fix(plugin-workflow): hide condition configuration in destroy collection event [`#4952`](https://github.com/nocobase/nocobase/pull/4952)
- fix(plugin-workflow): fix schedule event on date field [`#4953`](https://github.com/nocobase/nocobase/pull/4953)
- refactor(client): allow select to show null option as tag in read pretty mode if configured [`#4950`](https://github.com/nocobase/nocobase/pull/4950)
- fix: clear default value immediately after field deletion [`#4915`](https://github.com/nocobase/nocobase/pull/4915)
- fix: autoGenId default value should be false when adding collection [`#4942`](https://github.com/nocobase/nocobase/pull/4942)
- refactor: migrate DataBlockCollector to DataBlockProvider [`#4938`](https://github.com/nocobase/nocobase/pull/4938)
- fix(action-import): import with createdBy & updatedBy field [`#4939`](https://github.com/nocobase/nocobase/pull/4939)
### Commits
- chore(versions): 😊 publish v1.2.25-alpha [`306035c`](https://github.com/nocobase/nocobase/commit/306035c607d2d8d22b540e5653cd9095abf906f0)
- chore: update changelog [`b2f82a2`](https://github.com/nocobase/nocobase/commit/b2f82a26dfc113db7a8bad9e2c21ddcad4d71a0b)
- Update LICENSE.txt [`027d54d`](https://github.com/nocobase/nocobase/commit/027d54dc8785a01c0af0d7e7a33aedb0af166e4e)
## [v1.2.24-alpha](https://github.com/nocobase/nocobase/compare/v1.2.23-alpha...v1.2.24-alpha) - 2024-07-23
### Merged

View File

@ -0,0 +1,12 @@
import { useTranslation } from 'react-i18next';
// @ts-ignore
import pkg from './../../package.json';
export function usePluginTranslation() {
return useTranslation([pkg.name, 'client'], { nsMode: 'fallback' });
}
export function generatePluginTranslationTemplate(key: string) {
return `{{t('${key}', { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`;
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -47,10 +47,6 @@ export interface CustomToken extends AliasToken {
marginBlock: number;
/** 区块的圆角 */
borderRadiusBlock: number;
/** 区块的 boxShadow */
boxShadowBlock: string;
/** 区块的下边框 */
borderBottomBlock: string;
}
export interface ThemeConfig extends _ThemeConfig {

View File

@ -15,10 +15,6 @@ const useStyles = genStyleHook('nb-card-item', (token) => {
return {
[componentCls]: {
marginBottom: token.marginBlock,
'> .ant-card': {
boxShadow: token.boxShadowBlock,
borderBottom: token.borderBottomBlock,
},
},
};
});

View File

@ -31,16 +31,16 @@ export const ReadPretty = observer(
(props: SelectReadPrettyProps) => {
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
const field = useField<any>();
const collectionField = useCollectionField();
const dataSource = field.dataSource || props.options || collectionField?.uiSchema.enum || [];
const currentOptions = getCurrentOptions(field.value, dataSource, fieldNames);
if (!isValid(props.value)) {
if (!isValid(props.value) && !currentOptions.length) {
return <div />;
}
if (isArrayField(field) && field?.value?.length === 0) {
return <div />;
}
const collectionField = useCollectionField();
const dataSource = field.dataSource || props.options || collectionField?.uiSchema.enum || [];
const currentOptions = getCurrentOptions(field.value, dataSource, fieldNames);
return (
<div>

View File

@ -0,0 +1,49 @@
import React from 'react';
import { mockApp } from '@nocobase/client/demo-utils';
import { SchemaComponent, Plugin, ISchema } from '@nocobase/client';
const options = [
{
label: '福建',
value: 'FuJian',
children: [
{ label: '{{t("福州")}}', value: 'FZ' },
{ label: '莆田', value: 'PT' },
],
},
{ label: '江苏', value: 'XZ' },
{ label: '浙江', value: 'ZX' },
{ lable: '未选择', value: null },
];
const schema: ISchema = {
type: 'void',
name: 'root',
'x-decorator': 'FormV2',
'x-component': 'ShowFormData',
properties: {
test: {
type: 'string',
title: 'Test',
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Select',
default: null,
},
},
};
const Demo = () => {
return <SchemaComponent schema={schema} />;
};
class DemoPlugin extends Plugin {
async load() {
this.app.router.add('root', { path: '/', Component: Demo });
}
}
const app = mockApp({
plugins: [DemoPlugin],
});
export default app.getRootComponent();

View File

@ -43,11 +43,9 @@ function flatData(data: any[], fieldNames: FieldNames): any[] {
return newArr;
}
export function getCurrentOptions(values: string | string[], dataSource: any[], fieldNames: FieldNames): Option[] {
export function getCurrentOptions(values: any | any[], dataSource: any[], fieldNames: FieldNames): Option[] {
const result = flatData(dataSource, fieldNames);
const arrValues = castArray(values)
.filter((item) => item != null)
.map((val) => (isPlainObject(val) ? val[fieldNames.value] : val)) as string[];
const arrValues = castArray(values).map((val) => (isPlainObject(val) ? val[fieldNames.value] : val)) as any[];
function findOptions(options: any[]): Option[] {
if (!options) return [];

View File

@ -123,10 +123,10 @@ function getTypedConstantOption(type: string, types: true | string[], fieldNames
Object.keys(item).reduce(
(result, key) =>
fieldNames[key] in item
? result
: Object.assign(result, {
? Object.assign(result, {
[fieldNames[key]]: item[key],
}),
})
: result,
item,
),
);
@ -397,9 +397,9 @@ export function Input(props: VariableInputProps) {
<div style={{ flex: 1 }}>
{children && isFieldValue ? (
children
) : (
) : ConstantComponent ? (
<ConstantComponent role="button" aria-label="variable-constant" value={value} onChange={onChange} />
)}
) : null}
</div>
)}
<Cascader

View File

@ -76,7 +76,6 @@ export class SyncRunner {
try {
const beforeColumns = await this.queryInterface.describeTable(this.tableName, options);
await this.checkAutoIncrementField(beforeColumns, options);
await this.handlePrimaryKeyBeforeSync(beforeColumns, options);
await this.handleUniqueFieldBeforeSync(beforeColumns, options);
} catch (e) {
@ -95,20 +94,6 @@ export class SyncRunner {
return syncResult;
}
async checkAutoIncrementField(beforeColumns, options) {
// if there is auto increment field, throw error
if (!this.database.isMySQLCompatibleDialect()) {
return;
}
const autoIncrFields = Object.keys(this.rawAttributes).filter((key) => {
return this.rawAttributes[key].autoIncrement;
});
if (autoIncrFields.length > 1) {
throw new Error(`Auto increment field can't be more than one: ${autoIncrFields.join(', ')}`);
}
}
async handleUniqueFieldBeforeSync(beforeColumns, options) {
if (!this.database.inDialect('sqlite')) {
return;

View File

@ -52,7 +52,7 @@
"dependencies": {
"@faker-js/faker": "8.1.0",
"@nocobase/server": "1.3.0-alpha",
"@playwright/test": "^1.44.0",
"@playwright/test": "^1.45.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",

View File

@ -0,0 +1,44 @@
/**
* 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.
*/
export function beforeCreateCheckFieldInMySQL(db) {
return async (model, { transaction }) => {
if (!db.isMySQLCompatibleDialect()) {
return;
}
const fieldOptions = model.get();
if (fieldOptions.autoIncrement) {
const collection = db.getCollection(fieldOptions.collectionName);
if (!collection) {
return;
}
const rawAttributes = collection.model.rawAttributes;
const fields = Object.keys(rawAttributes);
for (const key of fields) {
if (key === fieldOptions.name) {
continue;
}
const field = rawAttributes[key];
if (field.autoIncrement) {
throw new Error(
`Can not add field ${
fieldOptions.name
}, autoIncrement field ${key} is already in a table ${collection.getTableNameWithSchemaAsString()}`,
);
}
}
}
};
}

View File

@ -30,6 +30,7 @@ import viewResourcer from './resourcers/views';
import { FieldNameExistsError } from './errors/field-name-exists-error';
import { beforeDestoryField } from './hooks/beforeDestoryField';
import { FieldIsDependedOnByOtherError } from './errors/field-is-depended-on-by-other';
import { beforeCreateCheckFieldInMySQL } from './hooks/beforeCreateCheckFieldInMySQL';
export class PluginDataSourceMainServer extends Plugin {
public schema: string;
@ -115,6 +116,8 @@ export class PluginDataSourceMainServer extends Plugin {
});
// 要在 beforeInitOptions 之前处理
this.app.db.on('fields.beforeCreate', beforeCreateCheckFieldInMySQL(this.app.db));
this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db));
this.app.db.on('fields.beforeCreate', async (model, options) => {

View File

@ -40,9 +40,11 @@ export const getAntChart = (Component: React.FC<any>) => (props: any) => {
observer.observe(el);
return () => observer.disconnect();
}, [service.loading, fixedHeight, size.type, size.ratio?.width, size.ratio?.height]);
const chartHeight = fixedHeight || height;
return (
<div ref={chartRef} style={height ? { height: `${fixedHeight || height}px` } : {}}>
<Component {...props} {...(height ? { height: fixedHeight || height } : {})} />
<div ref={chartRef} style={chartHeight ? { height: `${chartHeight}px` } : {}}>
<Component {...props} {...(chartHeight ? { height: chartHeight } : {})} />
</div>
);
};

View File

@ -63,7 +63,7 @@ export const Mobile = () => {
token: {
marginBlock: 18,
borderRadiusBlock: 0,
boxShadowBlock: 'none',
boxShadowTertiary: 'none',
},
}}
>

View File

@ -262,7 +262,7 @@ const category: TokenTree<keyof AliasToken | string> = [
nameEn: 'Shadow',
desc: '',
descEn: '',
mapToken: ['boxShadow', 'boxShadowSecondary', 'boxShadowBlock'],
mapToken: ['boxShadow', 'boxShadowSecondary'],
},
],
},

View File

@ -1759,21 +1759,5 @@
"descEn": "Used to set the radius of the block",
"type": "number",
"source": "map"
},
"boxShadowBlock": {
"name": "区块的阴影",
"nameEn": "Shadow of block",
"desc": "用于设置区块的阴影",
"descEn": "Used to set the shadow of the block",
"type": "string",
"source": "map"
},
"borderBottomBlock": {
"name": "区块的底边框",
"nameEn": "Bottom border of block",
"desc": "用于设置区块的底边框",
"descEn": "Used to set the bottom border of the block",
"type": "string",
"source": "map"
}
}

View File

@ -12,6 +12,8 @@ export class CreateWorkFlow {
readonly page: Page;
name: Locator;
triggerType: Locator;
synchronouslyRadio: Locator;
asynchronouslyRadio: Locator;
description: Locator;
autoDeleteHistory: Locator;
submitButton: Locator;
@ -20,6 +22,8 @@ export class CreateWorkFlow {
this.page = page;
this.name = page.getByLabel('block-item-CollectionField-workflows-Name').getByRole('textbox');
this.triggerType = page.getByTestId('select-single');
this.synchronouslyRadio = page.getByLabel('Synchronously', { exact: true });
this.asynchronouslyRadio = page.getByLabel('Asynchronously', { exact: true });
this.description = page.getByTestId('description-item').getByRole('textbox');
this.autoDeleteHistory = page.getByTestId('select-multiple');
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
@ -365,6 +369,30 @@ export class FormEventTriggerNode {
}
}
export class CustomActionEventTriggerNode {
readonly page: Page;
node: Locator;
nodeTitle: Locator;
nodeConfigure: Locator;
collectionDropDown: Locator;
relationalDataDropdown: Locator;
submitButton: Locator;
cancelButton: Locator;
addNodeButton: Locator;
constructor(page: Page, triggerName: string, collectionName: string) {
this.page = page;
this.node = page.getByLabel(`Trigger-${triggerName}`);
this.nodeTitle = page.getByLabel(`Trigger-${triggerName}`).getByRole('textbox');
this.nodeConfigure = page.getByLabel(`Trigger-${triggerName}`).getByRole('button', { name: 'Configure' });
this.collectionDropDown = page
.getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection')
.locator('.ant-select-selection-search-input');
this.relationalDataDropdown = page.getByTestId('select-field-Preload associations');
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
this.addNodeButton = page.getByLabel('add-button', { exact: true });
}
}
export class CalculationNode {
readonly page: Page;
node: Locator;
@ -746,4 +774,5 @@ export default module.exports = {
SQLNode,
ParallelBranchNode,
ApprovalBranchModeNode,
CustomActionEventTriggerNode,
};

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { request, Browser } from '@nocobase/test/e2e';
import { Browser, request } from '@nocobase/test/e2e';
const PORT = process.env.APP_PORT || 20000;
const APP_BASE_URL = process.env.APP_BASE_URL || `http://localhost:${PORT}`;
@ -808,6 +808,57 @@ export const apiCreateRecordTriggerActionEvent = async (
return (await result.json()).data;
};
// 添加业务表单条数据触发工作流表单事件,triggerWorkflows=key1!field,key2,key3!field.subfield
export const apiTriggerCustomActionEvent = async (collectionName: string, triggerWorkflows: string, data: any) => {
const api = await request.newContext({
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
});
const state = await api.storageState();
const headers = getHeaders(state);
/*
{
"title": "a11",
"enabled": true,
"description": null
}
*/
const result = await api.post(`/api/${collectionName}:trigger?triggerWorkflows=${triggerWorkflows}`, {
headers,
data,
});
if (!result.ok()) {
throw new Error(await result.text());
}
/*
{
"data": {
"id": 1,
"createdAt": "2023-12-12T02:43:53.793Z",
"updatedAt": "2023-12-12T05:41:33.300Z",
"key": "fzk3j2oj4el",
"title": "a11",
"enabled": true,
"description": null
},
"meta": {
"allowedActions": {
"view": [
1
],
"update": [
1
],
"destroy": [
1
]
}
}
}
*/
return (await result.json()).data;
};
// 审批中心发起审批
export const apiApplyApprovalEvent = async (data: any) => {
const api = await request.newContext({
@ -1021,4 +1072,5 @@ export default module.exports = {
apiApplyApprovalEvent,
userLogin,
apiCreateField,
apiTriggerCustomActionEvent,
};

View File

@ -160,10 +160,10 @@ export default class extends Trigger {
'x-component-props': {},
'x-reactions': [
{
dependencies: ['collection'],
dependencies: ['collection', 'mode'],
fulfill: {
state: {
visible: '{{!!$deps[0]}}',
visible: `{{!!$deps[0] && !($deps[1] & ${COLLECTION_TRIGGER_MODE.DELETED})}}`,
},
},
},

View File

@ -434,6 +434,29 @@ describe('workflow > triggers > collection', () => {
expect(executions.length).toBe(1);
expect(executions[0].context.data.title).toBe('t1');
});
it('condition will not effect destroy', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'collection',
config: {
mode: 4,
collection: 'posts',
condition: {
title: 't1',
},
},
});
const post1 = await PostRepo.create({ values: { title: 't1' } });
await PostRepo.destroy({ filterByTk: post1.id });
await sleep(500);
const executions = await workflow.getExecutions();
expect(executions.length).toBe(1);
expect(executions[0].context.data.title).toBe('t1');
});
});
describe('config.appends', () => {

View File

@ -377,5 +377,30 @@ describe('workflow > triggers > schedule > date field mode', () => {
const e2c = await workflow.countExecutions();
expect(e2c).toBe(2);
});
it('empty endsOn as no end', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'schedule',
config: {
mode: 1,
collection: 'posts',
startsOn: {
field: 'createdAt',
},
repeat: 1000,
endsOn: {},
},
});
await sleepToEvenSecond();
const post = await PostRepo.create({ values: { title: 't1' } });
await sleep(1700);
const e1c = await workflow.countExecutions();
expect(e1c).toBe(2);
});
});
});

View File

@ -68,7 +68,7 @@ async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: M
}
// NOTE: if no configured condition, or not match, do not trigger
if (isValidFilter(condition)) {
if (isValidFilter(condition) && !(mode & MODE_BITMAP.DESTROY)) {
// TODO: change to map filter format to calculation format
// const calculation = toCalculation(condition);
const count = await repository.count({

View File

@ -54,7 +54,7 @@ function getDataOptionTime(record, on, dir = 1) {
}
case 'object': {
const { field, offset = 0, unit = 1000 } = on;
if (!record.get(field)) {
if (!field || !record.get(field)) {
return null;
}
const second = new Date(record.get(field).getTime());

View File

@ -30,7 +30,6 @@
"packages/**/dist",
"packages/**/public",
"packages/core/build/bin",
"packages/core/cli/**/*",
"packages/**/lib",
"packages/**/es"
]

View File

@ -5053,12 +5053,12 @@
picocolors "^1.0.0"
tslib "^2.6.0"
"@playwright/test@^1.44.0":
version "1.44.0"
resolved "https://registry.npmmirror.com/@playwright/test/-/test-1.44.0.tgz#ac7a764b5ee6a80558bdc0fcbc525fcb81f83465"
integrity sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==
"@playwright/test@^1.45.3":
version "1.45.3"
resolved "https://registry.npmmirror.com/@playwright/test/-/test-1.45.3.tgz#22e9c38b3081d6674b28c6e22f784087776c72e5"
integrity sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==
dependencies:
playwright "1.44.0"
playwright "1.45.3"
"@pm2/agent@~2.0.0":
version "2.0.3"
@ -20886,17 +20886,17 @@ platform@^1.3.1:
resolved "https://registry.npmmirror.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
playwright-core@1.44.0:
version "1.44.0"
resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.44.0.tgz#316c4f0bca0551ffb88b6eb1c97bc0d2d861b0d5"
integrity sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==
playwright-core@1.45.3:
version "1.45.3"
resolved "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.45.3.tgz#e77bc4c78a621b96c3e629027534ee1d25faac93"
integrity sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==
playwright@1.44.0:
version "1.44.0"
resolved "https://registry.npmmirror.com/playwright/-/playwright-1.44.0.tgz#22894e9b69087f6beb639249323d80fe2b5087ff"
integrity sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==
playwright@1.45.3:
version "1.45.3"
resolved "https://registry.npmmirror.com/playwright/-/playwright-1.45.3.tgz#75143f73093a6e1467f7097083d2f0846fb8dd2f"
integrity sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==
dependencies:
playwright-core "1.44.0"
playwright-core "1.45.3"
optionalDependencies:
fsevents "2.3.2"