✨(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:
Stanislas Bruhière
2025-09-02 20:13:11 +02:00
committed by GitHub
parent 38439b95ce
commit 02bce211ea
15 changed files with 645 additions and 6 deletions
+113
View File
@@ -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.",
+7
View File
@@ -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):
"""
+12 -1
View File
@@ -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>
);
@@ -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>
)
}
+22 -4
View File
@@ -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>
);
+1
View File
@@ -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";