From 02bce211ea4690a40ce5e5d99b8aefd6a7e4fe6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislas=20Bruhi=C3=A8re?= <46960549+Nastaliss@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:13:11 +0200 Subject: [PATCH] :sparkles:(frontend) add a button and modal to create maildomains (#313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles:(feat) create modal, serializer and add button * :sparkles:(frontend) refetch on domain creation * :rotating_light:(frontend) fix linter * :white_check_mark:(backend) add domain creation test * :sparkles:(frontend) add toaster for domain creation * 💬(frontend) add translation and fix small typo * ✨(front+back) add custom_attributes on domain creation * 💄(frontend) add style for create domain modal * 💬(frontend) add missing translations + add end of file line breaks * 🎨(frontend) move toaster to admin layout * 🦺(frontend) validate domain name in frontend * 💄(frontend) remove unused css * ✅(backend) add more tests * 🎨(backend) improve maildomain structure * 🎨(frontend) follow nitpicks --- src/backend/core/api/openapi.json | 113 +++++++++++++ src/backend/core/api/serializers.py | 7 + src/backend/core/api/viewsets/maildomain.py | 13 +- .../api/test_admin_maildomains_create.py | 86 ++++++++++ .../api/gen/maildomains/maildomains.ts | 99 +++++++++++ .../src/features/api/gen/models/index.ts | 2 + .../api/gen/models/mail_domain_admin_write.ts | 30 ++++ .../models/mail_domain_admin_write_request.ts | 25 +++ .../src/features/i18n/translations.json | 49 +++++- .../layouts/components/admin/admin-layout.tsx | 2 + .../domains-view/create-domain-action.tsx | 36 ++++ .../admin/modal-create-domain/_index.scss | 5 + .../admin/modal-create-domain/index.tsx | 157 ++++++++++++++++++ src/frontend/src/pages/domain/index.tsx | 26 ++- src/frontend/src/styles/main.scss | 1 + 15 files changed, 645 insertions(+), 6 deletions(-) create mode 100644 src/backend/core/tests/api/test_admin_maildomains_create.py create mode 100644 src/frontend/src/features/api/gen/models/mail_domain_admin_write.ts create mode 100644 src/frontend/src/features/api/gen/models/mail_domain_admin_write_request.ts create mode 100644 src/frontend/src/features/layouts/components/admin/domains-view/create-domain-action.tsx create mode 100644 src/frontend/src/features/layouts/components/admin/modal-create-domain/_index.scss create mode 100644 src/frontend/src/features/layouts/components/admin/modal-create-domain/index.tsx diff --git a/src/backend/core/api/openapi.json b/src/backend/core/api/openapi.json index 4d260678..050e32cf 100644 --- a/src/backend/core/api/openapi.json +++ b/src/backend/core/api/openapi.json @@ -2370,6 +2370,45 @@ "description": "" } } + }, + "post": { + "operationId": "maildomains_create", + "description": "ViewSet for listing MailDomains the user administers.\nProvides a top-level entry for mail domain administration.\nEndpoint: /maildomains/", + "tags": [ + "maildomains" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MailDomainAdminWriteRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/MailDomainAdminWriteRequest" + } + } + }, + "required": true + }, + "security": [ + { + "cookieAuth": [] + } + ], + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MailDomainAdminWrite" + } + } + }, + "description": "" + } + } } }, "/api/v1.0/maildomains/{maildomain_pk}/mailboxes/": { @@ -4336,6 +4375,80 @@ "updated_at" ] }, + "MailDomainAdminWrite": { + "type": "object", + "description": "Serialize mail domains for creating / editing admin view.", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "readOnly": true, + "description": "primary key for the record as UUID" + }, + "name": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9.-]*[a-z0-9]$", + "maxLength": 253 + }, + "created_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "title": "Created on", + "description": "date and time at which a record was created" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "readOnly": true, + "title": "Updated on", + "description": "date and time at which a record was last updated" + }, + "oidc_autojoin": { + "type": "boolean", + "description": "Create mailboxes automatically based on OIDC emails." + }, + "identity_sync": { + "type": "boolean", + "description": "Sync mailboxes to an identity provider." + }, + "custom_attributes": { + "description": "Metadata to sync to the maildomain group in the identity provider." + } + }, + "required": [ + "created_at", + "id", + "name", + "updated_at" + ] + }, + "MailDomainAdminWriteRequest": { + "type": "object", + "description": "Serialize mail domains for creating / editing admin view.", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z0-9][a-z0-9.-]*[a-z0-9]$", + "maxLength": 253 + }, + "oidc_autojoin": { + "type": "boolean", + "description": "Create mailboxes automatically based on OIDC emails." + }, + "identity_sync": { + "type": "boolean", + "description": "Sync mailboxes to an identity provider." + }, + "custom_attributes": { + "description": "Metadata to sync to the maildomain group in the identity provider." + } + }, + "required": [ + "name" + ] + }, "Mailbox": { "type": "object", "description": "Serialize mailboxes.", diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index 4f1ece85..b1a8ea3a 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -808,6 +808,13 @@ class MailDomainAdminSerializer(AbilitiesModelSerializer): """Return the abilities for the mail domain.""" return super().get_abilities(instance) +class MailDomainAdminWriteSerializer(serializers.ModelSerializer): + """Serialize mail domains for creating / editing admin view.""" + + class Meta: + model = models.MailDomain + fields = ["id", "name", "created_at", "updated_at", "oidc_autojoin", "identity_sync", "custom_attributes"] + read_only_fields = ["id", "created_at", "updated_at"] class MailboxAccessNestedUserSerializer(serializers.ModelSerializer): """ diff --git a/src/backend/core/api/viewsets/maildomain.py b/src/backend/core/api/viewsets/maildomain.py index f31364f9..e327bf70 100644 --- a/src/backend/core/api/viewsets/maildomain.py +++ b/src/backend/core/api/viewsets/maildomain.py @@ -35,7 +35,7 @@ from core.services.identity.keycloak import reset_keycloak_user_password class AdminMailDomainViewSet( - mixins.ListModelMixin, viewsets.GenericViewSet, mixins.RetrieveModelMixin + mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet ): """ ViewSet for listing MailDomains the user administers. @@ -48,6 +48,17 @@ class AdminMailDomainViewSet( core_permissions.IsSuperUser | core_permissions.IsMailDomainAdmin ] + def get_permissions(self): + if self.action == "create": + return [core_permissions.IsSuperUser()] + return super().get_permissions() + + def get_serializer_class(self): + """Select serializer based on action.""" + if self.action == "create": + return core_serializers.MailDomainAdminWriteSerializer + return super().get_serializer_class() + def get_queryset(self): user = self.request.user if not user or not user.is_authenticated: diff --git a/src/backend/core/tests/api/test_admin_maildomains_create.py b/src/backend/core/tests/api/test_admin_maildomains_create.py new file mode 100644 index 00000000..fec90e59 --- /dev/null +++ b/src/backend/core/tests/api/test_admin_maildomains_create.py @@ -0,0 +1,86 @@ +"""Tests for the MailDomain Admin API endpoints.""" +# pylint: disable=redefined-outer-name, unused-argument + +from django.urls import reverse + +import pytest +from rest_framework import status + +from core import factories, models + +pytestmark = pytest.mark.django_db + + +@pytest.fixture(name="domain_superuser_user") +def fixture_domain_superuser_user(): + """Create a user for domain superuser testing.""" + return factories.UserFactory(is_superuser=True) + + +@pytest.fixture(name="domain_admin_user") +def fixture_domain_admin_user(): + """Create a user for domain administration testing.""" + return factories.UserFactory() + + +@pytest.fixture(name="other_user") +def fixture_other_user(): + """Create another user without admin privileges.""" + return factories.UserFactory() + + +class TestAdminMailDomainsCreate: + """Tests for the MailDomain Admin API create endpoint.""" + + CREATE_DOMAIN_URL = reverse("admin-maildomains-list") + + def test_create_mail_domain_as_superuser(self, api_client, domain_superuser_user): + """Test creating a mail domain as a superuser.""" + api_client.force_authenticate(user=domain_superuser_user) + url = self.CREATE_DOMAIN_URL + data = {"name": "super-user-domain.com"} + + response = api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_201_CREATED + assert models.MailDomain.objects.filter(name="super-user-domain.com").exists() + payload = response.json() + assert payload["name"] == "super-user-domain.com" + assert "id" in payload + assert payload["oidc_autojoin"] is False + assert payload["identity_sync"] is False + + def test_create_mail_domain_as_admin(self, api_client, domain_admin_user): + """Test creating a mail domain as an admin user.""" + api_client.force_authenticate(user=domain_admin_user) + url = self.CREATE_DOMAIN_URL + data = {"name": "unauthorized-admin-domain.com"} + + response = api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_403_FORBIDDEN + assert not models.MailDomain.objects.filter(name="unauthorized-admin-domain.com").exists() + + def test_create_mail_domain_as_non_admin(self, api_client, other_user): + """Test creating a mail domain as a non-admin user.""" + api_client.force_authenticate(user=other_user) + url = self.CREATE_DOMAIN_URL + data = {"name": "unauthorized-user-domain.com"} + + response = api_client.post(url, data, format="json") + assert response.status_code == status.HTTP_403_FORBIDDEN + assert not models.MailDomain.objects.filter(name="unauthorized-user-domain.com").exists() + + def test_create_mail_domain_invalid_name(self, api_client, domain_superuser_user): + """Test creating a mail domain with an invalid name.""" + api_client.force_authenticate(user=domain_superuser_user) + # Uppercase and trailing dash should be rejected by model validator + data = {"name": "Bad-Domain-.COM"} + response = api_client.post(self.CREATE_DOMAIN_URL, data, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert not models.MailDomain.objects.filter(name__iexact="bad-domain-.com").exists() + + def test_create_mail_domain_duplicate_name(self, api_client, domain_superuser_user): + """Test creating a mail domain with a duplicate name.""" + api_client.force_authenticate(user=domain_superuser_user) + models.MailDomain.objects.create(name="dup.com") + response = api_client.post(self.CREATE_DOMAIN_URL, {"name": "dup.com"}, format="json") + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/src/frontend/src/features/api/gen/maildomains/maildomains.ts b/src/frontend/src/features/api/gen/maildomains/maildomains.ts index 5cd60e65..1c8bedfb 100644 --- a/src/frontend/src/features/api/gen/maildomains/maildomains.ts +++ b/src/frontend/src/features/api/gen/maildomains/maildomains.ts @@ -24,6 +24,8 @@ import type { import type { DNSCheckResponse, MailDomainAdmin, + MailDomainAdminWrite, + MailDomainAdminWriteRequest, MailboxAdmin, MailboxAdminCreate, MailboxAdminCreatePayloadRequest, @@ -224,6 +226,103 @@ export function useMaildomainsList< return query; } +/** + * ViewSet for listing MailDomains the user administers. +Provides a top-level entry for mail domain administration. +Endpoint: /maildomains/ + */ +export type maildomainsCreateResponse201 = { + data: MailDomainAdminWrite; + status: 201; +}; + +export type maildomainsCreateResponseComposite = maildomainsCreateResponse201; + +export type maildomainsCreateResponse = maildomainsCreateResponseComposite & { + headers: Headers; +}; + +export const getMaildomainsCreateUrl = () => { + return `/api/v1.0/maildomains/`; +}; + +export const maildomainsCreate = async ( + mailDomainAdminWriteRequest: MailDomainAdminWriteRequest, + options?: RequestInit, +): Promise => { + return fetchAPI(getMaildomainsCreateUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(mailDomainAdminWriteRequest), + }); +}; + +export const getMaildomainsCreateMutationOptions = < + TError = unknown, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: MailDomainAdminWriteRequest }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: MailDomainAdminWriteRequest }, + TContext +> => { + const mutationKey = ["maildomainsCreate"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: MailDomainAdminWriteRequest } + > = (props) => { + const { data } = props ?? {}; + + return maildomainsCreate(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type MaildomainsCreateMutationResult = NonNullable< + Awaited> +>; +export type MaildomainsCreateMutationBody = MailDomainAdminWriteRequest; +export type MaildomainsCreateMutationError = unknown; + +export const useMaildomainsCreate = ( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: MailDomainAdminWriteRequest }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { data: MailDomainAdminWriteRequest }, + TContext +> => { + const mutationOptions = getMaildomainsCreateMutationOptions(options); + + return useMutation(mutationOptions, queryClient); +}; /** * ViewSet for managing Mailboxes within a specific MailDomain. Nested under /maildomains/{maildomain_pk}/mailboxes/ diff --git a/src/frontend/src/features/api/gen/models/index.ts b/src/frontend/src/features/api/gen/models/index.ts index d5105778..c887ad2d 100644 --- a/src/frontend/src/features/api/gen/models/index.ts +++ b/src/frontend/src/features/api/gen/models/index.ts @@ -48,6 +48,8 @@ export * from "./labels_list_params"; export * from "./labels_remove_threads_create_body"; export * from "./mail_domain_admin"; export * from "./mail_domain_admin_abilities"; +export * from "./mail_domain_admin_write"; +export * from "./mail_domain_admin_write_request"; export * from "./mailbox"; export * from "./mailbox_abilities"; export * from "./mailbox_access_nested_user"; diff --git a/src/frontend/src/features/api/gen/models/mail_domain_admin_write.ts b/src/frontend/src/features/api/gen/models/mail_domain_admin_write.ts new file mode 100644 index 00000000..752c799d --- /dev/null +++ b/src/frontend/src/features/api/gen/models/mail_domain_admin_write.ts @@ -0,0 +1,30 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * messages API + * This is the messages API schema. + * OpenAPI spec version: 1.0.0 (v1.0) + */ + +/** + * Serialize mail domains for creating / editing admin view. + */ +export interface MailDomainAdminWrite { + /** primary key for the record as UUID */ + readonly id: string; + /** + * @maxLength 253 + * @pattern ^[a-z0-9][a-z0-9.-]*[a-z0-9]$ + */ + name: string; + /** date and time at which a record was created */ + readonly created_at: string; + /** date and time at which a record was last updated */ + readonly updated_at: string; + /** Create mailboxes automatically based on OIDC emails. */ + oidc_autojoin?: boolean; + /** Sync mailboxes to an identity provider. */ + identity_sync?: boolean; + /** Metadata to sync to the maildomain group in the identity provider. */ + custom_attributes?: unknown; +} diff --git a/src/frontend/src/features/api/gen/models/mail_domain_admin_write_request.ts b/src/frontend/src/features/api/gen/models/mail_domain_admin_write_request.ts new file mode 100644 index 00000000..4a2c1cfe --- /dev/null +++ b/src/frontend/src/features/api/gen/models/mail_domain_admin_write_request.ts @@ -0,0 +1,25 @@ +/** + * Generated by orval v7.10.0 🍺 + * Do not edit manually. + * messages API + * This is the messages API schema. + * OpenAPI spec version: 1.0.0 (v1.0) + */ + +/** + * Serialize mail domains for creating / editing admin view. + */ +export interface MailDomainAdminWriteRequest { + /** + * @minLength 1 + * @maxLength 253 + * @pattern ^[a-z0-9][a-z0-9.-]*[a-z0-9]$ + */ + name: string; + /** Create mailboxes automatically based on OIDC emails. */ + oidc_autojoin?: boolean; + /** Sync mailboxes to an identity provider. */ + identity_sync?: boolean; + /** Metadata to sync to the maildomain group in the identity provider. */ + custom_attributes?: unknown; +} diff --git a/src/frontend/src/features/i18n/translations.json b/src/frontend/src/features/i18n/translations.json index 4eb58aeb..c36e3701 100644 --- a/src/frontend/src/features/i18n/translations.json +++ b/src/frontend/src/features/i18n/translations.json @@ -370,6 +370,26 @@ "default": "An error occurred while creating the address." } }, + "create_domain_modal": { + "title": "Add a domain", + "form": { + "labels": { + "name": "Name", + "oidc_autojoin": "Automatically create mailboxes according to OIDC emails", + "identity_sync": "Synchronize mailboxes with an identity provider" + }, + "placeholders": { + "name": "Domain name..." + }, + "errors": { + "name_required": "Name is required.", + "name_invalid": "Name must be a valid domain name." + } + }, + "api_errors": { + "default": "An error occurred while creating the domain." + } + }, "manage_accesses_modal": { "title": "Manage accesses", "roles": { @@ -398,6 +418,11 @@ "created_at": "Created at", "updated_at": "Updated at" }, + "actions": { + "manage_access": "Manage access", + "new_domain": "New domain" + }, + "creation_success": "The domain {{domain}} has been created successfully.", "loading": "Loading maildomains...", "loading_error": "An error occurred while loading maildomains." }, @@ -823,6 +848,26 @@ "default": "Une erreur est survenue lors de la création de l'adresse." } }, + "create_domain_modal": { + "title": "Ajout d'un domaine", + "form": { + "labels": { + "name": "Nom", + "oidc_autojoin": "Créer les boîtes aux lettres automatiquement selon les emails OIDC", + "identity_sync": "Synchroniser les boîtes aux lettres avec un fournisseur d'identité" + }, + "placeholders": { + "name": "Nom du domaine..." + }, + "errors": { + "name_required": "Le nom est requis.", + "name_invalid": "Le nom doit être un nom de domaine valide." + } + }, + "api_errors": { + "default": "Une erreur est survenue lors de la création du domaine." + } + }, "manage_accesses_modal": { "title": "Gérer les accès à {{mailbox}}", "roles": { @@ -852,8 +897,10 @@ "updated_at": "Modifié le" }, "actions": { - "manage_access": "Gérer les accès" + "manage_access": "Gérer les accès", + "new_domain": "Nouveau domaine" }, + "creation_success": "Le domaine {{domain}} a été créé avec succès.", "loading": "Chargement des domaines...", "loading_error": "Une erreur est survenue lors du chargement des domaines." }, diff --git a/src/frontend/src/features/layouts/components/admin/admin-layout.tsx b/src/frontend/src/features/layouts/components/admin/admin-layout.tsx index 7b04a929..835054e8 100644 --- a/src/frontend/src/features/layouts/components/admin/admin-layout.tsx +++ b/src/frontend/src/features/layouts/components/admin/admin-layout.tsx @@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"; import { AdminMailDomainProvider, useAdminMailDomain } from "@/features/providers/admin-maildomain"; import useAbility, { Abilities } from "@/hooks/use-ability"; import ErrorPage from "next/error"; +import { Toaster } from "@/features/ui/components/toaster"; type AdminLayoutProps = { children: React.ReactNode; @@ -122,6 +123,7 @@ export function AdminLayout(props: AdminLayoutProps) { > + ); diff --git a/src/frontend/src/features/layouts/components/admin/domains-view/create-domain-action.tsx b/src/frontend/src/features/layouts/components/admin/domains-view/create-domain-action.tsx new file mode 100644 index 00000000..6c080f60 --- /dev/null +++ b/src/frontend/src/features/layouts/components/admin/domains-view/create-domain-action.tsx @@ -0,0 +1,36 @@ +import { MailDomainAdminWrite } from "@/features/api/gen"; +import { ModalCreateDomain } from "@/features/layouts/components/admin/modal-create-domain"; +import useAbility, { Abilities } from "@/hooks/use-ability"; +import { Button, useModal } from "@openfun/cunningham-react"; +import { useTranslation } from "react-i18next"; + +type CreateDomainActionProps = { + onCreate: (createdDomain: MailDomainAdminWrite) => void; +} + +/** + * Action button to create a new domain. + * Only visible if the user has the ability to manage domains. + */ +export const CreateDomainAction = ({ onCreate }: CreateDomainActionProps) => { + const modal = useModal(); + const { t } = useTranslation(); + const canCreateDomains = useAbility(Abilities.CAN_CREATE_MAILDOMAINS); + + if (!canCreateDomains) { + return null; + } + + return ( + <> + + + + ) +} diff --git a/src/frontend/src/features/layouts/components/admin/modal-create-domain/_index.scss b/src/frontend/src/features/layouts/components/admin/modal-create-domain/_index.scss new file mode 100644 index 00000000..6bf502ef --- /dev/null +++ b/src/frontend/src/features/layouts/components/admin/modal-create-domain/_index.scss @@ -0,0 +1,5 @@ +.modal-create-domain { + .form-field-row { + margin-bottom: var(--c--theme--spacings--s); + } +} diff --git a/src/frontend/src/features/layouts/components/admin/modal-create-domain/index.tsx b/src/frontend/src/features/layouts/components/admin/modal-create-domain/index.tsx new file mode 100644 index 00000000..d0709aed --- /dev/null +++ b/src/frontend/src/features/layouts/components/admin/modal-create-domain/index.tsx @@ -0,0 +1,157 @@ +import { Button, Modal, ModalSize } from '@openfun/cunningham-react'; +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next'; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { FieldErrors, FormProvider, useForm } from 'react-hook-form'; +import { MailDomainAdminWrite, useMaildomainsCreate } from '@/features/api/gen'; +import { Banner } from '@/features/ui/components/banner'; +import { RhfInput } from '@/features/forms/components/react-hook-form'; +import { RhfCheckbox } from '@/features/forms/components/react-hook-form/rhf-checkbox'; +import { useConfig } from '@/features/providers/config'; +import { convertJsonSchemaToZod } from '@/features/forms/components/zod-json-schema-serializer'; +import { JSONSchema } from 'zod/v4/core'; +import { ItemJsonSchema } from '@/features/forms/components/zod-json-schema-serializer'; +import { RhfJsonSchemaField } from '@/features/forms/components/react-hook-form/rhf-json-schema-field'; + +type ModalCreateDomainProps = { + isOpen: boolean; + onClose: () => void; + onCreate: (createdDomain: MailDomainAdminWrite) => void; +} + +export const ModalCreateDomain = ({ isOpen, onClose, onCreate }: ModalCreateDomainProps) => { + + const { t } = useTranslation(); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN } = useConfig(); + const { mutateAsync: createDomain } = useMaildomainsCreate(); + + const createDomainSchema = z.object({ + name: z.string() + .min(1, { error: "create_domain_modal.form.errors.name_required" }) + .regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/, { message: "create_domain_modal.form.errors.name_invalid" }), + oidc_autojoin: z.boolean(), + identity_sync: z.boolean(), + ...convertJsonSchemaToZod(SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN as JSONSchema.Schema) + }) + + type CreateDomainFormData = z.infer; + + const customAttributes = SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN?.properties ?? {}; + const form = useForm({ + resolver: zodResolver(createDomainSchema), + defaultValues: { + name: '', + oidc_autojoin: false, + identity_sync: false, + ...Object.fromEntries(Object.entries(customAttributes).map(([name, schema]) => ([name, schema.default ?? '']))), + }, + }); + + const { handleSubmit } = form; + + + const handleClose = () => { + form.reset(); + setError(null); + onClose(); + }; + + const getFieldError = (fieldName: keyof CreateDomainFormData) => { + const errors = form.formState.errors as FieldErrors; + const error = errors?.[fieldName as keyof typeof errors]; + return error?.message ? t(error.message as string) : undefined; + } + + const onSubmit = async (data: CreateDomainFormData) => { + setError(null); + setIsSubmitting(true); + try { + const customAttributeKeys = Object.keys(SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN?.properties ?? {}); + const response = await createDomain({data: { + name: data.name, + oidc_autojoin: data.oidc_autojoin, + identity_sync: data.identity_sync, + custom_attributes: Object.fromEntries( + Object.entries(data).filter(([key]) => customAttributeKeys.includes(key)) + ) + }}); + onCreate(response.data); + handleClose(); + + } catch { + setError("create_domain_modal.api_errors.default"); + } finally { + setIsSubmitting(false); + } + }; + + return ( + +
+ +
+ {error && ( + + {t(error)} + + )} +
+ +
+ { + Object.entries(SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN?.properties ?? {}).map(([name, schema]: [string, ItemJsonSchema]) => ( +
+ +
+ )) + } +
+ +
+
+ +
+ +
+ +
+
+
+
+
+ ) +} + diff --git a/src/frontend/src/pages/domain/index.tsx b/src/frontend/src/pages/domain/index.tsx index 3b50dd99..172c2375 100644 --- a/src/frontend/src/pages/domain/index.tsx +++ b/src/frontend/src/pages/domain/index.tsx @@ -1,14 +1,17 @@ import { useEffect } from "react"; import { DataGrid, usePagination } from "@openfun/cunningham-react"; import { useRouter } from "next/router"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { Spinner } from "@gouvfr-lasuite/ui-kit"; import { AdminLayout } from "@/features/layouts/components/admin/admin-layout"; import Bar from "@/features/ui/components/bar"; -import { MailDomainAdmin } from "@/features/api/gen"; +import { getMaildomainsListQueryOptions, MailDomainAdmin, MailDomainAdminWrite } from "@/features/api/gen"; import { useAdminMailDomain } from "@/features/providers/admin-maildomain"; import useAbility, { Abilities } from "@/hooks/use-ability"; import { Banner } from "@/features/ui/components/banner"; +import { CreateDomainAction } from "@/features/layouts/components/admin/domains-view/create-domain-action"; +import { useQueryClient } from "@tanstack/react-query"; +import { addToast, ToasterItem } from "@/features/ui/components/toaster"; type AdminDataGridProps = { pagination: ReturnType; @@ -18,7 +21,6 @@ type AdminDataGridProps = { function AdminDataGrid({ domains, pagination }: AdminDataGridProps) { const router = useRouter(); const { t, i18n } = useTranslation(); - const columns = [ { id: "name", @@ -104,8 +106,24 @@ const AdminPageContent = () => { * Admin page which list all mail domains. */ export default function AdminPage() { + const queryClient = useQueryClient(); + + const handleCreateDomain = (domain: MailDomainAdminWrite) => { + queryClient.invalidateQueries({ + queryKey: getMaildomainsListQueryOptions().queryKey, + exact: false, + }); + addToast( + + }} /> + , { + toastId: `create-domain-success:${domain.id}`, + } + ) + }; + return ( - + }> ); diff --git a/src/frontend/src/styles/main.scss b/src/frontend/src/styles/main.scss index 23b9124c..3b659a56 100644 --- a/src/frontend/src/styles/main.scss +++ b/src/frontend/src/styles/main.scss @@ -46,6 +46,7 @@ @use "./../features/forms/components/search-filters-form"; @use "./../features/controlled-modals/message-importer"; @use "./../features/layouts/components/admin/modal-create-address"; +@use "./../features/layouts/components/admin/modal-create-domain"; @use "./../features/forms/components/combobox"; @use "./../features/layouts/components/mailbox-panel/components/mailbox-labels/components/label-form-modal/components/color-palette-field";