mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 18:55:42 +00:00
Merge branch 'dev' into freight/nati-2
# Conflicts: # apps/edr-freight-api/src/app.module.ts # apps/edr-freight-api/src/seed/freight-permissions.registry.ts # apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx # apps/edr-freight-web/backoffice/src/constants/URLS.ts # apps/edr-freight-web/backoffice/src/lib/permissions.ts
This commit is contained in:
@@ -33,6 +33,10 @@ export const FREIGHT_PERMS = {
|
||||
staffUsers: {
|
||||
view: "edr_freight_app:staff:users:view",
|
||||
},
|
||||
chat: {
|
||||
view: "edr_freight_app:chat:view",
|
||||
sync: "edr_freight_app:chat:sync",
|
||||
},
|
||||
bookings: {
|
||||
view: "edr_freight_app:bookings:view",
|
||||
create: "edr_freight_app:bookings:create",
|
||||
@@ -57,6 +61,7 @@ export const FREIGHT_PERMS = {
|
||||
wagonCancellationVoid: "edr_freight_app:bookings:wagon_cancellation_void",
|
||||
wagonCancellationRebook:
|
||||
"edr_freight_app:bookings:wagon_cancellation_rebook",
|
||||
approveConsolidation: "edr_freight_app:bookings:approve_consolidation",
|
||||
},
|
||||
contracts: {
|
||||
view: "edr_freight_app:contracts:view",
|
||||
@@ -213,6 +218,8 @@ export const FREIGHT_PERMS = {
|
||||
/** Train-builder detail Actions menu — each item its own grant. */
|
||||
changeLocomotives: "edr_freight_app:trains:change_locomotives",
|
||||
changeYard: "edr_freight_app:trains:change_yard",
|
||||
/** Move ONE coupled wagon to another yard from the Wagon order list. */
|
||||
changeWagonYard: "edr_freight_app:trains:change_wagon_yard",
|
||||
toggleActive: "edr_freight_app:trains:toggle_active",
|
||||
disband: "edr_freight_app:trains:disband",
|
||||
},
|
||||
@@ -378,6 +385,12 @@ export const FREIGHT_PERMS = {
|
||||
view: "edr_freight_app:settings:operations_standards:view",
|
||||
manage: "edr_freight_app:settings:operations_standards:manage",
|
||||
},
|
||||
// Whether Finance may settle invoices by hand, per currency. Finance holds
|
||||
// `view` (the worklist offers only enabled currencies); `manage` is admin.
|
||||
manualPayment: {
|
||||
view: "edr_freight_app:settings:manual_payment:view",
|
||||
manage: "edr_freight_app:settings:manual_payment:manage",
|
||||
},
|
||||
contractTemplates: {
|
||||
view: "edr_freight_app:settings:contract_templates:view",
|
||||
manage: "edr_freight_app:settings:contract_templates:manage",
|
||||
|
||||
58
apps/edr-freight-web/backoffice/src/lib/queryClient.test.ts
Normal file
58
apps/edr-freight-web/backoffice/src/lib/queryClient.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { queryClient } from "./queryClient";
|
||||
|
||||
/**
|
||||
* The MutationCache seeds `meta.updates` entries into the cache and then skips
|
||||
* exactly those keys when running `meta.invalidates`. Getting that skip wrong
|
||||
* silently reintroduces the refetch it exists to avoid, so it is worth pinning.
|
||||
*/
|
||||
const runOnSuccess = (meta: Record<string, unknown>, data: unknown, variables: unknown) => {
|
||||
const handler = (queryClient.getMutationCache() as unknown as {
|
||||
config: {
|
||||
onSuccess?: (
|
||||
data: unknown,
|
||||
variables: unknown,
|
||||
context: unknown,
|
||||
mutation: { meta?: Record<string, unknown> },
|
||||
) => void;
|
||||
};
|
||||
}).config.onSuccess;
|
||||
handler?.(data, variables, undefined, { meta });
|
||||
};
|
||||
|
||||
describe("MutationCache updates/invalidates", () => {
|
||||
it("writes the mutation response into the seeded key and leaves it fresh", () => {
|
||||
const seededKey = ["train-builder", "composition", "t1"] as const;
|
||||
const siblingKey = ["train-builder", "list", {}] as const;
|
||||
|
||||
queryClient.setQueryData(seededKey, { code: "STALE" });
|
||||
queryClient.setQueryData(siblingKey, { items: [] });
|
||||
|
||||
const response = { code: "FRESH" };
|
||||
runOnSuccess(
|
||||
{
|
||||
updates: (_v: unknown, d: unknown) => [[seededKey, d]],
|
||||
invalidates: () => [["train-builder"]],
|
||||
},
|
||||
response,
|
||||
{ id: "t1" },
|
||||
);
|
||||
|
||||
// Seeded key holds the response, and was NOT invalidated back to stale.
|
||||
expect(queryClient.getQueryData(seededKey)).toStrictEqual(response);
|
||||
expect(queryClient.getQueryState(seededKey)?.isInvalidated).toBe(false);
|
||||
|
||||
// Its siblings under the same root still get invalidated.
|
||||
expect(queryClient.getQueryState(siblingKey)?.isInvalidated).toBe(true);
|
||||
});
|
||||
|
||||
it("invalidates everything when a mutation declares no updates", () => {
|
||||
const key = ["train-builder", "composition", "t2"] as const;
|
||||
queryClient.setQueryData(key, { code: "X" });
|
||||
|
||||
runOnSuccess({ invalidates: () => [["train-builder"]] }, undefined, undefined);
|
||||
|
||||
expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MutationCache, QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import type { InvalidatesMeta } from "@/utils/endpoint";
|
||||
import type { InvalidatesMeta, UpdatesMeta } from "@/utils/endpoint";
|
||||
|
||||
/**
|
||||
* Single app-wide React Query client (do not nest additional providers).
|
||||
@@ -14,13 +14,37 @@ import type { InvalidatesMeta } from "@/utils/endpoint";
|
||||
export const queryClient = new QueryClient({
|
||||
mutationCache: new MutationCache({
|
||||
onSuccess: (data, variables, _context, mutation) => {
|
||||
// Seed first: endpoints that return the entity they just changed write it
|
||||
// straight into its cache key, so the screen updates from the response
|
||||
// instead of round-tripping for data it already holds.
|
||||
const updates = mutation.meta?.updates as UpdatesMeta | undefined;
|
||||
const seeded: readonly unknown[][] = [];
|
||||
if (typeof updates === "function") {
|
||||
for (const [queryKey, value] of updates(variables, data)) {
|
||||
queryClient.setQueryData(queryKey, value);
|
||||
seeded.push(queryKey as unknown[]);
|
||||
}
|
||||
}
|
||||
|
||||
const invalidates = mutation.meta?.invalidates as
|
||||
| InvalidatesMeta
|
||||
| undefined;
|
||||
if (typeof invalidates !== "function") return;
|
||||
|
||||
for (const queryKey of invalidates(variables, data)) {
|
||||
void queryClient.invalidateQueries({ queryKey });
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey,
|
||||
// A seeded key already holds the authoritative value from this very
|
||||
// response — invalidating it would refetch it right back.
|
||||
predicate: seeded.length
|
||||
? (query) =>
|
||||
!seeded.some(
|
||||
(key) =>
|
||||
key.length === query.queryKey.length &&
|
||||
key.every((part, i) => Object.is(part, query.queryKey[i])),
|
||||
)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
// Mutation failures are surfaced globally by the axios interceptor in
|
||||
|
||||
Reference in New Issue
Block a user