feat: 移动端支持筛选

Reviewed-on: daoyoucloud/tachycode#638
This commit is contained in:
sealday 2024-04-10 15:54:01 +08:00
parent a5010cd74c
commit 8a2548f480
40 changed files with 1396 additions and 901 deletions

View File

@ -0,0 +1,7 @@
---
"@nocobase/plugin-mobile-client": patch
"@hera/plugin-mobile": patch
"@nocobase/client": patch
---
collapse

View File

@ -1,4 +1,4 @@
import Icon, { TableOutlined } from '@ant-design/icons';
import { TableOutlined } from '@ant-design/icons';
import { Divider, Empty, Input, MenuProps, Spin } from 'antd';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -13,6 +13,7 @@ import { useCompile } from '../../schema-component';
import { useSchemaTemplateManager } from '../../schema-templates';
import { useCollectionDataSourceItems } from '../utils';
import { DataSource } from '../../data-source';
import { Icon } from '../../icon';
const MENU_ITEM_HEIGHT = 40;
const STEP = 15;

View File

@ -2,9 +2,16 @@
"name": "@hera/plugin-mobile",
"version": "1.0.4",
"main": "dist/server/index.js",
"dependencies": {
"devDependencies": {
"@ant-design/icons": "5.x",
"@nocobase/schema": "workspace:*",
"antd-mobile": "^5.29.1"
"@types/lodash": "^4.17.0",
"antd": "^5.12.8",
"antd-mobile": "^5.33.1",
"antd-mobile-icons": "^0.3.0",
"classnames": "^2.3.1",
"lodash": "4.17.21",
"react-i18next": "^11.15.1"
},
"peerDependencies": {
"@nocobase/client": "workspace:*",

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Icon } from '@nocobase/client';
import NoticeSvg from './notice.svg';
import SwiperSvg from './swiper.svg';
import TabSearchSvg from './tab-search.svg';
export const loadBase64Icon = (base64: string) => () => (
<div dangerouslySetInnerHTML={{ __html: atob(base64.replace('data:image/svg+xml;base64,', '')) }} />
);
export const NoticeIcon = (props: any) => <Icon component={loadBase64Icon(NoticeSvg)} {...props} />;
export const SwiperIcon = (props: any) => <Icon component={loadBase64Icon(SwiperSvg)} {...props} />;
export const TabSearchIcon = (props: any) => <Icon component={loadBase64Icon(TabSearchSvg)} {...props} />;
Icon.register({
'notice-block': NoticeIcon,
'swiper-block': SwiperIcon,
'tab-search': TabSearchIcon,
});

View File

@ -0,0 +1,6 @@
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
<path
d="M338.773333 362.666667H170.666667v298.666666h105.578666l45.013334 0.021334 17.258666 0.426666L682.666667 809.258667V214.890667L338.773333 362.666667z m-17.578666-85.333334L768 85.333333v853.333334l-448-191.978667L85.333333 746.666667V277.333333h235.861334zM170.666667 661.333333v85.333334h85.333333v-85.333334H170.666667zM853.333333 298.666667h85.333334v426.666666h-85.333334z"
fill="#000000"
></path>
</svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
<path d="M0 219.428571h73.142857v585.142858H0V219.428571z m950.857143 0h73.142857v585.142858h-73.142857V219.428571z m-146.285714-146.285714a73.142857 73.142857 0 0 1 73.142857 73.142857v731.428572a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V146.285714a73.142857 73.142857 0 0 1 73.142857-73.142857h585.142858z m0 73.142857H219.428571v731.428572h585.142858V146.285714zM365.714286 694.857143a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z"></path>
</svg>

After

Width:  |  Height:  |  Size: 804 B

View File

@ -0,0 +1,5 @@
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
<path d="M1263.243636 908.939636l-156.485818-156.392727c25.6-37.050182 39.842909-79.685818 39.842909-128.093091 0-125.114182-102.4-227.514182-227.607272-227.514182a228.258909 228.258909 0 0 0-227.514182 227.514182c0 125.207273 102.4 227.607273 227.514182 227.607273 48.407273 0 91.042909-14.242909 128-39.842909l156.392727 156.392727a41.239273 41.239273 0 0 0 59.764363 0c19.921455-17.035636 19.921455-45.428364 0-59.671273zM921.786182 766.696727a140.846545 140.846545 0 0 1-142.242909-142.242909c0-79.592727 62.557091-142.149818 142.242909-142.149818 79.592727 0 142.242909 62.557091 142.242909 142.149818 0 79.685818-62.650182 142.242909-142.242909 142.242909z"></path>
<path d="M694.272 894.696727H111.150545v-512h583.121455a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.728727H111.150545V98.304h853.364364v156.392727a43.752727 43.752727 0 0 0 42.635636 42.635637 43.752727 43.752727 0 0 0 42.728728-42.635637V69.818182a57.064727 57.064727 0 0 0-56.878546-56.878546H82.757818A57.064727 57.064727 0 0 0 25.879273 69.818182v853.364363c0 31.278545 25.6 56.785455 56.785454 56.785455h611.607273a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.635637z"></path>
<path d="M637.393455 155.182545c-22.714182 0-42.635636 21.690182-42.635637 46.545455 0 24.762182 19.921455 46.545455 42.635637 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635637-46.545455 0-24.855273-19.921455-46.545455-42.635637-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485819c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485819z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,46 +1,16 @@
import { Plugin } from '@nocobase/client';
import { SwiperBlock, SwiperBlockInitializer } from './schema-initializer/SwiperBlockInitializer';
import { TabSearchBlock, TabSearchBlockInitializer } from './schema-initializer/TabSearchBlockInitializer';
import { NoticeBlock, NoticeBlockInitializer } from './schema-initializer/NoticeBlockInitializer';
export class PluginMobileClient extends Plugin {
async afterAdd() {}
import PluginBlock from './schma-block';
import PluginTabSearch from './schma-component/tab-search';
import './assets/svg/index';
class PluginMobileClient extends Plugin {
async beforeLoad() {}
async load() {
this.app.addComponents({
NoticeBlock,
NoticeBlockInitializer,
SwiperBlock,
SwiperBlockInitializer,
TabSearchBlock,
TabSearchBlockInitializer,
});
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.swiper', {
title: 'swiper',
name: 'swiper',
type: 'item',
Component: 'SwiperBlockInitializer',
});
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.notice', {
title: 'notice',
name: 'notice',
type: 'item',
Component: 'NoticeBlockInitializer',
});
async load() {}
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'filterBlocks', {
title: '{{t("Filter blocks")}}',
type: 'itemGroup',
children: [
{
name: 'tabSearch',
title: 'tabSearch',
Component: 'TabSearchBlockInitializer',
},
],
});
async afterAdd() {
this.pm.add(PluginBlock);
this.pm.add(PluginTabSearch);
}
}

