mirror of
https://github.com/suitenumerique/messages.git
synced 2026-09-26 19:54:54 +02:00
✨(frontend) add a button and modal to create maildomains (#313)
* ✨(feat) create modal, serializer and add button * ✨(frontend) refetch on domain creation * 🚨(frontend) fix linter * ✅(backend) add domain creation test * ✨(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
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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<maildomainsCreateResponse> => {
|
||||
return fetchAPI<maildomainsCreateResponse>(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<ReturnType<typeof maildomainsCreate>>,
|
||||
TError,
|
||||
{ data: MailDomainAdminWriteRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof maildomainsCreate>>,
|
||||
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<ReturnType<typeof maildomainsCreate>>,
|
||||
{ data: MailDomainAdminWriteRequest }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return maildomainsCreate(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type MaildomainsCreateMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof maildomainsCreate>>
|
||||
>;
|
||||
export type MaildomainsCreateMutationBody = MailDomainAdminWriteRequest;
|
||||
export type MaildomainsCreateMutationError = unknown;
|
||||
|
||||
export const useMaildomainsCreate = <TError = unknown, TContext = unknown>(
|
||||
options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof maildomainsCreate>>,
|
||||
TError,
|
||||
{ data: MailDomainAdminWriteRequest },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof fetchAPI>;
|
||||
},
|
||||
queryClient?: QueryClient,
|
||||
): UseMutationResult<
|
||||
Awaited<ReturnType<typeof maildomainsCreate>>,
|
||||
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/
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 <strong>{{domain}}</strong> 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 <strong>{{domain}}</strong> a été créé avec succès.",
|
||||
"loading": "Chargement des domaines...",
|
||||
"loading_error": "Une erreur est survenue lors du chargement des domaines."
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
>
|
||||
<AdminMailDomainProvider>
|
||||
<AdminLayoutContent {...props} />
|
||||
<Toaster />
|
||||
</AdminMailDomainProvider>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
+36
@@ -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 (
|
||||
<>
|
||||
<Button color="primary" onClick={modal.open}>
|
||||
{t("admin_maildomains_list.actions.new_domain")}
|
||||
</Button>
|
||||
<ModalCreateDomain
|
||||
isOpen={modal.isOpen}
|
||||
onClose={modal.close}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.modal-create-domain {
|
||||
.form-field-row {
|
||||
margin-bottom: var(--c--theme--spacings--s);
|
||||
}
|
||||
}
|
||||
@@ -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<string | null>(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<typeof createDomainSchema>;
|
||||
|
||||
const customAttributes = SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN?.properties ?? {};
|
||||
const form = useForm<CreateDomainFormData>({
|
||||
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<CreateDomainFormData>;
|
||||
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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={t('create_domain_modal.title')}
|
||||
size={ModalSize.LARGE}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="modal-create-domain">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)} noValidate>
|
||||
{error && (
|
||||
<Banner type="error">
|
||||
{t(error)}
|
||||
</Banner>
|
||||
)}
|
||||
<div className="form-field-row">
|
||||
<RhfInput
|
||||
name="name"
|
||||
label={t('create_domain_modal.form.labels.name')}
|
||||
text={getFieldError('name')}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
Object.entries(SCHEMA_CUSTOM_ATTRIBUTES_MAILDOMAIN?.properties ?? {}).map(([name, schema]: [string, ItemJsonSchema]) => (
|
||||
<div className="form-field-row" key={`json-schema-field-${name}`}>
|
||||
<RhfJsonSchemaField
|
||||
schema={schema}
|
||||
text={getFieldError(name as keyof CreateDomainFormData)}
|
||||
name={name}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<div className="form-field-row">
|
||||
<RhfCheckbox
|
||||
name="oidc_autojoin"
|
||||
label={t('create_domain_modal.form.labels.oidc_autojoin')}
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-field-row">
|
||||
<RhfCheckbox
|
||||
name="identity_sync"
|
||||
label={t('create_domain_modal.form.labels.identity_sync')}
|
||||
type="checkbox"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
>
|
||||
{isSubmitting ? t('actions.creating') : t('actions.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<typeof usePagination>;
|
||||
@@ -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(
|
||||
<ToasterItem>
|
||||
<Trans i18nKey="admin_maildomains_list.creation_success" values={{ domain: domain.name }} components={{ strong: <strong /> }} />
|
||||
</ToasterItem>, {
|
||||
toastId: `create-domain-success:${domain.id}`,
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AdminLayout actions={<CreateDomainAction onCreate={handleCreateDomain} />}>
|
||||
<AdminPageContent />
|
||||
</AdminLayout>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user