diff --git a/packages/app/client/src/plugins/snapshot-field.ts b/packages/app/client/src/plugins/snapshot-field.ts new file mode 100644 index 000000000..2ec089416 --- /dev/null +++ b/packages/app/client/src/plugins/snapshot-field.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-snapshot-field/client'; \ No newline at end of file diff --git a/packages/core/client/src/api-client/index.tsx b/packages/core/client/src/api-client/index.tsx index f00e1fdad..5188e9add 100644 --- a/packages/core/client/src/api-client/index.tsx +++ b/packages/core/client/src/api-client/index.tsx @@ -1,3 +1,4 @@ export * from './hooks'; export * from './APIClient'; export * from './APIClientProvider'; +export * from './context'; diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index 4b6bf0c09..28e53fc10 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -8,19 +8,22 @@ import React, { createContext, useContext } from 'react'; import { Link } from 'react-router-dom'; import { ACLCollectionProvider, + TableFieldContext, TableFieldResource, + useActionContext, useAPIClient, useDesignable, useRecord, + useTableFieldContext, WithoutTableFieldResource, } from '../'; -import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager'; +import { CollectionProvider, useCollection, useCollectionField, useCollectionManager } from '../collection-manager'; import { useRecordIndex } from '../record-provider'; import { SharedFilterProvider } from './SharedFilterProvider'; export const BlockResourceContext = createContext(null); export const BlockAssociationContext = createContext(null); -const BlockRequestContext = createContext(null); +export const BlockRequestContext = createContext(null); export const useBlockResource = () => { return useContext(BlockResourceContext); @@ -86,26 +89,36 @@ const useActionParams = (props) => { }; export const useResourceAction = (props, opts = {}) => { - const { resource, action } = props; + /** + * fieldName: 来自 TableFieldProvider + */ + const { resource, action, fieldName: tableFieldName } = props; const { fields } = useCollection(); - const appends = fields?.filter((field) => field.target).map((field) => field.name); + const appends = fields?.filter((field) => field.target && field.interface !== 'snapshot').map((field) => field.name); const params = useActionParams(props); const api = useAPIClient(); const fieldSchema = useFieldSchema(); + const { snapshot } = useActionContext(); + const record = useRecord(); + if (!Object.keys(params).includes('appends') && appends?.length) { params['appends'] = appends; } const result = useRequest( - (opts) => { - if (!action) { - return Promise.resolve({}); - } - const actionParams = { ...opts }; - if (params.appends) { - actionParams.appends = params.appends; - } - return resource[action](actionParams).then((res) => res.data); - }, + snapshot + ? async () => ({ + data: record[tableFieldName] ?? [], + }) + : (opts) => { + if (!action) { + return Promise.resolve({}); + } + const actionParams = { ...opts }; + if (params.appends) { + actionParams.appends = params.appends; + } + return resource[action](actionParams).then((res) => res.data); + }, { ...opts, onSuccess(data, params) { @@ -121,7 +134,7 @@ export const useResourceAction = (props, opts = {}) => { return result; }; -const MaybeCollectionProvider = (props) => { +export const MaybeCollectionProvider = (props) => { const { collection } = props; return collection ? ( diff --git a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx index 2b2970d75..4234560d5 100644 --- a/packages/core/client/src/block-provider/KanbanBlockProvider.tsx +++ b/packages/core/client/src/block-provider/KanbanBlockProvider.tsx @@ -75,7 +75,7 @@ const useAssociationNames = (collection) => { const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { - if (collectionField.target) { + if (collectionField.target && collectionField.interface !== 'snapshot') { associationFields.add(collectionField.name); const fields = getCollectionFields(collectionField.target); for (const field of fields) { @@ -104,7 +104,6 @@ const useAssociationNames = (collection) => { export const KanbanBlockProvider = (props) => { const params = { ...props.params }; const appends = useAssociationNames(props.collection); - console.log('KanbanBlockProvider', appends); if (!Object.keys(params).includes('appends')) { params['appends'] = appends; } diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index d14c2306b..002896834 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -39,7 +39,7 @@ export const useAssociationNames = (collection) => { const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { - if (collectionField.target) { + if (collectionField.target && collectionField.interface !== 'snapshot') { associationFields.add(collectionField.name); const fields = getCollectionFields(collectionField.target); for (const field of fields) { diff --git a/packages/core/client/src/block-provider/TableFieldProvider.tsx b/packages/core/client/src/block-provider/TableFieldProvider.tsx index 410fbb71a..bcabbc38b 100644 --- a/packages/core/client/src/block-provider/TableFieldProvider.tsx +++ b/packages/core/client/src/block-provider/TableFieldProvider.tsx @@ -2,6 +2,7 @@ import { ArrayField, Field } from '@formily/core'; import { useField, useFieldSchema } from '@formily/react'; import React, { createContext, useContext, useEffect } from 'react'; import { APIClient } from '../api-client'; +import { useCollectionField } from '../collection-manager'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { useFormBlockContext } from './FormBlockProvider'; import { useFormFieldContext } from './FormFieldProvider'; diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index 1dd28e5f7..159b91bbe 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -37,7 +37,7 @@ const InternalTableSelectorProvider = (props) => { const useAssociationNames2 = (collection) => { const { getCollectionFields } = useCollectionManager(); const names = getCollectionFields(collection) - ?.filter((field) => field.target) + ?.filter((field) => field.target && field.interface !== 'snapshot') .map((field) => field.name); return names; }; @@ -55,7 +55,7 @@ const useAssociationNames = (collection) => { const collectionFields = getCollectionFields(collection); const associationFields = new Set(); for (const collectionField of collectionFields) { - if (collectionField.target) { + if (collectionField.target && collectionField.interface !== 'snapshot') { associationFields.add(collectionField.name); const fields = getCollectionFields(collectionField.target); for (const field of fields) { diff --git a/packages/core/client/src/collection-manager/CollectionField.tsx b/packages/core/client/src/collection-manager/CollectionField.tsx index ad6cfd43c..d353847b1 100644 --- a/packages/core/client/src/collection-manager/CollectionField.tsx +++ b/packages/core/client/src/collection-manager/CollectionField.tsx @@ -3,7 +3,7 @@ import { connect, useField, useFieldSchema } from '@formily/react'; import { merge } from '@formily/shared'; import { concat } from 'lodash'; import React, { useEffect } from 'react'; -import { useCompile, useComponent, useFormBlockContext } from '..'; +import { useActionContext, useCompile, useComponent, useFormBlockContext, useRecord } from '..'; import { CollectionFieldProvider } from './CollectionFieldProvider'; import { useCollectionField } from './hooks'; @@ -77,11 +77,43 @@ const InternalField: React.FC = (props) => { return React.createElement(component, props, props.children); }; +export const InternalFallbackField = () => { + const { uiSchema } = useCollectionField(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const record = useRecord(); + + const displayKey = fieldSchema['x-component-props']?.fieldNames?.label ?? 'id'; + + const value = record[fieldSchema.name]; + + useEffect(() => { + field.title = fieldSchema.title ?? fieldSchema.name; + }, [uiSchema?.title]); + + let displayText = value; + + if (Array.isArray(value) || typeof value === 'object') { + displayText = [] + .concat(value) + .map((i) => i[displayKey]) + .join(', '); + } + + return
{displayText}
; +}; + export const CollectionField = connect((props) => { const fieldSchema = useFieldSchema(); const field = fieldSchema?.['x-component-props']?.['field']; + const { snapshot } = useActionContext(); + return ( - + : null} + > ); diff --git a/packages/core/client/src/collection-manager/CollectionFieldProvider.tsx b/packages/core/client/src/collection-manager/CollectionFieldProvider.tsx index cbcca66b9..d5e800331 100644 --- a/packages/core/client/src/collection-manager/CollectionFieldProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionFieldProvider.tsx @@ -4,18 +4,18 @@ import { CollectionFieldContext } from './context'; import { useCollection, useCollectionManager } from './hooks'; import { CollectionFieldOptions } from './types'; -export const CollectionFieldProvider: React.FC<{ name?: SchemaKey; field?: CollectionFieldOptions }> = (props) => { - const { name, field, children } = props; +export const CollectionFieldProvider: React.FC<{ + name?: SchemaKey; + field?: CollectionFieldOptions; + fallback?: React.ReactElement; +}> = (props) => { + const { name, field, children, fallback } = props; const fieldSchema = useFieldSchema(); const { getField } = useCollection(); const { getCollectionJoinField } = useCollectionManager(); const value = field || getField(field?.name || name) || getCollectionJoinField(fieldSchema?.['x-collection-field']); if (!value) { - return null; + return fallback; } - return ( - - {children} - - ); + return {children}; }; diff --git a/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx b/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx new file mode 100644 index 000000000..01ff74e36 --- /dev/null +++ b/packages/core/client/src/collection-manager/CollectionHistoryProvider.tsx @@ -0,0 +1,63 @@ +import { Spin } from 'antd'; +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { APIClientContext, useRequest } from '../api-client'; + +export interface CollectionHistoryContextValue { + historyCollections: any[]; + refreshCH: () => Promise; +} + +const CollectionHistoryContext = createContext({ + historyCollections: [], + refreshCH: () => undefined, +}); + +export const CollectionHistoryProvider: React.FC = (props) => { + const api = useContext(APIClientContext); + + const options = { + resource: 'collectionsHistory', + action: 'list', + params: { + paginate: false, + appends: ['fields', 'fields.uiSchema'], + filter: { + // inherit: false, + }, + sort: ['sort'], + }, + }; + + const service = useRequest(options); + + // 刷新 collecionHistory + const refreshCH = async () => { + const { data } = await api.request(options); + service.mutate(data); + return data?.data || []; + }; + + if (service.loading) { + return ; + } + + return ( + + {props.children} + + ); +}; + +export const useHistoryCollectionsByNames = (collectionNames: string[]) => { + const { historyCollections } = useContext(CollectionHistoryContext); + return historyCollections.filter((i) => collectionNames.includes(i.name)); +}; + +export const useCollectionHistory = () => { + return useContext(CollectionHistoryContext); +}; diff --git a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx index 213d2ac2e..a5c3987ca 100644 --- a/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx +++ b/packages/core/client/src/collection-manager/CollectionManagerProvider.tsx @@ -4,9 +4,10 @@ import { keyBy } from 'lodash'; import { useAPIClient, useRequest } from '../api-client'; import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider'; import { CollectionManagerContext } from './context'; -import * as defaultInterfaces from './interfaces'; import { CollectionManagerOptions } from './types'; import { templateOptions } from '../collection-manager/Configuration/templates'; +import * as defaultInterfaces from './interfaces'; +import { useCollectionHistory } from './CollectionHistoryProvider'; export const CollectionManagerProvider: React.FC = (props) => { const { service, interfaces, collections = [], refreshCM, templates } = props; @@ -31,6 +32,7 @@ export const CollectionManagerProvider: React.FC = (pr export const RemoteCollectionManagerProvider = (props: any) => { const api = useAPIClient(); const [contentLoading, setContentLoading] = useState(false); + const { refreshCH } = useCollectionHistory(); const options = { resource: 'collections', action: 'list', @@ -47,21 +49,25 @@ export const RemoteCollectionManagerProvider = (props: any) => { if (service.loading) { return ; } + + const refreshCM = async (opts) => { + if (opts?.reload) { + setContentLoading(true); + } + const { data } = await api.request(options); + service.mutate(data); + await refreshCH(); + if (opts?.reload) { + setContentLoading(false); + } + return data?.data || []; + }; + return ( { - if (opts?.reload) { - setContentLoading(true); - } - const { data } = await api.request(options); - service.mutate(data); - if (opts?.reload) { - setContentLoading(false); - } - return data?.data || []; - }} + refreshCM={refreshCM} {...props} /> ); diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 792d0d714..c55caff63 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -11,6 +11,7 @@ import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useCancelAction } from '../action-hooks'; import { useCollectionManager } from '../hooks'; +import { useOptions } from '../hooks/useOptions'; import { IField } from '../interfaces/types'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import * as components from './components'; @@ -148,6 +149,7 @@ export const AddFieldAction = (props) => { const [schema, setSchema] = useState({}); const compile = useCompile(); const { t } = useTranslation(); + const options = useOptions(); const getFieldOptions = () => { const { availableFieldInterfaces } = getTemplate(record.template) || {}; const { exclude, include } = availableFieldInterfaces || {}; diff --git a/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx index 798ec1041..909db409a 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddSubFieldAction.tsx @@ -10,9 +10,9 @@ import { useRequest } from '../../api-client'; import { RecordProvider } from '../../record-provider'; import { ActionContext, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useCollectionManager } from '../hooks'; +import { useOptions } from '../hooks/useOptions'; import { IField } from '../interfaces/types'; import * as components from './components'; -import { options } from './interfaces'; const getSchema = (schema: IField): ISchema => { if (!schema) { @@ -99,6 +99,7 @@ export const AddSubFieldAction = () => { const [visible, setVisible] = useState(false); const [schema, setSchema] = useState({}); const compile = useCompile(); + const options = useOptions(); const { t } = useTranslation(); const items = options.map((option) => { const children = option.children.map((child) => { diff --git a/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx b/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx index d8ea4faa6..f0210c0a2 100644 --- a/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx +++ b/packages/core/client/src/collection-manager/Configuration/ConfigurationTable.tsx @@ -72,7 +72,7 @@ const useNewId = (prefix) => { }; export const ConfigurationTable = () => { - const { collections = [] } = useCollectionManager(); + const { collections = [], interfaces } = useCollectionManager(); const { data: { database }, } = useCurrentAppInfo(); @@ -114,6 +114,7 @@ export const ConfigurationTable = () => { useCurrentFields, useNewId, useCancelAction, + interfaces, enableInherits: database?.dialect === 'postgres', }} /> diff --git a/packages/core/client/src/collection-manager/Configuration/index.tsx b/packages/core/client/src/collection-manager/Configuration/index.tsx index 5adfe6afa..9eae73586 100644 --- a/packages/core/client/src/collection-manager/Configuration/index.tsx +++ b/packages/core/client/src/collection-manager/Configuration/index.tsx @@ -3,7 +3,6 @@ import { registerValidateFormats } from '@formily/core'; export * from './AddFieldAction'; export * from './ConfigurationTable'; export * from './EditFieldAction'; -export * from './interfaces'; export * from './components'; export * from './templates'; export * from './CollectionFieldsTable'; diff --git a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts index fb1daa7a2..4ab4bef1f 100644 --- a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts +++ b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts @@ -1,7 +1,6 @@ import { ISchema } from '@formily/react'; import { CollectionOptions } from '../../types'; import { CollectionFieldInterface } from '../components/CollectionFieldInterface'; -import { options } from '../interfaces'; const collection: CollectionOptions = { name: 'fields', @@ -31,7 +30,7 @@ const collection: CollectionOptions = { title: '{{ t("Field interface") }}', type: 'string', 'x-component': 'Select', - enum: options as any, + enum: '{{interfaces}}', }, }, { diff --git a/packages/core/client/src/collection-manager/hooks/useOptions.ts b/packages/core/client/src/collection-manager/hooks/useOptions.ts new file mode 100644 index 000000000..d737a9602 --- /dev/null +++ b/packages/core/client/src/collection-manager/hooks/useOptions.ts @@ -0,0 +1,45 @@ +import set from 'lodash/set'; +import { useCollectionManager } from './useCollectionManager'; + +export const useOptions = () => { + const { interfaces } = useCollectionManager(); + + const fields = {}; + + Object.keys(interfaces).forEach((type) => { + const schema = interfaces[type]; + registerField(schema.group || 'others', type, { order: 0, ...schema }); + }); + + function registerField(group: string, type: string, schema) { + fields[group] = fields[group] || {}; + set(fields, [group, type], schema); + } + + const groupLabels = { + basic: '{{t("Basic")}}', + choices: '{{t("Choices")}}', + media: '{{t("Media")}}', + datetime: '{{t("Date & Time")}}', + relation: '{{t("Relation")}}', + advanced: '{{t("Advanced type")}}', + systemInfo: '{{t("System info")}}', + others: '{{t("Others")}}', + }; + + return Object.keys(groupLabels).map((groupName) => ({ + label: groupLabels[groupName], + key: groupName, + children: Object.keys(fields[groupName] || {}) + .map((type) => { + const field = fields[groupName][type]; + return { + value: type, + label: field.title, + name: type, + ...fields[groupName][type], + }; + }) + .sort((a, b) => a.order - b.order), + })); +}; diff --git a/packages/core/client/src/collection-manager/index.tsx b/packages/core/client/src/collection-manager/index.tsx index 46802d805..d227b5152 100644 --- a/packages/core/client/src/collection-manager/index.tsx +++ b/packages/core/client/src/collection-manager/index.tsx @@ -15,3 +15,4 @@ export * from './ResourceActionProvider'; export { getConfigurableProperties } from './templates/properties'; export * from './templates/types'; export * from './types'; +export * from './CollectionHistoryProvider'; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index 00a2ed39a..338836592 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -29,6 +29,7 @@ export const type: ISchema = { { label: 'One to many', value: 'hasMany' }, { label: 'Many to one', value: 'belongsTo' }, { label: 'Many to many', value: 'belongsToMany' }, + { label: 'Snapshot', value: 'snapshot' }, ], }; diff --git a/packages/core/client/src/collection-manager/sub-table.tsx b/packages/core/client/src/collection-manager/sub-table.tsx index f5bec98d2..e78fc8329 100644 --- a/packages/core/client/src/collection-manager/sub-table.tsx +++ b/packages/core/client/src/collection-manager/sub-table.tsx @@ -8,10 +8,9 @@ import { useCollectionManager, useRecord, useRecordIndex, - useRequest + useRequest, } from '../'; import { useAPIClient } from '../api-client'; -import { options } from './Configuration/interfaces'; const collection: CollectionOptions = { name: 'fields', @@ -42,7 +41,7 @@ const collection: CollectionOptions = { title: '{{ t("Field interface") }}', type: 'string', 'x-component': 'Select', - enum: options as any, + enum: '{{interfaces}}', }, }, { diff --git a/packages/core/client/src/schema-component/antd/action/context.tsx b/packages/core/client/src/schema-component/antd/action/context.tsx index b682e9b82..a49feefd3 100644 --- a/packages/core/client/src/schema-component/antd/action/context.tsx +++ b/packages/core/client/src/schema-component/antd/action/context.tsx @@ -9,6 +9,7 @@ export interface ActionContextProps { visible?: boolean; setVisible?: (v: boolean) => void; openMode?: 'drawer' | 'modal' | 'page'; + snapshot?: boolean; openSize?: OpenSize; containerRefKey?: string; formValueChanged?: boolean; diff --git a/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts index 0e00c9164..217eade69 100644 --- a/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts +++ b/packages/core/client/src/schema-component/antd/association-select/useServiceOptions.ts @@ -1,9 +1,9 @@ -import { useFieldSchema } from "@formily/react"; -import { useMemo } from "react"; -import { useCollection } from "../../../collection-manager"; +import { useFieldSchema } from '@formily/react'; +import { useMemo } from 'react'; +import { useCollection } from '../../../collection-manager'; export default function useServiceOptions(props) { - const { action = 'list', service } = props + const { action = 'list', service } = props; const fieldSchema = useFieldSchema(); const { getField } = useCollection(); const collectionField = useMemo(() => { @@ -12,9 +12,9 @@ export default function useServiceOptions(props) { return useMemo(() => { return { - resource: collectionField.target, + resource: collectionField?.target, action, ...service, }; - }, [collectionField.target, action, service]); + }, [collectionField?.target, action, service]); } diff --git a/packages/core/client/src/schema-component/antd/input/index.ts b/packages/core/client/src/schema-component/antd/input/index.ts index 522be2339..987778bc4 100644 --- a/packages/core/client/src/schema-component/antd/input/index.ts +++ b/packages/core/client/src/schema-component/antd/input/index.ts @@ -1,3 +1,4 @@ export * from './Input'; export * from './ReadPretty'; export * from './Json'; +export * from './EllipsisWithTooltip'; diff --git a/packages/core/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx b/packages/core/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx index 660a3afd3..b1972cd3d 100644 --- a/packages/core/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx +++ b/packages/core/client/src/schema-component/antd/record-picker/ReadPrettyRecordPicker.tsx @@ -7,7 +7,7 @@ import { CollectionProvider, useCollection, useCollectionManager } from '../../. import { RecordProvider, useRecord } from '../../../record-provider'; import { FormProvider } from '../../core'; import { useCompile } from '../../hooks'; -import { ActionContext } from '../action'; +import { ActionContext, useActionContext } from '../action'; import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip'; import { useFieldNames } from './useFieldNames'; import { getLabelFormatValue, useLabelUiSchema } from './util'; @@ -28,7 +28,8 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => { const fieldSchema = useFieldSchema(); const recordCtx = useRecord(); const { getCollectionJoinField } = useCollectionManager(); - const field = useField(); + // value 做了转换,但 props.value 和原来 useField().value 的值不一致 + // const field = useField(); const fieldNames = useFieldNames(props); const [visible, setVisible] = useState(false); const [popoverVisible, setPopoverVisible] = useState(); @@ -37,16 +38,18 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => { const [record, setRecord] = useState({}); const compile = useCompile(); const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label'); + const { snapshot } = useActionContext(); const ellipsisWithTooltipRef = useRef(); const renderRecords = () => - toArr(field.value).map((record, index, arr) => { + toArr(props.value).map((record, index, arr) => { const val = toValue(compile(record?.[fieldNames?.label || 'label']), 'N/A'); return ( { + if (snapshot) return; e.stopPropagation(); e.preventDefault(); setVisible(true); @@ -95,7 +98,9 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => { {renderRecords()} - + {renderRecordProvider()}
diff --git a/packages/core/client/src/schema-component/antd/record-picker/index.tsx b/packages/core/client/src/schema-component/antd/record-picker/index.tsx index 8884fc5ca..eb417dc28 100644 --- a/packages/core/client/src/schema-component/antd/record-picker/index.tsx +++ b/packages/core/client/src/schema-component/antd/record-picker/index.tsx @@ -5,4 +5,7 @@ RecordPicker.Viewer = Action.Container; RecordPicker.Selector = Action.Container; export { RecordPicker }; - +export * from './useFieldNames'; +export * from './util'; +export * from './ReadPrettyRecordPicker'; +export * from './InputRecordPicker'; diff --git a/packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx index 3b7864458..1452597b6 100644 --- a/packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/remote-select/ReadPretty.tsx @@ -1,26 +1,35 @@ -import { observer, useField } from '@formily/react'; +import { observer, useField, useFieldSchema } from '@formily/react'; import React from 'react'; import { getValues } from './shared'; import { Select, defaultFieldNames } from '../select'; import { useRequest } from '../../../api-client'; +import { useRecord } from '../../../record-provider'; +import { useActionContext } from '../action'; export const ReadPretty = observer((props: any) => { const fieldNames = { ...defaultFieldNames, ...props.fieldNames }; const field = useField(); + const fieldSchema = useFieldSchema(); + const record = useRecord(); + const { snapshot } = useActionContext(); const { data } = useRequest( - { - action: 'list', - ...props.service, - params: { - paginate: false, - filter: { - [fieldNames.value]: { - $in: getValues(field.value, fieldNames), + snapshot + ? async () => ({ + data: record[fieldSchema.name], + }) + : { + action: 'list', + ...props.service, + params: { + paginate: false, + filter: { + [fieldNames.value]: { + $in: getValues(field.value, fieldNames), + }, + }, }, }, - }, - }, { refreshDeps: [props.service, field.value], }, diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx index 44422f24f..d125e9442 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.Decorator.tsx @@ -2,6 +2,7 @@ import { useField, useFieldSchema } from '@formily/react'; import React, { useLayoutEffect } from 'react'; import { SortableItem, useCollection, useCollectionManager, useCompile, useDesignable, useDesigner } from '../../../'; import { designerCss } from './Table.Column.ActionBar'; +import { isCollectionFieldComponent } from './utils'; export const useColumnSchema = () => { const { getField } = useCollection(); @@ -9,7 +10,7 @@ export const useColumnSchema = () => { const columnSchema = useFieldSchema(); const { getCollectionJoinField } = useCollectionManager(); const fieldSchema = columnSchema.reduceProperties((buf, s) => { - if (s['x-component'] === 'CollectionField') { + if (isCollectionFieldComponent(s)) { return s; } return buf; @@ -17,7 +18,7 @@ export const useColumnSchema = () => { if (!fieldSchema) { return {}; } - + const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']); return { columnSchema, fieldSchema, collectionField, uiSchema: compile(collectionField?.uiSchema) }; }; @@ -41,7 +42,7 @@ export const TableColumnDecorator = (props) => { }, [uiSchema?.title]); return ( - + {/* */} {field?.title || compile(uiSchema?.title)} {/*
{ }} /> )} - {['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && ( + {['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot'].includes(collectionField?.interface) && ( { - return schema['x-component']?.endsWith('.Column') > -1; -}; - -const isCollectionFieldComponent = (schema: ISchema) => { - return schema['x-component'] === 'CollectionField'; -}; +import { isCollectionFieldComponent, isColumnComponent } from './utils'; const useTableColumns = () => { const start = Date.now(); @@ -48,7 +41,6 @@ const useTableColumns = () => { ...s['x-component-props'], render: (v, record) => { const index = field.value?.indexOf(record); - // console.log((Date.now() - start) / 1000); return ( diff --git a/packages/core/client/src/schema-component/antd/table-v2/utils.ts b/packages/core/client/src/schema-component/antd/table-v2/utils.ts new file mode 100644 index 000000000..dd35c8876 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/table-v2/utils.ts @@ -0,0 +1,9 @@ +import { ISchema, Schema } from '@formily/react'; + +export const isCollectionFieldComponent = (schema: ISchema) => { + return schema['x-component'] === 'CollectionField'; +}; + +export const isColumnComponent = (schema: Schema) => { + return schema['x-component']?.endsWith('.Column') > -1; +}; diff --git a/packages/core/client/src/schema-initializer/index.ts b/packages/core/client/src/schema-initializer/index.ts index c67aed1dd..afdc47ec0 100644 --- a/packages/core/client/src/schema-initializer/index.ts +++ b/packages/core/client/src/schema-initializer/index.ts @@ -1,4 +1,5 @@ export * from './SchemaInitializer'; export * from './SchemaInitializerProvider'; export * from './types'; - +export * from './items'; +export { gridRowColWrap, useRecordCollectionDataSourceItems } from './utils'; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 54739dea0..28e2831a1 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { SchemaInitializerItemOptions } from '../'; import { useCollection, useCollectionManager } from '../collection-manager'; -import { useDesignable } from '../schema-component'; +import { useActionContext, useDesignable } from '../schema-component'; import { useSchemaTemplateManager } from '../schema-templates'; import { SelectCollection } from './SelectCollection'; @@ -193,6 +193,9 @@ export const useFormItemInitializerFields = (options?: any) => { const { getInterface } = useCollectionManager(); const form = useForm(); const { readPretty = form.readPretty, block = 'Form' } = options || {}; + const actionCtx = useActionContext(); + const action = actionCtx?.fieldSchema?.['x-action']; + const { snapshot } = useActionContext(); return currentFields ?.filter((field) => field?.interface && !field?.isForeignKey) @@ -204,7 +207,7 @@ export const useFormItemInitializerFields = (options?: any) => { name: field.name, // title: field?.uiSchema?.title || field.name, 'x-designer': 'FormItem.Designer', - 'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField', + 'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, 'x-component-props': {}, @@ -217,7 +220,7 @@ export const useFormItemInitializerFields = (options?: any) => { component: 'CollectionFieldInitializer', remove: removeGridFormItem, schemaInitialize: (s) => { - interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty }); + interfaceConfig?.schemaInitialize?.(s, { field, block, readPretty, action }); }, schema, } as SchemaInitializerItemOptions; @@ -288,6 +291,8 @@ export const useInheritsFormItemInitializerFields = (options?) => { const { name } = useCollection(); const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = useCollectionManager(); const inherits = getInheritCollections(name); + const { snapshot } = useActionContext(); + return inherits?.map((v) => { const fields = getParentCollectionFields(v, name); const form = useForm(); @@ -303,7 +308,7 @@ export const useInheritsFormItemInitializerFields = (options?) => { name: field.name, title: field?.uiSchema?.title || field.name, 'x-designer': 'FormItem.Designer', - 'x-component': field.interface === 'o2m' ? 'TableField' : 'CollectionField', + 'x-component': field.interface === 'o2m' && !snapshot ? 'TableField' : 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, 'x-component-props': {}, @@ -331,7 +336,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => { const remove = useRemoveGridFormItem(); return currentFields ?.filter((field) => { - return field?.interface && !field?.uiSchema?.['x-read-pretty']; + return field?.interface && !field?.uiSchema?.['x-read-pretty'] && field.interface !== 'snapshot'; }) ?.map((field) => { const interfaceConfig = getInterface(field.interface); @@ -365,7 +370,7 @@ export const useCustomBulkEditFormItemInitializerFields = (options?: any) => { const remove = useRemoveGridFormItem(); return fields ?.filter((field) => { - return field?.interface && !field?.uiSchema?.['x-read-pretty']; + return field?.interface && !field?.uiSchema?.['x-read-pretty'] && field.interface !== 'snapshot'; }) ?.map((field) => { const interfaceConfig = getInterface(field.interface); diff --git a/packages/core/sdk/src/APIClient.ts b/packages/core/sdk/src/APIClient.ts index 33bd8ab21..773d659e1 100644 --- a/packages/core/sdk/src/APIClient.ts +++ b/packages/core/sdk/src/APIClient.ts @@ -113,8 +113,8 @@ export class Auth { url: 'users:signin', data: values, params: { - authenticator - } + authenticator, + }, }); const data = response?.data?.data; this.setToken(data?.token); diff --git a/packages/plugins/collection-manager/src/index.ts b/packages/plugins/collection-manager/src/index.ts index 14bd3d5dd..962d2196e 100644 --- a/packages/plugins/collection-manager/src/index.ts +++ b/packages/plugins/collection-manager/src/index.ts @@ -1,3 +1,4 @@ export * from './repositories'; +export { default as fieldsCollection } from './collections/fields'; +export { default as collectionsCollection } from './collections/collections'; export { default } from './server'; - diff --git a/packages/plugins/snapshot-field/client.d.ts b/packages/plugins/snapshot-field/client.d.ts new file mode 100755 index 000000000..765db9222 --- /dev/null +++ b/packages/plugins/snapshot-field/client.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/client'; +export { default } from './lib/client'; + diff --git a/packages/plugins/snapshot-field/client.js b/packages/plugins/snapshot-field/client.js new file mode 100755 index 000000000..238820257 --- /dev/null +++ b/packages/plugins/snapshot-field/client.js @@ -0,0 +1,30 @@ +"use strict"; + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +var _index = _interopRequireWildcard(require("./lib/client")); + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = {}; +Object.defineProperty(exports, "default", { + enumerable: true, + get: function get() { + return _index.default; + } +}); + +Object.keys(_index).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _index[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _index[key]; + } + }); +}); diff --git a/packages/plugins/snapshot-field/package.json b/packages/plugins/snapshot-field/package.json new file mode 100644 index 000000000..1045d3e2c --- /dev/null +++ b/packages/plugins/snapshot-field/package.json @@ -0,0 +1,9 @@ +{ + "name": "@nocobase/plugin-snapshot-field", + "version": "0.8.0-alpha.13", + "main": "lib/server/index.js", + "devDependencies": { + "@nocobase/server": "0.8.0-alpha.13", + "@nocobase/test": "0.8.0-alpha.13" + } +} diff --git a/packages/plugins/snapshot-field/server.d.ts b/packages/plugins/snapshot-field/server.d.ts new file mode 100755 index 000000000..e70edb928 --- /dev/null +++ b/packages/plugins/snapshot-field/server.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/server'; +export { default } from './lib/server'; + diff --git a/packages/plugins/snapshot-field/server.js b/packages/plugins/snapshot-field/server.js new file mode 100755 index 000000000..d06a7eb92 --- /dev/null +++ b/packages/plugins/snapshot-field/server.js @@ -0,0 +1,30 @@ +"use strict"; + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +var _index = _interopRequireWildcard(require("./lib/server")); + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = {}; +Object.defineProperty(exports, "default", { + enumerable: true, + get: function get() { + return _index.default; + } +}); + +Object.keys(_index).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _index[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _index[key]; + } + }); +}); diff --git a/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers.tsx b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers.tsx new file mode 100644 index 000000000..ea9cbbfc9 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { SchemaInitializer, gridRowColWrap } from '@nocobase/client'; +import { useSnapshotTranslation } from '../../locale'; + +export const SnapshotBlockInitializers = (props: any) => { + const { t } = useSnapshotTranslation(); + const { insertPosition, component } = props; + return ( + + ); + + // const { t } = useSnapshotTranslation(); + // return ( + // + // ); +}; diff --git a/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem.tsx b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem.tsx new file mode 100644 index 000000000..0c2dea13c --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { FormOutlined } from '@ant-design/icons'; +import { + SchemaInitializer, + useBlockAssociationContext, + useCollection, + useSchemaTemplateManager, + useRecordCollectionDataSourceItems, + useBlockRequestContext, +} from '@nocobase/client'; +import { ISchema } from '@formily/react'; +import { uid } from '@formily/shared'; + +export const createSnapshotBlockSchema = (options) => { + const { + formItemInitializers = 'ReadPrettyFormItemInitializers', + actionInitializers = 'ReadPrettyFormActionInitializers', + collection, + association, + resource, + template, + ...others + } = options; + const resourceName = resource || association || collection; + const schema: ISchema = { + type: 'void', + 'x-acl-action': `${resourceName}:get`, + 'x-decorator': 'SnapshotBlockProvider', + 'x-decorator-props': { + resource: resourceName, + collection, + association, + readPretty: true, + action: 'get', + useParams: '{{ useParamsFromRecord }}', + ...others, + }, + 'x-designer': 'FormV2.ReadPrettyDesigner', + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'FormV2', + 'x-read-pretty': true, + 'x-component-props': { + useProps: '{{ useFormBlockProps }}', + }, + properties: { + grid: template || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': formItemInitializers, + properties: {}, + }, + }, + }, + }, + }; + console.log(JSON.stringify(schema, null, 2)); + return schema; +}; + +export const SnapshotBlockInitializersDetailItem = (props) => { + const { + onCreateBlockSchema, + componentType, + createBlockSchema, + insert, + icon = true, + targetCollection, + ...others + } = props; + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const collection = targetCollection || useCollection(); + const association = useBlockAssociationContext(); + const { block } = useBlockRequestContext(); + const actionInitializers = + block !== 'TableField' ? props.actionInitializers || 'ReadPrettyFormActionInitializers' : null; + + return ( + } + {...others} + key={'snapshotDetail'} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + if (item.template.componentName === 'ReadPrettyFormItem') { + const blockSchema = createSnapshotBlockSchema({ + actionInitializers, + association, + collection: collection.name, + action: 'get', + useSourceId: '{{ useSourceIdFromParentRecord }}', + useParams: '{{ useParamsFromRecord }}', + template: s, + }); + if (item.mode === 'reference') { + blockSchema['x-template-key'] = item.template.key; + } + insert(blockSchema); + } else { + insert(s); + } + } else { + insert( + createSnapshotBlockSchema({ + actionInitializers, + association, + collection: collection.name, + action: 'get', + useSourceId: '{{ useSourceIdFromParentRecord }}', + useParams: '{{ useParamsFromRecord }}', + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('ReadPrettyFormItem')} + /> + ); +}; diff --git a/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockProvider.tsx b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockProvider.tsx new file mode 100644 index 000000000..a5364dccd --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/SnapshotBlock/SnapshotBlockProvider.tsx @@ -0,0 +1,105 @@ +import React, { useContext, useMemo, useRef } from 'react'; +import { createForm } from '@formily/core'; +import { useField } from '@formily/react'; +import { + BlockAssociationContext, + BlockRequestContext, + BlockResourceContext, + FormBlockContext, + MaybeCollectionProvider, + RecordProvider, + useBlockRequestContext, + useBlockResource, + useCollectionManager, + useDesignable, + useRecord, + useResource, +} from '@nocobase/client'; +import { Spin } from 'antd'; + +const InternalFormBlockProvider = (props) => { + const { action, readPretty } = props; + const field = useField(); + const form = useMemo( + () => + createForm({ + readPretty, + }), + [], + ); + const { resource, service } = useBlockRequestContext(); + const formBlockRef = useRef(); + if (service.loading) { + return ; + } + return ( + + {readPretty ? ( + +
{props.children}
+
+ ) : ( +
{props.children}
+ )} +
+ ); +}; + +const BlockRequestProvider = (props) => { + const field = useField(); + const resource = useBlockResource(); + const service = { + loading: false, + data: { + data: useRecord(), + }, + }; + const __parent = useContext(BlockRequestContext); + return ( + + {props.children} + + ); +}; + +const BlockProvider = (props) => { + const { collection, association } = props; + const resource = useResource(props); + + return ( + + + + {props.children} + + + + ); +}; + +export const SnapshotBlockProvider = (props) => { + const record = useRecord(); + const { __tableName } = record; + const { getInheritCollections } = useCollectionManager(); + const inheritCollections = getInheritCollections(__tableName); + const { designable } = useDesignable(); + const flag = + !designable && __tableName && !inheritCollections.includes(props.collection) && __tableName !== props.collection; + return ( + !flag && ( + + + + ) + ); +}; diff --git a/packages/plugins/snapshot-field/src/client/SnapshotHistoryCollectionProvider.tsx b/packages/plugins/snapshot-field/src/client/SnapshotHistoryCollectionProvider.tsx new file mode 100644 index 000000000..8b6e733fe --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/SnapshotHistoryCollectionProvider.tsx @@ -0,0 +1,42 @@ +import { CollectionManagerContext, useHistoryCollectionsByNames } from '@nocobase/client'; +import React, { useContext } from 'react'; + +export const SnapshotHistoryCollectionProvider: React.FC<{ collectionName: string }> = (props) => { + const { collectionName } = props; + const { collections: allCollections, ...rest } = useContext(CollectionManagerContext); + + // 目标表 + const snapshotTargetCollection = useHistoryCollectionsByNames([collectionName])?.[0]; + // 目标如果是继承表则获取继承表 + const inheritCollections = useHistoryCollectionsByNames(snapshotTargetCollection?.inherits ?? []); + // 目标表内关联字段的表 + const associationFieldTargetCollections = useHistoryCollectionsByNames( + snapshotTargetCollection?.fields.filter((i) => i.interface !== 'snapshot').map((i) => i.target) ?? [], + ); + + // 替换表的集合 + const finallyHistoryCollecionts = [ + snapshotTargetCollection, + ...associationFieldTargetCollections, + ...inheritCollections, + ].filter((i) => i); + + // 过滤出不需要替换的表 + const filterdAllCollection = allCollections.filter( + (c) => !finallyHistoryCollecionts.map((i) => i.name).includes(c.name), + ); + + // 最终替换后的表 + const overridedCollections = [...filterdAllCollection, ...finallyHistoryCollecionts]; + + return ( + + {props.children} + + ); +}; diff --git a/packages/plugins/snapshot-field/src/client/SnapshotRecordPicker.tsx b/packages/plugins/snapshot-field/src/client/SnapshotRecordPicker.tsx new file mode 100644 index 000000000..f9055cf27 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/SnapshotRecordPicker.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { connect, mapReadPretty, useFieldSchema } from '@formily/react'; +import { + InputRecordPicker, + ReadPrettyRecordPicker, + useActionContext, + useCollection, + useCollectionHistory, +} from '@nocobase/client'; +import { SnapshotHistoryCollectionProvider } from './SnapshotHistoryCollectionProvider'; + +const useSnapshotFieldTargetCollectionName = () => { + const fieldSchema = useFieldSchema(); + const { getField } = useCollection(); + const collectionField = getField(fieldSchema.name); + const { historyCollections } = useCollectionHistory(); + return historyCollections.find((i) => i.name === collectionField.target)?.name; +}; + +const ReadPrettyRecordPickerWrapper = (props) => { + const collectionName = useSnapshotFieldTargetCollectionName(); + + return ( + + + + ); +}; + +const SnapshotRecordPickerInner: any = connect( + (props) => { + const actionCtx = useActionContext(); + + const isUpdateAction = actionCtx.fieldSchema['x-action'] === 'update'; + + return isUpdateAction ? : ; + }, + // mapProps(mapSuffixProps), + mapReadPretty(ReadPrettyRecordPickerWrapper), +); + +export const SnapshotRecordPicker = (props) => { + const { value, onChange, ...restProps } = props; + + const newProps = { + ...restProps, + value: value?.data, + onChange: (value) => onChange({ data: value }), + }; + + return ; +}; diff --git a/packages/plugins/snapshot-field/src/client/index.tsx b/packages/plugins/snapshot-field/src/client/index.tsx new file mode 100644 index 000000000..1e886ef19 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/index.tsx @@ -0,0 +1,50 @@ +import { + CollectionHistoryProvider, + CollectionManagerProvider, + registerField, + SchemaComponentOptions, + SchemaInitializerContext, + SchemaInitializerProvider, +} from '@nocobase/client'; +import React, { useContext, useState, useEffect } from 'react'; +import { useSnapshotInterface } from './interface'; +import { SnapshotRecordPicker } from './SnapshotRecordPicker'; +import { SnapshotBlockInitializers } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializers'; +import { SnapshotBlockInitializersDetailItem } from './SnapshotBlock/SnapshotBlockInitializers/SnapshotBlockInitializersDetailItem'; +import { SnapshotBlockProvider } from './SnapshotBlock/SnapshotBlockProvider'; + +export default React.memo((props) => { + const initializers = useContext(SchemaInitializerContext); + const snapshot = useSnapshotInterface(); + + useEffect(() => { + registerField(snapshot.group, snapshot.name as string, snapshot); + }, []); + + return ( + + + + + {props.children} + + + + + ); +}); diff --git a/packages/plugins/snapshot-field/src/client/interface.ts b/packages/plugins/snapshot-field/src/client/interface.ts new file mode 100644 index 000000000..703a661e7 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/interface.ts @@ -0,0 +1,102 @@ +import { ISchema } from '@formily/react'; +import { IField } from '@nocobase/client'; +import { cloneDeep } from 'lodash'; +import { interfacesProperties } from '@nocobase/client'; +import { useSnapshotTranslation } from './locale'; +import { useMemo } from 'react'; + +const { defaultProps, recordPickerSelector } = interfacesProperties; + +export const useSnapshotInterface = () => { + const { t } = useSnapshotTranslation(); + + const recordPickerViewer = { + type: 'void', + title: t('View record'), + 'x-component': 'RecordPicker.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('Detail'), + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'SnapshotBlockInitializers', + properties: {}, + }, + }, + }, + }, + }, + }, + }; + + const snapshot: IField = { + name: 'snapshot', + type: 'object', + group: 'advanced', + title: t('Snapshot'), + description: t('Snapshot to description'), + default: { + type: 'snapshot', + // name, + uiSchema: { + // title, + 'x-component': 'SnapshotRecordPicker', + 'x-component-props': { + // mode: 'tags', + multiple: true, + fieldNames: { + label: 'id', + value: 'id', + }, + }, + }, + }, + schemaInitialize(schema: ISchema, { field, readPretty, action, block }) { + if (readPretty || action === 'update') { + schema['properties'] = { + viewer: cloneDeep(recordPickerViewer), + }; + } else { + schema['properties'] = { + selector: cloneDeep(recordPickerSelector), + }; + } + }, + initialize: (values: any) => {}, + properties: { + ...defaultProps, + target: { + type: 'string', + title: t('Related collection'), + required: true, + 'x-reactions': ['{{useAsyncDataSource(loadCollections)}}'], + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-disabled': '{{ !createOnly }}', + }, + 'uiSchema.x-component-props.multiple': { + type: 'boolean', + 'x-content': t('Allow linking to multiple records'), + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + }, + }, + }; + + return useMemo(() => snapshot, [t]); +}; diff --git a/packages/plugins/snapshot-field/src/client/locale/en-US.ts b/packages/plugins/snapshot-field/src/client/locale/en-US.ts new file mode 100644 index 000000000..0794d0e6e --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/locale/en-US.ts @@ -0,0 +1,10 @@ +export default { + Detail: 'Detail', + Snapshot: 'Snapshot', + 'View record': 'View record', + 'Add block': 'Add block', + 'Allow linking to multiple records': 'Allow linking to multiple records', + 'Snapshot to description': + 'It is used to create a snapshot of the table, save the current data of the pointed table, and save it only when the record to which it belongs is created, and will not be updated later.', + 'Related collection': 'Related collection', +}; diff --git a/packages/plugins/snapshot-field/src/client/locale/index.ts b/packages/plugins/snapshot-field/src/client/locale/index.ts new file mode 100644 index 000000000..abff8ed44 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/locale/index.ts @@ -0,0 +1,24 @@ +import { i18n } from '@nocobase/client'; +import { useTranslation } from 'react-i18next'; + +import enUS from './en-US'; +import jaJP from './ja-JP'; +import ruRU from './ru-RU'; +import trTR from './tr-TR'; +import zhCN from './zh-CN'; + +export const NAMESPACE = 'snapshot-field'; + +i18n.addResources('zh-CN', NAMESPACE, zhCN); +i18n.addResources('en-US', NAMESPACE, enUS); +i18n.addResources('ja-JP', NAMESPACE, jaJP); +i18n.addResources('ru-RU', NAMESPACE, ruRU); +i18n.addResources('tr-TR', NAMESPACE, trTR); + +export function lang(key: string) { + return i18n.t(key, { ns: NAMESPACE }); +} + +export function useSnapshotTranslation() { + return useTranslation(NAMESPACE); +} diff --git a/packages/plugins/snapshot-field/src/client/locale/ja-JP.ts b/packages/plugins/snapshot-field/src/client/locale/ja-JP.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/locale/ja-JP.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/plugins/snapshot-field/src/client/locale/ru-RU.ts b/packages/plugins/snapshot-field/src/client/locale/ru-RU.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/locale/ru-RU.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/plugins/snapshot-field/src/client/locale/tr-TR.ts b/packages/plugins/snapshot-field/src/client/locale/tr-TR.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/locale/tr-TR.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/plugins/snapshot-field/src/client/locale/zh-CN.ts b/packages/plugins/snapshot-field/src/client/locale/zh-CN.ts new file mode 100644 index 000000000..4f27383a7 --- /dev/null +++ b/packages/plugins/snapshot-field/src/client/locale/zh-CN.ts @@ -0,0 +1,9 @@ +export default { + Detail: '详情', + Snapshot: '快照', + 'Add block': '创建区块', + 'Snapshot to description': '用于创建表的快照,保存指向的表的当前数据,仅在其所属的记录创建时保存,后续不再更新。', + 'View record': '查看数据', + 'Allow linking to multiple records': '允许关联多条记录', + 'Related collection': '关系表', +}; diff --git a/packages/plugins/snapshot-field/src/index.ts b/packages/plugins/snapshot-field/src/index.ts new file mode 100644 index 000000000..7ddad5814 --- /dev/null +++ b/packages/plugins/snapshot-field/src/index.ts @@ -0,0 +1 @@ +export { default } from './server'; diff --git a/packages/plugins/snapshot-field/src/server/collections/.gitkeep b/packages/plugins/snapshot-field/src/server/collections/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/snapshot-field/src/server/collections/collectionsHistory.ts b/packages/plugins/snapshot-field/src/server/collections/collectionsHistory.ts new file mode 100644 index 000000000..9d812e5b5 --- /dev/null +++ b/packages/plugins/snapshot-field/src/server/collections/collectionsHistory.ts @@ -0,0 +1,55 @@ +import { CollectionOptions } from '@nocobase/database'; + +export default { + name: 'collectionsHistory', + title: '数据表历史', + sortable: 'sort', + autoGenId: false, + model: 'CollectionModel', + repository: 'CollectionRepository', + timestamps: false, + filterTargetKey: 'name', + fields: [ + { + type: 'uid', + name: 'key', + primaryKey: true, + }, + { + type: 'uid', + name: 'name', + unique: true, + prefix: 't_', + }, + { + type: 'string', + name: 'title', + required: true, + }, + { + type: 'boolean', + name: 'inherit', + defaultValue: false, + }, + { + type: 'boolean', + name: 'hidden', + defaultValue: false, + }, + { + type: 'json', + name: 'options', + defaultValue: {}, + }, + { + type: 'hasMany', + name: 'fields', + target: 'fieldsHistory', + sourceKey: 'name', + targetKey: 'name', + foreignKey: 'collectionName', + onDelete: 'CASCADE', + sortBy: 'sort', + }, + ], +} as CollectionOptions; diff --git a/packages/plugins/snapshot-field/src/server/collections/fieldsHistory.ts b/packages/plugins/snapshot-field/src/server/collections/fieldsHistory.ts new file mode 100644 index 000000000..bbfc78385 --- /dev/null +++ b/packages/plugins/snapshot-field/src/server/collections/fieldsHistory.ts @@ -0,0 +1,73 @@ +import { CollectionOptions } from '@nocobase/database'; + +export default { + name: 'fieldsHistory', + title: '{{t("Fields history")}}', + autoGenId: false, + model: 'FieldModel', + timestamps: false, + sortable: { + name: 'sort', + scopeKey: 'collectionName', + }, + indexes: [ + { + type: 'UNIQUE', + fields: ['collectionName', 'name'], + }, + ], + fields: [ + { + type: 'uid', + name: 'key', + primaryKey: true, + }, + { + type: 'uid', + name: 'name', + prefix: 'f_', + }, + { + type: 'string', + name: 'type', + }, + { + type: 'string', + name: 'interface', + allowNull: true, + }, + { + type: 'belongsTo', + name: 'collection', + target: 'collectionsHistory', + foreignKey: 'collectionName', + targetKey: 'name', + onDelete: 'CASCADE', + }, + { + type: 'hasMany', + name: 'children', + target: 'fieldsHistory', + sourceKey: 'key', + foreignKey: 'parentKey', + }, + { + type: 'hasOne', + name: 'reverseField', + target: 'fieldsHistory', + sourceKey: 'key', + foreignKey: 'reverseKey', + }, + { + type: 'belongsTo', + name: 'uiSchema', + target: 'uiSchemas', + foreignKey: 'uiSchemaUid', + }, + { + type: 'json', + name: 'options', + defaultValue: {}, + }, + ], +} as CollectionOptions; diff --git a/packages/plugins/snapshot-field/src/server/fields/snapshot-field.ts b/packages/plugins/snapshot-field/src/server/fields/snapshot-field.ts new file mode 100644 index 000000000..fb4b2efbb --- /dev/null +++ b/packages/plugins/snapshot-field/src/server/fields/snapshot-field.ts @@ -0,0 +1,12 @@ +import { Field, BaseColumnFieldOptions } from '@nocobase/database'; +import { DataTypes } from 'sequelize'; + +export class SnapshotField extends Field { + get dataType() { + return DataTypes.JSON; + } +} + +export interface SnapshotFieldOptions extends BaseColumnFieldOptions { + type: 'snapshot'; +} diff --git a/packages/plugins/snapshot-field/src/server/index.ts b/packages/plugins/snapshot-field/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/snapshot-field/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/plugins/snapshot-field/src/server/plugin.ts b/packages/plugins/snapshot-field/src/server/plugin.ts new file mode 100644 index 000000000..c15f25a4d --- /dev/null +++ b/packages/plugins/snapshot-field/src/server/plugin.ts @@ -0,0 +1,108 @@ +import { Model } from '@nocobase/database'; +import { InstallOptions, Plugin } from '@nocobase/server'; +import { resolve } from 'path'; +import { SnapshotField } from './fields/snapshot-field'; + +export class SnapshotFieldPlugin extends Plugin { + afterAdd() {} + + async beforeLoad() { + const collectionHandler = async (model: Model, { transaction }) => { + const collectionDoc = model.toJSON(); + const collectionsHistoryRepository = this.app.db.getRepository('collectionsHistory'); + const fieldsHistoryRepository = this.app.db.getRepository('fieldsHistory'); + + const existCollection: Model = await collectionsHistoryRepository.findOne({ + filter: { + name: collectionDoc.name, + }, + }); + + if (existCollection) { + // 删除表和其关联字段 + await existCollection.destroy({ + transaction, + }); + } + + await collectionsHistoryRepository.create({ + values: collectionDoc, + transaction, + }); + + await fieldsHistoryRepository.createMany({ + records: collectionDoc.fields ?? [], + transaction, + }); + }; + + this.app.db.on('collections.afterCreateWithAssociations', collectionHandler); + + const fieldHandler = async (model: Model, { transaction }) => { + const fieldDoc = model.get(); + const fieldsHistoryRepository = this.app.db.getRepository('fieldsHistory'); + const existField: Model = await fieldsHistoryRepository.findOne({ + filter: { + name: fieldDoc.name, + }, + }); + if (existField) { + await existField.destroy({ + transaction, + }); + } + await fieldsHistoryRepository.create({ + values: fieldDoc, + transaction, + }); + }; + + this.app.db.on('fields.afterCreateWithAssociations', fieldHandler); + } + + async load() { + // 导入 collection + await this.db.import({ + directory: resolve(__dirname, 'collections'), + }); + + this.app.db.registerFieldTypes({ + snapshot: SnapshotField, + }); + } + + // 初始化安装的时候 + async install(options?: InstallOptions) { + await this.app.db.sequelize.transaction(async (transaction) => { + const collectionsRepository = this.app.db.getRepository('collections'); + const collectionsHistoryRepository = this.app.db.getRepository('collectionsHistory'); + + if ((await collectionsHistoryRepository.find()).length === 0) { + const collectionsModels: Model[] = await collectionsRepository.find(); + await collectionsHistoryRepository.createMany({ + records: collectionsModels.map((m) => m.get()), + transaction, + }); + } + + const fieldsRepository = this.app.db.getRepository('fields'); + const fieldsHistoryRepository = this.app.db.getRepository('fieldsHistory'); + + if ((await fieldsHistoryRepository.find()).length === 0) { + const fieldsModels: Model[] = await fieldsRepository.find(); + await fieldsHistoryRepository.createMany({ + records: fieldsModels.map((m) => m.get()), + transaction, + }); + } + }); + } + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default SnapshotFieldPlugin; diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index ed88cf6be..58e41d7e6 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -31,6 +31,7 @@ "@nocobase/plugin-verification": "0.8.0-alpha.13", "@nocobase/plugin-workflow": "0.8.0-alpha.13", "@nocobase/plugin-map": "0.8.0-alpha.13", + "@nocobase/plugin-snapshot-field": "0.8.0-alpha.13", "@nocobase/plugin-iframe-block": "0.8.0-alpha.13", "@nocobase/server": "0.8.0-alpha.13" }, diff --git a/packages/presets/nocobase/src/index.ts b/packages/presets/nocobase/src/index.ts index eb70efb7b..ba7678158 100644 --- a/packages/presets/nocobase/src/index.ts +++ b/packages/presets/nocobase/src/index.ts @@ -29,7 +29,7 @@ export class PresetNocoBase extends Plugin { } getLocalPlugins() { - const localPlugins = ['sample-hello', 'oidc', 'saml', 'map']; + const localPlugins = ['sample-hello', 'oidc', 'saml', 'map', 'snapshot-field']; return localPlugins; }