View File

@ -0,0 +1,15 @@
import { useApp, tval as nTval, i18n } from '@nocobase/client';
const NAMESPACE = '@hera/plugin-mobile';
export const useTranslation = (): any => {
const { i18n } = useApp();
const t = (key: string, props = {}) => i18n.t(key, { ns: NAMESPACE, ...props });
return { t };
};
export const tval = (key: string) => nTval(key, { ns: NAMESPACE });
export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE });
}

View File

@ -1,96 +0,0 @@
import { ISchema } from '@nocobase/schema';
import {
Icon,
DataBlockInitializer,
useCollectionManager,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import React from 'react';
import { uid } from '@nocobase/utils/client';
import { NoticeBar } from 'antd-mobile';
const NoticeSvg = () => (
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
<path
d="M338.773333 362.666667H170.666667v298.666666h105.578666l45.013334 0.021334 17.258666 0.426666L682.666667 809.258667V214.890667L338.773333 362.666667z m-17.578666-85.333334L768 85.333333v853.333334l-448-191.978667L85.333333 746.666667V277.333333h235.861334zM170.666667 661.333333v85.333334h85.333333v-85.333334H170.666667zM853.333333 298.666667h85.333334v426.666666h-85.333334z"
fill="#000000"
></path>
</svg>
);
const NoticeIcon = (props: any) => <Icon type="" component={NoticeSvg} {...props} />;
export const NoticeBlock = () => {
return <NoticeBar content="月底冲量佣金上调" color="alert" />;
};
export const NoticeBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const cm = useCollectionManager();
const itemConfig = useSchemaInitializerItem();
return (
<DataBlockInitializer
{...itemConfig}
icon={<NoticeIcon />}
componentType={'Notice'}
onCreateBlockSchema={async ({ item }) => {
const collection = cm.getCollection(item.name);
const schema = createGridCardBlockSchema({
collection: item.name,
dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:gridCard',
});
insert(schema);
}}
/>
);
};
export const createGridCardBlockSchema = (options) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
actionInitializers = 'GridCardActionInitializers',
itemActionInitializers = 'GridCardItemActionInitializers',
collection,
association,
resource,
template,
dataSource,
settings,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'GridCard.Decorator',
'x-decorator-props': {
resource: resourceName,
collection,
association,
dataSource,
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
...others,
},
'x-component': 'BlockItem',
'x-component-props': {
useProps: '{{ useGridCardBlockItemProps }}',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': settings,
properties: {
[uid()]: {
type: 'void',
'x-component': 'NoticeBlock',
},
},
};
return schema;
};

View File

@ -1,120 +0,0 @@
import { ISchema } from '@nocobase/schema';
import {
DataBlockInitializer,
css,
Icon,
useCollectionManager,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import React from 'react';
import { uid } from '@nocobase/utils/client';
import { Swiper, Toast } from 'antd-mobile';
const SwiperSvg = () => (
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
<path d="M0 219.428571h73.142857v585.142858H0V219.428571z m950.857143 0h73.142857v585.142858h-73.142857V219.428571z m-146.285714-146.285714a73.142857 73.142857 0 0 1 73.142857 73.142857v731.428572a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V146.285714a73.142857 73.142857 0 0 1 73.142857-73.142857h585.142858z m0 73.142857H219.428571v731.428572h585.142858V146.285714zM365.714286 694.857143a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z m146.285714 0a36.571429 36.571429 0 1 1 0 73.142857 36.571429 36.571429 0 0 1 0-73.142857z"></path>
</svg>
);
const SwiperIcon = (props: any) => <Icon type="" component={SwiperSvg} {...props} />;
export const SwiperBlock = () => {
const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac'];
const items = colors.map((color, index) => (
<Swiper.Item key={index}>
<div
className={css`
height: 120px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
user-select: none;
`}
style={{ background: color }}
onClick={() => {
Toast.show(`你点击了卡片 ${index + 1}`);
}}
>
{index + 1}
</div>
</Swiper.Item>
));
return (
<Swiper loop autoplay>
{items}
</Swiper>
);
};
export const SwiperBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const cm = useCollectionManager();
const itemConfig = useSchemaInitializerItem();
return (
<DataBlockInitializer
{...itemConfig}
icon={<SwiperIcon />}
componentType={'Swiper'}
onCreateBlockSchema={async ({ item }) => {
const collection = cm.getCollection(item.name);
const schema = createGridCardBlockSchema({
collection: item.name,
dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:gridCard',
});
insert(schema);
}}
/>
);
};
const createGridCardBlockSchema = (options) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
actionInitializers = 'GridCardActionInitializers',
itemActionInitializers = 'GridCardItemActionInitializers',
collection,
association,
resource,
template,
dataSource,
settings,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'GridCard.Decorator',
'x-decorator-props': {
resource: resourceName,
collection,
association,
dataSource,
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
...others,
},
'x-component': 'BlockItem',
'x-component-props': {
useProps: '{{ useGridCardBlockItemProps }}',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': settings,
properties: {
[uid()]: {
type: 'void',
'x-component': 'SwiperBlock',
},
},
};
return schema;
};

View File

