Compare commits
19 Commits
40648c3fdf
...
fix/firm_i
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ff15cdef4 | |||
| 72b6f26ebc | |||
| b06ce4eefd | |||
| 49317e905b | |||
| 189c896e60 | |||
| 4ae0b321b9 | |||
| 5a3b87e82c | |||
| 0731ac3b6e | |||
| 77fa4cde35 | |||
| 9aac1d3e34 | |||
| 7aced9477c | |||
| 5a0327c930 | |||
| 18e4fcea28 | |||
| 239ab2e241 | |||
| 9fcead8c95 | |||
| 73871ae04e | |||
| c35c63b421 | |||
| 9fd201c10a | |||
| cb81e233a5 |
@@ -210,15 +210,15 @@ def replace_variables_in_value(variables, value: str):
|
|||||||
|
|
||||||
|
|
||||||
class ContractDraftFilters(FilterSchema):
|
class ContractDraftFilters(FilterSchema):
|
||||||
status: Optional[str] = None
|
status__in: Optional[list[str]] = None
|
||||||
|
|
||||||
class Constants(Filter.Constants):
|
class Constants(Filter.Constants):
|
||||||
model = ContractDraft
|
model = ContractDraft
|
||||||
search_model_fields = ["label", "status"]
|
search_model_fields = ["label"]
|
||||||
|
|
||||||
class ContractFilters(FilterSchema):
|
class ContractFilters(FilterSchema):
|
||||||
status: Optional[str] = None
|
status__in: Optional[list[str]] = None
|
||||||
|
|
||||||
class Constants(Filter.Constants):
|
class Constants(Filter.Constants):
|
||||||
model = Contract
|
model = Contract
|
||||||
search_model_fields = ["label", "status"]
|
search_model_fields = ["label"]
|
||||||
|
|||||||
@@ -155,4 +155,11 @@ class Filter(BaseFilterModel):
|
|||||||
|
|
||||||
class FilterSchema(Filter):
|
class FilterSchema(Filter):
|
||||||
label__ilike: Optional[str] = None
|
label__ilike: Optional[str] = None
|
||||||
|
search: Optional[str] = None
|
||||||
order_by: Optional[list[str]] = None
|
order_by: Optional[list[str]] = None
|
||||||
|
created_at__lte: Optional[str] = None
|
||||||
|
created_at__gte: Optional[str] = None
|
||||||
|
created_by__in: Optional[list[str]] = None
|
||||||
|
updated_at__lte: Optional[str] = None
|
||||||
|
updated_at__gte: Optional[str] = None
|
||||||
|
updated_by__in: Optional[list[str]] = None
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
|
|
||||||
class Reader(BaseModel):
|
class Reader(BaseModel):
|
||||||
id: Optional[PydanticObjectId] = Field(default=None, validation_alias="_id")
|
id: Optional[PydanticObjectId] = Field(validation_alias="_id")
|
||||||
|
created_by: PydanticObjectId = Field(title="Créé par")
|
||||||
|
updated_by: PydanticObjectId = Field(title="Modifié par")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_model(cls, model):
|
def from_model(cls, model):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
from pydantic import Field
|
from pydantic import Field, BaseModel
|
||||||
|
|
||||||
from firm.core.models import CrudDocument, CrudDocumentConfig
|
from firm.core.models import CrudDocument, CrudDocumentConfig
|
||||||
from firm.core.schemas import Writer, Reader
|
from firm.core.schemas import Writer, Reader
|
||||||
@@ -32,7 +32,8 @@ class CurrentFirm(CrudDocument):
|
|||||||
return cls.model_validate(document)
|
return cls.model_validate(document)
|
||||||
|
|
||||||
|
|
||||||
class CurrentFirmSchemaRead(Reader):
|
class CurrentFirmSchemaRead(BaseModel):
|
||||||
|
id: Optional[PydanticObjectId]
|
||||||
entity: EntityRead
|
entity: EntityRead
|
||||||
partner: EntityRead
|
partner: EntityRead
|
||||||
partner_list: list[EntityRead]
|
partner_list: list[EntityRead]
|
||||||
|
|||||||
@@ -31,14 +31,22 @@ async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_
|
|||||||
corporation_schema.entity_data.employees.append(Employee(entity_id=owner_entity.id, position=schema.position))
|
corporation_schema.entity_data.employees.append(Employee(entity_id=owner_entity.id, position=schema.position))
|
||||||
corp = await Entity.create(reg.db, corporation_schema)
|
corp = await Entity.create(reg.db, corporation_schema)
|
||||||
|
|
||||||
document = await CurrentFirm.create(reg.db, CurrentFirm(
|
firm = await CurrentFirm.create(reg.db, CurrentFirm(
|
||||||
instance=reg.instance,
|
instance=reg.instance,
|
||||||
firm=reg.firm,
|
firm=reg.firm,
|
||||||
entity_id=corp.id,
|
entity_id=corp.id,
|
||||||
primary_color=schema.primary_color,
|
primary_color=schema.primary_color,
|
||||||
secondary_color=schema.secondary_color,
|
secondary_color=schema.secondary_color,
|
||||||
))
|
))
|
||||||
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(corp), EntityRead.from_model(owner_entity))
|
|
||||||
|
await Partner.create(Partner(user_id=reg.user.id, entity_id=owner_entity.id))
|
||||||
|
|
||||||
|
return CurrentFirmSchemaRead.from_model_and_entities(
|
||||||
|
firm,
|
||||||
|
EntityRead.from_model(corp),
|
||||||
|
EntityRead.from_model(owner_entity),
|
||||||
|
[EntityRead.from_model(owner_entity)]
|
||||||
|
)
|
||||||
|
|
||||||
@current_firm_router.put("/", response_description=f"Current Firm record updated")
|
@current_firm_router.put("/", response_description=f"Current Firm record updated")
|
||||||
async def update(schema: CurrentFirmSchemaUpdate, reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
|
async def update(schema: CurrentFirmSchemaUpdate, reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class Entity(CrudDocument):
|
|||||||
|
|
||||||
|
|
||||||
class EntityDataFilter(Filter):
|
class EntityDataFilter(Filter):
|
||||||
type__in: Optional[list[EntityTypeEnum]] = None
|
type__in: Optional[list[str]] = None
|
||||||
|
|
||||||
class Constants(Filter.Constants):
|
class Constants(Filter.Constants):
|
||||||
model = EntityType
|
model = EntityType
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class ProvisionTemplateReference(BaseModel):
|
|||||||
|
|
||||||
provision_template_id: PydanticObjectId = ForeignKey(
|
provision_template_id: PydanticObjectId = ForeignKey(
|
||||||
"templates/provisions",
|
"templates/provisions",
|
||||||
"TemplateProvision",
|
"ProvisionTemplate",
|
||||||
['title', 'body'],
|
['title', 'body'],
|
||||||
props={"parametrized": True},
|
props={"parametrized": True},
|
||||||
title="Template de clause"
|
title="Template de clause"
|
||||||
|
|||||||
1305
gui/rpk-gui/package-lock.json
generated
1305
gui/rpk-gui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
"@mui/lab": "^6.0.0-beta.14",
|
"@mui/lab": "^6.0.0-beta.14",
|
||||||
"@mui/material": "^6.1.7",
|
"@mui/material": "^6.1.7",
|
||||||
"@mui/x-data-grid": "^7.22.2",
|
"@mui/x-data-grid": "^7.22.2",
|
||||||
|
"@mui/x-date-pickers": "^8.3.0",
|
||||||
"@refinedev/cli": "^2.16.21",
|
"@refinedev/cli": "^2.16.21",
|
||||||
"@refinedev/core": "^4.47.1",
|
"@refinedev/core": "^4.47.1",
|
||||||
"@refinedev/devtools": "^1.1.32",
|
"@refinedev/devtools": "^1.1.32",
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useTranslation } from "@refinedev/core";
|
|||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import * as locales from '@mui/material/locale';
|
import * as locales from '@mui/material/locale';
|
||||||
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
import { createTheme, ThemeProvider } from "@mui/material/styles";
|
||||||
|
import { LocalizationProvider } from "@mui/x-date-pickers";
|
||||||
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
|
|
||||||
type SupportedLocales = keyof typeof locales;
|
type SupportedLocales = keyof typeof locales;
|
||||||
|
|
||||||
@@ -10,11 +12,13 @@ export const I18nTheme: React.FC<PropsWithChildren> = ({ children }: PropsWithCh
|
|||||||
const { getLocale } = useTranslation();
|
const { getLocale } = useTranslation();
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const themeWithLocale = createTheme(theme, locales[getLocale() as SupportedLocales])
|
const locale = getLocale() || "en"
|
||||||
|
const themeWithLocale = createTheme(theme, locales[locale as SupportedLocales])
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={themeWithLocale}>
|
<ThemeProvider theme={themeWithLocale}>
|
||||||
{ children }
|
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={locale.slice(0,2)} >
|
||||||
|
{ children }
|
||||||
|
</LocalizationProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
|
|||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={openUserMenu}
|
open={openUserMenu}
|
||||||
onClose={handleCloseUserMenu}
|
onClose={handleCloseUserMenu}
|
||||||
MenuListProps={{
|
slotProps={{
|
||||||
'aria-labelledby': 'user-menu-button',
|
list:{ 'aria-labelledby': 'user-menu-button' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
|
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import ArrayFieldTemplate from "./templates/ArrayFieldTemplate"
|
|||||||
import ArrayFieldItemTemplate from "./templates/ArrayFieldItemTemplate";
|
import ArrayFieldItemTemplate from "./templates/ArrayFieldItemTemplate";
|
||||||
import { ResourceContext } from "../contexts/ResourceContext";
|
import { ResourceContext } from "../contexts/ResourceContext";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { ParametersContextProvider } from "../contexts/parameters-context";
|
||||||
|
import CrudArrayField from "./fields/crud-array-field";
|
||||||
|
|
||||||
type BaseFormProps = {
|
type BaseFormProps = {
|
||||||
schema: RJSFSchema,
|
schema: RJSFSchema,
|
||||||
@@ -19,11 +21,12 @@ type BaseFormProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const customWidgets: RegistryWidgetsType = {
|
export const customWidgets: RegistryWidgetsType = {
|
||||||
TextWidget: CrudTextWidget
|
TextWidget: CrudTextWidget,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const customFields: RegistryFieldsType = {
|
export const customFields: RegistryFieldsType = {
|
||||||
AnyOfField: UnionEnumField
|
AnyOfField: UnionEnumField,
|
||||||
|
ArrayField: CrudArrayField
|
||||||
}
|
}
|
||||||
|
|
||||||
const customTemplates = {
|
const customTemplates = {
|
||||||
@@ -36,19 +39,21 @@ export const BaseForm: React.FC<BaseFormProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
|
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
|
||||||
<Form
|
<ParametersContextProvider>
|
||||||
schema={schema}
|
<Form
|
||||||
uiSchema={uiSchema === undefined ? {} : uiSchema}
|
schema={schema}
|
||||||
formData={formData}
|
uiSchema={uiSchema === undefined ? {} : uiSchema}
|
||||||
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
|
formData={formData}
|
||||||
validator={validator}
|
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
|
||||||
omitExtraData={true}
|
validator={validator}
|
||||||
widgets={customWidgets}
|
omitExtraData={true}
|
||||||
fields={customFields}
|
widgets={customWidgets}
|
||||||
templates={customTemplates}
|
fields={customFields}
|
||||||
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
|
templates={customTemplates}
|
||||||
children={children}
|
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
|
||||||
/>
|
children={children}
|
||||||
|
/>
|
||||||
|
</ParametersContextProvider>
|
||||||
</ResourceContext.Provider>
|
</ResourceContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
73
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
73
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Accordion, AccordionDetails, AccordionSummary, CircularProgress } from "@mui/material";
|
||||||
|
import FilterForm from "../../filter-form/components/filter-form";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { GridExpandMoreIcon } from "@mui/x-data-grid";
|
||||||
|
import { useResourceFilter } from "../hook";
|
||||||
|
|
||||||
|
export type OnChangeValue = {
|
||||||
|
search: string | null
|
||||||
|
filters: {[filter: string]: {[op: string]: string}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CrudFiltersProps = {
|
||||||
|
resourceName: string
|
||||||
|
resourcePath: string
|
||||||
|
onChange: (value: OnChangeValue) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CrudFilters = (props: CrudFiltersProps) => {
|
||||||
|
const { resourceName, resourcePath, onChange } = props
|
||||||
|
const { hasSearch, filtersSchema, filtersLoading } = useResourceFilter(resourceName, resourcePath)
|
||||||
|
|
||||||
|
if (filtersLoading) {
|
||||||
|
return <CircularProgress />
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentValue = {
|
||||||
|
search: "",
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasSearch &&
|
||||||
|
<SearchFilter value="" onChange={(value) => {
|
||||||
|
currentValue.search = value;
|
||||||
|
onChange(currentValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<>Advanced filters<GridExpandMoreIcon /></>}
|
||||||
|
/>
|
||||||
|
<AccordionDetails>
|
||||||
|
<FilterForm fields={filtersSchema} values={{}} onChange={(value) => {
|
||||||
|
currentValue.filters = value;
|
||||||
|
onChange(currentValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CrudFilters;
|
||||||
|
|
||||||
|
type SearchFilter = {
|
||||||
|
value: string
|
||||||
|
onChange: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchFilter = (props: SearchFilter) => {
|
||||||
|
const {value, onChange} = props;
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
label="schemas.search"
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
defaultValue={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode } from "react";
|
||||||
import { CircularProgress } from "@mui/material";
|
import { CircularProgress } from "@mui/material";
|
||||||
import { useForm } from "@refinedev/core";
|
|
||||||
import { UiSchema } from "@rjsf/utils";
|
import { UiSchema } from "@rjsf/utils";
|
||||||
import { jsonschemaProvider } from "../providers/jsonschema-provider";
|
|
||||||
import { BaseForm } from "./base-form";
|
import { BaseForm } from "./base-form";
|
||||||
|
import { useResourceSchema } from "../hook";
|
||||||
|
|
||||||
type CrudFormProps = {
|
type CrudFormProps = {
|
||||||
schemaName: string,
|
schemaName: string,
|
||||||
@@ -18,31 +17,8 @@ type CrudFormProps = {
|
|||||||
|
|
||||||
export const CrudForm: React.FC<CrudFormProps> = (props) => {
|
export const CrudForm: React.FC<CrudFormProps> = (props) => {
|
||||||
const { schemaName, uiSchema, record, resourceBasePath, defaultValue, children, onSubmit=(data: any) => {}, card=false } = props;
|
const { schemaName, uiSchema, record, resourceBasePath, defaultValue, children, onSubmit=(data: any) => {}, card=false } = props;
|
||||||
|
const type = record === undefined ? "create" : card ? "card" : "update"
|
||||||
const [schema, setSchema] = useState({});
|
const { schema, schemaLoading } = useResourceSchema(schemaName, type);
|
||||||
const [schemaLoading, setSchemaLoading] = useState(true);
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSchema = async () => {
|
|
||||||
try {
|
|
||||||
let resourceSchema
|
|
||||||
if (record === undefined) {
|
|
||||||
resourceSchema = await jsonschemaProvider.getCreateResourceSchema(schemaName);
|
|
||||||
} else {
|
|
||||||
if (card) {
|
|
||||||
resourceSchema = await jsonschemaProvider.getCardResourceSchema(schemaName);
|
|
||||||
} else {
|
|
||||||
resourceSchema = await jsonschemaProvider.getUpdateResourceSchema(schemaName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSchema(resourceSchema);
|
|
||||||
setSchemaLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
setSchemaLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchSchema();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if(schemaLoading) {
|
if(schemaLoading) {
|
||||||
return <CircularProgress />
|
return <CircularProgress />
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
import { DataGrid, GridColDef, GridColumnVisibilityModel, GridValidRowModel } from "@mui/x-data-grid";
|
import { DataGrid, GridColDef, GridColumnVisibilityModel, GridValidRowModel } from "@mui/x-data-grid";
|
||||||
import { UiSchema } from "@rjsf/utils";
|
import { UiSchema } from "@rjsf/utils";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useResourceColumns } from "../hook";
|
||||||
import { jsonschemaProvider } from "../providers/jsonschema-provider";
|
|
||||||
import { CircularProgress } from "@mui/material";
|
|
||||||
|
|
||||||
type CrudListProps = {
|
type CrudListProps = {
|
||||||
schemaName: string,
|
schemaName: string,
|
||||||
@@ -32,25 +31,9 @@ const CrudList = <T extends GridValidRowModel>(props: CrudListProps) => {
|
|||||||
columnDefinitions
|
columnDefinitions
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [columnSchema, setColumnSchema] = useState<ColumnSchema<T>>()
|
const { columnSchema, columnLoading } = useResourceColumns<T>(schemaName, columnDefinitions);
|
||||||
const [schemaLoading, setSchemaLoading] = useState(true);
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchSchema = async () => {
|
|
||||||
try {
|
|
||||||
const resourceColumns = await jsonschemaProvider.getReadResourceColumns(schemaName)
|
|
||||||
const definedColumns = computeColumnSchema<T>(columnDefinitions, resourceColumns)
|
|
||||||
setColumnSchema(definedColumns);
|
|
||||||
console.log(resourceColumns);
|
|
||||||
setSchemaLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching data:', error);
|
|
||||||
setSchemaLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchSchema();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (schemaLoading || columnSchema === undefined) {
|
if (columnLoading || columnSchema === undefined) {
|
||||||
return <CircularProgress />
|
return <CircularProgress />
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,26 +53,4 @@ const CrudList = <T extends GridValidRowModel>(props: CrudListProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CrudList;
|
export default CrudList;
|
||||||
|
|||||||
146
gui/rpk-gui/src/lib/crud/components/fields/crud-array-field.tsx
Normal file
146
gui/rpk-gui/src/lib/crud/components/fields/crud-array-field.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { JSONSchema7Definition } from "json-schema";
|
||||||
|
import {
|
||||||
|
FieldProps,
|
||||||
|
FormContextType,
|
||||||
|
getTemplate, getUiOptions,
|
||||||
|
RJSFSchema,
|
||||||
|
} from "@rjsf/utils";
|
||||||
|
import ArrayField from "@rjsf/core/lib/components/fields/ArrayField";
|
||||||
|
import validator from "@rjsf/validator-ajv8";
|
||||||
|
import Form from "@rjsf/mui";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Box, Paper } from "@mui/material";
|
||||||
|
import { CrudTextRJSFSchema } from "../widgets/crud-text-widget";
|
||||||
|
import { ParametersContext } from "../../contexts/parameters-context";
|
||||||
|
|
||||||
|
export type CrudArrayFieldSchema = RJSFSchema & {
|
||||||
|
props? : any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const CrudArrayField = <T = any, S extends CrudArrayFieldSchema = CrudArrayFieldSchema, F extends FormContextType = any> (props: FieldProps<T[], S, F>)=> {
|
||||||
|
const { schema } = props
|
||||||
|
let isDictionary = false;
|
||||||
|
if (schema.props) {
|
||||||
|
if (schema.props.hasOwnProperty("display")) {
|
||||||
|
if (schema.props.display == "dictionary") {
|
||||||
|
isDictionary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isDictionary) {
|
||||||
|
return <Dictionary {...props} />
|
||||||
|
}
|
||||||
|
return <ArrayField<T,S,F> {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CrudArrayField;
|
||||||
|
|
||||||
|
type DictionaryEntry = {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
};
|
||||||
|
|
||||||
|
const Dictionary = <
|
||||||
|
T = any,
|
||||||
|
S extends CrudTextRJSFSchema = CrudTextRJSFSchema,
|
||||||
|
F extends FormContextType = any
|
||||||
|
>(props: FieldProps<T[], S, F>)=> {
|
||||||
|
const { required, formData, onChange, registry, uiSchema, idSchema, schema } = props;
|
||||||
|
const uiOptions = getUiOptions<T[], S, F>(uiSchema);
|
||||||
|
const { parameters } = useContext(ParametersContext);
|
||||||
|
|
||||||
|
const ArrayFieldDescriptionTemplate = getTemplate<'ArrayFieldDescriptionTemplate', T[], S, F>(
|
||||||
|
'ArrayFieldDescriptionTemplate',
|
||||||
|
registry,
|
||||||
|
uiOptions
|
||||||
|
);
|
||||||
|
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T[], S, F>(
|
||||||
|
'ArrayFieldTitleTemplate',
|
||||||
|
registry,
|
||||||
|
uiOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
let properties = new Set<string>()
|
||||||
|
for (const field in parameters) {
|
||||||
|
for (const param of parameters[field]) {
|
||||||
|
properties.add(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: {[key:string]: string} = {}
|
||||||
|
if (formData !== undefined) {
|
||||||
|
for (const param of formData) {
|
||||||
|
// @ts-ignore
|
||||||
|
data[param.key] = param.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyDict = Object.values(data).length == 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={2}>
|
||||||
|
<Box p={2}>
|
||||||
|
<ArrayFieldTitleTemplate
|
||||||
|
idSchema={idSchema}
|
||||||
|
title={uiOptions.title || schema.title}
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
required={required}
|
||||||
|
registry={registry}
|
||||||
|
/>
|
||||||
|
<ArrayFieldDescriptionTemplate
|
||||||
|
idSchema={idSchema}
|
||||||
|
description={uiOptions.description || schema.description}
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
registry={registry}
|
||||||
|
/>
|
||||||
|
{ emptyDict && <Typography>No variables found</Typography>}
|
||||||
|
{ !emptyDict && (
|
||||||
|
<Form
|
||||||
|
schema={getFormSchema(Array.from(properties.values()), required || false)}
|
||||||
|
tagName="div"
|
||||||
|
formData={data}
|
||||||
|
validator={validator}
|
||||||
|
omitExtraData={true}
|
||||||
|
onChange={(e, id) => {
|
||||||
|
console.log(e)
|
||||||
|
let value: T[] = []
|
||||||
|
for (const prop of properties) {
|
||||||
|
value.push({
|
||||||
|
key: prop,
|
||||||
|
value: e.formData.hasOwnProperty(prop) ? e.formData[prop] : undefined
|
||||||
|
} as T)
|
||||||
|
}
|
||||||
|
onChange(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormSchema(properties: string[], isRequired: boolean) {
|
||||||
|
const schema: JSONSchema7Definition = {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
let required: string[] = []
|
||||||
|
for (const pname of properties) {
|
||||||
|
schema.properties![pname] = {
|
||||||
|
type: "string",
|
||||||
|
title: pname
|
||||||
|
}
|
||||||
|
if (isRequired) {
|
||||||
|
required.push(pname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema.required = required
|
||||||
|
|
||||||
|
return schema
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
FormContextType,
|
FormContextType,
|
||||||
} from '@rjsf/utils';
|
} from '@rjsf/utils';
|
||||||
import { CrudTextRJSFSchema } from "../widgets/crud-text-widget";
|
import { CrudTextRJSFSchema } from "../widgets/crud-text-widget";
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
|
/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
|
||||||
@@ -45,7 +44,7 @@ export default function ArrayFieldTemplate<
|
|||||||
} = registry.templates;
|
} = registry.templates;
|
||||||
|
|
||||||
let gridSize = 12;
|
let gridSize = 12;
|
||||||
let numbered = false
|
let numbered = false;
|
||||||
if (schema.props) {
|
if (schema.props) {
|
||||||
if (schema.props.hasOwnProperty("items_per_row")) {
|
if (schema.props.hasOwnProperty("items_per_row")) {
|
||||||
gridSize = gridSize / schema.props.items_per_row;
|
gridSize = gridSize / schema.props.items_per_row;
|
||||||
@@ -74,7 +73,7 @@ export default function ArrayFieldTemplate<
|
|||||||
registry={registry}
|
registry={registry}
|
||||||
/>
|
/>
|
||||||
<Grid2 container justifyContent='flex-start'>
|
<Grid2 container justifyContent='flex-start'>
|
||||||
{items &&
|
{ items &&
|
||||||
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>, index) => (
|
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>, index) => (
|
||||||
<Grid2 key={key} size={gridSize} >
|
<Grid2 key={key} size={gridSize} >
|
||||||
<Grid2 container sx={{alignItems: "center"}} >
|
<Grid2 container sx={{alignItems: "center"}} >
|
||||||
@@ -82,9 +81,10 @@ export default function ArrayFieldTemplate<
|
|||||||
<Grid2 size={numbered ? 11.5 : 12} ><ArrayFieldItemTemplate key={key} {...itemProps} /></Grid2>
|
<Grid2 size={numbered ? 11.5 : 12} ><ArrayFieldItemTemplate key={key} {...itemProps} /></Grid2>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
{canAdd && (
|
{ canAdd && (
|
||||||
<Grid2 container justifyContent='flex-end'>
|
<Grid2 container justifyContent='flex-end'>
|
||||||
<Grid2>
|
<Grid2>
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { getDefaultRegistry } from "@rjsf/core";
|
import { getDefaultRegistry } from "@rjsf/core";
|
||||||
import { FormContextType, getTemplate, RJSFSchema, WidgetProps } from "@rjsf/utils";
|
import { FormContextType, RJSFSchema, WidgetProps } from "@rjsf/utils";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
import ForeignKeyWidget from "./foreign-key";
|
import ForeignKeyWidget from "./foreign-key";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import React, { useState, useEffect, useContext, Fragment } from "react";
|
|||||||
import { useForm, useList, useOne } from "@refinedev/core";
|
import { useForm, useList, useOne } from "@refinedev/core";
|
||||||
import { ResourceContext } from "../../contexts/ResourceContext";
|
import { ResourceContext } from "../../contexts/ResourceContext";
|
||||||
import { CrudForm } from "../crud-form";
|
import { CrudForm } from "../crud-form";
|
||||||
|
import { ParametersContext } from "../../contexts/parameters-context";
|
||||||
|
|
||||||
export type ForeignKeyReference = {
|
export type ForeignKeyReference = {
|
||||||
resource: string,
|
resource: string,
|
||||||
@@ -21,6 +22,7 @@ export type ForeignKeySchema = RJSFSchema & {
|
|||||||
foreignKey?: {
|
foreignKey?: {
|
||||||
reference: ForeignKeyReference
|
reference: ForeignKeyReference
|
||||||
}
|
}
|
||||||
|
props? : any
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
@@ -42,10 +44,10 @@ export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = F
|
|||||||
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
props: WidgetProps<T, S, F>
|
props: WidgetProps<T, S, F>
|
||||||
) => {
|
) => {
|
||||||
if (props.schema.foreignKey === undefined) {
|
const { onChange, label, fieldId, schema } = props;
|
||||||
|
if (schema.foreignKey === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { onChange, label } = props
|
|
||||||
|
|
||||||
const [openFormModal, setOpenFormModal] = useState(false);
|
const [openFormModal, setOpenFormModal] = useState(false);
|
||||||
const [searchString, setSearchString] = useState<string>("");
|
const [searchString, setSearchString] = useState<string>("");
|
||||||
@@ -55,12 +57,19 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
|
|||||||
return () => clearTimeout(handler);
|
return () => clearTimeout(handler);
|
||||||
}, [searchString]);
|
}, [searchString]);
|
||||||
|
|
||||||
const { resource, schema, label: labelField = "label" } = props.schema.foreignKey.reference
|
const { setFieldParameters } = useContext(ParametersContext)
|
||||||
|
useEffect(() => {
|
||||||
|
if (schema.hasOwnProperty("props") && schema.props.hasOwnProperty("parametrized") && schema.props.parametrized) {
|
||||||
|
setFieldParameters(fieldId, [])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { resource, schema: fkSchema, label: labelField = "label" } = schema.foreignKey.reference
|
||||||
const { basePath } = useContext(ResourceContext)
|
const { basePath } = useContext(ResourceContext)
|
||||||
const { data, isLoading } = useList({
|
const { data, isLoading } = useList({
|
||||||
resource: `${basePath}/${resource}`,
|
resource: `${basePath}/${resource}`,
|
||||||
pagination: { current: 1, pageSize: 10, mode: "server" },
|
pagination: { current: 1, pageSize: 10, mode: "server" },
|
||||||
filters: [{ field: "label", operator: "contains", value: debouncedInputValue }],
|
filters: [{ field: "search", operator: "contains", value: debouncedInputValue }],
|
||||||
sorters: [{ field: "label", order: "asc" }],
|
sorters: [{ field: "label", order: "asc" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,13 +113,13 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
|
|||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<FormContainerNew
|
<FormContainerNew
|
||||||
schemaName={schema}
|
schemaName={fkSchema}
|
||||||
resourceBasePath={basePath}
|
resourceBasePath={basePath}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
uiSchema={{}}
|
uiSchema={{}}
|
||||||
onSuccess={(data: any) => {
|
onSuccess={(data: any) => {
|
||||||
setOpenFormModal(false)
|
setOpenFormModal(false)
|
||||||
onChange(data.data.id);
|
onChange(data.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -122,28 +131,37 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
|
|||||||
const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
props: WidgetProps<T, S, F> & { onClear: () => void }
|
props: WidgetProps<T, S, F> & { onClear: () => void }
|
||||||
) => {
|
) => {
|
||||||
const { onClear, value } = props;
|
const { onClear, value, schema, id: fieldId } = props;
|
||||||
|
|
||||||
const [openFormModal, setOpenFormModal] = React.useState(false);
|
const [openFormModal, setOpenFormModal] = React.useState(false);
|
||||||
|
|
||||||
if (props.schema.foreignKey === undefined) {
|
if (props.schema.foreignKey === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { resource, schema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
|
const { resource, schema: fkSchema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
|
||||||
const { basePath } = useContext(ResourceContext)
|
const { basePath } = useContext(ResourceContext)
|
||||||
|
|
||||||
const { data, isLoading } = useOne({
|
const { data, isLoading, isSuccess } = useOne({
|
||||||
resource: `${basePath}/${resource}`,
|
resource: `${basePath}/${resource}`,
|
||||||
id: value
|
id: value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { setFieldParameters } = useContext(ParametersContext)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuccess && schema.hasOwnProperty("props") && schema.props.hasOwnProperty("parametrized") && schema.props.parametrized) {
|
||||||
|
const record = data.data;
|
||||||
|
setFieldParameters(fieldId, extractParameters(record))
|
||||||
|
}
|
||||||
|
}, [isSuccess])
|
||||||
|
|
||||||
if (isLoading || data === undefined) {
|
if (isLoading || data === undefined) {
|
||||||
return <CircularProgress />
|
return <CircularProgress />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const record = data.data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextField label={ props.label } variant="outlined" disabled={true} value={data.data[labelField]}
|
<TextField label={ props.label } variant="outlined" disabled={true} value={record[labelField]}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@@ -158,7 +176,7 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{ displayedFields && <Preview id={value} basePath={basePath} resource={resource} displayedFields={displayedFields}/>}
|
{ displayedFields && <Preview record={record} displayedFields={displayedFields}/>}
|
||||||
<Modal
|
<Modal
|
||||||
open={openFormModal}
|
open={openFormModal}
|
||||||
onClose={() => setOpenFormModal(false)}
|
onClose={() => setOpenFormModal(false)}
|
||||||
@@ -167,7 +185,7 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
|
|||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<FormContainerEdit
|
<FormContainerEdit
|
||||||
schemaName={schema}
|
schemaName={fkSchema}
|
||||||
resourceBasePath={basePath}
|
resourceBasePath={basePath}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
uiSchema={{}}
|
uiSchema={{}}
|
||||||
@@ -233,7 +251,8 @@ const FormContainerNew = (props: FormContainerProps) => {
|
|||||||
const { schemaName, resourceBasePath, resource, uiSchema = {}, onSuccess } = props;
|
const { schemaName, resourceBasePath, resource, uiSchema = {}, onSuccess } = props;
|
||||||
const { onFinish } = useForm({
|
const { onFinish } = useForm({
|
||||||
resource: `${resourceBasePath}/${resource}`,
|
resource: `${resourceBasePath}/${resource}`,
|
||||||
action: "create"
|
action: "create",
|
||||||
|
onMutationSuccess: data => onSuccess(data.data)
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -242,25 +261,14 @@ const FormContainerNew = (props: FormContainerProps) => {
|
|||||||
schemaName={schemaName}
|
schemaName={schemaName}
|
||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
resourceBasePath={resourceBasePath}
|
resourceBasePath={resourceBasePath}
|
||||||
onSubmit={(data:any) => {
|
onSubmit={(data:any) => { onFinish(data);}}
|
||||||
onFinish(data);
|
/>
|
||||||
onSuccess(data);
|
|
||||||
}} />
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Preview = (props: {id: string, resource: string, basePath: string, displayedFields: [string]}) => {
|
const Preview = (props: {record: any, displayedFields: [string]}) => {
|
||||||
const { basePath, resource, id, displayedFields } = props
|
const { record, displayedFields } = props
|
||||||
|
|
||||||
const { data, isLoading } = useOne({
|
|
||||||
resource: `${basePath}/${resource}`,
|
|
||||||
id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading || data === undefined) {
|
|
||||||
return <CircularProgress />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
@@ -268,10 +276,29 @@ const Preview = (props: {id: string, resource: string, basePath: string, display
|
|||||||
return (
|
return (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<Grid2 size={2}><Container>{field}</Container></Grid2>
|
<Grid2 size={2}><Container>{field}</Container></Grid2>
|
||||||
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: data.data[field] }} ></Container></Grid2>
|
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: record[field] }} ></Container></Grid2>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractParameters = (obj: any)=> {
|
||||||
|
let result: string[] = [];
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
if (typeof(obj[k]) == "string") {
|
||||||
|
const matches = obj[k].match(/%[^\s.]+%/g);
|
||||||
|
if (matches) {
|
||||||
|
const filtered = matches.map((p: string | any[]) => p.slice(1,-1)) as string[]
|
||||||
|
result = result.concat(filtered);
|
||||||
|
}
|
||||||
|
} else if (typeof(obj[k]) == "object") {
|
||||||
|
if (obj[k]) {
|
||||||
|
result = result.concat(extractParameters(obj[k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, PropsWithChildren } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
type ResourceContextType = {
|
type ResourceContextType = {
|
||||||
basePath: string,
|
basePath: string,
|
||||||
|
|||||||
30
gui/rpk-gui/src/lib/crud/contexts/parameters-context.tsx
Normal file
30
gui/rpk-gui/src/lib/crud/contexts/parameters-context.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React, { createContext, PropsWithChildren, useState } from 'react';
|
||||||
|
|
||||||
|
type Parameters = {
|
||||||
|
[field: string]: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParametersContextType = {
|
||||||
|
parameters: Parameters,
|
||||||
|
setFieldParameters: (fieldName: string, parameterList: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ParametersContext = createContext<ParametersContextType>(
|
||||||
|
{} as ParametersContextType
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ParametersContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
|
||||||
|
const [parameters, setParameters] = useState<Parameters>({});
|
||||||
|
|
||||||
|
function setFieldParameters(fieldName: string, parameterList: string[]) {
|
||||||
|
let params = structuredClone(parameters)
|
||||||
|
params[fieldName] = parameterList
|
||||||
|
setParameters(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ParametersContext.Provider value={{ parameters, setFieldParameters }} >
|
||||||
|
{children}
|
||||||
|
</ParametersContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
gui/rpk-gui/src/lib/crud/hook/index.tsx
Normal file
109
gui/rpk-gui/src/lib/crud/hook/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { jsonschemaProvider } from "../providers/jsonschema-provider";
|
||||||
|
import { GridColDef, GridColumnVisibilityModel, GridValidRowModel } from "@mui/x-data-grid";
|
||||||
|
|
||||||
|
type ResourceSchemaType = "create" | "update" | "card";
|
||||||
|
|
||||||
|
export function useResourceSchema(schemaName: string, type: ResourceSchemaType) {
|
||||||
|
const [schema, setSchema] = useState({});
|
||||||
|
const [schemaLoading, setSchemaLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
let resourceSchema
|
||||||
|
if (type == "create") {
|
||||||
|
resourceSchema = await jsonschemaProvider.getCreateResourceSchema(schemaName);
|
||||||
|
} else if (type == "update") {
|
||||||
|
resourceSchema = await jsonschemaProvider.getCardResourceSchema(schemaName);
|
||||||
|
} else {
|
||||||
|
resourceSchema = await jsonschemaProvider.getUpdateResourceSchema(schemaName);
|
||||||
|
}
|
||||||
|
setSchema(resourceSchema);
|
||||||
|
setSchemaLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error while retrieving schema: ${schemaName} `, error);
|
||||||
|
setSchemaLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { schema, schemaLoading }
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnSchema<T extends GridValidRowModel> = {
|
||||||
|
columns: GridColDef<T>[],
|
||||||
|
columnVisibilityModel: GridColumnVisibilityModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnDefinition = {
|
||||||
|
field: string,
|
||||||
|
column: Partial<GridColDef>,
|
||||||
|
hide?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResourceColumns<T extends GridValidRowModel>(schemaName: string, columnDefinitions: ColumnDefinition[]) {
|
||||||
|
const [columnSchema, setColumnSchema] = useState<ColumnSchema<T>>()
|
||||||
|
const [columnLoading, setColumnLoading] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
const resourceColumns = await jsonschemaProvider.getReadResourceColumns(schemaName)
|
||||||
|
const definedColumns = computeColumnSchema<T>(columnDefinitions, resourceColumns)
|
||||||
|
setColumnSchema(definedColumns);
|
||||||
|
setColumnLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while retrieving columns schema:', error);
|
||||||
|
setColumnLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { columnSchema, columnLoading }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResourceFilter(resourceName: string, resourcePath: string) {
|
||||||
|
const [hasSearch, setHasSearch] = useState(false)
|
||||||
|
const [filtersSchema, setFiltersSchema] = useState<any[]>([])
|
||||||
|
const [filtersLoading, setFiltersLoading] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
setHasSearch(await jsonschemaProvider.hasSearch(resourcePath))
|
||||||
|
const resourceFilters = await jsonschemaProvider.getListFilters(resourceName, resourcePath)
|
||||||
|
setFiltersSchema(resourceFilters);
|
||||||
|
setFiltersLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while retrieving filter schema:', error);
|
||||||
|
setFiltersLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { hasSearch, filtersSchema, filtersLoading }
|
||||||
|
}
|
||||||
@@ -68,20 +68,140 @@ export const jsonschemaProvider = {
|
|||||||
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
|
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
|
||||||
return getColumns(`${resourceName}Read`)
|
return getColumns(`${resourceName}Read`)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getListFilters: async (resourceName: string, resourcePath: string): Promise<FilterField[]> => {
|
||||||
|
return getFilters(resourceName, resourcePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
hasSearch: async (resourcePath: string): Promise<boolean> => {
|
||||||
|
return hasSearch(resourcePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||||
|
return buildResource(await getJsonschema(), resourceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getColumns = async (resourceName: string): Promise<GridColDef[]> => {
|
const getColumns = async (resourceName: string): Promise<GridColDef[]> => {
|
||||||
return buildColumns(await getJsonschema(), resourceName)
|
return buildColumns(await getJsonschema(), resourceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: string|undefined = undefined): GridColDef[] {
|
const getFilters = async (resourceName: string, resourcePath: string): Promise<FilterField[]> => {
|
||||||
if (rawSchemas.components.schemas[resourceName] === undefined) {
|
return buildFilters(await getJsonschema(), resourceName, resourcePath);
|
||||||
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
}
|
||||||
|
|
||||||
|
type PathParameter = {
|
||||||
|
in: string,
|
||||||
|
name:string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PathSchema = {
|
||||||
|
parameters: PathParameter[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterField = {
|
||||||
|
name: string,
|
||||||
|
label: string,
|
||||||
|
type: string,
|
||||||
|
operators: { name: string, label: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Operator = {
|
||||||
|
operator: string,
|
||||||
|
fieldName: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasSearch(resourcePath: string): Promise<boolean> {
|
||||||
|
const jst = new JsonSchemaTraverser(await getJsonschema());
|
||||||
|
const pathSchema = jst.getPath(resourcePath);
|
||||||
|
for (const param of pathSchema.parameters) {
|
||||||
|
if (param.name == "search") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
return false
|
||||||
let resource = rawSchemas.components.schemas[resourceName];
|
}
|
||||||
|
|
||||||
|
function buildFilters(rawSchema: RJSFSchema, resourceName: string, resourcePath: string): FilterField[] {
|
||||||
|
const shortResourceName = shortenResourceName(resourceName);
|
||||||
|
const jst = new JsonSchemaTraverser(rawSchema);
|
||||||
|
const pathSchema = jst.getPath(resourcePath);
|
||||||
|
|
||||||
|
const seen: { [k: string]: number } = {};
|
||||||
|
let filters: FilterField[] = []
|
||||||
|
for (const param of pathSchema.parameters) {
|
||||||
|
if (param.name.indexOf("__") > -1) {
|
||||||
|
const { operator, fieldName } = processParamName(param)
|
||||||
|
if (! seen.hasOwnProperty(fieldName)) {
|
||||||
|
seen[fieldName] = filters.length;
|
||||||
|
const field = jst.getPropertyByPath(jst.getResource(`${resourceName}Read`), fieldName)
|
||||||
|
filters.push({
|
||||||
|
name: fieldName,
|
||||||
|
label: getPropertyI18nLabel(shortResourceName, fieldName),
|
||||||
|
type: getFieldFilterType(fieldName, field),
|
||||||
|
operators: [{ name: operator, label: operator }]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
filters[seen[fieldName]].operators?.push({ name: operator, label: operator });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processParamName(param: PathParameter): Operator {
|
||||||
|
const nameParts = param.name.split("__")
|
||||||
|
|
||||||
|
return {
|
||||||
|
operator: nameParts.pop() as string,
|
||||||
|
fieldName: nameParts.join("."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFieldFilterType = (fieldName: string, field: RJSFSchema): string => {
|
||||||
|
if (fieldName == "created_by" || fieldName == "updated_by") {
|
||||||
|
return "author";
|
||||||
|
} else if (Array.isArray(field)) {
|
||||||
|
let enumValues = [];
|
||||||
|
for (const f of field) {
|
||||||
|
enumValues.push(f.const)
|
||||||
|
}
|
||||||
|
return `enum(${enumValues.join("|")})`
|
||||||
|
} else if (is_enum(field) && field.enum != undefined) {
|
||||||
|
return `enum(${field.enum.join("|")})`
|
||||||
|
} else if (field.hasOwnProperty('type')) {
|
||||||
|
if (field.type == "string" && field.format == "date-time") {
|
||||||
|
return "dateTime";
|
||||||
|
}
|
||||||
|
return field.type as string;
|
||||||
|
} else if (field.hasOwnProperty('anyOf') && field.anyOf) {
|
||||||
|
for (const prop of field.anyOf) {
|
||||||
|
if (typeof prop != "boolean" && prop.type != "null") {
|
||||||
|
return prop.type as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
throw "Unimplemented field type"
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: string|undefined = undefined): GridColDef[] {
|
||||||
|
const shortResourceName = shortenResourceName(resourceName);
|
||||||
|
const jst = new JsonSchemaTraverser(rawSchemas);
|
||||||
|
let resource = structuredClone(jst.getResource(resourceName));
|
||||||
|
|
||||||
let result: GridColDef[] = [];
|
let result: GridColDef[] = [];
|
||||||
|
if (is_enum(resource) && prefix !== undefined) {
|
||||||
|
return [{
|
||||||
|
field: prefix,
|
||||||
|
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prefix)}`) as string,
|
||||||
|
type: "string"
|
||||||
|
}];
|
||||||
|
}
|
||||||
for (const prop_name in resource.properties) {
|
for (const prop_name in resource.properties) {
|
||||||
let prop = resource.properties[prop_name];
|
let prop = resource.properties[prop_name];
|
||||||
|
|
||||||
@@ -141,9 +261,7 @@ function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: str
|
|||||||
valueGetter = (value: string) => new Date(value)
|
valueGetter = (value: string) => new Date(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prop.type == "string" && prop.format == "date-time") {
|
|
||||||
|
|
||||||
}
|
|
||||||
const column: GridColDef = {
|
const column: GridColDef = {
|
||||||
field: prefix ? `${prefix}.${prop_name}` : prop_name,
|
field: prefix ? `${prefix}.${prop_name}` : prop_name,
|
||||||
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string,
|
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string,
|
||||||
@@ -156,42 +274,32 @@ function buildColumns (rawSchemas: RJSFSchema, resourceName: string, prefix: str
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
|
|
||||||
return buildResource(await getJsonschema(), resourceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertCamelToSnake(str: string): string {
|
|
||||||
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
||||||
if (rawSchemas.components.schemas[resourceName] === undefined) {
|
const shortResourceName = shortenResourceName(resourceName);
|
||||||
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
const jst = new JsonSchemaTraverser(rawSchemas);
|
||||||
}
|
let resource = structuredClone(jst.getResource(resourceName));
|
||||||
|
|
||||||
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
|
||||||
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) {
|
||||||
let prop = resource.properties[prop_name];
|
let prop = resource.properties[prop_name];
|
||||||
|
|
||||||
if (is_reference(prop)) {
|
if (is_reference(prop)) {
|
||||||
resolveReference(rawSchemas, resource, prop);
|
buildReference(rawSchemas, resource, prop);
|
||||||
} else if (is_union(prop)) {
|
} else if (is_union(prop)) {
|
||||||
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
|
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
|
||||||
for (let i in union) {
|
for (let i in union) {
|
||||||
if (is_reference(union[i])) {
|
if (is_reference(union[i])) {
|
||||||
resolveReference(rawSchemas, resource, union[i]);
|
buildReference(rawSchemas, resource, union[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (is_enum(prop)) {
|
} else if (is_enum(prop)) {
|
||||||
for (let i in prop.allOf) {
|
for (let i in prop.allOf) {
|
||||||
if (is_reference(prop.allOf[i])) {
|
if (is_reference(prop.allOf[i])) {
|
||||||
resolveReference(rawSchemas, resource, prop.allOf[i]);
|
buildReference(rawSchemas, resource, prop.allOf[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (is_array(prop) && is_reference(prop.items)) {
|
} else if (is_array(prop) && is_reference(prop.items)) {
|
||||||
resolveReference(rawSchemas, resource, prop.items);
|
buildReference(rawSchemas, resource, prop.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop.hasOwnProperty("title")) {
|
if (prop.hasOwnProperty("title")) {
|
||||||
@@ -205,16 +313,7 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
|||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rawSchema: RJSFSchema;
|
function buildReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
||||||
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) {
|
|
||||||
const subresourceName = get_reference_name(prop_reference);
|
const subresourceName = get_reference_name(prop_reference);
|
||||||
const subresource = buildResource(rawSchemas, subresourceName);
|
const subresource = buildResource(rawSchemas, subresourceName);
|
||||||
resource.components.schemas[subresourceName] = subresource;
|
resource.components.schemas[subresourceName] = subresource;
|
||||||
@@ -283,81 +382,157 @@ function get_reference_name(prop: any) {
|
|||||||
return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
|
return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_name: string): boolean {
|
function convertCamelToSnake(str: string): string {
|
||||||
if (is_array(resource)) {
|
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
|
||||||
return property_name == 'items';
|
|
||||||
} else if (is_object(resource)) {
|
|
||||||
return property_name in resource.properties!;
|
|
||||||
} else if (is_reference(resource)) {
|
|
||||||
let subresourceName = get_reference_name(resource);
|
|
||||||
return has_descendant(rawSchemas, buildResource(rawSchemas, subresourceName), property_name);
|
|
||||||
} else if (is_union(resource)) {
|
|
||||||
const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf;
|
|
||||||
if (union !== undefined) {
|
|
||||||
for (const ref of union) {
|
|
||||||
return has_descendant(rawSchemas, ref as RJSFSchema, property_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (is_enum(resource)) {
|
|
||||||
for (const ref of resource.allOf!) {
|
|
||||||
return has_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("Jsonschema format not implemented in property finder");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_descendant(rawSchemas: RJSFSchema, resource: RJSFSchema, property_name: string): RJSFSchema {
|
function shortenResourceName(resourceName: string) {
|
||||||
if (is_array(resource) && property_name == 'items') {
|
return convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
||||||
return resource.items as RJSFSchema;
|
}
|
||||||
} else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) {
|
|
||||||
return resource.properties[property_name] as RJSFSchema;
|
function getPropertyI18nLabel(shortResourceName: string, fieldName: string): string {
|
||||||
} else if (is_reference(resource)) {
|
if (meta_fields.indexOf(fieldName) > -1) {
|
||||||
let subresourceName = get_reference_name(resource);
|
return i18n.t(`schemas.${convertCamelToSnake(fieldName)}`);
|
||||||
let subresource = buildResource(rawSchemas, subresourceName);
|
}
|
||||||
return get_descendant(rawSchemas, subresource, property_name);
|
const path = `schemas.${shortResourceName}.${convertCamelToSnake(fieldName)}`
|
||||||
} else if (is_union(resource)) {
|
return i18n.t(path)
|
||||||
for (const ref of resource.oneOf!) {
|
}
|
||||||
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
|
|
||||||
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonSchemaTraverser = class {
|
||||||
|
private rawSchemas: RJSFSchema;
|
||||||
|
|
||||||
|
constructor(rawSchemas: RJSFSchema) {
|
||||||
|
this.rawSchemas = rawSchemas
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResource = (resourceName: string) => {
|
||||||
|
if (this.rawSchemas.components.schemas[resourceName] === undefined) {
|
||||||
|
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
||||||
}
|
}
|
||||||
} else if (is_enum(resource)) {
|
|
||||||
for (const ref of resource.allOf!) {
|
return this.rawSchemas.components.schemas[resourceName]
|
||||||
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
|
}
|
||||||
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
|
||||||
|
public getPath = (resourcePath: string) => {
|
||||||
|
const resourceParts = `/${resourcePath}/`.split("/");
|
||||||
|
let pathSchema: PathSchema|undefined;
|
||||||
|
for (const path in this.rawSchemas.paths) {
|
||||||
|
if (this.rawSchemas.paths[path].hasOwnProperty("get")) {
|
||||||
|
const pathParts = path.split("/")
|
||||||
|
if (pathParts.length == resourceParts.length) {
|
||||||
|
for (let i = 0; i < pathParts.length; i++) {
|
||||||
|
const isVariable = pathParts[i].slice(0,1) == "{" && pathParts[i].slice(-1) == "}";
|
||||||
|
if (! isVariable && pathParts[i] != resourceParts[i] ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == pathParts.length - 1) {
|
||||||
|
pathSchema = this.rawSchemas.paths[path].get
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (pathSchema !== undefined) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("property not found or Jsonschema format not implemented");
|
if (pathSchema === undefined) {
|
||||||
}
|
throw ("Path not found in schema");
|
||||||
|
}
|
||||||
function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{
|
return pathSchema
|
||||||
const pointFirstPosition = path.indexOf('.')
|
|
||||||
if (pointFirstPosition == -1) {
|
|
||||||
return has_descendant(rawSchemas, resource, path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
|
public hasDescendant = (resource: RJSFSchema, property_name: string): boolean => {
|
||||||
&& path_exists(
|
if (is_array(resource)) {
|
||||||
rawSchemas,
|
return property_name == 'items';
|
||||||
get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)),
|
} else if (is_object(resource)) {
|
||||||
|
return property_name in resource.properties!;
|
||||||
|
} else if (is_reference(resource)) {
|
||||||
|
let subresourceName = get_reference_name(resource);
|
||||||
|
return this.hasDescendant(this.getResource(subresourceName), property_name);
|
||||||
|
} else if (is_union(resource)) {
|
||||||
|
const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf;
|
||||||
|
if (union !== undefined) {
|
||||||
|
for (const ref of union) {
|
||||||
|
return this.hasDescendant(ref as RJSFSchema, property_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (is_enum(resource)) {
|
||||||
|
for (const ref of resource.allOf!) {
|
||||||
|
return this.hasDescendant(ref as RJSFSchema, property_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Jsonschema format not implemented in property finder");
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDescendant = (resource: RJSFSchema, property_name: string): RJSFSchema | RJSFSchema[] => {
|
||||||
|
if (is_array(resource) && property_name == 'items') {
|
||||||
|
return resource.items as RJSFSchema;
|
||||||
|
} else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) {
|
||||||
|
const prop = resource.properties[property_name];
|
||||||
|
if (is_reference(prop)) {
|
||||||
|
const subresourceName = get_reference_name(prop);
|
||||||
|
return this.getResource(subresourceName);
|
||||||
|
}
|
||||||
|
return prop as RJSFSchema;
|
||||||
|
} else if (is_reference(resource)) {
|
||||||
|
let subresourceName = get_reference_name(resource);
|
||||||
|
let subresource = this.getResource(subresourceName);
|
||||||
|
return this.getDescendant(subresource, property_name);
|
||||||
|
} else if (is_union(resource)) {
|
||||||
|
let descendants: RJSFSchema[] = [];
|
||||||
|
for (const ref of resource.oneOf!) {
|
||||||
|
if (this.hasDescendant(ref as RJSFSchema, property_name)) {
|
||||||
|
descendants.push(this.getDescendant(ref as RJSFSchema, property_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (descendants.length > 0) {
|
||||||
|
return descendants;
|
||||||
|
}
|
||||||
|
} else if (is_enum(resource)) {
|
||||||
|
for (const ref of resource.allOf!) {
|
||||||
|
if (this.hasDescendant(ref as RJSFSchema, property_name)) {
|
||||||
|
return this.getDescendant(ref as RJSFSchema, property_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("property not found or Jsonschema format not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
public pathExists = (resource: RJSFSchema, path: string): boolean => {
|
||||||
|
const pointFirstPosition = path.indexOf('.')
|
||||||
|
if (pointFirstPosition == -1) {
|
||||||
|
return this.hasDescendant(resource, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hasDescendant(resource, path.substring(0, pointFirstPosition))
|
||||||
|
&& this.pathExists(
|
||||||
|
this.getDescendant(resource, path.substring(0, pointFirstPosition)),
|
||||||
|
path.substring(pointFirstPosition + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPropertyByPath = (resource: RJSFSchema, path: string): RJSFSchema => {
|
||||||
|
const pointFirstPosition = path.indexOf('.')
|
||||||
|
if (pointFirstPosition == -1) {
|
||||||
|
return this.getDescendant(resource, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getPropertyByPath(
|
||||||
|
this.getDescendant(
|
||||||
|
resource,
|
||||||
|
path.substring(0, pointFirstPosition)
|
||||||
|
),
|
||||||
path.substring(pointFirstPosition + 1)
|
path.substring(pointFirstPosition + 1)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function get_property_by_path(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): RJSFSchema {
|
|
||||||
const pointFirstPosition = path.indexOf('.')
|
|
||||||
if (pointFirstPosition == -1) {
|
|
||||||
return get_descendant(rawSchemas, resource, path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return get_property_by_path(
|
|
||||||
rawSchemas,
|
|
||||||
get_descendant(
|
|
||||||
rawSchemas,
|
|
||||||
resource,
|
|
||||||
path.substring(0, pointFirstPosition)
|
|
||||||
),
|
|
||||||
path.substring(pointFirstPosition + 1)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
184
gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx
Normal file
184
gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import { FirmContext } from "../../../contexts/FirmContext";
|
||||||
|
import { Fragment, useContext } from "react";
|
||||||
|
import { Box, Grid2, styled } from "@mui/material";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export type FilterField = {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterFormProps = {
|
||||||
|
values: {[field_name: string]: { [operator: string]: string }}
|
||||||
|
fields: FilterField[]
|
||||||
|
onChange: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterForm = (props: FilterFormProps) => {
|
||||||
|
const { fields, values, onChange } = props;
|
||||||
|
|
||||||
|
let currentValue = values
|
||||||
|
|
||||||
|
const formField = fields.filter(f => f.name != "search").map((f, index) =>
|
||||||
|
<Fragment key={`${f.name}-${index}`} >
|
||||||
|
{ f.name == "created_at" && <Box width="100%" /> }
|
||||||
|
<Grid2 size={6}>
|
||||||
|
<FilterFormField
|
||||||
|
field={f}
|
||||||
|
value={values.hasOwnProperty(f.name) ? values[f.name] : {}}
|
||||||
|
onChange={(value) => {
|
||||||
|
for (const op in value) {
|
||||||
|
if (value[op] == null || value[op] == "") {
|
||||||
|
if (currentValue.hasOwnProperty(f.name)) {
|
||||||
|
if (currentValue[f.name].hasOwnProperty(op)) {
|
||||||
|
delete currentValue[f.name][op];
|
||||||
|
}
|
||||||
|
if (Object.entries(currentValue[f.name]).length == 0) {
|
||||||
|
delete currentValue[f.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (! currentValue.hasOwnProperty(f.name)) {
|
||||||
|
currentValue[f.name] = {};
|
||||||
|
}
|
||||||
|
currentValue[f.name][op] = value[op];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChange(currentValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
{formField}
|
||||||
|
</Grid2>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperatorValue = { [operator: string]: string }
|
||||||
|
|
||||||
|
type FilterFormFieldProps = {
|
||||||
|
field: FilterField
|
||||||
|
value: OperatorValue
|
||||||
|
onChange: (value: { [operator: string]: string | null }) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterFormField = (props: FilterFormFieldProps) => {
|
||||||
|
const { field, value, onChange } = props;
|
||||||
|
|
||||||
|
if (field.type == "string") {
|
||||||
|
const defaultValue = value.hasOwnProperty('ilike') ? value['ilike'] : undefined
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={(event) => onChange({"ilike": event.target.value})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (field.type == "dateTime") {
|
||||||
|
return (
|
||||||
|
<FilterFieldDateRange field={field} value={value} onChange={onChange} />
|
||||||
|
)
|
||||||
|
} else if (field.type == "author") {
|
||||||
|
return (
|
||||||
|
<FilterFieldAuthor field={field} value={value} onChange={onChange} />
|
||||||
|
);
|
||||||
|
} else if (field.type.slice(0, 4) == "enum") {
|
||||||
|
const values = field.type.slice(5,-1).split("|");
|
||||||
|
const defaultValue = value.hasOwnProperty("in") ? [{
|
||||||
|
value: value["in"],
|
||||||
|
label: value["in"]
|
||||||
|
}] : undefined;
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={values.map(opt => {
|
||||||
|
return {
|
||||||
|
value: opt,
|
||||||
|
label: opt
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
onChange={(event, value) => onChange({ "in": value.map(v => v.value).join(",") })}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label={field.label}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw("Unsupported field filter type");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type FilterFieldAuthorProp = FilterFormFieldProps
|
||||||
|
|
||||||
|
const FilterFieldAuthor = (props: FilterFieldAuthorProp) => {
|
||||||
|
const { field, onChange } = props;
|
||||||
|
const { partnerMap } = useContext(FirmContext)
|
||||||
|
|
||||||
|
if (partnerMap == undefined) {
|
||||||
|
throw "Can't use author filter outside of the context of a firm";
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = []
|
||||||
|
for(let key of Array.from(partnerMap.keys()) ) {
|
||||||
|
options.push({
|
||||||
|
id: key,
|
||||||
|
label: partnerMap.get(key)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
renderInput={(params) => <TextField {...params} label={field.label} />}
|
||||||
|
options={options}
|
||||||
|
onChange={(event, value) => onChange({ "in": value.length == 0 ? null : value.map(v => v.id).join(",") })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterFieldDateRange = (props: FilterFormFieldProps) => {
|
||||||
|
const { field, value, onChange } = props;
|
||||||
|
|
||||||
|
const defaultAfterValue = value.hasOwnProperty('gte') ? dayjs(value['gte']) : undefined;
|
||||||
|
const defaultBeforeValue = value.hasOwnProperty('lte') ? dayjs(value['lte']) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row">
|
||||||
|
<DateTimePicker
|
||||||
|
name={field.name}
|
||||||
|
label={`${field.label} After:`}
|
||||||
|
slotProps={{ textField: { fullWidth: true }, field: { clearable: true }}}
|
||||||
|
defaultValue={defaultAfterValue}
|
||||||
|
onChange={(value) => onChange({'gte': value === null ? null : value.toJSON()})}
|
||||||
|
/>
|
||||||
|
<DateTimePicker
|
||||||
|
name={field.name}
|
||||||
|
label={`${field.label} Before:`}
|
||||||
|
slotProps={{ textField: { fullWidth: true }, field: { clearable: true } }}
|
||||||
|
defaultValue={defaultBeforeValue}
|
||||||
|
onChange={(value) => onChange({'lte': value === null ? null : value.toJSON()})}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterForm;
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ export const ContractRoutes = () => {
|
|||||||
const ListContract = () => {
|
const ListContract = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", column: { flex: 1 }},
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "status", column: { width: 160 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Contract> resource={`contracts`} schemaName={"Contract"} columns={columns} />
|
return <List<Contract> resource={`contracts`} schemaName={"Contract"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ export const DraftRoutes = () => {
|
|||||||
|
|
||||||
const ListDraft = () => {
|
const ListDraft = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", column: { flex: 1 }},
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "status", column: { width: 160 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Draft> resource={`contracts/drafts`} columns={columns} schemaName={"Contract"} />
|
return <List<Draft> resource={`contracts/drafts`} columns={columns} schemaName={"Contract"} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const ProvisionRoutes = () => {
|
|||||||
const ListProvision = () => {
|
const ListProvision = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", column: { flex: 1 }},
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} columns={columns} />
|
return <List<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const TemplateRoutes = () => {
|
|||||||
const ListTemplate = () => {
|
const ListTemplate = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", column: { flex: 1 }},
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} columns={columns} />
|
return <List<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { Link, useNavigate } from "react-router"
|
import { Link, useNavigate } from "react-router"
|
||||||
import { UiSchema } from "@rjsf/utils";
|
import { UiSchema } from "@rjsf/utils";
|
||||||
import { useTranslation } from "@refinedev/core";
|
import { CrudFilter, useTranslation } from "@refinedev/core";
|
||||||
import { List as RefineList, useDataGrid } from "@refinedev/mui";
|
import { List as RefineList, useDataGrid } from "@refinedev/mui";
|
||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid";
|
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid";
|
||||||
import { FirmContext } from "../../../contexts/FirmContext";
|
import { FirmContext } from "../../../contexts/FirmContext";
|
||||||
import CrudList from "../../../lib/crud/components/crud-list";
|
import CrudList from "../../../lib/crud/components/crud-list";
|
||||||
|
import CrudFilters, { OnChangeValue } from "../../../lib/crud/components/crud-filters";
|
||||||
|
import { CrudOperators } from "@refinedev/core/src/contexts/data/types";
|
||||||
|
|
||||||
type ListProps = {
|
type ListProps = {
|
||||||
resource: string,
|
resource: string,
|
||||||
@@ -27,13 +29,34 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
|
|||||||
const { currentFirm } = useContext(FirmContext);
|
const { currentFirm } = useContext(FirmContext);
|
||||||
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
||||||
|
|
||||||
const { dataGridProps, tableQueryResult } = useDataGrid<T>({
|
const { dataGridProps, tableQuery, setFilters } = useDataGrid<T>({
|
||||||
resource: `${resourceBasePath}/${resource}`,
|
resource: `${resourceBasePath}/${resource}`,
|
||||||
});
|
});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (tableQueryResult.error?.status == 404) {
|
if (tableQuery.error?.status == 404) {
|
||||||
throw tableQueryResult.error
|
throw tableQuery.error
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFilterChange = (value: OnChangeValue) => {
|
||||||
|
let newFilters: CrudFilter[] = []
|
||||||
|
if (value.search != null) {
|
||||||
|
newFilters.push({
|
||||||
|
field: "search",
|
||||||
|
operator: "eq",
|
||||||
|
value: value.search
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const filterName in value.filters) {
|
||||||
|
for (const operator in value.filters[filterName]) {
|
||||||
|
newFilters.push({
|
||||||
|
field: filterName,
|
||||||
|
operator: operator as Exclude<CrudOperators, "or" | "and">,
|
||||||
|
value: value.filters[filterName][operator]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFilters(newFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,6 +64,11 @@ const List = <T extends GridValidRowModel>(props: ListProps) => {
|
|||||||
<Link to={"create"} >
|
<Link to={"create"} >
|
||||||
<Button>{t("buttons.create")}</Button>
|
<Button>{t("buttons.create")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
<CrudFilters
|
||||||
|
resourceName={schemaName}
|
||||||
|
resourcePath={`${resourceBasePath}/${resource}`}
|
||||||
|
onChange={onFilterChange}
|
||||||
|
/>
|
||||||
<CrudList
|
<CrudList
|
||||||
dataGridProps={dataGridProps}
|
dataGridProps={dataGridProps}
|
||||||
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
|
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
|
||||||
|
|||||||
@@ -61,8 +61,12 @@ const dataProvider: DataProvider = {
|
|||||||
|
|
||||||
if (filters && filters.length > 0) {
|
if (filters && filters.length > 0) {
|
||||||
filters.forEach((filter) => {
|
filters.forEach((filter) => {
|
||||||
if ("field" in filter && filter.value && filter.operator === "contains") {
|
if ("field" in filter) {
|
||||||
params.append(filter.field + "__ilike", filter.value);
|
if (filter.field == "search") {
|
||||||
|
params.append("search", filter.value);
|
||||||
|
} else {
|
||||||
|
params.append(`${filter.field.replace(".", "__")}__${filter.operator}`, filter.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user