Make slug optional in admin create forms
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
-3
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user