Merge branch 'next' into feat/table-action-linkage-rule

This commit is contained in:
katherinehhh 2025-03-31 11:32:01 +08:00
commit f224cb94fb
138 changed files with 1661 additions and 559 deletions

View File

@ -5,6 +5,43 @@ 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/) 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). 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
- **[Block: Multi-step form]**
- the submit button has the same color in its default and highlighted by @jiannx
- fixed the bug that form reset is invalid when the field is associated with other field by @jiannx
- **[Workflow: Approval]** Fix approval form values to submit by @mytharcher
## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27 ## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27
### 🚀 Improvements ### 🚀 Improvements

View File

@ -5,6 +5,43 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 并且本项目遵循 [语义化版本](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
### 🐛 修复
- **[区块:分步表单]**
- 提交按钮默认和高亮情况下颜色一样 by @jiannx
- 修复当字段与其他表单字段存在关联时,表单重置无效 by @jiannx
- **[工作流:审批]** 修复审批表单提交值的问题 by @mytharcher
## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27 ## [v1.6.11](https://github.com/nocobase/nocobase/compare/v1.6.10...v1.6.11) - 2025-03-27
### 🚀 优化 ### 🚀 优化

View File

@ -1,5 +1,5 @@
{ {
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"], "npmClientArgs": ["--ignore-engines"],

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/acl", "name": "@nocobase/acl",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/resourcer": "1.7.0-beta.9", "@nocobase/resourcer": "1.7.0-beta.12",
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"minimatch": "^5.1.1" "minimatch": "^5.1.1"
}, },
"repository": { "repository": {

View File

@ -1,14 +1,14 @@
{ {
"name": "@nocobase/actions", "name": "@nocobase/actions",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/cache": "1.7.0-beta.9", "@nocobase/cache": "1.7.0-beta.12",
"@nocobase/database": "1.7.0-beta.9", "@nocobase/database": "1.7.0-beta.12",
"@nocobase/resourcer": "1.7.0-beta.9" "@nocobase/resourcer": "1.7.0-beta.12"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,17 +1,17 @@
{ {
"name": "@nocobase/app", "name": "@nocobase/app",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/database": "1.7.0-beta.9", "@nocobase/database": "1.7.0-beta.12",
"@nocobase/preset-nocobase": "1.7.0-beta.9", "@nocobase/preset-nocobase": "1.7.0-beta.12",
"@nocobase/server": "1.7.0-beta.9" "@nocobase/server": "1.7.0-beta.12"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/client": "1.7.0-beta.9" "@nocobase/client": "1.7.0-beta.12"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/auth", "name": "@nocobase/auth",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "1.7.0-beta.9", "@nocobase/actions": "1.7.0-beta.12",
"@nocobase/cache": "1.7.0-beta.9", "@nocobase/cache": "1.7.0-beta.12",
"@nocobase/database": "1.7.0-beta.9", "@nocobase/database": "1.7.0-beta.12",
"@nocobase/resourcer": "1.7.0-beta.9", "@nocobase/resourcer": "1.7.0-beta.12",
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^8.5.1"
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/build", "name": "@nocobase/build",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "Library build tool based on rollup.", "description": "Library build tool based on rollup.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",

View File

@ -1,12 +1,12 @@
{ {
"name": "@nocobase/cache", "name": "@nocobase/cache",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/lock-manager": "1.7.0-beta.9", "@nocobase/lock-manager": "1.7.0-beta.12",
"bloom-filters": "^3.0.1", "bloom-filters": "^3.0.1",
"cache-manager": "^5.2.4", "cache-manager": "^5.2.4",
"cache-manager-redis-yet": "^4.1.2" "cache-manager-redis-yet": "^4.1.2"

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cli", "name": "@nocobase/cli",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./src/index.js", "main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js" "nocobase": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"@nocobase/app": "1.7.0-beta.9", "@nocobase/app": "1.7.0-beta.12",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20", "@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
@ -25,7 +25,7 @@
"tsx": "^4.19.0" "tsx": "^4.19.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "1.7.0-beta.9" "@nocobase/devtools": "1.7.0-beta.12"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/client", "name": "@nocobase/client",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "lib/index.js", "main": "lib/index.js",
"module": "es/index.mjs", "module": "es/index.mjs",
@ -27,9 +27,9 @@
"@formily/reactive-react": "^2.2.27", "@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27", "@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27", "@formily/validator": "^2.2.27",
"@nocobase/evaluators": "1.7.0-beta.9", "@nocobase/evaluators": "1.7.0-beta.12",
"@nocobase/sdk": "1.7.0-beta.9", "@nocobase/sdk": "1.7.0-beta.12",
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"ahooks": "^3.7.2", "ahooks": "^3.7.2",
"antd": "5.24.2", "antd": "5.24.2",
"antd-style": "3.7.1", "antd-style": "3.7.1",

View File

@ -18,7 +18,14 @@ export interface SelectWithTitleProps {
onChange?: (...args: any[]) => void; 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 [open, setOpen] = useState(false);
const timerRef = useRef<any>(null); const timerRef = useRef<any>(null);
return ( return (
@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN
> >
{title} {title}
<Select <Select
{...others}
open={open} open={open}
data-testid={`select-${title}`} data-testid={`select-${title}`}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}

View File

@ -563,6 +563,7 @@ const RenderButtonInner = observer(
designerProps, designerProps,
title, title,
isLink, isLink,
onlyIcon,
...others ...others
} = props; } = props;
const debouncedClick = useCallback( const debouncedClick = useCallback(
@ -606,7 +607,7 @@ const RenderButtonInner = observer(
type={type === 'danger' ? undefined : type} type={type === 'danger' ? undefined : type}
title={actionTitle} title={actionTitle}
> >
{actionTitle && ( {!onlyIcon && actionTitle && (
<span className={icon ? 'nb-action-title' : null} style={linkStyle}> <span className={icon ? 'nb-action-title' : null} style={linkStyle}>
{actionTitle} {actionTitle}
</span> </span>

View File

@ -69,6 +69,11 @@ export const filterAnalyses = (filters): any[] => {
return results; return results;
}; };
function getFieldPath(str) {
const lastIndex = str.lastIndexOf('.');
return lastIndex === -1 ? str : str.slice(0, lastIndex);
}
const InternalAssociationSelect = observer( const InternalAssociationSelect = observer(
(props: AssociationSelectProps) => { (props: AssociationSelectProps) => {
const { objectValue = true, addMode: propsAddMode, ...rest } = props; const { objectValue = true, addMode: propsAddMode, ...rest } = props;
@ -100,11 +105,14 @@ const InternalAssociationSelect = observer(
//支持深层次子表单 //支持深层次子表单
onFieldInputValueChange('*', (fieldPath: any) => { onFieldInputValueChange('*', (fieldPath: any) => {
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter) || []; const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter) || [];
const linageFieldEntire = getFieldPath(fieldPath.address.entire);
const targetFieldEntire = getFieldPath(field.address.entire);
if ( if (
linkageFields.includes(fieldPath?.props?.name) && linkageFields.includes(fieldPath?.props?.name) &&
field.value && field.value &&
isEqual(fieldPath?.indexes, field?.indexes) && isEqual(fieldPath?.indexes, field?.indexes) &&
fieldPath?.props?.name !== field.props.name fieldPath?.props?.name !== field.props.name &&
(!field?.indexes?.length || isEqual(linageFieldEntire, targetFieldEntire))
) { ) {
field.setValue(null); field.setValue(null);
setInnerValue(null); setInnerValue(null);

View File

@ -13,6 +13,8 @@ import { FormProvider, connect, createSchemaField, observer, useField, useFieldS
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd'; import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAPIClient, useCollectionManager_deprecated } from '../../../'; import { useAPIClient, useCollectionManager_deprecated } from '../../../';
@ -152,7 +154,11 @@ const CascadeSelect = connect((props) => {
} else { } else {
associationField.value = option; associationField.value = option;
} }
if (options.length === 1 && !options[0].value) {
onChange?.(null);
} else {
onChange?.(options); onChange?.(options);
}
}; };
const onDropdownVisibleChange = async (visible, selectedValue, index) => { const onDropdownVisibleChange = async (visible, selectedValue, index) => {
@ -238,17 +244,17 @@ export const InternalCascadeSelect = observer(
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { loading, data: formData } = useDataBlockRequest() || {}; const { loading, data: formData } = useDataBlockRequest() || {};
const initialValue = formData?.data?.[fieldSchema.name]; const initialValue = formData?.data?.[fieldSchema.name];
useEffect(() => {
const id = uid(); const handleFormValuesChange = debounce((form) => {
selectForm.addEffects(id, () => {
onFormValuesChange((form) => {
if (collectionField.interface === 'm2o') { if (collectionField.interface === 'm2o') {
// 对 m2o 类型字段,提取最后一个非 null 值
const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]); const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]);
setTimeout(() => { setTimeout(() => {
form.setValuesIn(fieldSchema.name, value); form.setValuesIn(fieldSchema.name, value);
field.value = value; field.value = value;
}); });
} else { } else {
// 对 select_array 类型字段,过滤掉空对象
const value = extractLastNonNullValueObjects(form.values?.select_array).filter( const value = extractLastNonNullValueObjects(form.values?.select_array).filter(
(v) => v && Object.keys(v).length > 0, (v) => v && Object.keys(v).length > 0,
); );
@ -256,10 +262,20 @@ export const InternalCascadeSelect = observer(
field.value = value; field.value = value;
}); });
} }
}, 300);
useEffect(() => {
const id = uid();
selectForm.addEffects(id, () => {
onFormValuesChange((form) => {
handleFormValuesChange(form);
}); });
}); });
return () => { return () => {
selectForm.removeEffects(id); selectForm.removeEffects(id);
// 清除防抖定时器
handleFormValuesChange.cancel();
}; };
}, []); }, []);
@ -282,6 +298,24 @@ export const InternalCascadeSelect = observer(
items: { items: {
type: 'void', type: 'void',
'x-component': 'Space', 'x-component': 'Space',
'x-component-props': {
style: {
width: '100%',
display: 'flex',
},
className: css`
.ant-formily-item-control {
max-width: 100% !important;
}
.ant-space-item:nth-child(1) {
flex: 0.1;
}
.ant-space-item:nth-child(2) {
flex: 3;
}
`,
},
properties: { properties: {
sort: { sort: {
type: 'void', type: 'void',

View File

@ -20,7 +20,6 @@ import { FilterBlockProvider } from '../../../filter-provider/FilterProvider';
import { import {
NocoBaseRecursionField, NocoBaseRecursionField,
RefreshComponentProvider, RefreshComponentProvider,
useRefreshComponent,
useRefreshFieldSchema, useRefreshFieldSchema,
} from '../../../formily/NocoBaseRecursionField'; } from '../../../formily/NocoBaseRecursionField';
import { DndContext, DndContextProps } from '../../common/dnd-context'; import { DndContext, DndContextProps } from '../../common/dnd-context';
@ -379,11 +378,9 @@ export const Grid: any = observer(
}, [fieldSchema, render, InitializerComponent, showDivider]); }, [fieldSchema, render, InitializerComponent, showDivider]);
const refreshFieldSchema = useRefreshFieldSchema(); const refreshFieldSchema = useRefreshFieldSchema();
const refreshComponent = useRefreshComponent();
const refresh = useCallback(() => { const refresh = useCallback(() => {
refreshFieldSchema?.(); refreshFieldSchema?.();
refreshComponent?.(); }, [refreshFieldSchema]);
}, [refreshComponent, refreshFieldSchema]);
return ( return (
<RefreshComponentProvider refresh={refresh}> <RefreshComponentProvider refresh={refresh}>

View File

@ -568,13 +568,14 @@ export interface SchemaSettingsSelectItemProps
extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>, extends Omit<SchemaSettingsItemProps, 'onChange' | 'onClick'>,
Omit<SelectWithTitleProps, 'title' | 'defaultValue'> { Omit<SelectWithTitleProps, 'title' | 'defaultValue'> {
value?: SelectWithTitleProps['defaultValue']; value?: SelectWithTitleProps['defaultValue'];
optionRender?: (option: any, info: { index: number }) => React.ReactNode;
} }
export const SchemaSettingsSelectItem: FC<SchemaSettingsSelectItemProps> = (props) => { export const SchemaSettingsSelectItem: FC<SchemaSettingsSelectItemProps> = (props) => {
const { title, options, value, onChange, ...others } = props; const { title, options, value, onChange, optionRender, ...others } = props;
return ( return (
<SchemaSettingsItem title={title} {...others}> <SchemaSettingsItem title={title} {...others}>
<SelectWithTitle {...{ title, defaultValue: value, onChange, options }} /> <SelectWithTitle {...{ title, defaultValue: value, onChange, options, optionRender }} />
</SchemaSettingsItem> </SchemaSettingsItem>
); );
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "create-nocobase-app", "name": "create-nocobase-app",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "src/index.js", "main": "src/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/data-source-manager", "name": "@nocobase/data-source-manager",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "1.7.0-beta.9", "@nocobase/actions": "1.7.0-beta.12",
"@nocobase/cache": "1.7.0-beta.9", "@nocobase/cache": "1.7.0-beta.12",
"@nocobase/database": "1.7.0-beta.9", "@nocobase/database": "1.7.0-beta.12",
"@nocobase/resourcer": "1.7.0-beta.9", "@nocobase/resourcer": "1.7.0-beta.12",
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^8.5.1"
}, },

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/database", "name": "@nocobase/database",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@nocobase/logger": "1.7.0-beta.9", "@nocobase/logger": "1.7.0-beta.12",
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"cron-parser": "4.4.0", "cron-parser": "4.4.0",

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/devtools", "name": "@nocobase/devtools",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./src/index.js", "main": "./src/index.js",
"dependencies": { "dependencies": {
"@nocobase/build": "1.7.0-beta.9", "@nocobase/build": "1.7.0-beta.12",
"@nocobase/client": "1.7.0-beta.9", "@nocobase/client": "1.7.0-beta.12",
"@nocobase/test": "1.7.0-beta.9", "@nocobase/test": "1.7.0-beta.12",
"@types/koa": "^2.15.0", "@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.4", "@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177", "@types/lodash": "^4.14.177",

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/evaluators", "name": "@nocobase/evaluators",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@formulajs/formulajs": "4.4.9", "@formulajs/formulajs": "4.4.9",
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"mathjs": "^10.6.0" "mathjs": "^10.6.0"
}, },
"repository": { "repository": {

View File

@ -1,10 +1,10 @@
{ {
"name": "@nocobase/lock-manager", "name": "@nocobase/lock-manager",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "lib/index.js", "main": "lib/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"async-mutex": "^0.5.0" "async-mutex": "^0.5.0"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/logger", "name": "@nocobase/logger",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "nocobase logging library", "description": "nocobase logging library",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",

View File

@ -1,12 +1,12 @@
{ {
"name": "@nocobase/resourcer", "name": "@nocobase/resourcer",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"koa-compose": "^4.1.0", "koa-compose": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/sdk", "name": "@nocobase/sdk",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/server", "name": "@nocobase/server",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",
@ -10,19 +10,19 @@
"@koa/cors": "^5.0.0", "@koa/cors": "^5.0.0",
"@koa/multer": "^3.0.2", "@koa/multer": "^3.0.2",
"@koa/router": "^9.4.0", "@koa/router": "^9.4.0",
"@nocobase/acl": "1.7.0-beta.9", "@nocobase/acl": "1.7.0-beta.12",
"@nocobase/actions": "1.7.0-beta.9", "@nocobase/actions": "1.7.0-beta.12",
"@nocobase/auth": "1.7.0-beta.9", "@nocobase/auth": "1.7.0-beta.12",
"@nocobase/cache": "1.7.0-beta.9", "@nocobase/cache": "1.7.0-beta.12",
"@nocobase/data-source-manager": "1.7.0-beta.9", "@nocobase/data-source-manager": "1.7.0-beta.12",
"@nocobase/database": "1.7.0-beta.9", "@nocobase/database": "1.7.0-beta.12",
"@nocobase/evaluators": "1.7.0-beta.9", "@nocobase/evaluators": "1.7.0-beta.12",
"@nocobase/lock-manager": "1.7.0-beta.9", "@nocobase/lock-manager": "1.7.0-beta.12",
"@nocobase/logger": "1.7.0-beta.9", "@nocobase/logger": "1.7.0-beta.12",
"@nocobase/resourcer": "1.7.0-beta.9", "@nocobase/resourcer": "1.7.0-beta.12",
"@nocobase/sdk": "1.7.0-beta.9", "@nocobase/sdk": "1.7.0-beta.12",
"@nocobase/telemetry": "1.7.0-beta.9", "@nocobase/telemetry": "1.7.0-beta.12",
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"@types/decompress": "4.2.7", "@types/decompress": "4.2.7",
"@types/ini": "^1.3.31", "@types/ini": "^1.3.31",
"@types/koa-send": "^4.1.3", "@types/koa-send": "^4.1.3",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/telemetry", "name": "@nocobase/telemetry",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"description": "nocobase telemetry library", "description": "nocobase telemetry library",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
@ -11,7 +11,7 @@
"directory": "packages/telemetry" "directory": "packages/telemetry"
}, },
"dependencies": { "dependencies": {
"@nocobase/utils": "1.7.0-beta.9", "@nocobase/utils": "1.7.0-beta.12",
"@opentelemetry/api": "^1.7.0", "@opentelemetry/api": "^1.7.0",
"@opentelemetry/instrumentation": "^0.46.0", "@opentelemetry/instrumentation": "^0.46.0",
"@opentelemetry/resources": "^1.19.0", "@opentelemetry/resources": "^1.19.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/test", "name": "@nocobase/test",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "lib/index.js", "main": "lib/index.js",
"module": "./src/index.ts", "module": "./src/index.ts",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -51,7 +51,7 @@
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "8.1.0", "@faker-js/faker": "8.1.0",
"@nocobase/server": "1.7.0-beta.9", "@nocobase/server": "1.7.0-beta.12",
"@playwright/test": "^1.45.3", "@playwright/test": "^1.45.3",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/utils", "name": "@nocobase/utils",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "AGPL-3.0", "license": "AGPL-3.0",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "权限控制", "displayName.zh-CN": "权限控制",
"description": "Based on roles, resources, and actions, access control can precisely manage interface configuration permissions, data operation permissions, menu access permissions, and plugin permissions.", "description": "Based on roles, resources, and actions, access control can precisely manage interface configuration permissions, data operation permissions, menu access permissions, and plugin permissions.",
"description.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。", "description.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/acl", "homepage": "https://docs.nocobase.com/handbook/acl",

View File

@ -51,7 +51,11 @@ describe('union role: full permissions', async () => {
roles: [role1.name, role2.name], roles: [role1.name, role2.name],
}, },
}); });
await rootAgent.resource('roles').setSystemRoleMode({
values: {
roleMode: SystemRoleMode.allowUseUnion,
},
});
agent = await app.agent().login(user, UNION_ROLE_KEY); 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); const rootAgent = await app.agent().login(rootUser);
let rolesResponse = await agent.resource('roles').check(); let rolesResponse = await agent.resource('roles').check();
expect(rolesResponse.status).toBe(200); 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({ await rootAgent.resource('roles').setSystemRoleMode({
values: { values: {
roleMode: SystemRoleMode.allowUseUnion, roleMode: SystemRoleMode.default,
}, },
}); });
rolesResponse = await agent.resource('roles').check(); rolesResponse = await agent.resource('roles').check();
expect(rolesResponse.status).toBe(200); 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 () => { 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.statusCode).toBe(200);
expect(getRolesResponse.body.meta.allowedActions.update.length).toBe(0); 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);
});
}); });

View File

@ -14,7 +14,7 @@ import { UNION_ROLE_KEY } from '../constants';
import { SystemRoleMode } from '../enum'; import { SystemRoleMode } from '../enum';
export async function setCurrentRole(ctx: Context, next) { export async function setCurrentRole(ctx: Context, next) {
const currentRole = ctx.get('X-Role'); let currentRole = ctx.get('X-Role');
if (currentRole === 'anonymous') { if (currentRole === 'anonymous') {
ctx.state.currentRole = currentRole; ctx.state.currentRole = currentRole;
@ -49,7 +49,8 @@ export async function setCurrentRole(ctx: Context, next) {
ctx.state.currentUser.roles = userRoles; ctx.state.currentUser.roles = userRoles;
const systemSettings = await ctx.db.getRepository('systemSettings').findOne(); const systemSettings = await ctx.db.getRepository('systemSettings').findOne();
const roleMode = systemSettings?.get('roleMode') || SystemRoleMode.default; 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.state.currentRole = userRoles[0].name;
ctx.headers['x-role'] = userRoles[0].name; ctx.headers['x-role'] = userRoles[0].name;
} else if (roleMode === SystemRoleMode.onlyUseUnion) { } 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 // 2. If the X-Role is not set, or the X-Role does not belong to the user, use the default role
if (!role) { if (!role) {
const defaultRole = userRoles.find((role) => role?.rolesUsers?.default); 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.currentRole = role;
ctx.state.currentRoles = [role]; ctx.state.currentRoles = [role];

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-bulk-edit", "name": "@nocobase/plugin-action-bulk-edit",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-bulk-edit", "homepage": "https://docs.nocobase.com/handbook/action-bulk-edit",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-edit", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-edit",

View File

@ -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();
});
});

View File

@ -1243,3 +1243,388 @@ export const theAddBlockButtonInDrawerShouldBeVisible = {
'x-index': 1, '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,
},
},
},
};

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-bulk-update", "name": "@nocobase/plugin-action-bulk-update",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-bulk-update", "homepage": "https://docs.nocobase.com/handbook/action-bulk-update",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-update", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-update",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-custom-request", "name": "@nocobase/plugin-action-custom-request",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-custom-request", "homepage": "https://docs.nocobase.com/handbook/action-custom-request",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-custom-request", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-custom-request",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-duplicate", "name": "@nocobase/plugin-action-duplicate",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-duplicate", "homepage": "https://docs.nocobase.com/handbook/action-duplicate",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-duplicate", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-duplicate",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "操作:导出记录", "displayName.zh-CN": "操作:导出记录",
"description": "Export filtered records to excel, you can configure which fields to export.", "description": "Export filtered records to excel, you can configure which fields to export.",
"description.zh-CN": "导出筛选后的记录到 Excel 中,可以配置导出哪些字段。", "description.zh-CN": "导出筛选后的记录到 Excel 中,可以配置导出哪些字段。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-export", "homepage": "https://docs.nocobase.com/handbook/action-export",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "操作:导入记录", "displayName.zh-CN": "操作:导入记录",
"description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.", "description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.",
"description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。", "description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-import", "homepage": "https://docs.nocobase.com/handbook/action-import",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-action-print", "name": "@nocobase/plugin-action-print",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/action-print", "homepage": "https://docs.nocobase.com/handbook/action-print",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-print", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-print",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "AI 集成", "displayName.zh-CN": "AI 集成",
"description": "Support integration with AI services, providing AI-related workflow nodes to enhance business processing capabilities.", "description": "Support integration with AI services, providing AI-related workflow nodes to enhance business processing capabilities.",
"description.zh-CN": "支持接入 AI 服务,提供 AI 相关的工作流节点,增强业务处理能力。", "description.zh-CN": "支持接入 AI 服务,提供 AI 相关的工作流节点,增强业务处理能力。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"peerDependencies": { "peerDependencies": {
"@nocobase/client": "1.x", "@nocobase/client": "1.x",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-api-doc", "name": "@nocobase/plugin-api-doc",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "API documentation", "displayName": "API documentation",
"displayName.zh-CN": "API 文档", "displayName.zh-CN": "API 文档",
"description": "An OpenAPI documentation generator for NocoBase HTTP API.", "description": "An OpenAPI documentation generator for NocoBase HTTP API.",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "认证API 密钥", "displayName.zh-CN": "认证API 密钥",
"description": "Allows users to use API key to access application's HTTP API", "description": "Allows users to use API key to access application's HTTP API",
"description.zh-CN": "允许用户使用 API 密钥访问应用的 HTTP API", "description.zh-CN": "允许用户使用 API 密钥访问应用的 HTTP API",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/api-keys", "homepage": "https://docs.nocobase.com/handbook/api-keys",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "异步任务管理器", "displayName.zh-CN": "异步任务管理器",
"description": "Manage and monitor asynchronous tasks such as data import/export. Support task progress tracking and notification.", "description": "Manage and monitor asynchronous tasks such as data import/export. Support task progress tracking and notification.",
"description.zh-CN": "管理和监控数据导入导出等异步任务。支持任务进度跟踪和通知。", "description.zh-CN": "管理和监控数据导入导出等异步任务。支持任务进度跟踪和通知。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"peerDependencies": { "peerDependencies": {
"@nocobase/client": "1.x", "@nocobase/client": "1.x",

View File

@ -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 { 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) => { 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 ( return (
<AsyncTaskContext.Provider value={contextValue}>
<PinnedPluginListProvider <PinnedPluginListProvider
items={ items={{
tasks.length > 0
? {
asyncTasks: { order: 300, component: 'AsyncTasks', pin: true, snippet: '*' }, asyncTasks: { order: 300, component: 'AsyncTasks', pin: true, snippet: '*' },
} }}
: {}
}
> >
<SchemaComponentOptions components={{ AsyncTasks }}>{props.children}</SchemaComponentOptions> <SchemaComponentOptions components={{ AsyncTasks }}>{props.children}</SchemaComponentOptions>
</PinnedPluginListProvider> </PinnedPluginListProvider>
</AsyncTaskContext.Provider>
); );
}; };

View File

@ -1,13 +1,31 @@
import React, { useEffect } from 'react'; /**
import { Button, Popover, Table, Tag, Progress, Space, Tooltip, Popconfirm, Modal, Empty } from 'antd'; * This file is part of the NocoBase (R) project.
import { createStyles, Icon, useApp, usePlugin } from '@nocobase/client'; * 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 from 'dayjs';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import { useT } from '../locale'; import { useT } from '../locale';
import { useAsyncTask } from '../AsyncTaskManagerProvider';
import { useCurrentAppInfo } from '@nocobase/client';
const useStyles = createStyles(({ token }) => { const useStyles = createStyles(({ token }) => {
return { return {
button: { button: {
@ -34,93 +52,27 @@ const renderTaskResult = (status, t) => {
); );
}; };
export const AsyncTasks = () => { const useAsyncTask = () => {
const { const { data, refreshAsync, loading } = useRequest<any>({
tasks, url: 'asyncTasks:list',
popoverVisible, });
setPopoverVisible, return { loading, tasks: data?.data || [], refresh: refreshAsync };
hasProcessingTasks, };
cancellingTasks,
modalVisible,
setModalVisible,
currentError,
setCurrentError,
resultModalVisible,
setResultModalVisible,
currentTask,
setCurrentTask,
handleCancelTask,
} = useAsyncTask();
const plugin = usePlugin<any>('async-task-manager'); const AsyncTasksButton = (props) => {
const { popoverVisible, setPopoverVisible, tasks, refresh, loading, hasProcessingTasks } = props;
const app = useApp(); const app = useApp();
const api = useAPIClient();
const appInfo = useCurrentAppInfo(); const appInfo = useCurrentAppInfo();
const t = useT(); const t = useT();
const { styles } = useStyles(); const { styles } = useStyles();
const plugin = usePlugin<any>('async-task-manager');
useEffect(() => { const cm = useCollectionManager();
const handleClickOutside = (event: MouseEvent) => { const compile = useCompile();
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 showTaskResult = (task) => { const showTaskResult = (task) => {
setCurrentTask(task);
setResultModalVisible(true);
setPopoverVisible(false); 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 = [ const columns = [
{ {
title: t('Created at'), title: t('Created at'),
@ -140,7 +92,7 @@ export const AsyncTasks = () => {
if (!title) { if (!title) {
return '-'; return '-';
} }
const collection = cm.getCollection(title.collection);
const actionTypeMap = { const actionTypeMap = {
export: t('Export'), export: t('Export'),
import: t('Import'), import: t('Import'),
@ -156,7 +108,7 @@ export const AsyncTasks = () => {
}; };
const taskTemplate = taskTypeMap[title.actionType] || `${actionText}`; 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, width: 180,
render: (_, record: any) => { render: (_, record: any) => {
const actions = []; const actions = [];
const isTaskCancelling = cancellingTasks.has(record.taskId); const isTaskCancelling = false;
if ((record.status.type === 'running' || record.status.type === 'pending') && record.cancelable) { if ((record.status.type === 'running' || record.status.type === 'pending') && record.cancelable) {
actions.push( actions.push(
@ -282,7 +234,15 @@ export const AsyncTasks = () => {
key="cancel" key="cancel"
title={t('Confirm cancel')} title={t('Confirm cancel')}
description={t('Confirm cancel description')} 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')} okText={t('Confirm')}
cancelText={t('Cancel')} cancelText={t('Cancel')}
disabled={isTaskCancelling} disabled={isTaskCancelling}
@ -309,8 +269,16 @@ export const AsyncTasks = () => {
icon={<Icon type="DownloadOutlined" />} icon={<Icon type="DownloadOutlined" />}
onClick={() => { onClick={() => {
const token = app.apiClient.auth.token; 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( 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); window.open(url);
}} }}
@ -325,7 +293,19 @@ export const AsyncTasks = () => {
type="link" type="link"
size="small" size="small"
icon={<Icon type="EyeOutlined" />} 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')} {t('View result')}
</Button>, </Button>,
@ -341,9 +321,22 @@ export const AsyncTasks = () => {
size="small" size="small"
icon={<Icon type="ExclamationCircleOutlined" />} icon={<Icon type="ExclamationCircleOutlined" />}
onClick={() => { onClick={() => {
setCurrentError(record.status.errors);
setModalVisible(true);
setPopoverVisible(false); 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')} {t('Error details')}
@ -357,9 +350,9 @@ export const AsyncTasks = () => {
]; ];
const content = ( 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 ? ( {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' }}> <div style={{ padding: '24px 0', display: 'flex', justifyContent: 'center' }}>
<Empty description={t('No tasks')} image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty description={t('No tasks')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
@ -383,30 +376,53 @@ export const AsyncTasks = () => {
onClick={() => setPopoverVisible(!popoverVisible)} onClick={() => setPopoverVisible(!popoverVisible)}
/> />
</Popover> </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 }} />
)
);
};

View File

@ -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 { 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 { 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 { export class PluginAsyncExportServer extends Plugin {
private progressThrottles: Map<string, Function> = new Map(); 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.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) { getThrottledProgressEmitter(taskId: string, userId: string) {

View File

@ -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 fs from 'fs';
import _ from 'lodash';
import { basename } from 'path'; import { basename } from 'path';
export default { export default {
name: 'asyncTasks', name: 'asyncTasks',
actions: { 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) { async get(ctx, next) {
const { filterByTk } = ctx.action.params; const { filterByTk } = ctx.action.params;
const taskManager = ctx.app.container.get('AsyncTaskManager'); const taskManager = ctx.app.container.get('AsyncTaskManager');
@ -11,8 +29,29 @@ export default {
ctx.body = taskStatus; ctx.body = taskStatus;
await next(); await next();
}, },
async fetchFile(ctx, next) { async cancel(ctx, next) {
const { filterByTk } = ctx.action.params; 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 taskManager = ctx.app.container.get('AsyncTaskManager');
const taskStatus = await taskManager.getTaskStatus(filterByTk); const taskStatus = await taskManager.getTaskStatus(filterByTk);
// throw error if task is not success // throw error if task is not success
@ -28,10 +67,12 @@ export default {
// send file to client // send file to client
ctx.body = fs.createReadStream(filePath); ctx.body = fs.createReadStream(filePath);
// 处理文件名
let finalFileName = filename ? filename : basename(filePath);
finalFileName = encodeURIComponent(finalFileName); // 避免中文或特殊字符问题
ctx.set({ ctx.set({
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename=${basename(filePath)}`, 'Content-Disposition': `attachment; filename=${finalFileName}`,
}); });
await next(); await next();

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-audit-logs", "name": "@nocobase/plugin-audit-logs",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "Audit logs (deprecated)", "displayName": "Audit logs (deprecated)",
"displayName.zh-CN": "审计日志(废弃)", "displayName.zh-CN": "审计日志(废弃)",
"description": "This plugin is deprecated. There will be a new audit log plugin in the future.", "description": "This plugin is deprecated. There will be a new audit log plugin in the future.",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "认证:短信", "displayName.zh-CN": "认证:短信",
"description": "SMS authentication.", "description": "SMS authentication.",
"description.zh-CN": "通过短信验证码认证身份。", "description.zh-CN": "通过短信验证码认证身份。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/auth-sms", "homepage": "https://docs.nocobase.com/handbook/auth-sms",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth-sms", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth-sms",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-auth", "name": "@nocobase/plugin-auth",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/auth", "homepage": "https://docs.nocobase.com/handbook/auth",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "应用的备份与还原(废弃)", "displayName.zh-CN": "应用的备份与还原(废弃)",
"description": "Backup and restore applications for scenarios such as application replication, migration, and upgrades.", "description": "Backup and restore applications for scenarios such as application replication, migration, and upgrades.",
"description.zh-CN": "备份和还原应用,可用于应用的复制、迁移、升级等场景。", "description.zh-CN": "备份和还原应用,可用于应用的复制、迁移、升级等场景。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/backup-restore", "homepage": "https://docs.nocobase.com/handbook/backup-restore",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "区块iframe", "displayName.zh-CN": "区块iframe",
"description": "Create an iframe block on the page to embed and display external web pages or content.", "description": "Create an iframe block on the page to embed and display external web pages or content.",
"description.zh-CN": "在页面上创建和管理iframe用于嵌入和展示外部网页或内容。", "description.zh-CN": "在页面上创建和管理iframe用于嵌入和展示外部网页或内容。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-iframe", "homepage": "https://docs.nocobase.com/handbook/block-iframe",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "区块:模板", "displayName.zh-CN": "区块:模板",
"description": "Create and manage block templates for reuse on pages.", "description": "Create and manage block templates for reuse on pages.",
"description.zh-CN": "创建和管理区块模板,用于在页面中重复使用。", "description.zh-CN": "创建和管理区块模板,用于在页面中重复使用。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-template", "homepage": "https://docs.nocobase.com/handbook/block-template",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-block-workbench", "name": "@nocobase/plugin-block-workbench",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "Block: Action panel", "displayName": "Block: Action panel",
"displayName.zh-CN": "区块:操作面板", "displayName.zh-CN": "区块:操作面板",
"description": "Centrally manages and displays various actions, allowing users to efficiently perform tasks. It supports extensibility, with current action types including pop-ups, links, scanning, and custom requests.", "description": "Centrally manages and displays various actions, allowing users to efficiently perform tasks. It supports extensibility, with current action types including pop-ups, links, scanning, and custom requests.",

View File

@ -9,13 +9,13 @@
import { FileImageOutlined, LeftOutlined } from '@ant-design/icons'; import { FileImageOutlined, LeftOutlined } from '@ant-design/icons';
import { useActionContext } from '@nocobase/client'; import { useActionContext } from '@nocobase/client';
import { Html5Qrcode } from 'html5-qrcode'; 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 { useTranslation } from 'react-i18next';
import { ScanBox } from './ScanBox'; import { ScanBox } from './ScanBox';
import { useScanner } from './useScanner'; import { useScanner } from './useScanner';
const qrcodeEleId = 'qrcode'; const qrcodeEleId = 'qrcode';
export const QRCodeScannerInner = (props) => { export const QRCodeScannerInner = ({ setVisible }) => {
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
const imgUploaderRef = useRef<HTMLInputElement>(); const imgUploaderRef = useRef<HTMLInputElement>();
const { t } = useTranslation('block-workbench'); 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 vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const onScanSuccess = useCallback(
(text) => {
setVisible(false);
},
[setVisible],
);
const { startScanFile } = useScanner({ const { startScanFile } = useScanner({
onScannerSizeChanged: setOriginVideoSize, onScannerSizeChanged: setOriginVideoSize,
elementId: qrcodeEleId, elementId: qrcodeEleId,
onScanSuccess,
}); });
const getBoxStyle = (): React.CSSProperties => { const getBoxStyle = (): React.CSSProperties => {
@ -174,7 +182,7 @@ export const QRCodeScanner = (props) => {
return visible && cameraAvaliable ? ( return visible && cameraAvaliable ? (
<div style={style}> <div style={style}>
<QRCodeScannerInner /> <QRCodeScannerInner setVisible={setVisible} />
<LeftOutlined style={backIconStyle} onClick={() => setVisible(false)} /> <LeftOutlined style={backIconStyle} onClick={() => setVisible(false)} />
<div style={titleStyle}>{t('Scan QR code')}</div> <div style={titleStyle}>{t('Scan QR code')}</div>
</div> </div>

View File

@ -20,7 +20,7 @@ function removeStringIfStartsWith(text: string, prefix: string): string {
return text; return text;
} }
export function useScanner({ onScannerSizeChanged, elementId }) { export function useScanner({ onScannerSizeChanged, elementId, onScanSuccess }) {
const app = useApp(); const app = useApp();
const mobileManager = app.pm.get(MobileManager); const mobileManager = app.pm.get(MobileManager);
const basename = mobileManager.mobileRouter.basename.replace(/\/+$/, ''); const basename = mobileManager.mobileRouter.basename.replace(/\/+$/, '');
@ -50,12 +50,17 @@ export function useScanner({ onScannerSizeChanged, elementId }) {
}, },
}, },
(text) => { (text) => {
if (text?.startsWith('http')) {
window.location.href = text;
return;
}
navigate(removeStringIfStartsWith(text, basename)); navigate(removeStringIfStartsWith(text, basename));
onScanSuccess && onScanSuccess(text);
}, },
undefined, undefined,
); );
}, },
[navigate, onScannerSizeChanged, viewPoint, basename], [navigate, onScannerSizeChanged, viewPoint, basename, onScanSuccess],
); );
const stopScanner = useCallback(async (scanner: Html5Qrcode) => { const stopScanner = useCallback(async (scanner: Html5Qrcode) => {
const state = scanner.getState(); const state = scanner.getState();
@ -69,13 +74,18 @@ export function useScanner({ onScannerSizeChanged, elementId }) {
await stopScanner(scanner); await stopScanner(scanner);
try { try {
const { decodedText } = await scanner.scanFileV2(file, false); const { decodedText } = await scanner.scanFileV2(file, false);
if (decodedText?.startsWith('http')) {
window.location.href = decodedText;
return;
}
navigate(removeStringIfStartsWith(decodedText, basename)); navigate(removeStringIfStartsWith(decodedText, basename));
onScanSuccess && onScanSuccess(decodedText);
} catch (error) { } catch (error) {
alert(t('QR code recognition failed, please scan again')); alert(t('QR code recognition failed, please scan again'));
startScanCamera(scanner); startScanCamera(scanner);
} }
}, },
[stopScanner, scanner, navigate, basename, t, startScanCamera], [stopScanner, scanner, navigate, basename, t, startScanCamera, onScanSuccess],
); );
useEffect(() => { useEffect(() => {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-calendar", "name": "@nocobase/plugin-calendar",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "Calendar", "displayName": "Calendar",
"displayName.zh-CN": "日历", "displayName.zh-CN": "日历",
"description": "Provides callendar collection template and block for managing date data, typically for date/time related information such as events, appointments, tasks, and so on.", "description": "Provides callendar collection template and block for managing date data, typically for date/time related information such as events, appointments, tasks, and so on.",

View File

@ -108,7 +108,7 @@ const useEvents = (
title: string; title: string;
}, },
date: Date, date: Date,
view: (typeof Weeks)[number], view: (typeof Weeks)[number] | any = 'month',
) => { ) => {
const parseExpression = useLazy<typeof import('cron-parser').parseExpression>( const parseExpression = useLazy<typeof import('cron-parser').parseExpression>(
() => import('cron-parser'), () => import('cron-parser'),
@ -132,8 +132,8 @@ const useEvents = (
const intervalTime = end.diff(start, 'millisecond', true); const intervalTime = end.diff(start, 'millisecond', true);
const dateM = dayjs(date); const dateM = dayjs(date);
const startDate = dateM.clone().startOf('month'); const startDate = dateM.clone().startOf(view);
const endDate = startDate.clone().endOf('month'); const endDate = startDate.clone().endOf(view);
/** /**
* view === month * view === month
@ -425,7 +425,6 @@ export const Calendar: any = withDynamicSchemaProps(
}; };
}; };
const BigCalendar = reactBigCalendar?.BigCalendar; const BigCalendar = reactBigCalendar?.BigCalendar;
return wrapSSR( return wrapSSR(
<div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}> <div className={`${hashId} ${containerClassName}`} style={{ height: height || 700 }}>
<PopupContextProvider visible={visible} setVisible={setVisible}> <PopupContextProvider visible={visible} setVisible={setVisible}>

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "图表(废弃)", "displayName.zh-CN": "图表(废弃)",
"description": "The plugin has been deprecated, please use the data visualization plugin instead.", "description": "The plugin has been deprecated, please use the data visualization plugin instead.",
"description.zh-CN": "已废弃插件,请使用数据可视化插件代替。", "description.zh-CN": "已废弃插件,请使用数据可视化插件代替。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "WEB 客户端", "displayName.zh-CN": "WEB 客户端",
"description": "Provides a client interface for the NocoBase server", "description": "Provides a client interface for the NocoBase server",
"description.zh-CN": "为 NocoBase 服务端提供客户端界面", "description.zh-CN": "为 NocoBase 服务端提供客户端界面",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"devDependencies": { "devDependencies": {

View File

@ -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); agent = await app.agent().login(user, UNION_ROLE_KEY);
}); });

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表: SQL", "displayName.zh-CN": "数据表: SQL",
"description": "Provides SQL collection template", "description": "Provides SQL collection template",
"description.zh-CN": "提供 SQL 数据表模板", "description.zh-CN": "提供 SQL 数据表模板",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"homepage": "https://docs-cn.nocobase.com/handbook/collection-sql", "homepage": "https://docs-cn.nocobase.com/handbook/collection-sql",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/collection-sql", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/collection-sql",
"main": "dist/server/index.js", "main": "dist/server/index.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-collection-tree", "name": "@nocobase/plugin-collection-tree",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "Collection: Tree", "displayName": "Collection: Tree",
"displayName.zh-CN": "数据表:树", "displayName.zh-CN": "数据表:树",
"description": "Provides tree collection template", "description": "Provides tree collection template",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据源:主数据库", "displayName.zh-CN": "数据源:主数据库",
"description": "NocoBase main database, supports relational databases such as PostgreSQL, MySQL, MariaDB and so on.", "description": "NocoBase main database, supports relational databases such as PostgreSQL, MySQL, MariaDB and so on.",
"description.zh-CN": "NocoBase 主数据库,支持 PostgreSQL、MySQL、MariaDB 等关系型数据库。", "description.zh-CN": "NocoBase 主数据库,支持 PostgreSQL、MySQL、MariaDB 等关系型数据库。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/data-source-main", "homepage": "https://docs.nocobase.com/handbook/data-source-main",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/data-source-main", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/data-source-main",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-data-source-manager", "name": "@nocobase/plugin-data-source-manager",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"displayName": "Data source manager", "displayName": "Data source manager",
"displayName.zh-CN": "数据源管理", "displayName.zh-CN": "数据源管理",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-data-visualization", "name": "@nocobase/plugin-data-visualization",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "Data visualization", "displayName": "Data visualization",
"displayName.zh-CN": "数据可视化", "displayName.zh-CN": "数据可视化",
"description": "Provides data visualization feature, including chart block and chart filter block, support line charts, area charts, bar charts and more than a dozen kinds of charts, you can also extend more chart types.", "description": "Provides data visualization feature, including chart block and chart filter block, support line charts, area charts, bar charts and more than a dozen kinds of charts, you can also extend more chart types.",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-disable-pm-add", "name": "@nocobase/plugin-disable-pm-add",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"peerDependencies": { "peerDependencies": {
"@nocobase/client": "1.x", "@nocobase/client": "1.x",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-environment-variables", "name": "@nocobase/plugin-environment-variables",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"peerDependencies": { "peerDependencies": {
"@nocobase/client": "1.x", "@nocobase/client": "1.x",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "错误处理器", "displayName.zh-CN": "错误处理器",
"description": "Handling application errors and exceptions.", "description": "Handling application errors and exceptions.",
"description.zh-CN": "处理应用程序中的错误和异常。", "description.zh-CN": "处理应用程序中的错误和异常。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"devDependencies": { "devDependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-field-china-region", "name": "@nocobase/plugin-field-china-region",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "Collection field: administrative divisions of China", "displayName": "Collection field: administrative divisions of China",
"displayName.zh-CN": "数据表字段:中国行政区划", "displayName.zh-CN": "数据表字段:中国行政区划",
"description": "Provides data and field type for administrative divisions of China.", "description": "Provides data and field type for administrative divisions of China.",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:公式", "displayName.zh-CN": "数据表字段:公式",
"description": "Configure and store the results of calculations between multiple field values in the same record, supporting both Math.js and Excel formula functions.", "description": "Configure and store the results of calculations between multiple field values in the same record, supporting both Math.js and Excel formula functions.",
"description.zh-CN": "可以配置并存储同一条记录的多字段值之间的计算结果,支持 Math.js 和 Excel formula functions 两种引擎", "description.zh-CN": "可以配置并存储同一条记录的多字段值之间的计算结果,支持 Math.js 和 Excel formula functions 两种引擎",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/field-formula", "homepage": "https://docs.nocobase.com/handbook/field-formula",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:多对多 (数组)", "displayName.zh-CN": "数据表字段:多对多 (数组)",
"description": "Allows to create many to many relationships between two models by storing an array of unique keys of the target model.", "description": "Allows to create many to many relationships between two models by storing an array of unique keys of the target model.",
"description.zh-CN": "支持通过在数组中存储目标表唯一键的方式建立多对多关系。", "description.zh-CN": "支持通过在数组中存储目标表唯一键的方式建立多对多关系。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"peerDependencies": { "peerDependencies": {
"@nocobase/client": "1.x", "@nocobase/client": "1.x",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段Markdown(Vditor)", "displayName.zh-CN": "数据表字段Markdown(Vditor)",
"description": "Used to store Markdown and render it using Vditor editor, supports common Markdown syntax such as list, code, quote, etc., and supports uploading images, recordings, etc.It also allows for instant rendering, where what you see is what you get.", "description": "Used to store Markdown and render it using Vditor editor, supports common Markdown syntax such as list, code, quote, etc., and supports uploading images, recordings, etc.It also allows for instant rendering, where what you see is what you get.",
"description.zh-CN": "用于存储 Markdown并使用 Vditor 编辑器渲染,支持常见 Markdown 语法,如列表,代码,引用等,并支持上传图片,录音等。同时可以做到即时渲染,所见即所得。", "description.zh-CN": "用于存储 Markdown并使用 Vditor 编辑器渲染,支持常见 Markdown 语法,如列表,代码,引用等,并支持上传图片,录音等。同时可以做到即时渲染,所见即所得。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/field-markdown-vditor", "homepage": "https://docs.nocobase.com/handbook/field-markdown-vditor",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "数据表字段:自动编码", "displayName.zh-CN": "数据表字段:自动编码",
"description": "Automatically generate codes based on configured rules, supporting combinations of dates, numbers, and text.", "description": "Automatically generate codes based on configured rules, supporting combinations of dates, numbers, and text.",
"description.zh-CN": "根据配置的规则自动生成编码,支持日期、数字、文本的组合。", "description.zh-CN": "根据配置的规则自动生成编码,支持日期、数字、文本的组合。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/field-sequence", "homepage": "https://docs.nocobase.com/handbook/field-sequence",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-field-sort", "name": "@nocobase/plugin-field-sort",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"displayName": "Collection field: Sort", "displayName": "Collection field: Sort",
"displayName.zh-CN": "数据表字段:排序", "displayName.zh-CN": "数据表字段:排序",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-file-manager", "name": "@nocobase/plugin-file-manager",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "File manager", "displayName": "File manager",
"displayName.zh-CN": "文件管理器", "displayName.zh-CN": "文件管理器",
"description": "Provides files storage services with files collection template and attachment field.", "description": "Provides files storage services with files collection template and attachment field.",

View File

@ -0,0 +1,50 @@
/**
* 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 { getApp } from '.';
import PluginFileManagerServer from '../server';
import { STORAGE_TYPE_LOCAL } from '../../constants';
import { getFileKey } from '../utils';
describe('file manager > utils', () => {
let app;
let agent;
let db;
let plugin: PluginFileManagerServer;
let StorageRepo;
let AttachmentRepo;
let local;
beforeEach(async () => {
app = await getApp();
agent = app.agent();
db = app.db;
plugin = app.pm.get(PluginFileManagerServer) as PluginFileManagerServer;
AttachmentRepo = db.getCollection('attachments').repository;
StorageRepo = db.getCollection('storages').repository;
local = await StorageRepo.findOne({
filter: {
type: STORAGE_TYPE_LOCAL,
},
});
});
afterEach(async () => {
await app.destroy();
});
describe('getFileKey', () => {
it('path as null should works', async () => {
expect(getFileKey({ path: null, filename: 'test.jpg' })).toBe('test.jpg');
});
});
});

View File

@ -28,7 +28,7 @@ export const cloudFilenameGetter = (storage) => (req, file, cb) => {
}; };
export function getFileKey(record) { export function getFileKey(record) {
return urlJoin(record.path, record.filename).replace(/^\//, ''); return urlJoin(record.path || '', record.filename).replace(/^\//, '');
} }
export function ensureUrlEncoded(value) { export function ensureUrlEncoded(value) {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-gantt", "name": "@nocobase/plugin-gantt",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"displayName": "Block: Gantt", "displayName": "Block: Gantt",
"displayName.zh-CN": "区块:甘特图", "displayName.zh-CN": "区块:甘特图",
"description": "Provides Gantt block.", "description": "Provides Gantt block.",

View File

@ -4,7 +4,7 @@
"displayName.zh-CN": "可视化数据表管理", "displayName.zh-CN": "可视化数据表管理",
"description": "An ER diagram-like tool. Currently only the Master database is supported.", "description": "An ER diagram-like tool. Currently only the Master database is supported.",
"description.zh-CN": "类似 ER 图的工具,目前只支持主数据库。", "description.zh-CN": "类似 ER 图的工具,目前只支持主数据库。",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./dist/server/index.js", "main": "./dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/graph-collection-manager", "homepage": "https://docs.nocobase.com/handbook/graph-collection-manager",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/plugin-kanban", "name": "@nocobase/plugin-kanban",
"version": "1.7.0-beta.9", "version": "1.7.0-beta.12",
"main": "dist/server/index.js", "main": "dist/server/index.js",
"homepage": "https://docs.nocobase.com/handbook/block-kanban", "homepage": "https://docs.nocobase.com/handbook/block-kanban",
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-kanban", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-kanban",

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @nocobase/plugin-locale-tester

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -0,0 +1,13 @@
{
"name": "@nocobase/plugin-locale-tester",
"displayName": "Locale tester",
"displayName.zh-CN": "翻译测试工具",
"version": "1.7.0-beta.12",
"homepage": "https://github.com/nocobase/locales",
"main": "dist/server/index.js",
"peerDependencies": {
"@nocobase/client": "1.x",
"@nocobase/server": "1.x",
"@nocobase/test": "1.x"
}
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View 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;
}

View File

@ -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;

View File

@ -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' })}}`;
}

View 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';

View File

@ -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."
}

View File

@ -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> 获取需要翻译的语言文件,粘贴到下方并进行翻译。"
}

View File

@ -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',
},
],
});

View File

@ -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';

Some files were not shown because too many files have changed in this diff Show More