Drag-to-reorder service cards (iOS jiggle mode)
Long-press a service tile (~500 ms) → 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; the new order is shared globally via
servicesTable.sortOrder.
Backend
- POST /api/services/reorder (admin-gated, requireAdmin)
- Validates full-set match (no missing/dup/unknown IDs)
- 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 rewritten
- PointerSensor (distance 6) + TouchSensor (delay 150 ms) — long-press
and tap-to-open stay distinct
- touch-none only applied while in edit mode so normal scrolling is
unaffected outside it
- exitEditMode awaits mutation + invalidate before flipping editMode,
preventing snap-back from stale react-query cache
- exitingRef guard against duplicate exit calls (Done button +
outside-tap handler firing for the same gesture)
- Non-admins can enter edit mode (haptic affordance) but local reorder
is silently reverted on exit
- i18n: services.editMode.{done,hint,saveFailed} added to ar.json/en.json
Pre-existing unrelated typecheck errors in push.ts and
executive-meeting-font-settings.ts are not touched by this change.
This commit is contained in:
@@ -643,6 +643,21 @@ export interface UpdateServiceBody {
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* New global service order. `orderedIds` must contain every
|
||||
existing service id exactly once. The server sets each
|
||||
service's `sortOrder` to its position in the list (0-based),
|
||||
so a later GET /services returns them in the same order.
|
||||
|
||||
*/
|
||||
export interface ReorderServicesBody {
|
||||
/**
|
||||
* @minItems 1
|
||||
* @maxItems 1000
|
||||
*/
|
||||
orderedIds: number[];
|
||||
}
|
||||
|
||||
export interface ServiceCategory {
|
||||
id: number;
|
||||
nameAr: string;
|
||||
|
||||
@@ -89,6 +89,7 @@ import type {
|
||||
PermissionAuditList,
|
||||
ReceivedNote,
|
||||
RegisterBody,
|
||||
ReorderServicesBody,
|
||||
ReplaceRolePermissionsBody,
|
||||
ReplyToNoteBody,
|
||||
RequestUploadUrlBody,
|
||||
@@ -2660,6 +2661,98 @@ export const useCreateService = <
|
||||
return useMutation(getCreateServiceMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists a new global `sortOrder` for the service catalogue by
|
||||
applying the index of each id in `orderedIds`. Admin-only.
|
||||
The body must contain every existing service id exactly once;
|
||||
partial reorders are rejected with 400 so the client can never
|
||||
accidentally drop services from the sort plan.
|
||||
|
||||
* @summary Replace the global service sort order (admin)
|
||||
*/
|
||||
export const getReorderServicesUrl = () => {
|
||||
return `/api/services/reorder`;
|
||||
};
|
||||
|
||||
export const reorderServices = async (
|
||||
reorderServicesBody: ReorderServicesBody,
|
||||
options?: RequestInit,
|
||||
): Promise<void> => {
|
||||
return customFetch<void>(getReorderServicesUrl(), {
|
||||
...options,
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(reorderServicesBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getReorderServicesMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof reorderServices>>,
|
||||
TError,
|
||||
{ data: BodyType<ReorderServicesBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof reorderServices>>,
|
||||
TError,
|
||||
{ data: BodyType<ReorderServicesBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["reorderServices"];
|
||||
const { mutation: mutationOptions, request: requestOptions } = options
|
||||
? options.mutation &&
|
||||
"mutationKey" in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey }, request: undefined };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof reorderServices>>,
|
||||
{ data: BodyType<ReorderServicesBody> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return reorderServices(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type ReorderServicesMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof reorderServices>>
|
||||
>;
|
||||
export type ReorderServicesMutationBody = BodyType<ReorderServicesBody>;
|
||||
export type ReorderServicesMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Replace the global service sort order (admin)
|
||||
*/
|
||||
export const useReorderServices = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof reorderServices>>,
|
||||
TError,
|
||||
{ data: BodyType<ReorderServicesBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof reorderServices>>,
|
||||
TError,
|
||||
{ data: BodyType<ReorderServicesBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getReorderServicesMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get service by ID
|
||||
*/
|
||||
|
||||
@@ -730,6 +730,33 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Service"
|
||||
|
||||
/services/reorder:
|
||||
post:
|
||||
operationId: reorderServices
|
||||
tags: [services]
|
||||
summary: Replace the global service sort order (admin)
|
||||
description: |
|
||||
Persists a new global `sortOrder` for the service catalogue by
|
||||
applying the index of each id in `orderedIds`. Admin-only.
|
||||
The body must contain every existing service id exactly once;
|
||||
partial reorders are rejected with 400 so the client can never
|
||||
accidentally drop services from the sort plan.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ReorderServicesBody"
|
||||
responses:
|
||||
"204":
|
||||
description: Sort order updated
|
||||
"400":
|
||||
description: Validation error or mismatched id set
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/services/{id}:
|
||||
get:
|
||||
operationId: getService
|
||||
@@ -4031,6 +4058,24 @@ components:
|
||||
sortOrder:
|
||||
type: integer
|
||||
|
||||
ReorderServicesBody:
|
||||
type: object
|
||||
description: |
|
||||
New global service order. `orderedIds` must contain every
|
||||
existing service id exactly once. The server sets each
|
||||
service's `sortOrder` to its position in the list (0-based),
|
||||
so a later GET /services returns them in the same order.
|
||||
properties:
|
||||
orderedIds:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 1000
|
||||
items:
|
||||
type: integer
|
||||
minimum: 1
|
||||
required:
|
||||
- orderedIds
|
||||
|
||||
ServiceCategory:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1117,6 +1117,29 @@ export const CreateServiceBody = zod.object({
|
||||
sortOrder: zod.number().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Persists a new global `sortOrder` for the service catalogue by
|
||||
applying the index of each id in `orderedIds`. Admin-only.
|
||||
The body must contain every existing service id exactly once;
|
||||
partial reorders are rejected with 400 so the client can never
|
||||
accidentally drop services from the sort plan.
|
||||
|
||||
* @summary Replace the global service sort order (admin)
|
||||
*/
|
||||
|
||||
export const reorderServicesBodyOrderedIdsMax = 1000;
|
||||
|
||||
export const ReorderServicesBody = zod
|
||||
.object({
|
||||
orderedIds: zod
|
||||
.array(zod.number().min(1))
|
||||
.min(1)
|
||||
.max(reorderServicesBodyOrderedIdsMax),
|
||||
})
|
||||
.describe(
|
||||
"New global service order. `orderedIds` must contain every\nexisting service id exactly once. The server sets each\nservice's `sortOrder` to its position in the list (0-based),\nso a later GET \/services returns them in the same order.\n",
|
||||
);
|
||||
|
||||
/**
|
||||
* @summary Get service by ID
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user