chore: migrate to new hera
This commit is contained in:
parent
aab43d17d0
commit
37fa7feac0
2
packages/plugins/@hera/plugin-core/.npmignore
Normal file
2
packages/plugins/@hera/plugin-core/.npmignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
1
packages/plugins/@hera/plugin-core/README.md
Normal file
1
packages/plugins/@hera/plugin-core/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @hera/plugin-core
|
2
packages/plugins/@hera/plugin-core/client.d.ts
vendored
Normal file
2
packages/plugins/@hera/plugin-core/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
1
packages/plugins/@hera/plugin-core/client.js
Normal file
1
packages/plugins/@hera/plugin-core/client.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
38
packages/plugins/@hera/plugin-core/package.json
Normal file
38
packages/plugins/@hera/plugin-core/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "@hera/plugin-core",
|
||||||
|
"version": "1.3.0-alpha.8",
|
||||||
|
"displayName": "Hera platform",
|
||||||
|
"displayName.zh-CN": "赫拉平台",
|
||||||
|
"description": "Hera platform as nocobase plugin.",
|
||||||
|
"description.zh-CN": "提供标准赫拉平台能力",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@formily/antd-v5": "1.x",
|
||||||
|
"@formily/core": "^2.2.27",
|
||||||
|
"@formily/react": "^2.2.27",
|
||||||
|
"@formily/shared": "^2.2.27",
|
||||||
|
"@react-pdf/renderer": "^3.3.2",
|
||||||
|
"antd": "5.12.8",
|
||||||
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"mathjs2": "npm:mathjs@^10.6.0",
|
||||||
|
"qrcode": "^1.5.1",
|
||||||
|
"dayjs": "^1.11.8",
|
||||||
|
"qrcode.react": "^3.1.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-pdf": "^7.5.1",
|
||||||
|
"vitest": "0.x",
|
||||||
|
"signature_pad": "4.1.7",
|
||||||
|
"react-router-dom": "^6.11.2",
|
||||||
|
"redis": "^4.6.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nocobase/client": "0.x",
|
||||||
|
"@nocobase/server": "0.x",
|
||||||
|
"@nocobase/test": "0.x"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"System management"
|
||||||
|
]
|
||||||
|
}
|
2
packages/plugins/@hera/plugin-core/server.d.ts
vendored
Normal file
2
packages/plugins/@hera/plugin-core/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
1
packages/plugins/@hera/plugin-core/server.js
Normal file
1
packages/plugins/@hera/plugin-core/server.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
@ -0,0 +1,15 @@
|
|||||||
|
import format, { formatCurrency } from '../utils/currencyUtils';
|
||||||
|
|
||||||
|
describe('utils', () => {
|
||||||
|
beforeEach(async () => {});
|
||||||
|
|
||||||
|
afterEach(() => {});
|
||||||
|
|
||||||
|
describe('currencyUtils', () => {
|
||||||
|
it('formatCurrency', async () => {
|
||||||
|
expect(formatCurrency(100, 2)).toBe('¥100.00');
|
||||||
|
expect(formatCurrency(null, 2)).toBe('¥0.00');
|
||||||
|
expect(formatCurrency(undefined, 2)).toBe('¥0.00');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,86 @@
|
|||||||
|
import { ISchema, useFieldSchema } from '@formily/react';
|
||||||
|
import { SchemaSettingsModalItem, useDesignable } from '@nocobase/client';
|
||||||
|
import { useTranslation } from '../locale';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function AfterSuccess() {
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const component = fieldSchema.parent.parent['x-component'];
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
title: t('After successful submission'),
|
||||||
|
properties: {
|
||||||
|
successMessage: {
|
||||||
|
title: t('Popup message'),
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
dataClear: {
|
||||||
|
title: t('Clear data method'),
|
||||||
|
enum: [
|
||||||
|
{ label: t('Automatic clear'), value: false },
|
||||||
|
{ label: t('Manually clear'), value: true },
|
||||||
|
],
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
manualClose: {
|
||||||
|
title: t('Popup close method'),
|
||||||
|
enum: [
|
||||||
|
{ label: t('Automatic close'), value: false },
|
||||||
|
{ label: t('Manually close'), value: true },
|
||||||
|
],
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
redirecting: {
|
||||||
|
title: t('Then'),
|
||||||
|
enum: [
|
||||||
|
{ label: t('Stay on current page'), value: false },
|
||||||
|
{ label: t('Redirect to'), value: true },
|
||||||
|
],
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-reactions': {
|
||||||
|
target: 'redirectTo',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{!!$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redirectTo: {
|
||||||
|
title: t('Link'),
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (!(component as string).includes('Form')) {
|
||||||
|
delete schema.properties.dataClear;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
title={t('After successful submission')}
|
||||||
|
initialValues={fieldSchema?.['x-action-settings']?.['onSuccess']}
|
||||||
|
schema={{ ...schema } as ISchema}
|
||||||
|
onSubmit={(onSuccess) => {
|
||||||
|
fieldSchema['x-action-settings']['onSuccess'] = onSuccess;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-action-settings': fieldSchema['x-action-settings'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { ISchema, RecursionField, connect, observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import {
|
||||||
|
ActionContextProvider,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
FormProvider,
|
||||||
|
RecordProvider,
|
||||||
|
useDesignable,
|
||||||
|
useRequest,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const viewerSchema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("View record") }}',
|
||||||
|
'x-component': 'AssociationField.Viewer',
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-initializer': 'TabPaneInitializers',
|
||||||
|
properties: {
|
||||||
|
tab1: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Details")}}',
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
'x-designer': 'Tabs.Designer',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'RecordBlockInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInsertSchema = (component) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { insertAfterBegin } = useDesignable();
|
||||||
|
const insert = useCallback(
|
||||||
|
(ss) => {
|
||||||
|
const schema = fieldSchema.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-component'] === 'AssociationField.' + component) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null);
|
||||||
|
if (!schema) {
|
||||||
|
insertAfterBegin(_.cloneDeep(ss));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[component],
|
||||||
|
);
|
||||||
|
return insert;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssociatedFieldImplement = observer<any>((props) => {
|
||||||
|
const { collection, fieldExp, dateFieldExp, sourceCollection, sourceField } = props;
|
||||||
|
const [visible, setVisible] = useState<boolean>(false);
|
||||||
|
const insertViewer = useInsertSchema('Viewer');
|
||||||
|
const field = useField();
|
||||||
|
const form = useForm();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { designable } = useDesignable();
|
||||||
|
const fieldName = fieldExp?.replace(/{{(.*?)}}/, '$1');
|
||||||
|
const dateFieldName = dateFieldExp?.replace(/{{(.*?)}}/, '$1');
|
||||||
|
|
||||||
|
const { data, loading, run } = useRequest<{ data: any }>(
|
||||||
|
{
|
||||||
|
url: `/${sourceCollection}:get`,
|
||||||
|
params: {
|
||||||
|
appends: [sourceField],
|
||||||
|
filter: {
|
||||||
|
contract_id: form.values[fieldName]?.id,
|
||||||
|
start_date: { $lte: form.values[dateFieldName] },
|
||||||
|
end_date: { $gte: form.values[dateFieldName] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
manual: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (form.values[fieldName]?.id && form.values[dateFieldName]) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}, [form.values[fieldName]?.id, form.values[dateFieldName]]);
|
||||||
|
|
||||||
|
if (loading || !data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!data.data?.[sourceField + '_id']) {
|
||||||
|
form.setValues({
|
||||||
|
// FIXME should add base path
|
||||||
|
[fieldSchema.name]: null,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const record = data.data[sourceField];
|
||||||
|
form.setValues({
|
||||||
|
// FIXME should add base path
|
||||||
|
[fieldSchema.name]: record,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
if (designable) {
|
||||||
|
insertViewer(viewerSchema);
|
||||||
|
}
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{record.name}
|
||||||
|
</a>
|
||||||
|
<CollectionProvider_deprecated name={collection}>
|
||||||
|
<ActionContextProvider value={{ visible, setVisible, openMode: 'drawer', snapshot: false }}>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<FormProvider>
|
||||||
|
<RecursionField
|
||||||
|
schema={fieldSchema}
|
||||||
|
onlyRenderProperties
|
||||||
|
basePath={field.address}
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] === 'AssociationField.Viewer';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
</RecordProvider>
|
||||||
|
</ActionContextProvider>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AssociatedField = connect(AssociatedFieldImplement);
|
@ -0,0 +1,191 @@
|
|||||||
|
import { onFormValuesChange } from '@formily/core';
|
||||||
|
import { useField, useFieldSchema, useForm, useFormEffects } from '@formily/react';
|
||||||
|
import { Input } from '@nocobase/client';
|
||||||
|
import { Descriptions, DescriptionsProps } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { evaluate } from 'mathjs2';
|
||||||
|
|
||||||
|
const transformFormula = (formula: string) => {
|
||||||
|
if (!formula) return [];
|
||||||
|
const formulaArray = formula.split(/([+\-*/?:()%])/).filter((item) => item);
|
||||||
|
return formulaArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalcResult = (props) => {
|
||||||
|
// 公式,单位,前缀,后缀,小数点位数,面板逻辑代码
|
||||||
|
const { formula, prefix, suffix, decimal, panel } = props;
|
||||||
|
const form = useForm();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const path: any = field.path.entire;
|
||||||
|
const fieldPath = path?.replace(`.${fieldSchema.name}`, '');
|
||||||
|
const defaultValue = fieldSchema.name === 'subtotal' ? '¥0.00' : [];
|
||||||
|
const [value, setValue] = useState<string | DescriptionsProps['items']>(defaultValue);
|
||||||
|
|
||||||
|
const transformFormulaArray = transformFormula(formula);
|
||||||
|
|
||||||
|
let calculateData = [];
|
||||||
|
|
||||||
|
const newFormulaArray = (data): [string, object] => {
|
||||||
|
calculateData = [];
|
||||||
|
let count = 0;
|
||||||
|
const scopes = {};
|
||||||
|
if (transformFormulaArray.length === 0) return;
|
||||||
|
for (let i = 0; i < transformFormulaArray.length; i++) {
|
||||||
|
const item = transformFormulaArray[i];
|
||||||
|
if (!item) continue;
|
||||||
|
const isNumber = !isNaN(Number(item));
|
||||||
|
const symbol = ['+', '-', '*', '/', '?', ':', '(', ')', '%'].includes(item);
|
||||||
|
if (!isNumber && !symbol) {
|
||||||
|
let value;
|
||||||
|
// 举例: ${fieldObj}.fieldName
|
||||||
|
const pattern = /\${(.*?)}/g;
|
||||||
|
if (item.match(pattern)?.length) {
|
||||||
|
const target = item.match(pattern)[0].replace(/\${|}/g, '');
|
||||||
|
// 以.分割字符串
|
||||||
|
const targetField = item.split('.')[1];
|
||||||
|
|
||||||
|
if (path === fieldPath) {
|
||||||
|
// @ts-ignore
|
||||||
|
value = _.chain(data).get(target).get(targetField, 0).value();
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
value = _.chain(data).get(fieldPath).get(target).get(targetField, 0).value();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (path === fieldPath) {
|
||||||
|
// @ts-ignore
|
||||||
|
value = _.chain(data).get(item, 0).value();
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
value = _.chain(data).get(fieldPath).get(item, 0).value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
const varName = 'var' + count;
|
||||||
|
const varValue = value;
|
||||||
|
scopes[varName] = varValue;
|
||||||
|
calculateData.push(varName);
|
||||||
|
} else {
|
||||||
|
calculateData.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [calculateData.join(''), scopes];
|
||||||
|
};
|
||||||
|
const fun = () => {
|
||||||
|
if (!panel && transformFormulaArray.length) {
|
||||||
|
const [code, scopes] = newFormulaArray(form.values);
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
const pre = prefix || '';
|
||||||
|
const suf = suffix || '';
|
||||||
|
const res = evaluate(code, scopes);
|
||||||
|
const main = isNaN(res) ? res : Number(res).toFixed(decimal || 0);
|
||||||
|
result = pre + main + suf;
|
||||||
|
} catch (error) {
|
||||||
|
result = `${code}`;
|
||||||
|
console.warn('code: ' + code + ' scopes: ' + scopes + 'error: ' + result + ' error message ' + error.message);
|
||||||
|
}
|
||||||
|
setValue(result.toString());
|
||||||
|
} else if (panel) {
|
||||||
|
let items = [];
|
||||||
|
// ====================字段配置举例==================
|
||||||
|
const exampleTemplate = `
|
||||||
|
let total = 0;
|
||||||
|
let allWeight = 0;
|
||||||
|
const products = {};
|
||||||
|
for (let i = 0; i < form.values.items.length; i++) {
|
||||||
|
const item = form.values.items[i];
|
||||||
|
// 计算 合计金额
|
||||||
|
const count = item?.count || 0;
|
||||||
|
const unitPrice = item?.unit_price || 0;
|
||||||
|
// 换算数量
|
||||||
|
let scale = 1;
|
||||||
|
if (item && item.product && item.product?.ratio) {
|
||||||
|
scale = item.product?.ratio;
|
||||||
|
}
|
||||||
|
total += count * scale * unitPrice;
|
||||||
|
// 计算 理论重量
|
||||||
|
let weight = 1;
|
||||||
|
if (item && item.product && item.product?.weight) {
|
||||||
|
weight = item.product.weight || 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
allWeight += weight * count;
|
||||||
|
// 计算 产品分类
|
||||||
|
if (item && item.product) {
|
||||||
|
if (products[item.product.name]) {
|
||||||
|
products[item.product.name].count += count * scale;
|
||||||
|
} else {
|
||||||
|
products[item.product.name] = {
|
||||||
|
count: count * scale,
|
||||||
|
unit: item.product?.category?.conversion_unit || item.product?.category?.unit || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 生成产品分类的数据
|
||||||
|
const weight = {
|
||||||
|
key: '1',
|
||||||
|
label: '理论重量',
|
||||||
|
children: (allWeight / 1000).toFixed(3) + '吨',
|
||||||
|
};
|
||||||
|
// const totalPrice = {
|
||||||
|
// key: '2',
|
||||||
|
// label: '合计',
|
||||||
|
// children: '¥' + total.toFixed(2),
|
||||||
|
// }
|
||||||
|
// ,, totalPrice
|
||||||
|
items.push(weight);
|
||||||
|
|
||||||
|
if (Object.keys(products).length > 0) {
|
||||||
|
for (const key in products) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(products, key)) {
|
||||||
|
const value = products[key];
|
||||||
|
items.push({
|
||||||
|
key: key,
|
||||||
|
label: key,
|
||||||
|
children: value.count.toFixed(3) + value.unit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
try {
|
||||||
|
eval(panel);
|
||||||
|
} catch (error) {
|
||||||
|
items = [];
|
||||||
|
items.push({
|
||||||
|
key: '1',
|
||||||
|
label: '数据异常',
|
||||||
|
children: '请检查字段配置内容,error:' + error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const showItems = items.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.label,
|
||||||
|
children: <p>{item.children}</p>,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setValue(showItems);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
fun();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useFormEffects(() => {
|
||||||
|
onFormValuesChange((form) => {
|
||||||
|
fun();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return <Input.ReadPretty value={value as string} />;
|
||||||
|
} else {
|
||||||
|
return <Descriptions items={value as DescriptionsProps['items']} />;
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const CustomAssociatedField = (props) => {
|
||||||
|
if (!props.component) return;
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'string',
|
||||||
|
name: props.component,
|
||||||
|
'x-component': props.component,
|
||||||
|
'x-component-props': props,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,75 @@
|
|||||||
|
import { SchemaComponent, SchemaSettings, useApp, useDesignable, usePlugin } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from '../locale';
|
||||||
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { Field } from '@nocobase/database';
|
||||||
|
import { CUSTOM_COMPONENT_TYPE_FORM_ITEM } from '..';
|
||||||
|
import { useCustomComponent } from '../hooks/useCustomComponent';
|
||||||
|
|
||||||
|
export const CustomComponentStub = (props) => {
|
||||||
|
return <div>请选择组件</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomComponentDispatcher = (props) => {
|
||||||
|
if (!props.component) return;
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'void',
|
||||||
|
name: props.component,
|
||||||
|
'x-component': props.component,
|
||||||
|
'x-component-props': props,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const customComponentDispatcherSettings = new SchemaSettings({
|
||||||
|
name: 'customComponentDispatcherSettings',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'component',
|
||||||
|
type: 'select',
|
||||||
|
useComponentProps() {
|
||||||
|
const formItemComponents = useCustomComponent(CUSTOM_COMPONENT_TYPE_FORM_ITEM);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
return {
|
||||||
|
title: t('component'),
|
||||||
|
value: field.componentProps?.component || 'CustomComponentStub',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '未选择组件',
|
||||||
|
value: 'CustomComponentStub',
|
||||||
|
},
|
||||||
|
...formItemComponents,
|
||||||
|
],
|
||||||
|
onChange(component) {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
fieldSchema['x-component-props']['component'] = component;
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
field.componentProps.component = component;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'divider',
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'remove',
|
||||||
|
type: 'remove',
|
||||||
|
componentProps: {
|
||||||
|
removeParentsIfNoChildren: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,16 @@
|
|||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const CustomField = (props) => {
|
||||||
|
if (!props.component) return;
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
type: 'string',
|
||||||
|
name: props.component,
|
||||||
|
'x-component': props.component,
|
||||||
|
'x-component-props': props,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useAssociatedFields, useCollectionManager_deprecated, useCompile, Variable } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const Expression = (props) => {
|
||||||
|
const { value = '', useCurrentFields, onChange } = props;
|
||||||
|
const compile = useCompile();
|
||||||
|
const { interfaces, getCollectionFields } = useCollectionManager_deprecated();
|
||||||
|
|
||||||
|
const fields = useCurrentFields?.() ?? [];
|
||||||
|
const associatedFields = useAssociatedFields();
|
||||||
|
console.log(associatedFields, 'associatedFields', fields);
|
||||||
|
|
||||||
|
const options = fields.map((field) => ({
|
||||||
|
label: compile(field.uiSchema.title),
|
||||||
|
value: field.name,
|
||||||
|
children:
|
||||||
|
getCollectionFields(field.target)
|
||||||
|
?.filter((subField) => subField.uiSchema)
|
||||||
|
.map((subField) => ({
|
||||||
|
label: subField.uiSchema ? compile(subField.uiSchema.title) : '',
|
||||||
|
value: subField.name,
|
||||||
|
})) ?? [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return <Variable.Input value={value} onChange={onChange} scope={options} changeOnSelect />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Expression;
|
@ -0,0 +1,51 @@
|
|||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import { SchemaSettingsSwitchItem, useDesignable } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from '../locale';
|
||||||
|
|
||||||
|
// 添加跳转页面选项
|
||||||
|
export const SessionSubmit = () => {
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return (
|
||||||
|
<SchemaSettingsSwitchItem
|
||||||
|
title={t('Navigate to new page')}
|
||||||
|
checked={!!fieldSchema?.['x-action-settings']?.sessionSubmit}
|
||||||
|
onChange={(value) => {
|
||||||
|
fieldSchema['x-action-settings'].sessionSubmit = value;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-action-settings': {
|
||||||
|
...fieldSchema['x-action-settings'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SessionUpdate() {
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return (
|
||||||
|
<SchemaSettingsSwitchItem
|
||||||
|
title={t('更新询问')}
|
||||||
|
checked={!!fieldSchema?.['x-action-settings']?.sessionUpdate}
|
||||||
|
onChange={(value) => {
|
||||||
|
fieldSchema['x-action-settings'].sessionUpdate = value;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-action-settings': {
|
||||||
|
...fieldSchema['x-action-settings'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,416 @@
|
|||||||
|
import { TreeSelect } from '@formily/antd-v5';
|
||||||
|
import { Field, onFieldChange } from '@formily/core';
|
||||||
|
import { ISchema, Schema, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import {
|
||||||
|
GeneralSchemaDesigner,
|
||||||
|
SchemaSettingsDivider,
|
||||||
|
SchemaSettingsItem,
|
||||||
|
SchemaSettingsModalItem,
|
||||||
|
SchemaSettingsRemove,
|
||||||
|
SchemaSettingsSubMenu,
|
||||||
|
createDesignable,
|
||||||
|
findByUid,
|
||||||
|
useAPIClient,
|
||||||
|
useDesignable,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
|
||||||
|
const toItems = (properties = {}) => {
|
||||||
|
const items = [];
|
||||||
|
for (const key in properties) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(properties, key)) {
|
||||||
|
const element = properties[key];
|
||||||
|
const item = {
|
||||||
|
label: element.title,
|
||||||
|
value: `${element['x-uid']}||${element['x-component']}`,
|
||||||
|
};
|
||||||
|
if (element.properties) {
|
||||||
|
const children = toItems(element.properties);
|
||||||
|
if (children?.length) {
|
||||||
|
item['children'] = children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findMenuSchema = (fieldSchema: Schema) => {
|
||||||
|
let parent = fieldSchema.parent;
|
||||||
|
while (parent) {
|
||||||
|
if (parent['x-component'] === 'Menu') {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
parent = parent.parent;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const InsertMenuItems = (props) => {
|
||||||
|
const { eventKey, title, insertPosition } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const isSubMenu = fieldSchema['x-component'] === 'Menu.SubMenu';
|
||||||
|
const api = useAPIClient();
|
||||||
|
if (!isSubMenu && insertPosition === 'beforeEnd') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const serverHooks = [
|
||||||
|
{
|
||||||
|
type: 'onSelfCreate',
|
||||||
|
method: 'bindMenuToRole',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'onSelfSave',
|
||||||
|
method: 'extractTextToLocale',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<SchemaSettingsSubMenu eventKey={eventKey} title={title}>
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
eventKey={`${insertPosition}group`}
|
||||||
|
title={t('Group')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Add group'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-component-props': {},
|
||||||
|
// description: `原字段标题:${collectionField?.uiSchema?.title}`,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ title, icon }) => {
|
||||||
|
dn.insertAdjacent(insertPosition, {
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-component': 'Menu.SubMenu',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
|
'x-component-props': {
|
||||||
|
icon,
|
||||||
|
},
|
||||||
|
'x-server-hooks': serverHooks,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
eventKey={`${insertPosition}page`}
|
||||||
|
title={t('Page')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Add page'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ title, icon }) => {
|
||||||
|
dn.insertAdjacent(insertPosition, {
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-component': 'Menu.Item',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
|
'x-component-props': {
|
||||||
|
icon,
|
||||||
|
},
|
||||||
|
'x-server-hooks': serverHooks,
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Page',
|
||||||
|
'x-async': true,
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'BlockInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
eventKey={`${insertPosition}link`}
|
||||||
|
title={t('Link')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Add link'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
href: {
|
||||||
|
title: t('Link'),
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ title, icon, href }) => {
|
||||||
|
dn.insertAdjacent(insertPosition, {
|
||||||
|
type: 'void',
|
||||||
|
title,
|
||||||
|
'x-component': 'Menu.URL',
|
||||||
|
'x-decorator': 'ACLMenuItemProvider',
|
||||||
|
'x-component-props': {
|
||||||
|
icon,
|
||||||
|
href,
|
||||||
|
},
|
||||||
|
'x-server-hooks': serverHooks,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettingsDivider />
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
eventKey={`${insertPosition}restore`}
|
||||||
|
title="加载……"
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: '加载……',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
type: 'object',
|
||||||
|
title: '文件',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Upload.Attachment',
|
||||||
|
'x-component-props': {
|
||||||
|
action: 'attachments:create',
|
||||||
|
multiple: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={async ({ title, file }) => {
|
||||||
|
const { data } = await api.request({
|
||||||
|
url: file.url,
|
||||||
|
baseURL: '/',
|
||||||
|
});
|
||||||
|
const s = data ?? {};
|
||||||
|
s.title = title;
|
||||||
|
dn.insertAdjacent(insertPosition, s);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SchemaSettingsSubMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MenuDesigner = () => {
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { dn, refresh } = useDesignable();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const menuSchema = findMenuSchema(fieldSchema);
|
||||||
|
const items = toItems(menuSchema?.properties);
|
||||||
|
const effects = (form) => {
|
||||||
|
onFieldChange('target', (field: Field) => {
|
||||||
|
const [, component] = field?.value?.split?.('||') || [];
|
||||||
|
field.query('position').take((f: Field) => {
|
||||||
|
f.dataSource =
|
||||||
|
component === 'Menu.SubMenu'
|
||||||
|
? [
|
||||||
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
|
{ label: t('Inner'), value: 'beforeEnd' },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
title: t('Edit menu item'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: t('Menu item title'),
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
title: t('Menu item icon'),
|
||||||
|
'x-component': 'IconPicker',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const initialValues = {
|
||||||
|
title: field.title,
|
||||||
|
icon: field.componentProps.icon,
|
||||||
|
};
|
||||||
|
if (fieldSchema['x-component'] === 'Menu.URL') {
|
||||||
|
schema.properties['href'] = {
|
||||||
|
title: t('Link'),
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
};
|
||||||
|
initialValues['href'] = field.componentProps.href;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<GeneralSchemaDesigner>
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
title={t('Edit')}
|
||||||
|
eventKey="edit"
|
||||||
|
schema={schema as ISchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={({ title, icon, href }) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
'x-server-hooks': [
|
||||||
|
{
|
||||||
|
type: 'onSelfSave',
|
||||||
|
method: 'extractTextToLocale',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
if (title) {
|
||||||
|
fieldSchema.title = title;
|
||||||
|
field.title = title;
|
||||||
|
schema['title'] = title;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
field.componentProps.icon = icon;
|
||||||
|
field.componentProps.href = href;
|
||||||
|
schema['x-component-props'] = { icon, href };
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props']['icon'] = icon;
|
||||||
|
fieldSchema['x-component-props']['href'] = href;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
title={t('Move to')}
|
||||||
|
eventKey="move-to"
|
||||||
|
components={{ TreeSelect }}
|
||||||
|
effects={effects}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Move to'),
|
||||||
|
properties: {
|
||||||
|
target: {
|
||||||
|
title: t('Target'),
|
||||||
|
enum: items,
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'TreeSelect',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
title: t('Position'),
|
||||||
|
required: true,
|
||||||
|
enum: [
|
||||||
|
{ label: t('Before'), value: 'beforeBegin' },
|
||||||
|
{ label: t('After'), value: 'afterEnd' },
|
||||||
|
],
|
||||||
|
default: 'afterEnd',
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ target, position }) => {
|
||||||
|
const [uid] = target?.split?.('||') || [];
|
||||||
|
if (!uid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = findByUid(menuSchema, uid);
|
||||||
|
const dn = createDesignable({
|
||||||
|
t,
|
||||||
|
api,
|
||||||
|
refresh,
|
||||||
|
current,
|
||||||
|
});
|
||||||
|
dn.loadAPIClientEvents();
|
||||||
|
dn.insertAdjacent(position, fieldSchema);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettingsDivider />
|
||||||
|
<InsertMenuItems eventKey={'insertbeforeBegin'} title={t('Insert before')} insertPosition={'beforeBegin'} />
|
||||||
|
<InsertMenuItems eventKey={'insertafterEnd'} title={t('Insert after')} insertPosition={'afterEnd'} />
|
||||||
|
<InsertMenuItems eventKey={'insertbeforeEnd'} title={t('Insert inner')} insertPosition={'beforeEnd'} />
|
||||||
|
<SchemaSettingsDivider />
|
||||||
|
<SchemaSettingsItem
|
||||||
|
title="保存……"
|
||||||
|
onClick={async () => {
|
||||||
|
const deleteUid = (s: ISchema) => {
|
||||||
|
delete s['name'];
|
||||||
|
delete s['x-uid'];
|
||||||
|
Object.keys(s.properties || {}).forEach((key) => {
|
||||||
|
deleteUid(s.properties[key]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const { data } = await api.request({
|
||||||
|
url: `/uiSchemas:getJsonSchema/${fieldSchema['x-uid']}?includeAsyncNode=true`,
|
||||||
|
});
|
||||||
|
const s = data?.data || {};
|
||||||
|
deleteUid(s);
|
||||||
|
const blob = new Blob([JSON.stringify(s, null, 2)], { type: 'application/json' });
|
||||||
|
saveAs(blob, 'export.json');
|
||||||
|
message.success('保存成功');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SchemaSettingsRemove
|
||||||
|
confirm={{
|
||||||
|
title: t('Delete menu item'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeneralSchemaDesigner>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,105 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { ArrayCollapse } from '@formily/antd-v5';
|
||||||
|
import { Form } from '@formily/core';
|
||||||
|
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import {
|
||||||
|
FormBlockContext,
|
||||||
|
RecordProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
getShouldChange,
|
||||||
|
useCollectionManager_deprecated,
|
||||||
|
__UNSAFE__VariablesContextType,
|
||||||
|
__UNSAFE__DynamicComponentProps,
|
||||||
|
__UNSAFE__VariableOption,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { FilterContext } from './context';
|
||||||
|
import { VariableInput } from './VariableInput';
|
||||||
|
|
||||||
|
interface usePropsReturn {
|
||||||
|
options: any;
|
||||||
|
defaultValues: any[];
|
||||||
|
collectionName: string;
|
||||||
|
form: Form;
|
||||||
|
variables: __UNSAFE__VariablesContextType;
|
||||||
|
localVariables: __UNSAFE__VariableOption | __UNSAFE__VariableOption[];
|
||||||
|
record: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* create 表示创建表单,update 表示更新表单
|
||||||
|
*/
|
||||||
|
formBlockType: 'create' | 'update';
|
||||||
|
fields: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
useProps: () => usePropsReturn;
|
||||||
|
dynamicComponent: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormFilterScope = observer(
|
||||||
|
(props: Props) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { useProps, dynamicComponent } = props;
|
||||||
|
const { options, defaultValues, collectionName, form, formBlockType, variables, localVariables, record, fields } =
|
||||||
|
useProps();
|
||||||
|
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
|
||||||
|
const components = useMemo(() => ({ ArrayCollapse }), []);
|
||||||
|
const schema = useMemo(
|
||||||
|
() => ({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
condition: {
|
||||||
|
'x-component': 'Filter',
|
||||||
|
default: defaultValues,
|
||||||
|
'x-component-props': {
|
||||||
|
collectionName,
|
||||||
|
useProps() {
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
className: css`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 10px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
dynamicComponent: (props: __UNSAFE__DynamicComponentProps) => {
|
||||||
|
const { collectionField } = props;
|
||||||
|
return (
|
||||||
|
<VariableInput
|
||||||
|
{...props}
|
||||||
|
form={form}
|
||||||
|
record={record}
|
||||||
|
shouldChange={getShouldChange({
|
||||||
|
collectionField,
|
||||||
|
variables,
|
||||||
|
localVariables,
|
||||||
|
getAllCollectionsInheritChain,
|
||||||
|
})}
|
||||||
|
fields={fields}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[collectionName, defaultValues, form, localVariables, options, props, record, variables, fields],
|
||||||
|
);
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ field: options, fieldSchema, dynamicComponent, options: options || [] }),
|
||||||
|
[dynamicComponent, fieldSchema, options],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormBlockContext.Provider value={{ form, type: formBlockType }}>
|
||||||
|
<RecordProvider record={record}>
|
||||||
|
<FilterContext.Provider value={value}>
|
||||||
|
<SchemaComponent components={components} schema={schema} />
|
||||||
|
</FilterContext.Provider>
|
||||||
|
</RecordProvider>
|
||||||
|
</FormBlockContext.Provider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'FormLinkageRules' },
|
||||||
|
);
|
@ -0,0 +1,63 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { SchemaSettingsItem, useFormActiveFields, useSchemaSettings } from '@nocobase/client';
|
||||||
|
import { App, ModalFuncProps } from 'antd';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { useTranslation } from '../../locale';
|
||||||
|
import { Field } from '@formily/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface SchemaSettingsRemoveProps {
|
||||||
|
confirm?: ModalFuncProps;
|
||||||
|
removeParentsIfNoChildren?: boolean;
|
||||||
|
breakRemoveOn?: ISchema | ((s: ISchema) => boolean);
|
||||||
|
}
|
||||||
|
export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
|
||||||
|
const { confirm, removeParentsIfNoChildren, breakRemoveOn } = props;
|
||||||
|
const { dn, template } = useSchemaSettings();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const form = useForm();
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const { removeActiveFieldName } = useFormActiveFields() || {};
|
||||||
|
return (
|
||||||
|
<SchemaSettingsItem
|
||||||
|
title="Delete"
|
||||||
|
eventKey="remove"
|
||||||
|
onClick={() => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Delete block'),
|
||||||
|
content: t('Are you sure you want to delete it?'),
|
||||||
|
...confirm,
|
||||||
|
async onOk() {
|
||||||
|
const options = {
|
||||||
|
removeParentsIfNoChildren,
|
||||||
|
breakRemoveOn,
|
||||||
|
};
|
||||||
|
if (field?.required) {
|
||||||
|
field.required = false;
|
||||||
|
fieldSchema['required'] = false;
|
||||||
|
}
|
||||||
|
await dn.remove(null, options);
|
||||||
|
await confirm?.onOk?.();
|
||||||
|
delete form.values[fieldSchema.name];
|
||||||
|
const name = fieldSchema.name as string;
|
||||||
|
const title = fieldSchema.title;
|
||||||
|
for (const key in form.fields) {
|
||||||
|
if (key.includes(name) && form.fields[key].title == title) {
|
||||||
|
delete form.fields[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
removeActiveFieldName?.(fieldSchema.name as string);
|
||||||
|
if (field?.setInitialValue && field?.reset) {
|
||||||
|
field.setInitialValue(null);
|
||||||
|
field.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Delete')}
|
||||||
|
</SchemaSettingsItem>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,432 @@
|
|||||||
|
import { Form } from '@formily/core';
|
||||||
|
// @ts-ignore
|
||||||
|
import { Schema } from '@formily/json-schema';
|
||||||
|
import { SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
|
||||||
|
import {
|
||||||
|
CollectionFieldOptions,
|
||||||
|
useVariableScope,
|
||||||
|
__UNSAFE__VariablesContextType,
|
||||||
|
__UNSAFE__VariableOption,
|
||||||
|
Variable,
|
||||||
|
isVariable,
|
||||||
|
useUserVariable,
|
||||||
|
__UNSAFE__VariableInputOption,
|
||||||
|
__UNSAFE__,
|
||||||
|
useDateVariable,
|
||||||
|
useCollectionManager_deprecated,
|
||||||
|
useCompile,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React, { useCallback, useContext, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FilterContext } from './context';
|
||||||
|
const { useBlockCollection, useValues, useVariableOptions, useContextAssociationFields, useRecordVariable } =
|
||||||
|
__UNSAFE__;
|
||||||
|
|
||||||
|
interface GetShouldChangeProps {
|
||||||
|
collectionField: CollectionFieldOptions;
|
||||||
|
variables: __UNSAFE__VariablesContextType;
|
||||||
|
localVariables: __UNSAFE__VariableOption | __UNSAFE__VariableOption[];
|
||||||
|
/** `useCollectionManager` 返回的 */
|
||||||
|
getAllCollectionsInheritChain: (collectionName: string) => string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderSchemaComponentProps {
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any, optionPath?: any[]) => void;
|
||||||
|
renderSchemaComponent: (props: RenderSchemaComponentProps) => any;
|
||||||
|
schema?: any;
|
||||||
|
/** 消费变量值的字段 */
|
||||||
|
targetFieldSchema?: Schema;
|
||||||
|
children?: any;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
collectionField: CollectionFieldOptions;
|
||||||
|
contextCollectionName?: string;
|
||||||
|
/**指定当前表单数据表 */
|
||||||
|
currentFormCollectionName?: string;
|
||||||
|
/**指定当前对象数据表 */
|
||||||
|
currentIterationCollectionName?: string;
|
||||||
|
/**
|
||||||
|
* 根据 `onChange` 的第一个参数,判断是否需要触发 `onChange`
|
||||||
|
* @param value `onChange` 的第一个参数
|
||||||
|
* @returns 返回为 `true` 时,才会触发 `onChange`
|
||||||
|
*/
|
||||||
|
shouldChange?: (value: any, optionPath?: any[]) => Promise<boolean>;
|
||||||
|
form?: Form;
|
||||||
|
/**
|
||||||
|
* 当前表单的记录,数据来自数据库
|
||||||
|
*/
|
||||||
|
record?: Record<string, any>;
|
||||||
|
/**
|
||||||
|
* 可以用该方法对内部的 scope 进行筛选和修改
|
||||||
|
* @param scope
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
returnScope?: (scope: __UNSAFE__VariableInputOption[]) => any[];
|
||||||
|
fields: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注意:该组件存在以下问题:
|
||||||
|
* - 在选中选项的时候该组件不能触发重渲染
|
||||||
|
* - 如果触发重渲染可能会导致无法展开子选项列表
|
||||||
|
* @param props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const VariableInput = (props: Props) => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
renderSchemaComponent: RenderSchemaComponent,
|
||||||
|
style,
|
||||||
|
schema,
|
||||||
|
className,
|
||||||
|
contextCollectionName,
|
||||||
|
collectionField,
|
||||||
|
shouldChange,
|
||||||
|
form,
|
||||||
|
record,
|
||||||
|
returnScope = _.identity,
|
||||||
|
targetFieldSchema,
|
||||||
|
currentFormCollectionName,
|
||||||
|
currentIterationCollectionName,
|
||||||
|
fields,
|
||||||
|
} = props;
|
||||||
|
const { name: blockCollectionName } = useBlockCollection();
|
||||||
|
const scope = useVariableScope();
|
||||||
|
const { operator, schema: uiSchema = collectionField?.uiSchema } = useValues();
|
||||||
|
|
||||||
|
const variableOptions = useVariableOptions({
|
||||||
|
collectionField,
|
||||||
|
form,
|
||||||
|
record,
|
||||||
|
operator,
|
||||||
|
uiSchema,
|
||||||
|
targetFieldSchema,
|
||||||
|
currentFormCollectionName,
|
||||||
|
currentIterationCollectionName,
|
||||||
|
});
|
||||||
|
const contextVariable = useContextAssociationFields({ schema, maxDepth: 2, contextCollectionName, collectionField });
|
||||||
|
const { compatOldVariables } = useCompatOldVariables({
|
||||||
|
collectionField,
|
||||||
|
uiSchema,
|
||||||
|
targetFieldSchema,
|
||||||
|
blockCollectionName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (contextCollectionName && variableOptions.every((item) => item.value !== contextVariable.value)) {
|
||||||
|
variableOptions.push(contextVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(value: any, optionPath: any[]) => {
|
||||||
|
if (!shouldChange) {
|
||||||
|
return onChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `shouldChange` 这个函数的运算量比较大,会导致展开变量列表时有明显的卡顿感,在这里加个延迟能有效解决这个问题
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (await shouldChange(value, optionPath)) {
|
||||||
|
onChange(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, shouldChange],
|
||||||
|
);
|
||||||
|
const options = useVariableCustomOptions(fields);
|
||||||
|
return (
|
||||||
|
<Variable.Input
|
||||||
|
className={className}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
scope={options}
|
||||||
|
style={style}
|
||||||
|
changeOnSelect
|
||||||
|
>
|
||||||
|
<RenderSchemaComponent value={value} onChange={onChange} />
|
||||||
|
</Variable.Input>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过限制用户的选择,来防止用户选择错误的变量
|
||||||
|
*/
|
||||||
|
export const getShouldChange = ({
|
||||||
|
collectionField,
|
||||||
|
variables,
|
||||||
|
localVariables,
|
||||||
|
getAllCollectionsInheritChain,
|
||||||
|
}: GetShouldChangeProps) => {
|
||||||
|
const collectionsInheritChain = collectionField ? getAllCollectionsInheritChain(collectionField.target) : [];
|
||||||
|
|
||||||
|
return async (value: any, optionPath: any[]) => {
|
||||||
|
if (_.isString(value) && value.includes('$nRole')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVariable(value) || !variables || !collectionField) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `json` 可以选择任意类型的变量,详见:https://nocobase.feishu.cn/docx/EmNEdEBOnoQohUx2UmBcqIQ5nyh#FPLfdSRDEoXR65xW0mBcdfL5n0c
|
||||||
|
if (collectionField.interface === 'json') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastOption = optionPath[optionPath.length - 1];
|
||||||
|
|
||||||
|
// 点击叶子节点时,必须更新 value
|
||||||
|
if (lastOption && _.isEmpty(lastOption.children) && !lastOption.loadChildren) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionFieldOfVariable = await variables.getCollectionField(value, localVariables);
|
||||||
|
|
||||||
|
if (!collectionField) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `一对一` 和 `一对多` 的不能用于设置默认值,因为其具有唯一性
|
||||||
|
if (['o2o', 'o2m', 'oho'].includes(collectionFieldOfVariable?.interface)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!collectionField.target && collectionFieldOfVariable?.target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (collectionField.target && !collectionFieldOfVariable?.target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
collectionField.target &&
|
||||||
|
collectionFieldOfVariable?.target &&
|
||||||
|
!collectionsInheritChain.includes(collectionFieldOfVariable?.target)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FormatVariableScopeParam {
|
||||||
|
children: any[];
|
||||||
|
disabled: boolean;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormatVariableScopeReturn {
|
||||||
|
value: string;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
disabled: boolean;
|
||||||
|
children?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兼容老版本的变量
|
||||||
|
* @param variables
|
||||||
|
*/
|
||||||
|
export function useCompatOldVariables(props: {
|
||||||
|
uiSchema: any;
|
||||||
|
collectionField: CollectionFieldOptions;
|
||||||
|
blockCollectionName: string;
|
||||||
|
noDisabled?: boolean;
|
||||||
|
targetFieldSchema?: Schema;
|
||||||
|
}) {
|
||||||
|
const { uiSchema, collectionField, noDisabled, targetFieldSchema, blockCollectionName } = props;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const lowLevelUserVariable = useUserVariable({
|
||||||
|
maxDepth: 1,
|
||||||
|
uiSchema: uiSchema,
|
||||||
|
collectionField,
|
||||||
|
noDisabled,
|
||||||
|
targetFieldSchema,
|
||||||
|
});
|
||||||
|
const currentRecordVariable = useRecordVariable({
|
||||||
|
schema: uiSchema,
|
||||||
|
collectionName: blockCollectionName,
|
||||||
|
collectionField,
|
||||||
|
noDisabled,
|
||||||
|
targetFieldSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
const compatOldVariables = useCallback(
|
||||||
|
(variables: __UNSAFE__VariableInputOption[], { value }) => {
|
||||||
|
if (!isVariable(value)) {
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
variables = _.cloneDeep(variables);
|
||||||
|
|
||||||
|
const systemVariable: __UNSAFE__VariableInputOption = {
|
||||||
|
value: '$system',
|
||||||
|
key: '$system',
|
||||||
|
label: t('System variables'),
|
||||||
|
isLeaf: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'now',
|
||||||
|
key: 'now',
|
||||||
|
label: t('Current time'),
|
||||||
|
isLeaf: true,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
depth: 0,
|
||||||
|
};
|
||||||
|
const currentTime = {
|
||||||
|
value: 'currentTime',
|
||||||
|
label: t('Current time'),
|
||||||
|
children: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value.includes('$system')) {
|
||||||
|
variables.push(systemVariable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes(`${blockCollectionName}.`)) {
|
||||||
|
const variable = variables.find((item) => item.value === '$nForm' || item.value === '$nRecord');
|
||||||
|
if (variable) {
|
||||||
|
variable.value = blockCollectionName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes('$form')) {
|
||||||
|
const variable = variables.find((item) => item.value === '$nForm');
|
||||||
|
if (variable) {
|
||||||
|
variable.value = '$form';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes('currentUser')) {
|
||||||
|
const userVariable = variables.find((item) => item.value === '$user');
|
||||||
|
if (userVariable) {
|
||||||
|
userVariable.value = 'currentUser';
|
||||||
|
} else {
|
||||||
|
variables.unshift({ ...lowLevelUserVariable, value: 'currentUser' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes('currentRecord')) {
|
||||||
|
const formVariable = variables.find((item) => item.value === '$nRecord');
|
||||||
|
if (formVariable) {
|
||||||
|
formVariable.value = 'currentRecord';
|
||||||
|
} else {
|
||||||
|
variables.unshift({ ...currentRecordVariable, value: 'currentRecord' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes('currentTime')) {
|
||||||
|
variables.push(currentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.includes('$date')) {
|
||||||
|
const formVariable = variables.find((item) => item.value === '$nDate');
|
||||||
|
if (formVariable) {
|
||||||
|
formVariable.value = '$date';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
},
|
||||||
|
[blockCollectionName],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { compatOldVariables };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVariableCustomOptions = (fields) => {
|
||||||
|
const field = useField<any>();
|
||||||
|
const { operator, schema } = field.data || {};
|
||||||
|
const userVariable = useUserVariable({
|
||||||
|
collectionField: { uiSchema: schema },
|
||||||
|
uiSchema: schema,
|
||||||
|
});
|
||||||
|
const dateVariable = useDateVariable({ operator, schema });
|
||||||
|
const filterVariable = useFilterVariable(fields);
|
||||||
|
|
||||||
|
const result = useMemo(
|
||||||
|
() => [userVariable, dateVariable, filterVariable].filter(Boolean),
|
||||||
|
[dateVariable, userVariable, filterVariable],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!operator || !schema) return [];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFilterVariable = (fields) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const compile = useCompile();
|
||||||
|
const { collections } = useCollectionManager_deprecated();
|
||||||
|
const customFields = [];
|
||||||
|
for (const field in fields) {
|
||||||
|
customFields.push({
|
||||||
|
key: field,
|
||||||
|
...fields[field],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = customFields
|
||||||
|
.filter((value) => value?.props?.name.includes('custom.'))
|
||||||
|
.map((custom) => {
|
||||||
|
const value = custom?.props?.name.replace(/^custom\./, '');
|
||||||
|
const collection = collections.filter((collection) => collection.name === value)[0];
|
||||||
|
let children = collection.fields.map((collectionField) => {
|
||||||
|
if (
|
||||||
|
collectionField.interface !== 'o2o' &&
|
||||||
|
collectionField.interface !== 'oho' &&
|
||||||
|
collectionField.interface !== 'm2o' &&
|
||||||
|
collectionField.interface !== 'createdBy' &&
|
||||||
|
collectionField.interface !== 'updatedBy' &&
|
||||||
|
collectionField.interface !== 'o2m' &&
|
||||||
|
collectionField.interface !== 'm2m' &&
|
||||||
|
collectionField.interface !== 'linkTo' &&
|
||||||
|
collectionField.interface !== 'chinaRegion' &&
|
||||||
|
collectionField.interface !== 'obo' &&
|
||||||
|
collectionField.interface !== 'createdAt' &&
|
||||||
|
collectionField.interface !== 'updatedAt'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
key: collectionField.key,
|
||||||
|
value: collectionField.name,
|
||||||
|
label: compile(collectionField.uiSchema.title),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
children = children.filter(Boolean);
|
||||||
|
return children.length
|
||||||
|
? {
|
||||||
|
key: custom?.props?.name,
|
||||||
|
value,
|
||||||
|
label: custom.title,
|
||||||
|
children,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key: custom?.props?.name,
|
||||||
|
value,
|
||||||
|
label: custom.title,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const result = useMemo(
|
||||||
|
() => ({
|
||||||
|
label: t('Current filter'),
|
||||||
|
value: '$nFilter',
|
||||||
|
key: '$nFilter',
|
||||||
|
children: options,
|
||||||
|
}),
|
||||||
|
[options, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options.length) return null;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import { ObjectField } from '@formily/core';
|
||||||
|
import { Schema } from '@formily/react';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export interface FilterContextProps {
|
||||||
|
field?: ObjectField & { collectionName?: string };
|
||||||
|
fieldSchema?: Schema;
|
||||||
|
dynamicComponent?: any;
|
||||||
|
options?: any[];
|
||||||
|
disabled?: boolean;
|
||||||
|
collectionName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RemoveConditionContext = createContext(null);
|
||||||
|
export const FilterContext = createContext<FilterContextProps>(null);
|
||||||
|
export const FilterLogicContext = createContext(null);
|
@ -0,0 +1,21 @@
|
|||||||
|
import { SchemaSettingsItem } from '@nocobase/client';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { useTranslation } from '../../locale';
|
||||||
|
import { Modal } from 'antd';
|
||||||
|
import { GroupBlockContext } from '../../schema-initializer/GroupBlockInitializer';
|
||||||
|
|
||||||
|
export const GroupBlockConfigure = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setVisible, visible } = useContext(GroupBlockContext);
|
||||||
|
return (
|
||||||
|
<SchemaSettingsItem
|
||||||
|
title="Configure"
|
||||||
|
key="configure"
|
||||||
|
onClick={async () => {
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Configure')}
|
||||||
|
</SchemaSettingsItem>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,149 @@
|
|||||||
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { useDesignable, useFieldNames } from '@nocobase/client';
|
||||||
|
import { App, Button, Flex, Modal, Select } from 'antd';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from '../../locale';
|
||||||
|
import { GroupBlockContext } from '../../schema-initializer/GroupBlockInitializer';
|
||||||
|
import { transformers } from './transformers';
|
||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export type SelectedField = {
|
||||||
|
field: string | string[];
|
||||||
|
alias?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GroupConfigure = (props) => {
|
||||||
|
const { visible, setVisible } = useContext(GroupBlockContext);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const measure = fieldSchema['x-decorator-props']?.params?.measures;
|
||||||
|
const [fieldOption, setFieldOption] = useState(
|
||||||
|
JSON.parse(JSON.stringify(measure.filter((item) => item.fieldFormat))),
|
||||||
|
);
|
||||||
|
const valueOption = measure.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.label,
|
||||||
|
value: item.field[0],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const decimal = transformers.option.filter((item) => item.value === 'decimal')[0].childrens;
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('Configure Group')}
|
||||||
|
open={visible}
|
||||||
|
onOk={() => {
|
||||||
|
measure.forEach((measureItem) => {
|
||||||
|
delete measureItem['fieldFormat'];
|
||||||
|
fieldOption.forEach((fieldOptionItem) => {
|
||||||
|
if (measureItem.field[0] === fieldOptionItem?.fieldFormat?.fieldValue) {
|
||||||
|
measureItem['fieldFormat'] = { ...fieldOptionItem.fieldFormat };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fieldSchema['x-decorator-props']['params'].measures = measure;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: fieldSchema,
|
||||||
|
});
|
||||||
|
setVisible(false);
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Are you sure to cancel?'),
|
||||||
|
content: t('You changes are not saved. If you click OK, your changes will be lost.'),
|
||||||
|
okButtonProps: {
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
onOk: () => {
|
||||||
|
setVisible(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
width={'30%'}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', marginBottom: '10px', marginTop: '10px', flexDirection: 'column' }}>
|
||||||
|
{fieldOption.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: '7px' }} key={index}>
|
||||||
|
<Select
|
||||||
|
options={valueOption}
|
||||||
|
placeholder="Field"
|
||||||
|
style={{ marginRight: '5px' }}
|
||||||
|
value={item?.fieldFormat?.fieldValue ? item?.fieldFormat?.fieldValue : undefined}
|
||||||
|
onChange={(v) => {
|
||||||
|
fieldConfig('field', fieldOption, setFieldOption, index, v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
options={transformers.option}
|
||||||
|
placeholder="Format"
|
||||||
|
style={{ marginRight: '5px' }}
|
||||||
|
value={item?.fieldFormat?.option ? item?.fieldFormat?.option : undefined}
|
||||||
|
onChange={(v) => {
|
||||||
|
fieldConfig('format', fieldOption, setFieldOption, index, v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{item?.fieldFormat?.option === 'decimal' ? (
|
||||||
|
<Select
|
||||||
|
options={decimal}
|
||||||
|
placeholder="Digits"
|
||||||
|
style={{ marginRight: '5px' }}
|
||||||
|
value={item.fieldFormat.decimal}
|
||||||
|
onChange={(v) => fieldConfig('decimal', fieldOption, setFieldOption, index, v)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<DeleteOutlined
|
||||||
|
style={{ fontSize: '14px' }}
|
||||||
|
onClick={() => {
|
||||||
|
fieldConfig('del', fieldOption, setFieldOption, index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
style={{ borderRadius: '0', width: '30%' }}
|
||||||
|
onClick={() => {
|
||||||
|
fieldConfig('add', fieldOption, setFieldOption);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Add Field
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldConfig = (type, fieldOption, setFieldOption, index?, record?) => {
|
||||||
|
const option = [...fieldOption];
|
||||||
|
if (type === 'add') {
|
||||||
|
option.push({});
|
||||||
|
} else if (type === 'del') {
|
||||||
|
option.splice(index, 1);
|
||||||
|
} else if (type === 'field') {
|
||||||
|
option[index]['fieldFormat'] = {
|
||||||
|
...option[index]['fieldFormat'],
|
||||||
|
fieldValue: record,
|
||||||
|
};
|
||||||
|
} else if (type === 'format') {
|
||||||
|
option[index]['fieldFormat'] = {
|
||||||
|
...option[index]['fieldFormat'],
|
||||||
|
option: record,
|
||||||
|
};
|
||||||
|
} else if (type === 'decimal') {
|
||||||
|
option[index]['fieldFormat'] = {
|
||||||
|
...option[index]['fieldFormat'],
|
||||||
|
decimal: record,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setFieldOption(option);
|
||||||
|
};
|
@ -0,0 +1,91 @@
|
|||||||
|
export const transformers = {
|
||||||
|
option: [
|
||||||
|
{
|
||||||
|
label: 'Percent',
|
||||||
|
value: 'pertent',
|
||||||
|
component: (val: number) =>
|
||||||
|
new Intl.NumberFormat('en-US', { style: 'percent', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(
|
||||||
|
val,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Currency',
|
||||||
|
value: 'currency',
|
||||||
|
component: (val: number, locale = 'en-US') => {
|
||||||
|
const currency = {
|
||||||
|
'zh-CN': 'CNY',
|
||||||
|
'en-US': 'USD',
|
||||||
|
'ja-JP': 'JPY',
|
||||||
|
'ko-KR': 'KRW',
|
||||||
|
'pt-BR': 'BRL',
|
||||||
|
'ru-RU': 'RUB',
|
||||||
|
'tr-TR': 'TRY',
|
||||||
|
'es-ES': 'EUR',
|
||||||
|
}[locale];
|
||||||
|
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(val);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: 'Exponential', value: 'exponential', component: (val: number | string) => (+val)?.toExponential() },
|
||||||
|
{
|
||||||
|
label: 'Abbreviation',
|
||||||
|
value: 'abbreviation',
|
||||||
|
component: (val: number, locale = 'en-US') => new Intl.NumberFormat(locale, { notation: 'compact' }).format(val),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Decimal',
|
||||||
|
value: 'decimal',
|
||||||
|
childrens: [
|
||||||
|
{
|
||||||
|
label: '1.0',
|
||||||
|
value: 'OneDigits',
|
||||||
|
component: (val: number) =>
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
}).format(val),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1.00',
|
||||||
|
value: 'TwoDigits',
|
||||||
|
component: (val: number) =>
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(val),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1.000',
|
||||||
|
value: 'ThreeDigits',
|
||||||
|
component: (val: number) =>
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: 3,
|
||||||
|
maximumFractionDigits: 3,
|
||||||
|
}).format(val),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1.0000',
|
||||||
|
value: 'FourDigits',
|
||||||
|
component: (val: number) =>
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: 4,
|
||||||
|
maximumFractionDigits: 4,
|
||||||
|
}).format(val),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '1.00000',
|
||||||
|
value: 'FiveDigits',
|
||||||
|
component: (val: number) =>
|
||||||
|
new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'decimal',
|
||||||
|
minimumFractionDigits: 5,
|
||||||
|
maximumFractionDigits: 5,
|
||||||
|
}).format(val),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,103 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button, Drawer, Form, Input, Select, Space, Table } from 'antd';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import { useRequest } from '@nocobase/client';
|
||||||
|
export const LinkManager = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [formData, setFormData] = useState<any>({});
|
||||||
|
const [tableData, setTableData] = useState<any>([]);
|
||||||
|
const { data, run } = useRequest<{ data: any }>({
|
||||||
|
url: `/link-manage:get`,
|
||||||
|
});
|
||||||
|
const { run: updateLink } = useRequest<{ data: any }>({
|
||||||
|
url: `/link-manage:set`,
|
||||||
|
params: { id: formData.id, link: formData.link },
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setTableData(data.data);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
const showDrawer = (record) => {
|
||||||
|
setFormData(record);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
const onClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
const update = () => {
|
||||||
|
updateLink();
|
||||||
|
// 更新表格数据
|
||||||
|
const mergedArray = tableData.map((item) => {
|
||||||
|
const matchingItem = item.id == formData.id;
|
||||||
|
if (matchingItem) {
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
setTableData(mergedArray);
|
||||||
|
setOpen(false);
|
||||||
|
setFormData({});
|
||||||
|
};
|
||||||
|
const columns: ColumnsType = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
render: (text) => <a>{text}</a>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '链接',
|
||||||
|
dataIndex: 'link',
|
||||||
|
key: 'link',
|
||||||
|
render: (text) => <a>{text}</a>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<a onClick={() => showDrawer(record)}>编辑</a>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tableData && <Table columns={columns} dataSource={tableData}></Table>}
|
||||||
|
<Drawer
|
||||||
|
title="设定链接地址"
|
||||||
|
width={720}
|
||||||
|
onClose={onClose}
|
||||||
|
open={open}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
paddingBottom: 80,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={onClose}>关闭</Button>
|
||||||
|
<Button onClick={update} type="primary">
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item name="name" label="Name">
|
||||||
|
<Input placeholder="Please enter Name" defaultValue={formData?.name} disabled />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="link" label="Link">
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setFormData({ ...formData, link: e.target.value })}
|
||||||
|
placeholder="Please enter Link"
|
||||||
|
defaultValue={formData?.link}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from '@nocobase/client';
|
||||||
|
import { Button, Tooltip } from 'antd';
|
||||||
|
import { MobileOutlined } from '@ant-design/icons';
|
||||||
|
import { useTranslation } from '../locale';
|
||||||
|
|
||||||
|
export const MobileLink = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
.ant-btn {
|
||||||
|
border: 0;
|
||||||
|
height: 46px;
|
||||||
|
width: 46px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{ display: 'inline-block' }}
|
||||||
|
>
|
||||||
|
<Tooltip title={t('Mobile UI')} placement="bottom">
|
||||||
|
<Button role="button" href="/mobile">
|
||||||
|
<MobileOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css, useRequest } from '@nocobase/client';
|
||||||
|
import { Badge, Button, Popover, List, message } from 'antd';
|
||||||
|
import { BellOutlined } from '@ant-design/icons';
|
||||||
|
import { useLinkKey, useInitializationLinkKey } from '../hooks/useNotifications';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { dayjs } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
export const Notifications = () => {
|
||||||
|
// 注册链接关联,链接管理使用nocobase代码方式注册数据表
|
||||||
|
useInitializationLinkKey();
|
||||||
|
const linkDetail = useLinkKey();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { data, run } = useRequest<{ data: any }>({
|
||||||
|
url: `/system_notifications:get`,
|
||||||
|
});
|
||||||
|
const getNotificationList = data?.data || [];
|
||||||
|
const { run: updateRead } = useRequest<{ data: any }>({
|
||||||
|
url: `/system_notifications:update`,
|
||||||
|
params: {
|
||||||
|
ids: getNotificationList.map((item) => item.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const content = (
|
||||||
|
<List
|
||||||
|
style={{ width: '400px' }}
|
||||||
|
itemLayout="horizontal"
|
||||||
|
dataSource={getNotificationList}
|
||||||
|
footer={
|
||||||
|
<Button.Group style={{ width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
updateRead();
|
||||||
|
run();
|
||||||
|
message.info('成功');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
全部已读
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
navigate(linkDetail?.link);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
查看更多
|
||||||
|
</Button>
|
||||||
|
</Button.Group>
|
||||||
|
}
|
||||||
|
renderItem={(item: any) => (
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta title={item.title} description={dayjs(item.createdAt).calendar()} />
|
||||||
|
{item.content}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
.ant-btn {
|
||||||
|
border: 0;
|
||||||
|
height: 46px;
|
||||||
|
width: 46px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
.ant-badge-count {
|
||||||
|
min-width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
line-height: 10px;
|
||||||
|
font-size: 9px;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{ display: 'inline-block' }}
|
||||||
|
>
|
||||||
|
<Popover content={content}>
|
||||||
|
<Badge count={getNotificationList.length} size="small" offset={[-15, 15]}>
|
||||||
|
<Button role="button">
|
||||||
|
<BellOutlined />
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { css, useAPIClient, useApp } from '@nocobase/client';
|
||||||
|
import { Button, Dropdown } from 'antd';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const OnlineUserManger = () => {
|
||||||
|
const app = useApp();
|
||||||
|
const [onlineUserItems, setOnlineUserItems] = useState([]);
|
||||||
|
const api = useAPIClient();
|
||||||
|
useEffect(() => {
|
||||||
|
app.ws.on('message', (event: MessageEvent) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data?.type === 'plugin-online-user') {
|
||||||
|
const onlineUserItems = data.payload.users?.map((user) => {
|
||||||
|
if (user) {
|
||||||
|
return {
|
||||||
|
key: uid(),
|
||||||
|
label: user.nickname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setOnlineUserItems(onlineUserItems);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [app]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const data = {
|
||||||
|
type: 'plugin-online-user:client',
|
||||||
|
payload: {
|
||||||
|
token: api.auth.getToken(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
app.ws.send(JSON.stringify(data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown menu={{ items: onlineUserItems }}>
|
||||||
|
<Button style={{ width: 'auto' }} type="text">
|
||||||
|
在线 {_.size(onlineUserItems)} 人
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OnlineUserDropdown = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
.ant-btn {
|
||||||
|
border: 0;
|
||||||
|
height: 46px;
|
||||||
|
width: 46px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none;
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{ display: 'inline-block' }}
|
||||||
|
>
|
||||||
|
<OnlineUserManger />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Popover, Space, Button, Input } from 'antd';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import { useProps } from '@nocobase/client';
|
||||||
|
import { ShareAltOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const OutboundButton = (props) => {
|
||||||
|
const { onClick } = useProps(props);
|
||||||
|
const schema = useFieldSchema();
|
||||||
|
const url = window.location.href.split('/', 3).join('/');
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
placement="bottomRight"
|
||||||
|
trigger="click"
|
||||||
|
autoAdjustOverflow
|
||||||
|
content={
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Input defaultValue={`${url}/r/${schema['x-uid']}`} />
|
||||||
|
<Button type="primary" onClick={onClick}>
|
||||||
|
复制链接
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<ShareAltOutlined />
|
||||||
|
外链
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,100 @@
|
|||||||
|
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Document, Page, pdfjs } from 'react-pdf';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||||
|
import 'react-pdf/dist/Page/TextLayer.css';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { useRequest } from '@nocobase/client';
|
||||||
|
import { useTranslation } from '../locale';
|
||||||
|
// TODO CMap settings.
|
||||||
|
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cat.net/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
|
||||||
|
|
||||||
|
interface PDFViewerProps {
|
||||||
|
file: string;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PDFViewerRef {
|
||||||
|
download: () => void;
|
||||||
|
print: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingSpin = ({ children, spinning }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Spin tip={t('loading...')} indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={spinning}>
|
||||||
|
{children}
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PDFViewer = forwardRef<PDFViewerRef, PDFViewerProps>((props, ref) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [numPages, setNumPages] = useState<number>(0);
|
||||||
|
const [contentWindow, setContentWindow] = useState<Window>(null);
|
||||||
|
const { file, width = 960 } = props;
|
||||||
|
const { loading, data, run } = useRequest({
|
||||||
|
url: file,
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
download() {
|
||||||
|
const blob = new Blob([data as ArrayBuffer], { type: 'application/pdf' });
|
||||||
|
saveAs(blob, uid() + '.pdf');
|
||||||
|
},
|
||||||
|
print() {
|
||||||
|
contentWindow.print();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
useEffect(() => {
|
||||||
|
run();
|
||||||
|
}, [file]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = new Blob([data as ArrayBuffer], { type: 'application/pdf' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
iframe.src = url;
|
||||||
|
iframe.onload = () => {
|
||||||
|
setContentWindow(iframe.contentWindow);
|
||||||
|
};
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
return () => {
|
||||||
|
// 需要释放资源
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
};
|
||||||
|
}, [data, loading]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadingSpin spinning={loading}>
|
||||||
|
<Document
|
||||||
|
file={data as ArrayBuffer}
|
||||||
|
loading={(props) => (
|
||||||
|
<LoadingSpin {...props} spinning={true}>
|
||||||
|
<div style={{ height: '100vh' }}></div>
|
||||||
|
</LoadingSpin>
|
||||||
|
)}
|
||||||
|
onLoadSuccess={async (doc) => {
|
||||||
|
setNumPages(doc.numPages);
|
||||||
|
}}
|
||||||
|
noData={<div style={{ height: '100vh' }}></div>}
|
||||||
|
error={<div>{t('error')}</div>}
|
||||||
|
>
|
||||||
|
{Array.from(new Array(numPages), (el, index) => (
|
||||||
|
<Page width={width} key={`page_${index + 1}`} pageNumber={index + 1}>
|
||||||
|
<p style={{ marginLeft: '92%', marginBottom: '30pt', fontSize: '14px', fontFamily: 'source-han-sans' }}>
|
||||||
|
{index + 1}/{numPages}页
|
||||||
|
</p>
|
||||||
|
</Page>
|
||||||
|
))}
|
||||||
|
</Document>
|
||||||
|
</LoadingSpin>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,378 @@
|
|||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { ISchema, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import {
|
||||||
|
SchemaSettingsLinkageRules,
|
||||||
|
SchemaSettingsModalItem,
|
||||||
|
SchemaSettingsSelectItem,
|
||||||
|
SchemaSettingsSwitchItem,
|
||||||
|
css,
|
||||||
|
mergeFilter,
|
||||||
|
removeNullCondition,
|
||||||
|
useBlockRequestContext,
|
||||||
|
useCollection_deprecated,
|
||||||
|
useCollectionManager_deprecated,
|
||||||
|
useCompile,
|
||||||
|
useDesignable,
|
||||||
|
useFieldModeOptions,
|
||||||
|
useFilterFieldProps,
|
||||||
|
useFilterOptions,
|
||||||
|
useFormBlockContext,
|
||||||
|
useFormBlockType,
|
||||||
|
useLinkageCollectionFilterOptions,
|
||||||
|
useLocalVariables,
|
||||||
|
useProps,
|
||||||
|
useRecord,
|
||||||
|
useSchemaTemplateManager,
|
||||||
|
useSortFields,
|
||||||
|
useVariables,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import _, { get } from 'lodash';
|
||||||
|
import React, { useCallback, useContext, useMemo } from 'react';
|
||||||
|
import { useTranslation } from '../locale';
|
||||||
|
import { FormFilterScope } from './FormFilter/FormFilterScope';
|
||||||
|
|
||||||
|
export const useFormulaTitleOptions = () => {
|
||||||
|
const compile = useCompile();
|
||||||
|
const { getCollectionJoinField, getCollectionFields } = useCollectionManager_deprecated();
|
||||||
|
const { getField } = useCollection_deprecated();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const collectionManage = useCollectionManager_deprecated();
|
||||||
|
const collectionManageField = fieldSchema['x-compoent-custom']
|
||||||
|
? collectionManage.collections.filter((value) => value.name === fieldSchema['x-decorator-props'])[0]
|
||||||
|
: {};
|
||||||
|
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||||
|
let fields = [];
|
||||||
|
if (collectionField) {
|
||||||
|
fields = getCollectionFields(
|
||||||
|
collectionField
|
||||||
|
? collectionField.target
|
||||||
|
? collectionField.target
|
||||||
|
: collectionField.collectionName
|
||||||
|
: fieldSchema['name'],
|
||||||
|
);
|
||||||
|
} else if (collectionManageField) {
|
||||||
|
fields = collectionManageField['fields'];
|
||||||
|
}
|
||||||
|
const options = [];
|
||||||
|
fields?.forEach((field) => {
|
||||||
|
if (field.interface !== 'm2m') {
|
||||||
|
if (field.uiSchema) {
|
||||||
|
options.push({
|
||||||
|
label: compile(field.uiSchema.title),
|
||||||
|
value: field.name,
|
||||||
|
children:
|
||||||
|
getCollectionFields(field.target)
|
||||||
|
?.filter((subField) => subField.uiSchema)
|
||||||
|
.map((subField) => ({
|
||||||
|
label: subField.uiSchema ? compile(subField.uiSchema.title) : '',
|
||||||
|
value: subField.name,
|
||||||
|
})) ?? [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFormulaTitleVisible = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const options = useFormulaTitleOptions();
|
||||||
|
// FIXME 这里现在只有当设置为 select,默认为 select 的时候看不到
|
||||||
|
return (
|
||||||
|
options.length > 0 &&
|
||||||
|
((fieldSchema['x-component-props']?.mode === 'Select' &&
|
||||||
|
fieldSchema['x-component-props']?.fieldNames?.value !== undefined &&
|
||||||
|
fieldSchema['x-component'] === 'CollectionField') ||
|
||||||
|
fieldSchema['x-compoent-custom'])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditFormulaTitleField = () => {
|
||||||
|
const { getCollectionJoinField, collections } = useCollectionManager_deprecated();
|
||||||
|
const { getField } = useCollection_deprecated();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
|
||||||
|
let collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||||
|
collectionField = collectionField
|
||||||
|
? collectionField
|
||||||
|
: collections.find((value) => value.name === fieldSchema['x-decorator-props'])?.fields;
|
||||||
|
const options = useFormulaTitleOptions();
|
||||||
|
const editTitle = async (formula) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
const fieldNames = {
|
||||||
|
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
|
||||||
|
...field.componentProps.fieldNames,
|
||||||
|
formula,
|
||||||
|
};
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
key="edit-field-title"
|
||||||
|
title={t('Custom option label')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Custom option label'),
|
||||||
|
properties: {
|
||||||
|
formula: {
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Variable.TextArea',
|
||||||
|
'x-component-props': {
|
||||||
|
scope: options,
|
||||||
|
},
|
||||||
|
default: field?.componentProps?.fieldNames?.formula || '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ formula }) => {
|
||||||
|
if (formula) {
|
||||||
|
editTitle(formula);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePaginationVisible = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return fieldSchema['x-component-props']?.mode === 'SubTable';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSetFilterScopeVisible = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return fieldSchema['x-component-props']?.useProps === '{{ useFilterBlockActionProps }}';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditTitle = () => {
|
||||||
|
const { getCollectionJoinField } = useCollectionManager_deprecated();
|
||||||
|
const { getField } = useCollection_deprecated();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||||
|
return (
|
||||||
|
<SchemaSettingsModalItem
|
||||||
|
key="edit-field-title"
|
||||||
|
title={t('Edit field title')}
|
||||||
|
schema={
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
title: t('Edit field title'),
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
title: t('Field title'),
|
||||||
|
default: field?.title,
|
||||||
|
description: `${t('Original field title: ')}${
|
||||||
|
collectionField ? collectionField?.uiSchema?.title : fieldSchema['name']
|
||||||
|
}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ISchema
|
||||||
|
}
|
||||||
|
onSubmit={({ title }) => {
|
||||||
|
if (title) {
|
||||||
|
field.title = title;
|
||||||
|
fieldSchema.title = title;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
title: fieldSchema.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditTitleField = () => {
|
||||||
|
const { getCollectionFields, getCollectionJoinField } = useCollectionManager_deprecated();
|
||||||
|
const { getField } = useCollection_deprecated();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const compile = useCompile();
|
||||||
|
const collectionManage = useCollectionManager_deprecated();
|
||||||
|
const collectionManageField = fieldSchema['x-compoent-custom']
|
||||||
|
? collectionManage.collections.filter((value) => value.name === fieldSchema['x-decorator-props'])[0]
|
||||||
|
: {};
|
||||||
|
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||||
|
let targetFields = [];
|
||||||
|
if (collectionField) {
|
||||||
|
targetFields = collectionField?.target
|
||||||
|
? getCollectionFields(collectionField?.target)
|
||||||
|
: getCollectionFields(collectionField?.targetCollection) ?? [];
|
||||||
|
} else if (collectionManageField) {
|
||||||
|
targetFields = collectionManageField['fields'];
|
||||||
|
}
|
||||||
|
const options = targetFields
|
||||||
|
.filter((field) => !field?.target && field.type !== 'boolean')
|
||||||
|
.map((field) => ({
|
||||||
|
value: field?.name,
|
||||||
|
label: compile(field?.uiSchema?.title) || field?.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return options.length > 0 &&
|
||||||
|
(fieldSchema['x-component'] === 'CollectionField' || fieldSchema['x-compoent-custom']) ? (
|
||||||
|
<SchemaSettingsSelectItem
|
||||||
|
key="title-field"
|
||||||
|
title={t('Title field')}
|
||||||
|
options={options}
|
||||||
|
value={field?.componentProps?.fieldNames?.label}
|
||||||
|
onChange={(label) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
const fieldNames = {
|
||||||
|
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
|
||||||
|
...field.componentProps.fieldNames,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IsTablePageSize = () => {
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<SchemaSettingsSwitchItem
|
||||||
|
title={t('Pagination')}
|
||||||
|
checked={fieldSchema['x-component-props'].pagination}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (!fieldSchema['x-component-props'].pagination) {
|
||||||
|
fieldSchema['x-component-props'] = {
|
||||||
|
...fieldSchema['x-component-props'],
|
||||||
|
pagination: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
fieldSchema['x-component-props'].pagination = v;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
'x-component-props': {
|
||||||
|
...fieldSchema?.['x-component-props'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findGridSchema = (fieldSchema) => {
|
||||||
|
return fieldSchema.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-component'] === 'FormV2') {
|
||||||
|
const f = s.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-component'] === 'Grid' || s['x-component'] === 'BlockTemplate') {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null);
|
||||||
|
if (f) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SetFilterScope = (props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField<Field>();
|
||||||
|
const fields = field.form.fields;
|
||||||
|
const { collectionName } = props;
|
||||||
|
const gridSchema = findGridSchema(fieldSchema) || fieldSchema;
|
||||||
|
const { form } = useFormBlockContext();
|
||||||
|
const type = props?.type || ['Action', 'Action.Link'].includes(fieldSchema['x-component']) ? 'button' : 'field';
|
||||||
|
const variables = useVariables();
|
||||||
|
const localVariables = useLocalVariables();
|
||||||
|
const record = useRecord();
|
||||||
|
const { type: formBlockType } = useFormBlockType();
|
||||||
|
const schema = useMemo<ISchema>(
|
||||||
|
() => ({
|
||||||
|
type: 'object',
|
||||||
|
title: t('Custom filter'),
|
||||||
|
properties: {
|
||||||
|
fieldReaction: {
|
||||||
|
'x-component': FormFilterScope,
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: () => {
|
||||||
|
const options = useLinkageCollectionFilterOptions(collectionName);
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
defaultValues: gridSchema?.['x-filter-rules'] || fieldSchema?.['x-filter-rules'],
|
||||||
|
type,
|
||||||
|
collectionName,
|
||||||
|
form,
|
||||||
|
variables,
|
||||||
|
localVariables,
|
||||||
|
record,
|
||||||
|
formBlockType,
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const { getTemplateById } = useSchemaTemplateManager();
|
||||||
|
const { dn } = useDesignable();
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(v) => {
|
||||||
|
const rules = v.fieldReaction.condition;
|
||||||
|
const templateId = gridSchema['x-component'] === 'BlockTemplate' && gridSchema['x-component-props'].templateId;
|
||||||
|
const uid = (templateId && getTemplateById(templateId).uid) || gridSchema['x-uid'];
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: uid,
|
||||||
|
};
|
||||||
|
|
||||||
|
gridSchema['x-filter-rules'] = rules;
|
||||||
|
schema['x-filter-rules'] = rules;
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
dn.refresh();
|
||||||
|
},
|
||||||
|
[dn, getTemplateById, gridSchema],
|
||||||
|
);
|
||||||
|
return <SchemaSettingsModalItem title={t('Custom filter')} width={770} schema={schema} onSubmit={onSubmit} />;
|
||||||
|
};
|
@ -0,0 +1,48 @@
|
|||||||
|
import { connect, mapProps } from '@formily/react';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
import SignaturePad from './SignaturePad';
|
||||||
|
|
||||||
|
export const SignatureInput = connect(
|
||||||
|
(props) => {
|
||||||
|
const { onChange, value } = props;
|
||||||
|
const signatureRef = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
signatureRef.current.fromDataURL(value);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const handleClear = () => {
|
||||||
|
if (signatureRef.current) {
|
||||||
|
signatureRef.current.clear();
|
||||||
|
onChange && onChange('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleSave = () => {
|
||||||
|
if (signatureRef.current) {
|
||||||
|
const signatureDataURL = signatureRef.current.toDataURL();
|
||||||
|
onChange && onChange(signatureDataURL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const divStyle: any = {
|
||||||
|
pointerEvents: props.disabled ? 'none' : 'auto',
|
||||||
|
opacity: props.disabled ? 0.8 : 1,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div style={divStyle}>
|
||||||
|
<SignaturePad ref={signatureRef} options={{ penColor: 'black', backgroundColor: 'lightgrey' }} />
|
||||||
|
{!props.disabled && (
|
||||||
|
<div>
|
||||||
|
<Button onClick={handleClear}>清空</Button>
|
||||||
|
<Button onClick={handleSave}>确认</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
mapProps((props, field) => {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
@ -0,0 +1,394 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import SigPad, { Options, PointGroup, ToSVGOptions } from 'signature_pad';
|
||||||
|
import { debounce } from 'throttle-debounce';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
options?: Options;
|
||||||
|
canvasProps?: { [key: string]: string | { [key: string]: string } };
|
||||||
|
} & DefaultProps;
|
||||||
|
|
||||||
|
type DefaultProps = {
|
||||||
|
redrawOnResize: boolean;
|
||||||
|
debounceInterval: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
canvasWidth: number;
|
||||||
|
canvasHeight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class
|
||||||
|
* @classdesc Signature pad component.
|
||||||
|
* @extends {PureComponent}
|
||||||
|
*/
|
||||||
|
class SignaturePad extends React.PureComponent<Props, State> {
|
||||||
|
static displayName = 'react-signature-pad-wrapper';
|
||||||
|
|
||||||
|
static defaultProps: DefaultProps = {
|
||||||
|
redrawOnResize: false,
|
||||||
|
debounceInterval: 150,
|
||||||
|
};
|
||||||
|
|
||||||
|
private canvasRef = React.createRef<HTMLCanvasElement>();
|
||||||
|
|
||||||
|
private signaturePad!: SigPad;
|
||||||
|
|
||||||
|
private callResizeHandler!: debounce<() => void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new signature pad.
|
||||||
|
*
|
||||||
|
* @param {Props} props
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { canvasWidth: 0, canvasHeight: 0 };
|
||||||
|
|
||||||
|
this.callResizeHandler = debounce<() => void>(this.props.debounceInterval, this.handleResize.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise the signature pad once the canvas element is rendered.
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
componentDidMount(): void {
|
||||||
|
const canvas = this.canvasRef.current;
|
||||||
|
|
||||||
|
if (canvas) {
|
||||||
|
if (!this.props.width || !this.props.height) {
|
||||||
|
canvas.style.width = '100%';
|
||||||
|
window.addEventListener('resize', this.callResizeHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.signaturePad = new SigPad(canvas, this.props.options);
|
||||||
|
|
||||||
|
this.scaleCanvas(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the resize event listener and switch the signature pad off on
|
||||||
|
* unmount.
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if (!this.props.width || !this.props.height) {
|
||||||
|
window.removeEventListener('resize', this.callResizeHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.signaturePad.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the original signature_pad instance.
|
||||||
|
*
|
||||||
|
* @return {SigPad}
|
||||||
|
*/
|
||||||
|
get instance(): SigPad {
|
||||||
|
return this.signaturePad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the canvas ref.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
get canvas(): React.RefObject<HTMLCanvasElement> {
|
||||||
|
return this.canvasRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the radius of a single dot.
|
||||||
|
*
|
||||||
|
* @param {number} dotSize
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
set dotSize(dotSize: number) {
|
||||||
|
this.signaturePad.dotSize = dotSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the radius of a single dot.
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get dotSize(): number {
|
||||||
|
return this.signaturePad.dotSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the minimum width of a line.
|
||||||
|
*
|
||||||
|
* @param {number} minWidth
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
set minWidth(minWidth: number) {
|
||||||
|
this.signaturePad.minWidth = minWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the minimum width of a line.
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get minWidth(): number {
|
||||||
|
return this.signaturePad.minWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum width of a line.
|
||||||
|
*
|
||||||
|
* @param {number} maxWidth
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
set maxWidth(maxWidth: number) {
|
||||||
|
this.signaturePad.maxWidth = maxWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum width of a line.
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get maxWidth(): number {
|
||||||
|
return this.signaturePad.maxWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the throttle for drawing the next point at most once every x ms.
|
||||||
|
*
|
||||||
|
* @param {number} throttle
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
set throttle(throttle: number) {
|
||||||
|
this.signaturePad.throttle = throttle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the throttle for drawing the next point at most once every x ms.
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get throttle(): number {
|
||||||
|
return this.signaturePad.throttle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the color used to clear the background.
|
||||||
|
*
|
||||||
|
* @param {string} color
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
set backgroundColor(color: string) {
|
||||||
|
this.signaturePad.backgroundColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color used to clear the background.
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get backgroundColor(): string {
|
||||||
|
return this.signaturePad.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the color used to draw the lines.
|
||||||
|
*
|
||||||
|
* @param {string} color
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
set penColor(color: string) {
|
||||||
|
this.signaturePad.penColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color used to draw the lines.
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
get penColor(): string {
|
||||||
|
return this.signaturePad.penColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set weight used to modify new velocity based on the previous velocity.
|
||||||
|
*
|
||||||
|
* @param {number} weight
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
set velocityFilterWeight(weight: number) {
|
||||||
|
this.signaturePad.velocityFilterWeight = weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get weight used to modify new velocity based on the previous velocity.
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
get velocityFilterWeight(): number {
|
||||||
|
return this.signaturePad.velocityFilterWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the canvas is empty.
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isEmpty(): boolean {
|
||||||
|
return this.signaturePad.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the canvas.
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
this.signaturePad.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a signature from a data URL.
|
||||||
|
*
|
||||||
|
* @param {string} dataUrl
|
||||||
|
* @param {object} options
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
fromDataURL(
|
||||||
|
dataUrl: string,
|
||||||
|
options: Partial<{ ratio: number; width: number; height: number; xOffset: number; yOffset: number }> = {},
|
||||||
|
): void {
|
||||||
|
this.signaturePad.fromDataURL(dataUrl, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the signature data as a data URL.
|
||||||
|
*
|
||||||
|
* @param {?string} mime
|
||||||
|
* @param {?number} encoderOptions
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
toDataURL(type?: string, encoderOptions?: number): string {
|
||||||
|
return this.signaturePad.toDataURL(type, encoderOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the signature data as an SVG string without converting to base64.
|
||||||
|
*
|
||||||
|
* @param {?ToSVGOptions} svgOptions
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
toSVG(svgOptions?: ToSVGOptions): string {
|
||||||
|
return this.signaturePad.toSVG(svgOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a signature from an array of point groups.
|
||||||
|
*
|
||||||
|
* @param {PointGroup[]} data
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
fromData(data: PointGroup[]): void {
|
||||||
|
this.signaturePad.fromData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the signature pad data an array of point groups.
|
||||||
|
*
|
||||||
|
* @return {PointGroup[]}
|
||||||
|
*/
|
||||||
|
toData(): PointGroup[] {
|
||||||
|
return this.signaturePad.toData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn the signature pad off.
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
off(): void {
|
||||||
|
this.signaturePad.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn the signature pad on.
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
on(): void {
|
||||||
|
this.signaturePad.on();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a resize event.
|
||||||
|
*
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
handleResize(): void {
|
||||||
|
const canvas = this.canvasRef.current;
|
||||||
|
|
||||||
|
if (canvas) {
|
||||||
|
this.scaleCanvas(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale the canvas.
|
||||||
|
*
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
scaleCanvas(canvas: HTMLCanvasElement): void {
|
||||||
|
const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
||||||
|
const width = (this.props.width || canvas.offsetWidth) * ratio;
|
||||||
|
const height = (this.props.height || canvas.offsetHeight) * ratio;
|
||||||
|
|
||||||
|
// Avoid needlessly setting height / width if dimensions haven't changed
|
||||||
|
const { canvasWidth, canvasHeight } = this.state;
|
||||||
|
|
||||||
|
if (width === canvasWidth && height === canvasHeight) return;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (this.props.redrawOnResize && this.signaturePad && !this.signaturePad.isEmpty()) {
|
||||||
|
data = this.signaturePad.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
this.setState({ canvasWidth: width, canvasHeight: height });
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
ctx.scale(ratio, ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
this.signaturePad.fromDataURL(data);
|
||||||
|
} else if (this.signaturePad) {
|
||||||
|
this.signaturePad.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the signature pad component.
|
||||||
|
*
|
||||||
|
* @return {ReactNode}
|
||||||
|
*/
|
||||||
|
render(): React.ReactNode {
|
||||||
|
const { canvasProps } = this.props;
|
||||||
|
|
||||||
|
return <canvas data-testid="canvas-element" ref={this.canvasRef} {...canvasProps} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignaturePad;
|
@ -0,0 +1,105 @@
|
|||||||
|
import { useAPIClient } from '@nocobase/client';
|
||||||
|
import { useBoolean } from 'ahooks';
|
||||||
|
import { Button, Card, Form, Input, Tabs, message } from 'antd';
|
||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { TokenConfigurationResourceKey, getSSKey, useMapConfiguration } from '../hooks/useTokenConfiguration';
|
||||||
|
interface BaseConfigurationProps {
|
||||||
|
type: 'feishu';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TokenTypes = [{ label: '飞书', value: 'feishu' }];
|
||||||
|
|
||||||
|
const BaseConfiguration: React.FC<BaseConfigurationProps> = ({ type, children }) => {
|
||||||
|
const [isDisabled, disableAction] = useBoolean(false);
|
||||||
|
const apiClient = useAPIClient();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const data = useMapConfiguration(type);
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.setFieldsValue(data);
|
||||||
|
disableAction.toggle();
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const resource = useMemo(() => {
|
||||||
|
return apiClient.resource(TokenConfigurationResourceKey);
|
||||||
|
}, [apiClient]);
|
||||||
|
|
||||||
|
const onSubmit = (values) => {
|
||||||
|
resource
|
||||||
|
.set({
|
||||||
|
...values,
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
sessionStorage.removeItem(getSSKey(type));
|
||||||
|
disableAction.toggle();
|
||||||
|
message.success('保存成功');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
message.success('保存失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Form disabled={isDisabled} form={form} layout="vertical" onFinish={onSubmit}>
|
||||||
|
{children}
|
||||||
|
{isDisabled ? (
|
||||||
|
<Button disabled={false} onClick={disableAction.toggle}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Form.Item>
|
||||||
|
<Button disabled={false} type="primary" htmlType="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AMapConfiguration = () => {
|
||||||
|
return (
|
||||||
|
<BaseConfiguration type="feishu">
|
||||||
|
<Form.Item required name="app_id" label="App ID">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required name="app_secret" label="App Secret">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required name="chat_id" label="Chat ID">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</BaseConfiguration>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
feishu: AMapConfiguration,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabList = TokenTypes.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
component: components[item.value],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Configuration = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const search = new URLSearchParams(location.search);
|
||||||
|
return (
|
||||||
|
<Card bordered>
|
||||||
|
<Tabs type="card" defaultActiveKey={search.get('tab')}>
|
||||||
|
{tabList.map((tab) => {
|
||||||
|
return (
|
||||||
|
<Tabs.TabPane key={tab.value} tab={tab.label}>
|
||||||
|
<tab.component type={tab.value} />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,221 @@
|
|||||||
|
import { ISchema, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import {
|
||||||
|
__UNSAFE__,
|
||||||
|
getFormValues,
|
||||||
|
isVariable,
|
||||||
|
transformVariableValue,
|
||||||
|
useActionContext,
|
||||||
|
useBlockRequestContext,
|
||||||
|
useCollection_deprecated,
|
||||||
|
useCompile,
|
||||||
|
useDesignable,
|
||||||
|
useFilterByTk,
|
||||||
|
useFormActiveFields,
|
||||||
|
useLocalVariables,
|
||||||
|
useRecord,
|
||||||
|
useVariables,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { App, message } from 'antd';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { isURL } from '@nocobase/utils/client';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const pageDetailsViewer = 'PageLayout';
|
||||||
|
|
||||||
|
const viewerSchema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("View record") }}',
|
||||||
|
'x-component': pageDetailsViewer,
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
page: {
|
||||||
|
type: 'void',
|
||||||
|
title: '详情页面',
|
||||||
|
'x-designer': 'Page.Designer',
|
||||||
|
'x-component': 'Page',
|
||||||
|
'x-component-props': { disablePageHeader: true },
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'RecordBlockInitializers',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInsertSchema = () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { insertAfterBegin } = useDesignable();
|
||||||
|
const insert = useCallback(
|
||||||
|
(ss) => {
|
||||||
|
const schema = fieldSchema.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-component'] === pageDetailsViewer) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}, null);
|
||||||
|
if (!schema) {
|
||||||
|
insertAfterBegin(_.cloneDeep(ss));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[pageDetailsViewer],
|
||||||
|
);
|
||||||
|
return insert;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateActionProps = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const { field, resource, __parent } = useBlockRequestContext();
|
||||||
|
const { setVisible, fieldSchema } = useActionContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const actionSchema = useFieldSchema();
|
||||||
|
const actionField = useField();
|
||||||
|
const { fields, getField, getTreeParentField, name } = useCollection_deprecated();
|
||||||
|
const compile = useCompile();
|
||||||
|
const filterByTk = useFilterByTk();
|
||||||
|
const currentRecord = useRecord();
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const variables = useVariables();
|
||||||
|
const localVariables = useLocalVariables({ currentForm: form });
|
||||||
|
const { getActiveFieldsName } = useFormActiveFields() || {};
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const action = actionField.componentProps.saveMode || 'create';
|
||||||
|
const filterKeys = actionField.componentProps.filterKeys?.checked || [];
|
||||||
|
const dn = useDesignable();
|
||||||
|
const params = useParams();
|
||||||
|
const insert = useInsertSchema();
|
||||||
|
let fieldSchema2 = useFieldSchema();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
const fieldNames = fields.map((field) => field.name);
|
||||||
|
const {
|
||||||
|
assignedValues: originalAssignedValues = {},
|
||||||
|
onSuccess,
|
||||||
|
overwriteValues,
|
||||||
|
skipValidator,
|
||||||
|
triggerWorkflows,
|
||||||
|
sessionSubmit,
|
||||||
|
} = actionSchema?.['x-action-settings'] ?? {};
|
||||||
|
const addChild = fieldSchema?.['x-component-props']?.addChild;
|
||||||
|
const assignedValues = {};
|
||||||
|
|
||||||
|
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
|
||||||
|
const value = originalAssignedValues[key];
|
||||||
|
const collectionField = getField(key);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
if (!collectionField) {
|
||||||
|
throw new Error(`useCreateActionProps: field "${key}" not found in collection "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVariable(value)) {
|
||||||
|
const result = await variables?.parseVariable(value, localVariables);
|
||||||
|
if (result) {
|
||||||
|
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
|
||||||
|
}
|
||||||
|
} else if (value !== null && value !== '') {
|
||||||
|
assignedValues[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(waitList);
|
||||||
|
|
||||||
|
if (!skipValidator) {
|
||||||
|
await form.submit();
|
||||||
|
}
|
||||||
|
const values = getFormValues({
|
||||||
|
filterByTk,
|
||||||
|
field,
|
||||||
|
form,
|
||||||
|
fieldNames,
|
||||||
|
getField,
|
||||||
|
resource,
|
||||||
|
actionFields: getActiveFieldsName?.('form') || [],
|
||||||
|
});
|
||||||
|
// const values = omitBy(formValues, (value) => isEqual(JSON.stringify(value), '[{}]'));
|
||||||
|
if (addChild) {
|
||||||
|
const treeParentField = getTreeParentField();
|
||||||
|
values[treeParentField?.name ?? 'parent'] = currentRecord?.__parent;
|
||||||
|
values[treeParentField?.foreignKey ?? 'parentId'] = currentRecord?.__parent?.id;
|
||||||
|
}
|
||||||
|
actionField.data = field.data || {};
|
||||||
|
actionField.data.loading = true;
|
||||||
|
try {
|
||||||
|
const data = await resource[action]({
|
||||||
|
values: {
|
||||||
|
...values,
|
||||||
|
...overwriteValues,
|
||||||
|
...assignedValues,
|
||||||
|
},
|
||||||
|
filterKeys: filterKeys,
|
||||||
|
// TODO(refactor): should change to inject by plugin
|
||||||
|
triggerWorkflows: triggerWorkflows?.length
|
||||||
|
? triggerWorkflows.map((row) => [row.workflowKey, row.context].filter(Boolean).join('!')).join(',')
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
actionField.data.loading = false;
|
||||||
|
actionField.data.data = data;
|
||||||
|
__parent?.service?.refresh?.();
|
||||||
|
if (!onSuccess?.successMessage) {
|
||||||
|
if (sessionSubmit) {
|
||||||
|
message.success(t('Saved successfully'));
|
||||||
|
if (dn.designable) {
|
||||||
|
insert(viewerSchema);
|
||||||
|
}
|
||||||
|
fieldSchema2.reduceProperties((buf, s) => {
|
||||||
|
if (s['x-component'] === pageDetailsViewer) {
|
||||||
|
fieldSchema2 = s;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
});
|
||||||
|
if (fieldSchema2['x-component'] === pageDetailsViewer) {
|
||||||
|
navigate(`page/${fieldSchema2['x-uid']}/records/${name}/${data?.data?.data?.id ?? ''}`);
|
||||||
|
}
|
||||||
|
await form.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!onSuccess?.dataClear) {
|
||||||
|
await form.reset();
|
||||||
|
}
|
||||||
|
if (onSuccess?.manualClose) {
|
||||||
|
modal.success({
|
||||||
|
title: compile(onSuccess?.successMessage),
|
||||||
|
onOk: async () => {
|
||||||
|
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
|
||||||
|
if (isURL(onSuccess.redirectTo)) {
|
||||||
|
window.location.href = onSuccess.redirectTo;
|
||||||
|
} else {
|
||||||
|
navigate(onSuccess.redirectTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.success(compile(onSuccess?.successMessage));
|
||||||
|
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
|
||||||
|
if (isURL(onSuccess.redirectTo)) {
|
||||||
|
window.location.href = onSuccess.redirectTo;
|
||||||
|
} else {
|
||||||
|
navigate(onSuccess.redirectTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onSuccess) {
|
||||||
|
setVisible?.(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
actionField.data.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import { useApp } from '@nocobase/client';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { KEY_CUSTOM_COMPONENT_LABEL, KEY_CUSTOM_COMPONENT_TYPE } from '..';
|
||||||
|
|
||||||
|
export const useCustomComponent = (type: string) => {
|
||||||
|
const app = useApp();
|
||||||
|
return _.filter(app.components, (component) => component[KEY_CUSTOM_COMPONENT_TYPE] === type).map((component) => ({
|
||||||
|
label: component[KEY_CUSTOM_COMPONENT_LABEL],
|
||||||
|
value: component.displayName,
|
||||||
|
}));
|
||||||
|
};
|
@ -0,0 +1,120 @@
|
|||||||
|
import { useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import {
|
||||||
|
TableFieldResource,
|
||||||
|
__UNSAFE__,
|
||||||
|
isVariable,
|
||||||
|
transformVariableValue,
|
||||||
|
useBlockRequestContext,
|
||||||
|
useCollection_deprecated,
|
||||||
|
useCompile,
|
||||||
|
useFilterByTk,
|
||||||
|
useLocalVariables,
|
||||||
|
useVariables,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { App, Modal, message } from 'antd';
|
||||||
|
import { isURL } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { ExclamationCircleFilled } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const useCustomizeUpdateActionProps = () => {
|
||||||
|
const { resource, __parent, service } = useBlockRequestContext();
|
||||||
|
const filterByTk = useFilterByTk();
|
||||||
|
const actionSchema = useFieldSchema();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const compile = useCompile();
|
||||||
|
const form = useForm();
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const variables = useVariables();
|
||||||
|
const localVariables = useLocalVariables({ currentForm: form });
|
||||||
|
const { name, getField } = useCollection_deprecated();
|
||||||
|
const { confirm } = Modal;
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
const {
|
||||||
|
assignedValues: originalAssignedValues = {},
|
||||||
|
onSuccess,
|
||||||
|
skipValidator,
|
||||||
|
sessionUpdate,
|
||||||
|
} = actionSchema?.['x-action-settings'] ?? {};
|
||||||
|
if (sessionUpdate) {
|
||||||
|
confirm({
|
||||||
|
title: '问询',
|
||||||
|
content: '确认要继续此操作?',
|
||||||
|
icon: <ExclamationCircleFilled />,
|
||||||
|
onOk() {
|
||||||
|
sessionUpdatePopconfirm();
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
message.warning('取消更新');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sessionUpdatePopconfirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sessionUpdatePopconfirm() {
|
||||||
|
const assignedValues = {};
|
||||||
|
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
|
||||||
|
const value = originalAssignedValues[key];
|
||||||
|
const collectionField = getField(key);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
if (!collectionField) {
|
||||||
|
throw new Error(`useCustomizeUpdateActionProps: field "${key}" not found in collection "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVariable(value)) {
|
||||||
|
const result = await variables?.parseVariable(value, localVariables);
|
||||||
|
if (result) {
|
||||||
|
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
|
||||||
|
}
|
||||||
|
} else if (value != null && value !== '') {
|
||||||
|
assignedValues[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(waitList);
|
||||||
|
|
||||||
|
if (skipValidator === false) {
|
||||||
|
await form.submit();
|
||||||
|
}
|
||||||
|
await resource.update({
|
||||||
|
filterByTk,
|
||||||
|
values: { ...assignedValues },
|
||||||
|
});
|
||||||
|
service?.refresh?.();
|
||||||
|
if (!(resource instanceof TableFieldResource)) {
|
||||||
|
__parent?.service?.refresh?.();
|
||||||
|
}
|
||||||
|
if (!onSuccess?.successMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onSuccess?.manualClose) {
|
||||||
|
modal.success({
|
||||||
|
title: compile(onSuccess?.successMessage),
|
||||||
|
onOk: async () => {
|
||||||
|
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
|
||||||
|
if (isURL(onSuccess.redirectTo)) {
|
||||||
|
window.location.href = onSuccess.redirectTo;
|
||||||
|
} else {
|
||||||
|
navigate(onSuccess.redirectTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.success(compile(onSuccess?.successMessage));
|
||||||
|
if (onSuccess?.redirecting && onSuccess?.redirectTo) {
|
||||||
|
if (isURL(onSuccess.redirectTo)) {
|
||||||
|
window.location.href = onSuccess.redirectTo;
|
||||||
|
} else {
|
||||||
|
navigate(onSuccess.redirectTo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,159 @@
|
|||||||
|
import { useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { isEmpty } from '@formily/shared';
|
||||||
|
import {
|
||||||
|
findFilterTargets,
|
||||||
|
mergeFilter,
|
||||||
|
transformToFilter,
|
||||||
|
useCollection_deprecated,
|
||||||
|
useCollectionManager_deprecated,
|
||||||
|
useFilterBlock,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import flat from 'flat';
|
||||||
|
|
||||||
|
export const removeNullCondition = (filter, fieldSchema?) => {
|
||||||
|
const filterSchema = fieldSchema ? fieldSchema['x-filter-rules'] : '';
|
||||||
|
const filterSchemaItem = flat(filterSchema || '');
|
||||||
|
const filterItem = {};
|
||||||
|
const items = flat(filter || {});
|
||||||
|
const values = {};
|
||||||
|
let isFilterCustom = false;
|
||||||
|
if (filterSchema && (filterSchema.$and?.length || filterSchema.$or?.length)) {
|
||||||
|
for (const key in items) {
|
||||||
|
for (const filterItems in filterSchemaItem) {
|
||||||
|
const match = filterSchemaItem[filterItems].slice(11, -2);
|
||||||
|
if (key.includes(filterItems)) {
|
||||||
|
isFilterCustom = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isFilterCustom) {
|
||||||
|
if (Object.keys(items).filter((item) => item.includes('custom')).length) {
|
||||||
|
if (filterSchema && (filterSchema.$and?.length || filterSchema.$or?.length)) {
|
||||||
|
for (const filterKey in filterSchemaItem) {
|
||||||
|
const match = filterSchemaItem[filterKey]?.slice(11, -2);
|
||||||
|
const collection = match?.split('.')[0];
|
||||||
|
if (Object.keys(items).filter((item) => item.includes(collection)).length) {
|
||||||
|
for (const key in items) {
|
||||||
|
if (key.includes(collection)) {
|
||||||
|
if (key.includes(match)) {
|
||||||
|
filterSchemaItem[filterKey] = items[key];
|
||||||
|
filterItem[match] = items[key];
|
||||||
|
}
|
||||||
|
if (key.includes(collection)) delete items[key];
|
||||||
|
else {
|
||||||
|
const value = items[key];
|
||||||
|
if (value != null && !isEmpty(value)) {
|
||||||
|
values[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (Object.keys(items).filter((item) => !item.includes('custom')).length) {
|
||||||
|
const filterItem = Object.keys(items).filter((item) => !item.includes('custom'));
|
||||||
|
filterItem.forEach((key) => {
|
||||||
|
values[key] = items[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const item in filterSchemaItem) {
|
||||||
|
for (const key in filterItem) {
|
||||||
|
if (filterSchemaItem[item].includes(key)) {
|
||||||
|
filterSchemaItem[item] = filterItem[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filterSchemaItem[item].includes('$nFilter')) {
|
||||||
|
delete filterSchemaItem[item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const flatValue = flat.unflatten(values);
|
||||||
|
const flatFieldSchema = flat.unflatten(filterSchemaItem);
|
||||||
|
return {
|
||||||
|
$and: [flatValue, flatFieldSchema],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
for (const key in items) {
|
||||||
|
if (!key.includes('custom')) {
|
||||||
|
const value = items[key];
|
||||||
|
if (value != null && !isEmpty(value)) {
|
||||||
|
values[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flat.unflatten(values);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const key in items) {
|
||||||
|
const value = items[key];
|
||||||
|
if (value != null && !isEmpty(value)) {
|
||||||
|
values[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flat.unflatten(values);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return flat.unflatten(items);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFilterBlockActionProps = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const actionField = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { getDataBlocks } = useFilterBlock();
|
||||||
|
const { name } = useCollection_deprecated();
|
||||||
|
const { getCollectionJoinField } = useCollectionManager_deprecated();
|
||||||
|
|
||||||
|
actionField.data = actionField.data || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
const { targets = [], uid } = findFilterTargets(fieldSchema);
|
||||||
|
|
||||||
|
actionField.data.loading = true;
|
||||||
|
try {
|
||||||
|
// 收集 filter 的值
|
||||||
|
await Promise.all(
|
||||||
|
getDataBlocks().map(async (block) => {
|
||||||
|
const target = targets.find((target) => target.uid === block.uid);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
const param = block.service.params?.[0] || {};
|
||||||
|
// 保留原有的 filter
|
||||||
|
const storedFilter = block.service.params?.[1]?.filters || {};
|
||||||
|
|
||||||
|
storedFilter[uid] = removeNullCondition(
|
||||||
|
transformToFilter(form.values, fieldSchema, getCollectionJoinField, name),
|
||||||
|
fieldSchema,
|
||||||
|
);
|
||||||
|
if (block.defaultFilter) {
|
||||||
|
getDataBlocks().forEach((getblock) => {
|
||||||
|
if (getblock.uid !== block.uid && getblock.collection.name === block.collection.name) {
|
||||||
|
getblock['defaultFilter'] = block.defaultFilter;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const mergedFilter = mergeFilter([
|
||||||
|
...Object.values(storedFilter).map((filter) => removeNullCondition(filter, fieldSchema)),
|
||||||
|
block.defaultFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return block.doFilter(
|
||||||
|
{
|
||||||
|
...param,
|
||||||
|
page: 1,
|
||||||
|
filter: mergedFilter,
|
||||||
|
},
|
||||||
|
{ filters: storedFilter },
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
actionField.data.loading = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
actionField.data.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,16 @@
|
|||||||
|
import { useSchemaInitializer } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const useFilterFormCustomProps = () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
return {
|
||||||
|
title: 'Custom',
|
||||||
|
onClick: () => {
|
||||||
|
insert({
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'CardItem',
|
||||||
|
'x-component': 'h1',
|
||||||
|
'x-content': 'Custom block',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { CUSTOM_COMPONENT_TYPE_ASSOCIATED_FIELD } from '..';
|
||||||
|
import { useCustomComponent } from './useCustomComponent';
|
||||||
|
|
||||||
|
export const useGetCustomAssociatedComponents = () => {
|
||||||
|
return useCustomComponent(CUSTOM_COMPONENT_TYPE_ASSOCIATED_FIELD);
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { CUSTOM_COMPONENT_TYPE_FIELD } from '..';
|
||||||
|
import { useCustomComponent } from './useCustomComponent';
|
||||||
|
|
||||||
|
export const useGetCustomComponents = () => {
|
||||||
|
return useCustomComponent(CUSTOM_COMPONENT_TYPE_FIELD);
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
import { useRequest } from '@nocobase/client';
|
||||||
|
export const useLinkKey = () => {
|
||||||
|
const { data } = useRequest<{
|
||||||
|
data: any;
|
||||||
|
}>({
|
||||||
|
resource: 'link-manage',
|
||||||
|
action: 'get',
|
||||||
|
params: {
|
||||||
|
name: 'Notifications',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data?.data[0];
|
||||||
|
};
|
||||||
|
export const useInitializationLinkKey = () => {
|
||||||
|
useRequest<{
|
||||||
|
data: any;
|
||||||
|
}>({
|
||||||
|
resource: 'link-manage',
|
||||||
|
action: 'init',
|
||||||
|
params: {
|
||||||
|
name: 'Notifications',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import { message } from 'antd';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
|
||||||
|
export const useOutboundActionProps = () => {
|
||||||
|
let schema = useFieldSchema();
|
||||||
|
|
||||||
|
while (!('x-decorator-props' in schema)) {
|
||||||
|
schema = schema.parent;
|
||||||
|
}
|
||||||
|
const url = window.location.href.split('/', 3).join('/');
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
const c = copy(`${url}/r/${schema['x-uid']}`);
|
||||||
|
if (c) {
|
||||||
|
message.success('链接保存成功');
|
||||||
|
} else {
|
||||||
|
message.success('链接保存失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import { useRequest } from '@nocobase/client';
|
||||||
|
interface IPluginDetailData {
|
||||||
|
packageJson: PackageJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PackageJSON {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
description?: string;
|
||||||
|
repository?: string | { type: string; url: string };
|
||||||
|
homepage?: string;
|
||||||
|
license?: string;
|
||||||
|
devDependencies?: Record<string, string>;
|
||||||
|
dependencies?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePluginVersion = () => {
|
||||||
|
const { data } = useRequest<{ data: IPluginDetailData }>({
|
||||||
|
url: 'hera:version',
|
||||||
|
});
|
||||||
|
return data?.data?.packageJson?.version;
|
||||||
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
import { useRequest } from '@nocobase/client';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const TokenConfigurationResourceKey = 'token-configuration';
|
||||||
|
export const getSSKey = (type) => {
|
||||||
|
return `NOCOBASE_PLUGIN_TOKEN_CONFIGURATION_${type}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMapConfiguration = (type: string) => {
|
||||||
|
// cache
|
||||||
|
const config = useMemo(() => {
|
||||||
|
const d = sessionStorage.getItem(getSSKey(type));
|
||||||
|
if (d) {
|
||||||
|
return JSON.parse(d);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
const { data } = useRequest<{
|
||||||
|
data: any;
|
||||||
|
}>(
|
||||||
|
{
|
||||||
|
resource: TokenConfigurationResourceKey,
|
||||||
|
action: 'get',
|
||||||
|
params: {
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
sessionStorage.setItem(getSSKey(type), JSON.stringify(data?.data));
|
||||||
|
},
|
||||||
|
refreshOnWindowFocus: false,
|
||||||
|
refreshDeps: [],
|
||||||
|
manual: config ? true : false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (config) return config;
|
||||||
|
|
||||||
|
return data?.data;
|
||||||
|
};
|
404
packages/plugins/@hera/plugin-core/src/client/index.tsx
Normal file
404
packages/plugins/@hera/plugin-core/src/client/index.tsx
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
import React, { ComponentType } from 'react';
|
||||||
|
import { autorun } from '@formily/reactive';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Plugin,
|
||||||
|
RemoteSchemaTemplateManagerProvider,
|
||||||
|
TableV2,
|
||||||
|
EditTitleField,
|
||||||
|
useCollection_deprecated,
|
||||||
|
SchemaSettingOptions,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { HeraAdminLayout } from './layouts';
|
||||||
|
import { remove } from 'lodash';
|
||||||
|
import { CalendarBlockInitializer } from './schema-initializer/CalendarBlockInitializer';
|
||||||
|
import { MenuDesigner } from './components/ExtendedMenuDesigner';
|
||||||
|
import { SessionSubmit, SessionUpdate } from './components/ExtendedActionDesigner';
|
||||||
|
import { DetailsPage } from './pages/DetailsPage';
|
||||||
|
import { OutboundPage } from './pages/OutboundPage';
|
||||||
|
import { HomePageConfiguration } from './pages/HomePageConfiguration';
|
||||||
|
import { Configuration } from './components/TokenConfiguration';
|
||||||
|
import { HomePage } from './pages/Home';
|
||||||
|
import { InternalPDFViewer } from './schema-components/PDFViewer';
|
||||||
|
import {
|
||||||
|
PDFViewerBlockInitializer,
|
||||||
|
PDFViewerPrintActionInitializer,
|
||||||
|
PDFViewerProvider,
|
||||||
|
pdfViewActionInitializer,
|
||||||
|
usePDFViewerPrintActionProps,
|
||||||
|
} from './schema-initializer/PDFVIewerBlockInitializer';
|
||||||
|
import { OutboundButton } from './components/OutboundButton';
|
||||||
|
import { useCustomizeUpdateActionProps } from './hooks/useCustomizeUpdateActionProps';
|
||||||
|
import { OutboundLinkActionInitializer } from './schema-initializer/OutboundLinkActionInitializer';
|
||||||
|
import { CreateSubmitActionInitializer } from './schema-initializer/CreateSubmitActionInitializer';
|
||||||
|
import { FilterAssociatedFields } from './schema-initializer/FilterAssociatedFields';
|
||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import { isValid } from '@formily/shared';
|
||||||
|
import { useCreateActionProps } from './hooks/useCreateActionProps';
|
||||||
|
import { PageLayout } from './pages/PageLayout';
|
||||||
|
import { useOutboundActionProps } from './hooks/useOutboundActionProps';
|
||||||
|
import { ExtendedAssociationField } from './schema-components/association-field/Editable';
|
||||||
|
import { AssociatedField } from './components/AssociatedField';
|
||||||
|
import { DatePicker } from './schema-components/date-picker';
|
||||||
|
import { Page } from './schema-components/page';
|
||||||
|
import { SettingBlockInitializer } from './schema-initializer/SettingBlockInitializer';
|
||||||
|
import {
|
||||||
|
EditFormulaTitleField,
|
||||||
|
IsTablePageSize,
|
||||||
|
useFormulaTitleVisible,
|
||||||
|
usePaginationVisible,
|
||||||
|
EditTitle,
|
||||||
|
SetFilterScope,
|
||||||
|
useSetFilterScopeVisible,
|
||||||
|
} from './components/SchemaSettingOptions';
|
||||||
|
import { SignatureInput } from './components/SignatureInput';
|
||||||
|
import { RemoteSelect } from './schema-components/remote-select';
|
||||||
|
import { Select } from './schema-components/select/Select';
|
||||||
|
import { Locale, tval } from './locale';
|
||||||
|
import { LinkManager } from './components/LinkManager';
|
||||||
|
import {
|
||||||
|
GroupBlockInitializer,
|
||||||
|
GroupBlockProvider,
|
||||||
|
GroupBlockToolbar,
|
||||||
|
groupBlockSettings,
|
||||||
|
} from './schema-initializer/GroupBlockInitializer';
|
||||||
|
import { useFilterFormCustomProps } from './hooks/useFilterFormCustomProps';
|
||||||
|
import {
|
||||||
|
FilterFormItem,
|
||||||
|
FilterItemCustomDesigner,
|
||||||
|
FilterFormItemCustom,
|
||||||
|
} from './schema-initializer/FilterFormItemCustomInitializer/FilterFormItemCustom';
|
||||||
|
import { GroupBlock } from './schema-components/GroupBlock';
|
||||||
|
import {
|
||||||
|
CustomComponentDispatcher,
|
||||||
|
CustomComponentStub,
|
||||||
|
customComponentDispatcherSettings,
|
||||||
|
} from './components/CustomComponentDispatcher';
|
||||||
|
import { useFilterBlockActionProps } from './hooks/useFilterBlockActionProps';
|
||||||
|
import { AfterSuccess } from './components/Action.Designer';
|
||||||
|
import { GroupBlockConfigure } from './components/GroupBlockConfigure/GroupBlockConfigure';
|
||||||
|
import { AssociatedFieldInterface } from './interfaces/associated';
|
||||||
|
import { CalcFieldInterface } from './interfaces/calc';
|
||||||
|
import { CustomFieldInterface } from './interfaces/custom';
|
||||||
|
import { CustomAssociatedFieldInterface } from './interfaces/customAssociated';
|
||||||
|
import { SignaturePadFieldInterface } from './interfaces/signatureSchema';
|
||||||
|
import { CalcResult } from './components/CalcResult';
|
||||||
|
import { CustomAssociatedField } from './components/CustomAssociatedField';
|
||||||
|
import Expression from './components/Expression';
|
||||||
|
import { CustomField } from './components/CustomField';
|
||||||
|
import { useGetCustomAssociatedComponents } from './hooks/useGetCustomAssociatedComponents';
|
||||||
|
import { useGetCustomComponents } from './hooks/useGetCustomComponents';
|
||||||
|
|
||||||
|
export enum CustomComponentType {
|
||||||
|
CUSTOM_FORM_ITEM,
|
||||||
|
CUSTOM_FIELD,
|
||||||
|
CUSTOM_ASSOCIATED_FIELD,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomComponentOption {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
type?: CustomComponentType;
|
||||||
|
component: ComponentType;
|
||||||
|
}
|
||||||
|
export { usePDFViewerRef } from './schema-initializer/PDFVIewerBlockInitializer';
|
||||||
|
|
||||||
|
export class PluginCoreClient extends Plugin {
|
||||||
|
locale: Locale;
|
||||||
|
|
||||||
|
async registerSettings() {
|
||||||
|
this.app.pluginSettingsManager.add('home_page', {
|
||||||
|
title: this.locale.lang('HomePage Config'),
|
||||||
|
icon: 'HomeOutlined',
|
||||||
|
Component: HomePageConfiguration,
|
||||||
|
});
|
||||||
|
this.app.pluginSettingsManager.add('token', {
|
||||||
|
title: '第三方接入配置',
|
||||||
|
icon: 'ShareAltOutlined',
|
||||||
|
Component: Configuration,
|
||||||
|
});
|
||||||
|
this.app.pluginSettingsManager.add('linkmanage', {
|
||||||
|
title: '配置链接',
|
||||||
|
icon: 'ShareAltOutlined',
|
||||||
|
Component: LinkManager,
|
||||||
|
});
|
||||||
|
this.schemaSettingsManager.add(groupBlockSettings);
|
||||||
|
this.schemaSettingsManager.add(customComponentDispatcherSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerActions() {
|
||||||
|
const actionSettings = this.app.schemaSettingsManager.get('ActionSettings');
|
||||||
|
actionSettings.add('sessionSubmit', {
|
||||||
|
Component: SessionSubmit,
|
||||||
|
useVisible() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return isValid(fieldSchema?.['x-action-settings']?.sessionSubmit);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
actionSettings.add('sessionUpdate', {
|
||||||
|
Component: SessionUpdate,
|
||||||
|
useVisible() {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
return isValid(fieldSchema?.['x-action-settings']?.sessionUpdate);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerScopesAndComponents() {
|
||||||
|
this.app.addScopes({
|
||||||
|
useOutboundActionProps,
|
||||||
|
useCustomizeUpdateActionProps,
|
||||||
|
useCreateActionProps,
|
||||||
|
useFilterFormCustomProps,
|
||||||
|
useFilterBlockActionProps,
|
||||||
|
usePDFViewerPrintActionProps,
|
||||||
|
useGetCustomAssociatedComponents,
|
||||||
|
useGetCustomComponents,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.addComponents({
|
||||||
|
AssociatedField,
|
||||||
|
Expression,
|
||||||
|
CustomField,
|
||||||
|
CustomAssociatedField,
|
||||||
|
CalcResult,
|
||||||
|
PDFViewerPrintActionInitializer,
|
||||||
|
PDFViewerProvider,
|
||||||
|
GroupBlock,
|
||||||
|
CustomComponentStub,
|
||||||
|
CustomComponentDispatcher,
|
||||||
|
GroupBlockInitializer,
|
||||||
|
GroupBlockToolbar,
|
||||||
|
GroupBlockProvider,
|
||||||
|
Page,
|
||||||
|
DatePicker,
|
||||||
|
RemoteSelect,
|
||||||
|
SignatureInput,
|
||||||
|
AssociationField: ExtendedAssociationField,
|
||||||
|
OutboundButton,
|
||||||
|
OutboundLinkActionInitializer,
|
||||||
|
PDFViewerBlockInitializer,
|
||||||
|
PDFViwer: InternalPDFViewer,
|
||||||
|
AdminLayout: HeraAdminLayout,
|
||||||
|
ExtendedCalendarBlockInitializer: CalendarBlockInitializer,
|
||||||
|
SettingBlock: SettingBlockInitializer,
|
||||||
|
CreateSubmitActionInitializer,
|
||||||
|
PageLayout,
|
||||||
|
FilterAssociatedFields,
|
||||||
|
FilterFormItemCustom,
|
||||||
|
FilterFormItem,
|
||||||
|
FilterItemCustomDesigner,
|
||||||
|
Select,
|
||||||
|
EditTitle,
|
||||||
|
EditTitleField,
|
||||||
|
AfterSuccess,
|
||||||
|
GroupBlockConfigure,
|
||||||
|
Menu: {
|
||||||
|
...Menu,
|
||||||
|
// @ts-ignore
|
||||||
|
Designer: MenuDesigner,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerRouters() {
|
||||||
|
this.app.router.add('outbound', {
|
||||||
|
path: '/r/:id',
|
||||||
|
element: <OutboundPage />,
|
||||||
|
});
|
||||||
|
this.app.router.remove('root');
|
||||||
|
this.app.router.add('home', {
|
||||||
|
path: '/',
|
||||||
|
element: <HomePage />,
|
||||||
|
});
|
||||||
|
this.app.router.add('admin.details_page', {
|
||||||
|
path: '/admin/:name/page/:pageId/records/*',
|
||||||
|
Component: DetailsPage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerTricks() {
|
||||||
|
// Loading this provider in an unauthenticated state will result in an error; remove it here.
|
||||||
|
remove(this.app.providers, ([provider]) => provider === RemoteSchemaTemplateManagerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerSchemaInitializer() {
|
||||||
|
const associationFields = {
|
||||||
|
type: 'item',
|
||||||
|
name: 'associationFields',
|
||||||
|
title: '筛选区块添加一对一的引用',
|
||||||
|
Component: 'FilterAssociatedFields',
|
||||||
|
};
|
||||||
|
const outboundItem = {
|
||||||
|
type: 'item',
|
||||||
|
name: 'enableActions.outbound',
|
||||||
|
title: '外链',
|
||||||
|
Component: 'OutboundLinkActionInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const calendarBlockItem = {
|
||||||
|
name: 'calendarV2',
|
||||||
|
title: '{{t("Calendar")}}',
|
||||||
|
Component: 'ExtendedCalendarBlockInitializer',
|
||||||
|
};
|
||||||
|
const settingBlockItem = {
|
||||||
|
name: 'setting',
|
||||||
|
title: tval('System setting'),
|
||||||
|
Component: 'SettingBlock',
|
||||||
|
};
|
||||||
|
const refreshActionItem = {
|
||||||
|
type: 'item',
|
||||||
|
name: 'refreshAction',
|
||||||
|
title: "{{t('Refresh')}}",
|
||||||
|
Component: 'RefreshActionInitializer',
|
||||||
|
schema: {
|
||||||
|
'x-align': 'right',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const customItem = {
|
||||||
|
title: '自定义',
|
||||||
|
name: 'custom',
|
||||||
|
type: 'item',
|
||||||
|
Component: 'FilterFormItemCustom',
|
||||||
|
};
|
||||||
|
this.schemaInitializerManager.add(pdfViewActionInitializer);
|
||||||
|
this.app.schemaInitializerManager.addItem('TableActionInitializers', outboundItem.name, outboundItem);
|
||||||
|
this.app.schemaInitializerManager.get('ReadPrettyFormActionInitializers').add(outboundItem.name, outboundItem);
|
||||||
|
this.app.schemaInitializerManager.get('BlockInitializers').add(calendarBlockItem.name, calendarBlockItem);
|
||||||
|
this.app.schemaInitializerManager.get('BlockInitializers').add(settingBlockItem.name, settingBlockItem);
|
||||||
|
this.app.schemaInitializerManager.get('BlockInitializers').add('dataBlocks.groupBlock', {
|
||||||
|
title: tval('Group block'),
|
||||||
|
Component: 'GroupBlockInitializer',
|
||||||
|
});
|
||||||
|
this.app.schemaInitializerManager.get('KanbanActionInitializers').add(refreshActionItem.name, refreshActionItem);
|
||||||
|
this.app.schemaInitializerManager.get('KanbanActionInitializers').add(outboundItem.name, outboundItem);
|
||||||
|
this.app.schemaInitializerManager.get('FilterFormItemInitializers').add(associationFields.name, associationFields);
|
||||||
|
this.app.schemaInitializerManager.addItem('FilterFormItemInitializers', 'custom-item-divider', {
|
||||||
|
type: 'divider',
|
||||||
|
});
|
||||||
|
this.app.schemaInitializerManager.addItem('FilterFormItemInitializers', customItem.name, customItem);
|
||||||
|
|
||||||
|
const addCustomComponent = {
|
||||||
|
name: 'addCustomComponent',
|
||||||
|
title: this.locale.lang('Add custom component'),
|
||||||
|
Component: 'BlockItemInitializer',
|
||||||
|
schema: {
|
||||||
|
type: 'void',
|
||||||
|
'x-editable': false,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-settings': 'customComponentDispatcherSettings',
|
||||||
|
'x-component': 'CustomComponentDispatcher',
|
||||||
|
'x-component-props': {
|
||||||
|
component: 'CustomComponentStub',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.app.schemaInitializerManager.addItem('FormItemInitializers', addCustomComponent.name, addCustomComponent);
|
||||||
|
this.app.schemaInitializerManager.addItem(
|
||||||
|
'ReadPrettyFormItemInitializers',
|
||||||
|
addCustomComponent.name,
|
||||||
|
addCustomComponent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterAdd() {}
|
||||||
|
async beforeLoad() {}
|
||||||
|
async afterLoad() {
|
||||||
|
// log for debug
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
console.info('current components', this.app.components);
|
||||||
|
console.info('current schemaSettings', this.app.schemaSettingsManager.getAll());
|
||||||
|
console.info('current schemaInitializer', this.app.schemaInitializerManager.getAll());
|
||||||
|
console.info('current providers', this.app.providers);
|
||||||
|
}
|
||||||
|
await this.registerSchemaInitializer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerInterfaces() {
|
||||||
|
this.app.dataSourceManager.addFieldInterfaces([
|
||||||
|
AssociatedFieldInterface,
|
||||||
|
CalcFieldInterface,
|
||||||
|
CustomFieldInterface,
|
||||||
|
CustomAssociatedFieldInterface,
|
||||||
|
SignaturePadFieldInterface,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this.locale = new Locale(this.app);
|
||||||
|
await this.registerTricks();
|
||||||
|
await this.registerScopesAndComponents();
|
||||||
|
await this.registerSettings();
|
||||||
|
await this.registerActions();
|
||||||
|
await this.registerRouters();
|
||||||
|
await this.registerInterfaces();
|
||||||
|
this.schemaSettingsManager.addItem('FilterFormItemSettings', 'formulatitleField', {
|
||||||
|
Component: EditFormulaTitleField,
|
||||||
|
useVisible: useFormulaTitleVisible,
|
||||||
|
});
|
||||||
|
this.schemaSettingsManager.addItem('FormItemSettings', 'hera-divider', {
|
||||||
|
type: 'divider',
|
||||||
|
useVisible() {
|
||||||
|
const v1 = useFormulaTitleVisible();
|
||||||
|
const v2 = usePaginationVisible();
|
||||||
|
return v1 || v2;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.schemaSettingsManager.addItem('FormItemSettings', 'formulatitleField', {
|
||||||
|
Component: EditFormulaTitleField,
|
||||||
|
useVisible: useFormulaTitleVisible,
|
||||||
|
});
|
||||||
|
this.schemaSettingsManager.addItem('FormItemSettings', 'isTablePageSize', {
|
||||||
|
Component: IsTablePageSize,
|
||||||
|
useVisible: usePaginationVisible,
|
||||||
|
});
|
||||||
|
this.schemaSettingsManager.addItem('ActionSettings', 'Customize.setFilterScope', {
|
||||||
|
Component: SetFilterScope,
|
||||||
|
useVisible: useSetFilterScopeVisible,
|
||||||
|
useComponentProps() {
|
||||||
|
const { name } = useCollection_deprecated();
|
||||||
|
return {
|
||||||
|
collectionName: name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const SchemaSettingOptionItems = this.schemaSettingsManager
|
||||||
|
.get('ActionSettings')
|
||||||
|
.items.filter((item) => item.name === 'Customize')[0].children;
|
||||||
|
SchemaSettingOptionItems.forEach((item) => {
|
||||||
|
if (item.name === 'afterSuccess') {
|
||||||
|
(item as SchemaSettingOptions).Component = AfterSuccess;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const previewBlockItem = {
|
||||||
|
title: tval('preview block'),
|
||||||
|
name: 'previewBlock',
|
||||||
|
type: 'itemGroup',
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
this.app.schemaInitializerManager.get('RecordBlockInitializers').add(previewBlockItem.name, previewBlockItem);
|
||||||
|
|
||||||
|
// listen to connected events.
|
||||||
|
autorun(() => {
|
||||||
|
if (this.app.ws.connected) {
|
||||||
|
const data = {
|
||||||
|
type: 'plugin-online-user:client',
|
||||||
|
payload: {
|
||||||
|
token: this.app.apiClient.auth.getToken(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.app.ws.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginCoreClient;
|
||||||
|
|
||||||
|
export const KEY_CUSTOM_COMPONENT_TYPE = '__componentType';
|
||||||
|
export const KEY_CUSTOM_COMPONENT_LABEL = '__componentLabel';
|
||||||
|
export const CUSTOM_COMPONENT_TYPE_FIELD = 'FIELD';
|
||||||
|
export const CUSTOM_COMPONENT_TYPE_FORM_ITEM = 'FORM_ITEM';
|
||||||
|
export const CUSTOM_COMPONENT_TYPE_ASSOCIATED_FIELD = 'ASSOCIATED_FIELD';
|
@ -0,0 +1,107 @@
|
|||||||
|
import { CollectionFieldInterface, defaultProps } from '@nocobase/client';
|
||||||
|
|
||||||
|
export class AssociatedFieldInterface extends CollectionFieldInterface {
|
||||||
|
name = 'associated';
|
||||||
|
type = 'object';
|
||||||
|
group = 'relation';
|
||||||
|
order = 10;
|
||||||
|
title = '关联字段';
|
||||||
|
description = '关联字段';
|
||||||
|
isAssociation = true;
|
||||||
|
default = {
|
||||||
|
type: 'belongsTo',
|
||||||
|
// name,
|
||||||
|
uiSchema: {
|
||||||
|
// title,
|
||||||
|
'x-component': 'AssociatedField',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
availableTypes = ['belongsTo'];
|
||||||
|
properties = {
|
||||||
|
...defaultProps,
|
||||||
|
'uiSchema.x-component-props.collection': {
|
||||||
|
type: 'string',
|
||||||
|
title: '关联数据表',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
},
|
||||||
|
enum: '{{collections}}',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.sourceCollection': {
|
||||||
|
type: 'string',
|
||||||
|
title: '查询数据表',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
},
|
||||||
|
enum: '{{collections}}',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.sourceField': {
|
||||||
|
type: 'string',
|
||||||
|
title: '查询数据表到关联数据表的路径',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.fieldExp': {
|
||||||
|
type: 'string',
|
||||||
|
title: '关联字段',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Expression',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component-props': {
|
||||||
|
useCurrentFields: '{{ useCurrentFields }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.dateFieldExp': {
|
||||||
|
type: 'string',
|
||||||
|
title: '关联日期',
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Expression',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component-props': {
|
||||||
|
useCurrentFields: '{{ useCurrentFields }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: 'string',
|
||||||
|
title: '关联数据表',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
},
|
||||||
|
enum: '{{collections}}',
|
||||||
|
required: true,
|
||||||
|
'x-display': 'hidden',
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
dependencies: ['uiSchema.x-component-props.collection'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$deps[0]}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
targetKey: {
|
||||||
|
type: 'string',
|
||||||
|
title: '关联数据表键',
|
||||||
|
required: true,
|
||||||
|
default: 'id',
|
||||||
|
'x-display': 'hidden',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
filterable = {
|
||||||
|
nested: true,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
}
|
111
packages/plugins/@hera/plugin-core/src/client/interfaces/calc.ts
Normal file
111
packages/plugins/@hera/plugin-core/src/client/interfaces/calc.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { interfacesProperties, CollectionFieldInterface } from '@nocobase/client';
|
||||||
|
const { defaultProps } = interfacesProperties;
|
||||||
|
|
||||||
|
const formulaType = [
|
||||||
|
{
|
||||||
|
dependencies: ['dataType'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
display: '{{["formula"].includes($deps[0]) ? "visible" : "none"}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const panelType = [
|
||||||
|
{
|
||||||
|
dependencies: ['dataType'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
display: '{{["jsCode"].includes($deps[0]) ? "visible" : "none"}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export class CalcFieldInterface extends CollectionFieldInterface {
|
||||||
|
name = 'calc2';
|
||||||
|
type = 'object';
|
||||||
|
group = 'advanced';
|
||||||
|
title = '数据计算';
|
||||||
|
description = '数据字段计算';
|
||||||
|
sortable = true;
|
||||||
|
default = {
|
||||||
|
type: 'virtual',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CalcResult',
|
||||||
|
'x-component-props': {
|
||||||
|
formula: '',
|
||||||
|
prefix: '',
|
||||||
|
suffix: '',
|
||||||
|
decimal: '',
|
||||||
|
panel: '',
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
properties = {
|
||||||
|
...defaultProps,
|
||||||
|
dataType: {
|
||||||
|
type: 'string',
|
||||||
|
title: '计算类型',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: [
|
||||||
|
{ value: 'formula', label: '公式' },
|
||||||
|
{ value: 'jsCode', label: '代码' },
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
default: 'formula',
|
||||||
|
description: '公式:使用公式计算,代码:使用js代码计算',
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.formula': {
|
||||||
|
type: 'string',
|
||||||
|
title: '公式',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
'x-reactions': formulaType,
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.panel': {
|
||||||
|
type: 'string',
|
||||||
|
title: 'jsCode',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
description: '注意,此内容有值计算操作将以此内容为准,而不是公式,适用于面板中的计算',
|
||||||
|
required: true,
|
||||||
|
'x-reactions': panelType,
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.prefix': {
|
||||||
|
type: 'string',
|
||||||
|
title: '前缀',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.suffix': {
|
||||||
|
type: 'string',
|
||||||
|
title: '后缀',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
'uiSchema.x-component-props.decimal': {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Precision")}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
required: true,
|
||||||
|
default: '0',
|
||||||
|
enum: [
|
||||||
|
{ value: '0', label: '1' },
|
||||||
|
{ value: '1', label: '1.0' },
|
||||||
|
{ value: '2', label: '1.00' },
|
||||||
|
{ value: '3', label: '1.000' },
|
||||||
|
{ value: '4', label: '1.0000' },
|
||||||
|
{ value: '5', label: '1.00000' },
|
||||||
|
],
|
||||||
|
'x-reactions': formulaType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { interfacesProperties, CollectionFieldInterface } from '@nocobase/client';
|
||||||
|
const { defaultProps } = interfacesProperties;
|
||||||
|
|
||||||
|
export class CustomFieldInterface extends CollectionFieldInterface {
|
||||||
|
name = 'custom';
|
||||||
|
type = 'object';
|
||||||
|
group = 'advanced';
|
||||||
|
title = '自定义字段';
|
||||||
|
description = '自定义字段';
|
||||||
|
sortable = true;
|
||||||
|
default = {
|
||||||
|
type: 'virtual',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CustomField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
properties = {
|
||||||
|
...defaultProps,
|
||||||
|
'uiSchema.x-component-props.component': {
|
||||||
|
type: 'string',
|
||||||
|
title: '组件',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: ' {{ useGetCustomComponents() }} ',
|
||||||
|
required: true,
|
||||||
|
description: '需要在插件中注册相应的组件后可以使用',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import { defaultProps, CollectionFieldInterface } from '@nocobase/client';
|
||||||
|
|
||||||
|
export class CustomAssociatedFieldInterface extends CollectionFieldInterface {
|
||||||
|
name = 'customAssociated';
|
||||||
|
type = 'object';
|
||||||
|
group = 'relation';
|
||||||
|
order = 10;
|
||||||
|
title = '自定义关联字段';
|
||||||
|
description = '自定义关联字段';
|
||||||
|
isAssociation = true;
|
||||||
|
default = {
|
||||||
|
type: 'virtual',
|
||||||
|
// type: 'belongsTo',
|
||||||
|
// name,
|
||||||
|
uiSchema: {
|
||||||
|
// title,
|
||||||
|
'x-component': 'CustomAssociatedField',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
availableTypes = ['belongsTo'];
|
||||||
|
properties = {
|
||||||
|
...defaultProps,
|
||||||
|
'uiSchema.x-component-props.component': {
|
||||||
|
type: 'string',
|
||||||
|
title: '组件',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
enum: ' {{ useGetCustomAssociatedComponents() }}',
|
||||||
|
required: true,
|
||||||
|
description: '需要在插件中注册相应的组件后可以使用',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
type: 'string',
|
||||||
|
title: '关联数据表',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-component-props': {
|
||||||
|
multiple: false,
|
||||||
|
},
|
||||||
|
enum: '{{collections}}',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
targetKey: {
|
||||||
|
type: 'string',
|
||||||
|
title: '关联数据表键',
|
||||||
|
required: true,
|
||||||
|
default: 'id',
|
||||||
|
'x-display': 'hidden',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
filterable = {
|
||||||
|
nested: true,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { defaultProps, CollectionFieldInterface } from '@nocobase/client';
|
||||||
|
import { tval } from '../locale';
|
||||||
|
|
||||||
|
export class SignaturePadFieldInterface extends CollectionFieldInterface {
|
||||||
|
name = 'signatureSchema';
|
||||||
|
type = 'object';
|
||||||
|
group = 'advanced';
|
||||||
|
order = 2; // 可以调整字段的顺序
|
||||||
|
title = tval('Signature input');
|
||||||
|
sortable = true;
|
||||||
|
default = {
|
||||||
|
interface: 'signature', // 添加手写签名的接口标识
|
||||||
|
type: 'json',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'signature', // 添加手写签名的 UI 类型
|
||||||
|
'x-component': 'SignatureInput',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
availableTypes = ['json'];
|
||||||
|
hasDefaultValue = false; // 手写签名通常不需要默认值
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
...defaultProps,
|
||||||
|
};
|
||||||
|
}
|
510
packages/plugins/@hera/plugin-core/src/client/layouts/index.tsx
Normal file
510
packages/plugins/@hera/plugin-core/src/client/layouts/index.tsx
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useSessionStorageState } from 'ahooks';
|
||||||
|
import { App, Layout, Spin, FloatButton } from 'antd';
|
||||||
|
import { ToolOutlined, CommentOutlined, CalculatorOutlined, HighlightOutlined } from '@ant-design/icons';
|
||||||
|
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { Link, Outlet, useMatch, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
CurrentUser,
|
||||||
|
PinnedPluginList,
|
||||||
|
SchemaComponent,
|
||||||
|
findByUid,
|
||||||
|
findMenuItem,
|
||||||
|
useACLRoleContext,
|
||||||
|
useAdminSchemaUid,
|
||||||
|
useDocumentTitle,
|
||||||
|
useRequest,
|
||||||
|
useSystemSettings,
|
||||||
|
useToken,
|
||||||
|
useApp,
|
||||||
|
AdminProvider,
|
||||||
|
RemoteSchemaComponent,
|
||||||
|
useCurrentUserSettingsMenu,
|
||||||
|
SelectWithTitle,
|
||||||
|
useDesignable,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { Tabs } from 'antd';
|
||||||
|
import type { TabsProps } from 'antd';
|
||||||
|
import { usePluginVersion } from '../hooks/usePluginVersion';
|
||||||
|
import { OnlineUserDropdown } from '../components/OnlineUserProvider';
|
||||||
|
import { MobileLink } from '../components/MobileLink';
|
||||||
|
import { Notifications } from '../components/Notifications';
|
||||||
|
import { useTranslation } from '../locale';
|
||||||
|
|
||||||
|
export const useAppSpin = () => {
|
||||||
|
const app = useApp();
|
||||||
|
return {
|
||||||
|
render: () => (app ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterByACL = (schema, options) => {
|
||||||
|
const { allowAll, allowMenuItemIds = [] } = options;
|
||||||
|
if (allowAll) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
const filterSchema = (s) => {
|
||||||
|
if (!s) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const key in s.properties) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(s.properties, key)) {
|
||||||
|
const element = s.properties[key];
|
||||||
|
if (element['x-uid'] && !allowMenuItemIds.includes(element['x-uid'])) {
|
||||||
|
delete s.properties[key];
|
||||||
|
}
|
||||||
|
if (element['x-uid']) {
|
||||||
|
filterSchema(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
filterSchema(schema);
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SchemaIdContext = createContext(null);
|
||||||
|
const useMenuProps = () => {
|
||||||
|
const defaultSelectedUid = useContext(SchemaIdContext);
|
||||||
|
return {
|
||||||
|
selectedUid: defaultSelectedUid,
|
||||||
|
defaultSelectedUid,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuEditor = (props) => {
|
||||||
|
const { notification } = App.useApp();
|
||||||
|
const [hasNotice, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false });
|
||||||
|
const { setTitle } = useDocumentTitle();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const params = useParams<any>();
|
||||||
|
const isMatchAdmin = useMatch('/admin');
|
||||||
|
const isMatchAdminName = useMatch('/admin/:name');
|
||||||
|
const defaultSelectedUid = params.name;
|
||||||
|
const { sideMenuRef } = props;
|
||||||
|
const ctx = useACLRoleContext();
|
||||||
|
const [current, setCurrent] = useState(null);
|
||||||
|
const onSelect = ({ item }) => {
|
||||||
|
const schema = item.props.schema;
|
||||||
|
setTitle(schema.title);
|
||||||
|
setCurrent(schema);
|
||||||
|
navigate(`/admin/${schema['x-uid']}`);
|
||||||
|
};
|
||||||
|
const { render } = useAppSpin();
|
||||||
|
const adminSchemaUid = useAdminSchemaUid();
|
||||||
|
const { data, loading } = useRequest<{
|
||||||
|
data: any;
|
||||||
|
}>(
|
||||||
|
{
|
||||||
|
url: `/uiSchemas:getJsonSchema/${adminSchemaUid}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refreshDeps: [adminSchemaUid],
|
||||||
|
onSuccess(data) {
|
||||||
|
const schema = filterByACL(data?.data, ctx);
|
||||||
|
// url 为 `/admin` 的情况
|
||||||
|
if (isMatchAdmin) {
|
||||||
|
const s = findMenuItem(schema);
|
||||||
|
if (s) {
|
||||||
|
navigate(`/admin/${s['x-uid']}`);
|
||||||
|
setTitle(s.title);
|
||||||
|
} else {
|
||||||
|
navigate(`/admin/`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// url 不为 `/admin/xxx` 的情况,不做处理
|
||||||
|
if (!isMatchAdminName) return;
|
||||||
|
|
||||||
|
// url 为 `admin/xxx` 的情况
|
||||||
|
const s = findByUid(schema, defaultSelectedUid);
|
||||||
|
if (s) {
|
||||||
|
setTitle(s.title);
|
||||||
|
} else {
|
||||||
|
const s = findMenuItem(schema);
|
||||||
|
if (s) {
|
||||||
|
navigate(`/admin/${s['x-uid']}`);
|
||||||
|
setTitle(s.title);
|
||||||
|
} else {
|
||||||
|
navigate(`/admin/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const properties = Object.values(current?.root?.properties || {}).shift()?.['properties'] || data?.data?.properties;
|
||||||
|
if (properties && sideMenuRef.current) {
|
||||||
|
const pageType = Object.values(properties).find(
|
||||||
|
(item) => item['x-uid'] === params.name && item['x-component'] === 'Menu.Item',
|
||||||
|
);
|
||||||
|
if (pageType) {
|
||||||
|
sideMenuRef.current.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
sideMenuRef.current.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data?.data, params.name, sideMenuRef]);
|
||||||
|
|
||||||
|
const schema = useMemo(() => {
|
||||||
|
const s = filterByACL(data?.data, ctx);
|
||||||
|
if (s?.['x-component-props']) {
|
||||||
|
s['x-component-props']['useProps'] = useMenuProps;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}, [data?.data]);
|
||||||
|
|
||||||
|
useRequest(
|
||||||
|
{
|
||||||
|
url: 'applicationPlugins:list',
|
||||||
|
params: {
|
||||||
|
sort: 'id',
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ data }) => {
|
||||||
|
setHasNotice(true);
|
||||||
|
const errorPlugins = data.filter((item) => !item.isCompatible);
|
||||||
|
if (errorPlugins.length) {
|
||||||
|
notification.error({
|
||||||
|
message: 'Plugin dependencies check failed',
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
These plugins failed dependency checks. Please go to the{' '}
|
||||||
|
<Link to="/admin/pm/list/local/">plugin management page</Link> for more details.{' '}
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
{errorPlugins.map((item) => (
|
||||||
|
<li key={item.id}>
|
||||||
|
{item.displayName} - {item.packageName}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manual: true,
|
||||||
|
// ready: !hasNotice,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return void 0;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SchemaIdContext.Provider value={defaultSelectedUid}>
|
||||||
|
<SchemaComponent memoized scope={{ useMenuProps, onSelect, sideMenuRef, defaultSelectedUid }} schema={schema} />
|
||||||
|
</SchemaIdContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MyRouteSchemaComponent({ name }: { name: string }) {
|
||||||
|
return <RemoteSchemaComponent onlyRenderProperties uid={name} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InternalAdminLayout = (props: any) => {
|
||||||
|
const app = useApp();
|
||||||
|
const sideMenuRef = useRef<HTMLDivElement>();
|
||||||
|
const result = useSystemSettings();
|
||||||
|
const params = useParams<{ name?: string }>();
|
||||||
|
const { token } = useToken();
|
||||||
|
const { render } = useAppSpin();
|
||||||
|
const { title } = useDocumentTitle();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [items, setItems] = useState<TabsProps['items']>([]);
|
||||||
|
const pageStyle = usePageStyle();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (params.name && title && pageStyle === 'tab') {
|
||||||
|
setItems((items) => {
|
||||||
|
if (!items.find((value) => value.key === params.name)) {
|
||||||
|
return [
|
||||||
|
...items,
|
||||||
|
{
|
||||||
|
key: params.name,
|
||||||
|
label: title,
|
||||||
|
children: <MyRouteSchemaComponent name={params.name} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [params.name, title]);
|
||||||
|
|
||||||
|
const onEdit = (targetKey: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => {
|
||||||
|
if (action === 'remove') {
|
||||||
|
setItems((items) => {
|
||||||
|
return items.filter((item) => item.key !== targetKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Layout.Header
|
||||||
|
className={css`
|
||||||
|
.ant-menu.ant-menu-dark .ant-menu-item-selected,
|
||||||
|
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,
|
||||||
|
.ant-menu-submenu-horizontal.ant-menu-submenu-selected {
|
||||||
|
background-color: ${token.colorBgHeaderMenuActive};
|
||||||
|
color: ${token.colorTextHeaderMenuActive};
|
||||||
|
}
|
||||||
|
.ant-menu-dark.ant-menu-horizontal > .ant-menu-item:hover {
|
||||||
|
background-color: ${token.colorBgHeaderMenuHover};
|
||||||
|
color: ${token.colorTextHeaderMenuHover};
|
||||||
|
}
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: var(--nb-header-height);
|
||||||
|
line-height: var(--nb-header-height);
|
||||||
|
padding: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background-color: ${token.colorBgHeader};
|
||||||
|
|
||||||
|
.ant-menu {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-item {
|
||||||
|
color: ${token.colorTextHeaderMenu};
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px 0 12px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={css`
|
||||||
|
object-fit: contain;
|
||||||
|
height: 28px;
|
||||||
|
`}
|
||||||
|
src={result?.data?.data?.logo?.url}
|
||||||
|
/>
|
||||||
|
<h1
|
||||||
|
className={css`
|
||||||
|
color: #fff;
|
||||||
|
height: 32px;
|
||||||
|
margin: 0 0 0 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 32px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{result?.data?.data?.title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 0;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<MenuEditor sideMenuRef={sideMenuRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<PinnedPluginList />
|
||||||
|
<MobileLink />
|
||||||
|
<Notifications />
|
||||||
|
<OnlineUserDropdown />
|
||||||
|
<CurrentUser />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout.Header>
|
||||||
|
{params.name && (
|
||||||
|
<Layout.Sider
|
||||||
|
className={css`
|
||||||
|
height: 100%;
|
||||||
|
/* position: fixed; */
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
z-index: 100;
|
||||||
|
.ant-layout-sider-children {
|
||||||
|
top: var(--nb-header-height);
|
||||||
|
position: fixed;
|
||||||
|
width: 200px;
|
||||||
|
height: calc(100vh - var(--nb-header-height));
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
theme={'light'}
|
||||||
|
ref={sideMenuRef}
|
||||||
|
></Layout.Sider>
|
||||||
|
)}
|
||||||
|
<Layout.Content
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
> div {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.ant-layout-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
padding: 0px 50px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className={css`
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: var(--nb-header-height);
|
||||||
|
line-height: var(--nb-header-height);
|
||||||
|
background: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
`}
|
||||||
|
></header>
|
||||||
|
<>
|
||||||
|
{params.name && pageStyle === 'tab' ? (
|
||||||
|
<Tabs
|
||||||
|
className={css`
|
||||||
|
margin: 0;
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
type="editable-card"
|
||||||
|
items={items}
|
||||||
|
onEdit={onEdit}
|
||||||
|
hideAdd
|
||||||
|
onChange={(key) => {
|
||||||
|
navigate(`/admin/${key}`);
|
||||||
|
}}
|
||||||
|
activeKey={params.name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Outlet />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTabSettings = (props) => {
|
||||||
|
return {
|
||||||
|
key: 'tab',
|
||||||
|
eventKey: 'tab',
|
||||||
|
label: <Label {...props} />,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useHeraVersion = () => {
|
||||||
|
const version = usePluginVersion();
|
||||||
|
return {
|
||||||
|
key: 'hera-version',
|
||||||
|
eventKey: 'hera-version',
|
||||||
|
label: <span>赫拉系统 - {version}</span>,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePageStyle = () => {
|
||||||
|
return useContext(PageStyleContext).style;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HeraAdminLayout = (props) => {
|
||||||
|
const { addMenuItem } = useCurrentUserSettingsMenu();
|
||||||
|
const [style, setStyle] = useState('classical');
|
||||||
|
const tabItem = useTabSettings({ style, setStyle });
|
||||||
|
const heraVersion = useHeraVersion();
|
||||||
|
const { designable, setDesignable } = useDesignable();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addMenuItem(tabItem, { before: 'divider_3' });
|
||||||
|
}, [addMenuItem, tabItem]);
|
||||||
|
useEffect(() => {
|
||||||
|
addMenuItem(heraVersion, { before: 'divider_1' });
|
||||||
|
}, [addMenuItem, tabItem]);
|
||||||
|
|
||||||
|
const AdminComponent = (
|
||||||
|
<AdminProvider>
|
||||||
|
<PageStyleContext.Provider value={{ style }}>
|
||||||
|
<InternalAdminLayout {...props} />
|
||||||
|
</PageStyleContext.Provider>
|
||||||
|
<FloatButton.Group trigger="hover" type="primary" style={{ right: 24, zIndex: 1250 }} icon={<ToolOutlined />}>
|
||||||
|
<FloatButton icon={<HighlightOutlined />} onClick={() => setDesignable(!designable)} />
|
||||||
|
<FloatButton icon={<CalculatorOutlined />} />
|
||||||
|
<FloatButton icon={<CommentOutlined />} />
|
||||||
|
</FloatButton.Group>
|
||||||
|
</AdminProvider>
|
||||||
|
);
|
||||||
|
return AdminComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PageStyleContextValue {
|
||||||
|
style: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageStyleContext = createContext<PageStyleContextValue>({
|
||||||
|
style: 'classical',
|
||||||
|
});
|
||||||
|
|
||||||
|
function Label({ style, setStyle }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<SelectWithTitle
|
||||||
|
title={t('Page style')}
|
||||||
|
defaultValue={style}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: t('classical'),
|
||||||
|
value: 'classical',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('tabs'),
|
||||||
|
value: 'tab',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={setStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
23
packages/plugins/@hera/plugin-core/src/client/locale.tsx
Normal file
23
packages/plugins/@hera/plugin-core/src/client/locale.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Application, useApp, tval as nTval } from '@nocobase/client';
|
||||||
|
|
||||||
|
const NAMESPACE = '@hera/plugin-core';
|
||||||
|
|
||||||
|
export class Locale {
|
||||||
|
private app: Application;
|
||||||
|
|
||||||
|
constructor(app: Application) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
lang(key: string) {
|
||||||
|
return this.app.i18n.t(key, { ns: NAMESPACE });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTranslation = () => {
|
||||||
|
const { i18n } = useApp();
|
||||||
|
const t = (key: string, props = {}) => i18n.t(key, { ns: NAMESPACE, ...props });
|
||||||
|
return { t };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
|
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CollectionRecordProvider, DataBlockProvider, RemoteSchemaComponent, css } from '@nocobase/client';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import pathToRegexp from 'path-to-regexp';
|
||||||
|
import { useHeadStyles } from '../pages/style';
|
||||||
|
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
||||||
|
|
||||||
|
export const DetailsPage: React.FC = () => {
|
||||||
|
const params = useParams<any>();
|
||||||
|
const regexp = pathToRegexp(':collection/:cid?/:association?/:aid?');
|
||||||
|
const match = regexp.exec(params['*']);
|
||||||
|
const [_, collection, cid] = match;
|
||||||
|
const { styles } = useHeadStyles();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
.ant-tabs-nav {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 24px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.ant-tabs-content-holder {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className={`${styles['.pageHeaderCss']}`}>
|
||||||
|
<AntdPageHeader
|
||||||
|
ghost={false}
|
||||||
|
title={'详情页面'}
|
||||||
|
onBack={() => {
|
||||||
|
navigate(-1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataBlockProvider collection={collection}>
|
||||||
|
<CollectionRecordProvider record={{ id: cid }} parentRecord={null}>
|
||||||
|
<RemoteSchemaComponent uid={params.pageId} onlyRenderProperties />
|
||||||
|
</CollectionRecordProvider>
|
||||||
|
</DataBlockProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
68
packages/plugins/@hera/plugin-core/src/client/pages/Home.tsx
Normal file
68
packages/plugins/@hera/plugin-core/src/client/pages/Home.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useStyles } from './style';
|
||||||
|
import { Carousel, Image } from 'antd';
|
||||||
|
import { useRequest } from '@nocobase/client';
|
||||||
|
import { useAppSpin } from '../layouts';
|
||||||
|
|
||||||
|
export const HomePage: React.FC<{}> = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { render } = useAppSpin();
|
||||||
|
const { data, loading } = useRequest<{ data: any }>({
|
||||||
|
url: 'home_page_presentations:list',
|
||||||
|
params: {
|
||||||
|
'appends[]': 'pictures',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (loading) {
|
||||||
|
return render();
|
||||||
|
}
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
return (
|
||||||
|
<div className={styles.home}>
|
||||||
|
<header>
|
||||||
|
<div className="headerStyle">
|
||||||
|
<span className="headerTitle">上海创兴建筑设备租赁有限公司</span>
|
||||||
|
<ul>
|
||||||
|
<li className="active">公司简介</li>
|
||||||
|
<li>新闻中心</li>
|
||||||
|
<li>产品展示</li>
|
||||||
|
<li>工程案例</li>
|
||||||
|
<li>联系我们</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Carousel autoplay>
|
||||||
|
{data.data.map((item) => (
|
||||||
|
<div key={item.id}>
|
||||||
|
<Image preview={false} src={item.pictures[0].url} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a>公司简介</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a>真实合同</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a>企业文化</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a>联系我们</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin">管理系统入口</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<span>©2023-{year} 上海道有云网络科技有限公司 版权所有 沪ICP备2023024678号</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,645 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SchemaComponent } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const HomePageConfiguration = (props) => {
|
||||||
|
return (
|
||||||
|
<SchemaComponent
|
||||||
|
schema={{
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableBlockProvider',
|
||||||
|
'x-acl-action': 'home_page_presentations:list',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: 'home_page_presentations',
|
||||||
|
resource: 'home_page_presentations',
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
pageSize: 20,
|
||||||
|
},
|
||||||
|
rowKey: 'id',
|
||||||
|
showIndex: true,
|
||||||
|
dragSort: false,
|
||||||
|
disableTemplate: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-filter-targets': [],
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-initializer': 'TableActionInitializers',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 'var(--nb-spacing)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
bia185yhk1i: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-action': 'create',
|
||||||
|
title: "{{t('Add new')}}",
|
||||||
|
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-component-props': {
|
||||||
|
openMode: 'drawer',
|
||||||
|
type: 'primary',
|
||||||
|
component: 'CreateRecordAction',
|
||||||
|
icon: 'PlusOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'right',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Add record") }}',
|
||||||
|
'x-component': 'Action.Container',
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-initializer': 'TabPaneInitializersForCreateFormBlock',
|
||||||
|
properties: {
|
||||||
|
tab1: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Add new")}}',
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'CreateFormBlockInitializers',
|
||||||
|
properties: {
|
||||||
|
ij6ctts16tv: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
mrm9wm6wk4r: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
jab8tlxoft2: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: true,
|
||||||
|
},
|
||||||
|
'x-acl-action': 'home_page_presentations:create',
|
||||||
|
'x-decorator': 'FormBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
resource: 'home_page_presentations',
|
||||||
|
collection: 'home_page_presentations',
|
||||||
|
},
|
||||||
|
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
'8qckd230c0v': {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useFormBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'FormItemInitializers',
|
||||||
|
properties: {
|
||||||
|
'7ijaitrd558': {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
ib3pda67y9v: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'string',
|
||||||
|
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field': 'home_page_presentations.title',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-uid': 'vxy8me9095c',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'ycic8uhzuvi',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '8tec19ad33p',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
dgch495uwir: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
ullibu2n4sg: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
pictures: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'string',
|
||||||
|
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field':
|
||||||
|
'home_page_presentations.pictures',
|
||||||
|
'x-component-props': {
|
||||||
|
action: 'attachments:create',
|
||||||
|
},
|
||||||
|
'x-uid': '2ys0er1yt11',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '5suog4i8d3x',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'gikgxfwjxnh',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'q0jjxfzgxec',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-initializer': 'CreateFormActionInitializers',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
layout: 'one-column',
|
||||||
|
style: {
|
||||||
|
marginTop: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
'6zu1h8l4sjs': {
|
||||||
|
version: '2.0',
|
||||||
|
title: '{{ t("Submit") }}',
|
||||||
|
'x-action': 'submit',
|
||||||
|
'x-component': 'Action',
|
||||||
|
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
htmlType: 'submit',
|
||||||
|
useProps: '{{ useCreateActionProps }}',
|
||||||
|
},
|
||||||
|
'x-action-settings': {
|
||||||
|
triggerWorkflows: [],
|
||||||
|
},
|
||||||
|
type: 'void',
|
||||||
|
'x-uid': 'ycwy5sel90f',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'huu8pqmx0to',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '9936zs1ncni',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '2tjk5abg4db',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '8lqi5ffepko',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '4sho4nn7ffs',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'w8hhrq8tv7w',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'ujexdqygj0s',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'n1re231jc8z',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'kc5guu7w3ll',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'yr7deayw8b2',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'nm9vaco6bnm',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
yx9ugoktyio: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'array',
|
||||||
|
'x-initializer': 'TableColumnInitializers',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
useProps: '{{ useTableBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Actions") }}',
|
||||||
|
'x-action-column': 'actions',
|
||||||
|
'x-decorator': 'TableV2.Column.ActionBar',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
|
||||||
|
'x-initializer': 'TableActionColumnInitializers',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'DndContext',
|
||||||
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
split: '|',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
yaif2vuaskw: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Edit") }}',
|
||||||
|
'x-action': 'update',
|
||||||
|
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
openMode: 'drawer',
|
||||||
|
icon: 'EditOutlined',
|
||||||
|
},
|
||||||
|
'x-decorator': 'ACLActionProvider',
|
||||||
|
'x-designer-props': {
|
||||||
|
linkageAction: true,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Edit record") }}',
|
||||||
|
'x-component': 'Action.Container',
|
||||||
|
'x-component-props': {
|
||||||
|
className: 'nb-action-popup',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
tabs: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Tabs',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-initializer': 'TabPaneInitializers',
|
||||||
|
properties: {
|
||||||
|
tab1: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Edit")}}',
|
||||||
|
'x-component': 'Tabs.TabPane',
|
||||||
|
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'RecordBlockInitializers',
|
||||||
|
properties: {
|
||||||
|
og49nkup6th: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
a7woyamsray: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
'2pn91gtfnml': {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-acl-action-props': {
|
||||||
|
skipScopeCheck: false,
|
||||||
|
},
|
||||||
|
'x-acl-action': 'home_page_presentations:update',
|
||||||
|
'x-decorator': 'FormBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
useSourceId: '{{ useSourceIdFromParentRecord }}',
|
||||||
|
useParams: '{{ useParamsFromRecord }}',
|
||||||
|
action: 'get',
|
||||||
|
resource: 'home_page_presentations',
|
||||||
|
collection: 'home_page_presentations',
|
||||||
|
},
|
||||||
|
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-component-props': {},
|
||||||
|
properties: {
|
||||||
|
j7rysan6k0n: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-component-props': {
|
||||||
|
useProps: '{{ useFormBlockProps }}',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
grid: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid',
|
||||||
|
'x-initializer': 'FormItemInitializers',
|
||||||
|
properties: {
|
||||||
|
og920rtknti: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
t32058hd12p: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'string',
|
||||||
|
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field':
|
||||||
|
'home_page_presentations.title',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-uid': '70xzdgy6vgh',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '7e6gwdvzppj',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'k4m8a8dwsun',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
giboazc3b0y: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Row',
|
||||||
|
properties: {
|
||||||
|
lpkhllvqdga: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Grid.Col',
|
||||||
|
properties: {
|
||||||
|
pictures: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'string',
|
||||||
|
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-collection-field':
|
||||||
|
'home_page_presentations.pictures',
|
||||||
|
'x-component-props': {
|
||||||
|
action: 'attachments:create',
|
||||||
|
},
|
||||||
|
'x-uid': 'w6mytf69k6m',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'jzv9t7stcz0',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'chxq8h76j8o',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'eceqgc8ontl',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-initializer': 'UpdateFormActionInitializers',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
layout: 'one-column',
|
||||||
|
style: {
|
||||||
|
marginTop: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
zusitgob1u5: {
|
||||||
|
version: '2.0',
|
||||||
|
title: '{{ t("Submit") }}',
|
||||||
|
'x-action': 'submit',
|
||||||
|
'x-component': 'Action',
|
||||||
|
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
htmlType: 'submit',
|
||||||
|
useProps: '{{ useUpdateActionProps }}',
|
||||||
|
},
|
||||||
|
'x-action-settings': {
|
||||||
|
triggerWorkflows: [],
|
||||||
|
},
|
||||||
|
type: 'void',
|
||||||
|
'x-uid': 'pwuvu6v6eit',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'njgslgv92jr',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '49xyfm0iyd4',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'zux6vavjvq7',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'u9yk8wjhla7',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'indaws297z5',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'cn3gijbx29e',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'hkvq7lffm90',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'yg2hljxi2le',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '4v136x5hvps',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'ggpjjpvi54e',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': '7s3zizxt9l9',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'dm7808xtgog',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
r79uwspdiik: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
version: '2.0',
|
||||||
|
'x-collection-field': 'home_page_presentations.title',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-component-props': {
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': null,
|
||||||
|
'x-decorator-props': {
|
||||||
|
labelStyle: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'o34iouv6l1z',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'zfy17cqeelp',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
qni8cvpu4vk: {
|
||||||
|
version: '2.0',
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableV2.Column.Decorator',
|
||||||
|
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
pictures: {
|
||||||
|
version: '2.0',
|
||||||
|
'x-collection-field': 'home_page_presentations.pictures',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-component-props': {
|
||||||
|
size: 'small',
|
||||||
|
action: 'attachments:create',
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': null,
|
||||||
|
'x-decorator-props': {
|
||||||
|
labelStyle: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'ocqfnyahz32',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'r23g6b37mnt',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': 'uq0ueuxd6jy',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'ed94xx03myk',
|
||||||
|
'x-uid': 'hmriicsupa7',
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AdminProvider, RemoteCollectionManagerProvider, RemoteSchemaComponent } from '@nocobase/client';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
export const OutboundPage: React.FC = () => {
|
||||||
|
const params = useParams();
|
||||||
|
return (
|
||||||
|
<AdminProvider>
|
||||||
|
<RemoteCollectionManagerProvider>
|
||||||
|
<RemoteSchemaComponent uid={params.id} />
|
||||||
|
</RemoteCollectionManagerProvider>
|
||||||
|
</AdminProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const PageLayout = (props) => {
|
||||||
|
if (props.visible) {
|
||||||
|
return <div>{props.children}</div>;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
137
packages/plugins/@hera/plugin-core/src/client/pages/style.ts
Normal file
137
packages/plugins/@hera/plugin-core/src/client/pages/style.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
|
||||||
|
export const useStyles = createStyles(({ css }) => ({
|
||||||
|
home: css`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
header {
|
||||||
|
.headerStyle {
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 1px 1px #e3e3e3;
|
||||||
|
padding: 0px 10px 0px 15px;
|
||||||
|
.headerTitle {
|
||||||
|
height: 60px;
|
||||||
|
color: #6c6c6c;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
color: #6c6c6c;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 0 0 0 25px;
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 60px;
|
||||||
|
&:hover {
|
||||||
|
background-color: #e3e3e3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
background-color: #e3e3e3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 20px 10px;
|
||||||
|
height: 500px;
|
||||||
|
div {
|
||||||
|
height: 500px;
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-carousel .slick-dots-bottom {
|
||||||
|
bottom: 50px;
|
||||||
|
}
|
||||||
|
.ant-carousel .slick-dots li {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-carousel .slick-dots li button {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.ant-carousel .slick-dots li.slick-active button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 22px;
|
||||||
|
a {
|
||||||
|
color: #3372af;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
margin: 0;
|
||||||
|
bottom: 0;
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
a {
|
||||||
|
border-right: 1.5px solid black;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
&:nth-last-child(1) a {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
a {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useHeadStyles = createStyles(({ token }) => {
|
||||||
|
return {
|
||||||
|
'.pageHeaderCss': {
|
||||||
|
backgroundColor: token.colorBgContainer,
|
||||||
|
paddingInline: token.paddingXS,
|
||||||
|
'&.ant-page-header-has-footer': {
|
||||||
|
paddingTop: token.paddingSM,
|
||||||
|
paddingBottom: '0',
|
||||||
|
'.ant-page-header-heading-left': {},
|
||||||
|
'.ant-page-header-footer': { marginBlockStart: '0' },
|
||||||
|
},
|
||||||
|
'.ant-tabs-nav': { marginBottom: '0' },
|
||||||
|
'.ant-page-header-heading-title': {
|
||||||
|
color: token.colorText,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'.height0': {
|
||||||
|
fontSize: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
@ -0,0 +1,263 @@
|
|||||||
|
import { useAPIClient } from '@nocobase/client';
|
||||||
|
import { Button, Card, Drawer, Space, Spin, Table, Tag, Upload, message } from 'antd';
|
||||||
|
import type { UploadProps } from 'antd';
|
||||||
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
|
import React, { FC, useEffect, useState } from 'react';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import { UploadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const columns: ColumnsType<any> = [
|
||||||
|
{
|
||||||
|
title: '类型',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '命名空间',
|
||||||
|
dataIndex: 'namespace',
|
||||||
|
key: 'namespace',
|
||||||
|
render(value, record, index) {
|
||||||
|
return value ? <Tag>{value}</Tag> : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '功能',
|
||||||
|
dataIndex: 'function',
|
||||||
|
key: 'function',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据表标识集合',
|
||||||
|
dataIndex: 'collections',
|
||||||
|
key: 'collections',
|
||||||
|
render(value, record, index) {
|
||||||
|
return value ? (
|
||||||
|
<span>
|
||||||
|
{value.map((n) => (
|
||||||
|
<Tag key={n}>{n}</Tag>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '可导出',
|
||||||
|
dataIndex: 'dumpable',
|
||||||
|
key: 'dumpable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据表标识',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据表名称',
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const keyFn = (record) => [record.namespace ?? '', record.function ?? '', record.name ?? ''].join('_');
|
||||||
|
const RestoreButton = () => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const [requiredGroups, setRequiredGroups] = useState([]);
|
||||||
|
const [optionalGroups, setOptionalGroups] = useState([]);
|
||||||
|
const [userCollections, setUserCollections] = useState([]);
|
||||||
|
const [selectedOptionalGroups, setSelectedOptionalGroups] = useState([]);
|
||||||
|
const [selectedUserCollections, setSelectedUserCollections] = useState([]);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [key, setKey] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
setLoading(true);
|
||||||
|
api
|
||||||
|
.resource('duplicator')
|
||||||
|
.restore({
|
||||||
|
values: {
|
||||||
|
restoreKey: key,
|
||||||
|
selectedOptionalGroups,
|
||||||
|
selectedUserCollections,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setLoading(false);
|
||||||
|
message.success('恢复成功!');
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
message.error('恢复失败!');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
const props: UploadProps = {
|
||||||
|
name: 'file',
|
||||||
|
action: '/api/duplicator:upload',
|
||||||
|
headers: {
|
||||||
|
authorization: `Bearer ${api.auth.getToken()}`,
|
||||||
|
},
|
||||||
|
onChange(info) {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
message.success(`${info.file.name} file uploaded successfully`);
|
||||||
|
const key = info.file.response.data.key;
|
||||||
|
const meta = info.file.response.data.meta;
|
||||||
|
|
||||||
|
const { requiredGroups, selectedOptionalGroups, selectedUserCollections } = meta;
|
||||||
|
setKey(key);
|
||||||
|
setRequiredGroups(requiredGroups.map((i) => ({ ...i, category: 'required' })));
|
||||||
|
setOptionalGroups(selectedOptionalGroups.map((i) => ({ ...i, category: 'optional' })));
|
||||||
|
setUserCollections(
|
||||||
|
selectedUserCollections
|
||||||
|
.filter((i: string) => !i.startsWith('view_'))
|
||||||
|
.map((i) => ({ name: i, category: 'user' })),
|
||||||
|
);
|
||||||
|
} else if (info.file.status === 'error') {
|
||||||
|
message.error(`${info.file.name} file upload failed.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={showModal}>恢复</Button>
|
||||||
|
<Drawer
|
||||||
|
title="恢复"
|
||||||
|
open={isModalOpen}
|
||||||
|
onClose={handleCancel}
|
||||||
|
width={1200}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={handleCancel} loading={loading}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleOk} type="primary" loading={loading}>
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Upload {...props}>
|
||||||
|
<Button icon={<UploadOutlined />}>选择要恢复的文件</Button>
|
||||||
|
</Upload>
|
||||||
|
{requiredGroups.length !== 0 && (
|
||||||
|
<Table
|
||||||
|
rowKey={keyFn}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={[...requiredGroups, ...optionalGroups, ...userCollections]}
|
||||||
|
rowSelection={{
|
||||||
|
type: 'checkbox',
|
||||||
|
onChange(selectedRowKeys: React.Key[], selectedRows: any[]) {
|
||||||
|
setSelectedOptionalGroups(
|
||||||
|
selectedRows
|
||||||
|
.filter((item) => item.category === 'optional')
|
||||||
|
.map((item) => item.namespace + '.' + item.function),
|
||||||
|
);
|
||||||
|
setSelectedUserCollections(
|
||||||
|
selectedRows.filter((item) => item.category === 'user').map((item) => item.name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getCheckboxProps(record) {
|
||||||
|
return {
|
||||||
|
disabled: record.category === 'required',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectedRowKeys: requiredGroups.map(keyFn),
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 200,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const BackupCenterPanel = () => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const [requiredGroups, setRequiredGroups] = useState([]);
|
||||||
|
const [optionalGroups, setOptionalGroups] = useState([]);
|
||||||
|
const [userCollections, setUserCollections] = useState([]);
|
||||||
|
const [selectedOptionalGroups, setSelectedOptionalGroups] = useState([]);
|
||||||
|
const [selectedUserCollections, setSelectedUserCollections] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
api
|
||||||
|
.resource('duplicator')
|
||||||
|
.dumpableCollections()
|
||||||
|
.then((res) => {
|
||||||
|
const { requiredGroups, optionalGroups, userCollections } = res.data;
|
||||||
|
setRequiredGroups(requiredGroups.map((i) => ({ ...i, category: 'required' })));
|
||||||
|
setOptionalGroups(optionalGroups.map((i) => ({ ...i, category: 'optional' })));
|
||||||
|
setUserCollections(
|
||||||
|
userCollections
|
||||||
|
.filter((i: { name: string }) => !i.name.startsWith('view_'))
|
||||||
|
.map((i) => ({ ...i, category: 'user' })),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// ignore-error
|
||||||
|
});
|
||||||
|
}, [api]);
|
||||||
|
if (requiredGroups.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Card bordered={false}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Space> </Space>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
const response = await api.axios.post(
|
||||||
|
'/duplicator:dump',
|
||||||
|
{
|
||||||
|
selectedOptionalGroupNames: selectedOptionalGroups,
|
||||||
|
selectedUserCollections: selectedUserCollections,
|
||||||
|
},
|
||||||
|
{ responseType: 'arraybuffer' },
|
||||||
|
);
|
||||||
|
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||||
|
saveAs(blob, 'backup.nbdump');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
备份
|
||||||
|
</Button>
|
||||||
|
<RestoreButton />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
rowKey={keyFn}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={[...requiredGroups, ...optionalGroups, ...userCollections]}
|
||||||
|
rowSelection={{
|
||||||
|
type: 'checkbox',
|
||||||
|
onChange(selectedRowKeys: React.Key[], selectedRows: any[]) {
|
||||||
|
setSelectedOptionalGroups(
|
||||||
|
selectedRows
|
||||||
|
.filter((item) => item.category === 'optional')
|
||||||
|
.map((item) => item.namespace + '.' + item.function),
|
||||||
|
);
|
||||||
|
setSelectedUserCollections(
|
||||||
|
selectedRows.filter((item) => item.category === 'user').map((item) => item.name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getCheckboxProps(record) {
|
||||||
|
return {
|
||||||
|
disabled: record.category === 'required',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
defaultSelectedRowKeys: requiredGroups.map(keyFn),
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 200,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
|
import { useBlockRequestContext } from '@nocobase/client';
|
||||||
|
import { Descriptions, DescriptionsProps, Spin } from 'antd';
|
||||||
|
import { transformers } from '../components/GroupBlockConfigure/transformers';
|
||||||
|
import React from 'react';
|
||||||
|
export const GroupBlock = (props) => {
|
||||||
|
const field = useField<any>();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const measures = fieldSchema.parent['x-decorator-props'].params?.measures;
|
||||||
|
const { resource, service } = useBlockRequestContext();
|
||||||
|
if (service.loading && !field.loaded) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
const data = service.data?.data[0] ?? {};
|
||||||
|
const { option: tOption } = transformers;
|
||||||
|
measures.forEach((measuresItem) => {
|
||||||
|
if (measuresItem.fieldFormat) {
|
||||||
|
const value = measuresItem.fieldFormat.fieldValue;
|
||||||
|
const option = measuresItem.fieldFormat.option;
|
||||||
|
const decimal = measuresItem.fieldFormat.decimal;
|
||||||
|
if (option && option !== 'decimal') {
|
||||||
|
const component = tOption.filter((tValue) => tValue.value === option)[0].component;
|
||||||
|
data[value] = String(data[value]).includes(',') ? String(data[value]).replace(/,/g, '') : data[value];
|
||||||
|
data[value] = component(data[value]);
|
||||||
|
} else if (option && option === 'decimal') {
|
||||||
|
const component = tOption
|
||||||
|
.filter((tValue) => tValue.value === 'decimal')[0]
|
||||||
|
.childrens.filter((decimalOption) => decimalOption.value === decimal)[0].component;
|
||||||
|
data[value] = String(data[value]).includes(',') ? String(data[value]).replace(/,/g, '') : data[value];
|
||||||
|
data[value] = component(data[value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const items: DescriptionsProps['items'] = [];
|
||||||
|
for (const key in data) {
|
||||||
|
measures?.forEach((value) => {
|
||||||
|
if (value.field[0] === key) {
|
||||||
|
if (value.display) {
|
||||||
|
items.push({
|
||||||
|
key: key,
|
||||||
|
label: value.label,
|
||||||
|
children: data[key],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Descriptions title="汇总:" items={items} />;
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import { PDFViewer } from '../components/PDFViewer';
|
||||||
|
import { Space } from 'antd';
|
||||||
|
import { css } from '@nocobase/client';
|
||||||
|
import { usePDFViewerRef } from '../schema-initializer/PDFVIewerBlockInitializer';
|
||||||
|
|
||||||
|
export const InternalPDFViewer = (props) => {
|
||||||
|
const { usePdfPath: useMaybePdfPath } = props;
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const [width, setWidth] = useState(0);
|
||||||
|
const [, setHeight] = useState(0);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setWidth(containerRef.current.offsetWidth);
|
||||||
|
setHeight(containerRef.current.offsetHeight);
|
||||||
|
});
|
||||||
|
const ref = usePDFViewerRef();
|
||||||
|
const usePdfPath = useMaybePdfPath ?? (() => '');
|
||||||
|
const pdfPath = usePdfPath();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
display: flex;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
flex: 1;
|
||||||
|
`}
|
||||||
|
></div>
|
||||||
|
<Space></Space>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={css`
|
||||||
|
border: 1px dashed black;
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{pdfPath ? <PDFViewer file={pdfPath} width={width} ref={ref} /> : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from './useConfig';
|
||||||
|
export * from './usePrefixCls';
|
||||||
|
export * from './useToken';
|
@ -0,0 +1,5 @@
|
|||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
const { ConfigContext } = ConfigProvider;
|
||||||
|
export const useConfig = () => useContext(ConfigContext);
|
@ -0,0 +1,17 @@
|
|||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export const usePrefixCls = (
|
||||||
|
tag?: string,
|
||||||
|
props?: {
|
||||||
|
prefixCls?: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const { getPrefixCls } = useContext(ConfigProvider.ConfigContext) || {};
|
||||||
|
if ('ConfigContext' in ConfigProvider) {
|
||||||
|
return getPrefixCls?.(tag, props?.prefixCls) || '';
|
||||||
|
} else {
|
||||||
|
const prefix = props?.prefixCls ?? 'ant-';
|
||||||
|
return `${prefix}${tag ?? ''}`;
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import { theme } from 'antd';
|
||||||
|
import { CustomToken } from '../style';
|
||||||
|
|
||||||
|
interface Result extends ReturnType<typeof theme.useToken> {
|
||||||
|
token: CustomToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useToken = () => {
|
||||||
|
const result = theme.useToken();
|
||||||
|
return result as Result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useToken };
|
@ -0,0 +1,4 @@
|
|||||||
|
export * from './hooks';
|
||||||
|
export * from './loading';
|
||||||
|
export * from './portal';
|
||||||
|
export * from './style';
|
@ -0,0 +1,14 @@
|
|||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
export const loading = async (title: React.ReactNode = 'Loading...', processor: () => Promise<any>) => {
|
||||||
|
let hide: any = null;
|
||||||
|
const loading = setTimeout(() => {
|
||||||
|
hide = message.loading(title);
|
||||||
|
}, 100);
|
||||||
|
try {
|
||||||
|
return await processor();
|
||||||
|
} finally {
|
||||||
|
hide?.();
|
||||||
|
clearTimeout(loading);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,63 @@
|
|||||||
|
import { Observer, ReactFC } from '@formily/react';
|
||||||
|
import { observable } from '@formily/reactive';
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { render as reactRender, unmount as reactUnmount } from './render';
|
||||||
|
export interface IPortalProps {
|
||||||
|
id?: string | symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PortalMap = observable(new Map<string | symbol, React.ReactNode>());
|
||||||
|
|
||||||
|
export const createPortalProvider = (id: string | symbol) => {
|
||||||
|
const Portal: ReactFC<IPortalProps> = (props) => {
|
||||||
|
if (props.id && !PortalMap.has(props.id)) {
|
||||||
|
PortalMap.set(props.id, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{props.children}
|
||||||
|
<Observer>
|
||||||
|
{() => {
|
||||||
|
if (!props.id) return <></>;
|
||||||
|
const portal = PortalMap.get(props.id);
|
||||||
|
if (portal) return createPortal(portal, document.body);
|
||||||
|
return <></>;
|
||||||
|
}}
|
||||||
|
</Observer>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Portal.defaultProps = {
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
return Portal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createPortalRoot<T extends React.ReactNode>(host: HTMLElement, id: string) {
|
||||||
|
function render(renderer?: () => T) {
|
||||||
|
if (PortalMap.has(id)) {
|
||||||
|
PortalMap.set(id, renderer?.());
|
||||||
|
} else if (host) {
|
||||||
|
reactRender(<Fragment>{renderer?.()}</Fragment>, host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmount() {
|
||||||
|
if (PortalMap.has(id)) {
|
||||||
|
PortalMap.set(id, null);
|
||||||
|
}
|
||||||
|
if (host) {
|
||||||
|
const unmountResult = reactUnmount(host);
|
||||||
|
if (unmountResult && host.parentNode) {
|
||||||
|
host.parentNode?.removeChild(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
render,
|
||||||
|
unmount,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
import { ReactElement } from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
|
import type { Root } from 'react-dom/client';
|
||||||
|
|
||||||
|
// 移植自rc-util: https://github.com/react-component/util/blob/master/src/React/render.ts
|
||||||
|
|
||||||
|
type CreateRoot = (container: ContainerType) => Root;
|
||||||
|
|
||||||
|
// Let compiler not to search module usage
|
||||||
|
const fullClone = {
|
||||||
|
...ReactDOM,
|
||||||
|
} as typeof ReactDOM & {
|
||||||
|
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: {
|
||||||
|
usingClientEntryPoint?: boolean;
|
||||||
|
};
|
||||||
|
createRoot?: CreateRoot;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { version, render: reactRender, unmountComponentAtNode } = fullClone;
|
||||||
|
|
||||||
|
let createRoot: CreateRoot;
|
||||||
|
try {
|
||||||
|
const mainVersion = Number((version || '').split('.')[0]);
|
||||||
|
if (mainVersion >= 18 && fullClone.createRoot) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
createRoot = fullClone.createRoot;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Do nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWarning(skip: boolean) {
|
||||||
|
const { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } = fullClone;
|
||||||
|
|
||||||
|
if (
|
||||||
|
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED &&
|
||||||
|
typeof __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED === 'object'
|
||||||
|
) {
|
||||||
|
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = skip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MARK = '__antd_mobile_root__';
|
||||||
|
|
||||||
|
// ========================== Render ==========================
|
||||||
|
type ContainerType = (Element | DocumentFragment) & {
|
||||||
|
[MARK]?: Root;
|
||||||
|
};
|
||||||
|
|
||||||
|
function legacyRender(node: ReactElement, container: ContainerType) {
|
||||||
|
reactRender(node, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
function concurrentRender(node: ReactElement, container: ContainerType) {
|
||||||
|
toggleWarning(true);
|
||||||
|
const root = container[MARK] || createRoot(container);
|
||||||
|
toggleWarning(false);
|
||||||
|
root.render(node);
|
||||||
|
container[MARK] = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render(node: ReactElement, container: ContainerType) {
|
||||||
|
if (createRoot as unknown) {
|
||||||
|
concurrentRender(node, container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
legacyRender(node, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================== Unmount =========================
|
||||||
|
function legacyUnmount(container: ContainerType) {
|
||||||
|
return unmountComponentAtNode(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function concurrentUnmount(container: ContainerType) {
|
||||||
|
// Delay to unmount to avoid React 18 sync warning
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
container[MARK]?.unmount();
|
||||||
|
delete container[MARK];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unmount(container: ContainerType) {
|
||||||
|
if (createRoot as unknown) {
|
||||||
|
return concurrentUnmount(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacyUnmount(container);
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
|
||||||
|
import { useStyleRegister } from '@ant-design/cssinjs';
|
||||||
|
import { merge } from '@formily/shared';
|
||||||
|
import type { ComponentTokenMap, GlobalToken } from 'antd/es/theme/interface';
|
||||||
|
import { AliasToken } from 'antd/es/theme/internal';
|
||||||
|
import { useConfig, usePrefixCls, useToken } from './hooks';
|
||||||
|
|
||||||
|
export interface CustomToken extends AliasToken {
|
||||||
|
/** 顶部导航栏主色 */
|
||||||
|
colorPrimaryHeader: string;
|
||||||
|
/** 导航栏背景色 */
|
||||||
|
colorBgHeader: string;
|
||||||
|
/** 导航栏菜单背景色悬浮态 */
|
||||||
|
colorBgHeaderMenuHover: string;
|
||||||
|
/** 导航栏菜单背景色激活态 */
|
||||||
|
colorBgHeaderMenuActive: string;
|
||||||
|
/** 导航栏菜单文本色 */
|
||||||
|
colorTextHeaderMenu: string;
|
||||||
|
/** 导航栏菜单文本色悬浮态 */
|
||||||
|
colorTextHeaderMenuHover: string;
|
||||||
|
/** 导航栏菜单文本色激活态 */
|
||||||
|
colorTextHeaderMenuActive: string;
|
||||||
|
|
||||||
|
/** UI 配置色 */
|
||||||
|
colorSettings: string;
|
||||||
|
/** 鼠标悬浮时显示的背景色 */
|
||||||
|
colorBgSettingsHover: string;
|
||||||
|
/** 鼠标悬浮时显示的边框色 */
|
||||||
|
colorBorderSettingsHover: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OverrideComponent = keyof ComponentTokenMap | string;
|
||||||
|
|
||||||
|
export interface StyleInfo {
|
||||||
|
hashId: string;
|
||||||
|
prefixCls: string;
|
||||||
|
rootPrefixCls: string;
|
||||||
|
iconPrefixCls: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenWithCommonCls<T> = T & {
|
||||||
|
/** Wrap component class with `.` prefix */
|
||||||
|
componentCls: string;
|
||||||
|
/** Origin prefix which do not have `.` prefix */
|
||||||
|
prefixCls: string;
|
||||||
|
/** Wrap icon class with `.` prefix */
|
||||||
|
iconCls: string;
|
||||||
|
/** Wrap ant prefixCls class with `.` prefix */
|
||||||
|
antCls: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GenerateStyle<
|
||||||
|
ComponentToken extends object = TokenWithCommonCls<GlobalToken>,
|
||||||
|
ReturnType = CSSInterpolation,
|
||||||
|
> = (token: ComponentToken, options?: any) => ReturnType;
|
||||||
|
|
||||||
|
export const genCommonStyle = (token: any, componentPrefixCls: string): CSSObject => {
|
||||||
|
const { fontFamily, fontSize } = token;
|
||||||
|
|
||||||
|
const rootPrefixSelector = `[class^="${componentPrefixCls}"], [class*=" ${componentPrefixCls}"]`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
[rootPrefixSelector]: {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
|
||||||
|
'&::before, &::after': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
|
||||||
|
[rootPrefixSelector]: {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
|
||||||
|
'&::before, &::after': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type UseComponentStyleResult = {
|
||||||
|
wrapSSR: ReturnType<typeof useStyleRegister>;
|
||||||
|
hashId: string;
|
||||||
|
componentCls: string;
|
||||||
|
rootPrefixCls: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const genStyleHook = <ComponentName extends OverrideComponent>(
|
||||||
|
component: ComponentName,
|
||||||
|
styleFn: (token: TokenWithCommonCls<CustomToken>, props: any, info: StyleInfo) => CSSInterpolation,
|
||||||
|
) => {
|
||||||
|
return (props?: any): UseComponentStyleResult => {
|
||||||
|
const { theme, token, hashId } = useToken();
|
||||||
|
const { getPrefixCls, iconPrefixCls } = useConfig();
|
||||||
|
const prefixCls = usePrefixCls(component);
|
||||||
|
const rootPrefixCls = getPrefixCls();
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapSSR: useStyleRegister(
|
||||||
|
{
|
||||||
|
theme: theme as any,
|
||||||
|
token,
|
||||||
|
hashId,
|
||||||
|
path: ['formily-antd', component, prefixCls, iconPrefixCls],
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const componentCls = `.${prefixCls}`;
|
||||||
|
const mergedToken: TokenWithCommonCls<CustomToken> = merge(token, {
|
||||||
|
componentCls,
|
||||||
|
prefixCls,
|
||||||
|
iconCls: `.${iconPrefixCls}`,
|
||||||
|
antCls: `.${rootPrefixCls}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const styleInterpolation = styleFn(mergedToken, props, {
|
||||||
|
hashId,
|
||||||
|
prefixCls,
|
||||||
|
rootPrefixCls,
|
||||||
|
iconPrefixCls,
|
||||||
|
});
|
||||||
|
return [genCommonStyle(token, prefixCls), styleInterpolation];
|
||||||
|
},
|
||||||
|
),
|
||||||
|
hashId,
|
||||||
|
componentCls: prefixCls,
|
||||||
|
rootPrefixCls,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,182 @@
|
|||||||
|
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { onFieldChange, FormPath } from '@formily/core';
|
||||||
|
import { RecursionField, connect, mapProps, observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
import { Space, message } from 'antd';
|
||||||
|
import { isFunction } from 'mathjs2';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RecordProvider, RemoteSelectProps, useAPIClient } from '@nocobase/client';
|
||||||
|
import { __UNSAFE__ } from '@nocobase/client';
|
||||||
|
import { RemoteSelect } from '../remote-select';
|
||||||
|
|
||||||
|
const { getInnermostKeyAndValue, useServiceOptions, useAssociationFieldContext, isVariable } = __UNSAFE__;
|
||||||
|
|
||||||
|
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
||||||
|
action?: string;
|
||||||
|
multiple?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterAnalyses = (filters): any[] => {
|
||||||
|
if (!filters) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const type = Object.keys(filters)[0] || '$and';
|
||||||
|
const conditions = filters[type];
|
||||||
|
const results = [];
|
||||||
|
conditions?.map((c) => {
|
||||||
|
const jsonlogic = getInnermostKeyAndValue(c);
|
||||||
|
const operator = jsonlogic?.key;
|
||||||
|
|
||||||
|
if (!operator) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const regex = /\{\{\$(?:[a-zA-Z_]\w*)\.([a-zA-Z_]\w*)(?:\.id)?\}\}/;
|
||||||
|
const fieldName = jsonlogic?.value?.match?.(regex)?.[1];
|
||||||
|
if (fieldName) {
|
||||||
|
results.push(fieldName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InternalAssociationSelect = observer((props: AssociationSelectProps) => {
|
||||||
|
const { objectValue = true } = props;
|
||||||
|
const field: any = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const service = useServiceOptions(props);
|
||||||
|
const { options: collectionField } = useAssociationFieldContext();
|
||||||
|
const initValue = isVariable(props.value) ? undefined : props.value;
|
||||||
|
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
|
||||||
|
// 因为通过 Schema 的形式书写的组件,在值变更的时候 `value` 的值没有改变,所以需要维护一个 `innerValue` 来变更值
|
||||||
|
const [innerValue, setInnerValue] = useState(value);
|
||||||
|
const addMode = fieldSchema['x-component-props']?.addMode;
|
||||||
|
const isAllowAddNew = fieldSchema['x-add-new'];
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { multiple } = props;
|
||||||
|
const form = useForm();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const resource = api.resource(collectionField.target);
|
||||||
|
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter);
|
||||||
|
useEffect(() => {
|
||||||
|
const initValue = isVariable(field.value) ? undefined : field.value;
|
||||||
|
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
|
||||||
|
setInnerValue(value);
|
||||||
|
}, [field.value]);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = uid();
|
||||||
|
form.addEffects(id, () => {
|
||||||
|
if (linkageFields?.length > 0) {
|
||||||
|
//支持深层次子表单
|
||||||
|
onFieldChange('*', (fieldPath: any) => {
|
||||||
|
if (linkageFields.includes(fieldPath.props.name) && field.value) {
|
||||||
|
props.onChange(field.initialValue);
|
||||||
|
setInnerValue(field.initialValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
form.removeEffects(id);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateAction = async (props) => {
|
||||||
|
const { search: value, callBack } = props;
|
||||||
|
const {
|
||||||
|
data: { data },
|
||||||
|
} = await resource.create({
|
||||||
|
values: {
|
||||||
|
[field?.componentProps?.fieldNames?.label || 'id']: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (data) {
|
||||||
|
if (['m2m', 'o2m'].includes(collectionField?.interface) && multiple !== false) {
|
||||||
|
const values = form.getValuesIn(field.path) || [];
|
||||||
|
values.push(data);
|
||||||
|
form.setValuesIn(field.path, values);
|
||||||
|
field.onInput(values);
|
||||||
|
} else {
|
||||||
|
form.setValuesIn(field.path, data);
|
||||||
|
field.onInput(data);
|
||||||
|
}
|
||||||
|
isFunction(callBack) && callBack?.();
|
||||||
|
message.success(t('Saved successfully'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const QuickAddContent = (props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => handleCreateAction(props)}
|
||||||
|
style={{ cursor: 'pointer', padding: '5px 12px', color: '#0d0c0c' }}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
<span style={{ paddingLeft: 5 }}>{t('Add') + ` “${props.search}” `}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div key={fieldSchema.name}>
|
||||||
|
<Space.Compact style={{ display: 'flex', lineHeight: '32px' }}>
|
||||||
|
<RemoteSelect
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
{...props}
|
||||||
|
size={'middle'}
|
||||||
|
objectValue={objectValue}
|
||||||
|
value={value || innerValue}
|
||||||
|
service={service}
|
||||||
|
onChange={(value) => {
|
||||||
|
const val = value?.length !== 0 ? value : null;
|
||||||
|
props.onChange?.(val);
|
||||||
|
}}
|
||||||
|
CustomDropdownRender={addMode === 'quickAdd' && QuickAddContent}
|
||||||
|
></RemoteSelect>
|
||||||
|
|
||||||
|
{(addMode === 'modalAdd' || isAllowAddNew) && (
|
||||||
|
<RecordProvider record={null}>
|
||||||
|
<RecursionField
|
||||||
|
onlyRenderProperties
|
||||||
|
basePath={field.address}
|
||||||
|
schema={fieldSchema}
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] === 'Action';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RecordProvider>
|
||||||
|
)}
|
||||||
|
</Space.Compact>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface AssociationSelectInterface {
|
||||||
|
(props: any): React.ReactElement;
|
||||||
|
Designer: React.FC;
|
||||||
|
FilterDesigner: React.FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AssociationSelect = InternalAssociationSelect as unknown as AssociationSelectInterface;
|
||||||
|
|
||||||
|
export const AssociationSelectReadPretty = connect(
|
||||||
|
(props: any) => {
|
||||||
|
const service = useServiceOptions(props);
|
||||||
|
if (props.fieldNames) {
|
||||||
|
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
mapProps(
|
||||||
|
{
|
||||||
|
dataSource: 'options',
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
(props, field) => {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
fieldNames: props.fieldNames && { ...props.fieldNames, ...field.componentProps.fieldNames },
|
||||||
|
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
@ -0,0 +1,95 @@
|
|||||||
|
import { Field } from '@formily/core';
|
||||||
|
import { connect, mapReadPretty, observer, useField, useForm } from '@formily/react';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
SchemaComponentOptions,
|
||||||
|
useCollection_deprecated,
|
||||||
|
useAssociationCreateActionProps as useCAP,
|
||||||
|
Action,
|
||||||
|
__UNSAFE__,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { InternaDrawerSubTable } from './InternalDrawerSubTable';
|
||||||
|
import { AssociationSelect } from './AssociationSelect';
|
||||||
|
const {
|
||||||
|
CreateRecordAction,
|
||||||
|
useAssociationFieldContext,
|
||||||
|
InternalPicker,
|
||||||
|
InternalNester,
|
||||||
|
AssociationFieldProvider,
|
||||||
|
InternaPopoverNester,
|
||||||
|
InternalSubTable,
|
||||||
|
InternalCascader,
|
||||||
|
InternalFileManager,
|
||||||
|
InternalCascadeSelect,
|
||||||
|
SubTable,
|
||||||
|
ReadPretty,
|
||||||
|
Nester,
|
||||||
|
} = __UNSAFE__;
|
||||||
|
|
||||||
|
const EditableAssociationField = observer(
|
||||||
|
(props: any) => {
|
||||||
|
const { multiple } = props;
|
||||||
|
const field: Field = useField();
|
||||||
|
const form = useForm();
|
||||||
|
const { options: collectionField, currentMode } = useAssociationFieldContext();
|
||||||
|
|
||||||
|
const useCreateActionProps = () => {
|
||||||
|
const { onClick } = useCAP();
|
||||||
|
const actionField: any = useField();
|
||||||
|
const { getPrimaryKey } = useCollection_deprecated();
|
||||||
|
const primaryKey = getPrimaryKey();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
await onClick();
|
||||||
|
const { data } = actionField.data?.data?.data || {};
|
||||||
|
if (data) {
|
||||||
|
if (['m2m', 'o2m'].includes(collectionField?.interface) && multiple !== false) {
|
||||||
|
const values = form.getValuesIn(field.path) || [];
|
||||||
|
if (!values.find((v) => v[primaryKey] === data[primaryKey])) {
|
||||||
|
values.push(data);
|
||||||
|
form.setValuesIn(field.path, values);
|
||||||
|
field.onInput(values);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
form.setValuesIn(field.path, data);
|
||||||
|
field.onInput(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions scope={{ useCreateActionProps }} components={{ CreateRecordAction }}>
|
||||||
|
{currentMode === 'Picker' && <InternalPicker {...props} />}
|
||||||
|
{currentMode === 'Nester' && <InternalNester {...props} />}
|
||||||
|
{currentMode === 'PopoverNester' && <InternaPopoverNester {...props} />}
|
||||||
|
{currentMode === 'Select' && <AssociationSelect {...props} />}
|
||||||
|
{currentMode === 'SubTable' && <InternalSubTable {...props} />}
|
||||||
|
{currentMode === 'FileManager' && <InternalFileManager {...props} />}
|
||||||
|
{currentMode === 'CascadeSelect' && <InternalCascadeSelect {...props} />}
|
||||||
|
{currentMode === 'DrawerSubTable' && <InternaDrawerSubTable {...props} />}
|
||||||
|
{currentMode === 'Cascader' && <InternalCascader {...props} />}
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'EditableAssociationField' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const ExtendedEditable = observer(
|
||||||
|
(props) => {
|
||||||
|
return (
|
||||||
|
<AssociationFieldProvider>
|
||||||
|
<EditableAssociationField {...props} />
|
||||||
|
</AssociationFieldProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'ExtendedEditable' },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ExtendedAssociationField: any = connect(ExtendedEditable, mapReadPretty(ReadPretty));
|
||||||
|
ExtendedAssociationField.SubTable = SubTable;
|
||||||
|
ExtendedAssociationField.Nester = Nester;
|
||||||
|
ExtendedAssociationField.AddNewer = Action.Container;
|
||||||
|
ExtendedAssociationField.Selector = Action.Container;
|
||||||
|
ExtendedAssociationField.Viewer = Action.Container;
|
||||||
|
ExtendedAssociationField.InternalSelect = InternalPicker;
|
@ -0,0 +1,104 @@
|
|||||||
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { observer } from '@formily/react';
|
||||||
|
import React, { useContext, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { InternalSubTable } from './InternalSubTable';
|
||||||
|
import { ActionContext, ActionContextProvider, __UNSAFE__, useGetAriaLabelOfPopover } from '@nocobase/client';
|
||||||
|
import { Button, Drawer } from 'antd';
|
||||||
|
const { useAssociationFieldContext, useSetAriaLabelForPopover, ReadPrettyInternalViewer } = __UNSAFE__;
|
||||||
|
|
||||||
|
export const InternaDrawerSubTable = observer(
|
||||||
|
(props) => {
|
||||||
|
const { options } = useAssociationFieldContext();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const ref = useRef();
|
||||||
|
const nesterProps = {
|
||||||
|
...props,
|
||||||
|
shouldMountElement: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleProps = {
|
||||||
|
...props,
|
||||||
|
enableLink: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = useContext(ActionContext);
|
||||||
|
const { getAriaLabel } = useGetAriaLabelOfPopover();
|
||||||
|
|
||||||
|
if (process.env.__E2E__) {
|
||||||
|
useSetAriaLabelForPopover(visible);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
style={{ cursor: 'pointer', display: 'flex' }}
|
||||||
|
onClick={() => {
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
max-width: 95%;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<ReadPrettyInternalViewer {...titleProps} />
|
||||||
|
</div>
|
||||||
|
<EditOutlined style={{ display: 'inline-flex', marginLeft: '5px' }} />
|
||||||
|
</span>
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
aria-label={getAriaLabel('mask')}
|
||||||
|
onClick={() => setVisible(false)}
|
||||||
|
className={css`
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: 9999;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ActionContextProvider value={{ ...ctx, visible, setVisible, openMode: 'drawer' }}>
|
||||||
|
<Drawer
|
||||||
|
title={t(options?.uiSchema?.rawTitle)}
|
||||||
|
open={visible}
|
||||||
|
onClose={() => {
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: '#f3f3f3' }}
|
||||||
|
width={800}
|
||||||
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<div style={{ marginLeft: '90%' }}>
|
||||||
|
<Button type="primary" onClick={() => setVisible(false)}>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{ minWidth: '600px', maxWidth: '800px', maxHeight: '440px', overflow: 'auto' }}
|
||||||
|
className={css`
|
||||||
|
min-width: 600px;
|
||||||
|
max-height: 440px;
|
||||||
|
overflow: auto;
|
||||||
|
.ant-card {
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<InternalSubTable {...nesterProps} />
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</ActionContextProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'InternaDrawerSubTable' },
|
||||||
|
);
|
@ -0,0 +1,69 @@
|
|||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { FormLayout } from '@formily/antd-v5';
|
||||||
|
import { RecursionField, useField, useFieldSchema, observer } from '@formily/react';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useInsertSchema } from '../../components/AssociatedField';
|
||||||
|
import {
|
||||||
|
ACLCollectionProvider,
|
||||||
|
CollectionProvider_deprecated,
|
||||||
|
__UNSAFE__,
|
||||||
|
useACLActionParamsContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
const { useAssociationFieldContext, schema } = __UNSAFE__;
|
||||||
|
|
||||||
|
export const InternalSubTable = observer(
|
||||||
|
() => {
|
||||||
|
const field = useField();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const insertNester = useInsertSchema('SubTable');
|
||||||
|
const { options: collectionField } = useAssociationFieldContext();
|
||||||
|
const showTitle = fieldSchema['x-decorator-props']?.showTitle ?? true;
|
||||||
|
const { actionName } = useACLActionParamsContext();
|
||||||
|
useEffect(() => {
|
||||||
|
insertNester(schema.SubTable);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<CollectionProvider_deprecated name={collectionField.target}>
|
||||||
|
<ACLCollectionProvider actionPath={`${collectionField.target}:${actionName}`}>
|
||||||
|
<FormLayout layout={'vertical'}>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
css`
|
||||||
|
& .ant-formily-item-layout-vertical {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 15px 20px 5px;
|
||||||
|
}
|
||||||
|
.ant-divider-horizontal {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
[css`
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0px 20px 20px 0px;
|
||||||
|
}
|
||||||
|
> .ant-card-bordered {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
`]: showTitle === false,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RecursionField
|
||||||
|
onlyRenderProperties
|
||||||
|
basePath={field.address}
|
||||||
|
schema={fieldSchema}
|
||||||
|
filterProperties={(s) => {
|
||||||
|
return s['x-component'] === 'AssociationField.SubTable';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormLayout>
|
||||||
|
</ACLCollectionProvider>
|
||||||
|
</CollectionProvider_deprecated>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ displayName: 'InternalSubTable' },
|
||||||
|
);
|
@ -0,0 +1,64 @@
|
|||||||
|
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||||
|
import { DatePicker as AntdDatePicker } from 'antd';
|
||||||
|
import type {
|
||||||
|
DatePickerProps as AntdDatePickerProps,
|
||||||
|
RangePickerProps as AntdRangePickerProps,
|
||||||
|
} from 'antd/es/date-picker';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ReadPretty } from './ReadPretty';
|
||||||
|
import { getDateRanges, mapDatePicker, mapRangePicker } from './util';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
interface IDatePickerProps {
|
||||||
|
utc?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComposedDatePicker = React.FC<AntdDatePickerProps> & {
|
||||||
|
ReadPretty?: React.FC<AntdDatePickerProps>;
|
||||||
|
RangePicker?: React.FC<AntdRangePickerProps>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DatePickerContext = React.createContext<IDatePickerProps>({ utc: true });
|
||||||
|
|
||||||
|
export const useDatePickerContext = () => React.useContext(DatePickerContext);
|
||||||
|
export const DatePickerProvider = DatePickerContext.Provider;
|
||||||
|
|
||||||
|
const InternalDatePicker: ComposedDatePicker = connect(
|
||||||
|
AntdDatePicker,
|
||||||
|
mapProps(mapDatePicker()),
|
||||||
|
mapReadPretty(ReadPretty.DatePicker),
|
||||||
|
);
|
||||||
|
|
||||||
|
const InternalRangePicker = connect(
|
||||||
|
AntdDatePicker.RangePicker,
|
||||||
|
mapProps(mapRangePicker()),
|
||||||
|
mapReadPretty(ReadPretty.DateRangePicker),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DatePicker = (props) => {
|
||||||
|
const { utc = true } = useDatePickerContext();
|
||||||
|
const value = Array.isArray(props.value) ? props.value[0] : props.value;
|
||||||
|
props = { utc, ...props };
|
||||||
|
return <InternalDatePicker {...props} value={value} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
DatePicker.ReadPretty = ReadPretty.DatePicker;
|
||||||
|
|
||||||
|
DatePicker.RangePicker = function RangePicker(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { utc = true } = useDatePickerContext();
|
||||||
|
const rangesValue = getDateRanges();
|
||||||
|
const presets = [
|
||||||
|
{ label: t('This year'), value: rangesValue.thisYear },
|
||||||
|
...Array(5)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => ({ label: dayjs().year() - i - 1 + '年', value: rangesValue.year(dayjs().year() - i - 1) })),
|
||||||
|
{ label: t('This month'), value: rangesValue.thisMonth },
|
||||||
|
{ label: t('Last month'), value: rangesValue.lastMonth },
|
||||||
|
];
|
||||||
|
props = { utc, presets, ...props };
|
||||||
|
return <InternalRangePicker {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatePicker;
|
@ -0,0 +1,51 @@
|
|||||||
|
import { usePrefixCls } from '@formily/antd-v5/esm/__builtins__';
|
||||||
|
import { isArr } from '@formily/shared';
|
||||||
|
import { getDefaultFormat, str2moment } from '@nocobase/utils/client';
|
||||||
|
import type {
|
||||||
|
DatePickerProps as AntdDatePickerProps,
|
||||||
|
RangePickerProps as AntdRangePickerProps,
|
||||||
|
} from 'antd/es/date-picker';
|
||||||
|
import cls from 'classnames';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Composed = {
|
||||||
|
DatePicker: React.FC<AntdDatePickerProps>;
|
||||||
|
DateRangePicker: React.FC<AntdRangePickerProps>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReadPretty: Composed = () => null;
|
||||||
|
|
||||||
|
ReadPretty.DatePicker = function DatePicker(props: any) {
|
||||||
|
const prefixCls = usePrefixCls('description-date-picker', props);
|
||||||
|
|
||||||
|
if (!props.value) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLabels = () => {
|
||||||
|
const format = getDefaultFormat(props) as string;
|
||||||
|
const m = str2moment(props.value, props);
|
||||||
|
const labels = dayjs.isDayjs(m) ? m.format(format) : '';
|
||||||
|
return isArr(labels) ? labels.join('~') : labels;
|
||||||
|
};
|
||||||
|
return <div className={cls(prefixCls, props.className)}>{getLabels()}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
ReadPretty.DateRangePicker = function DateRangePicker(props: any) {
|
||||||
|
const prefixCls = usePrefixCls('description-text', props);
|
||||||
|
const format = getDefaultFormat(props);
|
||||||
|
const getLabels = () => {
|
||||||
|
const m = str2moment(props.value, props);
|
||||||
|
if (!m) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const labels = m.map((m) => m.format(format));
|
||||||
|
return isArr(labels) ? labels.join('~') : labels;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={cls(prefixCls, props.className)} style={props.style}>
|
||||||
|
{getLabels()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,252 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen, sleep, userEvent, waitFor } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
import App11 from '../demos/demo11';
|
||||||
|
import App2 from '../demos/demo2';
|
||||||
|
import App3 from '../demos/demo3';
|
||||||
|
import App4 from '../demos/demo4';
|
||||||
|
import App5 from '../demos/demo5';
|
||||||
|
import App6 from '../demos/demo6';
|
||||||
|
import App7 from '../demos/demo7';
|
||||||
|
import App8 from '../demos/demo8';
|
||||||
|
import App9 from '../demos/demo9';
|
||||||
|
|
||||||
|
describe('DatePicker', () => {
|
||||||
|
it('basic', async () => {
|
||||||
|
const { container, getByText } = render(<App1 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.type(input, '2023/05/01 00:00:00');
|
||||||
|
await userEvent.click(getByText('OK'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(input).toHaveValue('2023/05/01 00:00:00');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01 00:00:00', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
|
||||||
|
// TODO: 需要有个方法来固定测试环境的时区
|
||||||
|
if (!process.env.GITHUB_ACTIONS) {
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-04-30T16:00:00.000Z')).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GMT', async () => {
|
||||||
|
const { container, getByText } = render(<App2 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
// 清空默认值
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.type(input, '2023/05/01 00:00:00');
|
||||||
|
await userEvent.click(getByText('OK'));
|
||||||
|
|
||||||
|
expect(input).toHaveValue('2023/05/01 00:00:00');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01 00:00:00', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01T00:00:00.000Z')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-UTC', async () => {
|
||||||
|
const { container } = render(<App3 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.type(input, '2023/05/01');
|
||||||
|
|
||||||
|
let selected;
|
||||||
|
await waitFor(() => {
|
||||||
|
selected = document.querySelector('.ant-picker-cell-selected') as HTMLElement;
|
||||||
|
expect(selected).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
await userEvent.click(selected);
|
||||||
|
|
||||||
|
expect(input).toHaveValue('2023/05/01');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01 00:00:00', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RangePicker', () => {
|
||||||
|
it('GMT', async () => {
|
||||||
|
const { container, getByPlaceholderText } = render(<App4 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const startInput = getByPlaceholderText('Start date');
|
||||||
|
const endInput = getByPlaceholderText('End date');
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-02"]') as HTMLElement);
|
||||||
|
|
||||||
|
await waitFor(() => expect(startInput).toHaveValue('2023-05-01'));
|
||||||
|
await waitFor(() => expect(endInput).toHaveValue('2023-05-02'));
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023-05-01~2023-05-02', { selector: '.ant-description-text' })).toBeInTheDocument();
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01T00:00:00.000Z ~ 2023-05-02T23:59:59.999Z')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-GMT', async () => {
|
||||||
|
const { container, getByPlaceholderText } = render(<App5 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const startInput = getByPlaceholderText('Start date');
|
||||||
|
const endInput = getByPlaceholderText('End date');
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-02"]') as HTMLElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(startInput).toHaveValue('2023-05-01');
|
||||||
|
expect(endInput).toHaveValue('2023-05-02');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023-05-01~2023-05-02', { selector: '.ant-description-text' })).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (!process.env.GITHUB_ACTIONS) {
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText(/2023-04-30t16:00:00\.000z ~ 2023-05-02t15:59:59\.999z/i)).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-UTC', async () => {
|
||||||
|
const { container, getByPlaceholderText } = render(<App6 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const startInput = getByPlaceholderText('Start date');
|
||||||
|
const endInput = getByPlaceholderText('End date');
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await sleep();
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-02"]') as HTMLElement);
|
||||||
|
|
||||||
|
await waitFor(() => expect(startInput).toHaveValue('2023-05-01'));
|
||||||
|
await waitFor(() => expect(endInput).toHaveValue('2023-05-02'));
|
||||||
|
|
||||||
|
// Read pretty
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText('2023-05-01~2023-05-02', { selector: '.ant-description-text' })).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Value
|
||||||
|
await waitFor(() => expect(screen.getByText('2023-05-01 ~ 2023-05-02')).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showTime=false,gmt=true,utc=true', async () => {
|
||||||
|
const { container } = render(<App7 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.type(input, '2023/05/01');
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(input).toHaveValue('2023/05/01');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText('2023-05-01T00:00:00.000Z')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showTime=false,gmt=false,utc=true', async () => {
|
||||||
|
const { container } = render(<App8 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const input = container.querySelector('input') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
await userEvent.type(input, '2023/05/01');
|
||||||
|
await userEvent.click(document.querySelector('[title="2023-05-01"]') as HTMLElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(input).toHaveValue('2023/05/01');
|
||||||
|
// Read pretty
|
||||||
|
expect(screen.getByText('2023/05/01', { selector: '.ant-description-date-picker' })).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (!process.env.GITHUB_ACTIONS) {
|
||||||
|
// Value
|
||||||
|
// 当 gmt 为 false 时是按照客户端本地时区进行计算的,但是这里的测试环境是 UTC+8,所以会有 8 小时的误差
|
||||||
|
expect(screen.getByText('2023-04-30T16:00:00.000Z')).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showTime=false,gmt=true,utc=true & not input', async () => {
|
||||||
|
const currentDateString = new Date().toISOString().split('T')[0];
|
||||||
|
const { container } = render(<App9 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
const btn = document.querySelector(`[title="${currentDateString}"]`);
|
||||||
|
expect(btn).toBeInTheDocument();
|
||||||
|
await userEvent.click(btn as HTMLElement);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Read pretty
|
||||||
|
expect(
|
||||||
|
screen.getByText(currentDateString.replace(/-/g, '/'), { selector: '.ant-description-date-picker' }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Value
|
||||||
|
expect(screen.getByText(`${currentDateString}T00:00:00.000Z`)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// fix T-1506
|
||||||
|
it('shortcut', async () => {
|
||||||
|
const { container } = render(<App11 />);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
const picker = container.querySelector('.ant-picker') as HTMLElement;
|
||||||
|
const startInput = screen.getByPlaceholderText('Start date');
|
||||||
|
const endInput = screen.getByPlaceholderText('End date');
|
||||||
|
|
||||||
|
await userEvent.click(picker);
|
||||||
|
|
||||||
|
// shortcut: Today
|
||||||
|
await userEvent.click(screen.getByText(/today/i));
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
// 因为 Today 快捷键的值是动态生成的,所以这里没有断言具体的值
|
||||||
|
await waitFor(() => expect(startInput.getAttribute('value')).toBeTruthy());
|
||||||
|
await waitFor(() => expect(endInput.getAttribute('value')).toBeTruthy());
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,132 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { getDateRanges } from '../util';
|
||||||
|
|
||||||
|
describe('getDateRanges', () => {
|
||||||
|
const dateRanges = getDateRanges();
|
||||||
|
|
||||||
|
it('today', () => {
|
||||||
|
const [start, end] = dateRanges.today();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().startOf('day').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('day').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('yesterday', () => {
|
||||||
|
const [start, end] = dateRanges.yesterday();
|
||||||
|
expect(dayjs(start).isSame(dayjs().subtract(1, 'day'), 'day')).toBe(true);
|
||||||
|
expect(dayjs(end).isSame(dayjs().subtract(1, 'day'), 'day')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tomorrow', () => {
|
||||||
|
const [start, end] = dateRanges.tomorrow();
|
||||||
|
expect(dayjs(start).isSame(dayjs().add(1, 'day'), 'day')).toBe(true);
|
||||||
|
expect(dayjs(end).isSame(dayjs().add(1, 'day'), 'day')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastWeek', () => {
|
||||||
|
const [start, end] = dateRanges.lastWeek();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(-1, 'week').startOf('isoWeek').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(-1, 'week').endOf('isoWeek').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('thisWeek', () => {
|
||||||
|
const [start, end] = dateRanges.thisWeek();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().startOf('isoWeek').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('isoWeek').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nextWeek', () => {
|
||||||
|
const [start, end] = dateRanges.nextWeek();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(1, 'week').startOf('isoWeek').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(1, 'week').endOf('isoWeek').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastMonth', () => {
|
||||||
|
const [start, end] = dateRanges.lastMonth();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(-1, 'month').startOf('month').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(-1, 'month').endOf('month').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('thisMonth', () => {
|
||||||
|
const [start, end] = dateRanges.thisMonth();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().startOf('month').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('month').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nextMonth', () => {
|
||||||
|
const [start, end] = dateRanges.nextMonth();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(1, 'month').startOf('month').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(1, 'month').endOf('month').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastQuarter', () => {
|
||||||
|
const [start, end] = dateRanges.lastQuarter();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(-1, 'quarter').startOf('quarter').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(-1, 'quarter').endOf('quarter').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('thisQuarter', () => {
|
||||||
|
const [start, end] = dateRanges.thisQuarter();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().startOf('quarter').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('quarter').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nextQuarter', () => {
|
||||||
|
const [start, end] = dateRanges.nextQuarter();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(1, 'quarter').startOf('quarter').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(1, 'quarter').endOf('quarter').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lastYear', () => {
|
||||||
|
const [start, end] = dateRanges.lastYear();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(-1, 'year').startOf('year').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(-1, 'year').endOf('year').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('thisYear', () => {
|
||||||
|
const [start, end] = dateRanges.thisYear();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().startOf('year').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('year').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nextYear', () => {
|
||||||
|
const [start, end] = dateRanges.nextYear();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(1, 'year').startOf('year').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(1, 'year').endOf('year').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('last7Days', () => {
|
||||||
|
const [start, end] = dateRanges.last7Days();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(-6, 'days').startOf('days').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('days').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('next7Days', () => {
|
||||||
|
const [start, end] = dateRanges.next7Days();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(7, 'days').endOf('days').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('last30Days', () => {
|
||||||
|
const [start, end] = dateRanges.last30Days();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(-29, 'days').startOf('days').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('days').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('next30Days', () => {
|
||||||
|
const [start, end] = dateRanges.next30Days();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(30, 'days').endOf('days').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('last90Days', () => {
|
||||||
|
const [start, end] = dateRanges.last90Days();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(-89, 'days').startOf('days').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().endOf('days').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('next90Days', () => {
|
||||||
|
const [start, end] = dateRanges.next90Days();
|
||||||
|
expect(start.toISOString()).toBe(dayjs().add(1, 'day').startOf('day').toISOString());
|
||||||
|
expect(end.toISOString()).toBe(dayjs().add(90, 'days').endOf('days').toISOString());
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,221 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { mapDatePicker } from '../util';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
describe('mapDatePicker', () => {
|
||||||
|
it('showTime is true and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
value: '2022-02-22T22:22:22.000Z',
|
||||||
|
showTime: true,
|
||||||
|
gmt: true,
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 22:22:22');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showTime is true and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
value: '2022-02-22T22:22:22.000Z',
|
||||||
|
showTime: true,
|
||||||
|
gmt: false,
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe(
|
||||||
|
dayjs('2022-02-22T22:22:22.000Z').format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showTime is false and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
value: '2022-02-22T00:00:00.000Z',
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('showTime is false and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
value: '2022-02-22',
|
||||||
|
showTime: false,
|
||||||
|
gmt: false,
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-02-22 00:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when showTime is true and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: true,
|
||||||
|
gmt: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
result.onChange(dayjs.utc('2022-02-22 22:22:22'));
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T22:22:22.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when showTime is true and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: true,
|
||||||
|
gmt: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
const m = dayjs('2022-02-22 22:22:22');
|
||||||
|
result.onChange(m);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when showTime is false and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
result.onChange(dayjs.utc('2022-02-22'));
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith('2022-02-22T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when showTime is false and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: false,
|
||||||
|
gmt: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
const m = dayjs('2022-02-22');
|
||||||
|
result.onChange(m);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(m.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is year and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'year',
|
||||||
|
gmt: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
result.onChange(dayjs.utc('2022-01-01T00:00:00.000Z'));
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is year and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'year',
|
||||||
|
gmt: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
const m = dayjs('2022-02-01 00:00:00');
|
||||||
|
result.onChange(m);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(m.startOf('year').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is month and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'month',
|
||||||
|
gmt: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z'));
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith('2022-02-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is month and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'month',
|
||||||
|
gmt: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
const m = dayjs('2022-02-01 00:00:00');
|
||||||
|
result.onChange(m);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(m.startOf('month').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is quarter and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'quarter',
|
||||||
|
gmt: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z'));
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is quarter and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'quarter',
|
||||||
|
gmt: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
const m = dayjs('2022-02-01 00:00:00');
|
||||||
|
result.onChange(m);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(m.startOf('quarter').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is week and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'week',
|
||||||
|
gmt: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
const m = dayjs.utc('2022-02-21T00:00:00.000Z');
|
||||||
|
result.onChange(m);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is week and gmt is false', () => {
|
||||||
|
const props = {
|
||||||
|
picker: 'week',
|
||||||
|
gmt: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
const m = dayjs('2022-02-21 00:00:00');
|
||||||
|
result.onChange(m);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when utc is false', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: true,
|
||||||
|
gmt: true,
|
||||||
|
utc: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
result.onChange(dayjs('2022-02-22 22:22:22'));
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith('2022-02-22 22:22:22');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onChange with correct value when picker is year and utc is false', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
utc: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
result.onChange(dayjs('2022-01-01 23:00:00'));
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith('2022-01-01');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('utc is false and gmt is true', () => {
|
||||||
|
const props = {
|
||||||
|
value: '2022-01-01 23:00:00',
|
||||||
|
showTime: true,
|
||||||
|
gmt: true,
|
||||||
|
utc: false,
|
||||||
|
};
|
||||||
|
const result = mapDatePicker()(props);
|
||||||
|
expect(result.value.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-01-01 23:00:00');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,43 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { mapRangePicker } from '../util';
|
||||||
|
|
||||||
|
describe('mapRangePicker', () => {
|
||||||
|
it('should work with showTime=false, gmt=true, utc=true', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
utc: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const { onChange } = mapRangePicker()(props);
|
||||||
|
const value = [dayjs.utc('2023-01-01T00:00:00.000Z'), dayjs.utc('2023-01-02T00:00:00.000Z')];
|
||||||
|
onChange(value);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T23:59:59.999Z']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with showTime=true, gmt=true, utc=true', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: true,
|
||||||
|
gmt: true,
|
||||||
|
utc: true,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const { onChange } = mapRangePicker()(props);
|
||||||
|
const value = [dayjs.utc('2023-01-01T00:00:00.000Z'), dayjs.utc('2023-01-02T00:00:00.000Z')];
|
||||||
|
onChange(value);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01T00:00:00.000Z', '2023-01-02T00:00:00.000Z']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with showTime=false, gmt=true, utc=false', () => {
|
||||||
|
const props = {
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
utc: false,
|
||||||
|
onChange: vi.fn(),
|
||||||
|
};
|
||||||
|
const { onChange } = mapRangePicker()(props);
|
||||||
|
const value = [dayjs.utc('2023-01-01T00:00:00.000Z'), dayjs.utc('2023-01-02T00:00:00.000Z')];
|
||||||
|
onChange(value);
|
||||||
|
expect(props.onChange).toHaveBeenCalledWith(['2023-01-01', '2023-01-02']);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,140 @@
|
|||||||
|
import { str2moment } from '@nocobase/utils/client';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { moment2str } from '../util';
|
||||||
|
|
||||||
|
describe('str2moment', () => {
|
||||||
|
describe('string value', () => {
|
||||||
|
test('gmt date', async () => {
|
||||||
|
const m = str2moment('2022-06-21T00:00:00.000Z', { gmt: true });
|
||||||
|
expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-21 00:00:00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local date', async () => {
|
||||||
|
const m = str2moment('2022-06-21T00:00:00.000Z');
|
||||||
|
expect(m.toISOString()).toBe('2022-06-21T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value is null', async () => {
|
||||||
|
const m = str2moment(null);
|
||||||
|
expect(m).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is month', async () => {
|
||||||
|
const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month' });
|
||||||
|
expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-01 00:00:00');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('array value', () => {
|
||||||
|
test('gmt date', async () => {
|
||||||
|
const arr = str2moment(['2022-06-21T00:00:00.000Z', '2022-06-21T00:00:00.000Z'], { gmt: true });
|
||||||
|
for (const m of arr) {
|
||||||
|
expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-21 00:00:00');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('local date', async () => {
|
||||||
|
const arr = str2moment(['2022-06-21T00:00:00.000Z', '2022-06-21T00:00:00.000Z']);
|
||||||
|
for (const m of arr) {
|
||||||
|
expect(m.toISOString()).toBe('2022-06-21T00:00:00.000Z');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moment2str', () => {
|
||||||
|
test('gmt date', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { showTime: true, gmt: true });
|
||||||
|
expect(str).toBe('2023-06-21T10:10:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('showTime is true, gmt is false', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { showTime: true, gmt: false });
|
||||||
|
expect(str).toBe(m.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gmt is true', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { gmt: true });
|
||||||
|
expect(str).toBe('2023-06-21T10:10:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gmt is false', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { gmt: false });
|
||||||
|
expect(str).toBe(dayjs('2023-06-21 10:10:00').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with time', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { showTime: true });
|
||||||
|
expect(str).toBe(m.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is year, gmt is false', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'year', gmt: false });
|
||||||
|
expect(str).toBe(dayjs('2023-01-01 00:00:00').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is year, gmt is true', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'year', gmt: true });
|
||||||
|
expect(str).toBe('2023-01-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is year', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'year' });
|
||||||
|
expect(str).toBe('2023-01-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is quarter, gmt is false', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'quarter', gmt: false });
|
||||||
|
expect(str).toBe(dayjs('2023-04-01 00:00:00').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is quarter, gmt is true', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'quarter', gmt: true });
|
||||||
|
expect(str).toBe('2023-04-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is month, gmt is false', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'month', gmt: false });
|
||||||
|
expect(str).toBe(dayjs('2023-06-01 00:00:00').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is month, gmt is true', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'month', gmt: true });
|
||||||
|
expect(str).toBe('2023-06-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is month', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'month' });
|
||||||
|
expect(str).toBe('2023-06-01T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is week, gmt is false', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'week', gmt: false });
|
||||||
|
expect(str).toBe(dayjs('2023-06-19 00:00:00').toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker is week, gmt is true', () => {
|
||||||
|
const m = dayjs('2023-06-21 10:10:00');
|
||||||
|
const str = moment2str(m, { picker: 'week', gmt: true });
|
||||||
|
expect(str).toBe('2023-06-19T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value is null', async () => {
|
||||||
|
const m = moment2str(null);
|
||||||
|
expect(m).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: true,
|
||||||
|
},
|
||||||
|
'x-reactions': {
|
||||||
|
target: '*(read1,read2)',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: false,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
'x-reactions': {
|
||||||
|
target: '*(read1,read2)',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: false,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker.RangePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
gmt: true,
|
||||||
|
},
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
target: 'read1',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'read2',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
gmt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker (GMT)
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: true,
|
||||||
|
gmt: true,
|
||||||
|
},
|
||||||
|
default: '2022-06-04T15:00:00.000Z',
|
||||||
|
'x-reactions': {
|
||||||
|
target: '*(read1,read2)',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: true,
|
||||||
|
gmt: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
utc: false,
|
||||||
|
},
|
||||||
|
'x-reactions': {
|
||||||
|
target: '*(read1,read2)',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker.RangePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
gmt: true,
|
||||||
|
defaultPickerValue: [dayjs('2023-05-01')],
|
||||||
|
},
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
target: 'read1',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'read2',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
gmt: true,
|
||||||
|
defaultPickerValue: [dayjs('2023-05-01')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker.RangePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
gmt: false,
|
||||||
|
defaultPickerValue: [dayjs('2023-05-01')],
|
||||||
|
},
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
target: 'read1',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'read2',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
defaultPickerValue: [dayjs('2023-05-01')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker.RangePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
utc: false,
|
||||||
|
defaultPickerValue: [dayjs('2023-05-01')],
|
||||||
|
},
|
||||||
|
'x-reactions': [
|
||||||
|
{
|
||||||
|
target: 'read1',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'read2',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value && $self.value.join(" ~ ")}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker.RangePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
defaultPickerValue: [dayjs('2023-05-01')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
'x-reactions': {
|
||||||
|
target: '*(read1,read2)',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: false,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
'x-reactions': {
|
||||||
|
target: '*(read1,read2)',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: false,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* title: DatePicker
|
||||||
|
*/
|
||||||
|
import { FormItem } from '@formily/antd-v5';
|
||||||
|
import { DatePicker, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
input: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Editable`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
'x-reactions': {
|
||||||
|
target: '*(read1,read2)',
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$self.value}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read1: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `Read pretty`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {
|
||||||
|
dateFormat: 'YYYY/MM/DD',
|
||||||
|
showTime: false,
|
||||||
|
gmt: true,
|
||||||
|
utc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
read2: {
|
||||||
|
type: 'string',
|
||||||
|
title: `Value`,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<SchemaComponentProvider components={{ Input, DatePicker, FormItem }}>
|
||||||
|
<SchemaComponent schema={schema} />
|
||||||
|
</SchemaComponentProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
group:
|
||||||
|
title: Schema Components
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# DatePicker
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx"></code>
|
||||||
|
|
||||||
|
### DatePicker (GMT)
|
||||||
|
|
||||||
|
<code src="./demos/demo2.tsx"></code>
|
||||||
|
|
||||||
|
### DatePicker (showTime=false,gmt=true,utc=true)
|
||||||
|
|
||||||
|
<code src="./demos/demo7.tsx"></code>
|
||||||
|
|
||||||
|
|
||||||
|
### DatePicker (showTime=false,gmt=false,utc=true)
|
||||||
|
|
||||||
|
<code src="./demos/demo8.tsx"></code>
|
||||||
|
|
||||||
|
|
||||||
|
### DatePicker (non-UTC)
|
||||||
|
|
||||||
|
<code src="./demos/demo3.tsx"></code>
|
||||||
|
|
||||||
|
### RangePicker (GMT)
|
||||||
|
|
||||||
|
<code src="./demos/demo4.tsx"></code>
|
||||||
|
|
||||||
|
### RangePicker (non-GMT)
|
||||||
|
|
||||||
|
<code src="./demos/demo5.tsx"></code>
|
||||||
|
|
||||||
|
### RangePicker (non-UTC)
|
||||||
|
|
||||||
|
<code src="./demos/demo6.tsx"></code>
|
||||||
|
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
基于 antd 的 [DatePicker](https://ant.design/components/date-picker/#API),新增了以下扩展属性,用于支持 NocoBase 的日期字段设置。
|
||||||
|
|
||||||
|
- `dateFormat` 设置日期格式
|
||||||
|
- `timeFormat` 设置时间格式
|
@ -0,0 +1 @@
|
|||||||
|
export * from './DatePicker';
|
@ -0,0 +1,168 @@
|
|||||||
|
import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
|
||||||
|
if (!dayjs.isDayjs(value)) return value;
|
||||||
|
if (timezone === 'local') {
|
||||||
|
const offset = new Date().getTimezoneOffset();
|
||||||
|
return dayjs(toStringByPicker(value, picker, 'gmt'))
|
||||||
|
.add(offset, 'minutes')
|
||||||
|
.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (picker === 'year') {
|
||||||
|
return value.format('YYYY') + '-01-01T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
if (picker === 'month') {
|
||||||
|
return value.format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
if (picker === 'quarter') {
|
||||||
|
return value.startOf('quarter').format('YYYY-MM') + '-01T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
if (picker === 'week') {
|
||||||
|
return value.startOf('week').add(1, 'day').format('YYYY-MM-DD') + 'T00:00:00.000Z';
|
||||||
|
}
|
||||||
|
return value.format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
||||||
|
};
|
||||||
|
|
||||||
|
const toGmtByPicker = (value: Dayjs, picker?: any) => {
|
||||||
|
if (!value || !dayjs.isDayjs(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return toStringByPicker(value, picker, 'gmt');
|
||||||
|
};
|
||||||
|
|
||||||
|
const toLocalByPicker = (value: Dayjs, picker?: any) => {
|
||||||
|
if (!value || !dayjs.isDayjs(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return toStringByPicker(value, picker, 'local');
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Moment2strOptions {
|
||||||
|
showTime?: boolean;
|
||||||
|
gmt?: boolean;
|
||||||
|
utc?: boolean;
|
||||||
|
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => {
|
||||||
|
const { showTime, gmt, picker, utc = true } = options;
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (!utc) {
|
||||||
|
const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
||||||
|
return value.format(format);
|
||||||
|
}
|
||||||
|
if (showTime) {
|
||||||
|
return gmt ? toGmt(value) : toLocal(value);
|
||||||
|
}
|
||||||
|
if (typeof gmt === 'boolean') {
|
||||||
|
return gmt ? toGmtByPicker(value, picker) : toLocalByPicker(value, picker);
|
||||||
|
}
|
||||||
|
return toGmtByPicker(value, picker);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapDatePicker = function () {
|
||||||
|
return (props: any) => {
|
||||||
|
const format = getDefaultFormat(props) as any;
|
||||||
|
const onChange = props.onChange;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
format: format,
|
||||||
|
value: str2moment(props.value, props),
|
||||||
|
onChange: (value: Dayjs | null) => {
|
||||||
|
if (onChange) {
|
||||||
|
if (!props.showTime && value) {
|
||||||
|
value = value.startOf('day');
|
||||||
|
}
|
||||||
|
onChange(moment2str(value, props));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapRangePicker = function () {
|
||||||
|
return (props: any) => {
|
||||||
|
const format = getDefaultFormat(props) as any;
|
||||||
|
const onChange = props.onChange;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
format: format,
|
||||||
|
value: str2moment(props.value, props),
|
||||||
|
onChange: (value: Dayjs[]) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(
|
||||||
|
value
|
||||||
|
? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRangeStart(value: Dayjs, options: Moment2strOptions) {
|
||||||
|
const { showTime } = options;
|
||||||
|
if (showTime) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.startOf('day');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRangeEnd(value: Dayjs, options: Moment2strOptions) {
|
||||||
|
const { showTime } = options;
|
||||||
|
if (showTime) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.endOf('day');
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStart = (offset: any, unit: any) => {
|
||||||
|
return dayjs()
|
||||||
|
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||||
|
.startOf(unit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEnd = (offset: any, unit: any) => {
|
||||||
|
return dayjs()
|
||||||
|
.add(offset, unit === 'isoWeek' ? 'week' : unit)
|
||||||
|
.endOf(unit);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDateRanges = () => {
|
||||||
|
return {
|
||||||
|
now: () => dayjs().toISOString(),
|
||||||
|
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
|
||||||
|
yesterday: () => [getStart(-1, 'day'), getEnd(-1, 'day')],
|
||||||
|
tomorrow: () => [getStart(1, 'day'), getEnd(1, 'day')],
|
||||||
|
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
|
||||||
|
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
|
||||||
|
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
|
||||||
|
year: (year: number) => [getStart(year - dayjs().year(), 'year'), getEnd(year - dayjs().year(), 'year')],
|
||||||
|
thisIsoWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
|
||||||
|
lastIsoWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
|
||||||
|
nextIsoWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
|
||||||
|
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
|
||||||
|
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
|
||||||
|
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
|
||||||
|
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
|
||||||
|
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
|
||||||
|
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
|
||||||
|
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
|
||||||
|
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
|
||||||
|
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
|
||||||
|
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
|
||||||
|
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],
|
||||||
|
last30Days: () => [getStart(-29, 'days'), getEnd(0, 'days')],
|
||||||
|
next30Days: () => [getStart(1, 'day'), getEnd(30, 'days')],
|
||||||
|
last90Days: () => [getStart(-89, 'days'), getEnd(0, 'days')],
|
||||||
|
next90Days: () => [getStart(1, 'day'), getEnd(90, 'days')],
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
import { Button, Result, Typography } from 'antd';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { FallbackProps, useErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const { Paragraph, Text, Link } = Typography;
|
||||||
|
|
||||||
|
export const ErrorFallback: FC<FallbackProps> = ({ error }) => {
|
||||||
|
const { resetBoundary } = useErrorBoundary();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const subTitle = (
|
||||||
|
<Trans>
|
||||||
|
{'This is likely a NocoBase internals bug. Please open an issue at '}
|
||||||
|
<Link href="https://github.com/nocobase/nocobase/issues" target="_blank">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
</Trans>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ backgroundColor: 'white' }}>
|
||||||
|
<Result
|
||||||
|
style={{ maxWidth: '60vw', margin: 'auto' }}
|
||||||
|
status="error"
|
||||||
|
title={t('Render Failed')}
|
||||||
|
subTitle={subTitle}
|
||||||
|
extra={[
|
||||||
|
<Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank">
|
||||||
|
{t('Feedback')}
|
||||||
|
</Button>,
|
||||||
|
<Button key="try" onClick={resetBoundary}>
|
||||||
|
{t('Try again')}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Paragraph copyable>
|
||||||
|
<Text type="danger" style={{ whiteSpace: 'pre-line', textAlign: 'center' }}>
|
||||||
|
{error.stack}
|
||||||
|
</Text>
|
||||||
|
</Paragraph>
|
||||||
|
</Result>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from 'testUtils';
|
||||||
|
import App1 from '../demos/demo1';
|
||||||
|
|
||||||
|
describe('ErrorFallback', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
render(<App1 />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/render failed/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/this is likely a nocobase internals bug\. please open an issue at/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('link', { name: /feedback/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/try again/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/error: error message/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// 底部复制按钮
|
||||||
|
expect(document.querySelector('.ant-typography-copy')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { ErrorFallback } from '../ErrorFallback';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
throw new Error('error message');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback} onError={console.error}>
|
||||||
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
group:
|
||||||
|
title: Schema Components
|
||||||
|
---
|
||||||
|
|
||||||
|
# ErrorFallback
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
<code src="./demos/demo1.tsx"></code>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user