@ -1,144 +0,0 @@
import { ISchema } from '@nocobase/schema';
import {
DataBlockInitializer,
Icon,
css,
useCollectionManager,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import React, { useState } from 'react';
import { uid } from '@nocobase/utils/client';
import { Button, SearchBar, Tabs, Picker } from 'antd-mobile';
const TabSearchSvg = () => (
<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
<path d="M1263.243636 908.939636l-156.485818-156.392727c25.6-37.050182 39.842909-79.685818 39.842909-128.093091 0-125.114182-102.4-227.514182-227.607272-227.514182a228.258909 228.258909 0 0 0-227.514182 227.514182c0 125.207273 102.4 227.607273 227.514182 227.607273 48.407273 0 91.042909-14.242909 128-39.842909l156.392727 156.392727a41.239273 41.239273 0 0 0 59.764363 0c19.921455-17.035636 19.921455-45.428364 0-59.671273zM921.786182 766.696727a140.846545 140.846545 0 0 1-142.242909-142.242909c0-79.592727 62.557091-142.149818 142.242909-142.149818 79.592727 0 142.242909 62.557091 142.242909 142.149818 0 79.685818-62.650182 142.242909-142.242909 142.242909z"></path>
<path d="M694.272 894.696727H111.150545v-512h583.121455a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.728727H111.150545V98.304h853.364364v156.392727a43.752727 43.752727 0 0 0 42.635636 42.635637 43.752727 43.752727 0 0 0 42.728728-42.635637V69.818182a57.064727 57.064727 0 0 0-56.878546-56.878546H82.757818A57.064727 57.064727 0 0 0 25.879273 69.818182v853.364363c0 31.278545 25.6 56.785455 56.785454 56.785455h611.607273a43.752727 43.752727 0 0 0 42.728727-42.635636 43.752727 43.752727 0 0 0-42.728727-42.635637z"></path>
<path d="M637.393455 155.182545c-22.714182 0-42.635636 21.690182-42.635637 46.545455 0 24.762182 19.921455 46.545455 42.635637 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485818c22.714182 0 42.635636-21.783273 42.635637-46.545455 0-24.855273-19.921455-46.545455-42.635637-46.545455h-28.485818z m-199.121455 0c-22.714182 0-42.635636 21.690182-42.635636 46.545455 0 24.762182 19.921455 46.545455 42.635636 46.545455h28.485819c22.714182 0 42.635636-21.783273 42.635636-46.545455 0-24.855273-19.921455-46.545455-42.635636-46.545455h-28.485819z"></path>
</svg>
);
const TabSearchIcon = (props: any) => <Icon type="" component={TabSearchSvg} {...props} />;
export const TabSearchBlock = () => {
const [visible, setVisible] = useState(false);
const basicColumns = [
[
{ label: '福建', value: 'Mon' },
{ label: '上海', value: 'Wed' },
],
];
return (
<>
<div
className={css`
display: flex;
margin-bottom: 8px;
`}
>
<Tabs
className={css`
flex: 1;
`}
>
<Tabs.Tab title="水果" key="fruits" />
<Tabs.Tab title="蔬菜" key="vegetables" />
<Tabs.Tab title="动物" key="animals" />
</Tabs>
<Button onClick={() => setVisible(true)}></Button>
<Picker
columns={basicColumns}
visible={visible}
onClose={() => {
setVisible(false);
}}
/>
</div>
<div
className={css`
display: flex;
margin-bottom: 8px;
`}
>
<SearchBar
className={css`
flex: 1;
`}
placeholder="请输入内容"
/>
<Button></Button>
</div>
</>
);
};
export const TabSearchBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const cm = useCollectionManager();
const itemConfig = useSchemaInitializerItem();
return (
<DataBlockInitializer
{...itemConfig}
icon={<TabSearchIcon />}
componentType={'TabSearch'}
onCreateBlockSchema={async ({ item }) => {
const collection = cm.getCollection(item.name);
const schema = createGridCardBlockSchema({
collection: item.name,
dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:gridCard',
});
insert(schema);
}}
/>
);
};
export const createGridCardBlockSchema = (options) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
actionInitializers = 'GridCardActionInitializers',
itemActionInitializers = 'GridCardItemActionInitializers',
collection,
association,
resource,
template,
dataSource,
settings,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'GridCard.Decorator',
'x-decorator-props': {
resource: resourceName,
collection,
association,
dataSource,
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
...others,
},
'x-component': 'CardItem',
'x-component-props': {
useProps: '{{ useGridCardBlockItemProps }}',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': settings,
properties: {
[uid()]: {
type: 'void',
'x-component': 'TabSearchBlock',
},
},
};
return schema;
};

View File

@ -0,0 +1,29 @@
import { Plugin } from '@nocobase/client';
import { NoticeBlock, NoticeBlockInitializer } from './schema-initializer/NoticeBlockInitializer';
import { SwiperBlock, SwiperBlockInitializer } from './schema-initializer/SwiperBlockInitializer';
class PluginBlock extends Plugin {
async load() {
this.app.addComponents({
NoticeBlock,
NoticeBlockInitializer,
SwiperBlock,
SwiperBlockInitializer,
});
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.swiper', {
title: 'swiper',
name: 'swiper',
type: 'item',
Component: 'SwiperBlockInitializer',
});
this.app.schemaInitializerManager.addItem('MBlockInitializers', 'dataBlocks.notice', {
title: 'notice',
name: 'notice',
type: 'item',
Component: 'NoticeBlockInitializer',
});
}
}
export default PluginBlock;

View File

