feat: Add module association to documents and integrate workflow history and tracking.
This commit is contained in:
parent
254bc9f40e
commit
e4aede83d9
@ -6,6 +6,8 @@ import { documentService, type FileAttachmentItem } from "@/services/document-se
|
||||
import type { DocumentCategory } from "@/types/document";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { ArrowLeft, FileText, Info, Paperclip } from "lucide-react";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import type { MyModule } from "@/types/module";
|
||||
|
||||
const CreateDocument = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
@ -21,6 +23,8 @@ const CreateDocument = (): ReactElement => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
|
||||
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
||||
const [modules, setModules] = useState<MyModule[]>([]);
|
||||
const [selectedModuleId, setSelectedModuleId] = useState("");
|
||||
|
||||
// File attachment fields
|
||||
const [files, setFiles] = useState<FileAttachmentItem[]>([]);
|
||||
@ -35,12 +39,14 @@ const CreateDocument = (): ReactElement => {
|
||||
useEffect(() => {
|
||||
const loadLookups = async (): Promise<void> => {
|
||||
try {
|
||||
const [typesRes, categoriesRes] = await Promise.all([
|
||||
const [typesRes, categoriesRes, modulesRes] = await Promise.all([
|
||||
documentService.getTypes(),
|
||||
documentService.getCategories(),
|
||||
moduleService.getMyModules(),
|
||||
]);
|
||||
setTypes(typesRes.data || []);
|
||||
setCategories(categoriesRes.data || []);
|
||||
setModules(modulesRes.data || []);
|
||||
} catch {
|
||||
showToast.error("Failed to load document metadata");
|
||||
}
|
||||
@ -133,6 +139,8 @@ const CreateDocument = (): ReactElement => {
|
||||
file_size: fileSize,
|
||||
mime_type: mimeType || undefined,
|
||||
file_hash: fileHash || undefined,
|
||||
source_module: selectedModuleId ? modules.find(m => m.id === selectedModuleId)?.module_id : undefined,
|
||||
source_module_id: selectedModuleId || undefined,
|
||||
});
|
||||
showToast.success("Document created successfully");
|
||||
navigate(`/tenant/documents/${response.data.id}`);
|
||||
@ -248,6 +256,19 @@ const CreateDocument = (): ReactElement => {
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder="Comma separated tags (e.g. quality, sop)"
|
||||
/>
|
||||
<FormSelect
|
||||
label="Source Module"
|
||||
value={selectedModuleId}
|
||||
onValueChange={setSelectedModuleId}
|
||||
options={[
|
||||
{ value: "", label: "Platform (Default)" },
|
||||
...modules.map((m) => ({
|
||||
value: m.id,
|
||||
label: m.name,
|
||||
})),
|
||||
]}
|
||||
placeholder="Specify originating module (Optional)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -135,6 +135,13 @@ const Documents = (): ReactElement => {
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "module_name",
|
||||
label: "Module",
|
||||
render: (doc) => (
|
||||
<span className="text-[#0f1724]">{doc.module_name || "Platform"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "current_version",
|
||||
label: "Version",
|
||||
|
||||
@ -13,8 +13,9 @@ import {
|
||||
import { documentService, type FileAttachmentItem } from "@/services/document-service";
|
||||
import { workflowService } from "@/services/workflow-service";
|
||||
import type { DocumentDetail, DocumentVersion } from "@/types/document";
|
||||
import type { WorkflowInstance } from "@/types/workflow";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { Paperclip, Plus } from "lucide-react";
|
||||
import { Paperclip, Plus, User } from "lucide-react";
|
||||
|
||||
const formatDateTime = (value?: string | null): string => {
|
||||
if (!value) return "-";
|
||||
@ -53,9 +54,11 @@ const ViewDocument = (): ReactElement => {
|
||||
const [versions, setVersions] = useState<DocumentVersion[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<"overview" | "version-history">(
|
||||
const [activeTab, setActiveTab] = useState<"overview" | "version-history" | "workflow-history">(
|
||||
"overview",
|
||||
);
|
||||
const [workflowInstances, setWorkflowInstances] = useState<any[]>([]);
|
||||
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
|
||||
|
||||
const [activeAction, setActiveAction] = useState<DocumentAction | null>(null);
|
||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||
@ -73,6 +76,9 @@ const ViewDocument = (): ReactElement => {
|
||||
const [newVersionChangeSummary, setNewVersionChangeSummary] = useState("");
|
||||
const [isMajorVersion, setIsMajorVersion] = useState(false);
|
||||
const [isVersionSaving, setIsVersionSaving] = useState(false);
|
||||
const [showWorkflowTracker, setShowWorkflowTracker] = useState(false);
|
||||
const [workflowInstance, setWorkflowInstance] = useState<WorkflowInstance | null>(null);
|
||||
const [isWorkflowLoading, setIsWorkflowLoading] = useState(false);
|
||||
|
||||
// File attachment fields for new version
|
||||
const [versionFiles, setVersionFiles] = useState<FileAttachmentItem[]>([]);
|
||||
@ -111,6 +117,29 @@ const ViewDocument = (): ReactElement => {
|
||||
void loadVersionFiles();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "workflow-history" && id) {
|
||||
void loadWorkflowHistory();
|
||||
}
|
||||
}, [activeTab, id]);
|
||||
|
||||
const loadWorkflowHistory = async (): Promise<void> => {
|
||||
try {
|
||||
setIsHistoryLoading(true);
|
||||
const res = await workflowService.listInstances({
|
||||
entity_type: "document",
|
||||
entity_id: id,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
});
|
||||
setWorkflowInstances(res.data || []);
|
||||
} catch {
|
||||
showToast.error("Failed to load workflow history");
|
||||
} finally {
|
||||
setIsHistoryLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadVersionFiles = async (): Promise<void> => {
|
||||
setIsLoadingVersionFiles(true);
|
||||
try {
|
||||
@ -172,6 +201,26 @@ const ViewDocument = (): ReactElement => {
|
||||
setVersions(versionsRes.data || []);
|
||||
};
|
||||
|
||||
const openWorkflowTracker = async (instanceId?: string): Promise<void> => {
|
||||
const targetId = instanceId || document?.workflow_instance_id;
|
||||
if (!targetId) {
|
||||
showToast.error("No workflow instance associated with this document");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsWorkflowLoading(true);
|
||||
setShowWorkflowTracker(true);
|
||||
const res = await workflowService.getInstance(targetId);
|
||||
setWorkflowInstance(res.data);
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.error?.message || "Failed to load workflow tracker";
|
||||
showToast.error(msg);
|
||||
setShowWorkflowTracker(false);
|
||||
} finally {
|
||||
setIsWorkflowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetActionModal = (): void => {
|
||||
setActiveAction(null);
|
||||
setWorkflowDefinitionId("");
|
||||
@ -319,6 +368,11 @@ const ViewDocument = (): ReactElement => {
|
||||
label: "Change Summary",
|
||||
render: (version) => version.change_summary || "-",
|
||||
},
|
||||
{
|
||||
key: "module_name",
|
||||
label: "Module",
|
||||
render: (version) => version.module_name || "Platform",
|
||||
},
|
||||
{
|
||||
key: "created_by",
|
||||
label: "Author",
|
||||
@ -373,6 +427,13 @@ const ViewDocument = (): ReactElement => {
|
||||
)}
|
||||
{document?.status === "in_review" && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-[#112868] text-white text-xs font-medium hover:bg-[#0c1d4a]"
|
||||
onClick={() => void openWorkflowTracker()}
|
||||
>
|
||||
Workflow Status
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-emerald-600 text-white text-xs font-medium hover:bg-emerald-700"
|
||||
@ -419,6 +480,11 @@ const ViewDocument = (): ReactElement => {
|
||||
<span className="px-2 py-1 rounded-full bg-blue-100 text-blue-700 text-[11px] font-medium">
|
||||
v{document?.current_version || "-"}
|
||||
</span>
|
||||
{document?.module_name && (
|
||||
<span className="px-2 py-1 rounded-full bg-purple-100 text-purple-700 text-[11px] font-medium">
|
||||
{document.module_name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -438,6 +504,13 @@ const ViewDocument = (): ReactElement => {
|
||||
>
|
||||
Version History
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`text-sm pb-2 ${activeTab === "workflow-history" ? "text-[#084cc8] border-b-2 border-[#084cc8]" : "text-[#6b7280]"}`}
|
||||
onClick={() => setActiveTab("workflow-history")}
|
||||
>
|
||||
Workflow History
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
@ -622,6 +695,53 @@ const ViewDocument = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "workflow-history" && (
|
||||
<div className="space-y-4">
|
||||
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Workflow</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Current Step</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Status</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Started At</th>
|
||||
<th className="px-4 py-3 text-right text-[10px] font-bold text-gray-400 uppercase tracking-wider font-bold">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100">
|
||||
{isHistoryLoading ? (
|
||||
<tr><td colSpan={5} className="px-4 py-4 text-center text-xs text-gray-500">Loading history...</td></tr>
|
||||
) : workflowInstances.length === 0 ? (
|
||||
<tr><td colSpan={5} className="px-4 py-4 text-center text-xs text-gray-500">No workflow history found.</td></tr>
|
||||
) : workflowInstances.map((inst) => (
|
||||
<tr key={inst.id}>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs font-semibold text-gray-800">{inst.workflow_name}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-600">{inst.current_step || '-'}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
|
||||
inst.status === 'completed' ? 'bg-emerald-100 text-emerald-700' :
|
||||
inst.status === 'active' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{inst.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-500">{formatDateTime(inst.started_at)}</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-bold text-[#112868] hover:underline"
|
||||
onClick={() => void openWorkflowTracker(inst.id)}
|
||||
>
|
||||
View Tracker
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
<div className="mt-4">
|
||||
@ -725,6 +845,112 @@ const ViewDocument = (): ReactElement => {
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Workflow Instance Tracker Modal */}
|
||||
<Modal
|
||||
isOpen={showWorkflowTracker}
|
||||
onClose={() => setShowWorkflowTracker(false)}
|
||||
title="Workflow Status Tracker"
|
||||
description="Track the progress of this document's approval workflow."
|
||||
maxWidth="2xl"
|
||||
>
|
||||
<div className="p-6">
|
||||
{isWorkflowLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 space-y-3">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#112868]"></div>
|
||||
<p className="text-sm text-gray-500 font-medium">Loading workflow details...</p>
|
||||
</div>
|
||||
) : workflowInstance ? (
|
||||
<div className="space-y-6">
|
||||
{/* Workflow Header Info */}
|
||||
<div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-100">
|
||||
<div>
|
||||
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Workflow</label>
|
||||
<p className="text-sm font-semibold text-gray-800">{workflowInstance.workflow.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Current Status</label>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase ${
|
||||
workflowInstance.status === 'completed' ? 'bg-emerald-100 text-emerald-700' :
|
||||
workflowInstance.status === 'active' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{workflowInstance.status}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Started By</label>
|
||||
<p className="text-sm font-medium text-gray-700">{workflowInstance.started_by.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Started At</label>
|
||||
<p className="text-sm font-medium text-gray-700">{formatDateTime(workflowInstance.started_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Tasks Table View */}
|
||||
<div className="overflow-hidden border border-gray-100 rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Step</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Assignee</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Status</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Action</th>
|
||||
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Completed At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100">
|
||||
{workflowInstance.tasks.map((task) => (
|
||||
<tr key={task.id} className={task.status === 'pending' ? 'bg-blue-50/30' : ''}>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<span className={`text-xs font-bold ${task.status === 'pending' ? 'text-blue-700' : 'text-gray-800'}`}>
|
||||
{task.step}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||
<User className="w-3 h-3 text-gray-400" />
|
||||
{task.assigned_to}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase ${
|
||||
task.status === 'completed' ? 'bg-emerald-100 text-emerald-700' :
|
||||
task.status === 'pending' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{task.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
{task.action_taken ? (
|
||||
<span className="px-1.5 py-0.5 bg-gray-100 text-gray-700 rounded font-bold uppercase text-[9px]">
|
||||
{task.action_taken}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400 text-[10px] italic">No action</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-[11px] text-gray-500 font-medium">
|
||||
{task.completed_at ? formatDateTime(task.completed_at) : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-10">
|
||||
<p className="text-sm text-gray-500">No workflow tracking information available.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-end">
|
||||
<SecondaryButton onClick={() => setShowWorkflowTracker(false)}>
|
||||
Close Tracker
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -34,6 +34,8 @@ export interface CreateDocumentPayload {
|
||||
file_size?: number;
|
||||
mime_type?: string;
|
||||
file_hash?: string;
|
||||
source_module?: string;
|
||||
source_module_id?: string;
|
||||
}
|
||||
|
||||
export interface FileAttachmentItem {
|
||||
@ -72,6 +74,8 @@ export interface CreateDocumentVersionPayload {
|
||||
file_size?: number;
|
||||
mime_type?: string;
|
||||
file_hash?: string;
|
||||
source_module?: string;
|
||||
source_module_id?: string;
|
||||
}
|
||||
|
||||
export const documentService = {
|
||||
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
UpdateWorkflowDefinitionData,
|
||||
WorkflowDefinitionsResponse,
|
||||
WorkflowDefinitionResponse,
|
||||
WorkflowInstanceResponse,
|
||||
WorkflowDeleteResponse
|
||||
} from '@/types/workflow';
|
||||
|
||||
@ -35,6 +36,25 @@ class WorkflowService {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getInstance(id: string, tenantId?: string): Promise<WorkflowInstanceResponse> {
|
||||
const params = tenantId ? { tenantId: tenantId } : {};
|
||||
const response = await apiClient.get<WorkflowInstanceResponse>(`${this.baseUrl}/instances/${id}`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async listInstances(params: {
|
||||
entity_type?: string;
|
||||
entity_id?: string;
|
||||
status?: string;
|
||||
source_module?: string;
|
||||
source_module_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{ success: boolean; data: any[]; pagination: { total: number; limit: number; offset: number } }> {
|
||||
const response = await apiClient.get(`${this.baseUrl}/instances`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createDefinition(data: CreateWorkflowDefinitionData, tenantId?: string): Promise<WorkflowDefinitionResponse> {
|
||||
const params = tenantId ? { tenantId: tenantId } : {};
|
||||
const response = await apiClient.post<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions`, data, { params });
|
||||
|
||||
@ -22,6 +22,7 @@ export interface DocumentSummary {
|
||||
owner?: string | null;
|
||||
next_review_date?: string | null;
|
||||
updated_at?: string;
|
||||
module_name?: string | null;
|
||||
}
|
||||
|
||||
export interface DocumentDetail {
|
||||
@ -54,6 +55,7 @@ export interface DocumentDetail {
|
||||
created_by?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
module_name?: string | null;
|
||||
}
|
||||
|
||||
export interface DocumentVersion {
|
||||
@ -70,6 +72,7 @@ export interface DocumentVersion {
|
||||
approved_at?: string | null;
|
||||
created_by?: string | null;
|
||||
created_at?: string;
|
||||
module_name?: string | null;
|
||||
}
|
||||
|
||||
export interface DocumentListResponse {
|
||||
|
||||
@ -98,6 +98,57 @@ export interface WorkflowDefinitionResponse {
|
||||
data: WorkflowDefinition;
|
||||
}
|
||||
|
||||
export interface WorkflowInstance {
|
||||
id: string;
|
||||
workflow: {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
};
|
||||
entity: {
|
||||
type: string;
|
||||
id: string;
|
||||
name: string | null;
|
||||
};
|
||||
current_step: {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
};
|
||||
status: string;
|
||||
context: Record<string, any>;
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
step: string;
|
||||
assigned_to: string;
|
||||
status: string;
|
||||
action_taken: string | null;
|
||||
completed_at: string | null;
|
||||
due_at: string | null;
|
||||
}>;
|
||||
available_actions: Array<{
|
||||
action: string;
|
||||
name: string;
|
||||
requires_signature: boolean;
|
||||
requires_comment: boolean;
|
||||
next_step: string;
|
||||
}>;
|
||||
started_by: {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
started_at: string;
|
||||
completed_at: string | null;
|
||||
cancelled_at: string | null;
|
||||
cancelled_by: string | null;
|
||||
cancellation_reason: string | null;
|
||||
}
|
||||
|
||||
export interface WorkflowInstanceResponse {
|
||||
success: boolean;
|
||||
data: WorkflowInstance;
|
||||
}
|
||||
|
||||
export interface WorkflowDeleteResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user