diff --git a/api/package-lock.json b/api/package-lock.json index 404f3eaf..cd14a176 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -35,7 +35,6 @@ "dotenv": "^16.3.1", "ejs": "^3.1.9", "express-session": "^1.17.3", - "joi": "^17.11.0", "module-alias": "^2.2.3", "mongoose": "^8.0.0", "mongoose-lean-defaults": "^2.2.1", @@ -3646,19 +3645,6 @@ "@nestjs/core": "^10.x" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -5329,24 +5315,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -13186,18 +13154,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", diff --git a/api/package.json b/api/package.json index 2cd82dbe..6ffcd5de 100644 --- a/api/package.json +++ b/api/package.json @@ -70,7 +70,6 @@ "dotenv": "^16.3.1", "ejs": "^3.1.9", "express-session": "^1.17.3", - "joi": "^17.11.0", "module-alias": "^2.2.3", "mongoose": "^8.0.0", "mongoose-lean-defaults": "^2.2.1", diff --git a/api/src/chat/schemas/types/message.ts b/api/src/chat/schemas/types/message.ts index c8c8e14f..451d7db4 100644 --- a/api/src/chat/schemas/types/message.ts +++ b/api/src/chat/schemas/types/message.ts @@ -6,14 +6,24 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { z } from 'zod'; + import { PluginName } from '@/plugins/types'; import { Message } from '../message.schema'; -import { AttachmentPayload } from './attachment'; -import { Button } from './button'; -import { ContentOptions } from './options'; -import { StdQuickReply } from './quick-reply'; +import { attachmentPayloadSchema } from './attachment'; +import { buttonSchema } from './button'; +import { contentOptionsSchema } from './options'; +import { QuickReplyType, stdQuickReplySchema } from './quick-reply'; /** * StdEventType enum is declared, and currently not used @@ -41,6 +51,10 @@ export enum IncomingMessageType { unknown = '', } +export const incomingMessageType = z.nativeEnum(IncomingMessageType); + +export type IncomingMessageTypeLiteral = z.infer; + export enum OutgoingMessageFormat { text = 'text', quickReplies = 'quickReplies', @@ -50,6 +64,12 @@ export enum OutgoingMessageFormat { carousel = 'carousel', } +export const outgoingMessageFormatSchema = z.nativeEnum(OutgoingMessageFormat); + +export type OutgoingMessageFormatLiteral = z.infer< + typeof outgoingMessageFormatSchema +>; + /** * FileType enum is declared, and currently not used **/ @@ -61,6 +81,10 @@ export enum FileType { unknown = 'unknown', } +export const fileTypeSchema = z.nativeEnum(FileType); + +export type FileTypeLiteral = z.infer; + export enum PayloadType { location = 'location', attachments = 'attachments', @@ -68,85 +92,151 @@ export enum PayloadType { button = 'button', } -export type StdOutgoingTextMessage = { text: string }; +export const payloadTypeSchema = z.nativeEnum(PayloadType); -export type StdOutgoingQuickRepliesMessage = { - text: string; - quickReplies: StdQuickReply[]; -}; +export type PayloadTypeLiteral = z.infer; -export type StdOutgoingButtonsMessage = { - text: string; - buttons: Button[]; -}; +export const stdOutgoingTextMessageSchema = z.object({ + text: z.string(), +}); -export type ContentElement = { id: string; title: string } & Record< - string, - any +export type StdOutgoingTextMessage = z.infer< + typeof stdOutgoingTextMessageSchema >; -export type StdOutgoingListMessage = { - options: ContentOptions; - elements: ContentElement[]; - pagination: { - total: number; - skip: number; - limit: number; - }; -}; +export const stdOutgoingQuickRepliesMessageSchema = z.object({ + text: z.string(), + quickReplies: z.array(stdQuickReplySchema), +}); -export type StdOutgoingAttachmentMessage = { - // Stored in DB as `AttachmentPayload`, `Attachment` when populated for channels relaying - attachment: AttachmentPayload; - quickReplies?: StdQuickReply[]; -}; +export type StdOutgoingQuickRepliesMessage = z.infer< + typeof stdOutgoingQuickRepliesMessageSchema +>; -export type StdPluginMessage = { - plugin: PluginName; - args: { [key: string]: any }; -}; +export const stdOutgoingButtonsMessageSchema = z.object({ + text: z.string(), + buttons: z.array(buttonSchema), +}); -export type BlockMessage = - | string[] - | StdOutgoingTextMessage - | StdOutgoingQuickRepliesMessage - | StdOutgoingButtonsMessage - | StdOutgoingListMessage - | StdOutgoingAttachmentMessage - | StdPluginMessage; +export type StdOutgoingButtonsMessage = z.infer< + typeof stdOutgoingButtonsMessageSchema +>; -export type StdOutgoingMessage = - | StdOutgoingTextMessage - | StdOutgoingQuickRepliesMessage - | StdOutgoingButtonsMessage - | StdOutgoingListMessage - | StdOutgoingAttachmentMessage; +export const contentElementSchema = z + .object({ + id: z.string(), + title: z.string(), + }) + .catchall(z.any()); -type StdIncomingTextMessage = { text: string }; +export type ContentElement = z.infer; -export type StdIncomingPostBackMessage = StdIncomingTextMessage & { - postback: string; -}; +export const stdOutgoingListMessageSchema = z.object({ + options: contentOptionsSchema, + elements: z.array(contentElementSchema), + pagination: z.object({ + total: z.number(), + skip: z.number(), + limit: z.number(), + }), +}); -export type StdIncomingLocationMessage = { - type: PayloadType.location; - coordinates: { - lat: number; - lon: number; - }; -}; +export type StdOutgoingListMessage = z.infer< + typeof stdOutgoingListMessageSchema +>; -export type StdIncomingAttachmentMessage = { - type: PayloadType.attachments; - serialized_text: string; - attachment: AttachmentPayload | AttachmentPayload[]; -}; +export const stdOutgoingAttachmentMessageSchema = z.object({ + attachment: attachmentPayloadSchema, + quickReplies: z.array(stdQuickReplySchema).optional(), +}); -export type StdIncomingMessage = - | StdIncomingTextMessage - | StdIncomingPostBackMessage - | StdIncomingLocationMessage - | StdIncomingAttachmentMessage; +export type StdOutgoingAttachmentMessage = z.infer< + typeof stdOutgoingAttachmentMessageSchema +>; + +export const pluginNameSchema = z + .string() + .regex(/-plugin$/) as z.ZodType; + +export const stdPluginMessageSchema = z.object({ + plugin: pluginNameSchema, + args: z.record(z.any()), +}); + +export type StdPluginMessage = z.infer; + +export const blockMessageSchema = z.union([ + z.array(z.string()), + stdOutgoingTextMessageSchema, + stdOutgoingQuickRepliesMessageSchema, + stdOutgoingButtonsMessageSchema, + stdOutgoingListMessageSchema, + stdOutgoingAttachmentMessageSchema, + stdPluginMessageSchema, +]); + +export type BlockMessage = z.infer; + +export const StdOutgoingMessageSchema = z.union([ + stdOutgoingTextMessageSchema, + stdOutgoingQuickRepliesMessageSchema, + stdOutgoingButtonsMessageSchema, + stdOutgoingListMessageSchema, + stdOutgoingAttachmentMessageSchema, +]); + +export type StdOutgoingMessage = z.infer; + +export const stdIncomingTextMessageSchema = z.object({ + text: z.string(), +}); + +export type StdIncomingTextMessage = z.infer< + typeof stdIncomingTextMessageSchema +>; + +export const stdIncomingPostBackMessageSchema = + stdIncomingTextMessageSchema.extend({ + postback: z.string(), + }); + +export type StdIncomingPostBackMessage = z.infer< + typeof stdIncomingPostBackMessageSchema +>; + +export const stdIncomingLocationMessageSchema = z.object({ + type: z.literal(PayloadType.location), + coordinates: z.object({ + lat: z.number(), + lon: z.number(), + }), +}); + +export type StdIncomingLocationMessage = z.infer< + typeof stdIncomingLocationMessageSchema +>; + +export const stdIncomingAttachmentMessageSchema = z.object({ + type: z.literal(PayloadType.attachments), + serialized_text: z.string(), + attachment: z.union([ + attachmentPayloadSchema, + z.array(attachmentPayloadSchema), + ]), +}); + +export type StdIncomingAttachmentMessage = z.infer< + typeof stdIncomingAttachmentMessageSchema +>; + +export const stdIncomingMessageSchema = z.union([ + stdIncomingTextMessageSchema, + stdIncomingPostBackMessageSchema, + stdIncomingLocationMessageSchema, + stdIncomingAttachmentMessageSchema, +]); + +export type StdIncomingMessage = z.infer; export interface IncomingMessage extends Omit { message: StdIncomingMessage; @@ -162,34 +252,149 @@ export interface OutgoingMessage extends Omit { export type AnyMessage = IncomingMessage | OutgoingMessage; -export interface StdOutgoingTextEnvelope { - format: OutgoingMessageFormat.text; - message: StdOutgoingTextMessage; -} +export const stdOutgoingTextEnvelopeSchema = z.object({ + format: z.literal(OutgoingMessageFormat.text), + message: stdOutgoingTextMessageSchema, +}); -export interface StdOutgoingQuickRepliesEnvelope { - format: OutgoingMessageFormat.quickReplies; - message: StdOutgoingQuickRepliesMessage; -} +export type StdOutgoingTextEnvelope = z.infer< + typeof stdOutgoingTextEnvelopeSchema +>; -export interface StdOutgoingButtonsEnvelope { - format: OutgoingMessageFormat.buttons; - message: StdOutgoingButtonsMessage; -} +export const stdOutgoingQuickRepliesEnvelopeSchema = z.object({ + format: z.literal(OutgoingMessageFormat.quickReplies), + message: stdOutgoingQuickRepliesMessageSchema, +}); -export interface StdOutgoingListEnvelope { - format: OutgoingMessageFormat.list | OutgoingMessageFormat.carousel; - message: StdOutgoingListMessage; -} +export type StdOutgoingQuickRepliesEnvelope = z.infer< + typeof stdOutgoingQuickRepliesEnvelopeSchema +>; -export interface StdOutgoingAttachmentEnvelope { - format: OutgoingMessageFormat.attachment; - message: StdOutgoingAttachmentMessage; -} +export const stdOutgoingButtonsEnvelopeSchema = z.object({ + format: z.literal(OutgoingMessageFormat.buttons), + message: stdOutgoingButtonsMessageSchema, +}); -export type StdOutgoingEnvelope = - | StdOutgoingTextEnvelope - | StdOutgoingQuickRepliesEnvelope - | StdOutgoingButtonsEnvelope - | StdOutgoingListEnvelope - | StdOutgoingAttachmentEnvelope; +export type StdOutgoingButtonsEnvelope = z.infer< + typeof stdOutgoingButtonsEnvelopeSchema +>; + +export const stdOutgoingListEnvelopeSchema = z.object({ + format: z.enum(['list', 'carousel']), + message: stdOutgoingListMessageSchema, +}); + +export type StdOutgoingListEnvelope = z.infer< + typeof stdOutgoingListEnvelopeSchema +>; + +export const stdOutgoingAttachmentEnvelopeSchema = z.object({ + format: z.literal(OutgoingMessageFormat.attachment), + message: stdOutgoingAttachmentMessageSchema, +}); + +export type StdOutgoingAttachmentEnvelope = z.infer< + typeof stdOutgoingAttachmentEnvelopeSchema +>; + +export const stdOutgoingEnvelopeSchema = z.union([ + stdOutgoingTextEnvelopeSchema, + stdOutgoingQuickRepliesEnvelopeSchema, + stdOutgoingButtonsEnvelopeSchema, + stdOutgoingListEnvelopeSchema, + stdOutgoingAttachmentEnvelopeSchema, +]); + +export type StdOutgoingEnvelope = z.infer; + +// is-valid-message-text validation +export const validMessageTextSchema = z.object({ + message: z.string(), +}); + +// is-message validation +const MESSAGE_REGEX = /^function \(context\) \{[^]+\}/; + +export const messageRegexSchema = z.string().regex(MESSAGE_REGEX); + +export const textSchema = z.array(z.string().max(1000)); + +const quickReplySchema = z + .object({ + content_type: z.nativeEnum(QuickReplyType), + title: z.string().max(20).optional(), + payload: z.string().max(1000).optional(), + }) + .superRefine((data, ctx) => { + // When content_type is 'text', title and payload are required. + if (data.content_type === QuickReplyType.text) { + if (data.title == null) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Title is required when content_type is 'text'", + path: ['title'], + }); + } + if (data.payload == null) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Payload is required when content_type is 'text'", + path: ['payload'], + }); + } + } + }); + +// pluginBlockMessageSchema in case of Plugin Block +export const pluginBlockMessageSchema = z + .record(z.any()) + .superRefine((data, ctx) => { + if (!('plugin' in data)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "The object must contain the 'plugin' attribute", + path: ['plugin'], + }); + } + }); + +// textBlockMessageSchema in case of Text Block +const textBlockMessageSchema = z.string().max(1000); + +const buttonMessageSchema = z.object({ + text: z.string(), + buttons: z.array(buttonSchema).max(3), +}); + +// quickReplyMessageSchema in case of QuickReply Block +const quickReplyMessageSchema = z.object({ + text: z.string(), + quickReplies: z.array(quickReplySchema).max(11).optional(), +}); + +// listBlockMessageSchema in case of List Block +const listBlockMessageSchema = z.object({ + elements: z.boolean(), +}); + +// attachmentBlockMessageSchema in case of Attachment Block +const attachmentBlockMessageSchema = z.object({ + text: z.string().max(1000).optional(), + attachment: z.object({ + type: z.nativeEnum(FileType), + payload: z.union([ + z.object({ url: z.string().url() }), + z.object({ id: z.string().nullable() }), + ]), + }), +}); + +// BlockMessage Schema +export const blockMessageObjectSchema = z.union([ + pluginBlockMessageSchema, + textBlockMessageSchema, + buttonMessageSchema, + quickReplyMessageSchema, + listBlockMessageSchema, + attachmentBlockMessageSchema, +]); diff --git a/api/src/chat/schemas/types/options.ts b/api/src/chat/schemas/types/options.ts index db1fe8b4..f413a56d 100644 --- a/api/src/chat/schemas/types/options.ts +++ b/api/src/chat/schemas/types/options.ts @@ -1,42 +1,46 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Button } from './button'; -import { OutgoingMessageFormat } from './message'; +import { z } from 'zod'; -export interface ContentOptions { - display: OutgoingMessageFormat.list | OutgoingMessageFormat.carousel; - fields: { - title: string; - subtitle: string | null; - image_url: string | null; - url?: string; - action_title?: string; - action_payload?: string; - }; - buttons: Button[]; - limit: number; - query?: any; // Waterline model criteria - entity?: string | number; // ContentTypeID - top_element_style?: 'large' | 'compact'; -} +import { buttonSchema } from './button'; -export interface BlockOptions { - typing?: number; - // In case of carousel/list message - content?: ContentOptions; - // Only if the block has next blocks - fallback?: { - active: boolean; - message: string[]; - max_attempts: number; - }; - assignTo?: string; - // plugins effects - effects?: string[]; -} +export const contentOptionsSchema = z.object({ + display: z.enum(['list', 'carousel']), + fields: z.object({ + title: z.string(), + subtitle: z.string().nullable(), + image_url: z.string().nullable(), + url: z.string().optional(), + action_title: z.string().optional(), + action_payload: z.string().optional(), + }), + buttons: z.array(buttonSchema), + limit: z.number().finite(), + query: z.any().optional(), + entity: z.union([z.string(), z.number().finite()]).optional(), + top_element_style: z.enum(['large', 'compact']).optional(), +}); + +export type ContentOptions = z.infer; + +export const BlockOptionsSchema = z.object({ + typing: z.number().optional(), + content: contentOptionsSchema.optional(), + fallback: z + .object({ + active: z.boolean(), + message: z.array(z.string()), + max_attempts: z.number().finite(), + }) + .optional(), + assignTo: z.string().optional(), + effects: z.array(z.string()).optional(), +}); + +export type BlockOptions = z.infer; diff --git a/api/src/chat/schemas/types/quick-reply.ts b/api/src/chat/schemas/types/quick-reply.ts index e58cea15..95862874 100644 --- a/api/src/chat/schemas/types/quick-reply.ts +++ b/api/src/chat/schemas/types/quick-reply.ts @@ -9,7 +9,6 @@ import { z } from 'zod'; import { attachmentPayloadSchema } from './attachment'; -import { PayloadType } from './message'; export enum QuickReplyType { text = 'text', @@ -25,11 +24,11 @@ export const cordinatesSchema = z.object({ export const payloadSchema = z.discriminatedUnion('type', [ z.object({ - type: z.literal(PayloadType.location), + type: z.literal('location'), coordinates: cordinatesSchema, }), z.object({ - type: z.literal(PayloadType.attachments), + type: z.literal('attachments'), attachment: attachmentPayloadSchema, }), ]); diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index c7fedabf..bab88387 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -17,7 +17,7 @@ import { I18nService } from '@/i18n/services/i18n.service'; import { LanguageService } from '@/i18n/services/language.service'; import { LoggerService } from '@/logger/logger.service'; import { PluginService } from '@/plugins/plugins.service'; -import { PluginName, PluginType } from '@/plugins/types'; +import { PluginType } from '@/plugins/types'; import { SettingService } from '@/setting/services/setting.service'; import { BaseService } from '@/utils/generics/base-service'; import { getRandom } from '@/utils/helpers/safeRandom'; @@ -568,7 +568,6 @@ export class BlockService extends BaseService< contentBlockOptions, skip, ); - const envelope: StdOutgoingEnvelope = { format: contentBlockOptions.display, message: { @@ -576,7 +575,6 @@ export class BlockService extends BaseService< options: contentBlockOptions, }, }; - return envelope; } catch (err) { this.logger.error( @@ -588,7 +586,7 @@ export class BlockService extends BaseService< } else if (blockMessage && 'plugin' in blockMessage) { const plugin = this.pluginService.findPlugin( PluginType.block, - blockMessage.plugin as PluginName, + blockMessage.plugin, ); // Process custom plugin block try { diff --git a/api/src/chat/validation-rules/is-message.ts b/api/src/chat/validation-rules/is-message.ts index 67688d94..20fc2f2b 100644 --- a/api/src/chat/validation-rules/is-message.ts +++ b/api/src/chat/validation-rules/is-message.ts @@ -12,103 +12,40 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; -import Joi from 'joi'; -import { BlockMessage } from '../schemas/types/message'; +import { + BlockMessage, + blockMessageObjectSchema, + messageRegexSchema, + textSchema, +} from '../schemas/types/message'; +/* eslint-disable no-console */ export function isValidMessage(msg: any) { if (typeof msg === 'string' && msg !== '') { - // Custom code - const MESSAGE_REGEX = /^function \(context\) \{[^]+\}/; - if (!MESSAGE_REGEX.test(msg)) { - // eslint-disable-next-line - console.error('Block Model : Invalid custom code.', msg); + const result = messageRegexSchema.safeParse(msg); + if (!result.success) { + console.error('Block Model: Invalid custom code.', result.error); return false; - } else { - return true; } + return true; } else if (Array.isArray(msg)) { - // Simple text message - const textSchema = Joi.array().items(Joi.string().max(1000).required()); - const textCheck = textSchema.validate(msg); - return !textCheck.error; - } else if (typeof msg === 'object') { - if ('plugin' in msg) { - return true; - } else { - const buttonsSchema = Joi.array().items( - Joi.object().keys({ - type: Joi.string().valid('postback', 'web_url').required(), - title: Joi.string().max(20), - payload: Joi.alternatives().conditional('type', { - is: 'postback', - then: Joi.string().max(1000).required(), - otherwise: Joi.forbidden(), - }), - url: Joi.alternatives().conditional('type', { - is: 'web_url', - then: Joi.string().uri(), - otherwise: Joi.forbidden(), - }), - messenger_extensions: Joi.alternatives().conditional('type', { - is: 'web_url', - then: Joi.boolean(), - otherwise: Joi.forbidden(), - }), - webview_height_ratio: Joi.alternatives().conditional('type', { - is: 'web_url', - then: Joi.string().valid('compact', 'tall', 'full'), - otherwise: Joi.forbidden(), - }), - }), - ); - // Attachment message - const objectSchema = Joi.object().keys({ - text: Joi.string().max(1000), - attachment: Joi.object().keys({ - type: Joi.string() - .valid('image', 'audio', 'video', 'file', 'unknown') - .required(), - payload: Joi.object().keys({ - url: Joi.string().uri(), - id: Joi.string().allow(null), - }), - }), - elements: Joi.boolean(), - cards: Joi.object().keys({ - default_action: buttonsSchema.max(1), - buttons: buttonsSchema.max(3), - }), - buttons: buttonsSchema.max(3), - quickReplies: Joi.array() - .items( - Joi.object().keys({ - content_type: Joi.string() - .valid('text', 'location', 'user_phone_number', 'user_email') - .required(), - title: Joi.alternatives().conditional('content_type', { - is: 'text', - then: Joi.string().max(20).required(), - }), - payload: Joi.alternatives().conditional('content_type', { - is: 'text', - then: Joi.string().max(1000).required(), - }), - }), - ) - .max(11), - }); - const objectCheck = objectSchema.validate(msg); - if (objectCheck.error) { - // eslint-disable-next-line - console.log('Message validation failed! ', objectCheck); - } - return !objectCheck.error; + const result = textSchema.safeParse(msg); + if (!result.success) { + console.error('Block Model: Invalid text message array.', result.error); } - } else { - return false; + return result.success; + } else if (typeof msg === 'object' && msg !== null) { + const result = blockMessageObjectSchema.safeParse(msg); + if (!result.success) { + console.error('Block Model: Object validation failed!', result.error); + } + return result.success; } + console.log('Validation reached default false'); + return false; } +/* eslint-enable no-console */ @ValidatorConstraint({ async: false }) export class MessageValidator implements ValidatorConstraintInterface { diff --git a/api/src/chat/validation-rules/is-valid-message-text.ts b/api/src/chat/validation-rules/is-valid-message-text.ts index 485e78bc..caf63f51 100644 --- a/api/src/chat/validation-rules/is-valid-message-text.ts +++ b/api/src/chat/validation-rules/is-valid-message-text.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. @@ -11,6 +11,7 @@ import { registerDecorator, ValidationOptions } from 'class-validator'; import { StdIncomingMessage, StdOutgoingTextMessage, + validMessageTextSchema, } from '../schemas/types/message'; export function IsValidMessageText(validationOptions?: ValidationOptions) { @@ -21,7 +22,7 @@ export function IsValidMessageText(validationOptions?: ValidationOptions) { options: validationOptions, validator: { validate(message: StdOutgoingTextMessage | StdIncomingMessage) { - return !!(message as StdOutgoingTextMessage).text; + return validMessageTextSchema.safeParse(message).success; }, }, }); diff --git a/frontend/src/app-components/dialogs/FormDialog.tsx b/frontend/src/app-components/dialogs/FormDialog.tsx index 180ba58f..20469895 100644 --- a/frontend/src/app-components/dialogs/FormDialog.tsx +++ b/frontend/src/app-components/dialogs/FormDialog.tsx @@ -22,16 +22,20 @@ export const FormDialog = ({ ...rest }: FormDialogProps) => { const onCancel = () => rest.onClose?.({}, "backdropClick"); + const dialogActions = + rest.hasButtons === false ? null : ( + + + + ); return ( {title} {children} - - - + {dialogActions} ); }; diff --git a/frontend/src/components/content-types/ContentTypeDialog.tsx b/frontend/src/components/content-types/ContentTypeDialog.tsx deleted file mode 100644 index 735496da..00000000 --- a/frontend/src/components/content-types/ContentTypeDialog.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright © 2025 Hexastack. All rights reserved. - * - * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: - * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. - * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). - */ - -import AddIcon from "@mui/icons-material/Add"; -import { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; -import { FC, useEffect } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; - -import DialogButtons from "@/app-components/buttons/DialogButtons"; -import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; -import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; -import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem"; -import { Input } from "@/app-components/inputs/Input"; -import { useCreate } from "@/hooks/crud/useCreate"; -import { useUpdate } from "@/hooks/crud/useUpdate"; -import { DialogControlProps } from "@/hooks/useDialog"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { EntityType } from "@/services/types"; -import { ContentFieldType, IContentType } from "@/types/content-type.types"; - -import { FieldInput } from "./components/FieldInput"; -import { FIELDS_FORM_DEFAULT_VALUES, READ_ONLY_FIELDS } from "./constants"; - -export type ContentTypeDialogProps = DialogControlProps; -export const ContentTypeDialog: FC = ({ - open, - data, - closeDialog, -}) => { - const { toast } = useToast(); - const { t } = useTranslate(); - const { - handleSubmit, - register, - control, - reset, - setValue, - formState: { errors }, - } = useForm>({ - defaultValues: { - name: data?.name || "", - fields: data?.fields || FIELDS_FORM_DEFAULT_VALUES, - }, - }); - const { append, fields, remove } = useFieldArray({ - name: "fields", - control, - }); - const closeAndReset = () => { - closeDialog(); - reset({ - name: "", - fields: FIELDS_FORM_DEFAULT_VALUES, - }); - }; - const { mutate: createContentType } = useCreate(EntityType.CONTENT_TYPE, { - onError: (error) => { - toast.error(error.message || t("message.internal_server_error")); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { mutate: updateContentType } = useUpdate(EntityType.CONTENT_TYPE, { - onError: (error) => { - toast.error(error.message || t("message.internal_server_error")); - }, - onSuccess: () => { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const onSubmitForm = async (params) => { - const labelCounts: Record = params.fields.reduce( - (acc, field) => { - if (!field.label.trim()) return acc; - acc[field.label] = (acc[field.label] || 0) + 1; - - return acc; - }, - {} as Record, - ); - const hasDuplicates = Object.values(labelCounts).some( - (count: number) => count > 1, - ); - - if (hasDuplicates) { - toast.error(t("message.duplicate_labels_not_allowed")); - - return; - } - - if (data) { - updateContentType({ id: data.id, params }); - } else { - createContentType(params); - } - }; - - useEffect(() => { - if (open) reset(); - }, [open, reset]); - - useEffect(() => { - if (data) { - reset({ - name: data.name, - fields: data.fields || FIELDS_FORM_DEFAULT_VALUES, - }); - } else { - reset({ name: "", fields: FIELDS_FORM_DEFAULT_VALUES }); - } - }, [data, reset]); - - return ( - -
- - {data ? t("title.edit_content_type") : t("title.new_content_type")} - - - - - - - - {fields.map((f, index) => ( - - - - ))} - - - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/content-types/ContentTypeForm.tsx b/frontend/src/components/content-types/ContentTypeForm.tsx new file mode 100644 index 00000000..de23f7db --- /dev/null +++ b/frontend/src/components/content-types/ContentTypeForm.tsx @@ -0,0 +1,160 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import AddIcon from "@mui/icons-material/Add"; +import { Button } from "@mui/material"; +import { FC, Fragment, useEffect } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; + +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; +import { Input } from "@/app-components/inputs/Input"; +import { useCreate } from "@/hooks/crud/useCreate"; +import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useToast } from "@/hooks/useToast"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType } from "@/services/types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; +import { ContentFieldType, IContentType } from "@/types/content-type.types"; + +import { FieldInput } from "./components/FieldInput"; +import { FIELDS_FORM_DEFAULT_VALUES, READ_ONLY_FIELDS } from "./constants"; + +export const ContentTypeForm: FC> = ({ + data, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { toast } = useToast(); + const { t } = useTranslate(); + const { + reset, + control, + register, + setValue, + formState: { errors }, + handleSubmit, + } = useForm>({ + defaultValues: { + name: data?.name || "", + fields: data?.fields || FIELDS_FORM_DEFAULT_VALUES, + }, + }); + const { append, fields, remove } = useFieldArray({ + name: "fields", + control, + }); + const options = { + onError: (error: Error) => { + rest.onError?.(); + toast.error(error.message || t("message.internal_server_error")); + }, + onSuccess: () => { + rest.onSuccess?.(); + toast.success(t("message.success_save")); + }, + }; + const { mutate: createContentType } = useCreate( + EntityType.CONTENT_TYPE, + options, + ); + const { mutate: updateContentType } = useUpdate( + EntityType.CONTENT_TYPE, + options, + ); + const onSubmitForm = (params) => { + const labelCounts: Record = params.fields.reduce( + (acc, field) => { + if (!field.label.trim()) return acc; + acc[field.label] = (acc[field.label] || 0) + 1; + + return acc; + }, + {} as Record, + ); + const hasDuplicates = Object.values(labelCounts).some( + (count: number) => count > 1, + ); + + if (hasDuplicates) { + toast.error(t("message.duplicate_labels_not_allowed")); + + return; + } + + if (data) { + updateContentType({ id: data.id, params }); + } else { + createContentType(params); + } + }; + + useEffect(() => { + if (data) { + reset({ + name: data.name, + fields: data.fields || FIELDS_FORM_DEFAULT_VALUES, + }); + } else { + reset({ name: "", fields: FIELDS_FORM_DEFAULT_VALUES }); + } + }, [data, reset]); + + return ( + +
+ + + + + + {fields.map((f, index) => ( + + + + ))} + + + + +
+
+ ); +}; diff --git a/frontend/src/components/content-types/ContentTypeFormDialog.tsx b/frontend/src/components/content-types/ContentTypeFormDialog.tsx new file mode 100644 index 00000000..7675715f --- /dev/null +++ b/frontend/src/components/content-types/ContentTypeFormDialog.tsx @@ -0,0 +1,24 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; +import { IContentType } from "@/types/content-type.types"; + +import { ContentTypeForm } from "./ContentTypeForm"; + +export const ContentTypeFormDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={ContentTypeForm} + addText="title.new_content_type" + editText="title.edit_content_type" + {...props} + /> +); diff --git a/frontend/src/components/content-types/index.tsx b/frontend/src/components/content-types/index.tsx index 46ef5638..7cd13d74 100644 --- a/frontend/src/components/content-types/index.tsx +++ b/frontend/src/components/content-types/index.tsx @@ -6,13 +6,12 @@ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ - import { faAlignLeft } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; import { Button, Grid, Paper } from "@mui/material"; import { useRouter } from "next/router"; -import { DeleteDialog } from "@/app-components/dialogs"; +import { ConfirmDialogBody } from "@/app-components/dialogs"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -22,7 +21,7 @@ import { renderHeader } from "@/app-components/tables/columns/renderHeader"; import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; -import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; import { useToast } from "@/hooks/useToast"; @@ -33,16 +32,13 @@ import { IContentType } from "@/types/content-type.types"; import { PermissionAction } from "@/types/permission.types"; import { getDateTimeFormatter } from "@/utils/date"; -import { ContentTypeDialog } from "./ContentTypeDialog"; +import { ContentTypeFormDialog } from "./ContentTypeFormDialog"; export const ContentTypes = () => { const { t } = useTranslate(); const { toast } = useToast(); const router = useRouter(); - // Dialog Controls - const addDialogCtl = useDialog(false); - const deleteDialogCtl = useDialog(false); - const fieldsDialogCtl = useDialog(false); + const dialogs = useDialogs(); // data fetching const { onSearch, searchPayload } = useSearch({ $iLike: ["name"], @@ -53,18 +49,14 @@ export const ContentTypes = () => { params: searchPayload, }, ); - const { mutateAsync: deleteContentType } = useDelete( - EntityType.CONTENT_TYPE, - { - onSuccess: () => { - deleteDialogCtl.closeDialog(); - toast.success(t("message.item_delete_success")); - }, - onError: (error) => { - toast.error(error.message || t("message.internal_server_error")); - }, + const { mutate: deleteContentType } = useDelete(EntityType.CONTENT_TYPE, { + onSuccess: () => { + toast.success(t("message.item_delete_success")); }, - ); + onError: (error) => { + toast.error(error.message || t("message.internal_server_error")); + }, + }); const hasPermission = useHasPermission(); const actionColumns = useActionColumns( EntityType.CONTENT_TYPE, @@ -75,12 +67,18 @@ export const ContentTypes = () => { }, { label: ActionColumnLabel.Edit, - action: (row) => fieldsDialogCtl.openDialog(row), + action: (row) => dialogs.open(ContentTypeFormDialog, row), requires: [PermissionAction.UPDATE], }, { label: ActionColumnLabel.Delete, - action: (row) => deleteDialogCtl.openDialog(row.id), + action: async ({ id }) => { + const isConfirmed = await dialogs.confirm(ConfirmDialogBody); + + if (isConfirmed) { + deleteContentType(id); + } + }, requires: [PermissionAction.DELETE], }, ], @@ -106,7 +104,7 @@ export const ContentTypes = () => { - - - ); -}; diff --git a/frontend/src/components/roles/RoleDialog.tsx b/frontend/src/components/roles/RoleDialog.tsx deleted file mode 100644 index 144c1387..00000000 --- a/frontend/src/components/roles/RoleDialog.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright © 2024 Hexastack. All rights reserved. - * - * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: - * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. - * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). - */ - -import { Dialog, DialogActions, DialogContent } from "@mui/material"; -import { FC, useEffect } from "react"; -import { useForm } from "react-hook-form"; - -import DialogButtons from "@/app-components/buttons/DialogButtons"; -import { DialogTitle } from "@/app-components/dialogs/DialogTitle"; -import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer"; -import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem"; -import { Input } from "@/app-components/inputs/Input"; -import { useCreate } from "@/hooks/crud/useCreate"; -import { useUpdate } from "@/hooks/crud/useUpdate"; -import { DialogControlProps } from "@/hooks/useDialog"; -import { useToast } from "@/hooks/useToast"; -import { useTranslate } from "@/hooks/useTranslate"; -import { EntityType } from "@/services/types"; -import { IRole, IRoleAttributes } from "@/types/role.types"; - -export type RoleDialogProps = DialogControlProps; -export const RoleDialog: FC = ({ - open, - data, - closeDialog, - ...rest -}) => { - const { t } = useTranslate(); - const { toast } = useToast(); - const { mutateAsync: createRole } = useCreate(EntityType.ROLE, { - onError: (error) => { - toast.error(error); - }, - onSuccess() { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { mutateAsync: updateRole } = useUpdate(EntityType.ROLE, { - onError: () => { - toast.error(t("message.internal_server_error")); - }, - onSuccess() { - closeDialog(); - toast.success(t("message.success_save")); - }, - }); - const { - handleSubmit, - reset, - register, - formState: { errors }, - } = useForm({ - defaultValues: { name: "" }, - }); - const validationRules = { - name: { - required: t("message.name_is_required"), - }, - }; - const onSubmitForm = async (params: IRoleAttributes) => { - if (data) { - updateRole({ id: data.id, params }); - } else { - createRole(params); - } - }; - - useEffect(() => { - if (open) reset(); - }, [open, reset]); - - useEffect(() => { - if (data) { - reset({ - name: data.name, - }); - } else { - reset(); - } - }, [data, reset]); - - return ( - -
- - {data ? t("title.edit_role") : t("title.new_role")} - - - - - - - - - - - -
-
- ); -}; diff --git a/frontend/src/components/roles/RoleForm.tsx b/frontend/src/components/roles/RoleForm.tsx new file mode 100644 index 00000000..5cc39de2 --- /dev/null +++ b/frontend/src/components/roles/RoleForm.tsx @@ -0,0 +1,94 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { FC, Fragment, useEffect } from "react"; +import { useForm } from "react-hook-form"; + +import { ContentContainer, ContentItem } from "@/app-components/dialogs/"; +import { Input } from "@/app-components/inputs/Input"; +import { useCreate } from "@/hooks/crud/useCreate"; +import { useUpdate } from "@/hooks/crud/useUpdate"; +import { useToast } from "@/hooks/useToast"; +import { useTranslate } from "@/hooks/useTranslate"; +import { EntityType } from "@/services/types"; +import { ComponentFormProps } from "@/types/common/dialogs.types"; +import { IRole, IRoleAttributes } from "@/types/role.types"; + +export const RoleForm: FC> = ({ + data, + Wrapper = Fragment, + WrapperProps, + ...rest +}) => { + const { t } = useTranslate(); + const { toast } = useToast(); + const options = { + onError: (error: Error) => { + toast.error(error); + }, + onSuccess() { + rest.onSuccess?.(); + toast.success(t("message.success_save")); + }, + }; + const { mutate: createRole } = useCreate(EntityType.ROLE, options); + const { mutate: updateRole } = useUpdate(EntityType.ROLE, options); + const { + handleSubmit, + reset, + register, + formState: { errors }, + } = useForm({ + defaultValues: { name: "" }, + }); + const validationRules = { + name: { + required: t("message.name_is_required"), + }, + }; + const onSubmitForm = (params: IRoleAttributes) => { + if (data) { + updateRole({ id: data.id, params }); + } else { + createRole(params); + } + }; + + useEffect(() => { + if (data) { + reset({ + name: data.name, + }); + } else { + reset(); + } + }, [data, reset]); + + return ( + +
+ + + + + +
+
+ ); +}; diff --git a/frontend/src/components/roles/RoleFormDialog.tsx b/frontend/src/components/roles/RoleFormDialog.tsx new file mode 100644 index 00000000..92590398 --- /dev/null +++ b/frontend/src/components/roles/RoleFormDialog.tsx @@ -0,0 +1,24 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { GenericFormDialog } from "@/app-components/dialogs"; +import { ComponentFormDialogProps } from "@/types/common/dialogs.types"; +import { IRole } from "@/types/role.types"; + +import { RoleForm } from "./RoleForm"; + +export const RoleFormDialog = ( + props: ComponentFormDialogProps, +) => ( + + Form={RoleForm} + addText="title.new_role" + editText="title.edit_role" + {...props} + /> +); diff --git a/frontend/src/components/roles/index.tsx b/frontend/src/components/roles/index.tsx index f23655e4..531818da 100644 --- a/frontend/src/components/roles/index.tsx +++ b/frontend/src/components/roles/index.tsx @@ -10,9 +10,8 @@ import { faUniversalAccess } from "@fortawesome/free-solid-svg-icons"; import AddIcon from "@mui/icons-material/Add"; import { Button, Grid, Paper } from "@mui/material"; import { GridColDef } from "@mui/x-data-grid"; -import React from "react"; -import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog"; +import { ConfirmDialogBody } from "@/app-components/dialogs"; import { FilterTextfield } from "@/app-components/inputs/FilterTextfield"; import { ActionColumnLabel, @@ -22,7 +21,7 @@ import { renderHeader } from "@/app-components/tables/columns/renderHeader"; import { DataGrid } from "@/app-components/tables/DataGrid"; import { useDelete } from "@/hooks/crud/useDelete"; import { useFind } from "@/hooks/crud/useFind"; -import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; +import { useDialogs } from "@/hooks/useDialogs"; import { useHasPermission } from "@/hooks/useHasPermission"; import { useSearch } from "@/hooks/useSearch"; import { useToast } from "@/hooks/useToast"; @@ -33,18 +32,13 @@ import { PermissionAction } from "@/types/permission.types"; import { IRole } from "@/types/role.types"; import { getDateTimeFormatter } from "@/utils/date"; -import { PermissionsDialog } from "./PermissionsDialog"; -import { RoleDialog } from "./RoleDialog"; +import { PermissionBodyDialog } from "./PermissionsBodyDialog"; +import { RoleFormDialog } from "./RoleFormDialog"; export const Roles = () => { const { t } = useTranslate(); const { toast } = useToast(); - const addDialogCtl = useDialog(false); - const editDialogCtl = useDialog(false); - const deleteDialogCtl = useDialog(false); - const permissionDialogCtl = useDialog<{ - role: IRole; - }>(false); + const dialogs = useDialogs(); const hasPermission = useHasPermission(); const { onSearch, searchPayload } = useSearch({ $iLike: ["name"], @@ -55,12 +49,11 @@ export const Roles = () => { params: searchPayload, }, ); - const { mutateAsync: deleteRole } = useDelete(EntityType.ROLE, { + const { mutate: deleteRole } = useDelete(EntityType.ROLE, { onError: (error) => { toast.error(error); }, onSuccess() { - deleteDialogCtl.closeDialog(); toast.success(t("message.item_delete_success")); }, }); @@ -70,19 +63,25 @@ export const Roles = () => { { label: ActionColumnLabel.Permissions, action: (row) => - permissionDialogCtl.openDialog({ - role: row, + dialogs.open(PermissionBodyDialog, row, { + hasButtons: false, }), }, { label: ActionColumnLabel.Edit, - action: (row) => editDialogCtl.openDialog(row), + action: (row) => dialogs.open(RoleFormDialog, row), requires: [PermissionAction.UPDATE], }, { label: ActionColumnLabel.Delete, - action: (row) => deleteDialogCtl.openDialog(row.id), + action: async ({ id }) => { + const isConfirmed = await dialogs.confirm(ConfirmDialogBody); + + if (isConfirmed) { + deleteRole(id); + } + }, requires: [PermissionAction.DELETE], }, ], @@ -125,17 +124,6 @@ export const Roles = () => { return ( - {permissionDialogCtl.open ? ( - - ) : null} - - - { - if (deleteDialogCtl.data) deleteRole(deleteDialogCtl.data); - }} - /> { sx={{ float: "right", }} - onClick={() => { - addDialogCtl.openDialog(); - }} + onClick={() => dialogs.open(RoleFormDialog, null)} > {t("button.add")} diff --git a/frontend/src/types/common/dialogs.types.ts b/frontend/src/types/common/dialogs.types.ts index 64191fc1..d5b010b2 100644 --- a/frontend/src/types/common/dialogs.types.ts +++ b/frontend/src/types/common/dialogs.types.ts @@ -150,7 +150,8 @@ export interface DialogProviderProps { // form dialog export interface FormDialogProps extends FormButtonsProps, - Omit { + Omit, + DialogExtraOptions { title?: string; children?: React.ReactNode; }