Compare commits

9 Commits

Author SHA1 Message Date
d2bea9180a Adding Signature link, need a destination 2025-05-15 01:52:58 +02:00
f0b241f37d Adding preview du contract drafts 2025-05-15 00:34:48 +02:00
f76f4d5673 Refactoring schema fetching in hooks 2025-05-14 20:44:03 +02:00
72b6f26ebc Minor refacto 2025-05-14 18:27:15 +02:00
b06ce4eefd Correcting typo in foreignkey 2025-05-14 18:26:52 +02:00
49317e905b Handling Variables in form 2025-05-14 18:26:24 +02:00
189c896e60 New columns for Template lists 2025-05-13 21:22:48 +02:00
4ae0b321b9 Correcting foreign key short comings 2025-05-13 21:22:25 +02:00
5a3b87e82c Correcting key position in looping component 2025-05-13 20:31:00 +02:00
18 changed files with 492 additions and 173 deletions

View File

@@ -3,6 +3,7 @@ import os
import base64 import base64
from uuid import UUID from uuid import UUID
from beanie import PydanticObjectId
from fastapi import APIRouter, HTTPException, Request, Depends from fastapi import APIRouter, HTTPException, Request, Depends
from fastapi.responses import HTMLResponse, FileResponse from fastapi.responses import HTMLResponse, FileResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -18,24 +19,22 @@ from firm.template.models import ProvisionTemplate
from firm.contract.models import ContractDraft, Contract, ContractStatus, replace_variables_in_value from firm.contract.models import ContractDraft, Contract, ContractStatus, replace_variables_in_value
async def build_model(model): async def build_model(db, model):
parties = [] parties = []
for p in model.parties: for p in model.parties:
party = { party = {
"entity": await Entity.get(p.entity_id), "entity": await Entity.get(db, p.entity_id),
"part": p.part "part": p.part
} }
if p.representative_id: if p.representative_id:
party['representative'] = await Entity.get(p.representative_id) party['representative'] = await Entity.get(db, p.representative_id)
parties.append(party) parties.append(party)
model.parties = parties
provisions = [] provisions = []
for p in model.provisions: for p in model.provisions:
if p.provision.type == "template": if p.provision.type == "template":
provision = await ProvisionTemplate.get(p.provision.provision_template_id) provision = await ProvisionTemplate.get(db, p.provision.provision_template_id)
else: else:
provision = p.provision provision = p.provision
@@ -43,16 +42,16 @@ async def build_model(model):
provision.body = replace_variables_in_value(model.variables, provision.body) provision.body = replace_variables_in_value(model.variables, provision.body)
provisions.append(provision) provisions.append(provision)
model.provisions = provisions model_dict = model.dict()
model_dict['parties'] = parties
model = model.dict() model_dict['provisions'] = provisions
model['location'] = "Los Santos, SA" model_dict['location'] = "Los Santos, SA"
model['date'] = datetime.date(1970, 1, 1) model_dict['date'] = datetime.date(1970, 1, 1)
model['lawyer'] = {'entity_data': { model_dict['lawyer'] = {'entity_data': {
"firstname": "prénom avocat", "firstname": "prénom avocat",
"lastname": "nom avocat", "lastname": "nom avocat",
}} }}
return model return model_dict
BASE_PATH = Path(__file__).resolve().parent BASE_PATH = Path(__file__).resolve().parent
@@ -87,8 +86,14 @@ def retrieve_signature_png(filepath):
preview_router = APIRouter() preview_router = APIRouter()
@preview_router.get("/draft/{draft_id}", response_class=HTMLResponse, tags=["Contract Draft"]) @preview_router.get("/draft/{draft_id}", response_class=HTMLResponse, tags=["Contract Draft"])
async def preview_draft(draft_id: str, reg=Depends(get_tenant_registry)) -> str: async def preview_draft(draft_id: PydanticObjectId, reg=Depends(get_tenant_registry)) -> str:
draft = await build_model(await ContractDraft.get(reg.db, draft_id)) record = await ContractDraft.get(reg.db, draft_id)
if not record:
raise HTTPException(
status_code=404,
detail=f"Contract Draft record not found!"
)
draft = await build_model(reg.db, record)
return await render_print('', draft) return await render_print('', draft)
@@ -104,7 +109,7 @@ async def preview_contract_by_signature(signature_id: UUID, reg=Depends(get_tena
@preview_router.get("/{contract_id}", response_class=HTMLResponse, tags=["Contract"]) @preview_router.get("/{contract_id}", response_class=HTMLResponse, tags=["Contract"])
async def preview_contract(contract_id: str, reg=Depends(get_tenant_registry)) -> str: async def preview_contract(contract_id: PydanticObjectId, reg=Depends(get_tenant_registry)) -> str:
contract = await Contract.get(reg.db, contract_id) contract = await Contract.get(reg.db, contract_id)
for p in contract.parties: for p in contract.parties:
if p.signature_affixed: if p.signature_affixed:
@@ -115,7 +120,7 @@ async def preview_contract(contract_id: str, reg=Depends(get_tenant_registry)) -
print_router = APIRouter() print_router = APIRouter()
@print_router.get("/pdf/{contract_id}", response_class=FileResponse, tags=["Contract"]) @print_router.get("/pdf/{contract_id}", response_class=FileResponse, tags=["Contract"])
async def create_pdf(contract_id: str, reg=Depends(get_tenant_registry)) -> str: async def create_pdf(contract_id: PydanticObjectId, reg=Depends(get_tenant_registry)) -> str:
contract = await Contract.get(reg.db, contract_id) contract = await Contract.get(reg.db, contract_id)
contract_path = "media/contracts/{}.pdf".format(contract_id) contract_path = "media/contracts/{}.pdf".format(contract_id)
if not os.path.isfile(contract_path): if not os.path.isfile(contract_path):
@@ -142,7 +147,7 @@ async def create_pdf(contract_id: str, reg=Depends(get_tenant_registry)) -> str:
@print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse, tags=["Signature"]) @print_router.get("/opengraph/{signature_id}", response_class=HTMLResponse, tags=["Signature"])
async def get_signature_opengraph(signature_id: str, request: Request, reg=Depends(get_tenant_registry)) -> str: async def get_signature_opengraph(signature_id: UUID, request: Request, reg=Depends(get_tenant_registry)) -> str:
contract = await Contract.find_by_signature_id(reg.db, signature_id) contract = await Contract.find_by_signature_id(reg.db, signature_id)
signature = contract.get_signature(signature_id) signature = contract.get_signature(signature_id)
template = templates.get_template("opengraph.html") template = templates.get_template("opengraph.html")

View File

@@ -57,7 +57,7 @@
<div class="footer"> <div class="footer">
<hr/> <hr/>
<p>À {{ contract.location }} le {{ contract.date.strftime('%d/%m/%Y') }}</p> <p>À {{ contract.location }} le {{ contract.date.strftime('%d/%m/%Y') }}</p>
<p class="mention">(Signatures précédée de la mention « Lu et approuvé »)</p> <p class="mention">(Signatures précédées de la mention « Lu et approuvé »)</p>
<table class="signatures"> <table class="signatures">
<tr> <tr>
{% for party in contract.parties %} {% for party in contract.parties %}

View File

@@ -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"

View File

@@ -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,6 +39,7 @@ export const BaseForm: React.FC<BaseFormProps> = (props) => {
return ( return (
<ResourceContext.Provider value={{basePath: resourceBasePath}} > <ResourceContext.Provider value={{basePath: resourceBasePath}} >
<ParametersContextProvider>
<Form <Form
schema={schema} schema={schema}
uiSchema={uiSchema === undefined ? {} : uiSchema} uiSchema={uiSchema === undefined ? {} : uiSchema}
@@ -49,6 +53,7 @@ export const BaseForm: React.FC<BaseFormProps> = (props) => {
onChange={(e, id) => onChange != undefined && onChange(e.formData)} onChange={(e, id) => onChange != undefined && onChange(e.formData)}
children={children} children={children}
/> />
</ParametersContextProvider>
</ResourceContext.Provider> </ResourceContext.Provider>
) )
} }

View File

@@ -1,10 +1,8 @@
import { useEffect, useState } from "react";
import { jsonschemaProvider } from "../providers/jsonschema-provider";
import { Accordion, AccordionDetails, AccordionSummary, CircularProgress } from "@mui/material"; import { Accordion, AccordionDetails, AccordionSummary, CircularProgress } from "@mui/material";
import FilterForm from "../../filter-form/components/filter-form"; import FilterForm from "../../filter-form/components/filter-form";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import { useSearchParams } from "react-router";
import { GridExpandMoreIcon } from "@mui/x-data-grid"; import { GridExpandMoreIcon } from "@mui/x-data-grid";
import { useResourceFilter } from "../hook";
export type OnChangeValue = { export type OnChangeValue = {
search: string | null search: string | null
@@ -19,24 +17,7 @@ type CrudFiltersProps = {
const CrudFilters = (props: CrudFiltersProps) => { const CrudFilters = (props: CrudFiltersProps) => {
const { resourceName, resourcePath, onChange } = props const { resourceName, resourcePath, onChange } = props
const { hasSearch, filtersSchema, filtersLoading } = useResourceFilter(resourceName, resourcePath)
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 fetching data:', error);
setFiltersLoading(false);
}
};
fetchSchema();
}, []);
if (filtersLoading) { if (filtersLoading) {
return <CircularProgress /> return <CircularProgress />
@@ -81,8 +62,6 @@ type SearchFilter = {
const SearchFilter = (props: SearchFilter) => { const SearchFilter = (props: SearchFilter) => {
const {value, onChange} = props; const {value, onChange} = props;
const [searchParams, setSearchParams] = useSearchParams();
return ( return (
<TextField <TextField
label="schemas.search" label="schemas.search"

View File

@@ -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 />

View File

@@ -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,24 +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);
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 />
} }
@@ -69,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;

View 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)
}}
>
&nbsp;
</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
}

View File

@@ -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;
@@ -82,7 +81,8 @@ 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'>

View File

@@ -1,7 +1,10 @@
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 { Button, InputAdornment } from "@mui/material";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import TextField from "@mui/material/TextField";
import CopyAllIcon from '@mui/icons-material/CopyAll';
import ForeignKeyWidget from "./foreign-key"; import ForeignKeyWidget from "./foreign-key";
import RichtextWidget from "./richtext"; import RichtextWidget from "./richtext";
@@ -18,8 +21,37 @@ export default function CrudTextWidget<T = any, S extends CrudTextRJSFSchema = C
return <Typography >{schema.const as string}</Typography>; return <Typography >{schema.const as string}</Typography>;
} else if (schema.props?.hasOwnProperty("richtext")) { } else if (schema.props?.hasOwnProperty("richtext")) {
return <RichtextWidget {...props} />; return <RichtextWidget {...props} />;
} else if (schema.props?.hasOwnProperty("display") && schema.props.display == "signature-link") {
return <SignatureLink {...props} />
} else { } else {
const { widgets: { TextWidget } } = getDefaultRegistry<T,S,F>(); const { widgets: { TextWidget } } = getDefaultRegistry<T,S,F>();
return <TextWidget {...props} />; return <TextWidget {...props} />;
} }
} }
const SignatureLink = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(props: WidgetProps<T, S, F> )=> {
const { label, value } = props;
const basePath = "/contracts/signature/";
const url = location.origin + basePath + value
return <TextField
label={ label }
variant="outlined"
disabled={true}
value={url}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<Button
variant="outlined"
onClick={() => navigator.clipboard.writeText(url)}
color="primary"
>
<CopyAllIcon />
</Button>
</InputAdornment>),
},
}}
/>
}

