16 Commits

31 changed files with 2901 additions and 558 deletions

View File

@@ -5,8 +5,9 @@ from uuid import UUID
from beanie import PydanticObjectId from beanie import PydanticObjectId
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
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
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
@@ -25,27 +26,10 @@ class ContractDraftStatus(str, Enum):
class DraftParty(BaseModel): class DraftParty(BaseModel):
entity_id: PydanticObjectId = Field( entity_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Partie")
foreignKey={ entity: SkipJsonSchema[Entity] = Field(default=None, exclude=True, )
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Partie"
)
part: str = Field(title="Rôle") part: str = Field(title="Rôle")
representative_id: PydanticObjectId = Field( representative_id: PydanticObjectId = ForeignKey("entities", "Entity", default="", title="Représentant")
foreignKey={
"reference": {
"resource": "entities",
"schema": "Entity",
}
},
default="",
title="Représentant"
)
class Config: class Config:
title = 'Partie' title = 'Partie'
@@ -74,14 +58,10 @@ class ProvisionGenuine(BaseModel):
class ContractProvisionTemplateReference(BaseModel): class ContractProvisionTemplateReference(BaseModel):
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="",
title="Template de clause" title="Template de clause"
@@ -173,6 +153,9 @@ 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.

View File

@@ -114,6 +114,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 = ""

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -31,11 +31,15 @@
"@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",
"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-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": {

View 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"
}
}

View File

@@ -0,0 +1,273 @@
{
"pages": {
"home": {
"title": "Home"
},
"login": {
"title": "Sign in to your account",
"signin": "Sign in",
"signup": "Sign up",
"divider": "or",
"fields": {
"email": "Email",
"password": "Password"
},
"oauth": {
"google": "Sign in with Google",
"discord": "Sign in with Discord"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Login",
"forgotPassword": "Forgot password?",
"noAccount": "Dont have an account?",
"rememberMe": "Remember me"
}
},
"forgotPassword": {
"title": "Forgot your password?",
"fields": {
"email": "Email"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required"
},
"buttons": {
"submit": "Send reset instructions"
}
},
"register": {
"title": "Sign up for your account",
"fields": {
"email": "Email",
"password": "Password"
},
"errors": {
"validEmail": "Invalid email address",
"requiredEmail": "Email is required",
"requiredPassword": "Password is required"
},
"buttons": {
"submit": "Register",
"haveAccount": "Have an account?"
}
},
"updatePassword": {
"title": "Update password",
"fields": {
"password": "New Password",
"confirmPassword": "Confirm new password"
},
"errors": {
"confirmPasswordNotMatch": "Passwords do not match",
"requiredPassword": "Password required",
"requiredConfirmPassword": "Confirm password is required"
},
"buttons": {
"submit": "Update"
}
},
"error": {
"404": "Sorry, the page you visited does not exist.",
"info": "You may have forgotten to add the {{action}} component to {{resource}} resource.",
"resource404": "Are you sure you have created the {{resource}} resource.",
"backHome": "Back Home"
}
},
"actions": {
"list": "List",
"create": "Create",
"edit": "Edit",
"show": "Show"
},
"buttons": {
"create": "Create",
"save": "Save",
"logout": "Logout",
"delete": "Delete",
"edit": "Edit",
"cancel": "Cancel",
"confirm": "Are you sure?",
"filter": "Filter",
"clear": "Clear",
"refresh": "Refresh",
"show": "Show",
"undo": "Undo",
"import": "Import",
"clone": "Clone",
"notAccessTitle": "You don't have permission to access"
},
"warnWhenUnsavedChanges": "Are you sure you want to leave? You have unsaved changes.",
"notifications": {
"success": "Successful",
"error": "Error (status code: {{statusCode}})",
"undoable": "You have {{seconds}} seconds to undo",
"createSuccess": "Successfully created {{resource}}",
"createError": "There was an error creating {{resource}} (status code: {{statusCode}})",
"deleteSuccess": "Successfully deleted {{resource}}",
"deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})",
"editSuccess": "Successfully edited {{resource}}",
"editError": "Error when editing {{resource}} (status code: {{statusCode}})",
"importProgress": "Importing: {{processed}}/{{total}}"
},
"loading": "Loading",
"tags": {
"clone": "Clone"
},
"dashboard": {
"title": "Dashboard"
},
"posts": {
"posts": "Posts",
"fields": {
"id": "Id",
"title": "Title",
"category": "Category",
"status": {
"title": "Status",
"published": "Published",
"draft": "Draft",
"rejected": "Rejected"
},
"content": "Content",
"createdAt": "Created At"
},
"titles": {
"create": "Create Post",
"edit": "Edit Post",
"list": "Posts",
"show": "Show Post"
}
},
"table": {
"actions": "Actions"
},
"documentTitle": {
"default": "refine",
"suffix": " | Refine",
"post": {
"list": "Posts | Refine",
"show": "#{{id}} Show Post | Refine",
"edit": "#{{id}} Edit Post | Refine",
"create": "Create new Post | Refine",
"clone": "#{{id}} Clone Post | Refine"
}
},
"autoSave": {
"success": "saved",
"error": "auto save failure",
"loading": "saving...",
"idle": "waiting for changes"
},
"undefined": {
"undefined": "No translation",
"titles": {
"list": "No translation"
}
},
"schemas": {
"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"
},
"provision_template": {
"name": "Name",
"title": "Title",
"body": "Body",
"resource_title": "Provision Template"
},
"contract_template": {
"name": "Name",
"title": "Title",
"provisions": "Provisions",
"parties": "Parties",
"variables": "Variables",
"resource_title": "Contract Template"
},
"party_template": {
"entity_id": "Party Template",
"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"
},
"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",
"draft_id": "Draft"
}
}
}

