Compare commits

..

2 Commits

Author SHA1 Message Date
aff1586c9b WIP - starting to implement I18n 2025-04-25 17:34:01 +02:00
567c2c0853 Default exporting providers 2025-04-25 17:33:24 +02:00
71 changed files with 1421 additions and 5111 deletions

View File

@@ -4,11 +4,9 @@ from enum import Enum
from uuid import UUID
from beanie import PydanticObjectId
from pydantic import BaseModel, Field, ConfigDict
from pydantic.json_schema import SkipJsonSchema
from pydantic import BaseModel, Field
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey, \
CrudDocumentConfig
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
from firm.core.filter import Filter, FilterSchema
from firm.entity.models import Entity
@@ -18,7 +16,6 @@ class ContractStatus(str, Enum):
signed = 'signed'
printed = 'printed'
executed = 'executed'
canceled = 'canceled'
class ContractDraftStatus(str, Enum):
@@ -28,13 +25,30 @@ class ContractDraftStatus(str, Enum):
class DraftParty(BaseModel):
model_config = ConfigDict(title='Partie')
entity_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Partie")
entity_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Partie"
)
part: str = Field(title="Rôle")
representative_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Représentant")
representative_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Représentant"
)
entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, )
class Config:
title = 'Partie'
class Party(BaseModel):
@@ -50,32 +64,39 @@ class ContractProvisionType(Enum):
template = 'template'
class ProvisionGenuine(BaseModel):
model_config = ConfigDict(title='Clause personalisée')
type: Literal['genuine'] = ContractProvisionType.genuine
title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
class Config:
title = 'Clause personalisée'
class ContractProvisionTemplateReference(BaseModel):
model_config = ConfigDict(title='Template de clause')
type: Literal['template'] = ContractProvisionType.template
provision_template_id: PydanticObjectId = ForeignKey(
"templates/provisions",
"ProvisionTemplate",
displayed_fields=['title', 'body'],
provision_template_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "templates/provisions",
"schema": "ProvisionTemplate",
"displayedFields": ['title', 'body']
},
},
props={"parametrized": True},
default=None,
default="",
title="Template de clause"
)
class Config:
title = 'Template de clause'
class DraftProvision(BaseModel):
model_config = ConfigDict(title='Clause')
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
class Config:
title = 'Clause'
class Provision(BaseModel):
title: str = RichtextSingleline(title="Titre")
@@ -89,16 +110,34 @@ class ContractDraft(CrudDocument):
"""
Brouillon de contrat à remplir
"""
model_config = ConfigDict(title='Brouillon de contrat')
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[DraftParty] = Field(title="Parties")
provisions: List[DraftProvision] = Field(title='Clauses')
variables: List[DictionaryEntry] = Field(default=[], title='Variables')
provisions: List[DraftProvision] = Field(
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
variables: List[DictionaryEntry] = Field(
default=[],
format="dictionary",
title='Variables'
)
status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
todo: List[str] = Field(default=[], title="Reste à faire")
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
bson_encoders = {
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
class Config:
title = 'Brouillon de contrat'
async def check_is_ready(self, db):
if self.status == ContractDraftStatus.published:
return
@@ -134,24 +173,16 @@ class ContractDraft(CrudDocument):
update = ContractDraftUpdateStatus(status=status)
await self.update(db, self, update)
def compute_label(self) -> str:
return f"{self.name} - {self.title}"
class Contract(CrudDocument):
"""
Contrat publié. Les contrats ne peuvent pas être modifiés.
Ils peuvent seulement être signés par les parties et imprimés par l'avocat
"""
model_config = ConfigDict(title='Contrat')
document_config = CrudDocumentConfig(
indexes=["parties.signature_uuid"],
)
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[Party] = Field(title="Parties")
provisions: List[Provision] = Field(
props={"items_per_row": "1", "numbered": True},
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
@@ -167,6 +198,15 @@ class Contract(CrudDocument):
contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}"
return contract_label
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
bson_encoders = {
datetime.date: lambda dt: dt if hasattr(dt, 'hour')
else datetime.datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
@classmethod
async def find_by_signature_id(cls, db, signature_id: UUID):
request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}}
@@ -210,15 +250,15 @@ def replace_variables_in_value(variables, value: str):
class ContractDraftFilters(FilterSchema):
status__in: Optional[list[str]] = None
status: Optional[str] = None
class Constants(Filter.Constants):
model = ContractDraft
search_model_fields = ["label"]
search_model_fields = ["label", "status"]
class ContractFilters(FilterSchema):
status__in: Optional[list[str]] = None
status: Optional[str] = None
class Constants(Filter.Constants):
model = Contract
search_model_fields = ["label"]
search_model_fields = ["label", "status"]

View File

@@ -31,7 +31,8 @@ async def create(schema: ContractCreate, reg=Depends(get_authed_tenant_registry)
contract_dict = schema.model_dump()
del(contract_dict['draft_id'])
contract_dict['lawyer'] = reg.partner.model_dump()
lawyer = await Entity.get(reg.db, reg.user.entity_id)
contract_dict['lawyer'] = lawyer.model_dump()
contract_dict['name'] = draft.name
contract_dict['title'] = draft.title

View File

@@ -1,14 +1,13 @@
import datetime
from typing import List
from beanie import PydanticObjectId
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, Field
from firm.contract.models import ContractDraft, DraftProvision, DraftParty, Contract
from firm.entity.models import Entity
from firm.core.schemas import Writer, Reader
from firm.core.models import DictionaryEntry, ForeignKey
from firm.core.models import DictionaryEntry
class ContractDraftRead(Reader, ContractDraft):
@@ -20,12 +19,12 @@ class ContractDraftCreate(Writer):
title: str = Field(title='Titre')
parties: List[DraftParty] = Field(title='Parties')
provisions: List[DraftProvision] = Field(
props={"items_per_row": "1", "numbered": True},
props={"items-per-row": "1", "numbered": True},
title='Clauses'
)
variables: List[DictionaryEntry] = Field(
default=[],
props={"display": "dictionary"},
format="dictionary",
title='Variables'
)
@@ -42,34 +41,34 @@ class ContractDraftUpdate(ContractDraftCreate):
class ForeignEntityRead(BaseModel):
model_config = ConfigDict(title='Avocat')
label: str
class Config:
title = "Avocat"
class PartyRead(BaseModel):
model_config = ConfigDict(title='Partie')
signature_affixed: bool = Field(title='Signature apposée?')
signature_uuid: str = Field(props={"display": "signature-link"}, title="Lien vers signature")
signature_uuid: str = Field(format="signature-link", title="Lien vers signature")
part: str = Field(title='Rôle')
entity: ForeignEntityRead = Field(title='Client')
class Config:
title = "Partie"
class ContractRead(Reader, Contract):
model_config = ConfigDict(title='Contrat')
parties: List[PartyRead] = Field(
props={"items_per_row": "2"},
title='Parties'
)
parties: List[PartyRead]
lawyer: ForeignEntityRead
class Config:
title = "Contrat"
class ContractCreate(Writer):
date: datetime.date
location: str
draft_id: PydanticObjectId = ForeignKey(resource="contracts/drafts", schema="ContractDraft")
draft_id: str
class ContractInit(BaseModel):
date: datetime.date

View File

@@ -1,70 +1,51 @@
from fastapi import HTTPException, Depends
from hub.auth import get_current_user
from firm.current_firm import CurrentFirm, Partner
from firm.db import get_db_client
from firm.entity.models import Entity
from firm.current_firm import CurrentFirmModel
class Registry:
user = None
partner = None
def __init__(self, db_client, instance, firm):
self.db = db_client[f"tenant_{instance}_{firm}"]
self.instance = instance
self.firm = firm
self.current_firm = CurrentFirm.get_current(self.db)
self.current_firm = CurrentFirmModel.get_current(self.db)
def check_user(self, user):
def set_user(self, user):
for firm in user.firms:
if firm.instance == self.instance and firm.firm == self.firm:
return True
raise PermissionError
async def set_user(self, user):
self.check_user(user)
partner = await Partner.get_by_user_id(self.db, user.id)
partner_entity = await Entity.get(self.db, partner.entity_id)
self.user = user
self.partner = partner_entity
self.db.partner = partner_entity
return
self.user = user
self.db.user = user
return
raise PermissionError
async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client)) -> Registry:
registry = Registry(db_client, instance, firm)
if await registry.current_firm is None:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first")
return registry
async def get_authed_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry:
registry = Registry(db_client, instance, firm)
def get_authed_tenant_registry(registry=Depends(get_tenant_registry), user=Depends(get_current_user)) -> Registry:
try:
registry.check_user(user)
registry.set_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
if await registry.current_firm is None:
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first")
await registry.set_user(user)
return registry
async def get_uninitialized_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> Registry:
registry = Registry(db_client, instance, firm)
try:
registry.check_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
if await registry.current_firm is not None:
raise HTTPException(status_code=409, detail="Firm configuration already exists")
await registry.set_user(user)
try:
registry.set_user(user)
except PermissionError:
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
return registry

View File

