docs/docs/en/plugin-samples/schema-initializer/block-data.md
NocoBase has many Add block buttons for adding blocks to the interface. Some are related to data tables and are called data blocks Data Block, while others that are not related to data tables are called simple blocks Simple Block.
However, the existing block types may not meet our requirements, so we need to develop custom blocks according to our needs. This article focuses on explaining data blocks Data Block.
This example will create an Info block and add it to the Add block in Page, Table, and mobile.
This example is mainly to demonstrate the use of initializer. For more information about block extension, please refer to the Block Extension documentation.
The complete example code for this document can be found in plugin-samples.
<video width="100%" controls=""> <source src="https://static-docs.nocobase.com/20240522-182547.mp4" type="video/mp4" /> </video>Following the Write Your First Plugin documentation, if you don't have a project yet, you can create one first. If you already have one or have cloned the source code, you can skip this step.
yarn create nocobase-app my-nocobase-app -d postgres
cd my-nocobase-app
yarn install
yarn nocobase install
Then initialize a plugin and add it to the system:
yarn pm create @nocobase-sample/plugin-initializer-block-data
yarn pm enable @nocobase-sample/plugin-initializer-block-data
Then start the project:
yarn dev
After logging in, visit http://localhost:13000/admin/pm/list/local/ to see that the plugin has been installed and enabled.
Before implementing this example, we need to understand some basic knowledge:
.
├── client # Client plugin
│ ├── initializer # Initializer
│ ├── component # Block component
│ ├── index.tsx # Client plugin entry
│ ├── locale.ts # Multi-language utility function
│ ├── constants.ts # Constants
│ ├── schema # Schema
│ └── settings # Schema Settings
├── locale # Multi-language files
│ ├── en-US.json # English
│ └── zh-CN.json # Chinese
├── index.ts # Server plugin entry
└── server # Server plugin
First, we need to define the block name, which will be used in various places.
We create packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/constants.ts:
export const BlockName = 'Info';
export const BlockNameLowercase = BlockName.toLowerCase();
This example is about an Info block component with the following specific requirements:
First, we create packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/component/Info.tsx file with the following content:
import React, { FC } from 'react';
import { withDynamicSchemaProps } from '@nocobase/client'
import { BlockName } from '../constants';
export interface InfoProps {
collectionName: string;
data?: any[];
loading?: boolean;
}
export const Info: FC<InfoProps> = withDynamicSchemaProps(({ collectionName, data }) => {
return <div>
<div>collection: {collectionName}</div>
<div>data list: <pre>{JSON.stringify(data, null, 2)}</pre></div>
</div>
}, { displayName: BlockName })
The Info component is essentially a functional component wrapped by withDynamicSchemaProps. withDynamicSchemaProps is a higher-order component used to handle dynamic properties in Schema.
If we ignore withDynamicSchemaProps, Info is just a simple functional component.
Then export it in packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/component/index.ts:
export * from './Info';
We need to register Info to the system through the plugin.
import { Plugin } from '@nocobase/client';
import { Info } from './component';
export class PluginInitializerBlockDataClient extends Plugin {
async load() {
this.app.addComponents({ Info })
}
}
export default PluginInitializerBlockDataClient;
There are 2 ways to verify components:
Info component to check if it meets the requirementsyarn doc plugins/@nocobase-sample/plugin-initializer-block-data, and verify if it meets the requirements by writing documentation examples (TODO)We use temporary page verification as an example. We create a new page and add one or more Info components according to property parameters to check if they meet the requirements.
import React from 'react';
import { Plugin } from '@nocobase/client';
import { Info } from './component';
export class PluginInitializerBlockDataClient extends Plugin {
async load() {
this.app.addComponents({ Info })
this.app.router.add('admin.info-component', {
path: '/admin/info-component',
Component: () => {
return <>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<Info collectionName='test' data={[{ id: 1 }, { id: 2 }]} />
</div>
</>
}
})
}
}
export default PluginInitializerBlockDataClient;
Then visit http://localhost:13000/admin/info-component to see the corresponding test page content.
After verification, the test page needs to be deleted.
NocoBase's dynamic pages are all rendered through Schema, so we need to define a Schema, which will be used later to add the Info block to the interface. Before implementing this section, we need to understand some basic knowledge:
We create packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/schema/index.ts file:
import { useCollection, useDataBlockRequest } from "@nocobase/client";
import { InfoProps } from "../component";
import { BlockName, BlockNameLowercase } from "../constants";
export function useInfoProps(): InfoProps {
const collection = useCollection();
const { data, loading } = useDataBlockRequest<any[]>();
return {
collectionName: collection.name,
data: data?.data,
loading: loading
}
}
export function getInfoSchema({ dataSource = 'main', collection }) {
return {
type: 'void',
'x-decorator': 'DataBlockProvider',
'x-decorator-props': {
dataSource,
collection,
action: 'list',
},
'x-component': 'CardItem',
"x-toolbar": "BlockSchemaToolbar",
properties: {
[BlockNameLowercase]: {
type: 'void',
'x-component': BlockName,
'x-use-component-props': 'useInfoProps',
}
}
}
}
There are 2 points to explain here:
getInfoSchema(): The reason for defining it as a function is that dataSource and collection are dynamic and determined by the clicked data tableuseInfoProps(): Used to handle the dynamic properties of the Info component, and because it needs to be stored in the database, the value type here is a string type.getInfoSchema(): Returns the Schema of Info
type: 'void': Indicates no datax-decorator: 'DataBlockProvider': Data block provider, used to provide data. For more information about DataBlockProvider, please refer to DataBlockProviderx-decorator-props: Properties of DataBlockProvider
dataSource: Data sourcecollection: Data tableaction: 'list': Operation type, here it is list, to get the data listx-component: 'CardItem': CardItem component, currently all blocks are wrapped in cards, which provide styles, layouts, and drag-and-drop functionalityproperties: Child nodes
info: Info blockuseInfoProps(): Dynamic properties of the Info component
The above Schema is equivalent to the following React component:
<DataBlockProvider collection={collection} dataSource={dataSource} action='list'>
<CardItem>
<Info {...useInfoProps()} />
</CardItem>
</DataBlockProvider>
We need to register useInfoProps to the system, so that x-use-component-props can find the corresponding scope.
import { Plugin } from '@nocobase/client';
import { Info } from './component';
import { useInfoProps } from './schema';
export class PluginInitializerBlockDataClient extends Plugin {
async load() {
this.app.addComponents({ Info })
this.app.addScopes({ useInfoProps });
}
}
export default PluginInitializerBlockDataClient;
For more information about Scope, please refer to Global Registration of Component and Scope
Same as verifying components, we can verify the Schema by temporary page verification or documentation example verification. Here we use temporary page verification as an example:
import React from 'react';
import { Plugin, SchemaComponent, SchemaComponentOptions } from '@nocobase/client';
import { Info } from './component';
import { getInfoSchema, useInfoProps } from './schema';
export class PluginInitializerBlockDataClient extends Plugin {
async load() {
// ...
this.app.router.add('admin.info-schema', {
path: '/admin/info-schema',
Component: () => {
return <>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<SchemaComponent schema={{ properties: { test1: getInfoSchema({ collection: 'users' }) } }} />
</div>
<div style={{ marginTop: 20, marginBottom: 20 }}>
<SchemaComponent schema={{ properties: { test2: getInfoSchema({ collection: 'roles' }) } }} />
</div>
</>
}
})
}
}
export default PluginInitializerBlockDataClient;
components and scope required in Schema. For details, please refer to Local Registration of Component and ScopeWe visit http://localhost:13000/admin/info-schema to see the corresponding test page content.
After verification, the test page needs to be deleted.
We create packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/initializer/index.tsx file:
import React from 'react';
import { SchemaInitializerItemType, useSchemaInitializer } from '@nocobase/client'
import { CodeOutlined } from '@ant-design/icons';
import { getInfoSchema } from '../schema'
import { useT } from '../locale';
import { BlockName, BlockNameLowercase } from '../constants';
export const infoInitializerItem: SchemaInitializerItemType = {
name: BlockNameLowercase,
Component: 'DataBlockInitializer',
useComponentProps() {
const { insert } = useSchemaInitializer();
const t = useT();
return {
title: t(BlockName),
icon: <CodeOutlined />,
componentType: BlockName,
useTranslationHooks: useT,
onCreateBlockSchema({ item }) {
insert(getInfoSchema({ dataSource: item.dataSource, collection: item.name }))
},
};
},
}
The core to achieving the data block effect is DataBlockInitializer (documentation TODO).
infoInitializerItem:
Component: Unlike Adding Simple Block Simple Block which uses type, here we use Component. 2 ways to define are both acceptableuseComponentProps: Properties of DataBlockInitializer component
title: Titleicon: Icon, more icons can be found at Ant Design IconscomponentType: Component type, here it is InfoonCreateBlockSchema: Callback after clicking the data table
item: Information of the clicked data table
item.name: Data table nameitem.dataSource: Data source of the data table"x-toolbar": "BlockSchemaToolbar": BlockSchemaToolbar is used to display the current data table in the upper left corner, usually used with DataBlockProviderFor more information about Schema Initializer definitions, please refer to the Schema Initializer documentation.
A complete Block also needs to have Schema Settings, which are used to configure some properties and operations, but Schema Settings is not the focus of this example, so we only have a remove operation here.
We create packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/settings/index.ts file:
import { SchemaSettings } from "@nocobase/client";
import { BlockNameLowercase } from "../constants";
export const infoSettings = new SchemaSettings({
name: `blockSettings:${BlockNameLowercase}`,
items: [
{
type: 'remove',
name: 'remove',
componentProps: {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
}
}
]
})
import { Plugin } from '@nocobase/client';
import { infoSettings } from './settings';
export class PluginInitializerBlockDataClient extends Plugin {
async load() {
// ...
this.app.schemaSettingsManager.add(infoSettings)
}
}
export default PluginInitializerBlockDataClient;
We modify the getInfoSchema method in the packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/schema/index.ts file to set x-settings to infoSettings.name.
+ import { infoSettings } from "../settings";
export function getInfoSchema({ dataSource = 'main', collection }) {
return {
type: 'void',
'x-decorator': 'DataBlockProvider',
+ 'x-settings': infoSettings.name,
// ...
}
}
There are many Add block buttons in the system, but their names are different.
If we need to add it to the page-level Add block, we need to know the corresponding name. We can view the corresponding name through TODO method.
TODO
From the above figure, we can see that the page-level Add block corresponds to the name page:addBlock, and Data Blocks corresponds to the name dataBlocks.
Then we modify packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/index.tsx file:
import { Plugin } from '@nocobase/client';
import { Info } from './component';
import { useInfoProps } from './schema';
import { infoSettings } from './settings';
import { infoInitializerItem } from './initializer';
export class PluginDataBlockInitializerClient extends Plugin {
async load() {
this.app.addComponents({ Info });
this.app.addScopes({ useInfoProps });
this.app.schemaSettingsManager.add(infoSettings);
this.app.schemaInitializerManager.addItem('page:addBlock', `dataBlocks.${infoInitializerItem.name}`, infoInitializerItem)
}
}
export default PluginDataBlockInitializerClient;
<video controls width='100%' src="https://static-docs.nocobase.com/20240526170424_rec_.mp4"></video>
We need to add it not only to the page-level Add block, but also to the Add block in the Table block Add new modal.
According to the method of obtaining the page-level name, we get the Add block name of the Table block as popup:addNew:addBlock, and Data Blocks corresponds to the name dataBlocks.
Then modify packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/index.tsx file:
import { Plugin } from '@nocobase/client';
import { Plugin } from '@nocobase/client';
import { Info } from './component';
import { useInfoProps } from './schema';
import { infoSettings } from './settings';
import { infoInitializerItem } from './initializer';
export class PluginDataBlockInitializerClient extends Plugin {
async load() {
this.app.addComponents({ Info });
this.app.addScopes({ useInfoProps });
this.app.schemaSettingsManager.add(infoSettings);
this.app.schemaInitializerManager.addItem('page:addBlock', `dataBlocks.${infoInitializerItem.name}`, infoInitializerItem)
+ this.app.schemaInitializerManager.addItem('popup:addNew:addBlock', `dataBlocks.${infoInitializerItem.name}`, infoInitializerItem)
}
}
export default PluginDataBlockInitializerClient;
First, you need to activate the mobile plugin, refer to the Activate Plugin documentation.
We can add it to the mobile Add block. The method of obtaining the name will not be repeated here.
Then modify packages/plugins/@nocobase-sample/plugin-initializer-block-data/src/client/index.tsx file:
import { Plugin } from '@nocobase/client';
import { Info } from './component';
import { useInfoProps } from './schema';
import { infoSettings } from './settings';
import { infoInitializerItem } from './initializer';
export class PluginDataBlockInitializerClient extends Plugin {
async load() {
this.app.addComponents({ Info });
this.app.addScopes({ useInfoProps });
this.app.schemaSettingsManager.add(infoSettings);
this.app.schemaInitializerManager.addItem('page:addBlock', `dataBlocks.${infoInitializerItem.name}`, infoInitializerItem)
this.app.schemaInitializerManager.addItem('popup:addNew:addBlock', `dataBlocks.${infoInitializerItem.name}`, infoInitializerItem)
+ this.app.schemaInitializerManager.addItem('mobilePage:addBlock', `dataBlocks.${infoInitializerItem.name}`, infoInitializerItem)
}
}
export default PluginDataBlockInitializerClient;
If you need more Add block, you can continue to add them, just need to know the corresponding name.
According to the Build and Package Plugin documentation, we can package the plugin and upload it to the production environment.
If you cloned the source code, you need to execute a full build first to build the plugin's dependencies as well.
yarn build
If you used create-nocobase-app to create the project, you can directly execute:
yarn build @nocobase-sample/plugin-initializer-block-data --tar
This way you can see the storage/tar/@nocobase-sample/plugin-initializer-block-data.tar.gz file, and then install it by uploading.