View File

@@ -0,0 +1,273 @@
{
"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": {
"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": "Titre",
"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é"
},
"provision_template": {
"name": "Nom",
"body": "Corps",
"title": "Titre",
"resource_title": "Template de Clause"
},
"contract_template": {
"name": "Nom",
"title": "Titre",
"parties": "Parties",
"provisions": "Clauses",
"variables": "Variables",
"resource_title": "Template de Contrat"
},
"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"
},
"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",
"location": "Lieu",
"date": "Date"
}
}
}

View File

@@ -1,4 +1,5 @@
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";
@@ -12,8 +13,8 @@ import routerBindings, {
UnsavedChangesNotifier, UnsavedChangesNotifier,
} from "@refinedev/react-router"; } 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";
@@ -26,6 +27,14 @@ import { FirmRoutes } from "./pages/firm";
import rpcTheme from "./theme"; 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}> <ThemeProvider theme={rpcTheme}>
@@ -36,6 +45,7 @@ function App() {
<Refine <Refine
authProvider={authProvider} authProvider={authProvider}
dataProvider={dataProvider} dataProvider={dataProvider}
i18nProvider={i18nProvider}
notificationProvider={useNotificationProvider} notificationProvider={useNotificationProvider}
routerProvider={routerBindings} routerProvider={routerBindings}
options={{ options={{
@@ -67,7 +77,7 @@ function App() {
<Routes> <Routes>
<Route <Route
element={( element={(
<Authenticated key="authenticated-routes" redirectOnFail="/auth/login" fallback={<CatchAllNavigate to="/auth/login"/>}> <Authenticated key="authenticated-routes" redirectOnFail="/login" fallback={<CatchAllNavigate to="/login"/>}>
<Outlet /> <Outlet />
</Authenticated> </Authenticated>
)} )}
@@ -75,13 +85,13 @@ 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 />

View File

@@ -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"
>
Dont have an account?
</Typography>
<MuiLink
ml="4px"
fontSize="12px"
variant="body2"
color="primary"
component={Link}
underline="none"
to="/auth/register"
fontWeight="bold"
>
Sign up
</MuiLink>
</Box>
}
/> />
); );
}; };

View File

@@ -1,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>;
}; };

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

View File

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

21
gui/rpk-gui/src/i18n.tsx Normal file
View 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: ["EN", "FR"],
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // "http/locales/{{lng}}/{{ns}}.json"
},
//saveMissing: true,
ns: ["common"],
defaultNS: "common",
fallbackLng: ["EN", "FR"],
});
export default i18n;

View File

@@ -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>
); );

View File

