Compare commits
84 Commits
e01430f60e
...
fix/firm_i
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ff15cdef4 | |||
| 72b6f26ebc | |||
| b06ce4eefd | |||
| 49317e905b | |||
| 189c896e60 | |||
| 4ae0b321b9 | |||
| 5a3b87e82c | |||
| 0731ac3b6e | |||
| 77fa4cde35 | |||
| 9aac1d3e34 | |||
| 7aced9477c | |||
| 5a0327c930 | |||
| 18e4fcea28 | |||
| 239ab2e241 | |||
| 9fcead8c95 | |||
| 73871ae04e | |||
| c35c63b421 | |||
| 9fd201c10a | |||
| cb81e233a5 | |||
| 40648c3fdf | |||
| 6248248f0e | |||
| 7bbd607376 | |||
| 4f5d5425fc | |||
| d48edbbf5f | |||
| 0d337849c7 | |||
| 717a0ed830 | |||
| 990e7fa226 | |||
| 5a8050145d | |||
| 1cc6e1e85d | |||
| 765c0749bb | |||
| 5080e5fdde | |||
| 2fed7fa4e7 | |||
| 0613efa846 | |||
| c8466c557d | |||
| 0d7cad945c | |||
| ea5093f2c2 | |||
| b542fd40a6 | |||
| a9e9f97c14 | |||
| 90a46ada2d | |||
| 4f0d943e04 | |||
| 04ff66f187 | |||
| d28092874f | |||
| d0e720f469 | |||
| 3dc91b329f | |||
| 2f2c5a035d | |||
| 32ce981d40 | |||
| e7a4389fde | |||
| f03f8374c8 | |||
| 78ffcb9b71 | |||
| 0a657dca4b | |||
| 8941d69ba4 | |||
| b8d9e8e804 | |||
| 4bf414112a | |||
| 6cc99812d2 | |||
| 2b88e46ca6 | |||
| 2c23992e52 | |||
| ba46c10449 | |||
| f878fa7886 | |||
| ef43369425 | |||
| 37193d2246 | |||
| 87f9119b0b | |||
| f9b6aae927 | |||
| 6683d60be5 | |||
| 3942c54ad9 | |||
| 0a22bc1b8f | |||
| 237f8d5742 | |||
| 8d72172e0a | |||
| 90aa5e06f2 | |||
| 178f27cfe2 | |||
| f4c6cdab3b | |||
| 40e20d0e64 | |||
| 3a5a299b53 | |||
| 76143a9c2f | |||
| 1ba9a66c8e | |||
| 14aea2a475 | |||
| d38bb7d986 | |||
| f71dccf166 | |||
| cc73fc4af2 | |||
| 76a5c0b454 | |||
| 2b7a92097c | |||
| c9f8c69e42 | |||
| bc41823dc3 | |||
| 6c2047033b | |||
| 6c3f6c8d03 |
@@ -4,9 +4,11 @@ from enum import Enum
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from pydantic.json_schema import SkipJsonSchema
|
||||||
|
|
||||||
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry
|
from firm.core.models import CrudDocument, RichtextSingleline, RichtextMultiline, DictionaryEntry, ForeignKey, \
|
||||||
|
CrudDocumentConfig
|
||||||
from firm.core.filter import Filter, FilterSchema
|
from firm.core.filter import Filter, FilterSchema
|
||||||
from firm.entity.models import Entity
|
from firm.entity.models import Entity
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ class ContractStatus(str, Enum):
|
|||||||
signed = 'signed'
|
signed = 'signed'
|
||||||
printed = 'printed'
|
printed = 'printed'
|
||||||
executed = 'executed'
|
executed = 'executed'
|
||||||
|
canceled = 'canceled'
|
||||||
|
|
||||||
|
|
||||||
class ContractDraftStatus(str, Enum):
|
class ContractDraftStatus(str, Enum):
|
||||||
@@ -25,30 +28,13 @@ class ContractDraftStatus(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class DraftParty(BaseModel):
|
class DraftParty(BaseModel):
|
||||||
entity_id: PydanticObjectId = Field(
|
model_config = ConfigDict(title='Partie')
|
||||||
foreignKey={
|
|
||||||
"reference": {
|
|
||||||
"resource": "entities",
|
|
||||||
"schema": "Entity",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
default="",
|
|
||||||
title="Partie"
|
|
||||||
)
|
|
||||||
part: str = Field(title="Rôle")
|
|
||||||
representative_id: PydanticObjectId = Field(
|
|
||||||
foreignKey={
|
|
||||||
"reference": {
|
|
||||||
"resource": "entities",
|
|
||||||
"schema": "Entity",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
default="",
|
|
||||||
title="Représentant"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Config:
|
entity_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Partie")
|
||||||
title = 'Partie'
|
part: str = Field(title="Rôle")
|
||||||
|
representative_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Représentant")
|
||||||
|
|
||||||
|
entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, )
|
||||||
|
|
||||||
|
|
||||||
class Party(BaseModel):
|
class Party(BaseModel):
|
||||||
@@ -64,38 +50,31 @@ class ContractProvisionType(Enum):
|
|||||||
template = 'template'
|
template = 'template'
|
||||||
|
|
||||||
class ProvisionGenuine(BaseModel):
|
class ProvisionGenuine(BaseModel):
|
||||||
|
model_config = ConfigDict(title='Clause personalisée')
|
||||||
|
|
||||||
type: Literal['genuine'] = ContractProvisionType.genuine
|
type: Literal['genuine'] = ContractProvisionType.genuine
|
||||||
title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
|
title: str = RichtextSingleline(props={"parametrized": True}, default="", title="Titre")
|
||||||
body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
|
body: str = RichtextMultiline(props={"parametrized": True}, default="", title="Corps")
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = 'Clause personalisée'
|
|
||||||
|
|
||||||
|
|
||||||
class ContractProvisionTemplateReference(BaseModel):
|
class ContractProvisionTemplateReference(BaseModel):
|
||||||
|
model_config = ConfigDict(title='Template de clause')
|
||||||
|
|
||||||
type: Literal['template'] = ContractProvisionType.template
|
type: Literal['template'] = ContractProvisionType.template
|
||||||
provision_template_id: PydanticObjectId = Field(
|
provision_template_id: PydanticObjectId = ForeignKey(
|
||||||
foreignKey={
|
"templates/provisions",
|
||||||
"reference": {
|
"ProvisionTemplate",
|
||||||
"resource": "templates/provisions",
|
displayed_fields=['title', 'body'],
|
||||||
"schema": "ProvisionTemplate",
|
|
||||||
"displayedFields": ['title', 'body']
|
|
||||||
},
|
|
||||||
},
|
|
||||||
props={"parametrized": True},
|
props={"parametrized": True},
|
||||||
default="",
|
default=None,
|
||||||
title="Template de clause"
|
title="Template de clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = 'Template de clause'
|
|
||||||
|
|
||||||
|
|
||||||
class DraftProvision(BaseModel):
|
class DraftProvision(BaseModel):
|
||||||
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
|
model_config = ConfigDict(title='Clause')
|
||||||
|
|
||||||
class Config:
|
provision: ContractProvisionTemplateReference | ProvisionGenuine = Field(..., discriminator='type')
|
||||||
title = 'Clause'
|
|
||||||
|
|
||||||
|
|
||||||
class Provision(BaseModel):
|
class Provision(BaseModel):
|
||||||
@@ -110,34 +89,16 @@ class ContractDraft(CrudDocument):
|
|||||||
"""
|
"""
|
||||||
Brouillon de contrat à remplir
|
Brouillon de contrat à remplir
|
||||||
"""
|
"""
|
||||||
|
model_config = ConfigDict(title='Brouillon de contrat')
|
||||||
|
|
||||||
name: str = Field(title="Nom")
|
name: str = Field(title="Nom")
|
||||||
title: str = Field(title="Titre")
|
title: str = Field(title="Titre")
|
||||||
parties: List[DraftParty] = Field(title="Parties")
|
parties: List[DraftParty] = Field(title="Parties")
|
||||||
provisions: List[DraftProvision] = Field(
|
provisions: List[DraftProvision] = Field(title='Clauses')
|
||||||
props={"items-per-row": "1", "numbered": True},
|
variables: List[DictionaryEntry] = Field(default=[], title='Variables')
|
||||||
title='Clauses'
|
|
||||||
)
|
|
||||||
variables: List[DictionaryEntry] = Field(
|
|
||||||
default=[],
|
|
||||||
format="dictionary",
|
|
||||||
title='Variables'
|
|
||||||
)
|
|
||||||
status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
|
status: ContractDraftStatus = Field(default=ContractDraftStatus.in_progress, title="Statut")
|
||||||
todo: List[str] = Field(default=[], title="Reste à faire")
|
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):
|
async def check_is_ready(self, db):
|
||||||
if self.status == ContractDraftStatus.published:
|
if self.status == ContractDraftStatus.published:
|
||||||
return
|
return
|
||||||
@@ -173,16 +134,24 @@ class ContractDraft(CrudDocument):
|
|||||||
update = ContractDraftUpdateStatus(status=status)
|
update = ContractDraftUpdateStatus(status=status)
|
||||||
await self.update(db, self, update)
|
await self.update(db, self, update)
|
||||||
|
|
||||||
|
def compute_label(self) -> str:
|
||||||
|
return f"{self.name} - {self.title}"
|
||||||
|
|
||||||
class Contract(CrudDocument):
|
class Contract(CrudDocument):
|
||||||
"""
|
"""
|
||||||
Contrat publié. Les contrats ne peuvent pas être modifiés.
|
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
|
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")
|
name: str = Field(title="Nom")
|
||||||
title: str = Field(title="Titre")
|
title: str = Field(title="Titre")
|
||||||
parties: List[Party] = Field(title="Parties")
|
parties: List[Party] = Field(title="Parties")
|
||||||
provisions: List[Provision] = Field(
|
provisions: List[Provision] = Field(
|
||||||
props={"items-per-row": "1", "numbered": True},
|
props={"items_per_row": "1", "numbered": True},
|
||||||
title='Clauses'
|
title='Clauses'
|
||||||
)
|
)
|
||||||
status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
|
status: ContractStatus = Field(default=ContractStatus.published, title="Statut")
|
||||||
@@ -198,15 +167,6 @@ class Contract(CrudDocument):
|
|||||||
contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}"
|
contract_label = f"{contract_label} - {self.date.strftime('%m/%d/%Y')}"
|
||||||
return contract_label
|
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
|
@classmethod
|
||||||
async def find_by_signature_id(cls, db, signature_id: UUID):
|
async def find_by_signature_id(cls, db, signature_id: UUID):
|
||||||
request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}}
|
request = {'parties': {"$elemMatch": {"signature_uuid": str(signature_id) }}}
|
||||||
@@ -250,15 +210,15 @@ def replace_variables_in_value(variables, value: str):
|
|||||||
|
|
||||||
|
|
||||||
class ContractDraftFilters(FilterSchema):
|
class ContractDraftFilters(FilterSchema):
|
||||||
status: Optional[str] = None
|
status__in: Optional[list[str]] = None
|
||||||
|
|
||||||
class Constants(Filter.Constants):
|
class Constants(Filter.Constants):
|
||||||
model = ContractDraft
|
model = ContractDraft
|
||||||
search_model_fields = ["label", "status"]
|
search_model_fields = ["label"]
|
||||||
|
|
||||||
class ContractFilters(FilterSchema):
|
class ContractFilters(FilterSchema):
|
||||||
status: Optional[str] = None
|
status__in: Optional[list[str]] = None
|
||||||
|
|
||||||
class Constants(Filter.Constants):
|
class Constants(Filter.Constants):
|
||||||
model = Contract
|
model = Contract
|
||||||
search_model_fields = ["label", "status"]
|
search_model_fields = ["label"]
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ async def create(schema: ContractCreate, reg=Depends(get_authed_tenant_registry)
|
|||||||
contract_dict = schema.model_dump()
|
contract_dict = schema.model_dump()
|
||||||
del(contract_dict['draft_id'])
|
del(contract_dict['draft_id'])
|
||||||
|
|
||||||
lawyer = await Entity.get(reg.db, reg.user.entity_id)
|
contract_dict['lawyer'] = reg.partner.model_dump()
|
||||||
contract_dict['lawyer'] = lawyer.model_dump()
|
|
||||||
|
|
||||||
contract_dict['name'] = draft.name
|
contract_dict['name'] = draft.name
|
||||||
contract_dict['title'] = draft.title
|
contract_dict['title'] = draft.title
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from beanie import PydanticObjectId
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
|
||||||
from firm.contract.models import ContractDraft, DraftProvision, DraftParty, Contract
|
from firm.contract.models import ContractDraft, DraftProvision, DraftParty, Contract
|
||||||
|
|
||||||
from firm.entity.models import Entity
|
from firm.entity.models import Entity
|
||||||
from firm.core.schemas import Writer, Reader
|
from firm.core.schemas import Writer, Reader
|
||||||
from firm.core.models import DictionaryEntry
|
from firm.core.models import DictionaryEntry, ForeignKey
|
||||||
|
|
||||||
|
|
||||||
class ContractDraftRead(Reader, ContractDraft):
|
class ContractDraftRead(Reader, ContractDraft):
|
||||||
@@ -19,12 +20,12 @@ class ContractDraftCreate(Writer):
|
|||||||
title: str = Field(title='Titre')
|
title: str = Field(title='Titre')
|
||||||
parties: List[DraftParty] = Field(title='Parties')
|
parties: List[DraftParty] = Field(title='Parties')
|
||||||
provisions: List[DraftProvision] = Field(
|
provisions: List[DraftProvision] = Field(
|
||||||
props={"items-per-row": "1", "numbered": True},
|
props={"items_per_row": "1", "numbered": True},
|
||||||
title='Clauses'
|
title='Clauses'
|
||||||
)
|
)
|
||||||
variables: List[DictionaryEntry] = Field(
|
variables: List[DictionaryEntry] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
format="dictionary",
|
props={"display": "dictionary"},
|
||||||
title='Variables'
|
title='Variables'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,34 +42,34 @@ class ContractDraftUpdate(ContractDraftCreate):
|
|||||||
|
|
||||||
|
|
||||||
class ForeignEntityRead(BaseModel):
|
class ForeignEntityRead(BaseModel):
|
||||||
label: str
|
model_config = ConfigDict(title='Avocat')
|
||||||
|
|
||||||
class Config:
|
label: str
|
||||||
title = "Avocat"
|
|
||||||
|
|
||||||
|
|
||||||
class PartyRead(BaseModel):
|
class PartyRead(BaseModel):
|
||||||
|
model_config = ConfigDict(title='Partie')
|
||||||
|
|
||||||
signature_affixed: bool = Field(title='Signature apposée?')
|
signature_affixed: bool = Field(title='Signature apposée?')
|
||||||
signature_uuid: str = Field(format="signature-link", title="Lien vers signature")
|
signature_uuid: str = Field(props={"display": "signature-link"}, title="Lien vers signature")
|
||||||
part: str = Field(title='Rôle')
|
part: str = Field(title='Rôle')
|
||||||
entity: ForeignEntityRead = Field(title='Client')
|
entity: ForeignEntityRead = Field(title='Client')
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = "Partie"
|
|
||||||
|
|
||||||
|
|
||||||
class ContractRead(Reader, Contract):
|
class ContractRead(Reader, Contract):
|
||||||
parties: List[PartyRead]
|
model_config = ConfigDict(title='Contrat')
|
||||||
lawyer: ForeignEntityRead
|
|
||||||
|
|
||||||
class Config:
|
parties: List[PartyRead] = Field(
|
||||||
title = "Contrat"
|
props={"items_per_row": "2"},
|
||||||
|
title='Parties'
|
||||||
|
)
|
||||||
|
lawyer: ForeignEntityRead
|
||||||
|
|
||||||
|
|
||||||
class ContractCreate(Writer):
|
class ContractCreate(Writer):
|
||||||
date: datetime.date
|
date: datetime.date
|
||||||
location: str
|
location: str
|
||||||
draft_id: str
|
draft_id: PydanticObjectId = ForeignKey(resource="contracts/drafts", schema="ContractDraft")
|
||||||
|
|
||||||
class ContractInit(BaseModel):
|
class ContractInit(BaseModel):
|
||||||
date: datetime.date
|
date: datetime.date
|
||||||
|
|||||||
@@ -1,51 +1,70 @@
|
|||||||
from fastapi import HTTPException, Depends
|
from fastapi import HTTPException, Depends
|
||||||
|
|
||||||
from hub.auth import get_current_user
|
from hub.auth import get_current_user
|
||||||
|
|
||||||
|
from firm.current_firm import CurrentFirm, Partner
|
||||||
from firm.db import get_db_client
|
from firm.db import get_db_client
|
||||||
from firm.current_firm import CurrentFirmModel
|
from firm.entity.models import Entity
|
||||||
|
|
||||||
|
|
||||||
class Registry:
|
class Registry:
|
||||||
user = None
|
user = None
|
||||||
|
partner = None
|
||||||
|
|
||||||
def __init__(self, db_client, instance, firm):
|
def __init__(self, db_client, instance, firm):
|
||||||
self.db = db_client[f"tenant_{instance}_{firm}"]
|
self.db = db_client[f"tenant_{instance}_{firm}"]
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.firm = firm
|
self.firm = firm
|
||||||
|
|
||||||
self.current_firm = CurrentFirmModel.get_current(self.db)
|
self.current_firm = CurrentFirm.get_current(self.db)
|
||||||
|
|
||||||
def set_user(self, user):
|
def check_user(self, user):
|
||||||
for firm in user.firms:
|
for firm in user.firms:
|
||||||
if firm.instance == self.instance and firm.firm == self.firm:
|
if firm.instance == self.instance and firm.firm == self.firm:
|
||||||
self.user = user
|
return True
|
||||||
self.db.user = user
|
raise PermissionError
|
||||||
return
|
|
||||||
|
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
|
||||||
|
|
||||||
raise PermissionError
|
raise PermissionError
|
||||||
|
|
||||||
async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client)) -> Registry:
|
async def get_tenant_registry(instance: str, firm: str, db_client=Depends(get_db_client)) -> Registry:
|
||||||
registry = Registry(db_client, instance, firm)
|
registry = Registry(db_client, instance, firm)
|
||||||
if await registry.current_firm is None:
|
if await registry.current_firm is None:
|
||||||
raise HTTPException(status_code=405, detail=f"Firm needs to be initialized first")
|
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
|
||||||
|
|
||||||
return registry
|
return registry
|
||||||
|
|
||||||
def get_authed_tenant_registry(registry=Depends(get_tenant_registry), user=Depends(get_current_user)) -> 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)
|
||||||
try:
|
try:
|
||||||
registry.set_user(user)
|
registry.check_user(user)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
|
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
|
return registry
|
||||||
|
|
||||||
async def get_uninitialized_registry(instance: str, firm: str, db_client=Depends(get_db_client), user=Depends(get_current_user)) -> 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)
|
registry = Registry(db_client, instance, firm)
|
||||||
if await registry.current_firm is not None:
|
|
||||||
raise HTTPException(status_code=409, detail="Firm configuration already exists")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
registry.set_user(user)
|
registry.check_user(user)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
raise HTTPException(status_code=404, detail="This firm doesn't exist or you are not allowed to access it.")
|
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)
|
||||||
return registry
|
return registry
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
from collections import defaultdict
|
||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from typing import Any, Optional, Union
|
|
||||||
|
|
||||||
from pydantic import ValidationInfo, field_validator
|
from pydantic import ValidationInfo, field_validator
|
||||||
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from fastapi_filter.base.filter import BaseFilterModel
|
from fastapi_filter.base.filter import BaseFilterModel
|
||||||
|
|
||||||
@@ -24,28 +24,6 @@ _odm_operator_transformer: dict[str, Callable[[Optional[str]], Optional[dict[str
|
|||||||
|
|
||||||
|
|
||||||
class Filter(BaseFilterModel):
|
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):
|
def sort(self):
|
||||||
if not self.ordering_values:
|
if not self.ordering_values:
|
||||||
return None
|
return None
|
||||||
@@ -130,6 +108,58 @@ class Filter(BaseFilterModel):
|
|||||||
query[field_name] = value
|
query[field_name] = value
|
||||||
return query
|
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):
|
class FilterSchema(Filter):
|
||||||
label__ilike: Optional[str] = None
|
label__ilike: Optional[str] = None
|
||||||
|
search: Optional[str] = None
|
||||||
order_by: Optional[list[str]] = None
|
order_by: Optional[list[str]] = None
|
||||||
|
created_at__lte: Optional[str] = None
|
||||||
|
created_at__gte: Optional[str] = None
|
||||||
|
created_by__in: Optional[list[str]] = None
|
||||||
|
updated_at__lte: Optional[str] = None
|
||||||
|
updated_at__gte: Optional[str] = None
|
||||||
|
updated_by__in: Optional[list[str]] = None
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
from typing import Optional
|
from typing import Optional, TypedDict, ClassVar
|
||||||
|
|
||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
from motor.motor_asyncio import AsyncIOMotorCollection
|
from motor.motor_asyncio import AsyncIOMotorCollection
|
||||||
from pydantic import BaseModel, Field, computed_field
|
from pydantic import BaseModel, Field, computed_field
|
||||||
|
|
||||||
|
class CrudDocumentConfig(TypedDict, total=False):
|
||||||
|
fulltext_search: list[str]
|
||||||
|
indexes: list[str]
|
||||||
|
|
||||||
|
|
||||||
class CrudDocument(BaseModel):
|
class CrudDocument(BaseModel):
|
||||||
|
document_config: ClassVar[CrudDocumentConfig] = CrudDocumentConfig()
|
||||||
|
|
||||||
id: Optional[PydanticObjectId] = Field(default=None)
|
id: Optional[PydanticObjectId] = Field(default=None)
|
||||||
created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
|
created_at: datetime = Field(default=datetime.now(UTC), nullable=False, title="Créé le")
|
||||||
created_by: Optional[PydanticObjectId] = Field(default=None, title="Créé par")
|
created_by: Optional[PydanticObjectId] = Field(default=None, title="Créé par")
|
||||||
@@ -17,16 +23,13 @@ class CrudDocument(BaseModel):
|
|||||||
def _id(self):
|
def _id(self):
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
@computed_field
|
@computed_field(title="Label")
|
||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
return self.compute_label()
|
return self.compute_label()
|
||||||
|
|
||||||
def compute_label(self) -> str:
|
def compute_label(self) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
class Settings:
|
|
||||||
fulltext_search = []
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _collection_name(cls):
|
def _collection_name(cls):
|
||||||
return cls.__name__
|
return cls.__name__
|
||||||
@@ -35,9 +38,13 @@ class CrudDocument(BaseModel):
|
|||||||
def _get_collection(cls, db) -> AsyncIOMotorCollection:
|
def _get_collection(cls, db) -> AsyncIOMotorCollection:
|
||||||
return db.get_collection(cls._collection_name())
|
return db.get_collection(cls._collection_name())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_index(cls, db, index):
|
||||||
|
cls._get_collection(db).create_index(index)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, db, create_schema):
|
async def create(cls, db, create_schema):
|
||||||
model_dict = create_schema.model_dump() | {"created_by": db.user.id, "updated_by": db.user.id}
|
model_dict = create_schema.model_dump() | {"created_by": db.partner.id, "updated_by": db.partner.id}
|
||||||
document = cls.model_validate(model_dict).model_dump(mode="json")
|
document = cls.model_validate(model_dict).model_dump(mode="json")
|
||||||
result = await cls._get_collection(db).insert_one(document)
|
result = await cls._get_collection(db).insert_one(document)
|
||||||
|
|
||||||
@@ -52,8 +59,13 @@ class CrudDocument(BaseModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list(cls, db):
|
async def list(cls, db, criteria={}):
|
||||||
return cls._get_collection(db).find({})
|
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
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get(cls, db, model_id):
|
async def get(cls, db, model_id):
|
||||||
@@ -66,20 +78,22 @@ class CrudDocument(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def update(cls, db, model, update_schema):
|
async def update(cls, db, model, update_schema):
|
||||||
model_dict = update_schema.model_dump(mode="json") | {"updated_by": db.user.id}
|
model_dict = update_schema.model_dump(mode="json") | {"updated_by": db.partner.id, "updated_at": datetime.now(UTC)}
|
||||||
update_query = {
|
update_query = {
|
||||||
"$set": {field: value for field, value in model_dict.items() if field!= "id" }
|
"$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)
|
await cls._get_collection(db).update_one({"_id": model.id}, update_query)
|
||||||
return await cls.get(db, model.id)
|
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
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete(cls, db, model):
|
async def delete(cls, db, model):
|
||||||
await cls._get_collection(db).delete_one({"_id": model.id})
|
await cls._get_collection(db).delete_one({"_id": model.id})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def text_area(*args, **kwargs):
|
def text_area(*args, **kwargs):
|
||||||
kwargs['widget'] = {
|
kwargs['widget'] = {
|
||||||
"formlyConfig": {
|
"formlyConfig": {
|
||||||
@@ -114,6 +128,20 @@ def RichtextSingleline(*args, **kwargs):
|
|||||||
return Field(*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):
|
class DictionaryEntry(BaseModel):
|
||||||
key: str
|
key: str
|
||||||
value: str = ""
|
value: str = ""
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
|
|
||||||
class Reader(BaseModel):
|
class Reader(BaseModel):
|
||||||
id: Optional[PydanticObjectId] = Field(default=None, validation_alias="_id")
|
id: Optional[PydanticObjectId] = Field(validation_alias="_id")
|
||||||
|
created_by: PydanticObjectId = Field(title="Créé par")
|
||||||
|
updated_by: PydanticObjectId = Field(title="Modifié par")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_model(cls, model):
|
def from_model(cls, model):
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
from typing import Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
from pydantic import Field
|
from pydantic import Field, BaseModel
|
||||||
|
|
||||||
from firm.core.models import CrudDocument
|
from firm.core.models import CrudDocument, CrudDocumentConfig
|
||||||
from firm.core.schemas import Writer, Reader
|
from firm.core.schemas import Writer, Reader
|
||||||
from firm.entity.schemas import EntityIndividualCreate, EntityCorporationCreate, EntityRead
|
from firm.entity.schemas import EntityIndividualCreate, EntityCorporationCreate, EntityRead
|
||||||
|
|
||||||
|
class CurrentFirm(CrudDocument):
|
||||||
class CurrentFirmModel(CrudDocument):
|
|
||||||
instance: str = Field()
|
instance: str = Field()
|
||||||
firm: str = Field()
|
firm: str = Field()
|
||||||
entity_id: PydanticObjectId = Field()
|
entity_id: PydanticObjectId = Field()
|
||||||
@@ -33,17 +32,19 @@ class CurrentFirmModel(CrudDocument):
|
|||||||
return cls.model_validate(document)
|
return cls.model_validate(document)
|
||||||
|
|
||||||
|
|
||||||
class CurrentFirmSchemaRead(Reader):
|
class CurrentFirmSchemaRead(BaseModel):
|
||||||
|
id: Optional[PydanticObjectId]
|
||||||
entity: EntityRead
|
entity: EntityRead
|
||||||
partner: EntityRead
|
partner: EntityRead
|
||||||
|
partner_list: list[EntityRead]
|
||||||
instance: str
|
instance: str
|
||||||
firm: str
|
firm: str
|
||||||
primary_color: str
|
primary_color: str
|
||||||
secondary_color: str
|
secondary_color: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_model_and_entities(cls, model, entity, partner):
|
def from_model_and_entities(cls, model, entity, partner, partner_list):
|
||||||
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner)
|
schema = cls(**model.model_dump(mode="json"), entity=entity, partner=partner, partner_list=partner_list)
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
class CurrentFirmSchemaCreate(Writer):
|
class CurrentFirmSchemaCreate(Writer):
|
||||||
@@ -59,6 +60,10 @@ class CurrentFirmSchemaUpdate(Writer):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class Partner(CrudDocument):
|
class Partner(CrudDocument):
|
||||||
|
document_config = CrudDocumentConfig(
|
||||||
|
indexes=["user_id", "entity_id"],
|
||||||
|
)
|
||||||
|
|
||||||
user_id: PydanticObjectId = Field()
|
user_id: PydanticObjectId = Field()
|
||||||
entity_id: PydanticObjectId = Field()
|
entity_id: PydanticObjectId = Field()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from firm.core.depends import get_authed_tenant_registry, get_uninitialized_registry
|
from firm.core.depends import get_authed_tenant_registry, get_uninitialized_registry
|
||||||
from firm.current_firm import CurrentFirmModel, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
|
from firm.current_firm import CurrentFirm, CurrentFirmSchemaRead, CurrentFirmSchemaCreate, CurrentFirmSchemaUpdate, Partner
|
||||||
from firm.entity.models import Entity, Employee
|
from firm.entity.models import Entity, Employee
|
||||||
from firm.entity.schemas import EntityRead
|
from firm.entity.schemas import EntityRead
|
||||||
|
|
||||||
@@ -9,11 +9,18 @@ current_firm_router = APIRouter()
|
|||||||
|
|
||||||
@current_firm_router.get("/", response_model=CurrentFirmSchemaRead, response_description=f"Current Firm records retrieved")
|
@current_firm_router.get("/", response_model=CurrentFirmSchemaRead, response_description=f"Current Firm records retrieved")
|
||||||
async def read(reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
|
async def read(reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
|
||||||
document = await CurrentFirmModel.get_current(reg.db)
|
document = await CurrentFirm.get_current(reg.db)
|
||||||
entity = await Entity.get(reg.db, document.entity_id)
|
firm_entity = await Entity.get(reg.db, document.entity_id)
|
||||||
partner = await Partner.get_by_user_id(reg.db, reg.user.id)
|
partner = await Partner.get_by_user_id(reg.db, reg.user.id)
|
||||||
partner = await Entity.get(reg.db, partner.entity_id)
|
partner = await Entity.get(reg.db, partner.entity_id)
|
||||||
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(entity), EntityRead.from_model(partner))
|
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]
|
||||||
|
)
|
||||||
|
|
||||||
@current_firm_router.post("/", response_description=f"Current Firm added to the database")
|
@current_firm_router.post("/", response_description=f"Current Firm added to the database")
|
||||||
async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_registry)) -> CurrentFirmSchemaRead:
|
async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_registry)) -> CurrentFirmSchemaRead:
|
||||||
@@ -24,17 +31,25 @@ async def create(schema: CurrentFirmSchemaCreate, reg=Depends(get_uninitialized_
|
|||||||
corporation_schema.entity_data.employees.append(Employee(entity_id=owner_entity.id, position=schema.position))
|
corporation_schema.entity_data.employees.append(Employee(entity_id=owner_entity.id, position=schema.position))
|
||||||
corp = await Entity.create(reg.db, corporation_schema)
|
corp = await Entity.create(reg.db, corporation_schema)
|
||||||
|
|
||||||
document = await CurrentFirmModel.create(reg.db, CurrentFirmModel(
|
firm = await CurrentFirm.create(reg.db, CurrentFirm(
|
||||||
instance=reg.instance,
|
instance=reg.instance,
|
||||||
firm=reg.firm,
|
firm=reg.firm,
|
||||||
entity_id=corp.id,
|
entity_id=corp.id,
|
||||||
primary_color=schema.primary_color,
|
primary_color=schema.primary_color,
|
||||||
secondary_color=schema.secondary_color,
|
secondary_color=schema.secondary_color,
|
||||||
))
|
))
|
||||||
return CurrentFirmSchemaRead.from_model_and_entities(document, EntityRead.from_model(corp), EntityRead.from_model(owner_entity))
|
|
||||||
|
await Partner.create(Partner(user_id=reg.user.id, entity_id=owner_entity.id))
|
||||||
|
|
||||||
|
return CurrentFirmSchemaRead.from_model_and_entities(
|
||||||
|
firm,
|
||||||
|
EntityRead.from_model(corp),
|
||||||
|
EntityRead.from_model(owner_entity),
|
||||||
|
[EntityRead.from_model(owner_entity)]
|
||||||
|
)
|
||||||
|
|
||||||
@current_firm_router.put("/", response_description=f"Current Firm record updated")
|
@current_firm_router.put("/", response_description=f"Current Firm record updated")
|
||||||
async def update(schema: CurrentFirmSchemaUpdate, reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
|
async def update(schema: CurrentFirmSchemaUpdate, reg=Depends(get_authed_tenant_registry)) -> CurrentFirmSchemaRead:
|
||||||
document = await CurrentFirmModel.get_current(reg.db)
|
document = await CurrentFirm.get_current(reg.db)
|
||||||
document = await CurrentFirmModel.update(reg.db, document, schema)
|
document = await CurrentFirm.update(reg.db, document, schema)
|
||||||
return CurrentFirmSchemaRead.from_model(document)
|
return CurrentFirmSchemaRead.from_model(document)
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ client = motor.motor_asyncio.AsyncIOMotorClient(
|
|||||||
DATABASE_URL, uuidRepresentation="standard"
|
DATABASE_URL, uuidRepresentation="standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def init_db():
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def stop_db():
|
async def stop_db():
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date
|
||||||
|
from enum import Enum
|
||||||
from typing import List, Literal, Optional
|
from typing import List, Literal, Optional
|
||||||
|
|
||||||
from pydantic import Field, BaseModel
|
from fastapi_filter import FilterDepends, with_prefix
|
||||||
from beanie import Indexed, PydanticObjectId
|
from pydantic import Field, BaseModel, ConfigDict
|
||||||
|
from beanie import PydanticObjectId
|
||||||
|
|
||||||
from firm.core.models import CrudDocument
|
from firm.core.models import CrudDocument, ForeignKey, CrudDocumentConfig
|
||||||
from firm.core.filter import Filter, FilterSchema
|
from firm.core.filter import Filter, FilterSchema
|
||||||
|
|
||||||
|
|
||||||
@@ -13,15 +15,21 @@ class EntityType(BaseModel):
|
|||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
class EntityTypeEnum(str, Enum):
|
||||||
|
individual = 'individual'
|
||||||
|
corporation = 'corporation'
|
||||||
|
institution = 'institution'
|
||||||
|
|
||||||
class Individual(EntityType):
|
class Individual(EntityType):
|
||||||
|
model_config = ConfigDict(title='Particulier')
|
||||||
|
|
||||||
type: Literal['individual'] = 'individual'
|
type: Literal['individual'] = 'individual'
|
||||||
firstname: Indexed(str) = Field(title='Prénom')
|
firstname: str = Field(title='Prénom')
|
||||||
middlename: Indexed(str) = Field(default="", title='Autres prénoms')
|
middlename: str = Field(default="", title='Autres prénoms')
|
||||||
lastname: Indexed(str) = Field(title='Nom de famille')
|
lastname: str = Field(title='Nom de famille')
|
||||||
surnames: List[Indexed(str)] = Field(
|
surnames: List[str] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
props={"items-per-row": "4", "numbered": True},
|
props={"items_per_row": "4", "numbered": True},
|
||||||
title="Surnoms"
|
title="Surnoms"
|
||||||
)
|
)
|
||||||
day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
|
day_of_birth: Optional[date] = Field(default=None, title='Date de naissance')
|
||||||
@@ -30,51 +38,41 @@ class Individual(EntityType):
|
|||||||
@property
|
@property
|
||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
# if len(self.surnames) > 0:
|
# if len(self.surnames) > 0:
|
||||||
# return '{} "{}" {}'.format(self.firstname, self.surnames[0], self.lastname)
|
# return f"{self.firstname} \"{self.surnames[0]}\" {self.lastname}"
|
||||||
return f"{self.firstname} {self.lastname}"
|
return f"{self.firstname} {self.lastname}"
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = 'Particulier'
|
|
||||||
|
|
||||||
|
|
||||||
class Employee(BaseModel):
|
class Employee(BaseModel):
|
||||||
position: Indexed(str) = Field(title='Poste')
|
model_config = ConfigDict(title='Fiche Employé')
|
||||||
entity_id: PydanticObjectId = Field(
|
|
||||||
foreignKey={
|
|
||||||
"reference": {
|
|
||||||
"resource": "entities",
|
|
||||||
"schema": "Entity",
|
|
||||||
"condition": "entity_data.type=individual"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title='Employé'
|
|
||||||
)
|
|
||||||
|
|
||||||
class Config:
|
position: str = Field(title='Poste')
|
||||||
title = 'Fiche Employé'
|
entity_id: PydanticObjectId = ForeignKey("entities", "Entity", title='Employé')
|
||||||
|
|
||||||
|
|
||||||
class Corporation(EntityType):
|
class Corporation(EntityType):
|
||||||
type: Literal['corporation'] = 'corporation'
|
model_config = ConfigDict(title='Entreprise')
|
||||||
title: Indexed(str) = Field(title='Dénomination sociale')
|
|
||||||
activity: Indexed(str) = Field(title='Activité')
|
|
||||||
employees: List[Employee] = Field(default=[], title='Employés')
|
|
||||||
|
|
||||||
class Config:
|
type: Literal['corporation'] = 'corporation'
|
||||||
title = 'Entreprise'
|
title: str = Field(title='Dénomination sociale')
|
||||||
|
activity: str = Field(title='Activité')
|
||||||
|
employees: List[Employee] = Field(default=[], title='Employés')
|
||||||
|
|
||||||
|
|
||||||
class Institution(Corporation):
|
class Institution(Corporation):
|
||||||
type: Literal['institution'] = 'institution'
|
model_config = ConfigDict(title='Institution')
|
||||||
|
|
||||||
class Config:
|
type: Literal['institution'] = 'institution'
|
||||||
title = 'Institution'
|
|
||||||
|
|
||||||
|
|
||||||
class Entity(CrudDocument):
|
class Entity(CrudDocument):
|
||||||
"""
|
"""
|
||||||
Fiche d'un client
|
Fiche d'un client
|
||||||
"""
|
"""
|
||||||
|
model_config = ConfigDict(title='Client')
|
||||||
|
document_config = CrudDocumentConfig(
|
||||||
|
indexes=["entity_data.type"],
|
||||||
|
)
|
||||||
|
|
||||||
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
||||||
address: str = Field(default="", title='Adresse')
|
address: str = Field(default="", title='Adresse')
|
||||||
|
|
||||||
@@ -83,20 +81,17 @@ class Entity(CrudDocument):
|
|||||||
return ""
|
return ""
|
||||||
return self.entity_data.label
|
return self.entity_data.label
|
||||||
|
|
||||||
class Settings(CrudDocument.Settings):
|
|
||||||
fulltext_search = ['label']
|
|
||||||
|
|
||||||
bson_encoders = {
|
class EntityDataFilter(Filter):
|
||||||
date: lambda dt: dt if hasattr(dt, 'hour')
|
type__in: Optional[list[str]] = None
|
||||||
else datetime(year=dt.year, month=dt.month, day=dt.day,
|
|
||||||
hour=0, minute=0, second=0)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Config:
|
class Constants(Filter.Constants):
|
||||||
title = 'Client'
|
model = EntityType
|
||||||
|
|
||||||
|
|
||||||
class EntityFilters(FilterSchema):
|
class EntityFilters(FilterSchema):
|
||||||
|
entity_data: Optional[EntityDataFilter] = FilterDepends(with_prefix("entity_data", EntityDataFilter))
|
||||||
|
|
||||||
class Constants(Filter.Constants):
|
class Constants(Filter.Constants):
|
||||||
model = Entity
|
model = Entity
|
||||||
search_model_fields = ["label"]
|
search_model_fields = ["label"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from pydantic import Field
|
from pydantic import Field, ConfigDict
|
||||||
|
|
||||||
from firm.entity.models import Entity, Institution, Individual, Corporation
|
from firm.entity.models import Entity, Institution, Individual, Corporation
|
||||||
from firm.core.schemas import Writer, Reader
|
from firm.core.schemas import Writer, Reader
|
||||||
@@ -7,12 +7,11 @@ class EntityRead(Reader, Entity):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class EntityCreate(Writer):
|
class EntityCreate(Writer):
|
||||||
|
model_config = ConfigDict(title="Création d'un client")
|
||||||
|
|
||||||
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
entity_data: Individual | Corporation | Institution = Field(..., discriminator='type')
|
||||||
address: str = Field(default="", title='Adresse')
|
address: str = Field(default="", title='Adresse')
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = "Création d'un client"
|
|
||||||
|
|
||||||
class EntityIndividualCreate(EntityCreate):
|
class EntityIndividualCreate(EntityCreate):
|
||||||
entity_data: Individual
|
entity_data: Individual
|
||||||
|
|
||||||
|
|||||||
31
api/rpk-api/firm/init_db.py
Normal file
31
api/rpk-api/firm/init_db.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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()
|
||||||
@@ -1,38 +1,19 @@
|
|||||||
from typing import List
|
from typing import List, Optional
|
||||||
from html import unescape
|
from html import unescape
|
||||||
|
|
||||||
from beanie import PydanticObjectId
|
from beanie import PydanticObjectId
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
|
||||||
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry
|
from firm.core.models import CrudDocument, RichtextMultiline, RichtextSingleline, DictionaryEntry, ForeignKey
|
||||||
from firm.core.filter import Filter, FilterSchema
|
from firm.core.filter import Filter, FilterSchema
|
||||||
|
|
||||||
|
|
||||||
class PartyTemplate(BaseModel):
|
class PartyTemplate(BaseModel):
|
||||||
entity_id: PydanticObjectId = Field(
|
model_config = ConfigDict(title="Partie")
|
||||||
foreignKey={
|
|
||||||
"reference": {
|
|
||||||
"resource": "entities",
|
|
||||||
"schema": "Entity",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
default="",
|
|
||||||
title="Partie"
|
|
||||||
)
|
|
||||||
part: str = Field(title="Rôle")
|
|
||||||
representative_id: PydanticObjectId = Field(
|
|
||||||
foreignKey={
|
|
||||||
"reference": {
|
|
||||||
"resource": "entities",
|
|
||||||
"schema": "Entity",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
default="",
|
|
||||||
title="Représentant"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Config:
|
entity_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Partie")
|
||||||
title = 'Partie'
|
part: str = Field(title="Rôle")
|
||||||
|
representative_id: Optional[PydanticObjectId] = ForeignKey("entities", "Entity", default=None, title="Représentant")
|
||||||
|
|
||||||
|
|
||||||
def remove_html_tags(text):
|
def remove_html_tags(text):
|
||||||
@@ -46,6 +27,7 @@ class ProvisionTemplate(CrudDocument):
|
|||||||
"""
|
"""
|
||||||
Modèle de clause à décliner
|
Modèle de clause à décliner
|
||||||
"""
|
"""
|
||||||
|
model_config = ConfigDict(title="Template de clause")
|
||||||
|
|
||||||
name: str = Field(title="Nom")
|
name: str = Field(title="Nom")
|
||||||
title: str = RichtextSingleline(title="Titre")
|
title: str = RichtextSingleline(title="Titre")
|
||||||
@@ -54,57 +36,34 @@ class ProvisionTemplate(CrudDocument):
|
|||||||
def compute_label(self) -> str:
|
def compute_label(self) -> str:
|
||||||
return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
|
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):
|
class ProvisionTemplateReference(BaseModel):
|
||||||
provision_template_id: PydanticObjectId = Field(
|
model_config = ConfigDict(title="Clause")
|
||||||
foreignKey={
|
|
||||||
"reference": {
|
provision_template_id: PydanticObjectId = ForeignKey(
|
||||||
"resource": "templates/provisions",
|
"templates/provisions",
|
||||||
"schema": "ProvisionTemplate",
|
"ProvisionTemplate",
|
||||||
"displayedFields": ['title', 'body']
|
['title', 'body'],
|
||||||
},
|
|
||||||
},
|
|
||||||
props={"parametrized": True},
|
props={"parametrized": True},
|
||||||
title="Template de clause"
|
title="Template de clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = 'Clause'
|
|
||||||
|
|
||||||
|
|
||||||
class ContractTemplate(CrudDocument):
|
class ContractTemplate(CrudDocument):
|
||||||
"""
|
"""
|
||||||
Modèle de contrat à décliner
|
Modèle de contrat à décliner
|
||||||
"""
|
"""
|
||||||
|
model_config = ConfigDict(title="Template de contrat")
|
||||||
|
|
||||||
name: str = Field(title="Nom")
|
name: str = Field(title="Nom")
|
||||||
title: str = Field(title="Titre")
|
title: str = Field(title="Titre")
|
||||||
parties: List[PartyTemplate] = Field(default=[], title="Parties")
|
parties: List[PartyTemplate] = Field(default=[], title="Parties")
|
||||||
provisions: List[ProvisionTemplateReference] = Field(
|
provisions: List[ProvisionTemplateReference] = Field(default=[], title="Clauses")
|
||||||
default=[],
|
variables: List[DictionaryEntry] = Field(default=[], title="Variables")
|
||||||
props={"items-per-row": "1", "numbered": True},
|
|
||||||
title="Clauses"
|
|
||||||
)
|
|
||||||
variables: List[DictionaryEntry] = Field(
|
|
||||||
default=[],
|
|
||||||
format="dictionary",
|
|
||||||
title="Variables"
|
|
||||||
)
|
|
||||||
|
|
||||||
def compute_label(self) -> str:
|
def compute_label(self) -> str:
|
||||||
return f"{self.name} - \"{unescape(remove_html_tags(self.title))}\""
|
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 ContractTemplateFilters(FilterSchema):
|
||||||
class Constants(Filter.Constants):
|
class Constants(Filter.Constants):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from pydantic import Field
|
from pydantic import Field, ConfigDict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from firm.template.models import ContractTemplate, ProvisionTemplate, PartyTemplate, ProvisionTemplateReference, DictionaryEntry
|
from firm.template.models import ContractTemplate, ProvisionTemplate, PartyTemplate, ProvisionTemplateReference, DictionaryEntry
|
||||||
@@ -11,24 +11,25 @@ class ContractTemplateRead(Reader, ContractTemplate):
|
|||||||
|
|
||||||
|
|
||||||
class ContractTemplateCreate(Writer):
|
class ContractTemplateCreate(Writer):
|
||||||
|
model_config = ConfigDict(title="Template de Contrat")
|
||||||
|
|
||||||
name: str = Field(title="Nom")
|
name: str = Field(title="Nom")
|
||||||
title: str = Field(title="Titre")
|
title: str = Field(title="Titre")
|
||||||
parties: List[PartyTemplate] = Field(default=[], title="Parties")
|
parties: List[PartyTemplate] = Field(
|
||||||
|
default=[],
|
||||||
|
props={"items_per_row": "2"},
|
||||||
|
title="Parties")
|
||||||
provisions: List[ProvisionTemplateReference] = Field(
|
provisions: List[ProvisionTemplateReference] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
props={"items-per-row": "1", "numbered": True},
|
props={"items_per_row": "1", "numbered": True},
|
||||||
title="Clauses"
|
title="Clauses"
|
||||||
)
|
)
|
||||||
variables: List[DictionaryEntry] = Field(
|
variables: List[DictionaryEntry] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
format="dictionary",
|
props={"display": "dictionary", "required": False},
|
||||||
props={"required": False},
|
|
||||||
title="Variables"
|
title="Variables"
|
||||||
)
|
)
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = 'Template de Contrat'
|
|
||||||
|
|
||||||
|
|
||||||
class ContractTemplateUpdate(ContractTemplateCreate):
|
class ContractTemplateUpdate(ContractTemplateCreate):
|
||||||
pass
|
pass
|
||||||
@@ -39,13 +40,12 @@ class ProvisionTemplateRead(Reader, ProvisionTemplate):
|
|||||||
|
|
||||||
|
|
||||||
class ProvisionTemplateCreate(Writer):
|
class ProvisionTemplateCreate(Writer):
|
||||||
|
model_config = ConfigDict(title="Template de Clause")
|
||||||
|
|
||||||
name: str = Field(title="Nom")
|
name: str = Field(title="Nom")
|
||||||
title: str = RichtextSingleline(title="Titre")
|
title: str = RichtextSingleline(title="Titre")
|
||||||
body: str = RichtextMultiline(title="Corps")
|
body: str = RichtextMultiline(title="Corps")
|
||||||
|
|
||||||
class Config:
|
|
||||||
title = 'Template de Clause'
|
|
||||||
|
|
||||||
|
|
||||||
class ProvisionTemplateUpdate(ProvisionTemplateCreate):
|
class ProvisionTemplateUpdate(ProvisionTemplateCreate):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class AuthenticationBackendMe(AuthenticationBackend):
|
|||||||
|
|
||||||
class CookieTransportOauth(CookieTransport):
|
class CookieTransportOauth(CookieTransport):
|
||||||
async def get_login_response(self, token: str) -> Response:
|
async def get_login_response(self, token: str) -> Response:
|
||||||
response = RedirectResponse("/auth/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
|
response = RedirectResponse("/login?oauth=success", status_code=status.HTTP_301_MOVED_PERMANENTLY)
|
||||||
return self._set_login_cookie(response, token)
|
return self._set_login_cookie(response, token)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from hub import hub_router
|
|||||||
from hub.db import init_db as hub_init_db
|
from hub.db import init_db as hub_init_db
|
||||||
|
|
||||||
from firm import firm_router
|
from firm import firm_router
|
||||||
from firm.db import init_db as firm_init_db
|
from firm.init_db import init_db as firm_init_db
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ services:
|
|||||||
- "traefik.http.routers.gui.rule=PathPrefix(`/`)"
|
- "traefik.http.routers.gui.rule=PathPrefix(`/`)"
|
||||||
- "traefik.http.services.gui.loadbalancer.server.port=5173"
|
- "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:
|
proxy:
|
||||||
image: traefik:latest
|
image: traefik:latest
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
1701
gui/rpk-gui/package-lock.json
generated
1701
gui/rpk-gui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
"@mui/lab": "^6.0.0-beta.14",
|
"@mui/lab": "^6.0.0-beta.14",
|
||||||
"@mui/material": "^6.1.7",
|
"@mui/material": "^6.1.7",
|
||||||
"@mui/x-data-grid": "^7.22.2",
|
"@mui/x-data-grid": "^7.22.2",
|
||||||
|
"@mui/x-date-pickers": "^8.3.0",
|
||||||
"@refinedev/cli": "^2.16.21",
|
"@refinedev/cli": "^2.16.21",
|
||||||
"@refinedev/core": "^4.47.1",
|
"@refinedev/core": "^4.47.1",
|
||||||
"@refinedev/devtools": "^1.1.32",
|
"@refinedev/devtools": "^1.1.32",
|
||||||
@@ -31,11 +32,17 @@
|
|||||||
"@tiptap/extension-underline": "^2.11.7",
|
"@tiptap/extension-underline": "^2.11.7",
|
||||||
"@tiptap/react": "^2.11.7",
|
"@tiptap/react": "^2.11.7",
|
||||||
"@tiptap/starter-kit": "^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",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mui-tiptap": "^1.18.1",
|
"mui-tiptap": "^1.18.1",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^18.0.0",
|
||||||
|
"react-error-boundary": "^6.0.0",
|
||||||
"react-hook-form": "^7.30.0",
|
"react-hook-form": "^7.30.0",
|
||||||
|
"react-i18next": "^15.5.1",
|
||||||
"react-router": "^7.0.2"
|
"react-router": "^7.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
164
gui/rpk-gui/public/locales/deDE/common.json
Normal file
164
gui/rpk-gui/public/locales/deDE/common.json
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
284
gui/rpk-gui/public/locales/enUS/common.json
Normal file
284
gui/rpk-gui/public/locales/enUS/common.json
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"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": "Don’t 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
284
gui/rpk-gui/public/locales/frFR/common.json
Normal file
284
gui/rpk-gui/public/locales/frFR/common.json
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
import { Authenticated, Refine } from "@refinedev/core";
|
import { Authenticated, I18nProvider, Refine } from "@refinedev/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { RefineSnackbarProvider, useNotificationProvider } from "@refinedev/mui";
|
import { RefineSnackbarProvider, useNotificationProvider } from "@refinedev/mui";
|
||||||
|
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
import GlobalStyles from "@mui/material/GlobalStyles";
|
import GlobalStyles from "@mui/material/GlobalStyles";
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
|
||||||
import HistoryEduIcon from '@mui/icons-material/HistoryEdu';
|
import HistoryEduIcon from '@mui/icons-material/HistoryEdu';
|
||||||
import routerBindings, {
|
import routerBindings, { DocumentTitleHandler, UnsavedChangesNotifier } from "@refinedev/react-router";
|
||||||
CatchAllNavigate,
|
|
||||||
DocumentTitleHandler,
|
|
||||||
UnsavedChangesNotifier,
|
|
||||||
} from "@refinedev/react-router";
|
|
||||||
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
|
import { BrowserRouter, Outlet, Route, Routes } from "react-router";
|
||||||
import { authProvider } from "./providers/auth-provider";
|
import authProvider from "./providers/auth-provider";
|
||||||
import { dataProvider } from "./providers/data-provider";
|
import dataProvider from "./providers/data-provider";
|
||||||
import { ColorModeContextProvider } from "./contexts/color-mode";
|
import { ColorModeContextProvider } from "./contexts/color-mode";
|
||||||
import { Login } from "./components/auth/Login";
|
import { Login } from "./components/auth/Login";
|
||||||
import { Register } from "./components/auth/Register";
|
import { Register } from "./components/auth/Register";
|
||||||
@@ -21,53 +17,63 @@ import { ForgotPassword } from "./components/auth/ForgotPassword";
|
|||||||
import { UpdatePassword } from "./components/auth/UpdatePassword";
|
import { UpdatePassword } from "./components/auth/UpdatePassword";
|
||||||
|
|
||||||
import { Header } from "./components";
|
import { Header } from "./components";
|
||||||
|
import { I18nTheme } from "./components/I18nTheme";
|
||||||
import { HubRoutes } from "./pages/hub";
|
import { HubRoutes } from "./pages/hub";
|
||||||
import { FirmRoutes } from "./pages/firm";
|
import { FirmRoutes } from "./pages/firm";
|
||||||
import rpcTheme from "./theme";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
const i18nProvider: I18nProvider = {
|
||||||
|
translate: (key: string, options?: any) => t(key, options) as string,
|
||||||
|
changeLocale: (lang: string) => i18n.changeLanguage(lang),
|
||||||
|
getLocale: () => i18n.language,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ThemeProvider theme={rpcTheme}>
|
<ColorModeContextProvider>
|
||||||
<ColorModeContextProvider>
|
<CssBaseline />
|
||||||
<CssBaseline />
|
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
|
||||||
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
|
<RefineSnackbarProvider>
|
||||||
<RefineSnackbarProvider>
|
<Refine
|
||||||
<Refine
|
authProvider={authProvider}
|
||||||
authProvider={authProvider}
|
dataProvider={dataProvider}
|
||||||
dataProvider={dataProvider}
|
i18nProvider={i18nProvider}
|
||||||
notificationProvider={useNotificationProvider}
|
notificationProvider={useNotificationProvider}
|
||||||
routerProvider={routerBindings}
|
routerProvider={routerBindings}
|
||||||
options={{
|
options={{
|
||||||
title: {
|
title: {
|
||||||
text: "Roleplay Contracts",
|
text: "Roleplay Contracts",
|
||||||
icon: <HistoryEduIcon />
|
icon: <HistoryEduIcon />
|
||||||
},
|
},
|
||||||
syncWithLocation: true,
|
syncWithLocation: true,
|
||||||
warnWhenUnsavedChanges: true,
|
warnWhenUnsavedChanges: true,
|
||||||
useNewQueryKeys: true,
|
useNewQueryKeys: true,
|
||||||
disableTelemetry: true,
|
disableTelemetry: true,
|
||||||
reactQuery: {
|
reactQuery: {
|
||||||
clientConfig: {
|
clientConfig: {
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
retry: (failureCount, error) => {
|
retry: (failureCount, error) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (error.statusCode >= 400 && error.statusCode <= 499) {
|
const status = error.statusCode ? error.statusCode : error.status
|
||||||
return false
|
if (status >= 400 && status<= 499) {
|
||||||
}
|
return false
|
||||||
return failureCount < 4
|
}
|
||||||
},
|
return failureCount < 4
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
|
>
|
||||||
|
<I18nTheme>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
element={(
|
element={(
|
||||||
<Authenticated key="authenticated-routes" redirectOnFail="/auth/login" fallback={<CatchAllNavigate to="/auth/login"/>}>
|
<Authenticated key="authenticated-routes" fallback={<Login />}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Authenticated>
|
</Authenticated>
|
||||||
)}
|
)}
|
||||||
@@ -75,20 +81,20 @@ function App() {
|
|||||||
<Route path="hub/*" element={<HubRoutes />} />
|
<Route path="hub/*" element={<HubRoutes />} />
|
||||||
<Route path="firm/*" element={<FirmRoutes />} />
|
<Route path="firm/*" element={<FirmRoutes />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="auth/*" element={<Outlet />}>
|
<Route path="*" element={<Outlet />}>
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
<Route path="register" element={<Register />} />
|
<Route path="register" element={<Register />} />
|
||||||
<Route path="forgot-password" element={<ForgotPassword />} />
|
<Route path="forgot-password" element={<ForgotPassword />} />
|
||||||
<Route path="update-password" element={<UpdatePassword />} />
|
<Route path="update-password" element={<UpdatePassword />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route index element={<><Header /><h1>HOME</h1></>} />
|
<Route index element={<><Header /><h1>{t("pages.home.title")}</h1></>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<UnsavedChangesNotifier />
|
<UnsavedChangesNotifier />
|
||||||
<DocumentTitleHandler />
|
<DocumentTitleHandler />
|
||||||
</Refine>
|
</I18nTheme>
|
||||||
</RefineSnackbarProvider>
|
</Refine>
|
||||||
</ColorModeContextProvider>
|
</RefineSnackbarProvider>
|
||||||
</ThemeProvider>
|
</ColorModeContextProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
40
gui/rpk-gui/src/components/Cartouche.tsx
Normal file
40
gui/rpk-gui/src/components/Cartouche.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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")}</>
|
||||||
|
}
|
||||||
24
gui/rpk-gui/src/components/I18nTheme.tsx
Normal file
24
gui/rpk-gui/src/components/I18nTheme.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
|
import { useSearchParams, Navigate } from "react-router";
|
||||||
|
import { useTranslation } from "@refinedev/core";
|
||||||
import { AuthPage } from "@refinedev/mui";
|
import { AuthPage } from "@refinedev/mui";
|
||||||
|
|
||||||
import GoogleIcon from "@mui/icons-material/Google";
|
import GoogleIcon from "@mui/icons-material/Google";
|
||||||
import DiscordIcon from "../../components/DiscordIcon";
|
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 = () => {
|
export const Login = () => {
|
||||||
|
const { translate } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
if (searchParams.get("oauth") == "success") {
|
if (searchParams.get("oauth") == "success") {
|
||||||
const redirect_to = localStorage.getItem("redirect_after_login")
|
const redirect_to = localStorage.getItem("redirect_after_login")
|
||||||
@@ -24,67 +20,15 @@ export const Login = () => {
|
|||||||
rememberMe={false}
|
rememberMe={false}
|
||||||
providers={[{
|
providers={[{
|
||||||
name: "google",
|
name: "google",
|
||||||
label: "Sign in with Google",
|
label: translate("pages.login.oauth.google"),
|
||||||
icon: (<GoogleIcon style={{ fontSize: 24, }} />),
|
icon: (<GoogleIcon style={{ fontSize: 24, }} />),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "discord",
|
name: "discord",
|
||||||
label: "Sign in with Discord",
|
label: translate("pages.login.oauth.discord"),
|
||||||
icon: (<DiscordIcon style={{ fontSize: 24, }} />),
|
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"
|
|
||||||
>
|
|
||||||
Don’t 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>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
import { useLogout } from "@refinedev/core";
|
import { useLogout } from "@refinedev/core";
|
||||||
|
import { useTranslation } from "@refinedev/core";
|
||||||
|
|
||||||
export const Logout = () => {
|
export const Logout = () => {
|
||||||
|
const { translate } = useTranslation();
|
||||||
const { mutate: logout } = useLogout();
|
const { mutate: logout } = useLogout();
|
||||||
|
|
||||||
return <Button onClick={() => logout()} >Logout</Button>;
|
return <Button onClick={() => logout()} >{ translate("buttons.logout") }</Button>;
|
||||||
};
|
};
|
||||||
|
|||||||
41
gui/rpk-gui/src/components/header/I18nPicker.tsx
Normal file
41
gui/rpk-gui/src/components/header/I18nPicker.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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;
|
||||||
@@ -18,6 +18,7 @@ import { FirmContext } from "../../contexts/FirmContext";
|
|||||||
import { Logout } from "../auth/Logout";
|
import { Logout } from "../auth/Logout";
|
||||||
import { IUser } from "../../interfaces";
|
import { IUser } from "../../interfaces";
|
||||||
import MuiLink from "@mui/material/Link";
|
import MuiLink from "@mui/material/Link";
|
||||||
|
import I18nPicker from "./I18nPicker";
|
||||||
|
|
||||||
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
|
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
|
||||||
sticky = true,
|
sticky = true,
|
||||||
@@ -116,8 +117,8 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
|
|||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
open={openUserMenu}
|
open={openUserMenu}
|
||||||
onClose={handleCloseUserMenu}
|
onClose={handleCloseUserMenu}
|
||||||
MenuListProps={{
|
slotProps={{
|
||||||
'aria-labelledby': 'user-menu-button',
|
list:{ 'aria-labelledby': 'user-menu-button' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
|
<MenuItem onClick={handleCloseUserMenu}><Logout /></MenuItem>
|
||||||
@@ -130,9 +131,9 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{!user && (
|
{!user && (
|
||||||
<Link to="/auth/login"><Button>Login</Button></Link>
|
<Link to="/login"><Button>Login</Button></Link>
|
||||||
)}
|
)}
|
||||||
|
<I18nPicker />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import React, { createContext, PropsWithChildren } from 'react';
|
import React, { createContext, PropsWithChildren } from 'react';
|
||||||
import { IFirm } from "../interfaces";
|
import { IFirm } from "../interfaces";
|
||||||
import { useParams } from "react-router";
|
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 = {
|
type FirmContextType = {
|
||||||
currentFirm: IFirm,
|
currentFirm: IFirm,
|
||||||
|
partnerMap?: Map<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FirmContext = createContext<FirmContextType>(
|
export const FirmContext = createContext<FirmContextType>(
|
||||||
@@ -12,13 +17,35 @@ export const FirmContext = createContext<FirmContextType>(
|
|||||||
|
|
||||||
|
|
||||||
export const FirmContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
|
export const FirmContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
|
||||||
const { instance, firm } = useParams<IFirm>()
|
const { instance, firm } = useParams<IFirm>();
|
||||||
|
const { data, isError, error, isLoading } = useOne({resource: 'firm', id: `${instance}/${firm}/`, errorNotification: false});
|
||||||
|
|
||||||
if (instance === undefined || firm === undefined) {
|
if (instance === undefined || firm === undefined) {
|
||||||
return "Error"
|
throw({statusCode: 400});
|
||||||
}
|
}
|
||||||
|
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 (
|
return (
|
||||||
<FirmContext.Provider value={{currentFirm: {instance, firm}}} >
|
<FirmContext.Provider value={value} >
|
||||||
{ children }
|
{ children }
|
||||||
</FirmContext.Provider>
|
</FirmContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
21
gui/rpk-gui/src/i18n.tsx
Normal file
21
gui/rpk-gui/src/i18n.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
import Backend from "i18next-http-backend";
|
||||||
|
import detector from "i18next-browser-languagedetector";
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(Backend)
|
||||||
|
.use(detector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
supportedLngs: ["enUS", "frFR"],
|
||||||
|
backend: {
|
||||||
|
loadPath: "/locales/{{lng}}/{{ns}}.json", // "http/locales/{{lng}}/{{ns}}.json"
|
||||||
|
},
|
||||||
|
//saveMissing: true,
|
||||||
|
ns: ["common"],
|
||||||
|
defaultNS: "common",
|
||||||
|
fallbackLng: ["enUS", "frFR"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -2,12 +2,15 @@ import React from "react";
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import "./i18n";
|
||||||
|
|
||||||
const container = document.getElementById("root") as HTMLElement;
|
const container = document.getElementById("root") as HTMLElement;
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<React.Suspense fallback="loading">
|
||||||
|
<App />
|
||||||
|
</React.Suspense>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
export type IFirm = {
|
export type IFirm = {
|
||||||
instance: string,
|
instance: string,
|
||||||
firm: string
|
firm: string
|
||||||
|
entity?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import Form from "@rjsf/mui";
|
|||||||
import { RegistryFieldsType, RegistryWidgetsType, RJSFSchema, UiSchema } from "@rjsf/utils";
|
import { RegistryFieldsType, RegistryWidgetsType, RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||||
import CrudTextWidget from "./widgets/crud-text-widget";
|
import CrudTextWidget from "./widgets/crud-text-widget";
|
||||||
import UnionEnumField from "./fields/union-enum";
|
import UnionEnumField from "./fields/union-enum";
|
||||||
|
import ArrayFieldTemplate from "./templates/ArrayFieldTemplate"
|
||||||
|
import ArrayFieldItemTemplate from "./templates/ArrayFieldItemTemplate";
|
||||||
import { ResourceContext } from "../contexts/ResourceContext";
|
import { ResourceContext } from "../contexts/ResourceContext";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { ParametersContextProvider } from "../contexts/parameters-context";
|
||||||
|
import CrudArrayField from "./fields/crud-array-field";
|
||||||
|
|
||||||
type BaseFormProps = {
|
type BaseFormProps = {
|
||||||
schema: RJSFSchema,
|
schema: RJSFSchema,
|
||||||
@@ -12,32 +17,43 @@ type BaseFormProps = {
|
|||||||
onChange?: (data: any) => void,
|
onChange?: (data: any) => void,
|
||||||
uiSchema?: UiSchema,
|
uiSchema?: UiSchema,
|
||||||
formData?: any,
|
formData?: any,
|
||||||
|
children?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const customWidgets: RegistryWidgetsType = {
|
export const customWidgets: RegistryWidgetsType = {
|
||||||
TextWidget: CrudTextWidget
|
TextWidget: CrudTextWidget,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const customFields: RegistryFieldsType = {
|
export const customFields: RegistryFieldsType = {
|
||||||
AnyOfField: UnionEnumField
|
AnyOfField: UnionEnumField,
|
||||||
|
ArrayField: CrudArrayField
|
||||||
|
}
|
||||||
|
|
||||||
|
const customTemplates = {
|
||||||
|
ArrayFieldTemplate,
|
||||||
|
ArrayFieldItemTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BaseForm: React.FC<BaseFormProps> = (props) => {
|
export const BaseForm: React.FC<BaseFormProps> = (props) => {
|
||||||
const { schema, uiSchema, resourceBasePath, formData, onSubmit, onChange } = props;
|
const { schema, uiSchema, resourceBasePath, formData, children, onSubmit, onChange } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
|
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
|
||||||
<Form
|
<ParametersContextProvider>
|
||||||
schema={schema}
|
<Form
|
||||||
uiSchema={uiSchema === undefined ? {} : uiSchema}
|
schema={schema}
|
||||||
formData={formData}
|
uiSchema={uiSchema === undefined ? {} : uiSchema}
|
||||||
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
|
formData={formData}
|
||||||
validator={validator}
|
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
|
||||||
omitExtraData={true}
|
validator={validator}
|
||||||
widgets={customWidgets}
|
omitExtraData={true}
|
||||||
fields={customFields}
|
widgets={customWidgets}
|
||||||
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
|
fields={customFields}
|
||||||
/>
|
templates={customTemplates}
|
||||||
|
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
|
||||||
|
children={children}
|
||||||
|
/>
|
||||||
|
</ParametersContextProvider>
|
||||||
</ResourceContext.Provider>
|
</ResourceContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
73
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
73
gui/rpk-gui/src/lib/crud/components/crud-filters.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Accordion, AccordionDetails, AccordionSummary, CircularProgress } from "@mui/material";
|
||||||
|
import FilterForm from "../../filter-form/components/filter-form";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { GridExpandMoreIcon } from "@mui/x-data-grid";
|
||||||
|
import { useResourceFilter } from "../hook";
|
||||||
|
|
||||||
|
export type OnChangeValue = {
|
||||||
|
search: string | null
|
||||||
|
filters: {[filter: string]: {[op: string]: string}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CrudFiltersProps = {
|
||||||
|
resourceName: string
|
||||||
|
resourcePath: string
|
||||||
|
onChange: (value: OnChangeValue) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CrudFilters = (props: CrudFiltersProps) => {
|
||||||
|
const { resourceName, resourcePath, onChange } = props
|
||||||
|
const { hasSearch, filtersSchema, filtersLoading } = useResourceFilter(resourceName, resourcePath)
|
||||||
|
|
||||||
|
if (filtersLoading) {
|
||||||
|
return <CircularProgress />
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentValue = {
|
||||||
|
search: "",
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{hasSearch &&
|
||||||
|
<SearchFilter value="" onChange={(value) => {
|
||||||
|
currentValue.search = value;
|
||||||
|
onChange(currentValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<>Advanced filters<GridExpandMoreIcon /></>}
|
||||||
|
/>
|
||||||
|
<AccordionDetails>
|
||||||
|
<FilterForm fields={filtersSchema} values={{}} onChange={(value) => {
|
||||||
|
currentValue.filters = value;
|
||||||
|
onChange(currentValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CrudFilters;
|
||||||
|
|
||||||
|
type SearchFilter = {
|
||||||
|
value: string
|
||||||
|
onChange: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchFilter = (props: SearchFilter) => {
|
||||||
|
const {value, onChange} = props;
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
label="schemas.search"
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
defaultValue={value}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,62 +1,39 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { ReactNode } from "react";
|
||||||
import { CircularProgress } from "@mui/material";
|
import { CircularProgress } from "@mui/material";
|
||||||
import { useForm } from "@refinedev/core";
|
|
||||||
import { UiSchema } from "@rjsf/utils";
|
import { UiSchema } from "@rjsf/utils";
|
||||||
import { jsonschemaProvider } from "../providers/jsonschema-provider";
|
|
||||||
import { BaseForm } from "./base-form";
|
import { BaseForm } from "./base-form";
|
||||||
|
import { useResourceSchema } from "../hook";
|
||||||
|
|
||||||
type CrudFormProps = {
|
type CrudFormProps = {
|
||||||
schemaName: string,
|
schemaName: string,
|
||||||
uiSchema?: UiSchema,
|
uiSchema?: UiSchema,
|
||||||
resourceBasePath?: string,
|
record?: any,
|
||||||
resource: string,
|
resourceBasePath: string,
|
||||||
id?: string,
|
onSubmit?: (data: any) => void,
|
||||||
onSuccess?: (data: any) => void,
|
defaultValue?: any,
|
||||||
defaultValue?: any
|
children?: ReactNode
|
||||||
|
card?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CrudForm: React.FC<CrudFormProps> = (props) => {
|
export const CrudForm: React.FC<CrudFormProps> = (props) => {
|
||||||
const { schemaName, uiSchema, resourceBasePath="" ,resource, id, onSuccess, defaultValue } = 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 { onFinish, query, formLoading } = useForm({
|
if(schemaLoading) {
|
||||||
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 />
|
return <CircularProgress />
|
||||||
}
|
}
|
||||||
|
|
||||||
const record = query?.data?.data || defaultValue;
|
|
||||||
return (
|
return (
|
||||||
<BaseForm
|
<BaseForm
|
||||||
schema={schema}
|
schema={schema}
|
||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
formData={record}
|
formData={record || defaultValue}
|
||||||
resourceBasePath={resourceBasePath}
|
resourceBasePath={resourceBasePath}
|
||||||
onSubmit={
|
onSubmit={
|
||||||
(data: any) => onFinish(data)
|
(data: any) => onSubmit(data)
|
||||||
}
|
}
|
||||||
|
children={children}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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;
|
||||||
|
|||||||
146
gui/rpk-gui/src/lib/crud/components/fields/crud-array-field.tsx
Normal file
146
gui/rpk-gui/src/lib/crud/components/fields/crud-array-field.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { JSONSchema7Definition } from "json-schema";
|
||||||
|
import {
|
||||||
|
FieldProps,
|
||||||
|
FormContextType,
|
||||||
|
getTemplate, getUiOptions,
|
||||||
|
RJSFSchema,
|
||||||
|
} from "@rjsf/utils";
|
||||||
|
import ArrayField from "@rjsf/core/lib/components/fields/ArrayField";
|
||||||
|
import validator from "@rjsf/validator-ajv8";
|
||||||
|
import Form from "@rjsf/mui";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Box, Paper } from "@mui/material";
|
||||||
|
import { CrudTextRJSFSchema } from "../widgets/crud-text-widget";
|
||||||
|
import { ParametersContext } from "../../contexts/parameters-context";
|
||||||
|
|
||||||
|
export type CrudArrayFieldSchema = RJSFSchema & {
|
||||||
|
props? : any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const CrudArrayField = <T = any, S extends CrudArrayFieldSchema = CrudArrayFieldSchema, F extends FormContextType = any> (props: FieldProps<T[], S, F>)=> {
|
||||||
|
const { schema } = props
|
||||||
|
let isDictionary = false;
|
||||||
|
if (schema.props) {
|
||||||
|
if (schema.props.hasOwnProperty("display")) {
|
||||||
|
if (schema.props.display == "dictionary") {
|
||||||
|
isDictionary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isDictionary) {
|
||||||
|
return <Dictionary {...props} />
|
||||||
|
}
|
||||||
|
return <ArrayField<T,S,F> {...props}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CrudArrayField;
|
||||||
|
|
||||||
|
type DictionaryEntry = {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
};
|
||||||
|
|
||||||
|
const Dictionary = <
|
||||||
|
T = any,
|
||||||
|
S extends CrudTextRJSFSchema = CrudTextRJSFSchema,
|
||||||
|
F extends FormContextType = any
|
||||||
|
>(props: FieldProps<T[], S, F>)=> {
|
||||||
|
const { required, formData, onChange, registry, uiSchema, idSchema, schema } = props;
|
||||||
|
const uiOptions = getUiOptions<T[], S, F>(uiSchema);
|
||||||
|
const { parameters } = useContext(ParametersContext);
|
||||||
|
|
||||||
|
const ArrayFieldDescriptionTemplate = getTemplate<'ArrayFieldDescriptionTemplate', T[], S, F>(
|
||||||
|
'ArrayFieldDescriptionTemplate',
|
||||||
|
registry,
|
||||||
|
uiOptions
|
||||||
|
);
|
||||||
|
const ArrayFieldTitleTemplate = getTemplate<'ArrayFieldTitleTemplate', T[], S, F>(
|
||||||
|
'ArrayFieldTitleTemplate',
|
||||||
|
registry,
|
||||||
|
uiOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
let properties = new Set<string>()
|
||||||
|
for (const field in parameters) {
|
||||||
|
for (const param of parameters[field]) {
|
||||||
|
properties.add(param)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: {[key:string]: string} = {}
|
||||||
|
if (formData !== undefined) {
|
||||||
|
for (const param of formData) {
|
||||||
|
// @ts-ignore
|
||||||
|
data[param.key] = param.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyDict = Object.values(data).length == 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={2}>
|
||||||
|
<Box p={2}>
|
||||||
|
<ArrayFieldTitleTemplate
|
||||||
|
idSchema={idSchema}
|
||||||
|
title={uiOptions.title || schema.title}
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
required={required}
|
||||||
|
registry={registry}
|
||||||
|
/>
|
||||||
|
<ArrayFieldDescriptionTemplate
|
||||||
|
idSchema={idSchema}
|
||||||
|
description={uiOptions.description || schema.description}
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
registry={registry}
|
||||||
|
/>
|
||||||
|
{ emptyDict && <Typography>No variables found</Typography>}
|
||||||
|
{ !emptyDict && (
|
||||||
|
<Form
|
||||||
|
schema={getFormSchema(Array.from(properties.values()), required || false)}
|
||||||
|
tagName="div"
|
||||||
|
formData={data}
|
||||||
|
validator={validator}
|
||||||
|
omitExtraData={true}
|
||||||
|
onChange={(e, id) => {
|
||||||
|
console.log(e)
|
||||||
|
let value: T[] = []
|
||||||
|
for (const prop of properties) {
|
||||||
|
value.push({
|
||||||
|
key: prop,
|
||||||
|
value: e.formData.hasOwnProperty(prop) ? e.formData[prop] : undefined
|
||||||
|
} as T)
|
||||||
|
}
|
||||||
|
onChange(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormSchema(properties: string[], isRequired: boolean) {
|
||||||
|
const schema: JSONSchema7Definition = {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
let required: string[] = []
|
||||||
|
for (const pname of properties) {
|
||||||
|
schema.properties![pname] = {
|
||||||
|
type: "string",
|
||||||
|
title: pname
|
||||||
|
}
|
||||||
|
if (isRequired) {
|
||||||
|
required.push(pname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schema.required = required
|
||||||
|
|
||||||
|
return schema
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { getDefaultRegistry } from "@rjsf/core";
|
import { getDefaultRegistry } from "@rjsf/core";
|
||||||
import { FormContextType, getTemplate, RJSFSchema, WidgetProps } from "@rjsf/utils";
|
import { FormContextType, RJSFSchema, WidgetProps } from "@rjsf/utils";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
import ForeignKeyWidget from "./foreign-key";
|
import ForeignKeyWidget from "./foreign-key";
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import ClearIcon from '@mui/icons-material/Clear';
|
|||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import NoteAddIcon from '@mui/icons-material/NoteAdd';
|
import NoteAddIcon from '@mui/icons-material/NoteAdd';
|
||||||
import React, { useState, useEffect, useContext, Fragment } from "react";
|
import React, { useState, useEffect, useContext, Fragment } from "react";
|
||||||
import { useList, useOne } from "@refinedev/core";
|
import { useForm, useList, useOne } from "@refinedev/core";
|
||||||
import { ResourceContext } from "../../contexts/ResourceContext";
|
import { ResourceContext } from "../../contexts/ResourceContext";
|
||||||
import { CrudForm } from "../crud-form";
|
import { CrudForm } from "../crud-form";
|
||||||
|
import { ParametersContext } from "../../contexts/parameters-context";
|
||||||
|
|
||||||
export type ForeignKeyReference = {
|
export type ForeignKeyReference = {
|
||||||
resource: string,
|
resource: string,
|
||||||
@@ -21,6 +22,7 @@ export type ForeignKeySchema = RJSFSchema & {
|
|||||||
foreignKey?: {
|
foreignKey?: {
|
||||||
reference: ForeignKeyReference
|
reference: ForeignKeyReference
|
||||||
}
|
}
|
||||||
|
props? : any
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
@@ -42,10 +44,10 @@ export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = F
|
|||||||
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
props: WidgetProps<T, S, F>
|
props: WidgetProps<T, S, F>
|
||||||
) => {
|
) => {
|
||||||
if (props.schema.foreignKey === undefined) {
|
const { onChange, label, fieldId, schema } = props;
|
||||||
|
if (schema.foreignKey === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { onChange, label } = props
|
|
||||||
|
|
||||||
const [openFormModal, setOpenFormModal] = useState(false);
|
const [openFormModal, setOpenFormModal] = useState(false);
|
||||||
const [searchString, setSearchString] = useState<string>("");
|
const [searchString, setSearchString] = useState<string>("");
|
||||||
@@ -55,12 +57,19 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
|
|||||||
return () => clearTimeout(handler);
|
return () => clearTimeout(handler);
|
||||||
}, [searchString]);
|
}, [searchString]);
|
||||||
|
|
||||||
const { resource, schema, label: labelField = "label" } = props.schema.foreignKey.reference
|
const { setFieldParameters } = useContext(ParametersContext)
|
||||||
|
useEffect(() => {
|
||||||
|
if (schema.hasOwnProperty("props") && schema.props.hasOwnProperty("parametrized") && schema.props.parametrized) {
|
||||||
|
setFieldParameters(fieldId, [])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { resource, schema: fkSchema, label: labelField = "label" } = schema.foreignKey.reference
|
||||||
const { basePath } = useContext(ResourceContext)
|
const { basePath } = useContext(ResourceContext)
|
||||||
const { data, isLoading } = useList({
|
const { data, isLoading } = useList({
|
||||||
resource: `${basePath}/${resource}`,
|
resource: `${basePath}/${resource}`,
|
||||||
pagination: { current: 1, pageSize: 10, mode: "server" },
|
pagination: { current: 1, pageSize: 10, mode: "server" },
|
||||||
filters: [{ field: "label", operator: "contains", value: debouncedInputValue }],
|
filters: [{ field: "search", operator: "contains", value: debouncedInputValue }],
|
||||||
sorters: [{ field: "label", order: "asc" }],
|
sorters: [{ field: "label", order: "asc" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,14 +112,14 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
|
|||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<FormContainer
|
<FormContainerNew
|
||||||
schemaName={schema}
|
schemaName={fkSchema}
|
||||||
resourceBasePath={basePath}
|
resourceBasePath={basePath}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
uiSchema={{}}
|
uiSchema={{}}
|
||||||
onSuccess={(data: any) => {
|
onSuccess={(data: any) => {
|
||||||
setOpenFormModal(false)
|
setOpenFormModal(false)
|
||||||
onChange(data.data.id);
|
onChange(data.id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -122,28 +131,37 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
|
|||||||
const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
|
||||||
props: WidgetProps<T, S, F> & { onClear: () => void }
|
props: WidgetProps<T, S, F> & { onClear: () => void }
|
||||||
) => {
|
) => {
|
||||||
const { onClear, value } = props;
|
const { onClear, value, schema, id: fieldId } = props;
|
||||||
|
|
||||||
const [openFormModal, setOpenFormModal] = React.useState(false);
|
const [openFormModal, setOpenFormModal] = React.useState(false);
|
||||||
|
|
||||||
if (props.schema.foreignKey === undefined) {
|
if (props.schema.foreignKey === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { resource, schema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
|
const { resource, schema: fkSchema, label: labelField = "label", displayedFields } = props.schema.foreignKey.reference
|
||||||
const { basePath } = useContext(ResourceContext)
|
const { basePath } = useContext(ResourceContext)
|
||||||
|
|
||||||
const { data, isLoading } = useOne({
|
const { data, isLoading, isSuccess } = useOne({
|
||||||
resource: `${basePath}/${resource}`,
|
resource: `${basePath}/${resource}`,
|
||||||
id: value
|
id: value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { setFieldParameters } = useContext(ParametersContext)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSuccess && schema.hasOwnProperty("props") && schema.props.hasOwnProperty("parametrized") && schema.props.parametrized) {
|
||||||
|
const record = data.data;
|
||||||
|
setFieldParameters(fieldId, extractParameters(record))
|
||||||
|
}
|
||||||
|
}, [isSuccess])
|
||||||
|
|
||||||
if (isLoading || data === undefined) {
|
if (isLoading || data === undefined) {
|
||||||
return <CircularProgress />
|
return <CircularProgress />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const record = data.data;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextField label={ props.label } variant="outlined" disabled={true} value={data.data[labelField]}
|
<TextField label={ props.label } variant="outlined" disabled={true} value={record[labelField]}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
@@ -158,7 +176,7 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{ displayedFields && <Preview id={value} basePath={basePath} resource={resource} displayedFields={displayedFields}/>}
|
{ displayedFields && <Preview record={record} displayedFields={displayedFields}/>}
|
||||||
<Modal
|
<Modal
|
||||||
open={openFormModal}
|
open={openFormModal}
|
||||||
onClose={() => setOpenFormModal(false)}
|
onClose={() => setOpenFormModal(false)}
|
||||||
@@ -166,8 +184,8 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
|
|||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<FormContainer
|
<FormContainerEdit
|
||||||
schemaName={schema}
|
schemaName={fkSchema}
|
||||||
resourceBasePath={basePath}
|
resourceBasePath={basePath}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
uiSchema={{}}
|
uiSchema={{}}
|
||||||
@@ -203,26 +221,54 @@ type FormContainerProps = {
|
|||||||
onSuccess: (data: any) => void
|
onSuccess: (data: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormContainer = (props: FormContainerProps) => {
|
const FormContainerEdit = (props: FormContainerProps) => {
|
||||||
const { schemaName, resourceBasePath, resource, uiSchema = {}, id = undefined, onSuccess } = props;
|
const { schemaName, resourceBasePath, resource, uiSchema = {}, id, onSuccess } = props;
|
||||||
|
const { onFinish, query, formLoading } = useForm({
|
||||||
|
resource: `${resourceBasePath}/${resource}`,
|
||||||
|
action: "edit",
|
||||||
|
id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formLoading || query?.data === undefined) {
|
||||||
|
return <CircularProgress />
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Box sx={{ ...modalStyle, width: 800 }}>
|
<Box sx={{ ...modalStyle, width: 800 }}>
|
||||||
<CrudForm schemaName={schemaName} resourceBasePath={resourceBasePath} resource={resource} uiSchema={uiSchema} id={id} onSuccess={(data) => onSuccess(data)} />
|
<CrudForm
|
||||||
|
schemaName={schemaName}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
resourceBasePath={resourceBasePath}
|
||||||
|
record={query.data.data}
|
||||||
|
onSubmit={(data:any) => {
|
||||||
|
onFinish(data);
|
||||||
|
onSuccess(data);
|
||||||
|
}} />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Preview = (props: {id: string, resource: string, basePath: string, displayedFields: [string]}) => {
|
const FormContainerNew = (props: FormContainerProps) => {
|
||||||
const { basePath, resource, id, displayedFields } = props
|
const { schemaName, resourceBasePath, resource, uiSchema = {}, onSuccess } = props;
|
||||||
|
const { onFinish } = useForm({
|
||||||
const { data, isLoading } = useOne({
|
resource: `${resourceBasePath}/${resource}`,
|
||||||
resource: `${basePath}/${resource}`,
|
action: "create",
|
||||||
id
|
onMutationSuccess: data => onSuccess(data.data)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || data === undefined) {
|
return (
|
||||||
return <CircularProgress />
|
<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 (
|
return (
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
@@ -230,10 +276,29 @@ const Preview = (props: {id: string, resource: string, basePath: string, display
|
|||||||
return (
|
return (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<Grid2 size={2}><Container>{field}</Container></Grid2>
|
<Grid2 size={2}><Container>{field}</Container></Grid2>
|
||||||
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: data.data[field] }} ></Container></Grid2>
|
<Grid2 size={9}><Container dangerouslySetInnerHTML={{ __html: record[field] }} ></Container></Grid2>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extractParameters = (obj: any)=> {
|
||||||
|
let result: string[] = [];
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
if (typeof(obj[k]) == "string") {
|
||||||
|
const matches = obj[k].match(/%[^\s.]+%/g);
|
||||||
|
if (matches) {
|
||||||
|
const filtered = matches.map((p: string | any[]) => p.slice(1,-1)) as string[]
|
||||||
|
result = result.concat(filtered);
|
||||||
|
}
|
||||||
|
} else if (typeof(obj[k]) == "object") {
|
||||||
|
if (obj[k]) {
|
||||||
|
result = result.concat(extractParameters(obj[k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ declare module "@tiptap/core" {
|
|||||||
export default Extension.create<IndentOptions>({
|
export default Extension.create<IndentOptions>({
|
||||||
name: "indent",
|
name: "indent",
|
||||||
|
|
||||||
defaultOptions: {
|
addOptions() {
|
||||||
types: ["paragraph", "heading"],
|
return {
|
||||||
margin: 40
|
types: ["paragraph", "heading"],
|
||||||
|
margin: 40
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addGlobalAttributes() {
|
addGlobalAttributes() {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const StyledLabelledOutlined = styled(LabelledOutlined)(({ theme }) => [{
|
|||||||
const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(
|
const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSchema, F extends FormContextType = any>(
|
||||||
props: WidgetProps<T, S, F>
|
props: WidgetProps<T, S, F>
|
||||||
) => {
|
) => {
|
||||||
const { schema, value, onChange, label, id } = props;
|
const { schema, value, onChange, label, id, readonly } = props;
|
||||||
const isMultiline = schema.props.multiline === true;
|
const isMultiline = schema.props.multiline === true;
|
||||||
|
|
||||||
let editorOptions: UseEditorOptions;
|
let editorOptions: UseEditorOptions;
|
||||||
@@ -92,14 +92,15 @@ const RichtextWidget = <T = any, S extends CrudTextRJSFSchema = CrudTextRJSFSche
|
|||||||
<TextContainer>
|
<TextContainer>
|
||||||
<RichTextEditorProvider editor={editor}>
|
<RichTextEditorProvider editor={editor}>
|
||||||
<TableBubbleMenu />
|
<TableBubbleMenu />
|
||||||
<RichTextField
|
{!readonly && <RichTextField
|
||||||
controls={
|
controls={
|
||||||
<MenuControlsContainer>
|
<MenuControlsContainer>
|
||||||
{isMultiline ? multilineButtons : singlelineButtons}
|
{isMultiline ? multilineButtons : singlelineButtons}
|
||||||
</MenuControlsContainer>
|
</MenuControlsContainer>
|
||||||
}
|
}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
/>
|
/>}
|
||||||
|
{readonly && <RichTextField variant="standard" disabled={true}/>}
|
||||||
</RichTextEditorProvider>
|
</RichTextEditorProvider>
|
||||||
</TextContainer>
|
</TextContainer>
|
||||||
<RightContainer> </RightContainer>
|
<RightContainer> </RightContainer>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, PropsWithChildren } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
type ResourceContextType = {
|
type ResourceContextType = {
|
||||||
basePath: string,
|
basePath: string,
|
||||||
|
|||||||
30
gui/rpk-gui/src/lib/crud/contexts/parameters-context.tsx
Normal file
30
gui/rpk-gui/src/lib/crud/contexts/parameters-context.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React, { createContext, PropsWithChildren, useState } from 'react';
|
||||||
|
|
||||||
|
type Parameters = {
|
||||||
|
[field: string]: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParametersContextType = {
|
||||||
|
parameters: Parameters,
|
||||||
|
setFieldParameters: (fieldName: string, parameterList: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ParametersContext = createContext<ParametersContextType>(
|
||||||
|
{} as ParametersContextType
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ParametersContextProvider: React.FC<PropsWithChildren> = ({ children }: PropsWithChildren) => {
|
||||||
|
const [parameters, setParameters] = useState<Parameters>({});
|
||||||
|
|
||||||
|
function setFieldParameters(fieldName: string, parameterList: string[]) {
|
||||||
|
let params = structuredClone(parameters)
|
||||||
|
params[fieldName] = parameterList
|
||||||
|
setParameters(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ParametersContext.Provider value={{ parameters, setFieldParameters }} >
|
||||||
|
{children}
|
||||||
|
</ParametersContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
gui/rpk-gui/src/lib/crud/hook/index.tsx
Normal file
109
gui/rpk-gui/src/lib/crud/hook/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { jsonschemaProvider } from "../providers/jsonschema-provider";
|
||||||
|
import { GridColDef, GridColumnVisibilityModel, GridValidRowModel } from "@mui/x-data-grid";
|
||||||
|
|
||||||
|
type ResourceSchemaType = "create" | "update" | "card";
|
||||||
|
|
||||||
|
export function useResourceSchema(schemaName: string, type: ResourceSchemaType) {
|
||||||
|
const [schema, setSchema] = useState({});
|
||||||
|
const [schemaLoading, setSchemaLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
let resourceSchema
|
||||||
|
if (type == "create") {
|
||||||
|
resourceSchema = await jsonschemaProvider.getCreateResourceSchema(schemaName);
|
||||||
|
} else if (type == "update") {
|
||||||
|
resourceSchema = await jsonschemaProvider.getCardResourceSchema(schemaName);
|
||||||
|
} else {
|
||||||
|
resourceSchema = await jsonschemaProvider.getUpdateResourceSchema(schemaName);
|
||||||
|
}
|
||||||
|
setSchema(resourceSchema);
|
||||||
|
setSchemaLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error while retrieving schema: ${schemaName} `, error);
|
||||||
|
setSchemaLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { schema, schemaLoading }
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnSchema<T extends GridValidRowModel> = {
|
||||||
|
columns: GridColDef<T>[],
|
||||||
|
columnVisibilityModel: GridColumnVisibilityModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnDefinition = {
|
||||||
|
field: string,
|
||||||
|
column: Partial<GridColDef>,
|
||||||
|
hide?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResourceColumns<T extends GridValidRowModel>(schemaName: string, columnDefinitions: ColumnDefinition[]) {
|
||||||
|
const [columnSchema, setColumnSchema] = useState<ColumnSchema<T>>()
|
||||||
|
const [columnLoading, setColumnLoading] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
const resourceColumns = await jsonschemaProvider.getReadResourceColumns(schemaName)
|
||||||
|
const definedColumns = computeColumnSchema<T>(columnDefinitions, resourceColumns)
|
||||||
|
setColumnSchema(definedColumns);
|
||||||
|
setColumnLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while retrieving columns schema:', error);
|
||||||
|
setColumnLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { columnSchema, columnLoading }
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeColumnSchema<T extends GridValidRowModel>(definitionColumns: ColumnDefinition[], resourceColumns: GridColDef[]): ColumnSchema<T> {
|
||||||
|
//reorder resourceColumns as in definition
|
||||||
|
definitionColumns.slice().reverse().forEach(first => {
|
||||||
|
resourceColumns.sort(function(x,y){ return x.field == first.field ? -1 : y.field == first.field ? 1 : 0; });
|
||||||
|
})
|
||||||
|
|
||||||
|
let visibilityModel: GridColumnVisibilityModel = {}
|
||||||
|
resourceColumns.forEach((resource, index) =>{
|
||||||
|
visibilityModel[resource.field] = definitionColumns.some(col => col.field == resource.field && !col.hide)
|
||||||
|
definitionColumns.forEach((def) => {
|
||||||
|
if (def.field == resource.field) {
|
||||||
|
resourceColumns[index] = {...resource, ...def.column};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: resourceColumns,
|
||||||
|
columnVisibilityModel: visibilityModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResourceFilter(resourceName: string, resourcePath: string) {
|
||||||
|
const [hasSearch, setHasSearch] = useState(false)
|
||||||
|
const [filtersSchema, setFiltersSchema] = useState<any[]>([])
|
||||||
|
const [filtersLoading, setFiltersLoading] = useState(true);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchSchema = async () => {
|
||||||
|
try {
|
||||||
|
setHasSearch(await jsonschemaProvider.hasSearch(resourcePath))
|
||||||
|
const resourceFilters = await jsonschemaProvider.getListFilters(resourceName, resourcePath)
|
||||||
|
setFiltersSchema(resourceFilters);
|
||||||
|
setFiltersLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while retrieving filter schema:', error);
|
||||||
|
setFiltersLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSchema();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { hasSearch, filtersSchema, filtersLoading }
|
||||||
|
}
|
||||||
@@ -1,56 +1,319 @@
|
|||||||
import { JSONSchema7Definition } from "json-schema";
|
|
||||||
import { RJSFSchema } from '@rjsf/utils';
|
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";
|
||||||
|
|
||||||
const API_URL = "/api/v1";
|
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 = {
|
export const jsonschemaProvider = {
|
||||||
getResourceSchema: async (resourceName: string): Promise<RJSFSchema> => {
|
getCardResourceSchema: async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||||
return buildResource(await getJsonschema(), resourceName)
|
const updateSchema = await getResourceSchema(`${resourceName}Update`);
|
||||||
}
|
const readSchema = await getResourceSchema(`${resourceName}Read`);
|
||||||
};
|
|
||||||
|
|
||||||
let rawSchema: RJSFSchema;
|
for (let prop_name in readSchema.properties) {
|
||||||
const getJsonschema = async (): Promise<RJSFSchema> => {
|
if (meta_fields.indexOf(prop_name) > -1) {
|
||||||
if (rawSchema === undefined) {
|
delete readSchema.properties[prop_name];
|
||||||
const response = await fetch(`${API_URL}/openapi.json`,);
|
} else if (! updateSchema.hasOwnProperty(prop_name)) {
|
||||||
rawSchema = await response.json();
|
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);
|
||||||
}
|
}
|
||||||
return rawSchema;
|
}
|
||||||
|
|
||||||
|
const getResourceSchema = async (resourceName: string): Promise<CrudRJSFSchema> => {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
|
||||||
let resource;
|
const shortResourceName = shortenResourceName(resourceName);
|
||||||
|
const jst = new JsonSchemaTraverser(rawSchemas);
|
||||||
|
let resource = structuredClone(jst.getResource(resourceName));
|
||||||
|
|
||||||
resource = structuredClone(rawSchemas.components.schemas[resourceName]);
|
|
||||||
resource.components = { schemas: {} };
|
resource.components = { schemas: {} };
|
||||||
for (let prop_name in resource.properties) {
|
for (let prop_name in resource.properties) {
|
||||||
let prop = resource.properties[prop_name];
|
let prop = resource.properties[prop_name];
|
||||||
|
|
||||||
if (is_reference(prop)) {
|
if (is_reference(prop)) {
|
||||||
resolveReference(rawSchemas, resource, prop);
|
buildReference(rawSchemas, resource, prop);
|
||||||
} else if (is_union(prop)) {
|
} else if (is_union(prop)) {
|
||||||
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
|
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
|
||||||
for (let i in union) {
|
for (let i in union) {
|
||||||
if (is_reference(union[i])) {
|
if (is_reference(union[i])) {
|
||||||
resolveReference(rawSchemas, resource, union[i]);
|
buildReference(rawSchemas, resource, union[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (is_enum(prop)) {
|
} else if (is_enum(prop)) {
|
||||||
for (let i in prop.allOf) {
|
for (let i in prop.allOf) {
|
||||||
if (is_reference(prop.allOf[i])) {
|
if (is_reference(prop.allOf[i])) {
|
||||||
resolveReference(rawSchemas, resource, prop.allOf[i]);
|
buildReference(rawSchemas, resource, prop.allOf[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (is_array(prop) && is_reference(prop.items)) {
|
} else if (is_array(prop) && is_reference(prop.items)) {
|
||||||
resolveReference(rawSchemas, resource, prop.items);
|
buildReference(rawSchemas, resource, prop.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prop.hasOwnProperty("title")) {
|
||||||
|
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;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
function buildReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
|
||||||
const subresourceName = get_reference_name(prop_reference);
|
const subresourceName = get_reference_name(prop_reference);
|
||||||
const subresource = buildResource(rawSchemas, subresourceName);
|
const subresource = buildResource(rawSchemas, subresourceName);
|
||||||
resource.components.schemas[subresourceName] = subresource;
|
resource.components.schemas[subresourceName] = subresource;
|
||||||
@@ -63,23 +326,35 @@ function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference:
|
|||||||
|
|
||||||
function changePropertiesOrder(resource: any) {
|
function changePropertiesOrder(resource: any) {
|
||||||
let created_at;
|
let created_at;
|
||||||
|
let created_by;
|
||||||
let updated_at;
|
let updated_at;
|
||||||
|
let updated_by;
|
||||||
let new_properties: any = {};
|
let new_properties: any = {};
|
||||||
for (let prop_name in resource.properties) {
|
for (let prop_name in resource.properties) {
|
||||||
if (prop_name == 'created_at') {
|
if (prop_name == 'created_at') {
|
||||||
created_at = resource.properties[prop_name];
|
created_at = resource.properties[prop_name];
|
||||||
|
} else if (prop_name == 'created_by') {
|
||||||
|
created_by = resource.properties[prop_name];
|
||||||
} else if (prop_name == 'updated_at') {
|
} else if (prop_name == 'updated_at') {
|
||||||
updated_at = resource.properties[prop_name];
|
updated_at = resource.properties[prop_name];
|
||||||
} else {
|
} else if (prop_name == 'updated_by') {
|
||||||
|
updated_by = resource.properties[prop_name];
|
||||||
|
}else {
|
||||||
new_properties[prop_name] = resource.properties[prop_name];
|
new_properties[prop_name] = resource.properties[prop_name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (created_at) {
|
if (created_at) {
|
||||||
new_properties['created_at'] = created_at;
|
new_properties['created_at'] = created_at;
|
||||||
}
|
}
|
||||||
|
if (created_by) {
|
||||||
|
new_properties['created_by'] = created_by;
|
||||||
|
}
|
||||||
if (updated_at) {
|
if (updated_at) {
|
||||||
new_properties['updated_at'] = updated_at;
|
new_properties['updated_at'] = updated_at;
|
||||||
}
|
}
|
||||||
|
if (updated_by) {
|
||||||
|
new_properties['updated_by'] = updated_by;
|
||||||
|
}
|
||||||
resource.properties = new_properties
|
resource.properties = new_properties
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,82 +382,157 @@ function get_reference_name(prop: any) {
|
|||||||
return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
|
return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_name: string): boolean {
|
function convertCamelToSnake(str: string): string {
|
||||||
if (is_array(resource)) {
|
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
|
||||||
return property_name == 'items';
|
|
||||||
} else if (is_object(resource)) {
|
|
||||||
return property_name in resource.properties!;
|
|
||||||
} else if (is_reference(resource)) {
|
|
||||||
let subresourceName = get_reference_name(resource);
|
|
||||||
return has_descendant(rawSchemas, buildResource(rawSchemas, subresourceName), property_name);
|
|
||||||
} else if (is_union(resource)) {
|
|
||||||
const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf;
|
|
||||||
if (union !== undefined) {
|
|
||||||
for (const ref of union) {
|
|
||||||
return has_descendant(rawSchemas, ref as RJSFSchema, property_name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (is_enum(resource)) {
|
|
||||||
for (const ref of resource.allOf!) {
|
|
||||||
return has_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error("Jsonschema format not implemented in property finder");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_descendant(rawSchemas: RJSFSchema, resource: RJSFSchema, property_name: string): RJSFSchema {
|
function shortenResourceName(resourceName: string) {
|
||||||
if (is_array(resource) && property_name == 'items') {
|
return convertCamelToSnake(resourceName.replace(/(-Input|-Output|Create|Update|Read)$/g, ""));
|
||||||
return resource.items as RJSFSchema;
|
}
|
||||||
} else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) {
|
|
||||||
return resource.properties[property_name] as RJSFSchema;
|
function getPropertyI18nLabel(shortResourceName: string, fieldName: string): string {
|
||||||
} else if (is_reference(resource)) {
|
if (meta_fields.indexOf(fieldName) > -1) {
|
||||||
let subresourceName = get_reference_name(resource);
|
return i18n.t(`schemas.${convertCamelToSnake(fieldName)}`);
|
||||||
let subresource = buildResource(rawSchemas, subresourceName);
|
}
|
||||||
return get_descendant(rawSchemas, subresource, property_name);
|
const path = `schemas.${shortResourceName}.${convertCamelToSnake(fieldName)}`
|
||||||
} else if (is_union(resource)) {
|
return i18n.t(path)
|
||||||
for (const ref of resource.oneOf!) {
|
}
|
||||||
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
|
|
||||||
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
let rawSchema: RJSFSchema;
|
||||||
}
|
const getJsonschema = async (): Promise<RJSFSchema> => {
|
||||||
|
if (rawSchema === undefined) {
|
||||||
|
const response = await fetch(`${API_URL}/openapi.json`,);
|
||||||
|
rawSchema = await response.json();
|
||||||
|
}
|
||||||
|
return rawSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonSchemaTraverser = class {
|
||||||
|
private rawSchemas: RJSFSchema;
|
||||||
|
|
||||||
|
constructor(rawSchemas: RJSFSchema) {
|
||||||
|
this.rawSchemas = rawSchemas
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResource = (resourceName: string) => {
|
||||||
|
if (this.rawSchemas.components.schemas[resourceName] === undefined) {
|
||||||
|
throw new Error(`Resource "${resourceName}" not found in schema.`);
|
||||||
}
|
}
|
||||||
} else if (is_enum(resource)) {
|
|
||||||
for (const ref of resource.allOf!) {
|
return this.rawSchemas.components.schemas[resourceName]
|
||||||
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
|
}
|
||||||
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
|
|
||||||
|
public getPath = (resourcePath: string) => {
|
||||||
|
const resourceParts = `/${resourcePath}/`.split("/");
|
||||||
|
let pathSchema: PathSchema|undefined;
|
||||||
|
for (const path in this.rawSchemas.paths) {
|
||||||
|
if (this.rawSchemas.paths[path].hasOwnProperty("get")) {
|
||||||
|
const pathParts = path.split("/")
|
||||||
|
if (pathParts.length == resourceParts.length) {
|
||||||
|
for (let i = 0; i < pathParts.length; i++) {
|
||||||
|
const isVariable = pathParts[i].slice(0,1) == "{" && pathParts[i].slice(-1) == "}";
|
||||||
|
if (! isVariable && pathParts[i] != resourceParts[i] ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == pathParts.length - 1) {
|
||||||
|
pathSchema = this.rawSchemas.paths[path].get
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (pathSchema !== undefined) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error("property not found or Jsonschema format not implemented");
|
if (pathSchema === undefined) {
|
||||||
}
|
throw ("Path not found in schema");
|
||||||
|
}
|
||||||
function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{
|
return pathSchema
|
||||||
const pointFirstPosition = path.indexOf('.')
|
|
||||||
if (pointFirstPosition == -1) {
|
|
||||||
return has_descendant(rawSchemas, resource, path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
|
public getDescendant = (resource: RJSFSchema, property_name: string): RJSFSchema | RJSFSchema[] => {
|
||||||
&& path_exists(
|
if (is_array(resource) && property_name == 'items') {
|
||||||
rawSchemas,
|
return resource.items as RJSFSchema;
|
||||||
get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)),
|
} else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) {
|
||||||
|
const prop = resource.properties[property_name];
|
||||||
|
if (is_reference(prop)) {
|
||||||
|
const subresourceName = get_reference_name(prop);
|
||||||
|
return this.getResource(subresourceName);
|
||||||
|
}
|
||||||
|
return prop as RJSFSchema;
|
||||||
|
} else if (is_reference(resource)) {
|
||||||
|
let subresourceName = get_reference_name(resource);
|
||||||
|
let subresource = this.getResource(subresourceName);
|
||||||
|
return this.getDescendant(subresource, property_name);
|
||||||
|
} else if (is_union(resource)) {
|
||||||
|
let descendants: RJSFSchema[] = [];
|
||||||
|
for (const ref of resource.oneOf!) {
|
||||||
|
if (this.hasDescendant(ref as RJSFSchema, property_name)) {
|
||||||
|
descendants.push(this.getDescendant(ref as RJSFSchema, property_name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (descendants.length > 0) {
|
||||||
|
return descendants;
|
||||||
|
}
|
||||||
|
} else if (is_enum(resource)) {
|
||||||
|
for (const ref of resource.allOf!) {
|
||||||
|
if (this.hasDescendant(ref as RJSFSchema, property_name)) {
|
||||||
|
return this.getDescendant(ref as RJSFSchema, property_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("property not found or Jsonschema format not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
public pathExists = (resource: RJSFSchema, path: string): boolean => {
|
||||||
|
const pointFirstPosition = path.indexOf('.')
|
||||||
|
if (pointFirstPosition == -1) {
|
||||||
|
return this.hasDescendant(resource, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hasDescendant(resource, path.substring(0, pointFirstPosition))
|
||||||
|
&& this.pathExists(
|
||||||
|
this.getDescendant(resource, path.substring(0, pointFirstPosition)),
|
||||||
|
path.substring(pointFirstPosition + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPropertyByPath = (resource: RJSFSchema, path: string): RJSFSchema => {
|
||||||
|
const pointFirstPosition = path.indexOf('.')
|
||||||
|
if (pointFirstPosition == -1) {
|
||||||
|
return this.getDescendant(resource, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getPropertyByPath(
|
||||||
|
this.getDescendant(
|
||||||
|
resource,
|
||||||
|
path.substring(0, pointFirstPosition)
|
||||||
|
),
|
||||||
path.substring(pointFirstPosition + 1)
|
path.substring(pointFirstPosition + 1)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function get_property_by_path(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): RJSFSchema {
|
|
||||||
const pointFirstPosition = path.indexOf('.')
|
|
||||||
if (pointFirstPosition == -1) {
|
|
||||||
return get_descendant(rawSchemas, resource, path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return get_property_by_path(
|
|
||||||
rawSchemas,
|
|
||||||
get_descendant(
|
|
||||||
rawSchemas,
|
|
||||||
resource,
|
|
||||||
path.substring(0, pointFirstPosition)
|
|
||||||
),
|
|
||||||
path.substring(pointFirstPosition + 1)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
184
gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx
Normal file
184
gui/rpk-gui/src/lib/filter-form/components/filter-form.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
|
||||||
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
|
import { FirmContext } from "../../../contexts/FirmContext";
|
||||||
|
import { Fragment, useContext } from "react";
|
||||||
|
import { Box, Grid2, styled } from "@mui/material";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export type FilterField = {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterFormProps = {
|
||||||
|
values: {[field_name: string]: { [operator: string]: string }}
|
||||||
|
fields: FilterField[]
|
||||||
|
onChange: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterForm = (props: FilterFormProps) => {
|
||||||
|
const { fields, values, onChange } = props;
|
||||||
|
|
||||||
|
let currentValue = values
|
||||||
|
|
||||||
|
const formField = fields.filter(f => f.name != "search").map((f, index) =>
|
||||||
|
<Fragment key={`${f.name}-${index}`} >
|
||||||
|
{ f.name == "created_at" && <Box width="100%" /> }
|
||||||
|
<Grid2 size={6}>
|
||||||
|
<FilterFormField
|
||||||
|
field={f}
|
||||||
|
value={values.hasOwnProperty(f.name) ? values[f.name] : {}}
|
||||||
|
onChange={(value) => {
|
||||||
|
for (const op in value) {
|
||||||
|
if (value[op] == null || value[op] == "") {
|
||||||
|
if (currentValue.hasOwnProperty(f.name)) {
|
||||||
|
if (currentValue[f.name].hasOwnProperty(op)) {
|
||||||
|
delete currentValue[f.name][op];
|
||||||
|
}
|
||||||
|
if (Object.entries(currentValue[f.name]).length == 0) {
|
||||||
|
delete currentValue[f.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (! currentValue.hasOwnProperty(f.name)) {
|
||||||
|
currentValue[f.name] = {};
|
||||||
|
}
|
||||||
|
currentValue[f.name][op] = value[op];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onChange(currentValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
{formField}
|
||||||
|
</Grid2>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperatorValue = { [operator: string]: string }
|
||||||
|
|
||||||
|
type FilterFormFieldProps = {
|
||||||
|
field: FilterField
|
||||||
|
value: OperatorValue
|
||||||
|
onChange: (value: { [operator: string]: string | null }) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterFormField = (props: FilterFormFieldProps) => {
|
||||||
|
const { field, value, onChange } = props;
|
||||||
|
|
||||||
|
if (field.type == "string") {
|
||||||
|
const defaultValue = value.hasOwnProperty('ilike') ? value['ilike'] : undefined
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
name={field.name}
|
||||||
|
label={field.label}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
fullWidth={true}
|
||||||
|
onChange={(event) => onChange({"ilike": event.target.value})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (field.type == "dateTime") {
|
||||||
|
return (
|
||||||
|
<FilterFieldDateRange field={field} value={value} onChange={onChange} />
|
||||||
|
)
|
||||||
|
} else if (field.type == "author") {
|
||||||
|
return (
|
||||||
|
<FilterFieldAuthor field={field} value={value} onChange={onChange} />
|
||||||
|
);
|
||||||
|
} else if (field.type.slice(0, 4) == "enum") {
|
||||||
|
const values = field.type.slice(5,-1).split("|");
|
||||||
|
const defaultValue = value.hasOwnProperty("in") ? [{
|
||||||
|
value: value["in"],
|
||||||
|
label: value["in"]
|
||||||
|
}] : undefined;
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={values.map(opt => {
|
||||||
|
return {
|
||||||
|
value: opt,
|
||||||
|
label: opt
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
onChange={(event, value) => onChange({ "in": value.map(v => v.value).join(",") })}
|
||||||
|
getOptionLabel={(option) => option.label}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label={field.label}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw("Unsupported field filter type");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type FilterFieldAuthorProp = FilterFormFieldProps
|
||||||
|
|
||||||
|
const FilterFieldAuthor = (props: FilterFieldAuthorProp) => {
|
||||||
|
const { field, onChange } = props;
|
||||||
|
const { partnerMap } = useContext(FirmContext)
|
||||||
|
|
||||||
|
if (partnerMap == undefined) {
|
||||||
|
throw "Can't use author filter outside of the context of a firm";
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = []
|
||||||
|
for(let key of Array.from(partnerMap.keys()) ) {
|
||||||
|
options.push({
|
||||||
|
id: key,
|
||||||
|
label: partnerMap.get(key)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
renderInput={(params) => <TextField {...params} label={field.label} />}
|
||||||
|
options={options}
|
||||||
|
onChange={(event, value) => onChange({ "in": value.length == 0 ? null : value.map(v => v.id).join(",") })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterFieldDateRange = (props: FilterFormFieldProps) => {
|
||||||
|
const { field, value, onChange } = props;
|
||||||
|
|
||||||
|
const defaultAfterValue = value.hasOwnProperty('gte') ? dayjs(value['gte']) : undefined;
|
||||||
|
const defaultBeforeValue = value.hasOwnProperty('lte') ? dayjs(value['lte']) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row">
|
||||||
|
<DateTimePicker
|
||||||
|
name={field.name}
|
||||||
|
label={`${field.label} After:`}
|
||||||
|
slotProps={{ textField: { fullWidth: true }, field: { clearable: true }}}
|
||||||
|
defaultValue={defaultAfterValue}
|
||||||
|
onChange={(value) => onChange({'gte': value === null ? null : value.toJSON()})}
|
||||||
|
/>
|
||||||
|
<DateTimePicker
|
||||||
|
name={field.name}
|
||||||
|
label={`${field.label} Before:`}
|
||||||
|
slotProps={{ textField: { fullWidth: true }, field: { clearable: true } }}
|
||||||
|
defaultValue={defaultBeforeValue}
|
||||||
|
onChange={(value) => onChange({'lte': value === null ? null : value.toJSON()})}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterForm;
|
||||||
|
|
||||||
4
gui/rpk-gui/src/pages/ErrorPage.tsx
Normal file
4
gui/rpk-gui/src/pages/ErrorPage.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export const Error404Page = () => {
|
||||||
|
return <h2>EROR NO FUND</h2>
|
||||||
|
};
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Route, Routes } from "react-router";
|
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 List from "./base-page/List";
|
import List from "./base-page/List";
|
||||||
import Edit from "./base-page/Edit";
|
import Cartouche from "../../components/Cartouche";
|
||||||
import New from "./base-page/New";
|
|
||||||
|
|
||||||
type Contract = {
|
export type Contract = {
|
||||||
id: string,
|
id: string,
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
@@ -13,22 +18,53 @@ export const ContractRoutes = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={ <ListContract /> } />
|
<Route index element={ <ListContract /> } />
|
||||||
<Route path="/edit/:record_id" element={ <EditContract /> } />
|
<Route path="/edit/:record_id" element={ <EditContract /> } />
|
||||||
<Route path="/create" element={ <CreateContract /> } />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListContract = () => {
|
const ListContract = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "status", column: { width: 160 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Contract> resource={`contracts`} columns={columns} />
|
return <List<Contract> resource={`contracts`} schemaName={"Contract"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditContract = () => {
|
const EditContract = () => {
|
||||||
return <Edit<Contract> resource={`contracts`} schemaName={"Contract"} />
|
const { currentFirm } = useContext(FirmContext);
|
||||||
}
|
const { translate: t } = useTranslation();
|
||||||
|
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
||||||
|
const { record_id } = useParams();
|
||||||
|
|
||||||
const CreateContract = () => {
|
const { data, isLoading } = useOne({resource: `${resourceBasePath}/contracts`, id: record_id,});
|
||||||
return <New<Contract> resource={`contracts`} schemaName={"Contract"} />
|
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
import { Route, Routes } from "react-router";
|
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 List from "./base-page/List";
|
import List from "./base-page/List";
|
||||||
import Edit from "./base-page/Edit";
|
import Edit from "./base-page/Edit";
|
||||||
import New from "./base-page/New";
|
import New from "./base-page/New";
|
||||||
|
import { Contract } from "./ContractRoutes";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
type Draft = {
|
type Draft = {
|
||||||
id: string,
|
id: string,
|
||||||
@@ -20,15 +29,146 @@ export const DraftRoutes = () => {
|
|||||||
|
|
||||||
const ListDraft = () => {
|
const ListDraft = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "status", column: { width: 160 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Draft> resource={`contracts/drafts`} columns={columns} />
|
return <List<Draft> resource={`contracts/drafts`} columns={columns} schemaName={"Contract"} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditDraft = () => {
|
const EditDraft = () => {
|
||||||
return <Edit<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
|
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 } } }
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateDraft = () => {
|
const CreateDraft = () => {
|
||||||
return <New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
|
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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
</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 }/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ export const EntityRoutes = () => {
|
|||||||
|
|
||||||
const ListEntity = () => {
|
const ListEntity = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "entity_data.type", column: { width: 110 }},
|
||||||
{ field: "entity_data", headerName: "Type", flex: 1, valueFormatter: ({ type }: {type: string}) => type }
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Entity> resource={`entities`} columns={columns} />
|
return <List<Entity> resource={`entities`} schemaName={"Entity"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditEntity = () => {
|
const EditEntity = () => {
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ export const ProvisionRoutes = () => {
|
|||||||
|
|
||||||
const ListProvision = () => {
|
const ListProvision = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Provision> resource={`templates/provisions`} columns={columns} />
|
return <List<Provision> resource={`templates/provisions`} schemaName={"ProvisionTemplate"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditProvision = () => {
|
const EditProvision = () => {
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ export const TemplateRoutes = () => {
|
|||||||
|
|
||||||
const ListTemplate = () => {
|
const ListTemplate = () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: "label", headerName: "Label", flex: 1 },
|
{ field: "label", column: { flex: 1 }},
|
||||||
|
{ field: "updated_at", column: { width: 160 }},
|
||||||
];
|
];
|
||||||
return <List<Template> resource={`templates/contracts`} columns={columns} />
|
return <List<Template> resource={`templates/contracts`} schemaName={"ContractTemplate"} columns={columns} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditTemplate = () => {
|
const EditTemplate = () => {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { CrudForm } from "../../../lib/crud/components/crud-form";
|
|
||||||
import { UiSchema } from "@rjsf/utils";
|
import { UiSchema } from "@rjsf/utils";
|
||||||
import { useParams } from "react-router";
|
|
||||||
import { useContext } from "react";
|
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 { FirmContext } from "../../../contexts/FirmContext";
|
||||||
|
import { CrudForm } from "../../../lib/crud/components/crud-form";
|
||||||
|
import Cartouche from "../../../components/Cartouche";
|
||||||
|
|
||||||
type EditProps = {
|
type EditProps = {
|
||||||
resource: string,
|
resource: string,
|
||||||
@@ -13,17 +19,56 @@ type EditProps = {
|
|||||||
const Edit = <T,>(props: EditProps) => {
|
const Edit = <T,>(props: EditProps) => {
|
||||||
const { schemaName, resource, uiSchema } = props;
|
const { schemaName, resource, uiSchema } = props;
|
||||||
const { currentFirm } = useContext(FirmContext);
|
const { currentFirm } = useContext(FirmContext);
|
||||||
|
const { translate: t } = useTranslation();
|
||||||
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
||||||
const { record_id } = useParams();
|
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 (
|
return (
|
||||||
<CrudForm
|
<>
|
||||||
schemaName={schemaName}
|
<Link to={"../"} >
|
||||||
uiSchema={uiSchema}
|
<Button>{t("buttons.list")}</Button>
|
||||||
resourceBasePath={resourceBasePath}
|
</Link>
|
||||||
resource={resource}
|
<h2>{record.label}</h2>
|
||||||
id={record_id}
|
<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>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,81 @@
|
|||||||
import { UiSchema } from "@rjsf/utils";
|
|
||||||
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 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 { Button } from "@mui/material";
|
import { Button } from "@mui/material";
|
||||||
|
import { GridColDef, GridValidRowModel } from "@mui/x-data-grid";
|
||||||
import { FirmContext } from "../../../contexts/FirmContext";
|
import { FirmContext } from "../../../contexts/FirmContext";
|
||||||
|
import CrudList from "../../../lib/crud/components/crud-list";
|
||||||
|
import CrudFilters, { OnChangeValue } from "../../../lib/crud/components/crud-filters";
|
||||||
|
import { CrudOperators } from "@refinedev/core/src/contexts/data/types";
|
||||||
|
|
||||||
type ListProps<T extends GridValidRowModel> = {
|
type ListProps = {
|
||||||
resource: string,
|
resource: string,
|
||||||
columns: GridColDef<T>[],
|
columns: ColumnDefinition[],
|
||||||
schemaName?: string,
|
schemaName: string,
|
||||||
uiSchema?: UiSchema,
|
uiSchema?: UiSchema,
|
||||||
}
|
}
|
||||||
|
|
||||||
const List = <T extends GridValidRowModel>(props: ListProps<T>) => {
|
type ColumnDefinition = {
|
||||||
const { resource, columns } = props;
|
field: string,
|
||||||
|
column: Partial<GridColDef>,
|
||||||
|
hide?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const List = <T extends GridValidRowModel>(props: ListProps) => {
|
||||||
|
const { resource, columns, schemaName } = props;
|
||||||
|
const { translate: t } = useTranslation();
|
||||||
const { currentFirm } = useContext(FirmContext);
|
const { currentFirm } = useContext(FirmContext);
|
||||||
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
||||||
|
|
||||||
const { dataGridProps } = useDataGrid<T>({resource: `${resourceBasePath}/${resource}`});
|
const { dataGridProps, tableQuery, setFilters } = useDataGrid<T>({
|
||||||
|
resource: `${resourceBasePath}/${resource}`,
|
||||||
|
});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const cols = React.useMemo<GridColDef<T>[]>(
|
if (tableQuery.error?.status == 404) {
|
||||||
() => columns,
|
throw tableQuery.error
|
||||||
[],
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const handleRowClick = (params: any, event: any) => {
|
const onFilterChange = (value: OnChangeValue) => {
|
||||||
navigate(`edit/${params.id}`)
|
let newFilters: CrudFilter[] = []
|
||||||
|
if (value.search != null) {
|
||||||
|
newFilters.push({
|
||||||
|
field: "search",
|
||||||
|
operator: "eq",
|
||||||
|
value: value.search
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const filterName in value.filters) {
|
||||||
|
for (const operator in value.filters[filterName]) {
|
||||||
|
newFilters.push({
|
||||||
|
field: filterName,
|
||||||
|
operator: operator as Exclude<CrudOperators, "or" | "and">,
|
||||||
|
value: value.filters[filterName][operator]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setFilters(newFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RefineList>
|
<RefineList>
|
||||||
<Link to={"create"} >
|
<Link to={"create"} >
|
||||||
<Button>Create</Button>
|
<Button>{t("buttons.create")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<DataGrid
|
<CrudFilters
|
||||||
{...dataGridProps}
|
resourceName={schemaName}
|
||||||
columns={cols}
|
resourcePath={`${resourceBasePath}/${resource}`}
|
||||||
onRowClick={handleRowClick} />
|
onChange={onFilterChange}
|
||||||
|
/>
|
||||||
|
<CrudList
|
||||||
|
dataGridProps={dataGridProps}
|
||||||
|
onRowClick={(params: any) => { navigate(`edit/${params.id}`) }}
|
||||||
|
schemaName={schemaName}
|
||||||
|
resourceBasePath={resourceBasePath}
|
||||||
|
columnDefinitions={columns}
|
||||||
|
/>
|
||||||
</RefineList>
|
</RefineList>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { CrudForm } from "../../../lib/crud/components/crud-form";
|
|
||||||
import { UiSchema } from "@rjsf/utils";
|
import { UiSchema } from "@rjsf/utils";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
import { useForm, useTranslation } from "@refinedev/core";
|
||||||
|
import { CrudForm } from "../../../lib/crud/components/crud-form";
|
||||||
import { FirmContext } from "../../../contexts/FirmContext";
|
import { FirmContext } from "../../../contexts/FirmContext";
|
||||||
|
import SaveIcon from "@mui/icons-material/Save";
|
||||||
|
import { Button } from "@mui/material";
|
||||||
|
|
||||||
type NewProps = {
|
type NewProps = {
|
||||||
resource: string,
|
resource: string,
|
||||||
@@ -13,16 +16,25 @@ type NewProps = {
|
|||||||
const New = <T,>(props: NewProps) => {
|
const New = <T,>(props: NewProps) => {
|
||||||
const { schemaName, resource, uiSchema, defaultValue } = props;
|
const { schemaName, resource, uiSchema, defaultValue } = props;
|
||||||
const { currentFirm } = useContext(FirmContext);
|
const { currentFirm } = useContext(FirmContext);
|
||||||
|
const { translate: t } = useTranslation();
|
||||||
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
|
||||||
|
|
||||||
|
const { onFinish } = useForm({
|
||||||
|
resource: `${resourceBasePath}/${resource}`,
|
||||||
|
action: "create",
|
||||||
|
redirect: "show",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CrudForm
|
<CrudForm
|
||||||
schemaName={schemaName}
|
schemaName={schemaName}
|
||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
resourceBasePath={resourceBasePath}
|
resourceBasePath={resourceBasePath}
|
||||||
resource={resource}
|
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
/>
|
onSubmit={(data: any) => onFinish(data)}
|
||||||
|
>
|
||||||
|
<Button type='submit' variant="contained" size="large"><SaveIcon />{t("buttons.create")}</Button>
|
||||||
|
</CrudForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Route, Routes, Link } from "react-router";
|
import { Route, Routes, Link } from "react-router";
|
||||||
import React, { useContext } from "react";
|
import React from "react";
|
||||||
import { FirmContext, FirmContextProvider } from "../../contexts/FirmContext";
|
import { useForm, useTranslation } from "@refinedev/core";
|
||||||
|
import { FirmContextProvider } from "../../contexts/FirmContext";
|
||||||
import { Header } from "../../components";
|
import { Header } from "../../components";
|
||||||
import { useOne } from "@refinedev/core";
|
|
||||||
import { CrudForm } from "../../lib/crud/components/crud-form";
|
import { CrudForm } from "../../lib/crud/components/crud-form";
|
||||||
import { IFirm } from "../../interfaces";
|
import { IFirm } from "../../interfaces";
|
||||||
import { EntityRoutes } from "./EntityRoutes";
|
import { EntityRoutes } from "./EntityRoutes";
|
||||||
@@ -10,51 +10,44 @@ import { ContractRoutes } from "./ContractRoutes";
|
|||||||
import { DraftRoutes } from "./DraftRoutes";
|
import { DraftRoutes } from "./DraftRoutes";
|
||||||
import { TemplateRoutes } from "./TemplateRoutes";
|
import { TemplateRoutes } from "./TemplateRoutes";
|
||||||
import { ProvisionRoutes } from "./ProvisionRoutes";
|
import { ProvisionRoutes } from "./ProvisionRoutes";
|
||||||
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import { Error404Page } from "../ErrorPage";
|
||||||
|
|
||||||
export const FirmRoutes = () => {
|
export const FirmRoutes = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/:instance/:firm/*" element={
|
<Route path="/:instance/:firm/*" element={
|
||||||
<FirmContextProvider>
|
<ErrorBoundary fallback={<><Header /><Error404Page /></>} >
|
||||||
<Header />
|
<FirmContextProvider>
|
||||||
<Routes>
|
<Header />
|
||||||
<Route index element={ <FirmHome /> } />
|
<Routes>
|
||||||
<Route path="/entities/*" element={ <EntityRoutes /> } />
|
<Route index element={ <FirmHome /> } />
|
||||||
<Route path="/provisions/*" element={ <ProvisionRoutes /> } />
|
<Route path="/entities/*" element={ <EntityRoutes /> } />
|
||||||
<Route path="/templates/*" element={ <TemplateRoutes /> } />
|
<Route path="/provisions/*" element={ <ProvisionRoutes /> } />
|
||||||
<Route path="/drafts/*" element={ <DraftRoutes /> } />
|
<Route path="/templates/*" element={ <TemplateRoutes /> } />
|
||||||
<Route path="/contracts/*" element={ <ContractRoutes /> } />
|
<Route path="/drafts/*" element={ <DraftRoutes /> } />
|
||||||
</Routes>
|
<Route path="/contracts/*" element={ <ContractRoutes /> } />
|
||||||
</FirmContextProvider>
|
</Routes>
|
||||||
|
</FirmContextProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
} />
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const FirmHome = () => {
|
const FirmHome = () => {
|
||||||
const { currentFirm } = useContext(FirmContext);
|
const { translate: t } = useTranslation();
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>This is la firme {currentFirm.instance} / {currentFirm.firm}</h1>
|
<h1>{t("dashboard.title")}</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<li><Link to="entities">Entitées</Link></li>
|
<li><Link to="entities">{t("schemas.entity.resource_plural")}</Link></li>
|
||||||
<li><Link to="provisions">Templates de Clauses</Link></li>
|
<li><Link to="provisions">{t("schemas.provision_template.resource_plural")}</Link></li>
|
||||||
<li><Link to="templates">Templates de Contrats</Link></li>
|
<li><Link to="templates">{t("schemas.contract_template.resource_plural")}</Link></li>
|
||||||
<li><Link to="drafts">Brouillons</Link></li>
|
<li><Link to="drafts">{t("schemas.contract_draft.resource_plural")}</Link></li>
|
||||||
<li><Link to="contracts">Contrats</Link></li>
|
<li><Link to="contracts">{t("schemas.contract.resource_plural")}</Link></li>
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,18 +55,28 @@ type FirmInitFormPros = {
|
|||||||
currentFirm: IFirm
|
currentFirm: IFirm
|
||||||
}
|
}
|
||||||
|
|
||||||
const FirmInitForm = (props: FirmInitFormPros) => {
|
export const FirmInitForm = (props: FirmInitFormPros) => {
|
||||||
const { currentFirm } = props;
|
const { currentFirm } = props;
|
||||||
|
const { translate: t } = useTranslation();
|
||||||
|
const resourceBasePath = `firm`
|
||||||
|
|
||||||
|
const { onFinish } = useForm({
|
||||||
|
resource: `${resourceBasePath}/${currentFirm.instance}/${currentFirm.firm}`,
|
||||||
|
action: "create",
|
||||||
|
redirect: "show",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Initialization of {`${currentFirm.instance} / ${currentFirm.firm}`}</h1>
|
<h1>Initialization of {`${currentFirm.instance} / ${currentFirm.firm}`}</h1>
|
||||||
|
|
||||||
<CrudForm
|
<CrudForm
|
||||||
schemaName={"CurrentFirmSchemaCreate"}
|
schemaName={"CurrentFirmSchema"}
|
||||||
resource={`firm/${currentFirm.instance}/${currentFirm.firm}/`}
|
resourceBasePath={resourceBasePath}
|
||||||
|
defaultValue={{corporation: {entity_data: {activity: t("firm.default_activity") }}}}
|
||||||
uiSchema={{
|
uiSchema={{
|
||||||
corporation: {entity_data: {employees: {"ui:style": {"display": "none"}}}},
|
corporation: {entity_data: {employees: {"ui:style": {"display": "none"}}}},
|
||||||
}}
|
}}
|
||||||
|
onSubmit={(data: any) => onFinish(data)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useInvalidateAuthStore } from "@refinedev/core";
|
import { useForm, useInvalidateAuthStore } from "@refinedev/core";
|
||||||
import { CrudForm } from "../../lib/crud/components/crud-form";
|
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 = () => {
|
export const CreateFirm = () => {
|
||||||
const invalidateAuthStore = useInvalidateAuthStore()
|
const invalidateAuthStore = useInvalidateAuthStore()
|
||||||
@@ -9,11 +9,19 @@ export const CreateFirm = () => {
|
|||||||
invalidateAuthStore().then();
|
invalidateAuthStore().then();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resourceBasePath = "hub/users";
|
||||||
|
const { onFinish } = useForm({
|
||||||
|
resource: `${resourceBasePath}/firms`,
|
||||||
|
action: "create",
|
||||||
|
redirect: "list",
|
||||||
|
onMutationSuccess: data => refreshUser()
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CrudForm
|
<CrudForm
|
||||||
schemaName={"FirmCreate"}
|
schemaName={"Firm"}
|
||||||
resource={"hub/users/firms/"}
|
resourceBasePath={resourceBasePath}
|
||||||
onSuccess={() => { refreshUser() }}
|
onSubmit={(data: any) => onFinish(data)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ const DISCORD_SCOPES = { "scopes": "identify email" }
|
|||||||
|
|
||||||
const DEFAULT_LOGIN_REDIRECT = "/hub"
|
const DEFAULT_LOGIN_REDIRECT = "/hub"
|
||||||
|
|
||||||
export const authProvider: AuthProvider = {
|
const authProvider: AuthProvider = {
|
||||||
login: async ({ providerName, email, password }) => {
|
login: async ({ providerName, email, password }) => {
|
||||||
const to_param = findGetParameter("to");
|
const to_param = findGetParameter("to");
|
||||||
const redirect = to_param === null ? DEFAULT_LOGIN_REDIRECT : to_param
|
const redirect = to_param === null ? getLoginRedirect() : to_param
|
||||||
if (providerName) {
|
if (providerName) {
|
||||||
let scope = {};
|
let scope = {};
|
||||||
if (providerName === "google") {
|
if (providerName === "google") {
|
||||||
@@ -61,18 +61,22 @@ export const authProvider: AuthProvider = {
|
|||||||
const response = await fetch(`${API_URL}/hub/auth/logout`, { method: "POST" });
|
const response = await fetch(`${API_URL}/hub/auth/logout`, { method: "POST" });
|
||||||
if (response.status == 204 || response.status == 401) {
|
if (response.status == 204 || response.status == 401) {
|
||||||
forget_user();
|
forget_user();
|
||||||
return {
|
return { success: true };
|
||||||
success: true,
|
|
||||||
redirectTo: "/",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { success: false };
|
return { success: false };
|
||||||
},
|
},
|
||||||
check: async () => {
|
check: async () => {
|
||||||
if (get_user() == null) {
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
redirectTo: "/auth/login",
|
|
||||||
logout: true
|
logout: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,11 +88,7 @@ export const authProvider: AuthProvider = {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_URL}/hub/users/me`);
|
const user_data = get_me()
|
||||||
if (response.status < 200 || response.status > 299) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const user_data = await response.json();
|
|
||||||
store_user(user_data)
|
store_user(user_data)
|
||||||
|
|
||||||
return user_data;
|
return user_data;
|
||||||
@@ -154,9 +154,8 @@ export const authProvider: AuthProvider = {
|
|||||||
if (error?.status === 401) {
|
if (error?.status === 401) {
|
||||||
forget_user();
|
forget_user();
|
||||||
return {
|
return {
|
||||||
redirectTo: "/auth/login",
|
|
||||||
logout: true,
|
|
||||||
error: { message: "Authentication required" },
|
error: { message: "Authentication required" },
|
||||||
|
logout: true,
|
||||||
} as OnErrorResponse;
|
} as OnErrorResponse;
|
||||||
}
|
}
|
||||||
else if (error?.status === 403) {
|
else if (error?.status === 403) {
|
||||||
@@ -170,6 +169,15 @@ export 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) {
|
function store_user(user: any) {
|
||||||
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(user));
|
localStorage.setItem(LOCAL_STORAGE_USER_KEY, JSON.stringify(user));
|
||||||
}
|
}
|
||||||
@@ -199,3 +207,12 @@ function findGetParameter(parameterName: string) {
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLoginRedirect() {
|
||||||
|
if (location.pathname == "/login") {
|
||||||
|
return DEFAULT_LOGIN_REDIRECT
|
||||||
|
}
|
||||||
|
|
||||||
|
return location.pathname + location.search;
|
||||||
|
}
|
||||||
|
export default authProvider;
|
||||||
|
|||||||
@@ -2,21 +2,26 @@ import type { DataProvider, HttpError } from "@refinedev/core";
|
|||||||
|
|
||||||
const API_URL = "/api/v1";
|
const API_URL = "/api/v1";
|
||||||
|
|
||||||
export const dataProvider: DataProvider = {
|
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 }) => {
|
getOne: async ({ resource, id, meta }) => {
|
||||||
if (id === "") {
|
if (id === "") {
|
||||||
return { data: undefined };
|
return { data: undefined };
|
||||||
}
|
}
|
||||||
const response = await fetch(`${API_URL}/${resource}/${id}`);
|
const response = await fetch(`${API_URL}/${resource}/${id}`);
|
||||||
if (response.status < 200 || response.status > 299) {
|
if (response.status < 200 || response.status > 299) {
|
||||||
if (response.status == 405) {
|
return handleErrors(response);
|
||||||
const error: HttpError = {
|
|
||||||
message: "Resource is not ready",
|
|
||||||
statusCode: 405,
|
|
||||||
};
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
throw response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -35,17 +40,10 @@ export const dataProvider: DataProvider = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) {
|
if (response.status < 200 || response.status > 299) {
|
||||||
if (response.status == 405) {
|
return handleErrors(response);
|
||||||
const error: HttpError = {
|
|
||||||
message: "Resource is not ready",
|
|
||||||
statusCode: 405,
|
|
||||||
};
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
throw response;
|
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
return { data };
|
return { data };
|
||||||
},
|
},
|
||||||
getList: async ({ resource, pagination, filters, sorters, meta }) => {
|
getList: async ({ resource, pagination, filters, sorters, meta }) => {
|
||||||
@@ -63,8 +61,12 @@ export const dataProvider: DataProvider = {
|
|||||||
|
|
||||||
if (filters && filters.length > 0) {
|
if (filters && filters.length > 0) {
|
||||||
filters.forEach((filter) => {
|
filters.forEach((filter) => {
|
||||||
if ("field" in filter && filter.value && filter.operator === "contains") {
|
if ("field" in filter) {
|
||||||
params.append(filter.field + "__ilike", filter.value);
|
if (filter.field == "search") {
|
||||||
|
params.append("search", filter.value);
|
||||||
|
} else {
|
||||||
|
params.append(`${filter.field.replace(".", "__")}__${filter.operator}`, filter.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -72,14 +74,7 @@ export const dataProvider: DataProvider = {
|
|||||||
const response = await fetch(`${API_URL}/${resource}/?${params.toString()}`);
|
const response = await fetch(`${API_URL}/${resource}/?${params.toString()}`);
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) {
|
if (response.status < 200 || response.status > 299) {
|
||||||
if (response.status == 405) {
|
return handleErrors(response);
|
||||||
const error: HttpError = {
|
|
||||||
message: "Resource is not ready",
|
|
||||||
statusCode: 405,
|
|
||||||
};
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
throw response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -96,7 +91,7 @@ export const dataProvider: DataProvider = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
create: async ({ resource, variables }) => {
|
create: async ({ resource, variables }) => {
|
||||||
const response = await fetch(`${API_URL}/${resource}`, {
|
const response = await fetch(`${API_URL}/${resource}/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(variables),
|
body: JSON.stringify(variables),
|
||||||
headers: {
|
headers: {
|
||||||
@@ -105,18 +100,10 @@ export const dataProvider: DataProvider = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) {
|
if (response.status < 200 || response.status > 299) {
|
||||||
if (response.status == 405) {
|
return handleErrors(response);
|
||||||
const error: HttpError = {
|
|
||||||
message: "Resource is not ready",
|
|
||||||
statusCode: 405,
|
|
||||||
};
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
throw response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
return { data };
|
return { data };
|
||||||
},
|
},
|
||||||
deleteOne: async ({ resource, id, variables, meta }) => {
|
deleteOne: async ({ resource, id, variables, meta }) => {
|
||||||
@@ -125,21 +112,11 @@ export const dataProvider: DataProvider = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) {
|
if (response.status < 200 || response.status > 299) {
|
||||||
if (response.status == 405) {
|
return handleErrors(response);
|
||||||
const error: HttpError = {
|
|
||||||
message: "Resource is not ready",
|
|
||||||
statusCode: 405,
|
|
||||||
};
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
throw response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
return { data };
|
||||||
return {
|
|
||||||
data
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
getApiUrl: () => API_URL,
|
getApiUrl: () => API_URL,
|
||||||
// Optional methods:
|
// Optional methods:
|
||||||
@@ -149,3 +126,5 @@ export const dataProvider: DataProvider = {
|
|||||||
// updateMany: () => { /* ... */ },
|
// updateMany: () => { /* ... */ },
|
||||||
// custom: () => { /* ... */ },
|
// custom: () => { /* ... */ },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default dataProvider;
|
||||||
|
|||||||
11
i18n/Dockerfile
Normal file
11
i18n/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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
Normal file
26
i18n/app/.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 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?
|
||||||
1164
i18n/app/package-lock.json
generated
Normal file
1164
i18n/app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
i18n/app/package.json
Normal file
25
i18n/app/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
i18n/app/public/locales/.placeholder
Normal file
0
i18n/app/public/locales/.placeholder
Normal file
49
i18n/app/src/index.ts
Normal file
49
i18n/app/src/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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}`);
|
||||||
|
});
|
||||||
13
i18n/app/tsconfig.json
Normal file
13
i18n/app/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user