admin-panel: add shared modal/header/icon/delete-handler components

New src/components/common/:
- BaseFormModal.vue — the standard shell every converted add/edit modal will
  use: header=title, footer=buttons, body=form. Replaces the ad-hoc
  60%/70%/80% <a-modal> widths used across the codebase with 4 named sizes.
- ListPageHeader.vue — the add button that goes above every list page,
  gated on the same rid the create route used (modelled on the one place
  this already existed, ListUser.vue's add-user button).

New src/components/icons.ts: the legacy-Feather-name -> ant-icon map,
extracted out of TooltipBtn.vue so the sidebar can resolve the same names
through the same map instead of a second copy.

New src/composables/useDeleteHandler.ts: the delete-then-refetch-then-toast
handler that was copy-pasted verbatim in 6 list views (city/cityList.vue,
user/ListUser.vue, apps/category/List.vue, finical/list.vue,
apps/product/ProductList.vue, manage/Const.vue). Preserves each site's exact
success-message wording and message.success/info choice, and ProductList's
irregular POST-based delete endpoint.

Verified: pnpm build passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-14 15:18:17 +03:30
co-authored by Claude Sonnet 5
parent f0de478940
commit 02d92fc465
11 changed files with 321 additions and 101 deletions
@@ -0,0 +1,51 @@
// Shared "delete a row, refresh the list, show the server's refusal reason"
// handler. This exact try/catch body — api.delete, then either a success
// toast + refetch, or Modal.error with the server's message — was copy-pasted
// verbatim in 6 list views (city/cityList.vue, user/ListUser.vue,
// apps/category/List.vue, finical/list.vue, apps/product/ProductList.vue,
// manage/Const.vue). Centralised here as a composable rather than a component
// since it is pure behaviour with no template.
import { Modal, message } from 'ant-design-vue';
import api from '../api/client';
export function useDeleteHandler(options: {
/** Called after a confirmed, successful delete — typically `table.value?.fetch()`. */
onSuccess: () => void | Promise<void>;
successMessage?: string;
/** Some call sites used message.info rather than message.success; preserved
* per-site rather than silently unified. */
successType?: 'success' | 'info';
/** Shown when the server didn't return its own message. */
fallbackErrorMessage?: string;
}) {
const {
onSuccess,
successMessage = 'با موفقیت حذف شد',
successType = 'success',
fallbackErrorMessage = 'خطا در حذف',
} = options;
async function remove(url: string, opts?: { method?: 'delete' | 'post' }) {
try {
// ProductList's delete is a legacy irregular endpoint (POST, not
// DELETE) — see docs/ADMIN-PORT-PLAN.md's endpoint-mapping notes.
// `method` lets that call site keep using this composable too.
if (opts?.method === 'post') {
await api.post(url);
} else {
await api.delete(url);
}
message[successType](successMessage);
await onSuccess();
} catch (e: any) {
// The backend refuses deletion while other records still reference the
// row (e.g. a city with customers/branches, a brand with products).
Modal.error({
title: e?.response?.data?.message ?? fallbackErrorMessage,
okText: 'بستن',
});
}
}
return { remove };
}