@@ -1,8 +1,8 @@
from collections import defaultdict
from collections.abc import Callable, Mapping
from pydantic import ValidationInfo, field_validator
from typing import Any, Optional, Union
from pydantic import ValidationInfo, field_validator
from fastapi_filter.base.filter import BaseFilterModel
_odm_operator_transformer: dict[str, Callable[[Optional[str]], Optional[dict[str, Any]]]] = {
@@ -24,6 +24,28 @@ _odm_operator_transformer: dict[str, Callable[[Optional[str]], Optional[dict[str
class Filter(BaseFilterModel):
"""Base filter for beanie related filters.
Example:
```python
class MyModel:
id: PrimaryKey()
name: StringField(null=True)
count: IntField()
created_at: DatetimeField()
class MyModelFilter(Filter):
id: Optional[int]
id__in: Optional[str]
count: Optional[int]
count__lte: Optional[int]
created_at__gt: Optional[datetime]
name__ne: Optional[str]
name__nin: Optional[list[str]]
name__isnull: Optional[bool]
```
"""
def sort(self):
if not self.ordering_values:
return None
@@ -108,58 +130,6 @@ class Filter(BaseFilterModel):
query[field_name] = value
return query
@staticmethod
def field_exists(model, field_path: str) -> bool:
if "." in field_path:
[root, field] = field_path.split(".", 1)
return hasattr(model, "model_fields") and root in model.model_fields \
and model.model_fields[root].discriminator == field
return hasattr(model, field_path) or (hasattr(model, "model_fields") and field_path in model.model_fields)
@field_validator("*", mode="before", check_fields=False)
def validate_order_by(cls, value, field: ValidationInfo):
if field.field_name != cls.Constants.ordering_field_name:
return value
if not value:
return None
field_name_usages = defaultdict(list)
duplicated_field_names = set()
for field_name_with_direction in value:
field_name = field_name_with_direction.replace("-", "").replace("+", "")
if not cls.field_exists(cls.Constants.model, field_name):
raise ValueError(f"{field_name} is not a valid ordering field.")
field_name_usages[field_name].append(field_name_with_direction)
if len(field_name_usages[field_name]) > 1:
duplicated_field_names.add(field_name)
if duplicated_field_names:
ambiguous_field_names = ", ".join(
[
field_name_with_direction
for field_name in sorted(duplicated_field_names)
for field_name_with_direction in field_name_usages[field_name]
]
)
raise ValueError(
f"Field names can appear at most once for {cls.Constants.ordering_field_name}. "
f"The following was ambiguous: {ambiguous_field_names}."
)
return value
class FilterSchema(Filter):
label__ilike: Optional[str] = None
search: Optional[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

View File

@@ -1,18 +1,12 @@
from datetime import datetime, UTC
from typing import Optional, TypedDict, ClassVar
from typing import Optional
from beanie import PydanticObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pydantic import BaseModel, Field, computed_field
class CrudDocumentConfig(TypedDict, total=False):
fulltext_search: list[str]
indexes: list[str]
class CrudDocument(BaseModel):
document_config: ClassVar[CrudDocumentConfig] = CrudDocumentConfig()
id: Optional[PydanticObjectId] = Field(default=None)
created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
created_by: Optional[PydanticObjectId] = Field(default=None, title="Créé par")
@@ -23,13 +17,16 @@ class CrudDocument(BaseModel):
def _id(self):
return self.id
@computed_field(title="Label")
@computed_field
def label(self) -> str:
return self.compute_label()
def compute_label(self) -> str:
return ""
class Settings:
fulltext_search = []
@classmethod
def _collection_name(cls):
return cls.__name__
@@ -38,13 +35,9 @@ class CrudDocument(BaseModel):
def _get_collection(cls, db) -> AsyncIOMotorCollection:
return db.get_collection(cls._collection_name())
@classmethod
def create_index(cls, db, index):
cls._get_collection(db).create_index(index)
@classmethod
async def create(cls, db, create_schema):
model_dict = create_schema.model_dump() | {"created_by": db.partner.id, "updated_by": db.partner.id}
model_dict = create_schema.model_dump() | {"created_by": db.user.id, "updated_by": db.user.id}
document = cls.model_validate(model_dict).model_dump(mode="json")
result = await cls._get_collection(db).insert_one(document)
@@ -59,13 +52,8 @@ class CrudDocument(BaseModel):
}
@classmethod
async def list(cls, db, criteria={}):
result = []
for document in await cls._get_collection(db).find(criteria).to_list():
document["id"] = document.pop("_id")
result.append(cls.model_validate(document))
return result
def list(cls, db):
return cls._get_collection(db).find({})
@classmethod
async def get(cls, db, model_id):
@@ -78,22 +66,20 @@ class CrudDocument(BaseModel):
@classmethod
async def update(cls, db, model, update_schema):
model_dict = update_schema.model_dump(mode="json") | {"updated_by": db.partner.id, "updated_at": datetime.now(UTC)}
model_dict = update_schema.model_dump(mode="json") | {"updated_by": db.user.id}
update_query = {
"$set": {field: value for field, value in model_dict.items() if field!= "id" }
}
await cls._get_collection(db).update_one({"_id": model.id}, update_query)
new_model = await cls.get(db, model.id)
if new_model.label != model.label:
await cls._get_collection(db).update_one({"_id": model.id}, {"$set": {"label": new_model.label}})
return new_model
return await cls.get(db, model.id)
@classmethod
async def delete(cls, db, model):
await cls._get_collection(db).delete_one({"_id": model.id})
def text_area(*args, **kwargs):
kwargs['widget'] = {
"formlyConfig": {
@@ -128,20 +114,6 @@ def RichtextSingleline(*args, **kwargs):
return Field(*args, **kwargs)
def ForeignKey(resource, schema, displayed_fields=None, *args, **kwargs):
kwargs["foreignKey"] = {
"reference": {
"resource": resource,
"schema": schema,
}
}
if displayed_fields:
kwargs["foreignKey"]["reference"]["displayedFields"] = displayed_fields
return Field(*args, **kwargs)
class DictionaryEntry(BaseModel):
key: str
value: str = ""

View File

@@ -5,9 +5,7 @@ from pydantic import BaseModel, Field
class Reader(BaseModel):
id: Optional[PydanticObjectId] = Field(validation_alias="_id")
created_by: PydanticObjectId = Field(title="Créé par")
updated_by: PydanticObjectId = Field(title="Modifié par")
id: Optional[PydanticObjectId] = Field(default=None, validation_alias="_id")
@classmethod
def from_model(cls, model):

View File

@@ -1,13 +1,14 @@
from typing import Any, Optional
from typing import Any
from beanie import PydanticObjectId
from pydantic import Field, BaseModel
from pydantic import Field
from firm.core.models import CrudDocument, CrudDocumentConfig
from firm.core.models import CrudDocument
from firm.core.schemas import Writer, Reader
from firm.entity.schemas import EntityIndividualCreate, EntityCorporationCreate, EntityRead
class CurrentFirm(CrudDocument):
class CurrentFirmModel(CrudDocument):
instance: str = Field()
firm: str = Field()
entity_id: PydanticObjectId = Field()
@@ -32,19 +33,17 @@ class CurrentFirm(CrudDocument):
return cls.model_validate(document)
class CurrentFirmSchemaRead(BaseModel):
id: Optional[PydanticObjectId]
class CurrentFirmSchemaRead(Reader):
entity: EntityRead
partner: EntityRead
partner_list: list[EntityRead]
instance: str
firm: str
primary_color: str
secondary_color: str
@classmethod
def from_model_and_entities(cls, model, entity, partner, partner_list):
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner, partner_list=partner_list)
def from_model_and_entities(cls, model, entity, partner):
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner)
return schema
class CurrentFirmSchemaCreate(Writer):
@@ -60,10 +59,6 @@ class CurrentFirmSchemaUpdate(Writer):
pass
class Partner(CrudDocument):
document_config = CrudDocumentConfig(
indexes=["user_id", "entity_id"],
)
user_id: PydanticObjectId = Field()
entity_id: PydanticObjectId = Field()

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends
from firm.core.depends import get_authed_tenant_registry, get_uninitialized_registry
from firm.current_firm import CurrentFirm, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
from firm.current_firm import CurrentFirmModel, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
from firm.entity.models import Entity, Employee
from firm.entity.schemas import EntityRead
@@ -9,18 +9,11 @@ current_firm_router = APIRouter()
@current_firm_router.get("/", response_model=CurrentFirmSchemaRead, response_description=f"Current Firm records retrieved")
async def read(reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
document = await CurrentFirm.get_current(reg.db)
firm_entity = await Entity.get(reg.db, document.entity_id)
document = await CurrentFirmModel.get_current(reg.db)
entity = await Entity.get(reg.db, document.entity_id)
partner = await Partner.get_by_user_id(reg.db, reg.user.id)
partner = await Entity.get(reg.db, partner.entity_id)
partner_list = await Partner.list(reg.db)
partner_list = await Entity.list(reg.db, {"_id": {"$in": [p.entity_id for p in partner_list]}})
return CurrentFirmSchemaRead.from_model_and_entities(
document,
EntityRead.from_model(firm_entity),
EntityRead.from_model(partner),
[EntityRead.from_model(p) for p in partner_list]
)
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(entity), EntityRead.from_model(partner))
@current_firm_router.post("/", response_description=f"Current Firm added to the database")
async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_registry)) -> CurrentFirmSchemaRead:
@@ -31,25 +24,17 @@ async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_
corporation_schema.entity_data.employees.append(Employee(entity_id=owner_entity.id, position=schema.position))
corp = await Entity.create(reg.db, corporation_schema)
firm = await CurrentFirm.create(reg.db, CurrentFirm(
document = await CurrentFirmModel.create(reg.db, CurrentFirmModel(
instance=reg.instance,
firm=reg.firm,
entity_id=corp.id,
primary_color=schema.primary_color,
secondary_color=schema.secondary_color,
))
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)]
)
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(corp), EntityRead.from_model(owner_entity))
@current_firm_router.put("/", response_description=f"Current Firm record updated")
async def update(schema: CurrentFirmSchemaUpdate, reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
document = await CurrentFirm.get_current(reg.db)
document = await CurrentFirm.update(reg.db, document, schema)
document = await CurrentFirmModel.get_current(reg.db)
document = await CurrentFirmModel.update(reg.db, document, schema)
return CurrentFirmSchemaRead.from_model(document)

View File

@@ -11,6 +11,9 @@ client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
async def init_db():
pass
async def stop_db():
client.close()

View File

@@ -1,12 +1,10 @@
from datetime import date
from enum import Enum
from datetime import date, datetime
from typing import List, Literal, Optional
from fastapi_filter import FilterDepends, with_prefix
from pydantic import Field, BaseModel, ConfigDict
from beanie import PydanticObjectId
from pydantic import Field, BaseModel
from beanie import Indexed, PydanticObjectId
from firm.core.models import CrudDocument, ForeignKey, CrudDocumentConfig
from firm.core.models import CrudDocument
from firm.core.filter import Filter, FilterSchema
@@ -15,21 +13,15 @@ class EntityType(BaseModel):
def label(self) -> str:
return self.title
class EntityTypeEnum(str, Enum):
individual = 'individual'
corporation = 'corporation'
institution = 'institution'
class Individual(EntityType):
model_config = ConfigDict(title='Particulier')
type: Literal['individual'] = 'individual'
firstname: str = Field(title='Prénom')
middlename: str = Field(default="", title='Autres prénoms')
lastname: str = Field(title='Nom de famille')
surnames: List[str] = Field(
firstname: Indexed(str) = Field(title='Prénom')
middlename: Indexed(str) = Field(default="", title='Autres prénoms')
lastname: Indexed(str) = Field(title='Nom de famille')
surnames: List[Indexed(str)] = Field(
default=[],
props={"items_per_row": "4", "numbered": True},
props={"items-per-row": "4", "numbered": True},
title="Surnoms"
)
day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
@@ -38,41 +30,51 @@ class Individual(EntityType):
@property
def label(self) -> str:
# if len(self.surnames) > 0:
# return f"{self.firstname} \"{self.surnames[0]}\" {self.lastname}"
# return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
return f"{self.firstname} {self.lastname}"
class Config:
title = 'Particulier'
class Employee(BaseModel):
model_config = ConfigDict(title='Fiche Employé')
position: Indexed(str) = Field(title='Poste')
entity_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
"condition": "entity_data.type=individual"
}
},
title='Employé'
)
position: str = Field(title='Poste')
entity_id: PydanticObjectId = ForeignKey("entities", "Entity", title='Employé')
class Config:
title = 'Fiche Employé'
class Corporation(EntityType):
model_config = ConfigDict(title='Entreprise')
type: Literal['corporation'] = 'corporation'
title: str = Field(title='Dénomination sociale')
activity: str = Field(title='Activité')
title: Indexed(str) = Field(title='Dénomination sociale')
activity: Indexed(str) = Field(title='Activité')
employees: List[Employee] = Field(default=[], title='Employés')
class Config:
title = 'Entreprise'
class Institution(Corporation):
model_config = ConfigDict(title='Institution')
type: Literal['institution'] = 'institution'
class Config:
title = 'Institution'
class Entity(CrudDocument):
"""
Fiche d'un client
"""
model_config = ConfigDict(title='Client')
document_config = CrudDocumentConfig(
indexes=["entity_data.type"],
)
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
address: str = Field(default="", title='Adresse')
@@ -81,17 +83,20 @@ class Entity(CrudDocument):
return ""
return self.entity_data.label
class Settings(CrudDocument.Settings):
fulltext_search = ['label']
class EntityDataFilter(Filter):
type__in: Optional[list[str]] = None
bson_encoders = {
date: lambda dt: dt if hasattr(dt, 'hour')
else datetime(year=dt.year, month=dt.month, day=dt.day,
hour=0, minute=0, second=0)
}
class Constants(Filter.Constants):
model = EntityType
class Config:
title = 'Client'
class EntityFilters(FilterSchema):
entity_data: Optional[EntityDataFilter] = FilterDepends(with_prefix("entity_data", EntityDataFilter))
class Constants(Filter.Constants):
model = Entity
search_model_fields = ["label"]

View File

@@ -1,4 +1,4 @@
from pydantic import Field, ConfigDict
from pydantic import Field
from firm.entity.models import Entity, Institution, Individual, Corporation
from firm.core.schemas import Writer, Reader
@@ -7,11 +7,12 @@ class EntityRead(Reader, Entity):
pass
class EntityCreate(Writer):
model_config = ConfigDict(title="Création d'un client")
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
address: str = Field(default="", title='Adresse')
class Config:
title = "Création d'un client"
class EntityIndividualCreate(EntityCreate):
entity_data: Individual

View File

@@ -1,31 +0,0 @@
import logging
from firm.contract.models import Contract, ContractDraft
from firm.core.depends import Registry
from firm.current_firm import CurrentFirm, Partner
from firm.db import client
from firm.entity.models import Entity
from firm.template.models import ContractTemplate, ProvisionTemplate
from hub.firm import Firm
collections = [CurrentFirm, Entity, Partner, Contract, ContractDraft, ContractTemplate, ProvisionTemplate]
logger = logging.getLogger('uvicorn.error')
async def create_documents_indexes(db):
for collection in collections:
if "indexes" in collection.document_config:
for field in collection.document_config["indexes"]:
collection.create_index(db, field)
async def init_all_db():
logger.info("[FRM] Creating index for firms")
for firm in await Firm.find({}).to_list():
reg = Registry(client, firm.instance, firm.firm)
await reg.current_firm
await create_documents_indexes(reg.db)
async def init_db():
await init_all_db()

View File