@ -0,0 +1,49 @@
import { ISchema } from '@nocobase/schema';
import { uid } from '@nocobase/utils/client';
export const createGridCardBlockSchema = (options, componentName) => {
const {
formItemInitializers = 'ReadPrettyFormItemInitializers',
actionInitializers = 'GridCardActionInitializers',
itemActionInitializers = 'GridCardItemActionInitializers',
collection,
association,
resource,
template,
dataSource,
settings,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'GridCard.Decorator',
'x-decorator-props': {
resource: resourceName,
collection,
association,
dataSource,
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
...others,
},
'x-component': 'BlockItem',
'x-component-props': {
useProps: '{{ useGridCardBlockItemProps }}',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': settings,
properties: {
[uid()]: {
type: 'void',
'x-component': componentName,
},
},
};
return schema;
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import {
DataBlockInitializer,
Icon,
useCollectionManager,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import { NoticeBar } from 'antd-mobile';
import { createGridCardBlockSchema } from '../schema-create/createGridCardBlockSchma';
import { NoticeIcon } from '../../assets/svg';
export const NoticeBlock = () => {
return <NoticeBar content="月底冲量佣金上调" color="alert" />;
};
export const NoticeBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const cm = useCollectionManager();
const itemConfig = useSchemaInitializerItem();
const onCreateBlockSchema = async ({ item }) => {
const collection = cm.getCollection(item.name);
const schema = createGridCardBlockSchema(
{
collection: item.name,
dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:gridCard',
},
'NoticeBlock',
);
insert(schema);
};
return (
<DataBlockInitializer
{...itemConfig}
icon={<Icon type="notice-block" />}
componentType={'Notice'}
onCreateBlockSchema={onCreateBlockSchema}
/>
);
};

View File

@ -0,0 +1,71 @@
import {
DataBlockInitializer,
Icon,
css,
useCollectionManager,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import React from 'react';
import { Swiper, Toast } from 'antd-mobile';
import { createGridCardBlockSchema } from '../schema-create/createGridCardBlockSchma';
import { SwiperIcon } from '../../assets/svg';
export const SwiperBlock = () => {
const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac'];
const items = colors.map((color, index) => (
<Swiper.Item key={index}>
<div
className={css`
height: 120px;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
user-select: none;
`}
style={{ background: color }}
onClick={() => {
Toast.show(`你点击了卡片 ${index + 1}`);
}}
>
{index + 1}
</div>
</Swiper.Item>
));
return (
<Swiper loop autoplay>
{items}
</Swiper>
);
};
export const SwiperBlockInitializer = () => {
const { insert } = useSchemaInitializer();
const cm = useCollectionManager();
const itemConfig = useSchemaInitializerItem();
const onCreateBlockSchema = async ({ item }) => {
const collection = cm.getCollection(item.name);
const schema = createGridCardBlockSchema(
{
collection: item.name,
dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:gridCard',
},
'SwiperBlock',
);
insert(schema);
};
return (
<DataBlockInitializer
{...itemConfig}
icon={<Icon type="swiper-block" />}
componentType={'Swiper'}
onCreateBlockSchema={onCreateBlockSchema}
/>
);
};

View File

@ -0,0 +1,14 @@
import React from 'react';
import { RecursionField } from '@nocobase/schema';
import { useTabSearchAction } from './TabSearchAction';
export const TabSearch = () => {
const { fieldSchema, Designer, render, filterProperties } = useTabSearchAction();
return (
<>
<Designer />
<RecursionField schema={fieldSchema} onlyRenderProperties filterProperties={filterProperties} />
<React.Fragment>{render()}</React.Fragment>
</>
);
};

View File

@ -0,0 +1,26 @@
import { useDesigner, useSchemaInitializerRender } from '@nocobase/client';
import { useFieldSchema } from '@nocobase/schema';
import { isTabSearchCollapsibleInputItem } from '../utils';
export const useTabSearchAction = () => {
const Designer = useDesigner();
const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']);
const filterProperties = (s) => {
if (!isTabSearchCollapsibleInputItem(s['x-component'])) {
return true;
} else {
// 保留第一个如果进入这个判断一定有值所以取0不会出错
return (
s === Object.values(s.parent.properties).filter((s) => isTabSearchCollapsibleInputItem(s['x-component']))[0]
);
}
};
return {
fieldSchema,
Designer,
render,
filterProperties,
};
};

View File

@ -0,0 +1,43 @@
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
import { ConfigProvider, Row } from 'antd';
import React from 'react';
import { IButton, IInput, ISelect } from './TabSearchCollapsibleInputItemChild';
import { useTabSearchCollapsibleInputItemAction } from './TabSearchCollapsibleInputItemAction';
export const TabSearchCollapsibleInputItem = withDynamicSchemaProps(
(props) => {
const { collectionField, Designer, options, value, onSelectChange, onInputChange, onButtonClick, customLabelKey } =
useTabSearchCollapsibleInputItemAction(props);
if (!collectionField) {
return null;
}
return (
<SortableItem>
<Designer />
<ConfigProvider
theme={{
token: { colorBgContainer: '#f9f9f9', colorBorder: 'none' },
components: {
Input: {
colorBorder: '#f9f9f9',
activeShadow: 'none',
hoverBorderColor: 'none',
activeBorderColor: 'none',
},
Divider: { colorSplit: '#c1c2c5' },
},
}}
>
<Row style={{ marginTop: '2vh', backgroundColor: '#f9f9f9' }}>
<ISelect options={options} onChange={onSelectChange} customLabelKey={customLabelKey} />
<IInput options={options} value={value} onChange={onInputChange} />
<IButton onClick={onButtonClick} />
</Row>
</ConfigProvider>
</SortableItem>
);
},
{ displayName: 'TabSearchCollapsibleInputItem' },
);

View File

@ -0,0 +1,46 @@
import { useMemo, useState } from 'react';
import { useFieldSchema } from '@nocobase/schema';
import { useTranslation } from '../../../../locale';
import { useCollection, useDesigner } from '@nocobase/client';
import { isTabSearchCollapsibleInputItem } from '../../utils';
import { useTabSearchCollapsibleInputItem } from './hooks';
export const useTabSearchCollapsibleInputItemAction = (props) => {
const { onSelected } = useTabSearchCollapsibleInputItem();
const fieldSchema = useFieldSchema();
const collection = useCollection();
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
const properties = fieldSchema.parent.properties;
const Designer = useDesigner();
const [value, setValue] = useState('');
const [customLabelKey, setCustomLabelKey] = useState(fieldSchema['name'] as string);
const options = Object.values(properties)
.map((option) => {
if (isTabSearchCollapsibleInputItem(option['x-component'])) {
return {
label: option['title'],
value: option['name'],
};
}
})
.filter(Boolean);
const onSelect = (value) => {
onSelected([value], customLabelKey);
};
const onSelectChange = (label) => setCustomLabelKey(label as string);
const onInputChange = (v) => {
const inputValue = v.target?.value || v;
setValue(inputValue);
if (inputValue === '') {
onSelect(inputValue);
}
};
const onButtonClick = () => {
onSelect(value);
};
return { collectionField, Designer, options, value, onSelectChange, onInputChange, onButtonClick, customLabelKey };
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Button, Col, Divider, Input, Select } from 'antd';
import { useTranslation } from '../../../../locale';
export const ISelect = (props) => {
const { options, onChange } = props;
return options.length > 1 ? (
<Col flex={1} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Select style={{ width: '100%' }} options={options} defaultValue={options[0].value} onChange={onChange} />
<Divider type="vertical" style={{ height: '70%' }} />
</Col>
) : null;
};
export const IInput = (props) => {
const { options, value, onChange } = props;
const { t } = useTranslation();
return (
<Col flex={options.length > 1 ? 3 : 4}>
<Input placeholder={t('Please enter search content')} value={value} onChange={onChange} />
</Col>
);
};
export const IButton = (props) => {
const { onClick } = props;
const { t } = useTranslation();
return (
<Col flex={1}>
<Button style={{ width: '100%', color: '#2c6eff' }} type="link" onClick={onClick}>
{t('Search')}
</Button>
</Col>
);
};

View File

@ -0,0 +1,28 @@
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
import { Grid } from 'antd-mobile';
import React from 'react';
import { useTabSearchCollapsibleInputItemAction } from './TabSearchCollapsibleInputItemAction';
import { IButton, IInput, ISelect } from './TabSearchCollapsibleInputMItemChild';
export const TabSearchCollapsibleInputMItem = withDynamicSchemaProps(
(props) => {
const { collectionField, Designer, options, value, onSelectChange, onInputChange, onButtonClick, customLabelKey } =
useTabSearchCollapsibleInputItemAction(props);
if (!collectionField) {
return null;
}
return (
<SortableItem>
<Designer />
<Grid columns={4} style={{ backgroundColor: '#f9f9f9', borderRadius: '5px', margin: '5px 0 0 0' }}>
<ISelect options={options} onChange={onSelectChange} customLabelKey={customLabelKey} />
<IInput options={options} value={value} onChange={onInputChange} />
<IButton onClick={onButtonClick} />
</Grid>
</SortableItem>
);
},
{ displayName: 'TabSearchCollapsibleInputItem' },
);

View File

@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { useTranslation } from '../../../../locale';
import { Grid, Divider, Picker, Input, Space } from 'antd-mobile';
import { DownOutline } from 'antd-mobile-icons';
export const ISelect = (props) => {
const { options, onChange, customLabelKey } = props;
const [visible, setVisible] = useState(false);
return options.length > 1 ? (
<Grid.Item
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}
onClick={() => {
setVisible(true);
}}
>
<span style={{ textAlign: 'center', width: '100%' }}>
{options.find((option) => option.value === customLabelKey).label}
</span>
<DownOutline />
<Picker
columns={[options]}
visible={visible}
onClose={() => {
setVisible(false);
}}
defaultValue={customLabelKey}
onConfirm={(v) => {
onChange(v[0]);
}}
/>
<Divider direction="vertical" style={{ height: '70%', margin: '0 0 0 5px' }} />
</Grid.Item>
) : null;
};
export const IInput = (props) => {
const { options, value, onChange } = props;
const { t } = useTranslation();
return (
<Grid.Item span={options.length > 1 ? 2 : 3}>
<Input
placeholder={t('Please enter search content')}
value={value}
onChange={onChange}
style={{ '--font-size': '12px', marginLeft: '5px' }}
/>
</Grid.Item>
);
};
export const IButton = (props) => {
const { onClick } = props;
const { t } = useTranslation();
return (
<Grid.Item onClick={onClick} style={{ color: '#2c6eff', textAlign: 'center', cursor: 'pointer' }}>
{t('Search')}
</Grid.Item>
);
};

View File

@ -0,0 +1,30 @@
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
import { ConfigProvider, Menu } from 'antd';
import React from 'react';
import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction';
export const TabSearchFieldItem = withDynamicSchemaProps(
(props) => {
const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props);
if (!collectionField) {
return null;
}
return (
<SortableItem>
<Designer />
<ConfigProvider
theme={{
components: {
Menu: { itemColor: '#a5a5a5', itemHoverColor: '#1d2ffff18' },
},
}}
>
<Menu items={items} mode="horizontal" onSelect={onSelect} defaultSelectedKeys={['all']} />
</ConfigProvider>
</SortableItem>
);
},
{ displayName: 'TabSearchFieldItem' },
);

View File

@ -0,0 +1,45 @@
import React from 'react';
import { MenuProps } from 'antd';
import { useCollection, useDesigner } from '@nocobase/client';
import { useFieldSchema } from '@nocobase/schema';
import { useTranslation } from '../../../../locale';
export const useTabSearchFieldItemAction = (props) => {
const { list, onSelected, valueKey: _valueKey, labelKey: _labelKey, filterKey } = props;
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const c = useCollection();
const collectionField = React.useMemo(() => c?.getField(fieldSchema.name as any), [c, fieldSchema.name]);
const Designer = useDesigner();
const valueKey = _valueKey || collectionField?.targetKey || 'id';
const labelKey = _labelKey || fieldSchema['x-component-props']?.fieldNames?.label || valueKey;
const onSelect = (itemKey) => {
const key = itemKey.keyPath?.[0] || itemKey;
onSelected([key], null, filterKey);
};
const fieldNames = {
title: labelKey || valueKey,
key: valueKey,
};
const items: MenuProps['items'] = [];
if (list?.length) {
list.forEach((value) => {
items.push({
label: value[fieldNames.title],
key: value[fieldNames.key],
});
});
}
items?.unshift({
label: t('all'),
key: 'all',
});
return {
collectionField,
Designer,
items,
onSelect,
};
};

View File

@ -0,0 +1,47 @@
import { useMemo } from 'react';
import { useCollection, useCollectionManager } from '@nocobase/client';
import { useFieldSchema } from '@nocobase/schema';
import _ from 'lodash';
import { useTabSearchCollapsibleInputItem } from './hooks';
import { canBeOptionalField } from '../../utils';
export const useTabSearchFieldItemProps = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const optionalFieldList = useOptionalFieldList();
const cm = useCollectionManager();
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
const { onSelected } = useTabSearchCollapsibleInputItem();
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
const fieldInterface = fieldSchema['x-component-props'].interface;
if (canBeOptionalField(fieldInterface)) {
const field = optionalFieldList.find((field) => field.name === fieldSchema.name);
const operatorMap = {
select: '$in',
multipleSelect: '$anyOf',
checkbox: '$in',
checkboxGroup: '$anyOf',
};
const _list = field?.uiSchema?.enum || [];
result.valueKey = 'value';
result.labelKey = 'label';
result.list = _list;
result.filterKey = `${field.name}.${operatorMap[field.interface]}`;
}
return {
list: result.list,
valueKey: result.valueKey,
labelKey: result.labelKey,
onSelected,
filterKey: result.filterKey,
};
};
const useOptionalFieldList = () => {
const collection = useCollection();
const currentFields = collection?.fields ?? [];
return currentFields.filter((field) => canBeOptionalField(field.interface) && field.uiSchema.enum);
};

