mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 05:29:26 +08:00
* feat: support to add File collection * feat: support to upload files * refactor: rename 'ReadPretty.Attachment' to 'ReadPretty.File' * feat: support to associate the File collection * refactor: add Preview and replace Upload.Selector * fix(Preview): fix some problems in ReadPretty mode * feat: use 'preview' as a default title field * feat: support only local storage now * fix: should not show 'Add new' button * chore: add default value for file storage * fix: fix preview field of file collection cannot be displayed normally * fix: only Table and Details can display File collection * chore: translate * refactor: migration to plugin from core * refactor: change 'preview' to 'url' * fix: only 'belongsTo' and 'belongsToMany' can linked file collection * fix: fix storage and add a field called storage in file collection * feat: add 'deletable' to configure the visibility of the delete button * fix: fix can't upload attachment problem * fix: remove more option * fix: can't use preview to filter * fix: remove Import action option * refactor: remove useless code * chore: optimize condition * chore: remove comment * test: windows compatible * refactor: optimize upload * fix: upload action * fix: createAction * fix: uploads * fix: file collection cannot be inherited by other collections * fix: url should be editable * fix: url is filterable * fix: use input interface for url field * fix: fix error * fix: remove subform * Revert "chore: translate" This reverts commit 53cd346dab8cbee0c52a9da3cf83a99dff2def34. * refactor: move translation to plugin * fix: title is editable * fix: collection?.template === 'file' * fix: fix order of URL * fix(collection-manager): allow collectionCategories:list * chore: add translation * fix(upload): should enable to use drawer * refactor: move code to plugin --------- Co-authored-by: chenos <chenlinxh@gmail.com>
249 lines
8.4 KiB
TypeScript
249 lines
8.4 KiB
TypeScript
import { ArrayField, createForm } from '@formily/core';
|
|
import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
|
|
import uniq from 'lodash/uniq';
|
|
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
import { useCollectionManager } from '../collection-manager';
|
|
import { useFilterBlock } from '../filter-provider/FilterProvider';
|
|
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
|
|
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
|
|
import { mergeFilter } from './SharedFilterProvider';
|
|
import { findFilterTargets } from './hooks';
|
|
|
|
export const TableBlockContext = createContext<any>({});
|
|
|
|
interface Props {
|
|
params?: any;
|
|
showIndex?: boolean;
|
|
dragSort?: boolean;
|
|
rowKey?: string;
|
|
childrenColumnName: any;
|
|
}
|
|
|
|
const InternalTableBlockProvider = (props: Props) => {
|
|
const { params, showIndex, dragSort, rowKey, childrenColumnName } = props;
|
|
const field = useField();
|
|
const { resource, service } = useBlockRequestContext();
|
|
const [expandFlag, setExpandFlag] = useState(false);
|
|
return (
|
|
<FixedBlockWrapper>
|
|
<TableBlockContext.Provider
|
|
value={{
|
|
field,
|
|
service,
|
|
resource,
|
|
params,
|
|
showIndex,
|
|
dragSort,
|
|
rowKey,
|
|
expandFlag,
|
|
childrenColumnName,
|
|
setExpandFlag: () => setExpandFlag(!expandFlag),
|
|
}}
|
|
>
|
|
<RenderChildrenWithAssociationFilter {...props} />
|
|
</TableBlockContext.Provider>
|
|
</FixedBlockWrapper>
|
|
);
|
|
};
|
|
|
|
export const useAssociationNames = (collection) => {
|
|
const { getCollectionFields } = useCollectionManager();
|
|
const collectionFields = getCollectionFields(collection);
|
|
const associationFields = new Set();
|
|
for (const collectionField of collectionFields) {
|
|
if (collectionField.target) {
|
|
associationFields.add(collectionField.name);
|
|
const fields = getCollectionFields(collectionField.target);
|
|
for (const field of fields) {
|
|
if (field.target) {
|
|
associationFields.add(`${collectionField.name}.${field.name}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const fieldSchema = useFieldSchema();
|
|
const tableSchema = fieldSchema.reduceProperties((buf, schema) => {
|
|
if (schema['x-component'] === 'TableV2') {
|
|
return schema;
|
|
}
|
|
return buf;
|
|
}, new Schema({}));
|
|
return uniq(
|
|
tableSchema.reduceProperties((buf, schema) => {
|
|
if (schema['x-component'] === 'TableV2.Column') {
|
|
const s = schema.reduceProperties((buf, s) => {
|
|
const [name] = (s.name as string).split('.');
|
|
if (s['x-collection-field'] && associationFields.has(name)) {
|
|
return s;
|
|
}
|
|
return buf;
|
|
}, null);
|
|
if (s) {
|
|
// 关联字段和关联的关联字段
|
|
const [firstName] = s.name.split('.');
|
|
if (associationFields.has(s.name)) {
|
|
buf.push(s.name);
|
|
} else if (associationFields.has(firstName)) {
|
|
buf.push(firstName);
|
|
}
|
|
}
|
|
}
|
|
return buf;
|
|
}, []),
|
|
);
|
|
};
|
|
|
|
export const TableBlockProvider = (props) => {
|
|
const resourceName = props.resource;
|
|
const params = { ...props.params };
|
|
const appends = useAssociationNames(props.collection);
|
|
const fieldSchema = useFieldSchema();
|
|
const { getCollection, getCollectionField } = useCollectionManager();
|
|
const collection = getCollection(props.collection);
|
|
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
|
|
if (props.dragSort) {
|
|
params['sort'] = ['sort'];
|
|
}
|
|
let childrenColumnName = 'children';
|
|
if (collection?.tree && treeTable !== false) {
|
|
if (resourceName.includes('.')) {
|
|
const f = getCollectionField(resourceName);
|
|
if (f?.treeChildren) {
|
|
childrenColumnName = f.name;
|
|
params['tree'] = true;
|
|
}
|
|
} else {
|
|
const f = collection.fields.find((f) => f.treeChildren);
|
|
if (f) {
|
|
childrenColumnName = f.name;
|
|
}
|
|
params['tree'] = true;
|
|
}
|
|
}
|
|
if (!Object.keys(params).includes('appends')) {
|
|
params['appends'] = appends;
|
|
}
|
|
const form = useMemo(() => createForm(), [treeTable]);
|
|
return (
|
|
<SchemaComponentOptions scope={{ treeTable }}>
|
|
<FormContext.Provider value={form}>
|
|
<BlockProvider {...props} params={params}>
|
|
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
|
|
</BlockProvider>
|
|
</FormContext.Provider>
|
|
</SchemaComponentOptions>
|
|
);
|
|
};
|
|
|
|
export const useTableBlockContext = () => {
|
|
return useContext(TableBlockContext);
|
|
};
|
|
|
|
export const useTableBlockProps = () => {
|
|
const field = useField<ArrayField>();
|
|
const fieldSchema = useFieldSchema();
|
|
const ctx = useTableBlockContext();
|
|
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
|
|
const { getDataBlocks } = useFilterBlock();
|
|
|
|
useEffect(() => {
|
|
if (!ctx?.service?.loading) {
|
|
field.value = ctx?.service?.data?.data;
|
|
field.data = field.data || {};
|
|
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
|
|
field.componentProps.pagination = field.componentProps.pagination || {};
|
|
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
|
|
field.componentProps.pagination.total = ctx?.service?.data?.meta?.count;
|
|
field.componentProps.pagination.current = ctx?.service?.data?.meta?.page;
|
|
}
|
|
}, [ctx?.service?.loading]);
|
|
return {
|
|
childrenColumnName: ctx.childrenColumnName,
|
|
loading: ctx?.service?.loading,
|
|
showIndex: ctx.showIndex,
|
|
dragSort: ctx.dragSort,
|
|
rowKey: ctx.rowKey || 'id',
|
|
pagination:
|
|
ctx?.params?.paginate !== false
|
|
? {
|
|
defaultCurrent: ctx?.params?.page || 1,
|
|
defaultPageSize: ctx?.params?.pageSize,
|
|
}
|
|
: false,
|
|
onRowSelectionChange(selectedRowKeys) {
|
|
console.log(selectedRowKeys);
|
|
ctx.field.data = ctx?.field?.data || {};
|
|
ctx.field.data.selectedRowKeys = selectedRowKeys;
|
|
},
|
|
async onRowDragEnd({ from, to }) {
|
|
await ctx.resource.move({
|
|
sourceId: from[ctx.rowKey || 'id'],
|
|
targetId: to[ctx.rowKey || 'id'],
|
|
});
|
|
ctx.service.refresh();
|
|
},
|
|
onChange({ current, pageSize }, filters, sorter) {
|
|
let sort = sorter.order
|
|
? sorter.order === `ascend`
|
|
? [sorter.field]
|
|
: [`-${sorter.field}`]
|
|
: globalSort || ctx.service.params?.[0]?.sort;
|
|
ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort });
|
|
},
|
|
onClickRow(record, setSelectedRow, selectedRow) {
|
|
const { targets, uid } = findFilterTargets(fieldSchema);
|
|
const dataBlocks = getDataBlocks();
|
|
|
|
// 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错
|
|
if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) {
|
|
// 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。
|
|
// 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。
|
|
setSelectedRow((prev) => (prev.length ? [] : prev));
|
|
return;
|
|
}
|
|
|
|
const value = [record[ctx.rowKey]];
|
|
|
|
dataBlocks.forEach((block) => {
|
|
const target = targets.find((target) => target.uid === block.uid);
|
|
if (!target) return;
|
|
|
|
const param = block.service.params?.[0] || {};
|
|
// 保留原有的 filter
|
|
const storedFilter = block.service.params?.[1]?.filters || {};
|
|
|
|
if (selectedRow.includes(record[ctx.rowKey])) {
|
|
delete storedFilter[uid];
|
|
} else {
|
|
storedFilter[uid] = {
|
|
$and: [
|
|
{
|
|
[target.field || ctx.rowKey]: {
|
|
[target.field ? '$in' : '$eq']: value,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const mergedFilter = mergeFilter([
|
|
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
|
|
block.defaultFilter,
|
|
]);
|
|
|
|
return block.doFilter(
|
|
{
|
|
...param,
|
|
page: 1,
|
|
filter: mergedFilter,
|
|
},
|
|
{ filters: storedFilter },
|
|
);
|
|
});
|
|
|
|
// 更新表格的选中状态
|
|
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
|
|
},
|
|
};
|
|
};
|