Merge branch 'develop' into 2.0

This commit is contained in:
chenos 2025-07-01 07:16:28 +08:00 committed by GitHub
commit 407408b36b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 245 additions and 121 deletions

View File

@ -143,6 +143,17 @@ class Package {
});
console.log(chalk.greenBright(`Downloaded: ${this.packageName}@${version}`));
} catch (error) {
if (error.response.data && typeof error.response.data.pipe === 'function') {
let errorMessageBuffer = '';
error.response.data.on('data', (chunk) => {
errorMessageBuffer += chunk.toString('utf8'); // 收集错误信息
});
error.response.data.on('end', () => {
if (error.response.status === 403) {
console.error(chalk.redBright('You do not have permission to download this package version.'));
}
});
}
console.log(chalk.redBright(`Download failed: ${this.packageName}`));
}
}
@ -252,7 +263,13 @@ module.exports = (cli) => {
NOCOBASE_PKG_USERNAME,
NOCOBASE_PKG_PASSWORD,
} = process.env;
const { accessKeyId, accessKeySecret } = await getAccessKeyPair();
let accessKeyId;
let accessKeySecret;
try {
({ accessKeyId, accessKeySecret } = await getAccessKeyPair());
} catch (e) {
return;
}
if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD) && !(accessKeyId && accessKeySecret)) {
return;
}

View File