@@ -1,19 +1,38 @@
from typing import List, Optional
from typing import List
from html import unescape
from beanie import PydanticObjectId
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, Field
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry, ForeignKey
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry
from firm.core.filter import Filter, FilterSchema
class PartyTemplate(BaseModel):
model_config = ConfigDict(title="Partie")
entity_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Partie")
entity_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Partie"
)
part: str = Field(title="Rôle")
representative_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Représentant")
representative_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Représentant"
)
class Config:
title = 'Partie'
def remove_html_tags(text):
@@ -27,7 +46,6 @@ class ProvisionTemplate(CrudDocument):
"""
Modèle de clause à décliner
"""
model_config = ConfigDict(title="Template de clause")
name: str = Field(title="Nom")
title: str = RichtextSingleline(title="Titre")
@@ -36,34 +54,57 @@ class ProvisionTemplate(CrudDocument):
def compute_label(self) -> str:
return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title', 'body']
class Config:
title = 'Template de clause'
class ProvisionTemplateReference(BaseModel):
model_config = ConfigDict(title="Clause")
provision_template_id: PydanticObjectId = ForeignKey(
"templates/provisions",
"ProvisionTemplate",
['title', 'body'],
provision_template_id: PydanticObjectId = Field(
foreignKey={
"reference": {
"resource": "templates/provisions",
"schema": "ProvisionTemplate",
"displayedFields": ['title', 'body']
},
},
props={"parametrized": True},
title="Template de clause"
)
class Config:
title = 'Clause'
class ContractTemplate(CrudDocument):
"""
Modèle de contrat à décliner
"""
model_config = ConfigDict(title="Template de contrat")
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[PartyTemplate] = Field(default=[], title="Parties")
provisions: List[ProvisionTemplateReference] = Field(default=[], title="Clauses")
variables: List[DictionaryEntry] = Field(default=[], title="Variables")
provisions: List[ProvisionTemplateReference] = Field(
default=[],
props={"items-per-row": "1", "numbered": True},
title="Clauses"
)
variables: List[DictionaryEntry] = Field(
default=[],
format="dictionary",
title="Variables"
)
def compute_label(self) -> str:
return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
class Settings(CrudDocument.Settings):
fulltext_search = ['name', 'title']
class Config:
title = 'Template de contrat'
class ContractTemplateFilters(FilterSchema):
class Constants(Filter.Constants):

View File

@@ -1,4 +1,4 @@
from pydantic import Field, ConfigDict
from pydantic import Field
from typing import List
from firm.template.models import ContractTemplate, ProvisionTemplate, PartyTemplate, ProvisionTemplateReference, DictionaryEntry
@@ -11,25 +11,24 @@ class ContractTemplateRead(Reader, ContractTemplate):
class ContractTemplateCreate(Writer):
model_config = ConfigDict(title="Template de Contrat")
name: str = Field(title="Nom")
title: str = Field(title="Titre")
parties: List[PartyTemplate] = Field(
default=[],
props={"items_per_row": "2"},
title="Parties")
parties: List[PartyTemplate] = Field(default=[], title="Parties")
provisions: List[ProvisionTemplateReference] = Field(
default=[],
props={"items_per_row": "1", "numbered": True},
props={"items-per-row": "1", "numbered": True},
title="Clauses"
)
variables: List[DictionaryEntry] = Field(
default=[],
props={"display": "dictionary", "required": False},
format="dictionary",
props={"required": False},
title="Variables"
)
class Config:
title = 'Template de Contrat'
class ContractTemplateUpdate(ContractTemplateCreate):
pass
@@ -40,12 +39,13 @@ class ProvisionTemplateRead(Reader, ProvisionTemplate):
class ProvisionTemplateCreate(Writer):
model_config = ConfigDict(title="Template de Clause")
name: str = Field(title="Nom")
title: str = RichtextSingleline(title="Titre")
body: str = RichtextMultiline(title="Corps")
class Config:
title = 'Template de Clause'
class ProvisionTemplateUpdate(ProvisionTemplateCreate):
pass

View File

@@ -67,7 +67,7 @@ class AuthenticationBackendMe(AuthenticationBackend):
class CookieTransportOauth(CookieTransport):
async def get_login_response(self, token: str) -> Response:
response = RedirectResponse("/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
response = RedirectResponse("/auth/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
return self._set_login_cookie(response, token)
@staticmethod

View File

@@ -5,7 +5,7 @@ from hub import hub_router
from hub.db import init_db as hub_init_db
from firm import firm_router
from firm.init_db import init_db as firm_init_db
from firm.db import init_db as firm_init_db
if __name__ == '__main__':

View File

@@ -32,19 +32,6 @@ services:
- "traefik.http.routers.gui.rule=PathPrefix(`/`)"
- "traefik.http.services.gui.loadbalancer.server.port=5173"
i18n:
build:
context: ./i18n
restart: always
volumes:
- ./i18n/app/src:/app/src
- ./gui/rpk-gui/public:/app/public
labels:
- "traefik.enable=true"
- "traefik.http.routers.i18n.entrypoints=web"
- "traefik.http.routers.i18n.rule=PathPrefix(`/locales/add`)"
- "traefik.http.services.i18n.loadbalancer.server.port=8100"
proxy:
image: traefik:latest
restart: always

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@
"@mui/lab": "^6.0.0-beta.14",
"@mui/material": "^6.1.7",
"@mui/x-data-grid": "^7.22.2",
"@mui/x-date-pickers": "^8.3.0",
"@refinedev/cli": "^2.16.21",
"@refinedev/core": "^4.47.1",
"@refinedev/devtools": "^1.1.32",
@@ -32,7 +31,6 @@
"@tiptap/extension-underline": "^2.11.7",
"@tiptap/react": "^2.11.7",
"@tiptap/starter-kit": "^2.11.7",
"dayjs": "^1.11.13",
"i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",
@@ -40,7 +38,6 @@
"mui-tiptap": "^1.18.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.30.0",
"react-i18next": "^15.5.1",
"react-router": "^7.0.2"

View File

@@ -1,164 +0,0 @@
{
"pages": {
"login": {
"title": "Melden Sie sich bei Ihrem Konto an",
"signin": "Einloggen",
"signup": "Anmelden",
"divider": "oder",
"fields": {
"email": "Email",
"password": "Passwort"
},
"oauth": {
"google": "Einloggen mit Google",
"discord": "Einloggen mit Discord"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich",
"requiredPassword": "Passwort wird benötigt"
},
"buttons": {
"submit": "Anmeldung",
"forgotPassword": "Passwort vergessen?",
"noAccount": "Sie haben kein Konto?",
"rememberMe": "Erinnere dich an mich"
}
},
"forgotPassword": {
"title": "Haben Sie Ihr Passwort vergessen?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich"
},
"buttons": {
"submit": "Anweisungen zum Zurücksetzen senden"
}
},
"register": {
"title": "Registrieren Sie sich für Ihr Konto",
"fields": {
"email": "Email",
"password": "Passwort"
},
"errors": {
"validEmail": "Ungültige E-Mail-Adresse",
"requiredEmail": "E-Mail ist erforderlich",
"requiredPassword": "Passwort wird benötigt"
},
"buttons": {
"submit": "Registrieren",
"haveAccount": "Ein Konto haben?"
}
},
"updatePassword": {
"title": "Kennwort aktualisieren",
"fields": {
"password": "Neues Passwort",
"confirmPassword": "Bestätige neues Passwort"
},
"errors": {
"confirmPasswordNotMatch": "Passwörter stimmen nicht überein",
"requiredPassword": "Passwort wird benötigt",
"requiredConfirmPassword": "Das Feld „Passwort bestätigen“ ist erforderlich"
},
"buttons": {
"submit": "Aktualisieren"
}
},
"error": {
"info": "Sie haben vergessen, {{action}} component zu {{resource}} hinzufügen.",
"404": "Leider existiert diese Seite nicht.",
"resource404": "Haben Sie die {{resource}} resource erstellt?",
"backHome": "Zurück"
}
},
"actions": {
"list": "Aufführen",
"create": "Erstellen",
"edit": "Bearbeiten",
"show": "Zeigen"
},
"buttons": {
"create": "Erstellen",
"save": "Speichern",
"logout": "Abmelden",
"delete": "Löschen",
"edit": "Bearbeiten",
"cancel": "Abbrechen",
"confirm": "Sicher?",
"filter": "Filter",
"clear": "Löschen",
"refresh": "Erneuern",
"show": "Zeigen",
"undo": "Undo",
"import": "Importieren",
"clone": "Klon",
"notAccessTitle": "Sie haben keine zugriffsberechtigung"
},
"warnWhenUnsavedChanges": "Nicht gespeicherte Änderungen werden nicht übernommen.",
"notifications": {
"success": "Erfolg",
"error": "Fehler (status code: {{statusCode}})",
"undoable": "Sie haben {{seconds}} Sekunden Zeit für Undo.",
"createSuccess": "{{resource}} erfolgreich erstellt.",
"createError": "Fehler beim Erstellen {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "{{resource}} erfolgreich gelöscht.",
"deleteError": "Fehler beim Löschen {{resource}} (status code: {{statusCode}})",
"editSuccess": "{{resource}} erfolgreich bearbeitet.",
"editError": "Fehler beim Bearbeiten {{resource}} (status code: {{statusCode}})",
"importProgress": "{{processed}}/{{total}} importiert"
},
"loading": "Wird geladen",
"tags": {
"clone": "Klon"
},
"dashboard": {
"title": "Dashboard"
},
"posts": {
"posts": "Einträge",
"fields": {
"id": "Id",
"title": "Titel",
"category": "Kategorie",
"status": {
"title": "Status",
"published": "Veröffentlicht",
"draft": "Draft",
"rejected": "Abgelehnt"
},
"content": "Inhalh",
"createdAt": "Erstellt am"
},
"titles": {
"create": "Erstellen",
"edit": "Bearbeiten",
"list": "Einträge",
"show": "Eintrag zeigen"
}
},
"table": {
"actions": "Aktionen"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Beiträge | Refine",
"show": "#{{id}} Beitrag anzeigen | Refine",
"edit": "#{{id}} Beitrag bearbeiten | Refine",
"create": "Neuen Beitrag erstellen | Refine",
"clone": "#{{id}} Beitrag klonen | Refine"
}
},
"autoSave": {
"success": "gespeichert",
"error": "fehler beim automatischen speichern",
"loading": "speichern...",
"idle": "warten auf anderungen"
}
}

View File

@@ -0,0 +1,7 @@
{
"pages": {
"login": {
"title": "Sign in to your account"
}
}
}

View File

@@ -1,284 +0,0 @@
{
"pages": {
"home": {
"title": "Home"
},
"login": {
"title": "Sign in to your account",
"signin": "Sign in",
"signup": "Sign up",
"divider": "or",
"fields": {
"email": "Email",
"password": "Password"
},
"oauth": {
"google": "Sign in with Google",
"discord": "Sign in with Discord"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Login",
"forgotPassword": "Forgot password?",
"noAccount": "Dont have an account?",
"rememberMe": "Remember me"
}
},
"forgotPassword": {
"title": "Forgot your password?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required"
},
"buttons": {
"submit": "Send reset instructions"
}
},
"register": {
"title": "Sign up for your account",
"fields": {
"email": "Email",
"password": "Password"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Register",
"haveAccount": "Have an account?"
}
},
"updatePassword": {
"title": "Update password",
"fields": {
"password": "New Password",
"confirmPassword": "Confirm new password"
},
"errors": {
"confirmPasswordNotMatch": "Passwords do not match",
"requiredPassword": "Password required",
"requiredConfirmPassword": "Confirm password is required"
},
"buttons": {
"submit": "Update"
}
},
"error": {
"404": "Sorry, the page you visited does not exist.",
"info": "You may have forgotten to add the {{action}} component to {{resource}} resource.",
"resource404": "Are you sure you have created the {{resource}} resource.",
"backHome": "Back Home"
}
},
"actions": {
"list": "List",
"create": "Create",
"edit": "Edit",
"show": "Show"
},
"buttons": {
"create": "Create",
"save": "Save",
"logout": "Logout",
"delete": "Delete",
"edit": "Edit",
"cancel": "Cancel",
"confirm": "Are you sure?",
"filter": "Filter",
"clear": "Clear",
"refresh": "Refresh",
"show": "Show",
"undo": "Undo",
"import": "Import",
"clone": "Clone",
"notAccessTitle": "You don't have permission to access"
},
"warnWhenUnsavedChanges": "Are you sure you want to leave? You have unsaved changes.",
"notifications": {
"success": "Successful",
"error": "Error (status code: {{statusCode}})",
"undoable": "You have {{seconds}} seconds to undo",
"createSuccess": "Successfully created {{resource}}",
"createError": "There was an error creating {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "Successfully deleted {{resource}}",
"deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})",
"editSuccess": "Successfully edited {{resource}}",
"editError": "Error when editing {{resource}} (status code: {{statusCode}})",
"importProgress": "Importing: {{processed}}/{{total}}"
},
"loading": "Loading",
"tags": {
"clone": "Clone"
},
"dashboard": {
"title": "Dashboard"
},
"posts": {
"posts": "Posts",
"fields": {
"id": "Id",
"title": "Title",
"category": "Category",
"status": {
"title": "Status",
"published": "Published",
"draft": "Draft",
"rejected": "Rejected"
},
"content": "Content",
"createdAt": "Created At"
},
"titles": {
"create": "Create Post",
"edit": "Edit Post",
"list": "Posts",
"show": "Show Post"
}
},
"table": {
"actions": "Actions"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Posts | Refine",
"show": "#{{id}} Show Post | Refine",
"edit": "#{{id}} Edit Post | Refine",
"create": "Create new Post | Refine",
"clone": "#{{id}} Clone Post | Refine"
}
},
"autoSave": {
"success": "saved",
"error": "auto save failure",
"loading": "saving...",
"idle": "waiting for changes"
},
"undefined": {
"undefined": "No translation",
"titles": {
"list": "No translation"
}
},
"schemas": {
"created_by": "Created by",
"created_at": "Created at",
"updated_by": "Updated by",
"updated_at": "Updated at",
"label": "Label",
"type": "Type",
"individual": {
"type": "Individual",
"lastname": "Lastname",
"surnames": "Surname",
"day_of_birth": "Date of birth",
"firstname": "Firstname",
"place_of_birth": "Place of birth",
"middlename": "Middlename",
"resource_title": "Individual"
},
"corporation": {
"type": "Corporation",
"activity": "Activity",
"title": "Title",
"employees": "Employees",
"resource_title": "Corporation"
},
"employee": {
"position": "Position",
"entity_id": "Identity",
"resource_title": "Employee"
},
"institution": {
"type": "Institution",
"title": "Title",
"activity": "Activity",
"employees": "Employees",
"resource_title": "Institution"
},
"entity": {
"entity_data": "Informations",
"address": "Address",
"resource_title": "Entity",
"resource_plural": "Entities"
},
"provision_template": {
"name": "Name",
"title": "Title",
"body": "Body",
"resource_title": "Provision Template",
"resource_plural": "Provision Templates"
},
"contract_template": {
"name": "Name",
"title": "Title",
"provisions": "Provisions",
"parties": "Parties",
"variables": "Variables",
"resource_title": "Contract Template",
"resource_plural": "Contract Templates"
},
"party_template": {
"entity_id": "Entity",
"representative_id": "Representative",
"part": "Part",
"resource_title": "Party"
},
"provision_template_reference": {
"provision_template_id": "Provision Template",
"resource_title": "Provision Template"
},
"dictionary_entry": {
"key": "Variable",
"value": "Value",
"resource_title": "Variables"
},
"contract_draft": {
"name": "Name",
"title": "Title",
"parties": "Parties",
"provisions": "Provisions",
"variables": "Variables",
"resource_title": "Contract Draft",
"resource_plural": "Contract Drafts"
},
"draft_party": {
"entity_id": "Client",
"part": "Part",
"representative_id": "Representative",
"resource_title": "Party"
},
"contract_provision_template_reference": {
"provision_template_id": "Provision Template",
"type": "Provision Template",
"resource_title": "Provision Template"
},
"provision_genuine": {
"title": "Title",
"body": "Body",
"type": "Genuine Provision",
"resource_title": "Genuine Provision"
},
"draft_provision": {
"provision": "Provision",
"resource_title": "Provision"
},
"contract": {
"date": "Date",
"location": "Location",
"resource_title": "Contract",
"resource_plural": "Contracts",
"draft_id": "Draft"
}
}
}

View File

@@ -0,0 +1,7 @@
{
"pages": {
"login": {
"title": "S'authentifier"
}
}
}

View File

