mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 23:49:27 +08:00
Merge branch 'develop' of github.com:nocobase/nocobase into feat-main-datasource-mssql
This commit is contained in:
commit
c98508d12c
26
CHANGELOG.md
26
CHANGELOG.md
@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[Calendar]** missing data on boundary dates in weekly calendar view ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
|
||||
|
||||
- **[Auth: OIDC]** Incorrect redirection occurs when the callback path is the string 'null' by @2013xile
|
||||
|
||||
- **[Workflow: Approval]** Fix approval node configuration is incorrect after schema changed by @mytharcher
|
||||
|
||||
## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
|
||||
|
||||
### 🚀 Improvements
|
||||
|
||||
- **[Async task manager]** optimize import/export buttons in Pro ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
|
||||
|
||||
- **[Action: Export records Pro]** optimize import/export buttons in Pro by @katherinehhh
|
||||
|
||||
- **[Migration manager]** allow skip automatic backup and restore for migration by @gchust
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[client]** linkage conflict between same-named association fields in different sub-tables within the same form ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
|
||||
|
||||
- **[Action: Batch edit]** Click the batch edit button, configure the pop-up window, and then open it again, the pop-up window is blank ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
|
||||
|
||||
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
@ -5,6 +5,32 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
||||
|
||||
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[日历]** 日历区块以周为视图时,边界日期不显示数据 ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh
|
||||
|
||||
- **[认证:OIDC]** 回调路径是字符串'null'时导致跳转不正确 by @2013xile
|
||||
|
||||
- **[工作流:审批]** 修复审批节点界面配置变更后数据未同步的问题 by @mytharcher
|
||||
|
||||
## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28
|
||||
|
||||
### 🚀 优化
|
||||
|
||||
- **[异步任务管理器]** 优化 Pro 导入导出按钮异步逻辑 ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos
|
||||
|
||||
- **[操作:导出记录 Pro]** 优化 Pro 导入导出按钮 by @katherinehhh
|
||||
|
||||
- **[迁移管理]** 允许执行迁移时跳过自动备份还原 by @gchust
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[client]** 同一表单中不同关系字段的同名关系字段的联动互相影响 ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh
|
||||
|
||||
- **[操作:批量编辑]** 点击批量编辑按钮,配置完弹窗再打开,弹窗是空白的 ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe
|
||||
|
||||
## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27
|
||||
|
||||
### 🐛 修复
|
||||
|
@ -18,7 +18,14 @@ export interface SelectWithTitleProps {
|
||||
onChange?: (...args: any[]) => void;
|
||||
}
|
||||
|
||||
export function SelectWithTitle({ title, defaultValue, onChange, options, fieldNames }: SelectWithTitleProps) {
|
||||
export function SelectWithTitle({
|
||||
title,
|
||||
defaultValue,
|
||||
onChange,
|
||||
options,
|
||||
fieldNames,
|
||||
...others
|
||||
}: SelectWithTitleProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const timerRef = useRef<any>(null);
|
||||
return (
|
||||
@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
|
||||
>
|
||||
{title}
|
||||
<Select
|
||||
{...others}
|
||||
open={open}
|
||||
data-testid={`select-${title}`}
|
||||
popupMatchSelectWidth={false}
|
||||
|
@ -20,7 +20,6 @@ import { FilterBlockProvider } from '../../../filter-provider/FilterProvider';
|
||||
import {
|
||||
NocoBaseRecursionField,
|
||||
RefreshComponentProvider,
|
||||
useRefreshComponent,
|
||||
useRefreshFieldSchema,
|
||||
} from '../../../formily/NocoBaseRecursionField';
|
||||
import { DndContext, DndContextProps } from '../../common/dnd-context';
|
||||
@ -379,11 +378,9 @@ export const Grid: any = observer(
|
||||
}, [fieldSchema, render, InitializerComponent, showDivider]);
|
||||
|
||||
const refreshFieldSchema = useRefreshFieldSchema();
|
||||
const refreshComponent = useRefreshComponent();
|
||||
const refresh = useCallback(() => {
|
||||
refreshFieldSchema?.();
|
||||
refreshComponent?.();
|
||||
}, [refreshComponent, refreshFieldSchema]);
|
||||
}, [refreshFieldSchema]);
|
||||
|
||||
return (
|
||||
<RefreshComponentProvider refresh={refresh}>
|
||||
|
@ -568,13 +568,14 @@ export interface SchemaSettingsSelectItemProps
|
||||
extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>,
|
||||
Omit<SelectWithTitleProps, 'title' | 'defaultValue'> {
|
||||
value?: SelectWithTitleProps['defaultValue'];
|
||||
optionRender?: (option: any, info: { index: number }) => React.ReactNode;
|
||||
}
|
||||
export const SchemaSettingsSelectItem: FC<SchemaSettingsSelectItemProps> = (props) => {
|
||||
const { title, options, value, onChange, ...others } = props;
|
||||
const { title, options, value, onChange, optionRender, ...others } = props;
|
||||
|
||||
return (
|
||||
<SchemaSettingsItem title={title} {...others}>
|
||||
<SelectWithTitle {...{ title, defaultValue: value, onChange, options }} />
|
||||
<SelectWithTitle {...{ title, defaultValue: value, onChange, options, optionRender }} />
|
||||
</SchemaSettingsItem>
|
||||
);
|
||||
};
|
||||
|
@ -51,7 +51,11 @@ describe('union role: full permissions', async () => {
|
||||
roles: [role1.name, role2.name],
|
||||
},
|
||||
});
|
||||
|
||||
await rootAgent.resource('roles').setSystemRoleMode({
|
||||
values: {
|
||||
roleMode: SystemRoleMode.allowUseUnion,
|
||||
},
|
||||
});
|
||||
agent = await app.agent().login(user, UNION_ROLE_KEY);
|
||||
});
|
||||
|
||||
@ -415,15 +419,15 @@ describe('union role: full permissions', async () => {
|
||||
const rootAgent = await app.agent().login(rootUser);
|
||||
let rolesResponse = await agent.resource('roles').check();
|
||||
expect(rolesResponse.status).toBe(200);
|
||||
expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.default);
|
||||
expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.allowUseUnion);
|
||||
await rootAgent.resource('roles').setSystemRoleMode({
|
||||
values: {
|
||||
roleMode: SystemRoleMode.allowUseUnion,
|
||||
roleMode: SystemRoleMode.default,
|
||||
},
|
||||
});
|
||||
rolesResponse = await agent.resource('roles').check();
|
||||
expect(rolesResponse.status).toBe(200);
|
||||
expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.allowUseUnion);
|
||||
expect(rolesResponse.body.data.roleMode).toStrictEqual(SystemRoleMode.default);
|
||||
});
|
||||
|
||||
it(`should response no permission when createdById field is missing in data tables`, async () => {
|
||||
@ -501,4 +505,29 @@ describe('union role: full permissions', async () => {
|
||||
expect(getRolesResponse.statusCode).toBe(200);
|
||||
expect(getRolesResponse.body.meta.allowedActions.update.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should login successfully when use __union__ role in allowUseUnion mode #1906', async () => {
|
||||
const rootAgent = await app.agent().login(rootUser);
|
||||
await rootAgent.resource('roles').setSystemRoleMode({
|
||||
values: {
|
||||
roleMode: SystemRoleMode.allowUseUnion,
|
||||
},
|
||||
});
|
||||
agent = await app.agent().login(user);
|
||||
const createRoleResponse = await agent.resource('roles').check();
|
||||
expect(createRoleResponse.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('should currentRole not be __union__ when default role mode #1907', async () => {
|
||||
const rootAgent = await app.agent().login(rootUser);
|
||||
await rootAgent.resource('roles').setSystemRoleMode({
|
||||
values: {
|
||||
roleMode: SystemRoleMode.default,
|
||||
},
|
||||
});
|
||||
agent = await app.agent().login(user, UNION_ROLE_KEY);
|
||||
const createRoleResponse = await agent.resource('roles').check();
|
||||
expect(createRoleResponse.statusCode).toBe(200);
|
||||
expect(createRoleResponse.body.data.role).not.toBe(UNION_ROLE_KEY);
|
||||
});
|
||||
});
|
||||
|
@ -14,7 +14,7 @@ import { UNION_ROLE_KEY } from '../constants';
|
||||
import { SystemRoleMode } from '../enum';
|
||||
|
||||
export async function setCurrentRole(ctx: Context, next) {
|
||||
const currentRole = ctx.get('X-Role');
|
||||
let currentRole = ctx.get('X-Role');
|
||||
|
||||
if (currentRole === 'anonymous') {
|
||||
ctx.state.currentRole = currentRole;
|
||||
@ -49,7 +49,8 @@ export async function setCurrentRole(ctx: Context, next) {
|
||||
ctx.state.currentUser.roles = userRoles;
|
||||
const systemSettings = await ctx.db.getRepository('systemSettings').findOne();
|
||||
const roleMode = systemSettings?.get('roleMode') || SystemRoleMode.default;
|
||||
if (ctx.state.currentRole === UNION_ROLE_KEY && roleMode === SystemRoleMode.default) {
|
||||
if ([currentRole, ctx.state.currentRole].includes(UNION_ROLE_KEY) && roleMode === SystemRoleMode.default) {
|
||||
currentRole = userRoles[0].name;
|
||||
ctx.state.currentRole = userRoles[0].name;
|
||||
ctx.headers['x-role'] = userRoles[0].name;
|
||||
} else if (roleMode === SystemRoleMode.onlyUseUnion) {
|
||||
@ -85,7 +86,7 @@ export async function setCurrentRole(ctx: Context, next) {
|
||||
// 2. If the X-Role is not set, or the X-Role does not belong to the user, use the default role
|
||||
if (!role) {
|
||||
const defaultRole = userRoles.find((role) => role?.rolesUsers?.default);
|
||||
role = (defaultRole || userRoles[0])?.name;
|
||||
role = (defaultRole || userRoles.find((x) => x.name !== UNION_ROLE_KEY))?.name;
|
||||
}
|
||||
ctx.state.currentRole = role;
|
||||
ctx.state.currentRoles = [role];
|
||||
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 { expect, test } from '@nocobase/test/e2e';
|
||||
import { afterConfiguringTheModalWhenReopeningItTheContentShouldPersist } from './utils';
|
||||
|
||||
test.describe('refresh', () => {
|
||||
test('After configuring the modal, when reopening it, the content should persist', async ({ mockPage, page }) => {
|
||||
await mockPage(afterConfiguringTheModalWhenReopeningItTheContentShouldPersist).goto();
|
||||
|
||||
// 1. 点击 Bulk edit 按钮,打开弹窗
|
||||
await page.getByLabel('action-Action-Bulk edit-').click();
|
||||
|
||||
// 2. 新增一个表单区块
|
||||
await page.getByLabel('schema-initializer-Grid-popup').hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form' }).click();
|
||||
|
||||
// 3. 新增一个名为 Nickname 的字段
|
||||
await page.getByLabel('schema-initializer-Grid-bulkEditForm:configureFields-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
|
||||
// 4. 关闭弹窗,然后再打开,刚才新增的字段应该还在
|
||||
await page.getByLabel('drawer-Action.Container-users-Bulk edit-mask').click();
|
||||
await page.getByLabel('action-Action-Bulk edit-').click();
|
||||
await expect(page.getByLabel('block-item-BulkEditField-').getByText('Nickname')).toBeVisible();
|
||||
await page.getByLabel('block-item-BulkEditField-').click();
|
||||
});
|
||||
});
|
@ -1243,3 +1243,388 @@ export const theAddBlockButtonInDrawerShouldBeVisible = {
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
export const afterConfiguringTheModalWhenReopeningItTheContentShouldPersist = {
|
||||
pageSchema: {
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
name: 'rjzvy4bmawn',
|
||||
'x-uid': '1rs9caegbf2',
|
||||
'x-async': false,
|
||||
properties: {
|
||||
tab: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
properties: {
|
||||
bmsmf8futai: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'x84k7qs6jko',
|
||||
'x-async': false,
|
||||
'x-index': 4,
|
||||
},
|
||||
noe2oca30hc: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 't7jxa830ps6',
|
||||
'x-async': false,
|
||||
'x-index': 5,
|
||||
},
|
||||
w2hnq7rau9p: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': '0fjjtg8z7ws',
|
||||
'x-async': false,
|
||||
'x-index': 7,
|
||||
},
|
||||
fcfs4oot86g: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'nklv7lonpgn',
|
||||
'x-async': false,
|
||||
'x-index': 8,
|
||||
},
|
||||
i22fydav355: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'fz4g6cr9jvr',
|
||||
'x-async': false,
|
||||
'x-index': 10,
|
||||
},
|
||||
row_6u7y7uccrvz: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-index': 12,
|
||||
'x-uid': '7tzumo4nec7',
|
||||
'x-async': false,
|
||||
},
|
||||
higfesvgj7g: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': '8hpa6qf3sez',
|
||||
'x-async': false,
|
||||
'x-index': 13,
|
||||
},
|
||||
'37myao9n0wc': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'uw1dp2qxd3y',
|
||||
'x-async': false,
|
||||
'x-index': 14,
|
||||
},
|
||||
uvfd76q4ye9: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'nc56fu33m42',
|
||||
'x-async': false,
|
||||
'x-index': 15,
|
||||
},
|
||||
miidizeqgot: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'jf4qarrcs0z',
|
||||
'x-async': false,
|
||||
'x-index': 16,
|
||||
},
|
||||
hxmr87i5imu: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'l3kdiqd9a7k',
|
||||
'x-async': false,
|
||||
'x-index': 17,
|
||||
},
|
||||
pa8dwdi4h5a: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'uz5wcet83qn',
|
||||
'x-async': false,
|
||||
'x-index': 18,
|
||||
},
|
||||
pno0a05tbnp: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'b4bakhhasp3',
|
||||
'x-async': false,
|
||||
'x-index': 19,
|
||||
},
|
||||
uj09g5xgnr1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'qks035fnfl6',
|
||||
'x-async': false,
|
||||
'x-index': 20,
|
||||
},
|
||||
giobcwj316k: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': 'awwsb89nyso',
|
||||
'x-async': false,
|
||||
'x-index': 22,
|
||||
},
|
||||
oznewtbvuyw: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
bwtax0bnnp3: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
c0bypj7wg5q: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'users:list',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'table:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
'1dlvhzr308c': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-action': 'customize:bulkEdit',
|
||||
'x-action-settings': {
|
||||
updateMode: 'selected',
|
||||
},
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
'x-align': 'right',
|
||||
'x-decorator': 'BulkEditActionDecorator',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:bulkEdit',
|
||||
'x-acl-action': 'update',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'popup:addTab',
|
||||
'x-initializer-props': {
|
||||
gridInitializer: 'popup:bulkEdit:addBlock',
|
||||
},
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Bulk edit")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'popup:bulkEdit:addBlock',
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': '5ejbu8v5ol8',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'gfheiqtl7f7',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'if2rcx1dy2n',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'cxyi8q6lm3n',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'vbvf13xq15t',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'amlzm32jhwg',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
f232o2ds23n: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'table:configureColumns',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-initializer': 'table:configureItemActions',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-toolbar-props': {
|
||||
initializer: 'table:configureItemActions',
|
||||
},
|
||||
'x-app-version': '1.6.11',
|
||||
properties: {
|
||||
'153lpq30p5f': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-app-version': '1.6.11',
|
||||
'x-uid': '4mha1dmmyz9',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'afmceivuaf0',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'iu0xkmeuc5z',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ylor106s9ok',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'rl50hidu14n',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'xxhug2yumqf',
|
||||
'x-async': false,
|
||||
'x-index': 23,
|
||||
},
|
||||
},
|
||||
name: 'h63eibc46on',
|
||||
'x-uid': 'u9g23o0ohgk',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,176 +1,24 @@
|
||||
import { PinnedPluginListProvider, SchemaComponentOptions, useApp } from '@nocobase/client';
|
||||
/**
|
||||
* 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 { PinnedPluginListProvider, SchemaComponentOptions, useRequest } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { AsyncTasks } from './components/AsyncTasks';
|
||||
import React, { useEffect, useState, createContext, useContext, useCallback } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { useT } from './locale';
|
||||
|
||||
export const AsyncTaskContext = createContext<any>(null);
|
||||
|
||||
export const useAsyncTask = () => {
|
||||
const context = useContext(AsyncTaskContext);
|
||||
if (!context) {
|
||||
throw new Error('useAsyncTask must be used within AsyncTaskManagerProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AsyncTaskManagerProvider = (props) => {
|
||||
const app = useApp();
|
||||
const t = useT();
|
||||
const [tasks, setTasks] = useState<any[]>([]);
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [hasProcessingTasks, setHasProcessingTasks] = useState(false);
|
||||
const [cancellingTasks, setCancellingTasks] = useState<Set<string>>(new Set());
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentError, setCurrentError] = useState<any>(null);
|
||||
const [resultModalVisible, setResultModalVisible] = useState(false);
|
||||
const [currentTask, setCurrentTask] = useState(null);
|
||||
const [wsAuthorized, setWsAuthorized] = useState(() => app.isWsAuthorized);
|
||||
|
||||
useEffect(() => {
|
||||
setHasProcessingTasks(tasks.some((task) => task.status.type !== 'success' && task.status.type !== 'failed'));
|
||||
}, [tasks]);
|
||||
|
||||
const handleTaskMessage = useCallback((event: CustomEvent) => {
|
||||
const tasks = event.detail;
|
||||
setTasks(tasks ? tasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) : []);
|
||||
}, []);
|
||||
|
||||
const handleTaskCreated = useCallback((event: CustomEvent) => {
|
||||
const taskData = event.detail;
|
||||
setTasks((prev) => {
|
||||
const newTasks = [taskData, ...prev];
|
||||
return newTasks.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
});
|
||||
setPopoverVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleTaskProgress = useCallback((event: CustomEvent) => {
|
||||
const { taskId, progress } = event.detail;
|
||||
setTasks((prev) => prev.map((task) => (task.taskId === taskId ? { ...task, progress } : task)));
|
||||
}, []);
|
||||
|
||||
const handleTaskStatus = useCallback((event: CustomEvent) => {
|
||||
const { taskId, status } = event.detail;
|
||||
if (status.type === 'cancelled') {
|
||||
setTasks((prev) => prev.filter((task) => task.taskId !== taskId));
|
||||
} else {
|
||||
setTasks((prev) => {
|
||||
const newTasks = prev.map((task) => {
|
||||
if (task.taskId === taskId) {
|
||||
if (status.type === 'success' && task.status.type !== 'success') {
|
||||
message.success(t('Task completed'));
|
||||
}
|
||||
if (status.type === 'failed' && task.status.type !== 'failed') {
|
||||
message.error(t('Task failed'));
|
||||
}
|
||||
return { ...task, status };
|
||||
}
|
||||
return task;
|
||||
});
|
||||
return newTasks;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleWsAuthorized = useCallback(() => {
|
||||
setWsAuthorized(true);
|
||||
}, []);
|
||||
|
||||
const handleTaskCancelled = useCallback((event: CustomEvent) => {
|
||||
const { taskId } = event.detail;
|
||||
setCancellingTasks((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(taskId);
|
||||
return newSet;
|
||||
});
|
||||
message.success(t('Task cancelled'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
app.eventBus.addEventListener('ws:message:async-tasks', handleTaskMessage);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.addEventListener('ws:message:authorized', handleWsAuthorized);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
|
||||
if (wsAuthorized) {
|
||||
app.ws.send(
|
||||
JSON.stringify({
|
||||
type: 'request:async-tasks:list',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks', handleTaskMessage);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.removeEventListener('ws:message:authorized', handleWsAuthorized);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
};
|
||||
}, [
|
||||
app,
|
||||
handleTaskMessage,
|
||||
handleTaskCreated,
|
||||
handleTaskProgress,
|
||||
handleTaskStatus,
|
||||
handleWsAuthorized,
|
||||
handleTaskCancelled,
|
||||
wsAuthorized,
|
||||
]);
|
||||
|
||||
const handleCancelTask = async (taskId: string) => {
|
||||
setCancellingTasks((prev) => new Set(prev).add(taskId));
|
||||
try {
|
||||
app.ws.send(
|
||||
JSON.stringify({
|
||||
type: 'request:async-tasks:cancel',
|
||||
payload: { taskId },
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to cancel task:', error);
|
||||
setCancellingTasks((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(taskId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue = {
|
||||
tasks,
|
||||
popoverVisible,
|
||||
setPopoverVisible,
|
||||
hasProcessingTasks,
|
||||
cancellingTasks,
|
||||
modalVisible,
|
||||
setModalVisible,
|
||||
currentError,
|
||||
setCurrentError,
|
||||
resultModalVisible,
|
||||
setResultModalVisible,
|
||||
currentTask,
|
||||
setCurrentTask,
|
||||
handleCancelTask,
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncTaskContext.Provider value={contextValue}>
|
||||
<PinnedPluginListProvider
|
||||
items={
|
||||
tasks.length > 0
|
||||
? {
|
||||
asyncTasks: { order: 300, component: 'AsyncTasks', pin: true, snippet: '*' },
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<SchemaComponentOptions components={{ AsyncTasks }}>{props.children}</SchemaComponentOptions>
|
||||
</PinnedPluginListProvider>
|
||||
</AsyncTaskContext.Provider>
|
||||
<PinnedPluginListProvider
|
||||
items={{
|
||||
asyncTasks: { order: 300, component: 'AsyncTasks', pin: true, snippet: '*' },
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions components={{ AsyncTasks }}>{props.children}</SchemaComponentOptions>
|
||||
</PinnedPluginListProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,31 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Popover, Table, Tag, Progress, Space, Tooltip, Popconfirm, Modal, Empty } from 'antd';
|
||||
import { createStyles, Icon, useApp, usePlugin } from '@nocobase/client';
|
||||
/**
|
||||
* 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 {
|
||||
createStyles,
|
||||
Icon,
|
||||
useAPIClient,
|
||||
useApp,
|
||||
usePlugin,
|
||||
useRequest,
|
||||
useCollectionManager,
|
||||
useCompile,
|
||||
} from '@nocobase/client';
|
||||
import { Button, Empty, Modal, Popconfirm, Popover, Progress, Space, Table, Tag, Tooltip } from 'antd';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentAppInfo } from '@nocobase/client';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { useT } from '../locale';
|
||||
import { useAsyncTask } from '../AsyncTaskManagerProvider';
|
||||
import { useCurrentAppInfo } from '@nocobase/client';
|
||||
|
||||
const useStyles = createStyles(({ token }) => {
|
||||
return {
|
||||
button: {
|
||||
@ -34,93 +52,27 @@ const renderTaskResult = (status, t) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const AsyncTasks = () => {
|
||||
const {
|
||||
tasks,
|
||||
popoverVisible,
|
||||
setPopoverVisible,
|
||||
hasProcessingTasks,
|
||||
cancellingTasks,
|
||||
modalVisible,
|
||||
setModalVisible,
|
||||
currentError,
|
||||
setCurrentError,
|
||||
resultModalVisible,
|
||||
setResultModalVisible,
|
||||
currentTask,
|
||||
setCurrentTask,
|
||||
handleCancelTask,
|
||||
} = useAsyncTask();
|
||||
const useAsyncTask = () => {
|
||||
const { data, refreshAsync, loading } = useRequest<any>({
|
||||
url: 'asyncTasks:list',
|
||||
});
|
||||
return { loading, tasks: data?.data || [], refresh: refreshAsync };
|
||||
};
|
||||
|
||||
const plugin = usePlugin<any>('async-task-manager');
|
||||
const AsyncTasksButton = (props) => {
|
||||
const { popoverVisible, setPopoverVisible, tasks, refresh, loading, hasProcessingTasks } = props;
|
||||
const app = useApp();
|
||||
const api = useAPIClient();
|
||||
const appInfo = useCurrentAppInfo();
|
||||
const t = useT();
|
||||
const { styles } = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (popoverVisible) {
|
||||
const popoverElements = document.querySelectorAll('.ant-popover');
|
||||
const buttonElement = document.querySelector('.sync-task-button');
|
||||
let clickedInside = false;
|
||||
|
||||
popoverElements.forEach((element) => {
|
||||
if (element.contains(event.target as Node)) {
|
||||
clickedInside = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (buttonElement?.contains(event.target as Node)) {
|
||||
clickedInside = true;
|
||||
}
|
||||
|
||||
if (!clickedInside) {
|
||||
setPopoverVisible(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, [popoverVisible, setPopoverVisible]);
|
||||
|
||||
const plugin = usePlugin<any>('async-task-manager');
|
||||
const cm = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
const showTaskResult = (task) => {
|
||||
setCurrentTask(task);
|
||||
setResultModalVisible(true);
|
||||
setPopoverVisible(false);
|
||||
};
|
||||
|
||||
const renderTaskResultModal = () => {
|
||||
if (!currentTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { payload } = currentTask.status;
|
||||
const renderer = plugin.taskResultRendererManager.get(currentTask.title.actionType);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('Task result')}
|
||||
open={resultModalVisible}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setResultModalVisible(false)}>
|
||||
{t('Close')}
|
||||
</Button>,
|
||||
]}
|
||||
onCancel={() => setResultModalVisible(false)}
|
||||
>
|
||||
{renderer ? (
|
||||
React.createElement(renderer, { payload, task: currentTask })
|
||||
) : (
|
||||
<div>{t(`No renderer available for this task type, payload: ${payload}`)}</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('Created at'),
|
||||
@ -140,7 +92,7 @@ export const AsyncTasks = () => {
|
||||
if (!title) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const collection = cm.getCollection(title.collection);
|
||||
const actionTypeMap = {
|
||||
export: t('Export'),
|
||||
import: t('Import'),
|
||||
@ -156,7 +108,7 @@ export const AsyncTasks = () => {
|
||||
};
|
||||
|
||||
const taskTemplate = taskTypeMap[title.actionType] || `${actionText}`;
|
||||
return taskTemplate.replace('{collection}', title.collection);
|
||||
return taskTemplate.replace('{collection}', compile(collection?.title || title.collection));
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -274,7 +226,7 @@ export const AsyncTasks = () => {
|
||||
width: 180,
|
||||
render: (_, record: any) => {
|
||||
const actions = [];
|
||||
const isTaskCancelling = cancellingTasks.has(record.taskId);
|
||||
const isTaskCancelling = false;
|
||||
|
||||
if ((record.status.type === 'running' || record.status.type === 'pending') && record.cancelable) {
|
||||
actions.push(
|
||||
@ -282,7 +234,15 @@ export const AsyncTasks = () => {
|
||||
key="cancel"
|
||||
title={t('Confirm cancel')}
|
||||
description={t('Confirm cancel description')}
|
||||
onConfirm={() => handleCancelTask(record.taskId)}
|
||||
onConfirm={async () => {
|
||||
await api.request({
|
||||
url: 'asyncTasks:cancel',
|
||||
params: {
|
||||
filterByTk: record.taskId,
|
||||
},
|
||||
});
|
||||
refresh();
|
||||
}}
|
||||
okText={t('Confirm')}
|
||||
cancelText={t('Cancel')}
|
||||
disabled={isTaskCancelling}
|
||||
@ -309,8 +269,16 @@ export const AsyncTasks = () => {
|
||||
icon={<Icon type="DownloadOutlined" />}
|
||||
onClick={() => {
|
||||
const token = app.apiClient.auth.token;
|
||||
const collection = cm.getCollection(record.title.collection);
|
||||
const compiledTitle = compile(collection?.title);
|
||||
const suffix = record?.title?.actionType === 'export-attachments' ? '-attachments.zip' : '.xlsx';
|
||||
const fileText = `${compiledTitle}${suffix}`;
|
||||
const filename =
|
||||
record?.title?.actionType !== 'create migration' ? encodeURIComponent(fileText) : null;
|
||||
const url = app.getApiUrl(
|
||||
`asyncTasks:fetchFile/${record.taskId}?token=${token}&__appName=${appInfo?.data?.name || app.name}`,
|
||||
`asyncTasks:fetchFile/${record.taskId}?token=${token}&__appName=${encodeURIComponent(
|
||||
appInfo?.data?.name || app.name,
|
||||
)}${filename ? `&filename=${filename}` : ''}`,
|
||||
);
|
||||
window.open(url);
|
||||
}}
|
||||
@ -325,7 +293,19 @@ export const AsyncTasks = () => {
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<Icon type="EyeOutlined" />}
|
||||
onClick={() => showTaskResult(record)}
|
||||
onClick={() => {
|
||||
showTaskResult(record);
|
||||
const { payload } = record.status;
|
||||
const renderer = plugin.taskResultRendererManager.get(record.title.actionType);
|
||||
Modal.info({
|
||||
title: t('Task result'),
|
||||
content: renderer ? (
|
||||
React.createElement(renderer, { payload, task: record })
|
||||
) : (
|
||||
<div>{t(`No renderer available for this task type, payload: ${payload}`)}</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('View result')}
|
||||
</Button>,
|
||||
@ -341,9 +321,22 @@ export const AsyncTasks = () => {
|
||||
size="small"
|
||||
icon={<Icon type="ExclamationCircleOutlined" />}
|
||||
onClick={() => {
|
||||
setCurrentError(record.status.errors);
|
||||
setModalVisible(true);
|
||||
setPopoverVisible(false);
|
||||
Modal.info({
|
||||
title: t('Error Details'),
|
||||
content: record.status.errors?.map((error, index) => (
|
||||
<div key={index} style={{ marginBottom: 16 }}>
|
||||
<div style={{ color: '#ff4d4f', marginBottom: 8 }}>{error.message}</div>
|
||||
{error.code && (
|
||||
<div style={{ color: '#999', fontSize: 12 }}>
|
||||
{t('Error code')}: {error.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)),
|
||||
closable: true,
|
||||
width: 400,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Error details')}
|
||||
@ -357,9 +350,9 @@ export const AsyncTasks = () => {
|
||||
];
|
||||
|
||||
const content = (
|
||||
<div style={{ width: tasks.length > 0 ? 800 : 200 }}>
|
||||
<div style={{ maxHeight: '70vh', overflow: 'auto', width: tasks.length > 0 ? 800 : 200 }}>
|
||||
{tasks.length > 0 ? (
|
||||
<Table columns={columns} dataSource={tasks} size="small" pagination={false} rowKey="taskId" />
|
||||
<Table loading={loading} columns={columns} dataSource={tasks} size="small" pagination={false} rowKey="taskId" />
|
||||
) : (
|
||||
<div style={{ padding: '24px 0', display: 'flex', justifyContent: 'center' }}>
|
||||
<Empty description={t('No tasks')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
@ -383,30 +376,53 @@ export const AsyncTasks = () => {
|
||||
onClick={() => setPopoverVisible(!popoverVisible)}
|
||||
/>
|
||||
</Popover>
|
||||
{renderTaskResultModal()}
|
||||
|
||||
<Modal
|
||||
title={t('Error Details')}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="ok" type="primary" onClick={() => setModalVisible(false)}>
|
||||
{t('OK')}
|
||||
</Button>,
|
||||
]}
|
||||
width={400}
|
||||
>
|
||||
{currentError?.map((error, index) => (
|
||||
<div key={index} style={{ marginBottom: 16 }}>
|
||||
<div style={{ color: '#ff4d4f', marginBottom: 8 }}>{error.message}</div>
|
||||
{error.code && (
|
||||
<div style={{ color: '#999', fontSize: 12 }}>
|
||||
{t('Error code')}: {error.code}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AsyncTasks = () => {
|
||||
const { tasks, refresh, ...others } = useAsyncTask();
|
||||
const app = useApp();
|
||||
const [popoverVisible, setPopoverVisible] = useState(false);
|
||||
const [hasProcessingTasks, setHasProcessingTasks] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasProcessingTasks(tasks.some((task) => task.status.type !== 'success' && task.status.type !== 'failed'));
|
||||
}, [tasks]);
|
||||
|
||||
const handleTaskCreated = useCallback(async () => {
|
||||
setPopoverVisible(true);
|
||||
}, []);
|
||||
const handleTaskProgress = useCallback(() => {
|
||||
refresh();
|
||||
console.log('handleTaskProgress');
|
||||
}, []);
|
||||
const handleTaskStatus = useCallback(() => {
|
||||
refresh();
|
||||
console.log('handleTaskStatus');
|
||||
}, []);
|
||||
const handleTaskCancelled = useCallback(() => {
|
||||
refresh();
|
||||
console.log('handleTaskCancelled');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.addEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
|
||||
return () => {
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:created', handleTaskCreated);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:progress', handleTaskProgress);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:status', handleTaskStatus);
|
||||
app.eventBus.removeEventListener('ws:message:async-tasks:cancelled', handleTaskCancelled);
|
||||
};
|
||||
}, [app, handleTaskCancelled, handleTaskCreated, handleTaskProgress, handleTaskStatus]);
|
||||
|
||||
return (
|
||||
tasks?.length > 0 && (
|
||||
<AsyncTasksButton {...{ tasks, refresh, popoverVisible, setPopoverVisible, hasProcessingTasks, ...others }} />
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,18 @@
|
||||
/**
|
||||
* 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 { Plugin } from '@nocobase/server';
|
||||
import { BaseTaskManager } from './base-task-manager';
|
||||
import { AsyncTasksManager } from './interfaces/async-task-manager';
|
||||
import { CommandTaskType } from './command-task-type';
|
||||
import asyncTasksResource from './resourcers/async-tasks';
|
||||
import { throttle } from 'lodash';
|
||||
import { BaseTaskManager } from './base-task-manager';
|
||||
import { CommandTaskType } from './command-task-type';
|
||||
import { AsyncTasksManager } from './interfaces/async-task-manager';
|
||||
import asyncTasksResource from './resourcers/async-tasks';
|
||||
|
||||
export class PluginAsyncExportServer extends Plugin {
|
||||
private progressThrottles: Map<string, Function> = new Map();
|
||||
@ -20,7 +29,7 @@ export class PluginAsyncExportServer extends Plugin {
|
||||
});
|
||||
|
||||
this.app.container.get<AsyncTasksManager>('AsyncTaskManager').registerTaskType(CommandTaskType);
|
||||
this.app.acl.allow('asyncTasks', ['get', 'fetchFile'], 'loggedIn');
|
||||
this.app.acl.allow('asyncTasks', ['list', 'get', 'fetchFile', 'cancel'], 'loggedIn');
|
||||
}
|
||||
|
||||
getThrottledProgressEmitter(taskId: string, userId: string) {
|
||||
|
@ -1,8 +1,26 @@
|
||||
/**
|
||||
* 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 fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import { basename } from 'path';
|
||||
|
||||
export default {
|
||||
name: 'asyncTasks',
|
||||
actions: {
|
||||
async list(ctx, next) {
|
||||
const userId = ctx.auth.user.id;
|
||||
const asyncTaskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
const tasks = await asyncTaskManager.getTasksByTag('userId', userId);
|
||||
ctx.body = _.orderBy(tasks, 'createdAt', 'desc');
|
||||
await next();
|
||||
},
|
||||
async get(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
const taskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
@ -11,8 +29,29 @@ export default {
|
||||
ctx.body = taskStatus;
|
||||
await next();
|
||||
},
|
||||
async fetchFile(ctx, next) {
|
||||
async cancel(ctx, next) {
|
||||
const { filterByTk } = ctx.action.params;
|
||||
const userId = ctx.auth.user.id;
|
||||
const asyncTaskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
|
||||
const task = asyncTaskManager.getTask(filterByTk);
|
||||
|
||||
if (!task) {
|
||||
ctx.body = 'ok';
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.tags['userId'] != userId) {
|
||||
ctx.throw(403);
|
||||
}
|
||||
|
||||
const cancelled = await asyncTaskManager.cancelTask(filterByTk);
|
||||
ctx.body = cancelled;
|
||||
await next();
|
||||
},
|
||||
async fetchFile(ctx, next) {
|
||||
const { filterByTk, filename } = ctx.action.params;
|
||||
const taskManager = ctx.app.container.get('AsyncTaskManager');
|
||||
const taskStatus = await taskManager.getTaskStatus(filterByTk);
|
||||
// throw error if task is not success
|
||||
@ -28,10 +67,12 @@ export default {
|
||||
|
||||
// send file to client
|
||||
ctx.body = fs.createReadStream(filePath);
|
||||
|
||||
// 处理文件名
|
||||
let finalFileName = filename ? filename : basename(filePath);
|
||||
finalFileName = encodeURIComponent(finalFileName); // 避免中文或特殊字符问题
|
||||
ctx.set({
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Disposition': `attachment; filename=${basename(filePath)}`,
|
||||
'Content-Disposition': `attachment; filename=${finalFileName}`,
|
||||
});
|
||||
|
||||
await next();
|
||||
|
@ -9,13 +9,13 @@
|
||||
import { FileImageOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
import { useActionContext } from '@nocobase/client';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScanBox } from './ScanBox';
|
||||
import { useScanner } from './useScanner';
|
||||
|
||||
const qrcodeEleId = 'qrcode';
|
||||
export const QRCodeScannerInner = (props) => {
|
||||
export const QRCodeScannerInner = ({ setVisible }) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const imgUploaderRef = useRef<HTMLInputElement>();
|
||||
const { t } = useTranslation('block-workbench');
|
||||
@ -23,9 +23,17 @@ export const QRCodeScannerInner = (props) => {
|
||||
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
|
||||
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
|
||||
|
||||
const onScanSuccess = useCallback(
|
||||
(text) => {
|
||||
setVisible(false);
|
||||
},
|
||||
[setVisible],
|
||||
);
|
||||
|
||||
const { startScanFile } = useScanner({
|
||||
onScannerSizeChanged: setOriginVideoSize,
|
||||
elementId: qrcodeEleId,
|
||||
onScanSuccess,
|
||||
});
|
||||
|
||||
const getBoxStyle = (): React.CSSProperties => {
|
||||
@ -174,7 +182,7 @@ export const QRCodeScanner = (props) => {
|
||||
|
||||
return visible && cameraAvaliable ? (
|
||||
<div style={style}>
|
||||
<QRCodeScannerInner />
|
||||
<QRCodeScannerInner setVisible={setVisible} />
|
||||
<LeftOutlined style={backIconStyle} onClick={() => setVisible(false)} />
|
||||
<div style={titleStyle}>{t('Scan QR code')}</div>
|
||||
</div>
|
||||
|
@ -20,7 +20,7 @@ function removeStringIfStartsWith(text: string, prefix: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
export function useScanner({ onScannerSizeChanged, elementId }) {
|
||||
export function useScanner({ onScannerSizeChanged, elementId, onScanSuccess }) {
|
||||
const app = useApp();
|
||||
const mobileManager = app.pm.get(MobileManager);
|
||||
const basename = mobileManager.mobileRouter.basename.replace(/\/+$/, '');
|
||||
@ -50,12 +50,17 @@ export function useScanner({ onScannerSizeChanged, elementId }) {
|
||||
},
|
||||
},
|
||||
(text) => {
|
||||
if (text?.startsWith('http')) {
|
||||
window.location.href = text;
|
||||
return;
|
||||
}
|
||||
navigate(removeStringIfStartsWith(text, basename));
|
||||
onScanSuccess && onScanSuccess(text);
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
},
|
||||
[navigate, onScannerSizeChanged, viewPoint, basename],
|
||||
[navigate, onScannerSizeChanged, viewPoint, basename, onScanSuccess],
|
||||
);
|
||||
const stopScanner = useCallback(async (scanner: Html5Qrcode) => {
|
||||
const state = scanner.getState();
|
||||
@ -69,13 +74,18 @@ export function useScanner({ onScannerSizeChanged, elementId }) {
|
||||
await stopScanner(scanner);
|
||||
try {
|
||||
const { decodedText } = await scanner.scanFileV2(file, false);
|
||||
if (decodedText?.startsWith('http')) {
|
||||
window.location.href = decodedText;
|
||||
return;
|
||||
}
|
||||
navigate(removeStringIfStartsWith(decodedText, basename));
|
||||
onScanSuccess && onScanSuccess(decodedText);
|
||||
} catch (error) {
|
||||
alert(t('QR code recognition failed, please scan again'));
|
||||
startScanCamera(scanner);
|
||||
}
|
||||
},
|
||||
[stopScanner, scanner, navigate, basename, t, startScanCamera],
|
||||
[stopScanner, scanner, navigate, basename, t, startScanCamera, onScanSuccess],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -108,7 +108,7 @@ const useEvents = (
|
||||
title: string;
|
||||
},
|
||||
date: Date,
|
||||
view: (typeof Weeks)[number],
|
||||
view: (typeof Weeks)[number] | any = 'month',
|
||||
) => {
|
||||
const parseExpression = useLazy<typeof import('cron-parser').parseExpression>(
|
||||
() => import('cron-parser'),
|
||||
@ -132,8 +132,8 @@ const useEvents = (
|
||||
const intervalTime = end.diff(start, 'millisecond', true);
|
||||
|
||||
const dateM = dayjs(date);
|
||||
const startDate = dateM.clone().startOf('month');
|
||||
const endDate = startDate.clone().endOf('month');
|
||||
const startDate = dateM.clone().startOf(view);
|
||||
const endDate = startDate.clone().endOf(view);
|
||||
|
||||
/**
|
||||
* view === month 时,会显示当月日程
|
||||
@ -425,7 +425,6 @@ export const Calendar: any = withDynamicSchemaProps(
|
||||
};
|
||||
};
|
||||
const BigCalendar = reactBigCalendar?.BigCalendar;
|
||||
|
||||
return wrapSSR(
|
||||
<div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}>
|
||||
<PopupContextProvider visible={visible} setVisible={setVisible}>
|
||||
|
@ -55,6 +55,12 @@ describe('Web client desktopRoutes', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
await rootAgent.resource('roles').setSystemRoleMode({
|
||||
values: {
|
||||
roleMode: SystemRoleMode.allowUseUnion,
|
||||
},
|
||||
});
|
||||
|
||||
agent = await app.agent().login(user, UNION_ROLE_KEY);
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/src
|
@ -0,0 +1 @@
|
||||
# @nocobase/plugin-locale-tester
|
2
packages/plugins/@nocobase/plugin-locale-tester/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-locale-tester/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/client';
|
||||
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/client/index.js');
|
13
packages/plugins/@nocobase/plugin-locale-tester/package.json
Normal file
13
packages/plugins/@nocobase/plugin-locale-tester/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-locale-tester",
|
||||
"displayName": "Locale tester",
|
||||
"displayName.zh-CN": "翻译测试工具",
|
||||
"version": "1.7.0-alpha.10",
|
||||
"homepage": "https://github.com/nocobase/locales",
|
||||
"main": "dist/server/index.js",
|
||||
"peerDependencies": {
|
||||
"@nocobase/client": "1.x",
|
||||
"@nocobase/server": "1.x",
|
||||
"@nocobase/test": "1.x"
|
||||
}
|
||||
}
|
2
packages/plugins/@nocobase/plugin-locale-tester/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-locale-tester/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './dist/server';
|
||||
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/server/index.js');
|
249
packages/plugins/@nocobase/plugin-locale-tester/src/client/client.d.ts
vendored
Normal file
249
packages/plugins/@nocobase/plugin-locale-tester/src/client/client.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// CSS modules
|
||||
type CSSModuleClasses = { readonly [key: string]: string };
|
||||
|
||||
declare module '*.module.css' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.scss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sass' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.less' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.styl' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.stylus' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.pcss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
declare module '*.module.sss' {
|
||||
const classes: CSSModuleClasses;
|
||||
export default classes;
|
||||
}
|
||||
|
||||
// CSS
|
||||
declare module '*.css' { }
|
||||
declare module '*.scss' { }
|
||||
declare module '*.sass' { }
|
||||
declare module '*.less' { }
|
||||
declare module '*.styl' { }
|
||||
declare module '*.stylus' { }
|
||||
declare module '*.pcss' { }
|
||||
declare module '*.sss' { }
|
||||
|
||||
// Built-in asset types
|
||||
// see `src/node/constants.ts`
|
||||
|
||||
// images
|
||||
declare module '*.apng' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.png' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.jfif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjpeg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pjp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.gif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.svg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ico' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webp' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.avif' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// media
|
||||
declare module '*.mp4' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.webm' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ogg' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mp3' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.wav' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.flac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.aac' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.opus' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.mov' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.m4a' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.vtt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// fonts
|
||||
declare module '*.woff' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.woff2' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.eot' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.ttf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.otf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// other
|
||||
declare module '*.webmanifest' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.pdf' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
declare module '*.txt' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
// wasm?init
|
||||
declare module '*.wasm?init' {
|
||||
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||
export default initWasm;
|
||||
}
|
||||
|
||||
// web worker
|
||||
declare module '*?worker' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&inline' {
|
||||
const workerConstructor: {
|
||||
new(options?: { name?: string }): Worker;
|
||||
};
|
||||
export default workerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?worker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&inline' {
|
||||
const sharedWorkerConstructor: {
|
||||
new(options?: { name?: string }): SharedWorker;
|
||||
};
|
||||
export default sharedWorkerConstructor;
|
||||
}
|
||||
|
||||
declare module '*?sharedworker&url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?raw' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?url' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*?inline' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 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 { useForm } from '@formily/react';
|
||||
import { ActionProps, ISchema, Plugin, SchemaComponent, useAPIClient, useApp, useRequest } from '@nocobase/client';
|
||||
import { Alert, App as AntdApp, Card, Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import { useT } from './locale';
|
||||
|
||||
function LocaleTester() {
|
||||
const { data, loading } = useRequest<any>({
|
||||
url: 'localeTester:get',
|
||||
});
|
||||
const t = useT();
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
name: 'root',
|
||||
properties: {
|
||||
test: {
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
properties: {
|
||||
locale: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.JSON',
|
||||
'x-component-props': {
|
||||
autoSize: { minRows: 20, maxRows: 30 },
|
||||
},
|
||||
default: data?.data?.locale,
|
||||
title: t('Translations'),
|
||||
},
|
||||
button: {
|
||||
type: 'void',
|
||||
'x-component': 'Action',
|
||||
title: t('Submit'),
|
||||
'x-use-component-props': 'useSubmitActionProps',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const useSubmitActionProps = () => {
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
const { message } = AntdApp.useApp();
|
||||
const app = useApp();
|
||||
|
||||
return {
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
async onClick() {
|
||||
await form.submit();
|
||||
const values = form.values;
|
||||
await api.request({
|
||||
url: 'localeTester:updateOrCreate',
|
||||
method: 'post',
|
||||
params: {
|
||||
filterKeys: ['id'],
|
||||
},
|
||||
data: {
|
||||
id: data?.data?.id,
|
||||
locale: values.locale,
|
||||
},
|
||||
});
|
||||
message.success(app.i18n.t('Saved successfully!'));
|
||||
window.location.reload();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
<Alert
|
||||
style={{ marginBottom: 12 }}
|
||||
description={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t(
|
||||
`Please go to <a target="_blank" href="https://github.com/nocobase/locales">nocobase/locales</a> to get the language file that needs translation, then paste it below and provide the translation.`,
|
||||
),
|
||||
}}
|
||||
></div>
|
||||
}
|
||||
/>
|
||||
<SchemaComponent schema={schema} scope={{ useSubmitActionProps }} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export class PluginLocaleTesterClient extends Plugin {
|
||||
async afterAdd() {
|
||||
// await this.app.pm.add()
|
||||
}
|
||||
|
||||
async beforeLoad() {}
|
||||
|
||||
// You can get and modify the app instance here
|
||||
async load() {
|
||||
this.app.pluginSettingsManager.add('locale-tester', {
|
||||
title: this.t('Locale tester'),
|
||||
icon: 'TranslationOutlined',
|
||||
Component: LocaleTester,
|
||||
});
|
||||
// this.app.addComponents({})
|
||||
// this.app.addScopes({})
|
||||
// this.app.addProvider()
|
||||
// this.app.addProviders()
|
||||
// this.app.router.add()
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginLocaleTesterClient;
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import pkg from './../../package.json';
|
||||
import { useApp } from '@nocobase/client';
|
||||
|
||||
export function useT() {
|
||||
const app = useApp();
|
||||
return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] });
|
||||
}
|
||||
|
||||
export function tStr(key: string) {
|
||||
return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`;
|
||||
}
|
11
packages/plugins/@nocobase/plugin-locale-tester/src/index.ts
Normal file
11
packages/plugins/@nocobase/plugin-locale-tester/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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 * from './server';
|
||||
export { default } from './server';
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"Locale": "Locale",
|
||||
"Locale tester": "Locale tester",
|
||||
"Please go to <a target=\"_blank\" href=\"https://github.com/nocobase/locales\">nocobase/locales</a> to get the language file that needs translation, then paste it below and provide the translation.": "Please go to <a target=\"_blank\" href=\"https://github.com/nocobase/locales\">nocobase/locales</a> to get the language file that needs translation, then paste it below and provide the translation."
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"Translations": "翻译",
|
||||
"Locale tester": "翻译测试工具",
|
||||
"Please go to <a target=\"_blank\" href=\"https://github.com/nocobase/locales\">nocobase/locales</a> to get the language file that needs translation, then paste it below and provide the translation.": "请前往 <a target=\"_blank\" href=\"https://github.com/nocobase/locales\">nocobase/locales</a> 获取需要翻译的语言文件,粘贴到下方并进行翻译。"
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 { defineCollection } from '@nocobase/database';
|
||||
|
||||
export default defineCollection({
|
||||
name: 'localeTester',
|
||||
autoGenId: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'json',
|
||||
name: 'locale',
|
||||
},
|
||||
],
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 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 { default } from './plugin';
|
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 { Plugin } from '@nocobase/server';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class PluginLocaleTesterServer extends Plugin {
|
||||
async afterAdd() {}
|
||||
|
||||
async beforeLoad() {
|
||||
this.app.acl.registerSnippet({
|
||||
name: `pm.${this.name}`,
|
||||
actions: ['localeTester:*'],
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.app.resourceManager.use(async (ctx, next) => {
|
||||
await next();
|
||||
const { resourceName, actionName } = ctx.action;
|
||||
if (resourceName === 'app' && actionName === 'getLang') {
|
||||
const repository = this.db.getRepository('localeTester');
|
||||
const record = await repository.findOne();
|
||||
const locale = record?.locale || {};
|
||||
if (locale['cronstrue']) {
|
||||
_.set(ctx.body, 'cronstrue', locale['cronstrue']);
|
||||
}
|
||||
if (locale['react-js-cron']) {
|
||||
_.set(ctx.body, 'cron', locale['react-js-cron']);
|
||||
}
|
||||
Object.keys(locale).forEach((key) => {
|
||||
if (key === 'cronstrue' || key === 'react-js-cron') {
|
||||
return;
|
||||
}
|
||||
const value = locale[key];
|
||||
_.set(ctx.body, ['resources', key], value);
|
||||
const k = key.replace('@nocobase/', '').replace('@nocobase/plugin-', '');
|
||||
_.set(ctx.body, ['resources', k], value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async install() {}
|
||||
|
||||
async afterEnable() {}
|
||||
|
||||
async afterDisable() {}
|
||||
|
||||
async remove() {}
|
||||
}
|
||||
|
||||
export default PluginLocaleTesterServer;
|
@ -54,7 +54,11 @@ describe('union role mobileRoutes', async () => {
|
||||
roles: [role1.name, role2.name],
|
||||
},
|
||||
});
|
||||
|
||||
await rootAgent.resource('roles').setSystemRoleMode({
|
||||
values: {
|
||||
roleMode: SystemRoleMode.allowUseUnion,
|
||||
},
|
||||
});
|
||||
agent = await app.agent().login(user, UNION_ROLE_KEY);
|
||||
});
|
||||
|
||||
|
@ -34,8 +34,16 @@ import {
|
||||
} from '../../observables';
|
||||
import InfiniteScrollContent from './InfiniteScrollContent';
|
||||
|
||||
function removeStringIfStartsWith(text: string, prefix: string): string {
|
||||
if (text.startsWith(prefix)) {
|
||||
return text.slice(prefix.length);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
const MobileMessagePageInner = (props: { displayPageHeader?: boolean }) => {
|
||||
const app = useApp();
|
||||
const basename = app.router.basename.replace(/\/+$/, '');
|
||||
const { t } = useLocalTranslation();
|
||||
const navigate = useNavigate();
|
||||
const ctx = useCurrentUserContext();
|
||||
@ -61,7 +69,7 @@ const MobileMessagePageInner = (props: { displayPageHeader?: boolean }) => {
|
||||
if (url) {
|
||||
if (url.startsWith('/m/')) navigate(url.substring(2));
|
||||
else if (url.startsWith('/')) {
|
||||
navigate(url);
|
||||
navigate(removeStringIfStartsWith(url, basename));
|
||||
inboxVisible.value = false;
|
||||
} else {
|
||||
window.location.href = url;
|
||||
|
@ -44,6 +44,7 @@
|
||||
"@nocobase/plugin-gantt": "1.7.0-alpha.10",
|
||||
"@nocobase/plugin-graph-collection-manager": "1.7.0-alpha.10",
|
||||
"@nocobase/plugin-kanban": "1.7.0-alpha.10",
|
||||
"@nocobase/plugin-locale-tester": "1.7.0-alpha.10",
|
||||
"@nocobase/plugin-localization": "1.7.0-alpha.10",
|
||||
"@nocobase/plugin-logger": "1.7.0-alpha.10",
|
||||
"@nocobase/plugin-map": "1.7.0-alpha.10",
|
||||
|
Loading…
x
Reference in New Issue
Block a user