feat: snapshot field plugin (#1253)

* feat: snapshort init

* feat: snapshot update yarn.lock

* feat: snapshot add to preset

* feat: snapshot add field fix

* feat: snapshot remove Table Column SnapshotField

* feat: snapshot field label fix

* feat: snapshot request error fix

* feat: snapshot 二级关联数据打开

* feat: snapshot batch edit fix

* feat: snapshot 2 level draw fix

* feat: snapshot translate

* feat: snapshot global historyCollection provider

* feat: snapshot install initial

* feat: snapshot refreshCH

* feat: snapshot add transaction

* feat: snapshot default collecitonField

* feat: snapshot build fix

* feat: snapshot useSnapshotFieldTargetCollectionKey

* feat: snapshot batch update

* feat: snapshot linkto support

* feat: snapshot use getRepository

* feat: snapshot recreate fix

* feat: snapshot collectionKey to collectionName & rebuild collection

* feat: snapshot remove SnapshotHistoryCollectionProvider & collectionName

* feat: snapshot use historyCollections in inherit table

* feat: snapshot fix TableSelectorBlock appends

* feat: snapshot kanban fix

* feat: snapshot snapshot association field fix

* feat: snapshot add CollectionFieldProvider fallback

* feat: snapshot AssociationSelect fix

* feat: snapshot TableField fix
This commit is contained in:
anuoua 2023-01-04 19:27:37 +08:00 committed by GitHub
parent a44b778098
commit 6febdb041a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1212 additions and 96 deletions

View File

@ -0,0 +1 @@
export { default } from '@nocobase/plugin-snapshot-field/client';

View File

@ -1,3 +1,4 @@
export * from './hooks';
export * from './APIClient';
export * from './APIClientProvider';
export * from './context';

View File

@ -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<any>(null);
export const BlockRequestContext = createContext<any>(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 ? (
<CollectionProvider collection={collection}>

View File

@ -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;
}

View File

@ -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) {

View File

@ -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';

View File

@ -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) {

View File

@ -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<Field>();
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 <div>{displayText}</div>;
};
export const CollectionField = connect((props) => {
const fieldSchema = useFieldSchema();
const field = fieldSchema?.['x-component-props']?.['field'];
const { snapshot } = useActionContext();
return (
<CollectionFieldProvider name={fieldSchema.name} field={field}>
<CollectionFieldProvider
name={fieldSchema.name}
field={field}
fallback={snapshot ? <InternalFallbackField /> : null}
>
<InternalField {...props} />
</CollectionFieldProvider>
);

View File

@ -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 (
<CollectionFieldContext.Provider value={value}>
{children}
</CollectionFieldContext.Provider>
);
return <CollectionFieldContext.Provider value={value}>{children}</CollectionFieldContext.Provider>;
};

View File

@ -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<any>;
}
const CollectionHistoryContext = createContext<CollectionHistoryContextValue>({
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 <Spin />;
}
return (
<CollectionHistoryContext.Provider
value={{
historyCollections: service.data?.data,
refreshCH,
}}
>
{props.children}
</CollectionHistoryContext.Provider>
);
};
export const useHistoryCollectionsByNames = (collectionNames: string[]) => {
const { historyCollections } = useContext(CollectionHistoryContext);
return historyCollections.filter((i) => collectionNames.includes(i.name));
};
export const useCollectionHistory = () => {
return useContext(CollectionHistoryContext);
};

View File

@ -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<CollectionManagerOptions> = (props) => {
const { service, interfaces, collections = [], refreshCM, templates } = props;
@ -31,6 +32,7 @@ export const CollectionManagerProvider: React.FC<CollectionManagerOptions> = (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 <Spin />;
}
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 (
<CollectionManagerProvider
service={{ ...service, contentLoading, setContentLoading }}
collections={service?.data?.data}
refreshCM={async (opts) => {
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}
/>
);

View File

@ -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 || {};

View File

@ -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) => {

View File

@ -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',
}}
/>

View File

@ -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';

View File

@ -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}}',
},
},
{

View File

@ -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),
}));
};

View File

@ -15,3 +15,4 @@ export * from './ResourceActionProvider';
export { getConfigurableProperties } from './templates/properties';
export * from './templates/types';
export * from './types';
export * from './CollectionHistoryProvider';

View File

@ -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' },
],
};