View File

@ -0,0 +1,26 @@
import { SortableItem, withDynamicSchemaProps } from '@nocobase/client';
import React from 'react';
import { useTabSearchFieldItemAction } from './TabSearchFieldItemAction';
import { Tabs } from 'antd-mobile';
export const TabSearchFieldMItem = withDynamicSchemaProps(
(props) => {
const { collectionField, Designer, items, onSelect } = useTabSearchFieldItemAction(props);
if (!collectionField) {
return null;
}
return (
<SortableItem>
<Designer />
<Tabs style={{ '--title-font-size': '12px', color: '#a5a5a5' }} onChange={onSelect}>
{items.map((value) => (
<Tabs.Tab title={value.label} key={value.key} />
))}
</Tabs>
</SortableItem>
);
},
{ displayName: 'TabSearchFieldItem' },
);

View File

@ -0,0 +1,75 @@
// 关系字段类型
import { useEffect, useMemo } from 'react';
import {
useCollection,
useCollectionManager,
useDataSourceHeaders,
useFilterBlock,
useRequest,
} from '@nocobase/client';
import { useField, useFieldSchema } from '@nocobase/schema';
import _ from 'lodash';
import { useTabSearchCollapsibleInputItem } from './hooks';
import { canBeRelatedField } from '../../utils';
export const useTabSearchFieldItemRelatedProps = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const field = useField();
// TODO 这里需要替换成获取 source id 的方式,不过我们暂时没有用多数据源,就不着急
const blockProps = { dataSource: 'main' };
const headers = useDataSourceHeaders(blockProps?.dataSource);
const cm = useCollectionManager();
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
const collectionFieldName = collectionField?.name;
const fieldInterface = fieldSchema['x-component-props'].interface;
const result = { list: null, valueKey: '', labelKey: '', filterKey: '' };
const { onSelected } = useTabSearchCollapsibleInputItem();
result.valueKey = collectionField?.target ? cm.getCollection(collectionField.target)?.getPrimaryKey() : 'id';
result.labelKey = fieldSchema['x-component-props']?.fieldNames?.label || result.valueKey;
// eslint-disable-next-line prefer-const
const { data, run } = useRequest<{
data: { [key: string]: any }[];
}>(
{
headers,
resource: collectionField?.target,
action: 'list',
params: {
fields: [result.labelKey, result.valueKey],
pageSize: 200,
page: 1,
...field.componentProps?.params,
},
},
{
manual: true,
debounceWait: 300,
},
);
useEffect(() => {
if (canBeRelatedField(fieldInterface) && collectionField?.target) {
run();
}
}, [
result.labelKey,
result.valueKey,
JSON.stringify(field.componentProps?.params || {}),
canBeRelatedField(fieldInterface),
]);
if (!collectionField) {
return {};
}
result.filterKey = `${collectionFieldName}.${result.valueKey}.$in`;
result.list = data?.data || [];
return {
list: result.list,
valueKey: result.valueKey,
labelKey: result.labelKey,
onSelected,
filterKey: result.filterKey,
};
};

