feat: add association tab

This commit is contained in:
chenos 2020-12-25 16:15:58 +08:00
parent 32774657d7
commit 629a4de173
13 changed files with 195 additions and 49 deletions

View File

@ -2,6 +2,7 @@ import { TableOptions } from '@nocobase/database';
export default { export default {
title: '示例', title: '示例',
name: 'examples',
showInDataMenu: true, showInDataMenu: true,
// createdBy: true, // createdBy: true,
// updatedBy: true, // updatedBy: true,
@ -9,6 +10,7 @@ export default {
{ {
interface: 'string', interface: 'string',
title: '单行文本', title: '单行文本',
name: 'title',
component: { component: {
showInTable: true, showInTable: true,
showInDetail: true, showInDetail: true,
@ -163,6 +165,18 @@ export default {
showInForm: true, showInForm: true,
}, },
}, },
{
interface: 'linkTo',
type: 'belongsToMany',
title: '关联1',
target: 'examples',
labelField: 'title',
component: {
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{ {
interface: 'time', interface: 'time',
title: '时间', title: '时间',

View File

@ -37,6 +37,9 @@ const api = Api.create({
}, },
}); });
api.resourcer.use(associated);
api.resourcer.registerActionHandlers({...actions.common, ...actions.associate});
api.resourcer.use(async (ctx: actions.Context, next) => { api.resourcer.use(async (ctx: actions.Context, next) => {
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, ''); const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
// console.log('user check', ctx.action.params.actionName); // console.log('user check', ctx.action.params.actionName);
@ -59,8 +62,8 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
}); });
api.resourcer.use(async (ctx: actions.Context, next) => { api.resourcer.use(async (ctx: actions.Context, next) => {
const { resourceName, fields = {} } = ctx.action.params; const { resourceField, resourceName, fields = {} } = ctx.action.params;
const table = ctx.db.getTable(resourceName); const table = ctx.db.getTable(resourceField ? resourceField.options.target : resourceName);
// ctx.state.developerMode = {[Op.not]: null}; // ctx.state.developerMode = {[Op.not]: null};
ctx.state.developerMode = false; ctx.state.developerMode = false;
if (table && table.hasField('developerMode') && ctx.state.developerMode === false) { if (table && table.hasField('developerMode') && ctx.state.developerMode === false) {
@ -89,13 +92,13 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
}); });
api.resourcer.use(async (ctx: actions.Context, next) => { api.resourcer.use(async (ctx: actions.Context, next) => {
const { resourceName, viewName, filter } = ctx.action.params; const { resourceField, resourceName, viewName, filter } = ctx.action.params;
// TODO: 需要补充默认视图的情况 // TODO: 需要补充默认视图的情况
let view: any; let view: any;
if (viewName) { if (viewName) {
view = await ctx.db.getModel('views').findOne({ view = await ctx.db.getModel('views').findOne({
where: { where: {
collection_name: resourceName, collection_name: resourceField ? resourceField.options.target : resourceName,
name: viewName, name: viewName,
} }
}); });
@ -109,9 +112,6 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
await next(); await next();
}); });
api.resourcer.use(associated);
api.resourcer.registerActionHandlers({...actions.common, ...actions.associate});
api.registerPlugin('plugin-collections', [path.resolve(__dirname, '../../../plugin-collections'), {}]); api.registerPlugin('plugin-collections', [path.resolve(__dirname, '../../../plugin-collections'), {}]);
api.registerPlugin('plugin-pages', [path.resolve(__dirname, '../../../plugin-pages'), {}]); api.registerPlugin('plugin-pages', [path.resolve(__dirname, '../../../plugin-pages'), {}]);
api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-users'), {}]); api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-users'), {}]);

View File

@ -6,12 +6,13 @@ export function Create(props) {
console.log(props); console.log(props);
const { title, viewName, collection_name } = props.schema; const { title, viewName, collection_name } = props.schema;
const { activeTab = {}, item = {}, associatedName, associatedKey } = props; const { activeTab = {}, item = {}, associatedName, associatedKey } = props;
const { association } = activeTab; const { associationField } = activeTab;
const params = {}; const params = {};
if (association) { if (associationField && associationField.target) {
params['resourceName'] = association; params['resourceName'] = associationField.name;
params['resourceTarget'] = associationField.target;
params['associatedName'] = associatedName; params['associatedName'] = associatedName;
params['associatedKey'] = associatedKey; params['associatedKey'] = associatedKey;
} else { } else {

View File

@ -8,12 +8,13 @@ export function Filter(props) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { title, viewName, collection_name } = props.schema; const { title, viewName, collection_name } = props.schema;
const { activeTab = {}, item = {}, associatedName, associatedKey } = props; const { activeTab = {}, item = {}, associatedName, associatedKey } = props;
const { association } = activeTab; const { associationField } = activeTab;
const params = {}; const params = {};
if (association) { if (associationField && associationField.target) {
params['resourceName'] = association; params['resourceName'] = associationField.target;
params['resourceTarget'] = associationField.target;
params['associatedName'] = associatedName; params['associatedName'] = associatedName;
params['associatedKey'] = associatedKey; params['associatedKey'] = associatedKey;
} else { } else {

View File

@ -5,12 +5,13 @@ import ViewFactory from '@/components/views';
export function Update(props) { export function Update(props) {
const { title, viewName, resourceName, collection_name } = props.schema; const { title, viewName, resourceName, collection_name } = props.schema;
const { resourceKey, activeTab = {}, item = {} } = props; const { resourceKey, activeTab = {}, item = {} } = props;
const { association } = activeTab; const { associationField } = activeTab;
const params = {}; const params = {};
if (association) { if (associationField && associationField.target) {
params['resourceName'] = association; params['resourceName'] = associationField.target;
params['resourceTarget'] = associationField.target;
params['associatedName'] = resourceName; params['associatedName'] = resourceName;
params['associatedKey'] = item.itemId; params['associatedKey'] = item.itemId;
} else { } else {

View File

@ -14,18 +14,34 @@ import api from '@/api-client';
import { Spin } from '@nocobase/client' import { Spin } from '@nocobase/client'
function RemoteSelectComponent(props) { function RemoteSelectComponent(props) {
const { value, onChange, disabled, resourceName, associatedKey, labelField, valueField } = props; const { value, onChange, disabled, resourceName, associatedKey, filter, labelField, valueField = 'id', objectValue } = props;
const { data = [], loading = true } = useRequest(() => { const { data = [], loading = true } = useRequest(() => {
return api.resource(resourceName).list({ return api.resource(resourceName).list({
associatedKey, associatedKey,
filter,
}); });
}, { }, {
refreshDeps: [resourceName, associatedKey] refreshDeps: [resourceName, associatedKey]
}); });
return ( return (
<> <>
<Select disabled={disabled} notFoundContent={loading ? <Spin/> : undefined} allowClear loading={loading} value={value} onChange={onChange}> <Select
{!loading && data.map(item => (<Select.Option value={item[valueField]}>{item[labelField]}</Select.Option>))} disabled={disabled}
notFoundContent={loading ? <Spin/> : undefined}
allowClear
loading={loading}
value={value && typeof value === 'object' ? value[valueField] : value}
onChange={(value, option) => {
if (value === null || typeof value === 'undefined') {
onChange(undefined);
return;
}
// @ts-ignore
const item = option.item;
onChange(objectValue ? item : value);
}}
>
{!loading && data.map(item => (<Select.Option item={item} value={item[valueField]}>{item[labelField]}</Select.Option>))}
</Select> </Select>
</> </>
); );

View File

@ -5,13 +5,14 @@ import { useRequest, request, Spin } from '@nocobase/client';
export function CollectionTabPane(props) { export function CollectionTabPane(props) {
const { loading, pageInfo = {}, activeTab = {}, item = {} } = props; const { loading, pageInfo = {}, activeTab = {}, item = {} } = props;
const { viewName, association, collection_name, field = {} } = activeTab; const { viewName, associationField = {}, collection_name, field = {} } = activeTab;
const { sourceKey = 'id' } = field; const { name, target, sourceKey = 'id' } = associationField || {};
const params = {}; const params = {};
if (association) { if (target) {
params['resourceName'] = association; params['resourceName'] = name;
params['resourceTarget'] = target;
params['associatedName'] = collection_name; params['associatedName'] = collection_name;
params['associatedKey'] = pageInfo[sourceKey] || item.itemId; params['associatedKey'] = pageInfo[sourceKey] || item.itemId;
} else { } else {

View File

@ -130,7 +130,7 @@ function toFlat(items = []): Array<any> {
export function DataSourceField(props: any) { export function DataSourceField(props: any) {
const { schema: { dataSource = [] }, value } = props; const { schema: { dataSource = [] }, value } = props;
const items = toFlat(dataSource); const items = toFlat(dataSource);
console.log(items); // console.log(items);
if (isEmpty(value)) { if (isEmpty(value)) {
return null; return null;
} }
@ -243,7 +243,7 @@ export function AttachmentFieldItem(props: any) {
exclude: ['.png', '.jpg', '.jpeg', '.gif'] exclude: ['.png', '.jpg', '.jpeg', '.gif']
}) })
return ( return (
<a className={'attachment-field-item'} target={'_blank'} href={url}> <a onClick={(e) => e.stopPropagation()} className={'attachment-field-item'} target={'_blank'} href={url}>
<img style={{marginRight: 5}} height={20} alt={title} title={title} src={img}/> <img style={{marginRight: 5}} height={20} alt={title} title={title} src={img}/>
</a> </a>
); );

View File

@ -36,7 +36,7 @@ export const DrawerForm = forwardRef((props: any, ref) => {
const [resourceKey, setResourceKey] = useState(props.resourceKey); const [resourceKey, setResourceKey] = useState(props.resourceKey);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const name = associatedName ? `${associatedName}.${resourceName}` : resourceName; const name = associatedName ? `${associatedName}.${resourceName}` : resourceName;
const { data, run, loading } = useRequest((resourceKey) => { const { data = {}, run, loading } = useRequest((resourceKey) => {
setResourceKey(resourceKey); setResourceKey(resourceKey);
return api.resource(name).get({ return api.resource(name).get({
resourceKey, resourceKey,
@ -89,7 +89,8 @@ export const DrawerForm = forwardRef((props: any, ref) => {
<SchemaForm <SchemaForm
colon={true} colon={true}
layout={'vertical'} layout={'vertical'}
initialValues={data} // 暂时先这么处理,如果有 associatedKey 注入表单里
initialValues={{associatedKey, ...data}}
actions={actions} actions={actions}
schema={{ schema={{
type: 'object', type: 'object',

View File

@ -45,6 +45,7 @@ export default function ViewFactory(props: ViewProps) {
associatedName, associatedName,
associatedKey, associatedKey,
resourceName, resourceName,
resourceTarget,
viewName, viewName,
mode, mode,
reference, reference,
@ -59,9 +60,9 @@ export default function ViewFactory(props: ViewProps) {
mode mode
}, },
}; };
return api.resource(resourceName).getView(params); return api.resource(resourceTarget||resourceName).getView(params);
}, { }, {
refreshDeps: [associatedName, resourceName, viewName], refreshDeps: [associatedName, resourceTarget, resourceName, viewName],
}); });
if (loading) { if (loading) {
return <Spin/>; return <Spin/>;

View File

@ -64,23 +64,114 @@ export default {
showInDetail: true, showInDetail: true,
showInForm: true, showInForm: true,
"x-linkages": [ "x-linkages": [
// {
// "type": "value:visible",
// "target": "association",
// "condition": "{{ $self.value === 'association' }}"
// },
{ {
"type": "value:visible", type: "value:visible",
"target": "association", target: "associationField",
"condition": "{{ $self.value === 'association' }}" condition: "{{ $self.value === 'association' }}"
},
// {
// type: "value:schema",
// target: "association",
// condition: "{{ $self.value === 'association' }}",
// schema: {
// "x-component-props": {
// "associatedKey": "{{ $form.values && $form.values.associatedKey }}"
// },
// },
// },
{
type: "value:schema",
target: "associationField",
condition: "{{ $self.value === 'association' }}",
schema: {
"x-component-props": {
associatedKey: "{{ $form.values && $form.values.associatedKey }}"
},
},
},
],
},
},
// {
// interface: 'string',
// type: 'string',
// name: 'association',
// title: '相关数据',
// component: {
// type: 'remoteSelect',
// showInDetail: true,
// showInForm: true,
// 'x-component-props': {
// resourceName: 'collections.fields',
// labelField: 'title',
// valueField: 'name',
// filter: {
// interface: 'linkTo',
// },
// },
// },
// },
{
interface: 'linkTo',
type: 'belongsTo',
name: 'associationField',
target: 'fields',
title: '相关数据表',
labelField: 'title',
// valueField: 'name',
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
// valueField: 'name',
objectValue: true,
filter: {
interface: 'linkTo',
},
},
"x-linkages": [
{
type: "value:visible",
target: "viewName",
condition: "{{ !!$self.value }}"
},
{
type: "value:schema",
target: "viewName",
condition: "{{ !!$self.value }}",
schema: {
"x-component-props": {
associatedKey: "{{ $self.value.target }}"
},
},
}, },
], ],
}, },
}, },
{ {
interface: 'string', interface: 'string',
type: 'virtual', type: 'string',
name: 'association', name: 'viewName',
title: '相关数据表', title: '视图',
labelField: 'title',
// valueField: 'name',
component: { component: {
type: 'drawerSelect', type: 'remoteSelect',
showInDetail: true, showInDetail: true,
showInForm: true, showInForm: true,
'x-component-props': {
resourceName: 'collections.views',
labelField: 'title',
valueField: 'name',
},
}, },
}, },
{ {

View File

@ -166,6 +166,7 @@ export class CollectionModel extends BaseModel {
} }
const Model = this.database.getModel(key); const Model = this.database.getModel(key);
let ids = []; let ids = [];
const View = this.database.getModel('views');
for (const index in data[key]) { for (const index in data[key]) {
if (key === 'fields') { if (key === 'fields') {
ids = await Field.import(data[key], { ids = await Field.import(data[key], {
@ -204,6 +205,20 @@ export class CollectionModel extends BaseModel {
}, options); }, options);
} }
if (model) { if (model) {
if (key === 'tabs') {
let associationField;
if (item.association) {
associationField = await Field.findOne({
where: {
name: item.association,
collection_name: collection.name,
},
});
await model.updateAssociations({
associationField: associationField.id,
});
}
}
ids.push(model.id); ids.push(model.id);
} }
} }

View File

@ -16,13 +16,17 @@ export default async (ctx, next) => {
plain: true, plain: true,
}); });
collection.setDataValue('defaultViewName', defaultView.get('name')); collection.setDataValue('defaultViewName', defaultView.get('name'));
const tabs = await collection.getTabs({ const options = Tab.parseApiJson({
where: { filter: {
enabled: true, enabled: true,
developerMode: ctx.state.developerMode, developerMode: ctx.state.developerMode,
}, },
order: [['sort', 'asc']], fields: {
}) as Model[]; appends: ['associationField'],
},
sort: ['sort'],
});
const tabs = await collection.getTabs(options) as Model[];
const tabItems = []; const tabItems = [];
for (const tab of tabs) { for (const tab of tabs) {
const itemTab = { const itemTab = {
@ -31,15 +35,15 @@ export default async (ctx, next) => {
if (itemTab.type === 'details' && !itemTab.viewName) { if (itemTab.type === 'details' && !itemTab.viewName) {
itemTab.viewName = 'details'; itemTab.viewName = 'details';
} }
if (itemTab.type == 'association') { // if (itemTab.type == 'association') {
itemTab.field = await collection.getFields({ // itemTab.field = await collection.getFields({
where: { // where: {
name: itemTab.association, // name: itemTab.association,
}, // },
limit: 1, // limit: 1,
plain: true, // plain: true,
}); // });
} // }
tabItems.push(itemTab); tabItems.push(itemTab);
} }
ctx.body = { ctx.body = {