delete a template + update template

This commit is contained in:
hamid zarghami
2025-07-15 15:55:35 +03:30
parent ed8cd1392d
commit 019e96cb9d
13 changed files with 226 additions and 121 deletions
+55 -14
View File
@@ -1,6 +1,6 @@
import { FC, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { useLocation, useParams } from 'react-router-dom'
import { SettingTabEnum } from './enum/SettingEnum'
import Personality from './personality/Personality'
import Domain from './domain/Domain'
@@ -14,17 +14,30 @@ import { TickCircle } from 'iconsax-react'
import { usePersonalityStore } from './personality/store/Store'
import { exportPersonalityToHTML } from './personality/utils/ExportHTML'
import Input from '@/components/Input'
import { useSaveTemplate } from './personality/hooks/usePersonalityData'
import { useSaveTemplate, useUpdateTemplate, useGetTemplate } from './personality/hooks/usePersonalityData'
import { toast } from '@/components/Toast'
const Setting: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const { data } = usePersonalityStore()
const location = useLocation()
const [activeTab, setActiveTab] = useState<SettingTabEnum>(SettingTabEnum.SETTING_DOMAIN)
const [templateName, setTemplateName] = useState('')
const { mutate: saveTemplate, isPending } = useSaveTemplate()
const { mutate: saveTemplate, isPending: isSaving } = useSaveTemplate()
const { mutate: updateTemplate, isPending: isUpdating } = useUpdateTemplate()
const { data: template } = useGetTemplate(id || '')
const isEditMode = !!id
const isLoading = isSaving || isUpdating
// پر کردن نام قالب در حالت edit
useEffect(() => {
if (isEditMode && template?.data?.template?.name) {
setTemplateName(template.data.template.name)
}
}, [isEditMode, template])
const getTabFromPath = (pathname: string): SettingTabEnum => {
switch (pathname) {
@@ -39,6 +52,10 @@ const Setting: FC = () => {
case Paths.settingSignature:
return SettingTabEnum.SETTING_SIGNATURE
default:
// بررسی personality با id
if (pathname.startsWith(Paths.settingPersonality + '/')) {
return SettingTabEnum.SETTING_PERSONALITY
}
return SettingTabEnum.SETTING_DOMAIN
}
}
@@ -84,15 +101,39 @@ const Setting: FC = () => {
}
const html = await exportPersonalityToHTML(data)
saveTemplate({
name: templateName,
rawHtml: html,
structure: data
}, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
}
})
if (isEditMode) {
// حالت edit - استفاده از updateTemplate
updateTemplate({
id: id!,
params: {
name: templateName,
rawHtml: html,
structure: data
}
}, {
onSuccess: (data) => {
toast(data?.data?.message || 'قالب با موفقیت بروزرسانی شد', 'success')
},
onError: () => {
toast('خطا در بروزرسانی قالب', 'error')
}
})
} else {
// حالت جدید - استفاده از saveTemplate
saveTemplate({
name: templateName,
rawHtml: html,
structure: data
}, {
onSuccess: (data) => {
toast(data?.data?.message || 'قالب با موفقیت ایجاد شد', 'success')
},
onError: () => {
toast('خطا در ایجاد قالب', 'error')
}
})
}
}
// const handleExportHTML = async () => {
@@ -124,11 +165,11 @@ const Setting: FC = () => {
variant='secondary'
className='border border-black w-fit px-7'
onClick={handleSave}
loading={isPending}
loading={isLoading}
>
<div className='flex items-center gap-2'>
<TickCircle size={18} color='black' />
{t('save')}
{isEditMode ? 'بروزرسانی' : t('save')}
</div>
</Button>
</div>
+14 -1
View File
@@ -1,9 +1,22 @@
import { FC } from 'react'
import { FC, useEffect } from 'react'
import HeaderSection from './components/HeaderSection'
import ContentSection from './components/ContentSection'
import FooterSection from './components/FooterSection'
import { useParams } from 'react-router-dom'
import { usePersonalityStore } from './store/Store'
import { useGetTemplate } from './hooks/usePersonalityData'
const Personality: FC = () => {
const { id } = useParams()
const { setData } = usePersonalityStore()
const { data: template } = useGetTemplate(id || '')
useEffect(() => {
if (id && template?.data?.template?.structure) {
setData(template.data.template.structure)
}
}, [id, template, setData])
return (
<div className='flex-1 bg-white rounded-4xl p-8'>
<table width="100%" cellPadding="0" cellSpacing="0" border={0} style={{ borderCollapse: 'collapse' }}>
+4 -4
View File
@@ -98,28 +98,28 @@ const PersonalitySidebar: FC = () => {
/>
<Text
size={22}
color={selectedElement?.type === ElementType.TEXT ? '#0038FF' : 'black'}
color={'black'}
variant={SideBarTab.TEXT === active ? 'Bold' : 'Outline'}
onClick={() => setActive(SideBarTab.TEXT)}
className='cursor-pointer'
/>
<LinkSquare
size={22}
color={selectedElement?.type === ElementType.BUTTON ? '#0038FF' : 'black'}
color={'black'}
variant={SideBarTab.BUTTON === active ? 'Bold' : 'Outline'}
onClick={() => setActive(SideBarTab.BUTTON)}
className='cursor-pointer'
/>
<Gallery
size={22}
color={selectedElement?.type === ElementType.IMAGE ? '#0038FF' : 'black'}
color={'black'}
variant={SideBarTab.IMAGE === active ? 'Bold' : 'Outline'}
onClick={() => setActive(SideBarTab.IMAGE)}
className='cursor-pointer'
/>
<Profile2User
size={22}
color={selectedElement?.type === ElementType.SOCIAL ? '#0038FF' : 'black'}
color={'black'}
variant={SideBarTab.SOCIAL === active ? 'Bold' : 'Outline'}
onClick={() => setActive(SideBarTab.SOCIAL)}
className='cursor-pointer'
@@ -13,80 +13,47 @@ const ContentSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
console.log('🟢 ContentSection - Current state:', {
activeSection,
activeItemIndex,
contentColumnsCount: data.content.columnsCount,
contentItemsLength: data.content.items.length
});
// Early return if data is not available
if (!data || !data.content) {
return (
<tr>
<td style={{ height: '246px', textAlign: 'center', verticalAlign: 'middle', paddingTop: '16px' }}>
<div>در حال بارگذاری...</div>
</td>
</tr>
)
}
const handleSectionClick = (section: SectionName, e: React.MouseEvent) => {
console.log('🟢 ContentSection - handleSectionClick called:', {
section,
event: e,
target: e.target,
currentTarget: e.currentTarget
});
// اگر کلیک روی فضای خالی باشد (نه روی content elements)
const target = e.target as HTMLElement;
const isClickOnContent = target.closest('[data-element-type]') !== null;
console.log('🟢 ContentSection - Section click analysis:', {
isClickOnContent,
targetTagName: target.tagName,
targetClasses: target.className
});
if (!isClickOnContent) {
console.log('🟢 ContentSection - Setting active section:', section);
setActiveSection(section)
} else {
console.log('🟢 ContentSection - Click on content element, not setting section');
}
}
const handleItemClick = (index: number, e: React.MouseEvent) => {
console.log('🟢 ContentSection - handleItemClick called:', {
index,
event: e,
target: e.target,
currentTarget: e.currentTarget
});
// Check if click is on a content element (text, button, image, social)
const target = e.target as HTMLElement;
const isClickOnContent = target.closest('[data-element-type]') !== null;
console.log('🟢 ContentSection - Click analysis:', {
isClickOnContent,
targetTagName: target.tagName,
targetClasses: target.className,
closestDataElement: target.closest('[data-element-type]')
});
// Only select column if not clicking on content elements
if (!isClickOnContent) {
console.log('🟢 ContentSection - Selecting column:', {
index,
section: SectionName.CONTENT,
currentActiveSection: activeSection,
currentActiveItemIndex: activeItemIndex
});
e.stopPropagation()
// Only set active section if we're not already in content section
if (activeSection !== SectionName.CONTENT) {
console.log('🟢 ContentSection - Switching to content section');
setActiveSection(SectionName.CONTENT)
}
// Always set the active item index
console.log('🟢 ContentSection - Setting active item index to:', index);
setActiveItemIndex(index)
} else {
console.log('🟢 ContentSection - Click on content element, not selecting column');
}
}
@@ -13,6 +13,17 @@ const FooterSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
// Early return if data is not available
if (!data || !data.footer) {
return (
<tr>
<td style={{ height: '123px', textAlign: 'center', verticalAlign: 'middle', paddingTop: '16px' }}>
<div>در حال بارگذاری...</div>
</td>
</tr>
)
}
console.log('🔴 FooterSection - Current state:', {
activeSection,
activeItemIndex,
@@ -13,80 +13,50 @@ const HeaderSection: FC = () => {
const { t } = useTranslation()
const { activeSection, activeItemIndex, data, setActiveSection, setActiveItemIndex } = usePersonalityStore()
console.log('🔵 HeaderSection - Current state:', {
activeSection,
activeItemIndex,
headerColumnsCount: data.header.columnsCount,
headerItemsLength: data.header.items.length
});
// Early return if data is not available
if (!data || !data.header) {
return (
<tr>
<td style={{ height: '123px', textAlign: 'center', verticalAlign: 'middle' }}>
<div>در حال بارگذاری...</div>
</td>
</tr>
)
}
const handleSectionClick = (section: SectionName, e: React.MouseEvent) => {
console.log('🔵 HeaderSection - handleSectionClick called:', {
section,
event: e,
target: e.target,
currentTarget: e.currentTarget
});
// اگر کلیک روی فضای خالی باشد (نه روی content elements)
const target = e.target as HTMLElement;
const isClickOnContent = target.closest('[data-element-type]') !== null;
console.log('🔵 HeaderSection - Section click analysis:', {
isClickOnContent,
targetTagName: target.tagName,
targetClasses: target.className
});
if (!isClickOnContent) {
console.log('🔵 HeaderSection - Setting active section:', section);
setActiveSection(section)
} else {
console.log('🔵 HeaderSection - Click on content element, not setting section');
}
}
const handleItemClick = (index: number, e: React.MouseEvent) => {
console.log('🔵 HeaderSection - handleItemClick called:', {
index,
event: e,
target: e.target,
currentTarget: e.currentTarget
});
// Check if click is on a content element (text, button, image, social)
const target = e.target as HTMLElement;
const isClickOnContent = target.closest('[data-element-type]') !== null;
console.log('🔵 HeaderSection - Click analysis:', {
isClickOnContent,
targetTagName: target.tagName,
targetClasses: target.className,
closestDataElement: target.closest('[data-element-type]')
});
// Only select column if not clicking on content elements
if (!isClickOnContent) {
console.log('🔵 HeaderSection - Selecting column:', {
index,
section: SectionName.HEADER,
currentActiveSection: activeSection,
currentActiveItemIndex: activeItemIndex
});
e.stopPropagation()
// Only set active section if we're not already in header section
if (activeSection !== SectionName.HEADER) {
console.log('🔵 HeaderSection - Switching to header section');
setActiveSection(SectionName.HEADER)
}
// Always set the active item index
console.log('🔵 HeaderSection - Setting active item index to:', index);
setActiveItemIndex(index)
} else {
console.log('🔵 HeaderSection - Click on content element, not selecting column');
}
}
@@ -2,7 +2,10 @@ import { FC, useEffect, useState } from 'react'
import { TemplateResponseType } from '../types/Types'
import Radio from '@/components/Radio'
import { Brush2, Trash } from 'iconsax-react'
import { useSetSelectedTemplate } from '../hooks/usePersonalityData'
import { useSetSelectedTemplate, useDeleteTemplate } from '../hooks/usePersonalityData'
import { Link } from 'react-router-dom'
import { Paths } from '@/utils/Paths'
import ModalConfrim from '@/components/ModalConfrim'
const Templete: FC<{
item: TemplateResponseType
@@ -10,14 +13,24 @@ const Templete: FC<{
}> = ({ item, selectedTemplateId }) => {
const [isActive, setIsActive] = useState(item.selected)
const [isShowDeleteModal, setIsShowDeleteModal] = useState(false)
const { mutate: setSelectedTemplate } = useSetSelectedTemplate()
const { mutate: deleteTemplate, isPending: isDeleting } = useDeleteTemplate()
const handleChangeSelectedTemplate = (id: string) => {
setSelectedTemplate(id, {
})
}
const handleDeleteTemplate = () => {
deleteTemplate(item.id, {
onSuccess: () => {
setIsShowDeleteModal(false)
}
})
}
useEffect(() => {
if (selectedTemplateId) {
setIsActive(selectedTemplateId === item.id)
@@ -27,19 +40,34 @@ const Templete: FC<{
}, [item.selected, selectedTemplateId, item.id])
return (
<div key={item.id} className='flex-1 bg-white rounded-3xl p-4'>
<div className='h-[140px] bg-gray-200 rounded-3xl'></div>
<div className='mt-4 flex justify-between items-center'>
<div className='flex gap-2 items-center'>
<Radio isActive={isActive} onChange={handleChangeSelectedTemplate} value={item.id} />
<div className='text-sm'>{item.name}</div>
</div>
<div className='flex gap-2 items-center'>
<Brush2 size={20} color='black' />
<Trash size={20} color='red' />
<>
<div key={item.id} className='flex-1 bg-white rounded-3xl p-4'>
<div className='h-[140px] bg-gray-200 rounded-3xl'></div>
<div className='mt-4 flex justify-between items-center'>
<div className='flex gap-2 items-center'>
<Radio isActive={isActive} onChange={handleChangeSelectedTemplate} value={item.id} />
<div className='text-sm'>{item.name}</div>
</div>
<div className='flex gap-2 items-center'>
<Link to={`${Paths.settingPersonality}/${item.id}`}>
<Brush2 size={20} color='black' />
</Link>
<button onClick={() => setIsShowDeleteModal(true)} className='p-1'>
<Trash size={20} color='red' />
</button>
</div>
</div>
</div>
</div>
<ModalConfrim
isOpen={isShowDeleteModal}
close={() => setIsShowDeleteModal(false)}
onConfrim={handleDeleteTemplate}
isLoading={isDeleting}
title="حذف قالب"
label={`آیا از حذف قالب "${item.name}" اطمینان دارید؟`}
/>
</>
)
}
@@ -228,7 +228,6 @@ const TextSidebar: FC = () => {
<div className='space-y-3'>
<Button
onClick={handleUpdateText}
className='bg-[#0038FF] text-white w-full'
disabled={!value.trim()}
>
<div className='flex justify-center items-center gap-2'>
@@ -1,6 +1,10 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/Service";
import { TemplateType, TemplatesResponseType } from "../types/Types";
import {
TemplateType,
TemplatesResponseType,
GetTemplateResponse,
} from "../types/Types";
export const useSaveTemplate = () => {
return useMutation({
@@ -20,3 +24,29 @@ export const useSetSelectedTemplate = () => {
mutationFn: (id: string) => api.setSelectedTemplate(id),
});
};
export const useGetTemplate = (id: string) => {
return useQuery<GetTemplateResponse>({
queryKey: ["template", id],
queryFn: () => api.getTemplate(id),
enabled: !!id,
});
};
export const useUpdateTemplate = () => {
return useMutation({
mutationFn: ({ id, params }: { id: string; params: TemplateType }) =>
api.updateTemplate({ id, params }),
});
};
export const useDeleteTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.deleteTemplate(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["templates"] });
},
});
};
@@ -15,3 +15,24 @@ export const setSelectedTemplate = async (id: string) => {
const { data } = await axios.patch(`/templates/${id}/set-selected`);
return data;
};
export const getTemplate = async (id: string) => {
const { data } = await axios.get(`/templates/${id}`);
return data;
};
export const updateTemplate = async ({
id,
params,
}: {
id: string;
params: TemplateType;
}) => {
const { data } = await axios.patch(`/templates/${id}`, params);
return data;
};
export const deleteTemplate = async (id: string) => {
const { data } = await axios.delete(`/templates/${id}`);
return data;
};
@@ -26,6 +26,11 @@ export const usePersonalityStore = create<PersonalityStore>((set) => ({
selectedElement: null,
isEditMode: false,
setData: (data: PersonalityDataType) =>
set(() => ({
data,
})),
setActiveSection: (section: SectionName) =>
set((state) => {
console.log("🏪 Store - setActiveSection called:", {
@@ -168,6 +168,7 @@ export type PersonalityStore = {
selectedElement: SelectedElement; // New field for element selection
isEditMode: boolean; // New field for edit mode
setData: (data: PersonalityDataType) => void;
setActiveSection: (section: SectionName) => void;
setActiveItemIndex: (index: number) => void;
setSelectedElement: (element: SelectedElement) => void; // New method
@@ -270,6 +271,24 @@ export type TemplateType = {
structure: PersonalityDataType;
};
export type GetTemplateResponse = {
statusCode: number;
success: boolean;
data: {
template: {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
name: string;
structure: PersonalityDataType;
currentVersion: string;
selected: boolean;
business: BusinessType;
};
};
};
// API Response Types
export type BusinessType = {
id: string;
+1
View File
@@ -16,6 +16,7 @@ const AppRouter: FC = () => {
<Route path={Paths.settingPersonality} element={<Setting />} />
<Route path={Paths.settingSignature} element={<Setting />} />
<Route path={Paths.settingPersonalityList} element={<List />} />
<Route path={`${Paths.settingPersonality}/:id`} element={<Setting />} />
<Route path={Paths.emailBuilder} element={<EmailBuilder />} />
</Routes>
)