Make slug optional in admin create forms

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-24 15:58:58 +03:30
co-authored by Cursor
parent cca999baf9
commit 4c99389f5c
5 changed files with 43 additions and 30 deletions
+3 -3
View File
@@ -50,7 +50,7 @@ export type Business = {
export type CreateBusinessPayload = { export type CreateBusinessPayload = {
title: string; title: string;
slug: string; slug?: string;
categoryId: string; categoryId: string;
description?: string; description?: string;
contactPhone?: string; contactPhone?: string;
@@ -71,7 +71,7 @@ export type UpdateBusinessPayload = Partial<CreateBusinessPayload> & {
export type CreateCategoryPayload = { export type CreateCategoryPayload = {
title: string; title: string;
slug: string; slug?: string;
isActive?: boolean; isActive?: boolean;
}; };
@@ -92,7 +92,7 @@ export type ServiceTemplate = {
export type CreateServiceTemplatePayload = { export type CreateServiceTemplatePayload = {
title: string; title: string;
slug: string; slug?: string;
categoryId: string; categoryId: string;
description?: string; description?: string;
defaultDurationMinutes?: number; defaultDurationMinutes?: number;
+5
View File
@@ -14,3 +14,8 @@ export const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
export function isValidSlug(slug: string): boolean { export function isValidSlug(slug: string): boolean {
return SLUG_PATTERN.test(slug); return SLUG_PATTERN.test(slug);
} }
export function optionalSlug(value: string): string | undefined {
const trimmed = value.trim();
return trimmed || undefined;
}
+24 -16
View File
@@ -69,7 +69,13 @@
> >
<form class="space-y-4" @submit.prevent="onSaveBusiness"> <form class="space-y-4" @submit.prevent="onSaveBusiness">
<AppInput id="bizTitle" v-model="form.title" label="عنوان" required @blur="maybeSlug" /> <AppInput id="bizTitle" v-model="form.title" label="عنوان" required @blur="maybeSlug" />
<AppInput id="bizSlug" v-model="form.slug" label="اسلاگ" ltr required /> <AppInput
id="bizSlug"
v-model="form.slug"
label="اسلاگ (انگلیسی)"
ltr
placeholder="اختیاری — از عنوان ساخته می‌شود"
/>
<AppSelect <AppSelect
id="bizCategory" id="bizCategory"
v-model="form.categoryId" v-model="form.categoryId"
@@ -197,7 +203,7 @@ import AdminLayout from '@/layouts/AdminLayout.vue';
import { roleLabel } from '@/utils/format'; import { roleLabel } from '@/utils/format';
import { isValidIranMobile, normalizeIranMobile } from '@/utils/mobile'; import { isValidIranMobile, normalizeIranMobile } from '@/utils/mobile';
import { filterBySearch } from '@/utils/search'; import { filterBySearch } from '@/utils/search';
import { isValidSlug, slugify } from '@/utils/slug'; import { isValidSlug, optionalSlug, slugify } from '@/utils/slug';
const businesses = ref<Business[]>([]); const businesses = ref<Business[]>([]);
const searchQuery = ref(''); const searchQuery = ref('');
@@ -404,7 +410,8 @@ async function openAssign(biz: Business) {
} }
async function onSaveBusiness() { async function onSaveBusiness() {
if (!isValidSlug(form.value.slug)) { const slug = optionalSlug(form.value.slug);
if (slug && !isValidSlug(slug)) {
error.value = 'اسلاگ نامعتبر است.'; error.value = 'اسلاگ نامعتبر است.';
return; return;
} }
@@ -420,29 +427,30 @@ async function onSaveBusiness() {
try { try {
let businessId = editingId.value; let businessId = editingId.value;
const basePayload = {
title: form.value.title.trim(),
categoryId: form.value.categoryId,
description: form.value.description.trim() || null,
contactPhone: form.value.contactPhone.trim() || null,
address: form.value.address.trim() || null,
latitude: lat ?? null,
longitude: lng ?? null,
isActive: form.value.isActive,
};
if (businessId) { if (businessId) {
await updateBusiness(businessId, { await updateBusiness(businessId, {
title: form.value.title.trim(), ...basePayload,
slug: form.value.slug.trim(), ...(slug ? { slug } : {}),
categoryId: form.value.categoryId,
description: form.value.description.trim() || null,
contactPhone: form.value.contactPhone.trim() || null,
address: form.value.address.trim() || null,
latitude: lat ?? null,
longitude: lng ?? null,
isActive: form.value.isActive,
}); });
} else { } else {
const created = await createBusiness({ const created = await createBusiness({
title: form.value.title.trim(), ...basePayload,
slug: form.value.slug.trim(),
categoryId: form.value.categoryId,
description: form.value.description.trim() || undefined, description: form.value.description.trim() || undefined,
contactPhone: form.value.contactPhone.trim() || undefined, contactPhone: form.value.contactPhone.trim() || undefined,
address: form.value.address.trim() || undefined, address: form.value.address.trim() || undefined,
latitude: lat, latitude: lat,
longitude: lng, longitude: lng,
isActive: form.value.isActive, ...(slug ? { slug } : {}),
}); });
businessId = created.id; businessId = created.id;
} }
+6 -6
View File
@@ -68,8 +68,7 @@
v-model="form.slug" v-model="form.slug"
label="اسلاگ (انگلیسی)" label="اسلاگ (انگلیسی)"
ltr ltr
placeholder="car-wash" placeholder="اختیاری — از عنوان ساخته می‌شود"
required
/> />
<label class="flex items-center gap-2 text-sm text-text"> <label class="flex items-center gap-2 text-sm text-text">
<input v-model="form.isActive" type="checkbox" class="rounded border-border" /> <input v-model="form.isActive" type="checkbox" class="rounded border-border" />
@@ -102,7 +101,7 @@ import SearchInput from '@/components/ui/SearchInput.vue';
import AdminLayout from '@/layouts/AdminLayout.vue'; import AdminLayout from '@/layouts/AdminLayout.vue';
import { activeLabel } from '@/utils/format'; import { activeLabel } from '@/utils/format';
import { filterBySearch } from '@/utils/search'; import { filterBySearch } from '@/utils/search';
import { isValidSlug, slugify } from '@/utils/slug'; import { isValidSlug, optionalSlug, slugify } from '@/utils/slug';
const categories = ref<BusinessCategory[]>([]); const categories = ref<BusinessCategory[]>([]);
const searchQuery = ref(''); const searchQuery = ref('');
@@ -159,7 +158,8 @@ function maybeSlug() {
} }
async function onSave() { async function onSave() {
if (!isValidSlug(form.value.slug)) { const slug = optionalSlug(form.value.slug);
if (slug && !isValidSlug(slug)) {
error.value = 'اسلاگ فقط حروف کوچک انگلیسی و خط تیره باشد.'; error.value = 'اسلاگ فقط حروف کوچک انگلیسی و خط تیره باشد.';
return; return;
} }
@@ -169,13 +169,13 @@ async function onSave() {
if (editingId.value) { if (editingId.value) {
await updateCategory(editingId.value, { await updateCategory(editingId.value, {
title: form.value.title.trim(), title: form.value.title.trim(),
slug: form.value.slug.trim(), ...(slug ? { slug } : {}),
isActive: form.value.isActive, isActive: form.value.isActive,
}); });
} else { } else {
await createCategory({ await createCategory({
title: form.value.title.trim(), title: form.value.title.trim(),
slug: form.value.slug.trim(), ...(slug ? { slug } : {}),
isActive: form.value.isActive, isActive: form.value.isActive,
}); });
} }
+5 -5
View File
@@ -80,8 +80,7 @@
v-model="form.slug" v-model="form.slug"
label="اسلاگ (انگلیسی)" label="اسلاگ (انگلیسی)"
ltr ltr
placeholder="haircut" placeholder="اختیاری — از عنوان ساخته می‌شود"
required
/> />
<AppTextarea id="tplDesc" v-model="form.description" label="توضیحات" rows="2" /> <AppTextarea id="tplDesc" v-model="form.description" label="توضیحات" rows="2" />
<AppInput <AppInput
@@ -125,7 +124,7 @@ import SearchInput from '@/components/ui/SearchInput.vue';
import AdminLayout from '@/layouts/AdminLayout.vue'; import AdminLayout from '@/layouts/AdminLayout.vue';
import { activeLabel } from '@/utils/format'; import { activeLabel } from '@/utils/format';
import { filterBySearch } from '@/utils/search'; import { filterBySearch } from '@/utils/search';
import { isValidSlug, slugify } from '@/utils/slug'; import { isValidSlug, optionalSlug, slugify } from '@/utils/slug';
const categories = ref<BusinessCategory[]>([]); const categories = ref<BusinessCategory[]>([]);
const templates = ref<ServiceTemplate[]>([]); const templates = ref<ServiceTemplate[]>([]);
@@ -222,7 +221,8 @@ function maybeSlug() {
async function onSave() { async function onSave() {
if (!selectedCategoryId.value) return; if (!selectedCategoryId.value) return;
if (!isValidSlug(form.value.slug)) { const slug = optionalSlug(form.value.slug);
if (slug && !isValidSlug(slug)) {
error.value = 'اسلاگ فقط حروف کوچک انگلیسی و خط تیره باشد.'; error.value = 'اسلاگ فقط حروف کوچک انگلیسی و خط تیره باشد.';
return; return;
} }
@@ -236,7 +236,7 @@ async function onSave() {
try { try {
const payload = { const payload = {
title: form.value.title.trim(), title: form.value.title.trim(),
slug: form.value.slug.trim(), ...(slug ? { slug } : {}),
description: form.value.description.trim() || undefined, description: form.value.description.trim() || undefined,
defaultDurationMinutes: duration, defaultDurationMinutes: duration,
isActive: form.value.isActive, isActive: form.value.isActive,