Admin
Purpose
This is the place where you configure your enterprise level applications Administration interface. This bundle makes use of @bluelibs/x-ui
, so it is best if you familiarise yourself with it first.
This bundle uses Ant Design
to leverage its Admin interface. Allowing you to focus on creating Menu Routes that can be role-dependent. This enables Wordpress-like
functionality where external bundles that you just add to your Kernel
extend the menu nicely.
Install
npm i -S @bluelibs/x-ui-admin antd @ant-design/icons
kernel.addBundles([new XUIBundle(), new XUIAdminBundle()]);
UI
Let's explore a bit about the structure, by creating our first layout:
import { useUIComponents } from "@bluelibs/x-ui";
// This is to illustrate how you can have a custom layout:function Dashboard() { const Components = useUIComponents();
return ( <Components.AdminLayout content={}> <h1>Hello world!</h1> </Components.AdminLayout> );}
Now we need to create a route, and because we don't rely on strings for routing, we should read them from a separate place, and register them in our bundle:
import { IRoute } from "@bluelibs/x-ui";
export const DASHBOARD = { path: "/dashboard", component: Dashboard, // optionally require certain roles that you can define in a Roles enum roles: [Roles.ADMIN],
// You have the ability to create menu items from the route directly menu: { icon: RightOutlined, // Use any from: https://ant.design/components/icon/ key: "DASHBOARD", // a unique name label: "Dashboard", order: 0, // If you want it to hold priority. The menus will be sorted by order, otherwise, by the order they have been added into the menu },};
This is done only once, but don't forget to load your routes:
// In your bundle:import * as Routes from "./routes";
export class UIAppBundle extends Bundle { async function init() { const router = this.container.get(XRouter);
router.add(Routes); }}
If you want to nest menu items you have to specify inject
property:
export const DASHBOARD = { path: "/dashboard", component: Dashboard, // You have the ability to create menu items from the route directly menu: { key: "DASHBOARD", // a unique name label: "Dashboard", },};
export const DASHBOARD_SPECIFICS = { path: "/dashboard", component: Dashboard, menu: { key: "SPECIFICS", // a unique name inject: "DASHBOARD", // You can also inject another one under "DASHBOARD.SPECIFICS" },};
Consumers
In order to have a mechanism of using forms in a nice descriptive manner, we created a Consumer
class which allows us to construct it with a set of data and then consume elements one by one, and ability to display the rest of unconsumed elements.
The API is simple and straight forward:
import { Consumer } from "@bluelibs/x-ui-admin";
const elements = [ { id: "1", name: 123, }, { id: "2", name: 124, }, { id: "3", name: 125, },];
const consumer = new Consumer(elements);
// Check if we finished consumption, ofcourse, it's falseconsumer.isConsumed();
const e1 = consumer.consume("1"); // this is the "1" element
consumer.isElementConsumed("1"); // trueconsumer.isElementConsumed("2"); // falseconsumer.isConsumed(); // false
consumer.consume("4"); // throws error: Consumer.Errors.ElementNotFound
const rest = consumer.rest(); // returns an array "2" and "3" elementsconsumer.isConsumed(); // trueconsumer.isElementConsumed("2"); // trueconsumer.isElementConsumed("3"); // true
consumer.consume("1"); // throws error: Consumer.Errors.AlreadyConsumed
You can also benefit of autocompletion like: new Consumer<MyType>()
as long as MyType has an id: string
attached to it.
Table Smart
The table smart is an extension ListSmart
from x-ui
, but this one is designed to work specifically with Table
from antd
package.
export class PostsAntTableSmart extends AntTableSmart<Post> { // "Posts" represents the UICollection collectionClass = Posts;
// This represents the Nova query run through collection getBody(): QueryBodyType<Post> { return { title: 1, comments: { user: { fullName: 1, }, text: 1, }, }; }
// These are the antd columns: https://ant.design/components/table/#Column getColumns(): ColumnsType<Post> { return [ { title: "Title", key: "title", dataIndex: "title", // render: text => <a>{text}</a> }, ]; }
getSortMap() { return { // key -> what it sorts title: "title", }; }}
Using the smart is pretty straight-forward:
import { newSmart, useRouter, useUIComponents } from "@bluelibs/x-ui";import { useEffect, useState, useMemo } from "react";import { PlusOutlined, FilterOutlined } from "@ant-design/icons";import * as Ant from "antd";
export function PostsList() { const UIComponents = useUIComponents(); const router = useRouter(); const [api, Provider] = newSmart(PostsAntTableSmart); const onFiltersUpdate = useMemo(() => { return (filters) => { api.setFlexibleFilters(filters); }; }, []);
return ( <UIComponents.AdminLayout> <Ant.Layout.Content> <Provider> <Ant.Input.Search name="Search" placeholder="Search" className="search" onKeyUp={(e) => { const value = (e.target as HTMLInputElement).value; api.setFilters({ // Customise your search filters! title: new RegExp(`${value}`, "i"), }); }} /> <Ant.Table {...api.getTableProps()} /> </Provider> </Ant.Layout.Content> </UIComponents.AdminLayout> );}
Custom Components
The components which extend the UIComponents
from x-ui
can be found inside here
You can easily override them, as their file name is exactly the name inside UIComponents
.
XForm
Since we rely on ant
for our frontend, we also use Form
components from ant
which are really flexible and complex. Because our goal with our libs is to move as much as possible from the visual components we structure our forms in classes:
import { XForm } from "@bluelibs/x-ui-admin";import { Service, Inject } from "@bluelibs/core";import * as Ant from "antd";import { UsersCollection, CompaniesCollection,} from "@bundles/UIAppBundle/collections";
// Note transience we'll soon identify why@Service({ transient: true })export class CompanyEditForm extends XForm { @Inject(() => CompaniesCollection) collection: CompaniesCollection;
build() { // You can use certain components of your choice const { UIComponents } = this;
this.add([ { id: "name", label: "Name", name: ["name"], rules: [], // Ant Form Rules: https://ant.design/components/form/#Rule initialValue: "John Smith",
// You can either use render or specify component. Please be careful, you always have to have input right under Form.Item render: (props) => ( <Ant.Form.Item {...props}> <Ant.Input /> </Ant.Form.Item> ),
// Equivalent to the above is: component: Ant.Input, // Add additional props to the input component componentProps: {},
// Pass props to Form.Item, gets into props from render() and works with custom component and custom render() formItemProps: {}, }, // The rest of form items ]); // You can also add them one by one, up to you. }
onSubmit(data) { // Handle the data however you wish, you can inject ApolloClient or use the collection directly to perform manipulations }}
In order to use this form we employ the Consumer
pattern:
import { use } from "@bluelibs/x-ui";
function CompanyCreateForm() { // Transient means a new instance every time, do not omit this const form = use(CompanyEditForm, { transient: true }); // Now you have to build it form.build();
// The main concept here is that this form can be rendered // But also customised return ( <Ant.Form onFinish={(document) => form.onSubmit(props.id, document)}> {form.render()} <Ant.Form.Item> <Ant.Button type="primary" htmlType="submit"> Submit </Ant.Button> </Ant.Form.Item> </Ant.Form> );}
// Customise forms by rendering certain items in a different wayfunction CompanyCreateForm() { const form = use(CompanyEditForm, { transient: true });
// You can update or remove certain items form.build(); form.update("title", { render: "...", });
return ( <Ant.Form onFinish={(document) => form.onSubmit(props.id, document)}> <Wrap>{form.render("item")}</Wrap>
<h1>Rest of elements to render:</h1> {form.render()} <Ant.Form.Item> <Ant.Button type="primary" htmlType="submit"> Submit </Ant.Button> </Ant.Form.Item> </Ant.Form> );}
caution
The input component under Form.Item
has to be direct and only child, something like this will fail:
render() { // FAILS return ( <Ant.Form.Item> <div><Ant.Input /></div> </Ant.Form.Item> )}
If you have a lot of changes to make to your initial form. Typically if you use Blueprint, such a form will be generated and you may want to change how you render certain fields:
// Do not forget transient, you need a new instance every time you fetch it from the container.@Service({ transient: true })class MyForm extends BaseGeneratedForm { build() { super.build(); this.update("item", { .... }) }}
XList
This is designed to work easily with Table
from ant
and this consumer represents a list of columns (ColumnType
). It's the same pattern and same concepts as XForm:
@Service({ transient: true })export class BillHeadList extends XList<BillHead> { build() { const { UIComponents, router } = this;
this.add([ { // The id always needs to be here id: "invoiceNumber",
// The rest under are ColumnType elements title: "Invoice Number", key: "Invoice Number", dataIndex: ["invoiceNumber"], sorter: true, render: (value, model) => { const props = { type: "string", value, }; return <UIComponents.AdminListItemRenderer {...props} />; }, }, ]); }}
XViewer
Same concepts as with Forms and Lists apply. This is designed to work very well with Description
.
type XViewElementType = { id: string; label?: string; dataIndex: string[]; /** * If it's nested a component is not needed */ render?: (value: any) => JSX.Element;};
The typical viewer component would look like this:
export function BillHeadsViewComponent(props: { document: Partial<BillHead> }) { const document = props.document;
const viewer = use(BillHeadViewer, { transient: true }); // Notice that we need to set the document so it knows how to render the data // In Forms and Lists we don't do this as this is being handled by Ant's underlying systems viewer.setDocument(document);
viewer.build();
return ( <Ant.Descriptions> {viewer.rest().map((item) => { // Iterating and consuming "rest", this is just like walking through the elements // And rendering them however you please return ( <Ant.Descriptions.Item label={item.label}> {viewer.render(item)} </Ant.Descriptions.Item> ); })} </Ant.Descriptions> );}