@@ -1,284 +0,0 @@
{
"pages": {
"home": {
"title": "Page d'accueil"
},
"login": {
"title": "Authentification",
"signin": "S'authentifier",
"signup": "Créer un compte",
"divider": "ou",
"fields": {
"email": "Email",
"password": "Mot de passe"
},
"oauth": {
"google": "S'authentifier avec Google",
"discord": "S'authentifier avec Discord"
},
"errors": {
"validEmail": "Email invalide",
"requiredEmail": "l'Email est obligatoire",
"requiredPassword": "Le mot de passe est obligatoire"
},
"buttons": {
"submit": "S'authentifier",
"forgotPassword": "Mot de passe oublié?",
"noAccount": "Vous n'avec pas de compte?",
"rememberMe": "Se souvenir de moi"
}
},
"forgotPassword": {
"title": "Mot de passe oublié?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "mail invalide",
"requiredEmail": "l'Email est obligatoire"
},
"buttons": {
"submit": "Envoyer les instructions de récupération"
}
},
"register": {
"title": "Création de compte",
"fields": {
"email": "Email",
"password": "Mot de passe"
},
"errors": {
"validEmail": "Email invalide",
"requiredEmail": "l'Email est obligatoire",
"requiredPassword": "Le mot de passe est obligatoire"
},
"buttons": {
"submit": "Créer un compte",
"haveAccount": "Vous avez déjà un compte?"
}
},
"updatePassword": {
"title": "Mise à jour du mot de passe",
"fields": {
"password": "Nouveau mot de passe",
"confirmPassword": "Confirmation"
},
"errors": {
"confirmPasswordNotMatch": "Les mots de passe ne correspondent pas",
"requiredPassword": "Le mot de passe est obligatoire",
"requiredConfirmPassword": "Vous devez confirmer votre mot de passe"
},
"buttons": {
"submit": "Mettre à jour"
}
},
"error": {
"404": "Cette page n'existe pas.",
"info": "Il manque l'action {{action}} component à la ressource {{resource}} .",
"resource404": "Cette page n'existe pas.",
"backHome": "Retour à l'accueil"
}
},
"actions": {
"list": "Liste",
"create": "Création",
"edit": "Édtion",
"show": "Voir"
},
"buttons": {
"create": "Créer",
"save": "Sauvegarder",
"logout": "Déconnexion",
"delete": "Supprimer",
"edit": "Modifier",
"cancel": "Annuler",
"confirm": "Êtes vous sur?",
"filter": "Filtrer",
"clear": "Effacer",
"refresh": "Rafraîchir",
"show": "Voir",
"undo": "Annuler",
"import": "Importer",
"clone": "Cloner",
"notAccessTitle": "Vous n'avez pas la permission d'accéder à cette ressource"
},
"warnWhenUnsavedChanges": "Êtes vous sur de vouloir quitter la page? Vous avez des modification non sauvegardées.",
"notifications": {
"success": "Succès",
"error": "Erreur (Code de statut: {{statusCode}})",
"undoable": "Vous avez {{seconds}} secondes à annuler",
"createSuccess": "Création de {{resource}} réussie",
"createError": "Erreur pendant la création de {{resource}} (Code de statut: {{statusCode}})",
"deleteSuccess": "Suppression de {{resource}} réussie",
"deleteError": "Erreur pendant la suppression de {{resource}} (Code de statut: {{statusCode}})",
"editSuccess": "Modification de {{resource}} réussie",
"editError": "Erreur pendant la modification de {{resource}} (Code de statut: {{statusCode}})",
"importProgress": "Importation de: {{processed}}/{{total}}"
},
"loading": "Chargement",
"tags": {
"clone": "Clone"
},
"dashboard": {
"title": "Tableau de bord"
},
"posts": {
"posts": "Posts",
"fields": {
"id": "Id",
"title": "Title",
"category": "Category",
"status": {
"title": "Status",
"published": "Published",
"draft": "Draft",
"rejected": "Rejected"
},
"content": "Content",
"createdAt": "Created At"
},
"titles": {
"create": "Create Post",
"edit": "Edit Post",
"list": "Posts",
"show": "Show Post"
}
},
"table": {
"actions": "Actions"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Posts | Refine",
"show": "#{{id}} Show Post | Refine",
"edit": "#{{id}} Edit Post | Refine",
"create": "Create new Post | Refine",
"clone": "#{{id}} Clone Post | Refine"
}
},
"autoSave": {
"success": "Sauvegardé",
"error": "Sauvegarde automatique ratée",
"loading": "Sauvegarde...",
"idle": "En attente de modification"
},
"undefined": {
"undefined": "No translation",
"titles": {
"list": "No translation"
}
},
"schemas": {
"created_by": "Créé par",
"created_at": "Créé le",
"updated_by": "Modifié par",
"updated_at": "Modifié le",
"label": "Label",
"type": "Type",
"individual": {
"type": "Particulier",
"middlename": "Autres prénoms",
"lastname": "Nom",
"firstname": "Prénom",
"day_of_birth": "Date de naissance",
"surnames": "Surnoms",
"place_of_birth": "Lieu de naissance",
"resource_title": "Particulier"
},
"corporation": {
"type": "Entreprise",
"title": "Dénomination sociale",
"activity": "Activité",
"employees": "Employés",
"resource_title": "Entreprise"
},
"employee": {
"entity_id": "Identité",
"position": "Poste",
"resource_title": "Employé"
},
"institution": {
"type": "Institution",
"title": "Titre",
"employees": "Employés",
"activity": "Activité",
"resource_title": "Institution"
},
"entity": {
"entity_data": "Informations",
"address": "Adresse",
"resource_title": "Entité",
"resource_plural": "Entités"
},
"provision_template": {
"name": "Nom",
"body": "Corps",
"title": "Titre",
"resource_title": "Template de Clause",
"resource_plural": "Templates de Clauses"
},
"contract_template": {
"name": "Nom",
"title": "Titre",
"parties": "Parties",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Template de Contrat",
"resource_plural": "Templates de Contrats"
},
"party_template": {
"entity_id": "Entité",
"part": "Rôle",
"representative_id": "Représentant",
"resource_title": "Partie"
},
"provision_template_reference": {
"provision_template_id": "Template de clause",
"resource_title": "Template de clause"
},
"dictionary_entry": {
"key": "Variable",
"value": "Valeur",
"resource_title": "Variables"
},
"contract_draft": {
"name": "Nom",
"parties": "Parties",
"title": "Titre",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Brouillon de Contrat",
"resource_plural": "Brouillons de Contrats"
},
"draft_party": {
"part": "Rôle",
"representative_id": "Représentant",
"entity_id": "Entité",
"resource_title": "Partie"
},
"contract_provision_template_reference": {
"type": "Template",
"provision_template_id": "Template de clause",
"resource_title": "Template de clause"
},
"provision_genuine": {
"type": "Personalisée",
"title": "Titre",
"body": "Corps",
"resource_title": "Clause personnalisée"
},
"draft_provision": {
"provision": "Clause",
"resource_title": "Clause"
},
"contract": {
"draft_id": "Brouillon",
"resource_title": "Contrat",
"resource_plural": "Contrats",
"location": "Lieu",
"date": "Date"
}
}
}

View File

