mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): EditAccout form complete
This commit is contained in:
parent
494fcc0f9a
commit
5c7bf224c3
@ -1,24 +1,64 @@
|
|||||||
import { FC } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
|
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
|
||||||
import { ButtonComponent, CheckBoxComponent, TextBoxComponent } from '../components/editors'
|
import { ButtonComponent, CheckBoxComponent, TextBoxComponent } from '../components/editors'
|
||||||
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
|
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
|
||||||
import { useFormState } from '../hooks/useFormState'
|
import { useFormState } from '../hooks/useFormState'
|
||||||
import { boolean, object, Schema, string } from 'zod'
|
import { array, boolean, object, Schema, string } from 'zod'
|
||||||
|
import { PlusIcon, TrashIcon } from 'lucide-react'
|
||||||
|
import { getData, patchData } from '../axiosConfig'
|
||||||
|
import { ApiRoutes, GetApiRoute } from '../AppMap'
|
||||||
|
import { FieldContainer } from '../components/editors/FieldContainer'
|
||||||
|
import { deepCopy, deepDelta, deltaHasOperations } from '../functions'
|
||||||
|
import { PatchAccountRequest, PatchAccountRequestSchema } from '../models/letsEncryptServer/account/requests/PatchAccountRequest'
|
||||||
|
import { addToast } from '../components/Toast/addToast'
|
||||||
|
|
||||||
|
|
||||||
|
interface EditAccountHostnameFormProps {
|
||||||
|
isDisabled: boolean
|
||||||
|
hostname: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditAccountHostnameFormProto = (): EditAccountHostnameFormProps => ({
|
||||||
|
isDisabled: false,
|
||||||
|
hostname: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const EditAccountHostnameFormSchema: Schema<EditAccountHostnameFormProps> = object({
|
||||||
|
hostname: string(),
|
||||||
|
isDisabled: boolean()
|
||||||
|
})
|
||||||
|
|
||||||
interface EditAccountFormProps {
|
interface EditAccountFormProps {
|
||||||
|
isDisabled: boolean
|
||||||
description: string
|
description: string
|
||||||
disabled: boolean
|
|
||||||
|
contact: string
|
||||||
|
contacts: string[]
|
||||||
|
|
||||||
|
hostname: string,
|
||||||
|
hostnames: EditAccountHostnameFormProps[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegisterFormProto = (): EditAccountFormProps => ({
|
const RegisterFormProto = (): EditAccountFormProps => ({
|
||||||
|
isDisabled: false,
|
||||||
description: '',
|
description: '',
|
||||||
disabled: false
|
|
||||||
|
contact: '',
|
||||||
|
contacts: [],
|
||||||
|
|
||||||
|
hostname: '',
|
||||||
|
hostnames: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const RegisterFormSchema: Schema<EditAccountFormProps> = object({
|
const RegisterFormSchema: Schema<EditAccountFormProps> = object({
|
||||||
|
isDisabled: boolean(),
|
||||||
description: string(),
|
description: string(),
|
||||||
disabled: boolean()
|
|
||||||
|
contact: string(),
|
||||||
|
contacts: array(string()),
|
||||||
|
|
||||||
|
hostname: string(),
|
||||||
|
hostnames: array(EditAccountHostnameFormSchema)
|
||||||
})
|
})
|
||||||
|
|
||||||
interface EditAccountProps {
|
interface EditAccountProps {
|
||||||
@ -41,14 +81,88 @@ const EditAccount: FC<EditAccountProps> = (props) => {
|
|||||||
formState,
|
formState,
|
||||||
errors,
|
errors,
|
||||||
formIsValid,
|
formIsValid,
|
||||||
handleInputChange
|
handleInputChange,
|
||||||
|
setInitialState
|
||||||
} = useFormState<EditAccountFormProps>({
|
} = useFormState<EditAccountFormProps>({
|
||||||
initialState: RegisterFormProto(),
|
initialState: RegisterFormProto(),
|
||||||
validationSchema: RegisterFormSchema
|
validationSchema: RegisterFormSchema
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [backupState, setBackupState] = useState<EditAccountFormProps>(RegisterFormProto())
|
||||||
|
|
||||||
|
const handleInitialization = useCallback((response: GetAccountResponse) => {
|
||||||
|
const newState = {
|
||||||
|
...RegisterFormProto(),
|
||||||
|
isDisabled: response.isDisabled,
|
||||||
|
description: response.description,
|
||||||
|
contacts: response.contacts,
|
||||||
|
hostnames: (response.hostnames ?? []).map(h => ({
|
||||||
|
...EditAccountHostnameFormProto(),
|
||||||
|
isDisabled: h.isDisabled,
|
||||||
|
hostname: h.hostname
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitialState(newState)
|
||||||
|
setBackupState(deepCopy(newState))
|
||||||
|
}, [setInitialState])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getData<GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_GET).route
|
||||||
|
.replace('{accountId}', accountId)
|
||||||
|
).then((response) => {
|
||||||
|
if (!response) return
|
||||||
|
|
||||||
|
handleInitialization(response)
|
||||||
|
})
|
||||||
|
}, [accountId, handleInitialization])
|
||||||
|
|
||||||
|
|
||||||
|
const mapFormStateToPatchRequest = (formState: EditAccountFormProps) : PatchAccountRequest => {
|
||||||
|
const formStateCopy = deepCopy(formState)
|
||||||
|
|
||||||
|
const patchRequest: PatchAccountRequest = {
|
||||||
|
isDisabled: formStateCopy.isDisabled,
|
||||||
|
description: formStateCopy.description,
|
||||||
|
contacts: formStateCopy.contacts,
|
||||||
|
hostnames: formStateCopy.hostnames.map(h => ({
|
||||||
|
hostname: h.hostname
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return patchRequest
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
// onSubmitted && onSubmitted(updatedEntity)
|
if (!formIsValid) return
|
||||||
|
|
||||||
|
const fromFormState = mapFormStateToPatchRequest(formState)
|
||||||
|
const fromBackupState = mapFormStateToPatchRequest(backupState)
|
||||||
|
|
||||||
|
const delta = deepDelta(fromBackupState, fromFormState)
|
||||||
|
|
||||||
|
if (!deltaHasOperations(delta)) {
|
||||||
|
addToast('No changes detected', 'info')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = PatchAccountRequestSchema.safeParse(delta)
|
||||||
|
if (!request.success) {
|
||||||
|
request.error.issues.forEach(error => {
|
||||||
|
addToast(error.message, 'error')
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
patchData<PatchAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_PATCH).route
|
||||||
|
.replace('{accountId}', accountId), delta
|
||||||
|
).then((response) => {
|
||||||
|
if (!response) return
|
||||||
|
|
||||||
|
handleInitialization(response)
|
||||||
|
onSubmitted?.(response)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
@ -70,13 +184,100 @@ const EditAccount: FC<EditAccountProps> = (props) => {
|
|||||||
<CheckBoxComponent
|
<CheckBoxComponent
|
||||||
colspan={12}
|
colspan={12}
|
||||||
label={'Disabled'}
|
label={'Disabled'}
|
||||||
value={formState.disabled}
|
value={formState.isDisabled}
|
||||||
onChange={(e) => handleInputChange('disabled', e.target.checked)}
|
onChange={(e) => handleInputChange('isDisabled', e.target.checked)}
|
||||||
errorText={errors.disabled}
|
errorText={errors.isDisabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 className={'col-span-12'}>Contacts:</h3>
|
<h3 className={'col-span-12'}>Contacts:</h3>
|
||||||
|
<ul className={'col-span-12'}>
|
||||||
|
{formState.contacts
|
||||||
|
.map((contact) => (
|
||||||
|
<li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
||||||
|
<div className={'col-span-10'}>
|
||||||
|
{contact}
|
||||||
|
</div>
|
||||||
|
<ButtonComponent
|
||||||
|
colspan={2}
|
||||||
|
onClick={() => {
|
||||||
|
const updatedContacts = formState.contacts.filter(c => c !== contact)
|
||||||
|
handleInputChange('contacts', updatedContacts)
|
||||||
|
}}
|
||||||
|
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</ButtonComponent>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<TextBoxComponent
|
||||||
|
colspan={10}
|
||||||
|
label={'New Contact'}
|
||||||
|
value={formState.contact}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (formState.contacts.includes(e.target.value))
|
||||||
|
return
|
||||||
|
|
||||||
|
handleInputChange('contact', e.target.value)
|
||||||
|
}}
|
||||||
|
placeholder={'Add contact'}
|
||||||
|
type={'text'}
|
||||||
|
errorText={errors.contact}
|
||||||
|
/>
|
||||||
|
<FieldContainer colspan={2}>
|
||||||
|
<ButtonComponent
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange('contacts', [...formState.contacts, formState.contact])
|
||||||
|
handleInputChange('contact', '')
|
||||||
|
}}
|
||||||
|
disabled={formState.contact.trim() === ''}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</ButtonComponent>
|
||||||
|
</FieldContainer>
|
||||||
|
<h3 className={'col-span-12'}>Hostnames:</h3>
|
||||||
|
<ul className={'col-span-12'}>
|
||||||
|
{formState.hostnames.map((hostname) => (
|
||||||
|
<li key={hostname.hostname} className={'grid grid-cols-12 gap-4 w-full'}>
|
||||||
|
<span className={'col-span-10'}>{hostname.hostname}</span>
|
||||||
|
<ButtonComponent
|
||||||
|
colspan={2}
|
||||||
|
onClick={() => {
|
||||||
|
const updatedHostnames = formState.hostnames.filter(h => h !== hostname)
|
||||||
|
handleInputChange('hostnames', updatedHostnames)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</ButtonComponent>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<TextBoxComponent
|
||||||
|
colspan={10}
|
||||||
|
label={'New Hostname'}
|
||||||
|
value={formState.hostname}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (formState.hostnames.find(h => h.hostname === e.target.value))
|
||||||
|
return
|
||||||
|
|
||||||
|
handleInputChange('hostname', e.target.value)
|
||||||
|
}}
|
||||||
|
placeholder={'Add hostname'}
|
||||||
|
type={'text'}
|
||||||
|
errorText={errors.hostname}
|
||||||
|
/>
|
||||||
|
<FieldContainer colspan={2}>
|
||||||
|
<ButtonComponent
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange('hostnames', [...formState.hostnames, formState.hostname])
|
||||||
|
handleInputChange('hostname', '')
|
||||||
|
}}
|
||||||
|
disabled={formState.hostname.trim() === ''}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</ButtonComponent>
|
||||||
|
</FieldContainer>
|
||||||
</div>
|
</div>
|
||||||
</FormContent>
|
</FormContent>
|
||||||
<FormFooter
|
<FormFooter
|
||||||
|
|||||||
@ -137,13 +137,14 @@ const Home: FC = () => {
|
|||||||
: 'Not Upcoming'}
|
: 'Not Upcoming'}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<CheckBoxComponent
|
<span className={'col-span-3'}>
|
||||||
colspan={3}
|
<label className={'mr-2'}>Disabled:</label>
|
||||||
value={hostname.isDisabled}
|
<input
|
||||||
label={'Disabled'}
|
type={'checkbox'}
|
||||||
disabled={true}
|
checked={hostname.isDisabled}
|
||||||
/>
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import { PatchOperation } from './PatchOperation'
|
|
||||||
|
|
||||||
export interface PatchAction<T> {
|
|
||||||
op: PatchOperation // Enum for operation type
|
|
||||||
index?: number // Index for the operation (for arrays/lists)
|
|
||||||
value?: T // Value for the operation
|
|
||||||
}
|
|
||||||
@ -1,6 +1,22 @@
|
|||||||
export enum PatchOperation {
|
export enum PatchOperation {
|
||||||
Add,
|
|
||||||
Remove,
|
/// <summary>
|
||||||
Replace,
|
/// When you need to set or replace a normal field
|
||||||
None
|
/// </summary>
|
||||||
|
SetField,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When you need to set a normal field to null
|
||||||
|
/// </summary>
|
||||||
|
RemoveField,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When you need to add an item to a collection
|
||||||
|
/// </summary>
|
||||||
|
AddToCollection,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When you need to remove an item from a collection
|
||||||
|
/// </summary>
|
||||||
|
RemoveFromCollection,
|
||||||
}
|
}
|
||||||
14
src/MaksIT.WebUI/src/models/PatchRequestModelBase.ts
Normal file
14
src/MaksIT.WebUI/src/models/PatchRequestModelBase.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
import z, { object, record, string } from 'zod'
|
||||||
|
import { RequestModelBase, RequestModelBaseSchema } from './RequestModelBase'
|
||||||
|
import { PatchOperation } from './PatchOperation'
|
||||||
|
|
||||||
|
export interface PatchRequestModelBase extends RequestModelBase {
|
||||||
|
operations?: { [key: string]: PatchOperation }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PatchRequestModelBaseSchema = RequestModelBaseSchema.and(
|
||||||
|
object({
|
||||||
|
operations: record(string(), z.enum(PatchOperation)).optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
9
src/MaksIT.WebUI/src/models/RequestModelBase.ts
Normal file
9
src/MaksIT.WebUI/src/models/RequestModelBase.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { object, Schema } from 'zod'
|
||||||
|
|
||||||
|
export interface RequestModelBase {
|
||||||
|
[key: string]: unknown; // Add index signature
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RequestModelBaseSchema: Schema<RequestModelBase> = object({
|
||||||
|
// Define the schema for the base request model
|
||||||
|
})
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export interface ResponseModelBase {
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
@ -1,9 +1,20 @@
|
|||||||
import { PatchAction } from '@/models/PatchAction'
|
import { PatchHostnameRequest, PatchHostnameRequestSchema } from './PatchHostnameRequest'
|
||||||
import { PatchHostnameRequest } from './PatchHostnameRequest'
|
import { PatchRequestModelBase, PatchRequestModelBaseSchema } from '../../../PatchRequestModelBase'
|
||||||
|
import { array, boolean, object, Schema, string } from 'zod'
|
||||||
|
|
||||||
export interface PatchAccountRequest {
|
export interface PatchAccountRequest extends PatchRequestModelBase {
|
||||||
description?: PatchAction<string>
|
description?: string
|
||||||
isDisabled?: PatchAction<boolean>
|
isDisabled?: boolean
|
||||||
contacts?: PatchAction<string>[]
|
contacts?: string[]
|
||||||
hostnames?: PatchHostnameRequest[]
|
hostnames?: PatchHostnameRequest[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PatchAccountRequestSchema: Schema<PatchAccountRequest> = PatchRequestModelBaseSchema.and(
|
||||||
|
object({
|
||||||
|
description: string().optional(),
|
||||||
|
isDisabled: boolean().optional(),
|
||||||
|
contacts: array(string()).optional(),
|
||||||
|
hostnames: array(PatchHostnameRequestSchema).optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import { PatchAction } from '@/models/PatchAction'
|
import { boolean, object, Schema, string } from 'zod'
|
||||||
|
import { PatchRequestModelBase, PatchRequestModelBaseSchema } from '../../../PatchRequestModelBase'
|
||||||
|
|
||||||
export interface PatchHostnameRequest {
|
export interface PatchHostnameRequest extends PatchRequestModelBase {
|
||||||
hostname?: PatchAction<string>
|
hostname?: string
|
||||||
isDisabled?: PatchAction<boolean>
|
isDisabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PatchHostnameRequestSchema: Schema<PatchHostnameRequest> = PatchRequestModelBaseSchema.and(
|
||||||
|
object({
|
||||||
|
hostname: string().optional(),
|
||||||
|
isDisabled: boolean().optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import z, { array, boolean, object, Schema, string } from 'zod'
|
import z, { array, boolean, object, Schema, string } from 'zod'
|
||||||
import { ChallengeType } from '../../../../entities/ChallengeType'
|
import { ChallengeType } from '../../../../entities/ChallengeType'
|
||||||
|
import { RequestModelBase, RequestModelBaseSchema } from '../../../RequestModelBase'
|
||||||
|
|
||||||
export interface PostAccountRequest {
|
export interface PostAccountRequest extends RequestModelBase {
|
||||||
description: string
|
description: string
|
||||||
contacts: string[]
|
contacts: string[]
|
||||||
challengeType: string
|
challengeType: string
|
||||||
@ -9,10 +10,12 @@ export interface PostAccountRequest {
|
|||||||
isStaging: boolean
|
isStaging: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PostAccountRequestSchema: Schema<PostAccountRequest> = object({
|
export const PostAccountRequestSchema: Schema<PostAccountRequest> = RequestModelBaseSchema.and(
|
||||||
description: string(),
|
object({
|
||||||
contacts: array(string()),
|
description: string(),
|
||||||
hostnames: array(string()),
|
contacts: array(string()),
|
||||||
challengeType: z.enum(ChallengeType),
|
hostnames: array(string()),
|
||||||
isStaging: boolean()
|
challengeType: z.enum(ChallengeType),
|
||||||
})
|
isStaging: boolean()
|
||||||
|
})
|
||||||
|
)
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { CacheAccount } from '../../../../entities/CacheAccount'
|
import { CacheAccount } from '../../../../entities/CacheAccount'
|
||||||
|
import { ResponseModelBase } from '../../../ResponseModelBase'
|
||||||
import { GetHostnameResponse } from './GetHostnameResponse'
|
import { GetHostnameResponse } from './GetHostnameResponse'
|
||||||
|
|
||||||
export interface GetAccountResponse {
|
export interface GetAccountResponse extends ResponseModelBase {
|
||||||
accountId: string
|
accountId: string
|
||||||
isDisabled: boolean
|
isDisabled: boolean
|
||||||
description: string
|
description: string
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export interface GetHostnameResponse {
|
import { ResponseModelBase } from '../../../ResponseModelBase'
|
||||||
|
|
||||||
|
export interface GetHostnameResponse extends ResponseModelBase {
|
||||||
hostname: string
|
hostname: string
|
||||||
expires: string
|
expires: string
|
||||||
isUpcomingExpire: boolean
|
isUpcomingExpire: boolean
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user