View File

@ -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}}',
},
},
{

View File

@ -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;

View File

@ -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]);
}

View File

@ -1,3 +1,4 @@
export * from './Input';
export * from './ReadPretty';
export * from './Json';
export * from './EllipsisWithTooltip';

View File

@ -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<Field>();
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
// const field = useField<Field>();
const fieldNames = useFieldNames(props);
const [visible, setVisible] = useState(false);
const [popoverVisible, setPopoverVisible] = useState<boolean>();
@ -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<IEllipsisWithTooltipRef>();
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 (
<Fragment key={`${record.id}_${index}`}>
<span>
<a
onClick={(e) => {
if (snapshot) return;
e.stopPropagation();
e.preventDefault();
setVisible(true);
@ -95,7 +98,9 @@ export const ReadPrettyRecordPicker: React.FC = observer((props: any) => {
<EllipsisWithTooltip ellipsis={ellipsis} ref={ellipsisWithTooltipRef}>
{renderRecords()}
</EllipsisWithTooltip>
<ActionContext.Provider value={{ visible, setVisible, openMode: 'drawer' }}>
<ActionContext.Provider
value={{ visible, setVisible, openMode: 'drawer', snapshot: collectionField.interface === 'snapshot' }}
>
{renderRecordProvider()}
</ActionContext.Provider>
</CollectionProvider>

View File

@ -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';

View File

@ -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<any>();
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],
},

View File

@ -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 (
<SortableItem className={designerCss}>
<Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField}/>
<Designer fieldSchema={fieldSchema} uiSchema={uiSchema} collectionField={collectionField} />
{/* <RecursionField name={columnSchema.name} schema={columnSchema}/> */}
{field?.title || compile(uiSchema?.title)}
{/* <div

View File

@ -120,7 +120,7 @@ export const TableColumnDesigner = (props) => {
}}
/>
)}
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho'].includes(collectionField?.interface) && (
{['linkTo', 'm2m', 'm2o', 'o2m', 'obo', 'oho', 'snapshot'].includes(collectionField?.interface) && (
<SchemaSettings.SelectItem
title={t('Title field')}
options={options}

View File

@ -11,14 +11,7 @@ import React, { RefCallback, useCallback, useEffect, useMemo, useRef, useState }
import { useTranslation } from 'react-i18next';
import { DndContext, useDesignable } from '../..';
import { RecordIndexProvider, RecordProvider, useSchemaInitializer } from '../../../';
const isColumnComponent = (schema: Schema) => {
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 (
<RecordIndexProvider index={index}>
<RecordProvider record={record}>

View File

@ -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;
};

View File

@ -1,4 +1,5 @@
export * from './SchemaInitializer';
export * from './SchemaInitializerProvider';
export * from './types';
export * from './items';
export { gridRowColWrap, useRecordCollectionDataSourceItems } from './utils';

View File

@ -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);

View File

@ -113,8 +113,8 @@ export class Auth {
url: 'users:signin',
data: values,
params: {
authenticator
}
authenticator,
},
});
const data = response?.data?.data;
this.setToken(data?.token);

View File

@ -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';

4
packages/plugins/snapshot-field/client.d.ts vendored Executable file
View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/client';
export { default } from './lib/client';

View File

@ -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];
}
});
});

View File

@ -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"
}
}

4
packages/plugins/snapshot-field/server.d.ts vendored Executable file
View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/server';
export { default } from './lib/server';

View File

@ -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];
}
});
});

View File

@ -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 (
<SchemaInitializer.Button
wrap={gridRowColWrap}
insertPosition={insertPosition}
component={component}
title={component ? null : t('Add block')}
icon={'PlusOutlined'}
items={[
{
type: 'itemGroup',
title: '{{t("Current record blocks")}}',
children: [
{
key: 'details',
type: 'item',
title: '{{t("Details")}}',
component: 'SnapshotBlockInitializersDetailItem',
actionInitializers: 'CalendarFormActionInitializers',
},
],
},
{
type: 'itemGroup',
title: '{{t("Other blocks")}}',
children: [
{
key: 'markdown',
type: 'item',
title: '{{t("Markdown")}}',
component: 'MarkdownBlockInitializer',
},
],
},
]}
/>
);
// const { t } = useSnapshotTranslation();
// return (
// <SchemaInitializer.Button
// wrap={gridRowColWrap}
// title={t('Add block')}
// icon={'PlusOutlined'}
// items={[
// {
// type: 'itemGroup',
// title: '{{ t("Data blocks") }}',
// children: [
// {
// type: 'item',
// title: '{{t("Details")}}',
// component: 'SnapshotBlockInitializersDetailItem',
// actionInitializers: 'CalendarFormActionInitializers',
// },
// ],
// },
// {
// type: 'itemGroup',
// title: '{{t("Other blocks")}}',
// children: [
// {
// type: 'item',
// title: '{{t("Markdown")}}',
// component: 'MarkdownBlockInitializer',
// },
// ],
// },
// ]}
// />
// );
};

View File

@ -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 (
<SchemaInitializer.Item
icon={icon && <FormOutlined />}
{...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')}
/>
);
};

View File

@ -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 <Spin />;
}
return (
<FormBlockContext.Provider
value={{
action,
form,
field,
service,
resource,
updateAssociationValues: [],
formBlockRef,
}}
>
{readPretty ? (
<RecordProvider record={service?.data?.data}>
<div ref={formBlockRef}>{props.children}</div>
</RecordProvider>
) : (
<div ref={formBlockRef}>{props.children}</div>
)}
</FormBlockContext.Provider>
);
};
const BlockRequestProvider = (props) => {
const field = useField();
const resource = useBlockResource();
const service = {
loading: false,
data: {
data: useRecord(),
},
};
const __parent = useContext(BlockRequestContext);
return (
<BlockRequestContext.Provider value={{ block: props.block, props, field, service, resource, __parent }}>
{props.children}
</BlockRequestContext.Provider>
);
};
const BlockProvider = (props) => {
const { collection, association } = props;
const resource = useResource(props);
return (
<MaybeCollectionProvider collection={collection}>
<BlockAssociationContext.Provider value={association}>
<BlockResourceContext.Provider value={resource}>
<BlockRequestProvider {...props}>{props.children}</BlockRequestProvider>
</BlockResourceContext.Provider>
</BlockAssociationContext.Provider>
</MaybeCollectionProvider>
);
};
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 && (
<BlockProvider {...props}>
<InternalFormBlockProvider {...props} />
</BlockProvider>
)
);
};

View File

@ -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 (
<CollectionManagerContext.Provider
value={{
...rest,
collections: overridedCollections,
}}
>
{props.children}
</CollectionManagerContext.Provider>
);
};

View File

@ -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 (
<SnapshotHistoryCollectionProvider collectionName={collectionName}>
<ReadPrettyRecordPicker {...props} />
</SnapshotHistoryCollectionProvider>
);
};
const SnapshotRecordPickerInner: any = connect(
(props) => {
const actionCtx = useActionContext();
const isUpdateAction = actionCtx.fieldSchema['x-action'] === 'update';
return isUpdateAction ? <ReadPrettyRecordPickerWrapper {...props} /> : <InputRecordPicker {...props} />;
},
// 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 <SnapshotRecordPickerInner {...newProps} />;
};

View File

@ -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 (
<CollectionManagerProvider
interfaces={{
snapshot,
}}
>
<CollectionHistoryProvider>
<SchemaInitializerProvider
initializers={{
...initializers,
SnapshotBlockInitializers,
}}
>
<SchemaComponentOptions
components={{
SnapshotRecordPicker,
SnapshotBlockProvider,
SnapshotBlockInitializersDetailItem,
}}
>
{props.children}
</SchemaComponentOptions>
</SchemaInitializerProvider>
</CollectionHistoryProvider>
</CollectionManagerProvider>
);
});

View File

@ -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<IField>(() => snapshot, [t]);
};

View File

@ -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',
};

View File

@ -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);
}

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1,9 @@
export default {
Detail: '详情',
Snapshot: '快照',
'Add block': '创建区块',
'Snapshot to description': '用于创建表的快照,保存指向的表的当前数据,仅在其所属的记录创建时保存,后续不再更新。',
'View record': '查看数据',
'Allow linking to multiple records': '允许关联多条记录',
'Related collection': '关系表',
};

View File

@ -0,0 +1 @@
export { default } from './server';

View File

@ -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;

View File

@ -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;

View File

@ -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';
}

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -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;

View File

@ -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"
},

View File

@ -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;
}