@@ -5,8 +5,13 @@ import { RefineSnackbarProvider, useNotificationProvider } from "@refinedev/mui"
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import { ThemeProvider } from "@mui/material/styles";
import HistoryEduIcon from '@mui/icons-material/HistoryEdu';
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router";
import routerBindings, {
CatchAllNavigate,
DocumentTitleHandler,
UnsavedChangesNotifier,
} from "@refinedev/react-router";
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
import authProvider from "./providers/auth-provider";
import dataProvider from "./providers/data-provider";
@@ -17,11 +22,12 @@ import { ForgotPassword } from "./components/auth/ForgotPassword";
import { UpdatePassword } from "./components/auth/UpdatePassword";
import { Header } from "./components";
import { I18nTheme } from "./components/I18nTheme";
import { HubRoutes } from "./pages/hub";
import { FirmRoutes } from "./pages/firm";
import rpcTheme from "./theme";
function App() {
const { t, i18n } = useTranslation();
const i18nProvider: I18nProvider = {
@@ -32,48 +38,47 @@ function App() {
return (
<BrowserRouter>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<Refine
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
options={{
title: {
text: "Roleplay Contracts",
icon: <HistoryEduIcon />
},
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
disableTelemetry: true,
reactQuery: {
clientConfig: {
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// @ts-ignore
const status = error.statusCode ? error.statusCode : error.status
if (status >= 400 && status<= 499) {
return false
}
return failureCount < 4
},
<ThemeProvider theme={rpcTheme}>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
<RefineSnackbarProvider>
<Refine
authProvider={authProvider}
dataProvider={dataProvider}
i18nProvider={i18nProvider}
notificationProvider={useNotificationProvider}
routerProvider={routerBindings}
options={{
title: {
text: "Roleplay Contracts",
icon: <HistoryEduIcon />
},
syncWithLocation: true,
warnWhenUnsavedChanges: true,
useNewQueryKeys: true,
disableTelemetry: true,
reactQuery: {
clientConfig: {
defaultOptions: {
queries: {
retry: (failureCount, error) => {
// @ts-ignore
if (error.statusCode >= 400 && error.statusCode <= 499) {
return false
}
return failureCount < 4
},
}
}
}
}
}
}}
>
<I18nTheme>
}}
>
<Routes>
<Route
element={(
<Authenticated key="authenticated-routes" fallback={<Login />}>
<Authenticated key="authenticated-routes" redirectOnFail="/auth/login" fallback={<CatchAllNavigate to="/auth/login"/>}>
<Outlet />
</Authenticated>
)}
@@ -81,20 +86,20 @@ function App() {
<Route path="hub/*" element={<HubRoutes />} />
<Route path="firm/*" element={<FirmRoutes />} />
</Route>
<Route path="*" element={<Outlet />}>
<Route path="auth/*" element={<Outlet />}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="forgot-password" element={<ForgotPassword />} />
<Route path="update-password" element={<UpdatePassword />} />
</Route>
<Route index element={<><Header /><h1>{t("pages.home.title")}</h1></>} />
<Route index element={<><Header /><h1>HOME</h1></>} />
</Routes>
<UnsavedChangesNotifier />
<DocumentTitleHandler />
</I18nTheme>
</Refine>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</Refine>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</ThemeProvider>
</BrowserRouter>
);
}

View File

@@ -1,40 +0,0 @@
import { useTranslation } from "@refinedev/core";
import { useContext } from "react";
import { FirmContext } from "../contexts/FirmContext";
import Grid2 from '@mui/material/Grid2';
type CartoucheProps = {
record: any
}
const Cartouche = (props: CartoucheProps) => {
const { record } = props;
const { translate: t } = useTranslation();
return (
<>
<Grid2 container spacing={0}>
<Grid2 size={2}>{t("schemas.created_by")}:</Grid2>
<Grid2 size={4}><AuthorField partnerId={record.created_by} /></Grid2>
<Grid2 size={2}>{t("schemas.created_at")}:</Grid2>
<Grid2 size={4}>{new Date(record.created_at).toLocaleString()}</Grid2>
<Grid2 size={2}>{t("schemas.updated_by")}:</Grid2>
<Grid2 size={4}><AuthorField partnerId={record.updated_by} /></Grid2>
<Grid2 size={2}>{t("schemas.updated_at")}:</Grid2>
<Grid2 size={4}>{new Date(record.updated_at).toLocaleString()}</Grid2>
</Grid2>
</>
)
}
export default Cartouche;
const AuthorField = (props: {partnerId: string})=> {
const { partnerId } = props;
const { partnerMap } = useContext(FirmContext);
const { translate: t } = useTranslation();
if (partnerMap && partnerMap.has(partnerId)) {
return <>{ partnerMap.get(partnerId) }</>
}
return <>{t("REDACTED")}</>
}

View File

@@ -1,24 +0,0 @@
import React, { PropsWithChildren } from "react";
import { useTranslation } from "@refinedev/core";
import { useTheme } from "@mui/material";
import * as locales from '@mui/material/locale';
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;
export const I18nTheme: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
const { getLocale } = useTranslation();
const theme = useTheme()
const locale = getLocale() || "en"
const themeWithLocale = createTheme(theme, locales[locale as SupportedLocales])
return (
<ThemeProvider theme={themeWithLocale}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={locale.slice(0,2)} >
{ children }
</LocalizationProvider>
</ThemeProvider>
);
}

View File

@@ -1,11 +1,15 @@
import { useSearchParams, Navigate } from "react-router";
import { useTranslation } from "@refinedev/core";
import { AuthPage } from "@refinedev/mui";
import GoogleIcon from "@mui/icons-material/Google";
import DiscordIcon from "../../components/DiscordIcon";
import {useSearchParams, Navigate, Link} from "react-router";
import MuiLink from "@mui/material/Link";
import * as React from "react";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
export const Login = () => {
const { translate } = useTranslation();
const [searchParams] = useSearchParams();
if (searchParams.get("oauth") == "success") {
const redirect_to = localStorage.getItem("redirect_after_login")
@@ -20,15 +24,67 @@ export const Login = () => {
rememberMe={false}
providers={[{
name: "google",
label: translate("pages.login.oauth.google"),
label: "Sign in with Google",
icon: (<GoogleIcon style={{ fontSize: 24, }} />),
},
{
name: "discord",
label: translate("pages.login.oauth.discord"),
label: "Sign in with Discord",
icon: (<DiscordIcon style={{ fontSize: 24, }} />),
},
]}
forgotPasswordLink={
<Stack
sx={{
direction: "row",
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
}}
>
<MuiLink
variant="body2"
color="primary"
fontSize="12px"
component={Link}
underline="none"
to="/auth/forgot-password"
>
Forgot password?
</MuiLink>
</Stack>
}
registerLink={
<Box
sx={{
mt: "24px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Typography
textAlign="center"
variant="body2"
component="span"
fontSize="12px"
>
Dont have an account?
</Typography>
<MuiLink
ml="4px"
fontSize="12px"
variant="body2"
color="primary"
component={Link}
underline="none"
to="/auth/register"
fontWeight="bold"
>
Sign up
</MuiLink>
</Box>
}
/>
);
};

View File

@@ -1,10 +1,8 @@
import { Button } from "@mui/material";
import { useLogout } from "@refinedev/core";
import { useTranslation } from "@refinedev/core";
export const Logout = () => {
const { translate } = useTranslation();
const { mutate: logout } = useLogout();
return <Button onClick={() => logout()} >{ translate("buttons.logout") }</Button>;
return <Button onClick={() => logout()} >Logout</Button>;
};

View File

@@ -1,41 +0,0 @@
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import React from "react";
import { useTranslation } from "react-i18next";
import { useTranslation as useRefineTranslation } from "@refinedev/core";
const I18nPicker = () => {
const { i18n } = useTranslation();
const { getLocale, changeLocale } = useRefineTranslation();
const currentLocale = getLocale();
return (
<Autocomplete
value={currentLocale}
options={i18n.languages}
disableClearable={true}
renderInput={(params) => {
return <TextField {...params} label={ "Language" } variant="outlined" />
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<Box
key={key}
component="li"
sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
{...optionProps}
>
{ option }
</Box>
);
}}
onChange={(event, value) => {
changeLocale(value);
}}
/>
)
}
export default I18nPicker;

View File

@@ -18,11 +18,17 @@ import { FirmContext } from "../../contexts/FirmContext";
import { Logout } from "../auth/Logout";
import { IUser } from "../../interfaces";
import MuiLink from "@mui/material/Link";
import I18nPicker from "./I18nPicker";
import { useTranslation } from "react-i18next";
import { useTranslation as useTranslationR } from "@refinedev/core";
import { useSetLocale } from "@refinedev/core";
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
sticky = true,
}) => {
const { i18n } = useTranslation();
const { getLocale, changeLocale } = useTranslationR();
const currentLocale = getLocale();
const collapsed = false;
const { mode, setMode } = useContext(ColorModeContext);
const { currentFirm } = useContext(FirmContext);
@@ -38,6 +44,16 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
setAnchorEl(null);
};
const [anchorIn, setAnchorIn] = React.useState<null | HTMLElement>(null);
const openI18nMenu = Boolean(anchorEl);
const handleOpenI18nMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorIn(event.currentTarget);
}
const handleCloseI18nMenu = () => {
setAnchorIn(null);
};
return (
<AppBar position={sticky ? "sticky" : "relative"}>
<Toolbar>
@@ -117,8 +133,8 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
anchorEl={anchorEl}
open={openUserMenu}
onClose={handleCloseUserMenu}
slotProps={{
list:{ 'aria-labelledby': 'user-menu-button' }
MenuListProps={{
'aria-labelledby': 'user-menu-button',
}}
>
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
@@ -131,9 +147,45 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
</Stack>
)}
{!user && (
<Link to="/login"><Button>Login</Button></Link>
<Link to="/auth/login"><Button>Login</Button></Link>
)}
<I18nPicker />
<Button
id="i18n-button"
aria-controls={openI18nMenu ? 'i18n-menu' : undefined}
aria-haspopup="true"
aria-expanded={openI18nMenu ? 'true' : undefined}
onClick={handleOpenI18nMenu}>
<Typography
sx={{
display: {
xs: "none",
sm: "inline-block",
},
}}
variant="subtitle2"
>
{currentLocale}
</Typography>&nbsp;
<Avatar src={`/images/flags/${currentLocale}.svg`} alt={currentLocale}/>
</Button>
<Menu
id="i18n-menu"
open={openI18nMenu}
anchorEl={anchorIn}
onClose={handleCloseI18nMenu}
>
{[...(i18n.languages || [])].sort().map((lang: string) => (
<MenuItem
key={lang}
onClick={() => changeLocale(lang)}
>
<span style={{ marginRight: 8 }}>
<Avatar src={`/images/flags/${lang}.svg`} alt={lang}/>
</span>
{lang === "en" ? "English" : "Français"}
</MenuItem>
))}
</Menu>
</Stack>
</Stack>
</Toolbar>

View File

@@ -1,14 +1,9 @@
import React, { createContext, PropsWithChildren } from 'react';
import { IFirm } from "../interfaces";
import { useParams } from "react-router";
import { useOne } from "@refinedev/core";
import { CircularProgress } from "@mui/material";
import { FirmInitForm } from "../pages/firm";
import { Header } from "../components";
type FirmContextType = {
currentFirm: IFirm,
partnerMap?: Map<string, string>
}
export const FirmContext = createContext<FirmContextType>(
@@ -17,35 +12,13 @@ export const FirmContext = createContext<FirmContextType>(
export const FirmContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
const { instance, firm } = useParams<IFirm>();
const { data, isError, error, isLoading } = useOne({resource: 'firm', id: `${instance}/${firm}/`, errorNotification: false});
const { instance, firm } = useParams<IFirm>()
if (instance === undefined || firm === undefined) {
throw({statusCode: 400});
return "Error"
}
const currentFirm: IFirm = { instance, firm }
if (isLoading) {
return <CircularProgress />
}
if (isError && error) {
if (error.statusCode == 405) {
return <><Header /><FirmInitForm currentFirm={currentFirm} /></>
}
if (error.statusCode == 404) {
throw error;
}
}
currentFirm.entity = data?.data.entity;
let value: FirmContextType = {
currentFirm: currentFirm,
partnerMap: new Map(data?.data.partner_list.map((item: any) => [item.id, item.label])),
}
return (
<FirmContext.Provider value={value} >
<FirmContext.Provider value={{currentFirm: {instance, firm}}} >
{ children }
</FirmContext.Provider>
);

View File

@@ -8,14 +8,13 @@ i18n
.use(detector)
.use(initReactI18next)
.init({
supportedLngs: ["enUS", "frFR"],
supportedLngs: ["en", "fr"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // "http/locales/{{lng}}/{{ns}}.json"
loadPath: "/locales/{{lng}}/{{ns}}.json", // locale files path
},
//saveMissing: true,
ns: ["common"],
defaultNS: "common",
fallbackLng: ["enUS", "frFR"],
fallbackLng: ["en", "fr"],
});
export default i18n;

View File

@@ -2,7 +2,6 @@
export type IFirm = {
instance: string,
firm: string
entity?: any
}
type User = {

View File

@@ -3,12 +3,7 @@ import Form from "@rjsf/mui";
import { RegistryFieldsType, RegistryWidgetsType, RJSFSchema, UiSchema } from "@rjsf/utils";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
import ArrayFieldTemplate from "./templates/ArrayFieldTemplate"
import ArrayFieldItemTemplate from "./templates/ArrayFieldItemTemplate";
import { ResourceContext } from "../contexts/ResourceContext";
import { ReactNode } from "react";
import { ParametersContextProvider } from "../contexts/parameters-context";
import CrudArrayField from "./fields/crud-array-field";
type BaseFormProps = {
schema: RJSFSchema,
@@ -17,43 +12,32 @@ type BaseFormProps = {
onChange?: (data: any) => void,
uiSchema?: UiSchema,
formData?: any,
children?: ReactNode
}
export const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget,
TextWidget: CrudTextWidget
};
export const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField,
ArrayField: CrudArrayField
}
const customTemplates = {
ArrayFieldTemplate,
ArrayFieldItemTemplate
AnyOfField: UnionEnumField
}
export const BaseForm: React.FC<BaseFormProps> = (props) => {
const { schema, uiSchema, resourceBasePath, formData, children, onSubmit, onChange } = props;
const { schema, uiSchema, resourceBasePath, formData, onSubmit, onChange } = props;
return (
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
<ParametersContextProvider>
<Form
schema={schema}
uiSchema={uiSchema === undefined ? {} : uiSchema}
formData={formData}
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
templates={customTemplates}
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
children={children}
/>
</ParametersContextProvider>
<Form
schema={schema}
uiSchema={uiSchema === undefined ? {} : uiSchema}
formData={formData}
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
/>
</ResourceContext.Provider>
)
}

View File

@@ -1,73 +0,0 @@
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}
/>
)
}

View File

@@ -1,39 +1,62 @@
import { ReactNode } from "react";
import { useEffect, useState } from "react";
import { CircularProgress } from "@mui/material";
import { useForm } from "@refinedev/core";
import { UiSchema } from "@rjsf/utils";
import { jsonschemaProvider } from "../providers/jsonschema-provider";
import { BaseForm } from "./base-form";
import { useResourceSchema } from "../hook";
type CrudFormProps = {
schemaName: string,
uiSchema?: UiSchema,
record?: any,
resourceBasePath: string,
onSubmit?: (data: any) => void,
defaultValue?: any,
children?: ReactNode
card?: boolean
resourceBasePath?: string,
resource: string,
id?: string,
onSuccess?: (data: any) => void,
defaultValue?: any
}
export const CrudForm: React.FC<CrudFormProps> = (props) => {
const { schemaName, uiSchema, record, resourceBasePath, defaultValue, children, onSubmit=(data: any) => {}, card=false } = props;
const type = record === undefined ? "create" : card ? "card" : "update"
const { schema, schemaLoading } = useResourceSchema(schemaName, type);
const { schemaName, uiSchema, resourceBasePath="" ,resource, id, onSuccess, defaultValue } = props;
if(schemaLoading) {
const { onFinish, query, formLoading } = useForm({
resource: resourceBasePath == "" ? resource : `${resourceBasePath}/${resource}`,
action: id === undefined ? "create" : "edit",
redirect: "show",
id,
onMutationSuccess: (data: any) => { if (onSuccess) { onSuccess(data) } },
});
const [schema, setSchema] = useState({});
const [schemaLoading, setSchemaLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const schemaFullName = id === undefined ? `${schemaName}Create` : `${schemaName}Update`;
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaFullName);
setSchema(resourceSchema);
setSchemaLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setSchemaLoading(false);
}
};
fetchSchema();
}, []);
if(formLoading || schemaLoading) {
return <CircularProgress />
}
const record = query?.data?.data || defaultValue;
return (
<BaseForm
schema={schema}
uiSchema={uiSchema}
formData={record || defaultValue}
formData={record}
resourceBasePath={resourceBasePath}
onSubmit={
(data: any) => onSubmit(data)
(data: any) => onFinish(data)
}
children={children}
/>
)
}

View File

@@ -1,56 +0,0 @@
import { CircularProgress } from "@mui/material";
import { DataGrid, GridColDef, GridColumnVisibilityModel, GridValidRowModel } from "@mui/x-data-grid";
import { UiSchema } from "@rjsf/utils";
import { useResourceColumns } from "../hook";
type CrudListProps = {
schemaName: string,
uiSchema?: UiSchema,
columnDefinitions: ColumnDefinition[],
dataGridProps: any,
resourceBasePath: string,
onRowClick?: (params: any) => void
}
type ColumnSchema<T extends GridValidRowModel> = {
columns: GridColDef<T>[],
columnVisibilityModel: GridColumnVisibilityModel
}
type ColumnDefinition = {
field: string,
column: Partial<GridColDef>,
hide?: boolean
}
const CrudList = <T extends GridValidRowModel>(props: CrudListProps) => {
const {
dataGridProps,
onRowClick,
schemaName,
columnDefinitions
} = props;
const { columnSchema, columnLoading } = useResourceColumns<T>(schemaName, columnDefinitions);
if (columnLoading || columnSchema === undefined) {
return <CircularProgress />
}
return (
<DataGrid
{...dataGridProps}
columns={columnSchema.columns}
onRowClick={onRowClick}
pageSizeOptions={[10, 15, 25, 50, 100]}
disableColumnFilter={true}
initialState={{
columns: {
columnVisibilityModel: columnSchema.columnVisibilityModel
}
}}
/>
)
}
export default CrudList;

View File

@@ -1,146 +0,0 @@
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