View File

@@ -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
}

View File

@@ -1,4 +1,4 @@
import React, { createContext, PropsWithChildren } from 'react'; import { createContext } from 'react';
type ResourceContextType = { type ResourceContextType = {
basePath: string, basePath: string,

View 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>
);
}

View 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 == "card") {
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 }
}

View File

@@ -2,7 +2,7 @@ import TextField from "@mui/material/TextField";
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
import Autocomplete from "@mui/material/Autocomplete"; import Autocomplete from "@mui/material/Autocomplete";
import { FirmContext } from "../../../contexts/FirmContext"; import { FirmContext } from "../../../contexts/FirmContext";
import { useContext } from "react"; import { Fragment, useContext } from "react";
import { Box, Grid2, styled } from "@mui/material"; import { Box, Grid2, styled } from "@mui/material";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -25,9 +25,9 @@ const FilterForm = (props: FilterFormProps) => {
let currentValue = values let currentValue = values
const formField = fields.filter(f => f.name != "search").map((f, index) => const formField = fields.filter(f => f.name != "search").map((f, index) =>
<> <Fragment key={`${f.name}-${index}`} >
{ f.name == "created_at" && <Box width="100%" key={`created_at_break-${index}`} /> } { f.name == "created_at" && <Box width="100%" /> }
<Grid2 size={6} key={`${f.name}-${index}`} > <Grid2 size={6}>
<FilterFormField <FilterFormField
field={f} field={f}
value={values.hasOwnProperty(f.name) ? values[f.name] : {}} value={values.hasOwnProperty(f.name) ? values[f.name] : {}}
@@ -53,7 +53,7 @@ const FilterForm = (props: FilterFormProps) => {
}} }}
/> />
</Grid2> </Grid2>
</> </Fragment>
); );
return ( return (

View File

@@ -1,6 +1,9 @@
import { Navigate, Route, Routes, useParams } from "react-router"; import dayjs from "dayjs";
import { CircularProgress } from "@mui/material";
import React, { useContext, useState } from "react"; import React, { useContext, useState } from "react";
import { Navigate, Route, Routes, useParams } from "react-router";
import { Box, Button, CircularProgress, Container, DialogContent, Modal, Paper } from "@mui/material";
import Stack from "@mui/material/Stack";
import PreviewIcon from '@mui/icons-material/Preview';
import { useOne, useTranslation } from "@refinedev/core"; import { useOne, useTranslation } from "@refinedev/core";
import { BaseForm } from "../../lib/crud/components/base-form"; import { BaseForm } from "../../lib/crud/components/base-form";
import { ForeignKeyReference, ForeignKeySchema } from "../../lib/crud/components/widgets/foreign-key"; import { ForeignKeyReference, ForeignKeySchema } from "../../lib/crud/components/widgets/foreign-key";
@@ -10,7 +13,6 @@ 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";
import { Contract } from "./ContractRoutes"; import { Contract } from "./ContractRoutes";
import dayjs from "dayjs";
type Draft = { type Draft = {
id: string, id: string,
@@ -50,7 +52,7 @@ const EditDraft = () => {
return <CircularProgress /> return <CircularProgress />
} }
if (!data?.data) { if (record_id == undefined || !data?.data) {
return <Navigate to="../" /> return <Navigate to="../" />
} }
@@ -63,12 +65,56 @@ const EditDraft = () => {
return ( return (
<> <>
<DraftPreview resourceBasePath={resourceBasePath} recordId={record_id}/>
<Edit<Draft> resource={"contracts/drafts"} schemaName={"ContractDraft"} uiSchema={uiSchema} /> <Edit<Draft> resource={"contracts/drafts"} schemaName={"ContractDraft"} uiSchema={uiSchema} />
<ContractCreate draft={draft}></ContractCreate> <ContractCreate draft={draft}></ContractCreate>
</> </>
) )
} }
const DraftPreview = (props: {resourceBasePath: string, recordId: string}) => {
const { resourceBasePath, recordId } = props
const [openPreviewModal, setOpenPreviewModal] = React.useState(false);
return (
<>
<Button variant="outlined" onClick={() => setOpenPreviewModal(true)} color="primary" >
<PreviewIcon />Preview
</Button>
<Modal
open={openPreviewModal}
onClose={() => setOpenPreviewModal(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<DialogContent>
<Container>
<Paper>
<Stack
direction={"row"}
spacing={2}
sx={{
justifyContent: "center",
alignItems: "center",
}}
>
<Box padding={"45px"}>
<iframe
src={`/api/v1/${resourceBasePath}/contracts/preview/draft/${recordId}`}
width="675px"
height="955px"
style={{ backgroundColor: "white", border: "1px solid black" }}
/>
</Box>
</Stack>
</Paper>
</Container>
</DialogContent>
</Modal>
</>
)
}
const ContractCreate = (props: { draft: any}) => { const ContractCreate = (props: { draft: any}) => {
const { translate: t } = useTranslation(); const { translate: t } = useTranslation();
const { draft } = props; const { draft } = props;

View File

@@ -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} />
} }

View File

@@ -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} />
} }