View File

@ -0,0 +1,76 @@
import { findFilterTargets, mergeFilter, useApp, useCollection, useFilterBlock } from '@nocobase/client';
import { useFieldSchema } from '@nocobase/schema';
import { useMemo } from 'react';
import _ from 'lodash';
export const useTabSearchCollapsibleInputItem = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
const { getDataBlocks } = useFilterBlock();
const collectionField = useMemo(() => collection?.getField(fieldSchema.name as any), [collection, fieldSchema.name]);
const onSelected = (value, customLabelKey?, filterKey?) => {
const { targets, uid } = findFilterTargets(fieldSchema);
getDataBlocks().forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const key = `${uid}${fieldSchema.name}`;
const param = block.service.params?.[0] || {};
if (!collectionField?.target && customLabelKey) {
filterKey = `${customLabelKey}.$includes`;
}
// 保留原有的 filter
let storedFilter = block.service.params?.[1]?.filters || {};
if (value.length) {
storedFilter[key] = {
[filterKey]: value,
};
} else {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[key];
}
const mergedFilter = mergeFilter([...Object.values(storedFilter), block.defaultFilter]);
if (value.length === 1 && value[0] === 'all') {
const currentKey = Object.keys(storedFilter[key]);
if (mergedFilter['$and']) {
currentKey.forEach((currentValue) => {
mergedFilter['$and'].forEach((value, index) => {
if (value[currentValue]) {
mergedFilter['$and'].splice(index, 1);
}
});
});
mergedFilter['$and'] = mergedFilter['$and'].filter(Boolean);
} else {
delete mergedFilter[currentKey[0]];
}
storedFilter[key] = {};
}
storedFilter = _.omitBy(
storedFilter,
(value) => value === null || value === undefined || (_.isObject(value) && _.isEmpty(value)),
);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
};
return {
onSelected,
};
};
export const useIsMobile = () => {
const fieldSchema = useFieldSchema();
const isMobile = Object.values(fieldSchema.root.properties).find((value) => value['x-component'] === 'MContainer');
return isMobile;
};

View File

@ -0,0 +1,33 @@
import { ISchema } from '@nocobase/schema';
import { uid } from '@nocobase/utils/client';
export const createTabSearchBlockSchema = (options: {
collectionName: string;
dataSource: string;
blockType: string;
}): ISchema => {
const { collectionName, dataSource, blockType } = options;
return {
type: 'void',
'x-decorator': 'TabSearchProvider',
'x-decorator-props': {
collection: collectionName,
dataSource,
blockType,
},
// 这个工具栏本来就没什么特别的作用,就是用于显示名称、模板名称,用这个没问题
'x-toolbar': 'BlockSchemaToolbar',
// FIXME 模板在当前这个 provider 下是不支持的,要么添加相关的代码,要么这个 settings 用自己的
'x-settings': 'blockSettings:filterCollapse',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
[uid()]: {
type: 'void',
'x-action': 'tabSearch',
'x-initializer': 'tabSearch:configureFields',
'x-component': 'TabSearch',
},
},
};
};

View File