@@ -1,96 +0,0 @@
import { CSSProperties } from 'react';
import Box from '@mui/material/Box';
import Grid2 from '@mui/material/Grid2';
import Paper from '@mui/material/Paper';
import { ArrayFieldTemplateItemType, FormContextType, RJSFSchema, StrictRJSFSchema } from '@rjsf/utils';
import Stack from "@mui/material/Stack";
/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldItemTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateItemType<T, S, F>) {
const {
children,
disabled,
hasToolbar,
hasCopy,
hasMoveDown,
hasMoveUp,
hasRemove,
index,
onCopyIndexClick,
onDropIndexClick,
onReorderClick,
readonly,
uiSchema,
registry,
} = props;
const { CopyButton, MoveDownButton, MoveUpButton, RemoveButton } = registry.templates.ButtonTemplates;
const btnStyle: CSSProperties = {
flex: 1,
paddingLeft: 6,
paddingRight: 6,
fontWeight: 'bold',
minWidth: 0,
};
const displayToolbar = hasToolbar && !props.readonly;
return (
<Grid2 container alignItems='center'>
<Grid2 style={{ overflow: 'auto' }} size={ displayToolbar ? 11 : 12}>
<Box mb={2}>
<Paper elevation={2}>
<Box p={2}>{children}</Box>
</Paper>
</Box>
</Grid2>
{displayToolbar && (
<Grid2 size={1}>
<Stack direction="column">
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
style={btnStyle}
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasCopy && (
<CopyButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onCopyIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasRemove && (
<RemoveButton
style={btnStyle}
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
</Stack>
</Grid2>
)}
</Grid2>
);
}

View File

@@ -1,105 +0,0 @@
import Box from '@mui/material/Box';
import Grid2 from '@mui/material/Grid2';
import Paper from '@mui/material/Paper';
import {
getTemplate,
getUiOptions,
ArrayFieldTemplateProps,
ArrayFieldTemplateItemType,
FormContextType,
} from '@rjsf/utils';
import { CrudTextRJSFSchema } from "../widgets/crud-text-widget";
import Typography from "@mui/material/Typography";
/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldTemplate<
T = any,
S extends CrudTextRJSFSchema = CrudTextRJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateProps<T, S, F>) {
const { canAdd, disabled, idSchema, uiSchema, items, onAddClick, readonly, registry, required, schema, title } =
props;
const uiOptions = getUiOptions<T, S, F>(uiSchema);
const ArrayFieldDescriptionTemplate = getTemplate<'ArrayFieldDescriptionTemplate', T, S, F>(
'ArrayFieldDescriptionTemplate',
registry,
uiOptions
);
const ArrayFieldItemTemplate = getTemplate<'ArrayFieldItemTemplate', T, S, F>(
'ArrayFieldItemTemplate',
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T, S, F>(
'ArrayFieldTitleTemplate',
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton },
} = registry.templates;
let gridSize = 12;
let numbered = false;
if (schema.props) {
if (schema.props.hasOwnProperty("items_per_row")) {
gridSize = gridSize / schema.props.items_per_row;
}
if (schema.props.hasOwnProperty("numbered")) {
numbered = schema.props.numbered;
}
}
return (
<Paper elevation={2}>
<Box p={2}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
schema={schema}
uiSchema={uiSchema}
required={required}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<Grid2 container justifyContent='flex-start'>
{ items &&
items.map(({ key, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>, index) => (
<Grid2 key={key} size={gridSize} >
<Grid2 container sx={{alignItems: "center"}} >
{numbered &&<Grid2 size={.5} ><Typography variant="h4">{index + 1}</Typography></Grid2>}
<Grid2 size={numbered ? 11.5 : 12} ><ArrayFieldItemTemplate key={key} {...itemProps} /></Grid2>
</Grid2>
</Grid2>
))
}
</Grid2>
{ canAdd && (
<Grid2 container justifyContent='flex-end'>
<Grid2>
<Box mt={2}>
<AddButton
className='array-item-add'
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
</Box>
</Grid2>
</Grid2>
)}
</Box>
</Paper>
);
}

View File

@@ -1,6 +1,6 @@
import React from "react";
import { getDefaultRegistry } from "@rjsf/core";
import { FormContextType, RJSFSchema, WidgetProps } from "@rjsf/utils";
import { FormContextType, getTemplate, RJSFSchema, WidgetProps } from "@rjsf/utils";
import Typography from "@mui/material/Typography";
import ForeignKeyWidget from "./foreign-key";

View File

@@ -6,10 +6,9 @@ import ClearIcon from '@mui/icons-material/Clear';
import EditIcon from '@mui/icons-material/Edit';
import NoteAddIcon from '@mui/icons-material/NoteAdd';
import React, { useState, useEffect, useContext, Fragment } from "react";
import { useForm, useList, useOne } from "@refinedev/core";
import { useList, useOne } from "@refinedev/core";
import { ResourceContext } from "../../contexts/ResourceContext";
import { CrudForm } from "../crud-form";
import { ParametersContext } from "../../contexts/parameters-context";
export type ForeignKeyReference = {
resource: string,
@@ -22,7 +21,6 @@ export type ForeignKeySchema = RJSFSchema & {
foreignKey?: {
reference: ForeignKeyReference
}
props? : any
}
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
@@ -44,10 +42,10 @@ export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = F
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) => {
const { onChange, label, fieldId, schema } = props;
if (schema.foreignKey === undefined) {
if (props.schema.foreignKey === undefined) {
return;
}
const { onChange, label } = props
const [openFormModal, setOpenFormModal] = useState(false);
const [searchString, setSearchString] = useState<string>("");
@@ -57,19 +55,12 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
return () => clearTimeout(handler);
}, [searchString]);
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 { resource, schema, label: labelField = "label" } = props.schema.foreignKey.reference
const { basePath } = useContext(ResourceContext)
const { data, isLoading } = useList({
resource: `${basePath}/${resource}`,
pagination: { current: 1, pageSize: 10, mode: "server" },
filters: [{ field: "search", operator: "contains", value: debouncedInputValue }],
filters: [{ field: "label", operator: "contains", value: debouncedInputValue }],
sorters: [{ field: "label", order: "asc" }],
});
@@ -112,14 +103,14 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
aria-describedby="modal-modal-description"
>
<DialogContent>
<FormContainerNew
schemaName={fkSchema}
<FormContainer
schemaName={schema}
resourceBasePath={basePath}
resource={resource}
uiSchema={{}}
onSuccess={(data: any) => {
setOpenFormModal(false)
onChange(data.id);
onChange(data.data.id);
}}
/>
</DialogContent>
@@ -131,37 +122,28 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F> & { onClear: () => void }
) => {
const { onClear, value, schema, id: fieldId } = props;
const { onClear, value } = props;
const [openFormModal, setOpenFormModal] = React.useState(false);
if (props.schema.foreignKey === undefined) {
return;
}
const { resource, schema: fkSchema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
const { resource, schema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
const { basePath } = useContext(ResourceContext)
const { data, isLoading, isSuccess } = useOne({
const { data, isLoading } = useOne({
resource: `${basePath}/${resource}`,
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) {
return <CircularProgress />
}
const record = data.data;
return (
<>
<TextField label={ props.label } variant="outlined" disabled={true} value={record[labelField]}
<TextField label={ props.label } variant="outlined" disabled={true} value={data.data[labelField]}
slotProps={{
input: {
endAdornment: (
@@ -176,7 +158,7 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
},
}}
/>
{ displayedFields && <Preview record={record} displayedFields={displayedFields}/>}
{ displayedFields && <Preview id={value} basePath={basePath} resource={resource} displayedFields={displayedFields}/>}
<Modal
open={openFormModal}
onClose={() => setOpenFormModal(false)}
@@ -184,8 +166,8 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
aria-describedby="modal-modal-description"
>
<DialogContent>
<FormContainerEdit
schemaName={fkSchema}
<FormContainer
schemaName={schema}
resourceBasePath={basePath}
resource={resource}
uiSchema={{}}
@@ -221,54 +203,26 @@ type FormContainerProps = {
onSuccess: (data: any) => void
}
const FormContainerEdit = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, id, onSuccess } = props;
const { onFinish, query, formLoading } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "edit",
id: id,
const FormContainer = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, id = undefined, onSuccess } = props;
return (
<Box sx={{ ...modalStyle, width: 800 }}>
<CrudForm schemaName={schemaName} resourceBasePath={resourceBasePath} resource={resource} uiSchema={uiSchema} id={id} onSuccess={(data) => onSuccess(data)} />
</Box>
)
}
const Preview = (props: {id: string, resource: string, basePath: string, displayedFields: [string]}) => {
const { basePath, resource, id, displayedFields } = props
const { data, isLoading } = useOne({
resource: `${basePath}/${resource}`,
id
});
if (formLoading || query?.data === undefined) {
if (isLoading || data === undefined) {
return <CircularProgress />
}
return (
<Box sx={{ ...modalStyle, width: 800 }}>
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
record={query.data.data}
onSubmit={(data:any) => {
onFinish(data);
onSuccess(data);
}} />
</Box>
)
}
const FormContainerNew = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, onSuccess } = props;
const { onFinish } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "create",
onMutationSuccess: data => onSuccess(data.data)
});
return (
<Box sx={{ ...modalStyle, width: 800 }}>
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
onSubmit={(data:any) => { onFinish(data);}}
/>
</Box>
)
}
const Preview = (props: {record: any, displayedFields: [string]}) => {
const { record, displayedFields } = props
return (
<Grid2 container spacing={2}>
@@ -276,29 +230,10 @@ const Preview = (props: {record: any, displayedFields: [string]}) => {
return (
<Fragment key={index}>
<Grid2 size={2}><Container>{field}</Container></Grid2>
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: record[field] }} ></Container></Grid2>
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: data.data[field] }} ></Container></Grid2>
</Fragment>
)
})}
</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