@ -19,7 +19,7 @@ const fs = require('fs-extra');
const os = require('os');
const moment = require('moment-timezone');
const { keyDecrypt, getEnvAsync } = require('@nocobase/license-kit');
const _ = require('lodash');
const omit = require('lodash/omit');
exports.isPackageValid = (pkg) => {
try {
@ -491,10 +491,20 @@ exports.generatePlugins = function () {
}
};
async function isEnvMatch(keyData) {
const env = await getEnvAsync();
if (env?.container?.id && keyData?.instanceData?.container?.id) {
return (
JSON.stringify(omit(env, ['timestamp', 'container', 'hostname'])) ===
JSON.stringify(omit(keyData?.instanceData, ['timestamp', 'container', 'hostname']))
);
}
return JSON.stringify(omit(env, ['timestamp'])) === JSON.stringify(omit(keyData?.instanceData, ['timestamp']));
}
exports.getAccessKeyPair = async function () {
const keyFile = resolve(process.cwd(), 'storage/.license/license-key');
if (!fs.existsSync(keyFile)) {
// showLicenseInfo(LicenseKeyError.notExist);
return {};
}
@ -505,13 +515,13 @@ exports.getAccessKeyPair = async function () {
keyData = JSON.parse(keyDataStr);
} catch (error) {
showLicenseInfo(LicenseKeyError.parseFailed);
return {};
throw new Error(LicenseKeyError.parseFailed.title);
}
const currentEnv = await getEnvAsync();
if (!_.isEqual(_.omit(keyData?.instanceData, ['timestamp']), _.omit(currentEnv, ['timestamp']))) {
const isEnvMatched = await isEnvMatch(keyData);
if (!isEnvMatched) {
showLicenseInfo(LicenseKeyError.notMatch);
return {};
throw new Error(LicenseKeyError.notMatch.title);
}
const { accessKeyId, accessKeySecret } = keyData;
@ -520,21 +530,21 @@ exports.getAccessKeyPair = async function () {
const LicenseKeyError = {
notExist: {
title: 'License key not exist',
title: 'License key not found',
content:
'Please go to the license settings page to obtain the Instance ID for the current environment, and then generate the license key on the service platform.',
},
parseFailed: {
title: 'License key parse failed',
title: 'Invalid license key format',
content: 'Please check your license key, or regenerate the license key on the service platform.',
},
notMatch: {
title: 'License key not matched',
title: 'License key mismatch',
content:
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
},
notValid: {
title: 'License key not valid',
title: 'Invalid license key',
content:
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
},

View File

@ -24,6 +24,7 @@ export class DateFieldInterface extends CollectionFieldInterface {
'x-component': 'DatePicker',
'x-component-props': {
dateOnly: true,
showTime: false,
},
},
};

View File

@ -9,7 +9,7 @@
import { useField, useForm } from '@formily/react';
import { Cascader, Input, Select, Spin, Table, Tag } from 'antd';
import { last, omit } from 'lodash';
import _, { last, omit } from 'lodash';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ResourceActionContext, useCompile } from '../../../';
@ -120,9 +120,10 @@ const PreviewCom = (props) => {
}, [databaseView]);
const handleFieldChange = (record, index) => {
dataSource.splice(index, 1, record);
setDataSource(dataSource);
field.value = dataSource.map((v) => {
const newDataSource = _.cloneDeep(dataSource);
newDataSource[index] = record;
setDataSource(newDataSource);
field.value = newDataSource.map((v) => {
const source = typeof v.source === 'string' ? v.source : v.source?.filter?.(Boolean)?.join('.');
return {
...v,
@ -198,8 +199,7 @@ const PreviewCom = (props) => {
style={{ width: '100%' }}
popupMatchSelectWidth={false}
onChange={(value) => {
const interfaceConfig = getInterface(value);
handleFieldChange({ ...item, interface: value, uiSchema: interfaceConfig?.default?.uiSchema }, index);
handleFieldChange({ ...item, interface: value }, index);
}}
>
{data.map((group) => (

View File

@ -66,7 +66,7 @@ export const createFormBlockSettings = new SchemaSettings({
useVisible() {
const { action } = useFormBlockContext();
const schema = useFieldSchema();
return !action && schema?.['x-acl-action'].includes('create');
return !action && schema?.['x-acl-action']?.includes('create');
},
useComponentProps() {
const { name } = useCollection_deprecated();

View File

@ -10,6 +10,7 @@
import { getDefaultFormat, str2moment, toGmt, toLocal, getPickerFormat } from '@nocobase/utils/client';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { dayjsable, formatDayjsValue } from '@formily/antd-v5/esm/__builtins__';
const toStringByPicker = (value, picker = 'date', timezone: 'gmt' | 'local') => {
if (!dayjs.isDayjs(value)) return value;
@ -89,7 +90,7 @@ export const handleDateChangeOnForm = (value, dateOnly, utc, picker, showTime, g
return value;
}
if (dateOnly) {
return dayjs(value).startOf(picker).format('YYYY-MM-DD');
return formatDayjsValue(value, 'YYYY-MM-DD');
}
if (utc) {
if (gmt) {
@ -114,6 +115,7 @@ export const mapDatePicker = function () {
const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props;
const format = getDefaultFormat(props);
const onChange = props.onChange;
return {
...props,
inputReadOnly: isMobileMedia,

View File

@ -33,7 +33,7 @@ const toDate = (date, options: any = {}) => {
}
if (field.constructor.name === 'DateOnlyField') {
val = moment(val).format('YYYY-MM-DD HH:mm:ss');
val = moment.utc(val).format('YYYY-MM-DD HH:mm:ss');
}
const eventObj = {
@ -69,7 +69,6 @@ export default {
const r = parseDate(value, {
timezone: parseDateTimezone(ctx),
});
if (typeof r === 'string') {
return {
[Op.eq]: toDate(r, { ctx }),
@ -77,6 +76,9 @@ export default {
}
if (Array.isArray(r)) {
console.log(11111111, {
[Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }],
});
return {
[Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }],
};

View File

@ -15,6 +15,7 @@ export interface Str2momentOptions {
picker?: 'year' | 'month' | 'week' | 'quarter';
utcOffset?: number;
utc?: boolean;
dateOnly?: boolean;
}
export type Str2momentValue = string | string[] | dayjs.Dayjs | dayjs.Dayjs[];
@ -83,10 +84,13 @@ const toMoment = (val: any, options?: Str2momentOptions) => {
return;
}
const offset = options.utcOffset;
const { gmt, picker, utc = true } = options;
const { gmt, picker, utc = true, dateOnly } = options;
if (dayjs(val).isValid()) {
if (dateOnly) {
return dayjs.utc(val, 'YYYY-MM-DD');
}
if (!utc) {
console.log(888);
return dayjs(val);
}

View File

@ -188,9 +188,11 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) =>
const field = getField?.(path);
if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') {
if (value.type) {
return getDayRangeByParams({ ...value, timezone: field?.timezone || timezone });
}
return value;
}
return dateValueWrapper(value, field?.timezone || timezone);
}
return value;

View File

@ -63,7 +63,7 @@ const useSubmitProps = () => {
},
});
setLoading(false);
message.success(t('License key saved successfully, please restart the server'));
message.success(t('License key saved successfully, please re-run the plugin installation.'));
} catch (e) {
setLoading(false);
}

View File

@ -1,5 +1,5 @@
{
"License key saved successfully, please restart the server": "License key saved successfully, please restart the server",
"License key saved successfully, please re-run the plugin installation.": "License key saved successfully, please re-run the plugin installation.",
"License settings": "License settings",
"Instance ID": "Instance ID",
"License key": "License key",

View File

@ -1,5 +1,5 @@
{
"License key saved successfully, please restart the server": "授权密钥保存成功,请重启服务器",
"License key saved successfully, please re-run the plugin installation.": "授权密钥保存成功,请重新执行插件安装操作",
"License settings": "授权设置",
"Instance ID": "实例 ID",
"License key": "授权密钥",

View File

@ -30,13 +30,15 @@ import { WorkflowLink } from './WorkflowLink';
import OpenDrawer from './components/OpenDrawer';
import { workflowSchema } from './schemas/workflows';
import { ExecutionStatusSelect, ExecutionStatusColumn } from './components/ExecutionStatus';
import WorkflowPlugin, { ExecutionStatusOptions, RadioWithTooltip } from '.';
import WorkflowPlugin from '.';
import { RadioWithTooltip } from './components';
import { useRefreshActionProps } from './hooks/useRefreshActionProps';
import { useTranslation } from 'react-i18next';
import { TriggerOptionRender } from './components/TriggerOptionRender';
import { CategoryTabs } from './WorkflowCategoryTabs';
import { EnumerationField } from './components/EmunerationField';
import { useWorkflowFilterActionProps } from './hooks/useWorkflowFilterActionProps';
import { ExecutionStatusOptions } from './constants';
function SyncOptionSelect(props) {
const field = useField<any>();

View File

@ -100,4 +100,5 @@ export default class extends Instruction {
resultTitle: lang('Calculation result'),
};
}
testable = true;
}

View File

@ -194,4 +194,5 @@ export default class extends Instruction {
</NodeDefaultView>
);
}
testable = true;
}

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
import { CaretRightOutlined, CloseOutlined, DeleteOutlined } from '@ant-design/icons';
import { createForm, Field } from '@formily/core';
import { toJS } from '@formily/reactive';
import { ISchema, observer, useField, useForm } from '@formily/react';
import { Alert, App, Button, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd';
import { Alert, App, Button, Collapse, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd';
import { cloneDeep, get, set } from 'lodash';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -26,7 +26,6 @@ import {
cx,
useAPIClient,
useActionContext,
useCancelAction,
useCompile,
usePlugin,
useResourceActionContext,
@ -330,6 +329,7 @@ const useRunAction = () => {
async run() {
const template = parse(node.config);
const config = template(toJS(values.config));
const logField = query('log').take() as Field;
const resultField = query('result').take() as Field;
resultField.setValue(null);
resultField.setFeedback({});
@ -352,6 +352,7 @@ const useRunAction = () => {
messages: data.status > 0 ? [lang('Resolved')] : [lang('Failed')],
});
resultField.setValue(data.result);
logField.setValue(data.log || '');
} catch (err) {
resultField.setFeedback({
type: 'error',
@ -359,7 +360,6 @@ const useRunAction = () => {
});
}
field.data.loading = false;
ctx.setFormValueChanged(false);
},
};
};
@ -397,113 +397,163 @@ function TestFormFieldset({ value, onChange }) {
);
}
function LogCollapse({ value }) {
return value ? (
<Collapse
ghost
items={[
{
key: 'log',
label: lang('Log'),
children: (
<Input.TextArea
value={value}
autoSize={{ minRows: 5, maxRows: 20 }}
style={{ whiteSpace: 'pre', cursor: 'text', fontFamily: 'monospace', fontSize: '80%' }}
disabled
/>
),
},
]}
className={css`
.ant-collapse-item > .ant-collapse-header {
padding: 0;
}
.ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
`}
/>
) : null;
}
function useCancelAction() {
const form = useForm();
const ctx = useActionContext();
return {
async run() {
const resultField = form.query('result').take() as Field;
resultField.setFeedback();
form.setValues({ result: null, log: null });
form.clearFormGraph('*');
form.reset();
ctx.setVisible(false);
},
};
}
function TestButton() {
const node = useNodeContext();
const { values } = useForm();
const [visible, setVisible] = useState(false);
const template = parse(values);
const keys = template.parameters.map((item) => item.key);
const form = useMemo(() => createForm(), []);
const onOpen = useCallback(() => {
setVisible(true);
}, []);
const setModalVisible = useCallback(
(v: boolean) => {
if (v) {
setVisible(true);
return;
}
const resultField = form.query('result').take() as Field;
resultField?.setFeedback();
form.setValues({ result: null, log: null });
form.clearFormGraph('*');
form.reset();
setVisible(false);
},
[form],
);
return (
<NodeContext.Provider value={{ ...node, config: values }}>
<VariableKeysContext.Provider value={keys}>
<SchemaComponent
components={{
Alert,
TestFormFieldset,
}}
scope={{
useCancelAction,
useRunAction,
}}
schema={{
type: 'void',
name: 'testButton',
title: '{{t("Test run")}}',
'x-component': 'Action',
'x-component-props': {
icon: 'CaretRightOutlined',
// openSize: 'small',
},
properties: {
modal: {
type: 'void',
'x-decorator': 'FormV2',
'x-decorator-props': {
form,
<ActionContextProvider value={{ visible, setVisible: setModalVisible }}>
<Button icon={<CaretRightOutlined />} onClick={onOpen}>
{lang('Test run')}
</Button>
<SchemaComponent
components={{
Alert,
TestFormFieldset,
LogCollapse,
}}
scope={{
useCancelAction,
useRunAction,
}}
schema={{
type: 'void',
name: 'modal',
'x-decorator': 'FormV2',
'x-decorator-props': {
form,
},
'x-component': 'Action.Modal',
title: `{{t("Test run", { ns: "workflow" })}}`,
properties: {
alert: {
type: 'void',
'x-component': 'Alert',
'x-component-props': {
message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`,
type: 'warning',
showIcon: true,
className: css`
margin-bottom: 1em;
`,
},
},
'x-component': 'Action.Modal',
title: `{{t("Test run", { ns: "workflow" })}}`,
properties: {
alert: {
type: 'void',
'x-component': 'Alert',
'x-component-props': {
message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`,
type: 'warning',
showIcon: true,
className: css`
margin-bottom: 1em;
`,
},
},
config: {
type: 'object',
title: '{{t("Replace variables", { ns: "workflow" })}}',
'x-decorator': 'FormItem',
'x-component': 'TestFormFieldset',
},
actions: {
type: 'void',
'x-component': 'ActionBar',
properties: {
submit: {
type: 'void',
title: '{{t("Run")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useRunAction }}',
},
config: {
type: 'object',
title: '{{t("Replace variables", { ns: "workflow" })}}',
'x-decorator': 'FormItem',
'x-component': 'TestFormFieldset',
},
actions: {
type: 'void',
'x-component': 'ActionBar',
properties: {
submit: {
type: 'void',
title: '{{t("Run")}}',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{ useRunAction }}',
},
},
},
result: {
type: 'string',
title: `{{t("Result", { ns: "workflow" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
maxRows: 20,
},
style: {
whiteSpace: 'pre',
cursor: 'text',
},
},
result: {
type: 'string',
title: `{{t("Result", { ns: "workflow" })}}`,
'x-decorator': 'FormItem',
'x-component': 'Input.JSON',
'x-component-props': {
autoSize: {
minRows: 5,
maxRows: 20,
},
'x-pattern': 'disabled',
},
footer: {
type: 'void',
'x-component': 'Action.Modal.Footer',
properties: {
cancel: {
type: 'void',
title: '{{t("Close")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
style: {
whiteSpace: 'pre',
cursor: 'text',
},
},
'x-pattern': 'disabled',
},
log: {
type: 'string',
'x-component': 'LogCollapse',
},
},
},
}}
/>
}}
/>
</ActionContextProvider>
</VariableKeysContext.Provider>
</NodeContext.Provider>
);

View File

@ -39,6 +39,22 @@ export class CalculationInstruction extends Instruction {
};
}
}
async test({ engine = 'math.js', expression = '' }) {
const evaluator = <Evaluator | undefined>evaluators.get(engine);
try {
const result = evaluator && expression ? evaluator(expression) : null;
return {
result,
status: JOB_STATUS.RESOLVED,
};
} catch (e) {
return {
result: e.toString(),
status: JOB_STATUS.ERROR,
};
}
}
}
export default CalculationInstruction;

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { evaluators } from '@nocobase/evaluators';
import { Evaluator, evaluators } from '@nocobase/evaluators';
import { Instruction } from '.';
import type Processor from '../Processor';
import { JOB_STATUS } from '../constants';
@ -80,6 +80,22 @@ export class ConditionInstruction extends Instruction {
// pass control to upper scope by ending current scope
return processor.exit(branchJob.status);
}
async test({ engine, calculation, expression = '' }) {
const evaluator = <Evaluator | undefined>evaluators.get(engine);
try {
const result = evaluator ? evaluator(expression) : logicCalculate(calculation);
return {
result,
status: JOB_STATUS.RESOLVED,
};
} catch (e) {
return {
result: e.toString(),
status: JOB_STATUS.ERROR,
};
}
}
}
export default ConditionInstruction;