@ -0,0 +1,24 @@
export const createTabSearchItemSchema = (options) => {
const { field, itemComponent, label, itemUseProps } = options;
return {
name: field.key,
title: field.uiSchema?.title,
Component: 'TabSearchFieldSchemaInitializerGadget',
schema: {
name: field.name,
title: field.uiSchema?.title,
type: 'void',
'x-toolbar': 'CollapseItemSchemaToolbar',
'x-settings': 'fieldSettings:TabSearchItem',
'x-component': itemComponent,
'x-use-component-props': itemUseProps,
'x-component-props': {
fieldNames: {
label,
},
interface: field.interface,
},
properties: {},
},
};
};

View File

@ -0,0 +1,54 @@
import { Plugin } from '@nocobase/client';
import { TabSearch } from './components/TabSearch';
import { TabSearchProvider } from './provider/TabSearchProvider';
import { TabSearchBlockInitializer } from './initializer/TabSearchBlockInitializer';
import { TabSearchFieldItem } from './components/field-item/TabSearchFieldItem';
import { TabSearchCollapsibleInputItem } from './components/field-item/TabSearchCollapsibleInputItem';
import { TabSearchFieldSchemaInitializerGadget } from './initializer/TabSearchFieldSchemaInitializerGadget';
import { tval } from '../../locale';
import { useTabSearchFieldItemRelatedProps } from './components/field-item/TabSerachFieldItemRelatedProps';
import { useTabSearchFieldItemProps } from './components/field-item/TabSearchFieldItemProps';
import { TabSearchFieldSchemaInitializer } from './initializer/TabSearchFieldSchemaInitializer';
import { TabSearchItemFieldSettings } from './settings/TabSearchItemFieldSettings';
import { TabSearchFieldMItem } from './components/field-item/TabSearchFieldMItem';
import { TabSearchCollapsibleInputMItem } from './components/field-item/TabSearchCollapsibleInputMItem';
class PluginTabSearch extends Plugin {
async load() {
this.app.addScopes({
useTabSearchFieldItemProps,
useTabSearchFieldItemRelatedProps,
});
this.app.addComponents({
TabSearch,
TabSearchProvider,
TabSearchBlockInitializer,
TabSearchFieldItem,
TabSearchFieldMItem,
TabSearchCollapsibleInputItem,
TabSearchCollapsibleInputMItem,
TabSearchFieldSchemaInitializerGadget,
});
this.app.schemaInitializerManager.add(TabSearchFieldSchemaInitializer);
this.schemaSettingsManager.add(TabSearchItemFieldSettings);
this.app.schemaInitializerManager.addItem('mobilePage:addBlock', 'filterBlocks', {
title: tval('Filter blocks'),
type: 'itemGroup',
children: [
{
name: 'tabSearch',
title: 'tabSearch',
Component: 'TabSearchBlockInitializer',
},
],
});
this.app.schemaInitializerManager.addItem('page:addBlock', 'filterBlocks.tabSearch', {
name: 'tabSearch',
title: 'tabSearch',
Component: 'TabSearchBlockInitializer',
});
}
}
export default PluginTabSearch;

View File