@@ -0,0 +1,46 @@
import validator from "@rjsf/validator-ajv8";
import Form from "@rjsf/mui";
import { RegistryFieldsType, RegistryWidgetsType, RJSFSchema, UiSchema } from "@rjsf/utils";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
import { ResourceContext } from "../contexts/ResourceContext";
import { ReactNode } from "react";
type BaseFormProps = {
schema: RJSFSchema,
resourceBasePath: string,
onSubmit?: (data: any) => void,
onChange?: (data: any) => void,
uiSchema?: UiSchema,
formData?: any,
children?: ReactNode
}
export const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget
};
export const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
}
export const BaseForm: React.FC<BaseFormProps> = (props) => {
const { schema, uiSchema, resourceBasePath, formData, children, onSubmit, onChange } = props;
return (
<ResourceContext.Provider value={{basePath: resourceBasePath}} >
<Form
schema={schema}
uiSchema={uiSchema === undefined ? {} : uiSchema}
formData={formData}
onSubmit={(e, id) => onSubmit != undefined && onSubmit(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
onChange={(e, id) => onChange != undefined && onChange(e.formData)}
children={children}
/>
</ResourceContext.Provider>
)
}

View File

@@ -1,81 +1,64 @@
import validator from "@rjsf/validator-ajv8"; import { ReactNode, useEffect, useState } from "react";
import Form from "@rjsf/mui";
import { RegistryFieldsType, RegistryWidgetsType, UiSchema } from "@rjsf/utils";
import { useEffect, useState } from "react";
import { jsonschemaProvider } from "../providers/jsonschema-provider";
import { useForm } from "@refinedev/core";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
import { CircularProgress } from "@mui/material"; import { CircularProgress } from "@mui/material";
import { ResourceContext } from "../contexts/ResourceContext"; import { useForm } from "@refinedev/core";
import { UiSchema } from "@rjsf/utils";
import { jsonschemaProvider } from "../providers/jsonschema-provider";
import { BaseForm } from "./base-form";
type CrudFormProps = { type CrudFormProps = {
schemaName: string, schemaName: string,
uiSchema?: UiSchema, uiSchema?: UiSchema,
resource: string,
resourceBasePath?: string, resourceBasePath?: string,
resource: string,
id?: string, id?: string,
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void onSuccess?: (data: any) => void,
onSuccess?: (data: any) => void defaultValue?: any,
} children?: ReactNode
const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget
};
const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
} }
export const CrudForm: React.FC<CrudFormProps> = (props) => { export const CrudForm: React.FC<CrudFormProps> = (props) => {
const { schemaName, uiSchema, resourceBasePath="" ,resource, id, onSuccess } = props; const { schemaName, uiSchema, resourceBasePath="" ,resource, id, onSuccess, defaultValue, children } = props;
const { onFinish, query, formLoading } = useForm({ const { onFinish, query, formLoading } = useForm({
resource: `${resourceBasePath}/${resource}`, resource: resourceBasePath == "" ? resource : `${resourceBasePath}/${resource}`,
action: id === undefined ? "create" : "edit", action: id === undefined ? "create" : "edit",
redirect: "show", redirect: "show",
id, id,
onMutationSuccess: (data: any) => { if (onSuccess) { onSuccess(data) } }, onMutationSuccess: (data: any) => { if (onSuccess) { onSuccess(data) } },
}); });
const schemaValue = id === undefined ? `${schemaName}Create` : `${schemaName}Update`;
const record = query?.data?.data;
const [formData, setFormData] = useState(record);
const [schema, setSchema] = useState({}); const [schema, setSchema] = useState({});
const [loading, setLoading] = useState(true); const [schemaLoading, setSchemaLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchSchema = async () => { const fetchSchema = async () => {
try { try {
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaValue); const schemaFullName = id === undefined ? `${schemaName}Create` : `${schemaName}Update`;
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaFullName);
setSchema(resourceSchema); setSchema(resourceSchema);
setLoading(false); setSchemaLoading(false);
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error('Error fetching data:', error);
setLoading(false); setSchemaLoading(false);
} }
}; };
fetchSchema(); fetchSchema();
}, []); }, []);
if(formLoading || schemaLoading) {
if(formLoading || loading) {
return <CircularProgress /> return <CircularProgress />
} }
const record = query?.data?.data || defaultValue;
return ( return (
<ResourceContext.Provider value={{basePath: resourceBasePath}} > <BaseForm
<Form schema={schema}
schema={schema} uiSchema={uiSchema}
uiSchema={uiSchema === undefined ? {} : uiSchema} formData={record}
formData={record} resourceBasePath={resourceBasePath}
onChange={(e) => setFormData(e.formData)} onSubmit={
onSubmit={(e) => onFinish(e.formData)} (data: any) => onFinish(data)
validator={validator} }
omitExtraData={true} children={children}
widgets={customWidgets} />
fields={customFields}
/>
</ResourceContext.Provider>
) )
} }

