Dynamics list columns with a lot of work ahead
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { RJSFSchema } from '@rjsf/utils';
|
import { RJSFSchema } from '@rjsf/utils';
|
||||||
import i18n from '../../../i18n'
|
import i18n from '../../../i18n'
|
||||||
import { JSONSchema7Definition } from "json-schema";
|
import { JSONSchema7Definition } from "json-schema";
|
||||||
|
import { GridColDef } from "@mui/x-data-grid";
|
||||||
|
|
||||||
const API_URL = "/api/v1";
|
const API_URL = "/api/v1";
|
||||||
|
|
||||||
@@ -51,6 +52,10 @@ export const jsonschemaProvider = {
|
|||||||
return readSchema
|
return readSchema
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getReadResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||||
|
return getResourceSchema(`${resourceName}Read`)
|
||||||
|
},
|
||||||
|
|
||||||
getUpdateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
|
getUpdateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||||
return getResourceSchema(`${resourceName}Update`)
|
return getResourceSchema(`${resourceName}Update`)
|
||||||
},
|
},
|
||||||
@@ -58,21 +63,88 @@ export const jsonschemaProvider = {
|
|||||||
getCreateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
|
getCreateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||||
return getResourceSchema(`${resourceName}Create`)
|
return getResourceSchema(`${resourceName}Create`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
|
||||||
|
return getColumns(`${resourceName}Read`)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColumns = async (resourceName: string): Promise<GridColDef[]> => {
|
||||||
|
return buildColumns(await getJsonschema(), resourceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: string|undefined = undefined): GridColDef[] {
|
||||||
|
if (rawSchemas.components.schemas[resourceName] === undefined) {
|
||||||
|
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
||||||
|
let resource = rawSchemas.components.schemas[resourceName];
|
||||||
|
let result: GridColDef[] = [];
|
||||||
|
for (const prop_name in resource.properties) {
|
||||||
|
let prop = resource.properties[prop_name];
|
||||||
|
|
||||||
|
if (is_reference(prop)) {
|
||||||
|
const subresourceName = get_reference_name(prop);
|
||||||
|
result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name))
|
||||||
|
} else if (is_union(prop)) {
|
||||||
|
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
|
||||||
|
let seen = new Set<string>();
|
||||||
|
for (let i in union) {
|
||||||
|
if (is_reference(union[i])) {
|
||||||
|
const subresourceName = get_reference_name(union[i]);
|
||||||
|
const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name);
|
||||||
|
for (const s of subcolumns) {
|
||||||
|
if (! seen.has(s.field)) {
|
||||||
|
result.push(s);
|
||||||
|
seen.add(s.field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_enum(prop)) {
|
||||||
|
let seen = new Set<string>();
|
||||||
|
for (let i in prop.allOf) {
|
||||||
|
if (is_reference(prop.allOf[i])) {
|
||||||
|
const subresourceName = get_reference_name(prop.allOf[i]);
|
||||||
|
const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name);
|
||||||
|
for (const s of subcolumns) {
|
||||||
|
if (! seen.has(s.field)) {
|
||||||
|
result.push(s);
|
||||||
|
seen.add(s.field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_array(prop) && is_reference(prop.items)) {
|
||||||
|
const subresourceName = get_reference_name(prop.items);
|
||||||
|
result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name))
|
||||||
|
} else {
|
||||||
|
let column: GridColDef = {
|
||||||
|
field: prefix ? `${prefix}.${prop_name}` : prop_name,
|
||||||
|
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string,
|
||||||
|
valueGetter: (value: any, row: any ) => {
|
||||||
|
if (prefix === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent = row;
|
||||||
|
for (const column of prefix.split(".")) {
|
||||||
|
parent = parent[column];
|
||||||
|
}
|
||||||
|
return parent ? parent[prop_name] : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
|
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||||
return buildResource(await getJsonschema(), resourceName)
|
return buildResource(await getJsonschema(), resourceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
let rawSchema: RJSFSchema;
|
|
||||||
const getJsonschema = async (): Promise<RJSFSchema> => {
|
|
||||||
if (rawSchema === undefined) {
|
|
||||||
const response = await fetch(`${API_URL}/openapi.json`,);
|
|
||||||
rawSchema = await response.json();
|
|
||||||
}
|
|
||||||
return rawSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertCamelToSnake(str: string): string {
|
function convertCamelToSnake(str: string): string {
|
||||||
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
|
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
|
||||||
}
|
}
|
||||||
@@ -82,7 +154,7 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
|||||||
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|Create|Update)$/g, ""));
|
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
||||||
let resource = structuredClone(rawSchemas.components.schemas[resourceName]);
|
let resource = structuredClone(rawSchemas.components.schemas[resourceName]);
|
||||||
resource.components = { schemas: {} };
|
resource.components = { schemas: {} };
|
||||||
for (let prop_name in resource.properties) {
|
for (let prop_name in resource.properties) {
|
||||||
@@ -118,6 +190,15 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
|||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rawSchema: RJSFSchema;
|
||||||
|
const getJsonschema = async (): Promise<RJSFSchema> => {
|
||||||
|
if (rawSchema === undefined) {
|
||||||
|
const response = await fetch(`${API_URL}/openapi.json`,);
|
||||||
|
rawSchema = await response.json();
|
||||||
|
}
|
||||||
|
return rawSchema;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
||||||
const subresourceName = get_reference_name(prop_reference);
|
const subresourceName = get_reference_name(prop_reference);
|
||||||
const subresource = buildResource(rawSchemas, subresourceName);
|
const subresource = buildResource(rawSchemas, subresourceName);
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ export const ContractRoutes = () => {
|
|||||||
|
|
||||||
const ListContract = () => {
|
const ListContract = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
];
|
];
|
||||||
return <List<Contract> resource={`contracts`} columns={columns} />
|
return <List<Contract> resource={`contracts`} schemaName={"Contract"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditContract = () => {
|
const EditContract = () => {
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export const DraftRoutes = () => {
|
|||||||
|
|
||||||
const ListDraft = () => {
|
const ListDraft = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
];
|
];
|
||||||
return <List<Draft> resource={`contracts/drafts`} columns={columns} />
|
return <List<Draft> resource={`contracts/drafts`} columns={columns} schemaName={"Contract"} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditDraft = () => {
|
const EditDraft = () => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Route, Routes } from "react-router";
|
import { Route, Routes } from "react-router";
|
||||||
import { useTranslation } from "@refinedev/core";
|
|
||||||
import List from "./base-page/List";
|
import List from "./base-page/List";
|
||||||
import Edit from "./base-page/Edit";
|
import Edit from "./base-page/Edit";
|
||||||
import New from "./base-page/New";
|
import New from "./base-page/New";
|
||||||
@@ -22,12 +21,12 @@ export const EntityRoutes = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListEntity = () => {
|
const ListEntity = () => {
|
||||||
const { translate: t } = useTranslation();
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "entity_data", headerName: t("schemas.type"), width: 110, valueFormatter: ({ type }: {type: string}) => type },
|
{ field: "entity_data.type", column: { width: 110 }},
|
||||||
{ field: "label", headerName: t("schemas.label"), flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "updated_at", column: { flex: 1 }},
|
||||||
];
|
];
|
||||||
return <List<Entity> resource={`entities`} columns={columns} />
|
return <List<Entity> resource={`entities`} schemaName={"Entity"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditEntity = () => {
|
const EditEntity = () => {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ export const ProvisionRoutes = () => {
|
|||||||
|
|
||||||
const ListProvision = () => {
|
const ListProvision = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
];
|
];
|
||||||
return <List<Provision> resource={`templates/provisions`} columns={columns} />
|
return <List<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditProvision = () => {
|
const EditProvision = () => {
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ export const TemplateRoutes = () => {
|
|||||||
|
|
||||||
const ListTemplate = () => {
|
const ListTemplate = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
];
|
];
|
||||||
return <List<Template> resource={`templates/contracts`} columns={columns} />
|
return <List<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditTemplate = () => {
|
const EditTemplate = () => {
|
||||||
|
|||||||
@@ -1,21 +1,55 @@
|
|||||||
|
import { Link, useNavigate } from "react-router"
|
||||||
import { UiSchema } from "@rjsf/utils";
|
import { UiSchema } from "@rjsf/utils";
|
||||||
import { useTranslation } from "@refinedev/core";
|
import { useTranslation } from "@refinedev/core";
|
||||||
import { List as RefineList, useDataGrid } from "@refinedev/mui";
|
import { List as RefineList, useDataGrid } from "@refinedev/mui";
|
||||||
import { DataGrid, GridColDef, GridValidRowModel } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridValidRowModel, GridColumnVisibilityModel } from "@mui/x-data-grid";
|
||||||
import { Link, useNavigate } from "react-router"
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import React, { useContext } from "react";
|
import { Button, CircularProgress } from "@mui/material";
|
||||||
import { Button } from "@mui/material";
|
|
||||||
import { FirmContext } from "../../../contexts/FirmContext";
|
import { FirmContext } from "../../../contexts/FirmContext";
|
||||||
|
import { jsonschemaProvider } from "../../../lib/crud/providers/jsonschema-provider";
|
||||||
|
|
||||||
type ListProps<T extends GridValidRowModel> = {
|
type ListProps = {
|
||||||
resource: string,
|
resource: string,
|
||||||
columns: GridColDef<T>[],
|
columns: ColumnDefinition[],
|
||||||
schemaName?: string,
|
schemaName: string,
|
||||||
uiSchema?: UiSchema,
|
uiSchema?: UiSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const List = <T extends GridValidRowModel>(props: ListProps<T>) => {
|
type ColumnSchema<T extends GridValidRowModel> = {
|
||||||
const { resource, columns } = props;
|
columns: GridColDef<T>[],
|
||||||
|
columnVisibilityModel: GridColumnVisibilityModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnDefinition = {
|
||||||
|
field: string,
|
||||||
|
column: Partial<GridColDef>,
|
||||||
|
hide?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeColumnSchema<T extends GridValidRowModel>(definitionColumns: ColumnDefinition[], resourceColumns: GridColDef[]): ColumnSchema<T> {
|
||||||
|
//reorder resourceColumns as in definition
|
||||||
|
definitionColumns.slice().reverse().forEach(first => {
|
||||||
|
resourceColumns.sort(function(x,y){ return x.field == first.field ? -1 : y.field == first.field ? 1 : 0; });
|
||||||
|
})
|
||||||
|
|
||||||
|
let visibilityModel: GridColumnVisibilityModel = {}
|
||||||
|
resourceColumns.forEach((resource, index) =>{
|
||||||
|
visibilityModel[resource.field] = definitionColumns.some(col => col.field == resource.field && !col.hide)
|
||||||
|
definitionColumns.forEach((def) => {
|
||||||
|
if (def.field == resource.field) {
|
||||||
|
resourceColumns[index] = {...resource, ...def.column};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: resourceColumns,
|
||||||
|
columnVisibilityModel: visibilityModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const List = <T extends GridValidRowModel>(props: ListProps) => {
|
||||||
|
const { resource, columns, schemaName } = props;
|
||||||
const { translate: t } = useTranslation();
|
const { translate: t } = useTranslation();
|
||||||
const { currentFirm } = useContext(FirmContext);
|
const { currentFirm } = useContext(FirmContext);
|
||||||
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
||||||
@@ -25,15 +59,32 @@ const List = <T extends GridValidRowModel>(props: ListProps<T>) => {
|
|||||||
});
|
});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const cols = React.useMemo<GridColDef<T>[]>(
|
const [columnSchema, setColumnSchema] = useState<ColumnSchema<T>>()
|
||||||
() => columns,
|
const [schemaLoading, setSchemaLoading] = useState(true);
|
||||||
[],
|
useEffect(() => {
|
||||||
);
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
const resourceColumns = await jsonschemaProvider.getReadResourceColumns(schemaName)
|
||||||
|
const definedColumns = computeColumnSchema<T>(columns, resourceColumns)
|
||||||
|
setColumnSchema(definedColumns);
|
||||||
|
console.log(resourceColumns);
|
||||||
|
setSchemaLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
setSchemaLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleRowClick = (params: any, event: any) => {
|
const handleRowClick = (params: any, event: any) => {
|
||||||
navigate(`edit/${params.id}`)
|
navigate(`edit/${params.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (schemaLoading || columnSchema === undefined) {
|
||||||
|
return <CircularProgress />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RefineList>
|
<RefineList>
|
||||||
<Link to={"create"} >
|
<Link to={"create"} >
|
||||||
@@ -41,9 +92,14 @@ const List = <T extends GridValidRowModel>(props: ListProps<T>) => {
|
|||||||
</Link>
|
</Link>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
{...dataGridProps}
|
{...dataGridProps}
|
||||||
columns={cols}
|
columns={columnSchema.columns}
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
pageSizeOptions={[10, 15, 20, 50, 100]}
|
pageSizeOptions={[10, 15, 20, 50, 100]}
|
||||||
|
initialState={{
|
||||||
|
columns: {
|
||||||
|
columnVisibilityModel: columnSchema.columnVisibilityModel
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</RefineList>
|
</RefineList>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user