@ -0,0 +1,31 @@
import React from 'react';
import _ from 'lodash';
import { DataBlockInitializer, Icon, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client';
import { createTabSearchBlockSchema } from '../create/createTabSearchBlockSchema';
export const TabSearchBlockInitializer = (props) => {
const { filterCollections, onlyCurrentDataSource, hideChildrenIfSingleCollection } = props;
const itemConfig = useSchemaInitializerItem();
const { insert } = useSchemaInitializer();
const onCreateBlockSchema = async ({ item }) => {
const schema = createTabSearchBlockSchema({
dataSource: item.dataSource,
collectionName: item.collectionName || item.name,
blockType: 'filter',
});
insert(schema);
};
return (
<DataBlockInitializer
{...itemConfig}
onlyCurrentDataSource={onlyCurrentDataSource}
icon="tab-search" // FIXME 这里有 bug直接写字符串会出错
componentType={'TabSearch'}
filter={filterCollections}
hideChildrenIfSingleCollection={hideChildrenIfSingleCollection}
onCreateBlockSchema={onCreateBlockSchema}
/>
);
};

View File

@ -0,0 +1,70 @@
import { SchemaInitializer, SchemaInitializerItemType, useCollection, useCollectionManager } from '@nocobase/client';
import _ from 'lodash';
import { tval } from '../../../locale';
import { canBeOptionalField, canBeRelatedField, canBeSearchField } from '../utils';
import { createTabSearchItemSchema } from '../create/createTabSearchItemSchema';
import { useIsMobile } from '../components/field-item/hooks';
const textFieldsItem: SchemaInitializerItemType<any> = {
name: 'textFields',
type: 'itemGroup',
title: tval('Text fields'),
useChildren() {
const collection = useCollection();
const associatedFields = collection.fields;
const isMobield = useIsMobile();
const itemComponent = isMobield ? 'TabSearchCollapsibleInputMItem' : 'TabSearchCollapsibleInputItem';
const children = associatedFields
.map((field) => {
if (canBeSearchField(field.interface) && !field['isForeignKey']) {
const label = field.name;
return createTabSearchItemSchema({ field, itemComponent, label });
}
})
.filter(Boolean);
return children;
},
};
const choicesFieldsItem: SchemaInitializerItemType<any> = {
name: 'choicesFields',
type: 'itemGroup',
title: tval('Choices fields'),
useChildren() {
const collection = useCollection();
const optionalList = collection.fields;
const cm = useCollectionManager();
const isMobield = useIsMobile();
const itemComponent = isMobield ? 'TabSearchFieldMItem' : 'TabSearchFieldItem';
const optionalChildren = optionalList
.map((field) => {
const label = cm.getCollection(field.target)?.getPrimaryKey() || 'id';
if (canBeOptionalField(field.interface)) {
return createTabSearchItemSchema({
field,
itemComponent,
label,
itemUseProps: 'useTabSearchFieldItemProps',
});
} else if (canBeRelatedField(field.interface)) {
return createTabSearchItemSchema({
field,
itemComponent,
label,
itemUseProps: 'useTabSearchFieldItemRelatedProps',
});
}
})
.filter(Boolean);
return optionalChildren;
},
};
export const TabSearchFieldSchemaInitializer = new SchemaInitializer({
name: 'tabSearch:configureFields',
style: { marginTop: 16 },
icon: 'SettingOutlined',
title: tval('Configure fields'),
items: [textFieldsItem, choicesFieldsItem],
});

View File

@ -0,0 +1,26 @@
import {
SchemaInitializerSwitch,
useCurrentSchema,
useSchemaInitializer,
useSchemaInitializerItem,
} from '@nocobase/client';
import { merge } from '@nocobase/schema';
import React from 'react';
export const TabSearchFieldSchemaInitializerGadget = () => {
const itemConfig = useSchemaInitializerItem();
const { schema } = itemConfig;
const { exists, remove } = useCurrentSchema(schema.name, 'name', itemConfig.find, itemConfig.remove);
const { insert } = useSchemaInitializer();
const onClick = () => {
if (exists) {
return remove();
}
const s = merge(schema || {}, itemConfig.schema || {});
itemConfig?.schemaInitialize?.(s);
insert(s);
};
return <SchemaInitializerSwitch checked={exists} title={itemConfig.title} onClick={onClick} />;
};

View File

@ -0,0 +1,6 @@
import { DataBlockProvider } from '@nocobase/client';
import React from 'react';
export const TabSearchProvider = (props) => {
return <DataBlockProvider collection={props.collection}>{props.children}</DataBlockProvider>;
};

View File

@ -0,0 +1,152 @@
import {
SchemaSettings,
SchemaSettingsDataScope,
useCollection,
useCollectionManager,
useCompile,
useDesignable,
useFormBlockContext,
} from '@nocobase/client';
import { useTranslation } from '../../../locale';
import { useField, useFieldSchema } from '@nocobase/schema';
import _ from 'lodash';
import { isTabSearchCollapsibleInputItem } from '../utils';
export const TabSearchItemFieldSettings = new SchemaSettings({
name: 'fieldSettings:TabSearchItem',
items: [
{
name: 'decoratorOptions',
type: 'itemGroup',
hideIfNoChildren: true,
useComponentProps() {
const { t } = useTranslation();
return {
title: t('Generic properties'),
};
},
useChildren() {
return [
{
name: 'setTheDataScope',
Component: SchemaSettingsDataScope,
useComponentProps() {
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const cm = useCollectionManager();
const c = useCollection();
const collectionField =
c.getField(fieldSchema['name']) || cm.getCollectionField(fieldSchema['x-collection-field']);
const { dn } = useDesignable();
return {
collectionName: collectionField?.target,
defaultFilter: fieldSchema?.['x-component-props']?.params?.filter || {},
form: form,
onSubmit: ({ filter }) => {
_.set(field.componentProps, 'params', {
...field.componentProps?.params,
filter,
});
fieldSchema['x-component-props']['params'] = field.componentProps.params;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': fieldSchema['x-component-props'],
},
});
},
};
},
useVisible() {
const fieldSchema = useFieldSchema();
return !isTabSearchCollapsibleInputItem(fieldSchema['x-component']);
},
},
{
name: 'titleField',
type: 'select',
useComponentProps() {
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const cm = useCollectionManager();
const c = useCollection();
const collectionField =
c.getField(fieldSchema['name']) || cm.getCollectionField(fieldSchema['x-collection-field']);
const compile = useCompile();
const { dn } = useDesignable();
const targetFields = collectionField?.target ? cm.getCollectionFields(collectionField?.target) : [];
const options = targetFields
.filter((field) => !field?.target && field.type !== 'boolean')
.map((field) => ({
value: field?.name,
label: compile(field?.uiSchema?.title) || field?.name,
}));
const onTitleFieldChange = (label) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
const fieldNames = {
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
...fieldSchema['x-component-props']?.['fieldNames'],
label,
};
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames;
schema['x-component-props'] = fieldSchema['x-component-props'];
dn.emit('patch', {
schema,
});
dn.refresh();
};
return {
key: 'title-field',
title: t('Title field'),
options: options,
value: fieldSchema['x-component-props']?.fieldNames?.label,
onChange: onTitleFieldChange,
};
},
useVisible() {
const fieldSchema = useFieldSchema();
return !isTabSearchCollapsibleInputItem(fieldSchema['x-component']);
},
},
];
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'delete',
type: 'remove',
sort: 100,
useComponentProps() {
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { dn } = useDesignable();
return {
removeParentsIfNoChildren: true,
confirm: {
title: t('Delete field'),
},
breakRemoveOn: (s) => {
if (isTabSearchCollapsibleInputItem(fieldSchema['x-component'])) {
Object.values(s.properties).forEach((value) => {
if (isTabSearchCollapsibleInputItem(value['x-component']) && value.name !== fieldSchema.name) {
delete s.properties[value.name];
}
});
dn.emit('patch', { schema: s });
}
return s['x-component'] === 'TabSearch';
},
};
},
},
],
});

View File

@ -0,0 +1,25 @@
const canBeOptionalFields = ['select', 'multipleSelect', 'radioGroup', 'checkbox', 'checkboxGroup'];
const canBeRelatedFields = ['oho', 'obo', 'o2m', 'm2o', 'm2m'];
const canBeSearchFields = [
'input',
'textarea',
'phone',
'email',
'url',
'integer',
'number',
'percent',
'password',
'color',
'icon',
'formula',
];
export const isTabSearchCollapsibleInputItem = (component: string) =>
component === 'TabSearchCollapsibleInputItem' || component === 'TabSearchCollapsibleInputMItem';
export const canBeOptionalField = (_interface: string) => canBeOptionalFields.includes(_interface);
export const canBeRelatedField = (_interface: string) => canBeRelatedFields.includes(_interface);
export const canBeSearchField = (_interface: string) => canBeSearchFields.includes(_interface);

View File

@ -1,5 +1,13 @@
import { RecursionField, useField, useFieldSchema } from '@nocobase/schema';
import { ActionBarProvider, SortableItem, TabsContextProvider, css, cx, useDesigner } from '@nocobase/client';
import {
ActionBarProvider,
FilterBlockProvider,
SortableItem,
TabsContextProvider,
css,
cx,
useDesigner,
} from '@nocobase/client';
import { TabsProps } from 'antd';
import React, { useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
@ -131,7 +139,13 @@ const InternalPage: React.FC = (props) => {
);
};
export const MPage = InternalPage as unknown as typeof InternalPage & {
const InternalFilterablePage = (props) => (
<FilterBlockProvider>
<InternalPage {...props} />
</FilterBlockProvider>
);
export const MPage = InternalFilterablePage as unknown as typeof InternalPage & {
Designer: typeof PageDesigner;
};
MPage.Designer = PageDesigner;

File diff suppressed because it is too large Load Diff