@@ -25,11 +25,9 @@ declare module "@tiptap/core" {
export default Extension.create<IndentOptions>({
name: "indent",
addOptions() {
return {
types: ["paragraph", "heading"],
margin: 40
}
defaultOptions: {
types: ["paragraph", "heading"],
margin: 40
},
addGlobalAttributes() {

View File

@@ -57,7 +57,7 @@ const StyledLabelledOutlined = styled(LabelledOutlined)(({ theme }) => [{
const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) => {
const { schema, value, onChange, label, id, readonly } = props;
const { schema, value, onChange, label, id } = props;
const isMultiline = schema.props.multiline === true;
let editorOptions: UseEditorOptions;
@@ -92,15 +92,14 @@ const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSche
<TextContainer>
<RichTextEditorProvider editor={editor}>
<TableBubbleMenu />
{!readonly && <RichTextField
controls={
<MenuControlsContainer>
{isMultiline ? multilineButtons : singlelineButtons}
</MenuControlsContainer>
}
variant="standard"
/>}
{readonly && <RichTextField variant="standard" disabled={true}/>}
<RichTextField
controls={
<MenuControlsContainer>
{isMultiline ? multilineButtons : singlelineButtons}
</MenuControlsContainer>
}
variant="standard"
/>
</RichTextEditorProvider>
</TextContainer>
<RightContainer>&nbsp;</RightContainer>

View File

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

View File

@@ -1,30 +0,0 @@
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

@@ -1,109 +0,0 @@
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 }
}

View File

@@ -1,319 +1,56 @@
import { RJSFSchema } from '@rjsf/utils';
import i18n from '../../../i18n'
import { JSONSchema7Definition } from "json-schema";
import { GridColDef } from "@mui/x-data-grid";
import { GridColType } from "@mui/x-data-grid/models/colDef/gridColType";
import { RJSFSchema } from '@rjsf/utils';
const API_URL = "/api/v1";
type CrudRJSFSchema = RJSFSchema & {
properties?: {
[key: string]: JSONSchema7Definition & {
readOnly?: boolean | undefined;
};
} | undefined;
}
const meta_fields = ["id", "label", "created_at", "created_by", "updated_at", "updated_by"]
export const jsonschemaProvider = {
getCardResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
const updateSchema = await getResourceSchema(`${resourceName}Update`);
const readSchema = await getResourceSchema(`${resourceName}Read`);
for (let prop_name in readSchema.properties) {
if (meta_fields.indexOf(prop_name) > -1) {
delete readSchema.properties[prop_name];
} else if (! updateSchema.hasOwnProperty(prop_name)) {
if (is_reference(readSchema.properties[prop_name])) {
let subresourceName = get_reference_name(readSchema.properties[prop_name]);
readSchema.components.schemas[subresourceName].readOnly = true;
} else {
readSchema.properties[prop_name].readOnly = true;
}
}
}
changePropertiesOrder(readSchema);
return readSchema
},
getReadOnlyResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
const updateSchema = await getResourceSchema(`${resourceName}Update`);
const readSchema = await getResourceSchema(`${resourceName}Read`);
for (let prop_name in readSchema.properties) {
if (updateSchema.hasOwnProperty(prop_name)) {
delete readSchema.properties[prop_name];
} else {
readSchema.properties[prop_name].readOnly = true;
}
}
return readSchema
},
getReadResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
return getResourceSchema(`${resourceName}Read`)
},
getUpdateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
return getResourceSchema(`${resourceName}Update`)
},
getCreateResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
return getResourceSchema(`${resourceName}Create`)
},
getReadResourceColumns: async (resourceName: string): Promise<GridColDef[]> => {
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> => {
getResourceSchema: async (resourceName: string): Promise<RJSFSchema> => {
return buildResource(await getJsonschema(), resourceName)
}
const getColumns = async (resourceName: string): Promise<GridColDef[]> => {
return buildColumns(await getJsonschema(), resourceName)
}
const getFilters = async (resourceName: string, resourcePath: string): Promise<FilterField[]> => {
return buildFilters(await getJsonschema(), resourceName, resourcePath);
}
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
}
}
};
return false
}
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 });
}
}
let rawSchema: RJSFSchema;
const getJsonschema = async (): Promise<RJSFSchema> => {
if (rawSchema === undefined) {
const response = await fetch(`${API_URL}/openapi.json`,);
rawSchema = await response.json();
}
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[] = [];
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) {
let prop = resource.properties[prop_name];
if (is_reference(prop)) {
const subresourceName = get_reference_name(prop);
result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name))
} else if (is_union(prop)) {
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
let seen = new Set<string>();
for (let i in union) {
if (is_reference(union[i])) {
const subresourceName = get_reference_name(union[i]);
const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name);
for (const s of subcolumns) {
if (! seen.has(s.field)) {
result.push(s);
seen.add(s.field);
}
}
}
}
} else if (is_enum(prop)) {
let seen = new Set<string>();
for (let i in prop.allOf) {
if (is_reference(prop.allOf[i])) {
const subresourceName = get_reference_name(prop.allOf[i]);
const subcolumns = buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name);
for (const s of subcolumns) {
if (! seen.has(s.field)) {
result.push(s);
seen.add(s.field);
}
}
}
}
} else if (is_array(prop) && is_reference(prop.items)) {
const subresourceName = get_reference_name(prop.items);
result = result.concat(buildColumns(rawSchemas, subresourceName, prefix ? `${prefix}.${prop_name}` : prop_name))
} else {
let valueGetter: undefined|((value: any, row: any) => any) = undefined;
let type: GridColType = "string";
if (is_array(prop)) {
valueGetter = (value: any[], row: any ) => {
return value.concat(".");
}
} else if (prefix !== undefined) {
valueGetter = (value: any, row: any ) => {
let parent = row;
for (const col of prefix.split(".")) {
parent = parent[col];
}
return parent ? parent[prop_name] : "";
}
} else {
if (prop.type == "string" && prop.format == "date-time") {
type = "dateTime"
valueGetter = (value: string) => new Date(value)
}
}
const column: GridColDef = {
field: prefix ? `${prefix}.${prop_name}` : prop_name,
headerName: i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title) as string,
type: type,
valueGetter: valueGetter
}
result.push(column);
}
}
return result;
return rawSchema;
}
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
const shortResourceName = shortenResourceName(resourceName);
const jst = new JsonSchemaTraverser(rawSchemas);
let resource = structuredClone(jst.getResource(resourceName));
let resource;
resource = structuredClone(rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} };
for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name];
if (is_reference(prop)) {
buildReference(rawSchemas, resource, prop);
resolveReference(rawSchemas, resource, prop);
} else if (is_union(prop)) {
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
for (let i in union) {
if (is_reference(union[i])) {
buildReference(rawSchemas, resource, union[i]);
resolveReference(rawSchemas, resource, union[i]);
}
}
} else if (is_enum(prop)) {
for (let i in prop.allOf) {
if (is_reference(prop.allOf[i])) {
buildReference(rawSchemas, resource, prop.allOf[i]);
resolveReference(rawSchemas, resource, prop.allOf[i]);
}
}
} else if (is_array(prop) && is_reference(prop.items)) {
buildReference(rawSchemas, resource, prop.items);
resolveReference(rawSchemas, resource, prop.items);
}
if (prop.hasOwnProperty("title")) {
prop.title = i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title);
}
}
if (resource.hasOwnProperty("title")) {
resource.title = i18n.t(`schemas.${shortResourceName}.resource_title`, resource.title);
}
return resource;
}
function buildReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
const subresourceName = get_reference_name(prop_reference);
const subresource = buildResource(rawSchemas, subresourceName);
resource.components.schemas[subresourceName] = subresource;
@@ -326,35 +63,23 @@ function buildReference(rawSchemas: RJSFSchema, resource: any, prop_reference: a
function changePropertiesOrder(resource: any) {
let created_at;
let created_by;
let updated_at;
let updated_by;
let new_properties: any = {};
for (let prop_name in resource.properties) {
if (prop_name == 'created_at') {
created_at = resource.properties[prop_name];
} else if (prop_name == 'created_by') {
created_by = resource.properties[prop_name];
} else if (prop_name == 'updated_at') {
updated_at = resource.properties[prop_name];
} else if (prop_name == 'updated_by') {
updated_by = resource.properties[prop_name];
}else {
} else {
new_properties[prop_name] = resource.properties[prop_name];
}
}
if (created_at) {
new_properties['created_at'] = created_at;
}
if (created_by) {
new_properties['created_by'] = created_by;
}
if (updated_at) {
new_properties['updated_at'] = updated_at;
}
if (updated_by) {
new_properties['updated_by'] = updated_by;
}
resource.properties = new_properties
}
@@ -382,157 +107,82 @@ function get_reference_name(prop: any) {
return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
}
function convertCamelToSnake(str: string): string {
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
}
function shortenResourceName(resourceName: string) {
return convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
}
function getPropertyI18nLabel(shortResourceName: string, fieldName: string): string {
if (meta_fields.indexOf(fieldName) > -1) {
return i18n.t(`schemas.${convertCamelToSnake(fieldName)}`);
}
const path = `schemas.${shortResourceName}.${convertCamelToSnake(fieldName)}`
return i18n.t(path)
}
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.`);
function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_name: string): boolean {
if (is_array(resource)) {
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);
}
return this.rawSchemas.components.schemas[resourceName]
}
throw new Error("Jsonschema format not implemented in property finder");
}
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
}
function get_descendant(rawSchemas: RJSFSchema, resource: RJSFSchema, property_name: string): 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!) {
return resource.properties[property_name] as RJSFSchema;
} else if (is_reference(resource)) {
let subresourceName = get_reference_name(resource);
let subresource = buildResource(rawSchemas, subresourceName);
return get_descendant(rawSchemas, subresource, property_name);
} else if (is_union(resource)) {
for (const ref of resource.oneOf!) {
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
}
}
} else if (is_enum(resource)) {
for (const ref of resource.allOf!) {
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
}
}
if (pathSchema !== undefined) {
break
}
}
}
if (pathSchema === undefined) {
throw ("Path not found in schema");
}
return pathSchema
}
throw new Error("property not found or Jsonschema format not implemented");
}
function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{
const pointFirstPosition = path.indexOf('.')
if (pointFirstPosition == -1) {
return has_descendant(rawSchemas, resource, path);
}
public hasDescendant = (resource: RJSFSchema, property_name: string): boolean => {
if (is_array(resource)) {
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 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)
),
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
&& path_exists(
rawSchemas,
get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)),
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)
);
}

View File

@@ -1,184 +0,0 @@
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;

View File

@@ -1,4 +0,0 @@
export const Error404Page = () => {
return <h2>EROR NO FUND</h2>
};

View File

@@ -1,14 +1,9 @@
import { useContext } from "react";
import { Route, Routes, useParams } from "react-router";
import { useOne, useTranslation } from "@refinedev/core";
import { DeleteButton } from "@refinedev/mui";
import { CircularProgress, Stack } from "@mui/material";
import { CrudForm } from "../../lib/crud/components/crud-form";
import { FirmContext } from "../../contexts/FirmContext";
import { Route, Routes } from "react-router";
import List from "./base-page/List";
import Cartouche from "../../components/Cartouche";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
export type Contract = {
type Contract = {
id: string,
label: string
}
@@ -18,53 +13,22 @@ export const ContractRoutes = () => {
<Routes>
<Route index element={ <ListContract /> } />
<Route path="/edit/:record_id" element={ <EditContract /> } />
<Route path="/create" element={ <CreateContract /> } />
</Routes>
);
}
const ListContract = () => {
const columns = [
{ field: "label", column: { flex: 1 }},
{ field: "status", column: { width: 160 }},
{ field: "updated_at", column: { width: 160 }},
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Contract> resource={`contracts`} schemaName={"Contract"} columns={columns} />
return <List<Contract> resource={`contracts`} columns={columns} />
}
const EditContract = () => {
const { currentFirm } = useContext(FirmContext);
const { translate: t } = useTranslation();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { record_id } = useParams();
return <Edit<Contract> resource={`contracts`} schemaName={"Contract"} />
}
const { data, isLoading } = useOne({resource: `${resourceBasePath}/contracts`, id: record_id,});
if (isLoading || data?.data === undefined) {
return <CircularProgress />
}
const record = data.data;
return (
<>
<h2>{record.label}</h2>
<Cartouche record={record}/>
<CrudForm
resourceBasePath={resourceBasePath}
schemaName={"Contract"}
uiSchema={{"ui:readonly": true }}
record={record}
card={true}
>
<Stack
direction="row"
spacing={2}
sx={{
justifyContent: "flex-end",
alignItems: "center",
}}>
{ record.status == "published" && (<DeleteButton variant="contained" size="large" color="error" recordItemId={record_id}/>) }
</Stack>
</CrudForm>
</>
)
}
const CreateContract = () => {
return <New<Contract> resource={`contracts`} schemaName={"Contract"} />
}

View File

@@ -1,16 +1,7 @@
import { Navigate, Route, Routes, useParams } from "react-router";
import { CircularProgress } from "@mui/material";
import React, { useContext, useState } from "react";
import { useOne, useTranslation } from "@refinedev/core";
import { BaseForm } from "../../lib/crud/components/base-form";
import { ForeignKeyReference, ForeignKeySchema } from "../../lib/crud/components/widgets/foreign-key";
import { FirmContext } from "../../contexts/FirmContext";
import { Route, Routes } from "react-router";
import List from "./base-page/List";
import Edit from "./base-page/Edit";
import New from "./base-page/New";
import { Contract } from "./ContractRoutes";
import dayjs from "dayjs";
type Draft = {
id: string,
@@ -29,146 +20,15 @@ export const DraftRoutes = () => {
const ListDraft = () => {
const columns = [
{ field: "label", column: { flex: 1 }},
{ field: "status", column: { width: 160 }},
{ field: "updated_at", column: { width: 160 }},
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Draft> resource={`contracts/drafts`} columns={columns} schemaName={"Contract"} />
return <List<Draft> resource={`contracts/drafts`} columns={columns} />
}
const EditDraft = () => {
const { currentFirm } = useContext(FirmContext);
const { record_id } = useParams();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { data, isLoading } = useOne({
resource: `${resourceBasePath}/contracts/drafts`,
id: record_id
})
if (isLoading) {
return <CircularProgress />
}
if (!data?.data) {
return <Navigate to="../" />
}
const draft = data?.data
const readOnly = draft.status === "published";
const uiSchema = {
"ui:readonly": readOnly
}
return (
<>
<Edit<Draft> resource={"contracts/drafts"} schemaName={"ContractDraft"} uiSchema={uiSchema} />
<ContractCreate draft={draft}></ContractCreate>
</>
)
}
const ContractCreate = (props: { draft: any}) => {
const { translate: t } = useTranslation();
const { draft } = props;
if (draft.status === "published") {
return <h4>{t("resource.draft.already_published") }</h4>
}
if (draft.status === "in_progress") {
return (
<>
<h4>{ t("resource.draft.todo") + ":" }</h4>
<ul>{ draft.todo.map((item: any) => <li>{ item }</li>) }</ul>
</>
)
}
return <New<Contract>
resource={"contracts"}
schemaName={"Contract"}
defaultValue={{
date: dayjs().format("YYYY-MM-DD"),
location: "Los Santos, SA",
draft_id: draft.id
}}
uiSchema={{ draft_id: { 'ui:widget': 'hidden' } }}
/>
}
type ForeignKeySubSchema = ForeignKeySchema & {
properties: { [key: string]: { foreignKey: { reference: ForeignKeyReference } } }
return <Edit<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
}
const CreateDraft = () => {
const [chosenDraft, setChosenDraft] = useState<string|null>(null)
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const templateFieldSchema: ForeignKeySubSchema = {
type: "object",
properties: {
template_id: {
type: "string",
title: "Find a template",
foreignKey: {
reference: {
resource: "templates/contracts",
schema: "ContractTemplate"
}
}
}
},
};
const templateForm = (
<BaseForm
schema={templateFieldSchema}
formData={{template_id: chosenDraft}}
resourceBasePath={resourceBasePath}
onChange={(data) => {
const { template_id } = data;
setChosenDraft(template_id);
}}
>
&nbsp;
</BaseForm>
)
if (chosenDraft !== null) {
return (
<>
{templateForm}
<CreateDraftFromTemplate template_id={chosenDraft}/>
</>
)
}
return (
<>
{templateForm}
<New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
</>
)
}
const CreateDraftFromTemplate = (props: { template_id: string }) => {
const { template_id } = props;
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const resource = "templates/contracts"
const { data, isLoading } = useOne({
resource: `${resourceBasePath}/${resource}`,
id: template_id
});
if (isLoading || data === undefined) {
return <CircularProgress />
}
let template = { ...data.data };
template.provisions = data.data.provisions.map((item: any) => {
return { provision: {type: "template", provision_template_id: item.provision_template_id} }
})
return <New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} defaultValue={ template }/>
return <New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
}

View File

@@ -22,11 +22,10 @@ export const EntityRoutes = () => {
const ListEntity = () => {
const columns = [
{ field: "entity_data.type", column: { width: 110 }},
{ field: "label", column: { flex: 1 }},
{ field: "updated_at", column: { width: 160 }},
{ field: "label", headerName: "Label", flex: 1 },
{ field: "entity_data", headerName: "Type", flex: 1, valueFormatter: ({ type }: {type: string}) => type }
];
return <List<Entity> resource={`entities`} schemaName={"Entity"} columns={columns} />
return <List<Entity> resource={`entities`} columns={columns} />
}
const EditEntity = () => {

View File

@@ -21,10 +21,9 @@ export const ProvisionRoutes = () => {
const ListProvision = () => {
const columns = [
{ field: "label", column: { flex: 1 }},
{ field: "updated_at", column: { width: 160 }},
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} columns={columns} />
return <List<Provision> resource={`templates/provisions`} columns={columns} />
}
const EditProvision = () => {

View File

@@ -20,10 +20,9 @@ export const TemplateRoutes = () => {
const ListTemplate = () => {
const columns = [
{ field: "label", column: { flex: 1 }},
{ field: "updated_at", column: { width: 160 }},
{ field: "label", headerName: "Label", flex: 1 },
];
return <List<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} columns={columns} />
return <List<Template> resource={`templates/contracts`} columns={columns} />
}
const EditTemplate = () => {

View File

@@ -1,14 +1,8 @@
import { UiSchema } from "@rjsf/utils";
import { useContext } from "react";
import { useParams, Navigate, Link } from "react-router";
import { Button, CircularProgress } from "@mui/material";
import Stack from "@mui/material/Stack";
import SaveIcon from '@mui/icons-material/Save';
import { useForm, useTranslation } from "@refinedev/core";
import { DeleteButton } from "@refinedev/mui";
import { FirmContext } from "../../../contexts/FirmContext";
import { CrudForm } from "../../../lib/crud/components/crud-form";
import Cartouche from "../../../components/Cartouche";
import { UiSchema } from "@rjsf/utils";
import { useParams } from "react-router";
import { useContext } from "react";
import { FirmContext } from "../../../contexts/FirmContext";
type EditProps = {
resource: string,
@@ -19,56 +13,17 @@ type EditProps = {
const Edit = <T,>(props: EditProps) => {
const { schemaName, resource, uiSchema } = props;
const { currentFirm } = useContext(FirmContext);
const { translate: t } = useTranslation();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { record_id } = useParams();
const { onFinish, query, formLoading } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "edit",
redirect: "show",
id: record_id,
});
if (formLoading) {
return <CircularProgress />
}
if (!query?.data?.data) {
return <Navigate to="../" />
}
if (query.error?.status == 404) {
throw query.error
}
const record = query.data.data;
return (
<>
<Link to={"../"} >
<Button>{t("buttons.list")}</Button>
</Link>
<h2>{record.label}</h2>
<Cartouche record={record}/>
<CrudForm
resourceBasePath={resourceBasePath}
schemaName={schemaName}
uiSchema={uiSchema}
record={record}
onSubmit={(data: any) => onFinish(data)}
>
<Stack
direction="row"
spacing={2}
sx={{
justifyContent: "space-between",
alignItems: "center",
}}>
<Button type='submit' variant="contained" size="large"><SaveIcon />{t("buttons.save")}</Button>
<DeleteButton variant="contained" size="large" color="error" recordItemId={record_id}/>
</Stack>
</CrudForm>
</>
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
resource={resource}
id={record_id}
/>
)
}

View File

@@ -1,81 +1,44 @@
import React, { useContext } from "react";
import { Link, useNavigate } from "react-router"
import { UiSchema } from "@rjsf/utils";
import { CrudFilter, useTranslation } from "@refinedev/core";
import { List as RefineList, useDataGrid } from "@refinedev/mui";
import { DataGrid, GridColDef, GridValidRowModel } from "@mui/x-data-grid";
import { Link, useNavigate } from "react-router"
import React, { useContext } from "react";
import { Button } from "@mui/material";
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid";
import { FirmContext } from "../../../contexts/FirmContext";
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<T extends GridValidRowModel> = {
resource: string,
columns: ColumnDefinition[],
schemaName: string,
columns: GridColDef<T>[],
schemaName?: string,
uiSchema?: UiSchema,
}
type ColumnDefinition = {
field: string,
column: Partial<GridColDef>,
hide?: boolean
}
const List = <T extends GridValidRowModel>(props: ListProps) => {
const { resource, columns, schemaName } = props;
const { translate: t } = useTranslation();
const List = <T extends GridValidRowModel>(props: ListProps<T>) => {
const { resource, columns } = props;
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { dataGridProps, tableQuery, setFilters } = useDataGrid<T>({
resource: `${resourceBasePath}/${resource}`,
});
const { dataGridProps } = useDataGrid<T>({resource: `${resourceBasePath}/${resource}`});
const navigate = useNavigate();
if (tableQuery.error?.status == 404) {
throw tableQuery.error
}
const cols = React.useMemo<GridColDef<T>[]>(
() => columns,
[],
);
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);
const handleRowClick = (params: any, event: any) => {
navigate(`edit/${params.id}`)
}
return (
<RefineList>
<Link to={"create"} >
<Button>{t("buttons.create")}</Button>
<Button>Create</Button>
</Link>
<CrudFilters
resourceName={schemaName}
resourcePath={`${resourceBasePath}/${resource}`}
onChange={onFilterChange}
/>
<CrudList
dataGridProps={dataGridProps}
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
schemaName={schemaName}
resourceBasePath={resourceBasePath}
columnDefinitions={columns}
/>
<DataGrid
{...dataGridProps}
columns={cols}
onRowClick={handleRowClick} />
</RefineList>
)
}

View File

@@ -1,10 +1,7 @@
import { CrudForm } from "../../../lib/crud/components/crud-form";
import { UiSchema } from "@rjsf/utils";
import { useContext } from "react";
import { useForm, useTranslation } from "@refinedev/core";
import { CrudForm } from "../../../lib/crud/components/crud-form";
import { FirmContext } from "../../../contexts/FirmContext";
import SaveIcon from "@mui/icons-material/Save";
import { Button } from "@mui/material";
type NewProps = {
resource: string,
@@ -16,25 +13,16 @@ type NewProps = {
const New = <T,>(props: NewProps) => {
const { schemaName, resource, uiSchema, defaultValue } = props;
const { currentFirm } = useContext(FirmContext);
const { translate: t } = useTranslation();
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const { onFinish } = useForm({
resource: `${resourceBasePath}/${resource}`,
action: "create",
redirect: "show",
});
return (
<CrudForm
schemaName={schemaName}
uiSchema={uiSchema}
resourceBasePath={resourceBasePath}
resource={resource}
defaultValue={defaultValue}
onSubmit={(data: any) => onFinish(data)}
>
<Button type='submit' variant="contained" size="large"><SaveIcon />{t("buttons.create")}</Button>
</CrudForm>
/>
)
}

View File

@@ -1,8 +1,8 @@
import { Route, Routes, Link } from "react-router";
import React from "react";
import { useForm, useTranslation } from "@refinedev/core";
import { FirmContextProvider } from "../../contexts/FirmContext";
import React, { useContext } from "react";
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
import { Header } from "../../components";
import { useOne } from "@refinedev/core";
import { CrudForm } from "../../lib/crud/components/crud-form";
import { IFirm } from "../../interfaces";
import { EntityRoutes } from "./EntityRoutes";
@@ -10,44 +10,51 @@ import { ContractRoutes } from "./ContractRoutes";
import { DraftRoutes } from "./DraftRoutes";
import { TemplateRoutes } from "./TemplateRoutes";
import { ProvisionRoutes } from "./ProvisionRoutes";
import { ErrorBoundary } from "react-error-boundary";
import { Error404Page } from "../ErrorPage";
export const FirmRoutes = () => {
return (
<Routes>
<Route path="/:instance/:firm/*" element={
<ErrorBoundary fallback={<><Header /><Error404Page /></>} >
<FirmContextProvider>
<Header />
<Routes>
<Route index element={ <FirmHome /> } />
<Route path="/entities/*" element={ <EntityRoutes /> } />
<Route path="/provisions/*" element={ <ProvisionRoutes /> } />
<Route path="/templates/*" element={ <TemplateRoutes /> } />
<Route path="/drafts/*" element={ <DraftRoutes /> } />
<Route path="/contracts/*" element={ <ContractRoutes /> } />
</Routes>
</FirmContextProvider>
</ErrorBoundary>
<FirmContextProvider>
<Header />
<Routes>
<Route index element={ <FirmHome /> } />
<Route path="/entities/*" element={ <EntityRoutes /> } />
<Route path="/provisions/*" element={ <ProvisionRoutes /> } />
<Route path="/templates/*" element={ <TemplateRoutes /> } />
<Route path="/drafts/*" element={ <DraftRoutes /> } />
<Route path="/contracts/*" element={ <ContractRoutes /> } />
</Routes>
</FirmContextProvider>
} />
</Routes>
);
}
const FirmHome = () => {
const { translate: t } = useTranslation();
const { currentFirm } = useContext(FirmContext);
const { data: firm, isError, error, isLoading } = useOne({resource: 'firm', id: `${currentFirm.instance}/${currentFirm.firm}/`, errorNotification: false})
if (isLoading) {
return <h1>Loading...</h1>
}
if (isError && error?.statusCode == 405) {
return <FirmInitForm currentFirm={currentFirm} />
}
return (
<>
<h1>{t("dashboard.title")}</h1>
<h1>This is la firme {currentFirm.instance} / {currentFirm.firm}</h1>
<ul>
<li><Link to="entities">{t("schemas.entity.resource_plural")}</Link></li>
<li><Link to="provisions">{t("schemas.provision_template.resource_plural")}</Link></li>
<li><Link to="templates">{t("schemas.contract_template.resource_plural")}</Link></li>
<li><Link to="drafts">{t("schemas.contract_draft.resource_plural")}</Link></li>
<li><Link to="contracts">{t("schemas.contract.resource_plural")}</Link></li>
<li><Link to="entities">Entitées</Link></li>
<li><Link to="provisions">Templates de Clauses</Link></li>
<li><Link to="templates">Templates de Contrats</Link></li>
<li><Link to="drafts">Brouillons</Link></li>
<li><Link to="contracts">Contrats</Link></li>
</ul>
</>
);
}
@@ -55,28 +62,18 @@ type FirmInitFormPros = {
currentFirm: IFirm
}
export const FirmInitForm = (props: FirmInitFormPros) => {
const FirmInitForm = (props: FirmInitFormPros) => {
const { currentFirm } = props;
const { translate: t } = useTranslation();
const resourceBasePath = `firm`
const { onFinish } = useForm({
resource: `${resourceBasePath}/${currentFirm.instance}/${currentFirm.firm}`,
action: "create",
redirect: "show",
});
return (
<>
<h1>Initialization of {`${currentFirm.instance} / ${currentFirm.firm}`}</h1>
<CrudForm
schemaName={"CurrentFirmSchema"}
resourceBasePath={resourceBasePath}
defaultValue={{corporation: {entity_data: {activity: t("firm.default_activity") }}}}
schemaName={"CurrentFirmSchemaCreate"}
resource={`firm/${currentFirm.instance}/${currentFirm.firm}/`}
uiSchema={{
corporation: {entity_data: {employees: {"ui:style": {"display": "none"}}}},
}}
onSubmit={(data: any) => onFinish(data)}
/>
</>
)

View File

@@ -1,6 +1,6 @@
import { useForm, useInvalidateAuthStore } from "@refinedev/core";
import { useInvalidateAuthStore } from "@refinedev/core";
import { CrudForm } from "../../lib/crud/components/crud-form";
import { empty_user } from "../../providers/auth-provider";
import {empty_user} from "../../providers/auth-provider";
export const CreateFirm = () => {
const invalidateAuthStore = useInvalidateAuthStore()
@@ -9,19 +9,11 @@ export const CreateFirm = () => {
invalidateAuthStore().then();
}
const resourceBasePath = "hub/users";
const { onFinish } = useForm({
resource: `${resourceBasePath}/firms`,
action: "create",
redirect: "list",
onMutationSuccess: data => refreshUser()
});
return (
<CrudForm
schemaName={"Firm"}
resourceBasePath={resourceBasePath}
onSubmit={(data: any) => onFinish(data)}
schemaName={"FirmCreate"}
resource={"hub/users/firms/"}
onSuccess={() => { refreshUser() }}
/>
)
}

View File

@@ -12,7 +12,7 @@ const DEFAULT_LOGIN_REDIRECT = "/hub"
const authProvider: AuthProvider = {
login: async ({ providerName, email, password }) => {
const to_param = findGetParameter("to");
const redirect = to_param === null ? getLoginRedirect() : to_param
const redirect = to_param === null ? DEFAULT_LOGIN_REDIRECT : to_param
if (providerName) {
let scope = {};
if (providerName === "google") {
@@ -61,22 +61,18 @@ const authProvider: AuthProvider = {
const response = await fetch(`${API_URL}/hub/auth/logout`, { method: "POST" });
if (response.status == 204 || response.status == 401) {
forget_user();
return { success: true };
return {
success: true,
redirectTo: "/",
};
}
return { success: false };
},
check: async () => {
const user = get_user();
if (user == null || isEmpty(user)) {
const user_data = await get_me();
if (user_data) {
store_user(user_data)
return { authenticated: true }
}
if (get_user() == null) {
return {
authenticated: false,
redirectTo: "/auth/login",
logout: true
}
}
@@ -88,7 +84,11 @@ const authProvider: AuthProvider = {
return user;
}
const user_data = get_me()
const response = await fetch(`${API_URL}/hub/users/me`);
if (response.status < 200 || response.status > 299) {
return null;
}
const user_data = await response.json();
store_user(user_data)
return user_data;
@@ -154,8 +154,9 @@ const authProvider: AuthProvider = {
if (error?.status === 401) {
forget_user();
return {
error: { message: "Authentication required" },
redirectTo: "/auth/login",
logout: true,
error: { message: "Authentication required" },
} as OnErrorResponse;
}
else if (error?.status === 403) {
@@ -169,15 +170,6 @@ const authProvider: AuthProvider = {
},
};
async function get_me() {
const response = await fetch(`${API_URL}/hub/users/me`);
if (response.status < 200 || response.status > 299) {
return null;
}
const user_data = await response.json();
return user_data
}
function store_user(user: any) {
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(user));
}
@@ -208,11 +200,4 @@ function findGetParameter(parameterName: string) {
return result;
}
function getLoginRedirect() {
if (location.pathname == "/login") {
return DEFAULT_LOGIN_REDIRECT
}
return location.pathname + location.search;
}
export default authProvider;

View File

@@ -2,18 +2,6 @@ import type { DataProvider, HttpError } from "@refinedev/core";
const API_URL = "/api/v1";
function handleErrors(response: { status: number, statusText: string }) {
let message = response.statusText
if (response.status == 405) {
message = "Resource is not ready";
}
const error: HttpError = {
message: message,
statusCode: response.status,
};
return Promise.reject(error);
}
const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => {
if (id === "") {
@@ -21,7 +9,14 @@ const dataProvider: DataProvider = {
}
const response = await fetch(`${API_URL}/${resource}/${id}`);
if (response.status < 200 || response.status > 299) {
return handleErrors(response);
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
@@ -40,10 +35,17 @@ const dataProvider: DataProvider = {
});
if (response.status < 200 || response.status > 299) {
return handleErrors(response);
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
return { data };
},
getList: async ({ resource, pagination, filters, sorters, meta }) => {
@@ -61,12 +63,8 @@ const dataProvider: DataProvider = {
if (filters && filters.length > 0) {
filters.forEach((filter) => {
if ("field" in filter) {
if (filter.field == "search") {
params.append("search", filter.value);
} else {
params.append(`${filter.field.replace(".", "__")}__${filter.operator}`, filter.value);
}
if ("field" in filter && filter.value && filter.operator === "contains") {
params.append(filter.field + "__ilike", filter.value);
}
});
}
@@ -74,7 +72,14 @@ const dataProvider: DataProvider = {
const response = await fetch(`${API_URL}/${resource}/?${params.toString()}`);
if (response.status < 200 || response.status > 299) {
return handleErrors(response);
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
@@ -91,7 +96,7 @@ const dataProvider: DataProvider = {
};
},
create: async ({ resource, variables }) => {
const response = await fetch(`${API_URL}/${resource}/`, {
const response = await fetch(`${API_URL}/${resource}`, {
method: "POST",
body: JSON.stringify(variables),
headers: {
@@ -100,10 +105,18 @@ const dataProvider: DataProvider = {
});
if (response.status < 200 || response.status > 299) {
return handleErrors(response);
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
return { data };
},
deleteOne: async ({ resource, id, variables, meta }) => {
@@ -112,11 +125,21 @@ const dataProvider: DataProvider = {
});
if (response.status < 200 || response.status > 299) {
return handleErrors(response);
if (response.status == 405) {
const error: HttpError = {
message: "Resource is not ready",
statusCode: 405,
};
return Promise.reject(error);
}
throw response;
}
const data = await response.json();
return { data };
return {
data
};
},
getApiUrl: () => API_URL,
// Optional methods:

View File

@@ -1,11 +0,0 @@
FROM node:lts-alpine
WORKDIR /app
COPY app/package*.json ./
RUN npm install
COPY app/tsconfig*.json ./
COPY app/src ./src
EXPOSE 8100
CMD [ "npm", "--watch", "start" ]

26
i18n/app/.gitignore vendored
View File

@@ -1,26 +0,0 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
\*.local
# Editor directories and files
.vscode/_
!.vscode/extensions.json
.idea
.DS_Store
_.suo
_.ntvs_
_.njsproj
_.sln
\*.sw?

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
{
"name": "i18n Helper",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"serve": "node dist/index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@types/express": "^5.0.1",
"@types/node": "^22.15.3",
"express": "^5.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"dependencies": {
"body-parser": "^2.2.0"
}
}

View File

@@ -1,49 +0,0 @@
import express, { Request, Response } from 'express';
import bodyParser from "body-parser";
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
const app = express();
app.use(bodyParser.json());
const port = process.env.PORT || 8100;
app.post('/locales/add/:lng/:ns', (req: Request, res: Response) => {
const keyPath = Object.keys(req.body)[0];
const basePath = "public/locales";
const lgPath = `${basePath}/${req.params.lng}`;
if (!existsSync(lgPath)) {
mkdirSync(lgPath);
}
const filePath = `${lgPath}/common.json`;
let missingTrans;
try {
missingTrans = JSON.parse(readFileSync(filePath, 'utf8'));
} catch(err) {
missingTrans = JSON.parse("{}");
}
let current = missingTrans
const splitPath = keyPath.split(".");
for (let i=0; i < splitPath.length; i++) {
const key = splitPath[i];
if (! current.hasOwnProperty(key)) {
if (i + 1 == splitPath.length) {
current[key] = "No translation";
} else {
current[key] = JSON.parse("{}");
}
}
current = current[key];
}
writeFileSync(filePath, JSON.stringify(missingTrans, null, 2))
res.send("OK");
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

View File

@@ -1,13 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}