'use client' /** * This Source Code is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright (c) Infonomic Company Limited */ /** * Role details drawer form. * * Mirrors the admin-users update pattern: TanStack Form + Zod, blur-then- * change validation, diff-against-original patch, optimistic concurrency * via `vid`. `machine_name` is read-only here — it is captured at create * time and immutable thereafter. */ import { useMemo, useState } from 'react' import { revalidateLogic, useForm } from '@tanstack/react-form-start' import { useTranslation } from '@byline/i18n/react' import { Alert, Button, Input, LoaderEllipsis, TextArea } from '@byline/ui/react' import cx from 'clsx' import { z } from 'zod' import { useBylineAdminServices } from '../../../services/admin-services-context.js' import styles from './update.module.css' import type { UpdateAdminRoleInput } from '../../../services/admin-services-types.js' import type { AdminRoleResponse } from '../index.js' const MAX_NAME = 128 const MAX_DESCRIPTION = 2000 type UpdateRoleValues = { name: string description: string } function defaultsFrom(role: AdminRoleResponse): UpdateRoleValues { return { name: role.name, description: role.description ?? '', } } /** Build a patch object containing only fields whose values differ from the original role. */ function buildPatch( values: UpdateRoleValues, role: AdminRoleResponse ): UpdateAdminRoleInput['patch'] { const patch: UpdateAdminRoleInput['patch'] = {} const normaliseText = (value: string): string | null => (value.trim().length > 0 ? value : null) const nextDescription = normaliseText(values.description) const nextName = values.name.trim() if (nextName !== role.name) patch.name = nextName if (nextDescription !== role.description) patch.description = nextDescription return patch } interface UpdateRoleProps { role: AdminRoleResponse onClose?: () => void onSuccess?: (role: AdminRoleResponse) => void } export function UpdateRole({ role, onClose, onSuccess }: UpdateRoleProps) { const { updateAdminRole } = useBylineAdminServices() const { t } = useTranslation('byline-admin') const [formError, setFormError] = useState(null) const [successMessage, setSuccessMessage] = useState(null) const updateRoleSchema = useMemo( () => z.object({ name: z .string() .min(1, t('adminRoles.create.errors.nameRequired')) .max(MAX_NAME, t('adminRoles.create.errors.nameTooLong', { max: MAX_NAME })), description: z .string() .max( MAX_DESCRIPTION, t('adminRoles.create.errors.descriptionTooLong', { max: MAX_DESCRIPTION }) ), }), [t] ) const form = useForm({ defaultValues: defaultsFrom(role), validationLogic: revalidateLogic({ mode: 'blur', modeAfterSubmission: 'change', }), validators: { onDynamic: updateRoleSchema, }, onSubmit: async ({ value }) => { setFormError(null) setSuccessMessage(null) const patch = buildPatch(value, role) if (Object.keys(patch).length === 0) { setSuccessMessage(t('common.feedback.noChanges')) return } try { const updated = await updateAdminRole({ data: { id: role.id, vid: role.vid, patch }, }) setSuccessMessage(t('common.feedback.saved')) onSuccess?.(updated) } catch (err) { const code = getErrorCode(err) if (code === 'admin.roles.versionConflict') { setFormError(t('adminRoles.update.errors.versionConflict')) return } if (code === 'admin.roles.notFound') { setFormError(t('adminRoles.update.errors.notFound')) return } setFormError(t('common.errors.couldNotSave')) } }, }) return (
{ event.preventDefault() event.stopPropagation() void form.handleSubmit() }} className={cx('byline-role-update-form', styles.form)} > {formError ? {formError} : null} {successMessage ? {successMessage} : null} {(field) => ( field.handleChange(e.currentTarget.value)} error={field.state.meta.errors.length > 0} errorText={firstError(field.state.meta.errors)} required /> )} {(field) => (