View File

@@ -1,21 +1,23 @@
import { FormContextType, RJSFSchema, UiSchema, WidgetProps } from '@rjsf/utils'; import { FormContextType, RJSFSchema, UiSchema, WidgetProps } from '@rjsf/utils';
import { Autocomplete, Button, CircularProgress, Container, Grid2, InputAdornment, Modal, TextField, Box } from "@mui/material"; import {
Autocomplete, Button, CircularProgress, Container, Grid2, InputAdornment, Modal, TextField, Box, DialogContent
} from "@mui/material";
import ClearIcon from '@mui/icons-material/Clear'; 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 } from "react"; import React, { useState, useEffect, useContext, Fragment } from "react";
import { useList, useOne } from "@refinedev/core"; import { 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";
type ForeignKeyReference = { export type ForeignKeyReference = {
resource: string, resource: string,
label: string, label?: string,
displayedFields: [string], displayedFields?: [string],
schema: string schema: string
} }
type ForeignKeySchema = RJSFSchema & { export type ForeignKeySchema = RJSFSchema & {
foreignKey?: { foreignKey?: {
reference: ForeignKeyReference reference: ForeignKeyReference
} }
@@ -95,22 +97,24 @@ const RealAutocomplete = <T = any, S extends ForeignKeySchema = ForeignKeySchema
)} )}
/> />
<Modal <Modal
open={openFormModal} open={openFormModal}
onClose={() => setOpenFormModal(false)} onClose={() => setOpenFormModal(false)}
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description" aria-describedby="modal-modal-description"
> >
<FormContainer <DialogContent>
schemaName={schema} <FormContainer
resourceBasePath={basePath} schemaName={schema}
resource={resource} resourceBasePath={basePath}
uiSchema={{}} resource={resource}
onSuccess={(data: any) => { uiSchema={{}}
setOpenFormModal(false) onSuccess={(data: any) => {
onChange(data.data.id); setOpenFormModal(false)
}} onChange(data.data.id);
/> }}
</Modal> />
</DialogContent>
</Modal>
</> </>
); );
} }
@@ -161,14 +165,16 @@ const ChosenValue = <T = any, S extends ForeignKeySchema = ForeignKeySchema, F e
aria-labelledby="modal-modal-title" aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description" aria-describedby="modal-modal-description"
> >
<FormContainer <DialogContent>
schemaName={schema} <FormContainer
resourceBasePath={basePath} schemaName={schema}
resource={resource} resourceBasePath={basePath}
uiSchema={{}} resource={resource}
id={value} uiSchema={{}}
onSuccess={() => setOpenFormModal(false)} id={value}
/> onSuccess={() => setOpenFormModal(false)}
/>
</DialogContent>
</Modal> </Modal>
</> </>
) )
@@ -199,10 +205,9 @@ type FormContainerProps = {
const FormContainer = (props: FormContainerProps) => { const FormContainer = (props: FormContainerProps) => {
const { schemaName, resourceBasePath, resource, uiSchema = {}, id = undefined, onSuccess } = props; const { schemaName, resourceBasePath, resource, uiSchema = {}, id = undefined, onSuccess } = props;
return ( return (
<Box sx={{ ...modalStyle, width: 800 }}> <Box sx={{ ...modalStyle, width: 800 }}>
<CrudForm schemaName={schemaName} resourceBasePath={resourceBasePath} resource={resource} uiSchema={uiSchema} id={id} onSuccess={onSuccess} /> <CrudForm schemaName={schemaName} resourceBasePath={resourceBasePath} resource={resource} uiSchema={uiSchema} id={id} onSuccess={(data) => onSuccess(data)} />
</Box> </Box>
) )
} }
@@ -221,12 +226,12 @@ const Preview = (props: {id: string, resource: string, basePath: string, display
return ( return (
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{displayedFields.map((field: string) => { {displayedFields.map((field: string, index: number) => {
return ( return (
<> <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: data.data[field] }} ></Container></Grid2>
</> </Fragment>
) )
})} })}
</Grid2> </Grid2>

View File

@@ -1,5 +1,5 @@
import { JSONSchema7Definition } from "json-schema";
import { RJSFSchema } from '@rjsf/utils'; import { RJSFSchema } from '@rjsf/utils';
import i18n from '../../../i18n'
const API_URL = "/api/v1"; const API_URL = "/api/v1";
@@ -19,9 +19,14 @@ const getJsonschema = async (): Promise<RJSFSchema> => {
return rawSchema; return rawSchema;
} }
function convertCamelToSnake(str: string): string {
return str.replace(/([a-zA-Z])(?=[A-Z])/g,'$1_').toLowerCase()
}
function buildResource(rawSchemas: RJSFSchema, resourceName: string) { function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
let resource; let resource;
const shortResourceName = convertCamelToSnake(resourceName.replace(/(-Input|Create|Update)$/g, ""));
resource = structuredClone(rawSchemas.components.schemas[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) {
@@ -45,6 +50,13 @@ function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
} else if (is_array(prop) && is_reference(prop.items)) { } else if (is_array(prop) && is_reference(prop.items)) {
resolveReference(rawSchemas, resource, prop.items); resolveReference(rawSchemas, resource, prop.items);
} }
if (prop.hasOwnProperty("title")) {
prop.title = i18n.t(`schemas.${shortResourceName}.${convertCamelToSnake(prop_name)}`, prop.title);
}
}
if (resource.hasOwnProperty("title")) {
resource.title = i18n.t(`schemas.${shortResourceName}.resource_title`, resource.title);
} }
return resource; return resource;
@@ -161,7 +173,6 @@ function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string)
return has_descendant(rawSchemas, resource, path); return has_descendant(rawSchemas, resource, path);
} }
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)) return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
&& path_exists( && path_exists(
rawSchemas, rawSchemas,

View File

@@ -1,4 +1,11 @@
import { Route, Routes } from "react-router"; import { Route, Routes } from "react-router";
import { CircularProgress } from "@mui/material";
import React, { useContext, useState } from "react";
import { useOne } 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";
@@ -29,6 +36,79 @@ const EditDraft = () => {
return <Edit<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} /> return <Edit<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
} }
const CreateDraft = () => { type ForeignKeySubSchema = ForeignKeySchema & {
return <New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} /> properties: { [key: string]: { foreignKey: { reference: ForeignKeyReference } } }
}
const CreateDraft = () => {
const [chosenDraft, setChosenDraft] = useState<string|null>(null)
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const templateFieldSchema: ForeignKeySubSchema = {
type: "object",
properties: {
template_id: {
type: "string",
title: "Find a template",
foreignKey: {
reference: {
resource: "templates/contracts",
schema: "ContractTemplate"
}
}
}
},
};
const templateForm = (
<BaseForm
schema={templateFieldSchema}
formData={{template_id: chosenDraft}}
resourceBasePath={resourceBasePath}
onChange={(data) => {
const { template_id } = data;
setChosenDraft(template_id);
}}
>
&nbsp;
</BaseForm>
)
if (chosenDraft !== null) {
return (
<>
{templateForm}
<CreateDraftFromTemplate template_id={chosenDraft}/>
</>
)
}
return (
<>
{templateForm}
<New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} />
</>
)
}
const CreateDraftFromTemplate = (props: { template_id: string }) => {
const { template_id } = props;
const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
const resource = "templates/contracts"
const { data, isLoading } = useOne({
resource: `${resourceBasePath}/${resource}`,
id: template_id
});
if (isLoading || data === undefined) {
return <CircularProgress />
}
let template = { ...data.data };
template.provisions = data.data.provisions.map((item: any) => {
return { provision: {type: "template", provision_template_id: item.provision_template_id} }
})
return <New<Draft> resource={`contracts/drafts`} schemaName={"ContractDraft"} defaultValue={ template }/>
} }

