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