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