Drag-to-reorder service cards (iOS jiggle mode)
Long-press a service tile (~500 ms) on the Services page → grid enters
edit mode: every card jiggles, a floating "تم / Done" pill appears, and
cards become draggable via @dnd-kit. Drop on another tile to reorder.
Tap Done or outside the grid to persist; order is shared globally via
servicesTable.sortOrder.
Per the task default: admin-only. Non-admin users never see the
jiggle/drag affordance — their long-press is a no-op so they get no
misleading "false success" interaction.
Backend
- PATCH /api/services/reorder (admin-gated, requireAdmin)
- Validates full-set match (no missing/dup/unknown IDs) — full
catalogue rewrite, not a partial reorder
- Transactional sortOrder rewrite — no partial writes
- Route registered before /services/:id to avoid path-param collision
- OpenAPI op + ReorderServicesBody Zod schema → regenerated client
Frontend
- artifacts/tx-os/src/pages/services.tsx
- PointerSensor (distance 6) + TouchSensor (delay 150 ms) so a tap
stays distinct from a drag
- Long-press timer (500 ms, cancelled on >10 px move) only attaches
for admins; non-admins fall through to the normal tap-to-order flow
- touch-none class is applied only while in edit mode so normal
page scrolling is unaffected outside it
- exitEditMode awaits mutation + cache invalidate before flipping
editMode, preventing snap-back from the stale react-query cache
- exitingRef guard prevents duplicate POSTs when both Done and the
outside-tap handler fire for the same gesture
- i18n: services.editMode.{done,hint,saveFailed} in ar.json + en.json
This commit is contained in:
@@ -79,7 +79,7 @@ router.get("/service-categories", requireAuth, async (_req, res): Promise<void>
|
||||
// catalogue never observes a partially-applied order. Declared BEFORE
|
||||
// the `/services/:id` routes so Express doesn't try to parse "reorder"
|
||||
// as a numeric id and return a 400 from GetServiceParams.
|
||||
router.post("/services/reorder", requireAdmin, async (req, res): Promise<void> => {
|
||||
router.patch("/services/reorder", requireAdmin, async (req, res): Promise<void> => {
|
||||
const parsed = ReorderServicesBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
|
||||
@@ -72,6 +72,7 @@ type SortableServiceCardProps = {
|
||||
lang: string;
|
||||
editMode: boolean;
|
||||
isDragging: boolean;
|
||||
canEnterEditMode: boolean;
|
||||
onTapInView: () => void;
|
||||
onLongPress: () => void;
|
||||
unavailableLabel: string;
|
||||
@@ -82,6 +83,7 @@ function SortableServiceCard({
|
||||
lang,
|
||||
editMode,
|
||||
isDragging,
|
||||
canEnterEditMode,
|
||||
onTapInView,
|
||||
onLongPress,
|
||||
unavailableLabel,
|
||||
@@ -114,7 +116,7 @@ function SortableServiceCard({
|
||||
};
|
||||
|
||||
const handlePointerDown = (e: React.PointerEvent<HTMLButtonElement>) => {
|
||||
if (editMode) return;
|
||||
if (editMode || !canEnterEditMode) return;
|
||||
longPressed.current = false;
|
||||
pressStart.current = { x: e.clientX, y: e.clientY };
|
||||
pressTimer.current = window.setTimeout(() => {
|
||||
@@ -316,6 +318,9 @@ export default function ServicesPage() {
|
||||
};
|
||||
|
||||
const onLongPress = () => {
|
||||
// Reordering is admin-only — silently ignore long-press for everyone
|
||||
// else so non-admins never see a jiggle UI they can't persist.
|
||||
if (!isAdmin) return;
|
||||
if (!editMode) setEditMode(true);
|
||||
};
|
||||
|
||||
@@ -436,6 +441,7 @@ export default function ServicesPage() {
|
||||
lang={lang}
|
||||
editMode={editMode}
|
||||
isDragging={activeDragId === service.id}
|
||||
canEnterEditMode={isAdmin}
|
||||
onTapInView={() => {
|
||||
if (!service.isAvailable) return;
|
||||
setOrderTarget({
|
||||
|
||||
@@ -2680,7 +2680,7 @@ export const reorderServices = async (
|
||||
): Promise<void> => {
|
||||
return customFetch<void>(getReorderServicesUrl(), {
|
||||
...options,
|
||||
method: "POST",
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(reorderServicesBody),
|
||||
});
|
||||
|
||||
@@ -731,7 +731,7 @@ paths:
|
||||
$ref: "#/components/schemas/Service"
|
||||
|
||||
/services/reorder:
|
||||
post:
|
||||
patch:
|
||||
operationId: reorderServices
|
||||
tags: [services]
|
||||
summary: Replace the global service sort order (admin)
|
||||
|
||||
Reference in New Issue
Block a user