View File

@@ -1,8 +1,13 @@
import { CrudForm } from "../../../lib/crud/components/crud-form";
import { UiSchema } from "@rjsf/utils"; import { UiSchema } from "@rjsf/utils";
import { useParams } from "react-router"; import { useParams } from "react-router";
import { useContext } from "react"; import { useContext } from "react";
import { Button } from "@mui/material";
import DeleteIcon from '@mui/icons-material/Delete';
import SaveIcon from '@mui/icons-material/Save';
import { FirmContext } from "../../../contexts/FirmContext"; import { FirmContext } from "../../../contexts/FirmContext";
import { CrudForm } from "../../../lib/crud/components/crud-form";
import Stack from "@mui/material/Stack";
import { DeleteButton } from "@refinedev/mui";
type EditProps = { type EditProps = {
resource: string, resource: string,
@@ -17,13 +22,26 @@ const Edit = <T,>(props: EditProps) => {
const { record_id } = useParams(); const { record_id } = useParams();
return ( return (
<CrudForm <>
schemaName={schemaName} <CrudForm
uiSchema={uiSchema} schemaName={schemaName}
resourceBasePath={resourceBasePath} uiSchema={uiSchema}
resource={resource} resourceBasePath={resourceBasePath}
id={record_id} resource={resource}
/> id={record_id}
>
<Stack
direction="row"
spacing={2}
sx={{
justifyContent: "space-between",
alignItems: "center",
}}>
<Button type='submit' variant="contained" size="large"><SaveIcon />Save</Button>
<DeleteButton variant="contained" size="large" color="error" recordItemId={record_id}/>
</Stack>
</CrudForm>
</>
) )
} }

View File

@@ -7,10 +7,11 @@ type NewProps = {
resource: string, resource: string,
schemaName: string, schemaName: string,
uiSchema?: UiSchema, uiSchema?: UiSchema,
defaultValue?: any
} }
const New = <T,>(props: NewProps) => { const New = <T,>(props: NewProps) => {
const { schemaName, resource, uiSchema } = props; const { schemaName, resource, uiSchema, defaultValue } = props;
const { currentFirm } = useContext(FirmContext); const { currentFirm } = useContext(FirmContext);
const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}` const resourceBasePath = `firm/${currentFirm.instance}/${currentFirm.firm}`
@@ -20,7 +21,8 @@ const New = <T,>(props: NewProps) => {
uiSchema={uiSchema} uiSchema={uiSchema}
resourceBasePath={resourceBasePath} resourceBasePath={resourceBasePath}
resource={resource} resource={resource}
/> defaultValue={defaultValue}
/>
) )
} }

View File

@@ -9,7 +9,7 @@ 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 ? DEFAULT_LOGIN_REDIRECT : to_param
@@ -72,7 +72,7 @@ export const authProvider: AuthProvider = {
if (get_user() == null) { if (get_user() == null) {
return { return {
authenticated: false, authenticated: false,
redirectTo: "/auth/login", redirectTo: "/login",
logout: true logout: true
} }
} }
@@ -154,7 +154,7 @@ export const authProvider: AuthProvider = {
if (error?.status === 401) { if (error?.status === 401) {
forget_user(); forget_user();
return { return {
redirectTo: "/auth/login", redirectTo: "/login",
logout: true, logout: true,
error: { message: "Authentication required" }, error: { message: "Authentication required" },
} as OnErrorResponse; } as OnErrorResponse;
@@ -199,3 +199,5 @@ function findGetParameter(parameterName: string) {
}); });
return result; return result;
} }
export default authProvider;

View File

@@ -2,7 +2,7 @@ import type { DataProvider, HttpError } from "@refinedev/core";
const API_URL = "/api/v1"; const API_URL = "/api/v1";
export const dataProvider: DataProvider = { const dataProvider: DataProvider = {
getOne: async ({ resource, id, meta }) => { getOne: async ({ resource, id, meta }) => {
if (id === "") { if (id === "") {
return { data: undefined }; return { data: undefined };
@@ -96,7 +96,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: {
@@ -148,4 +148,6 @@ export const dataProvider: DataProvider = {
// deleteMany: () => { /* ... */ }, // deleteMany: () => { /* ... */ },
// updateMany: () => { /* ... */ }, // updateMany: () => { /* ... */ },
// custom: () => { /* ... */ }, // custom: () => { /* ... */ },
}; };
export default dataProvider;

11
i18n/Dockerfile Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

25
i18n/app/package.json Normal file
View 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"
}
}

View File

49
i18n/app/src/index.ts Normal file
View 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
View 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"]
}