mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 01:55:41 +00:00
fix(backoffice): scope permission copying to the selected organization
"Copy permissions from" listed every position type in every
organization, because the dropdown read an unfiltered GET
/position-types. IAM exposes no organization-scoped route and carries no
organizationId on a position type, so the list is now narrowed
client-side to the built-in (isSystem) types plus those whose unit
belongs to the selected organization, with the type being edited
excluded.
Also in position management:
- Invalidate every position-type cache key root after a mutation. React
Query matches prefixes element by element, so ["position-type"] never
reached ["position-types-common", ...] and the department pickers kept
serving a stale list. invalidatePositionTypeQueries() covers all three
roots and is shared by the hook and the form.
- Drop getByOrganizationId and getCommonTypesByOrganizationId. Both
issued the same requests as their unit counterparts and had no callers.
- Surface errors that were being swallowed. Three mutations had empty
onError handlers, hiding IAM's 403 for built-in position types, and
CreatePositionForm's bare catch discarded the reason for every failure.
- Move organization and unit into the zod schema so they validate with
translated messages and inline errors instead of an ad-hoc toast, and
keep submit disabled through the permission-assignment call that
follows the save.
- Report the two outcomes the form used to hide: a save that succeeded
while permission assignment failed, and clearing every permission,
which assign-seconds-for-first cannot express.
- Fix the list page's loading and error states, which rendered the
"Add User" string as a spinner, ignored the unit-scoped query, and
left the export button stuck after a failed download.
- Halve PermissionSearch's requests. It fetched 50 rows, read the total
off the response and immediately refetched, and it re-filtered results
on the undebounced term, blanking the list while typing.
Remove the three record toggles. They never worked: IAM's
PositionTypeConfiguration holds only { id, organizationId,
positionTypeId, timeframe } in every published build, canAssignRecord
and canCreateBankRecord exist nowhere in the package, and the global
ValidationPipe runs with forbidNonWhitelisted, so every write was a 400.
The reads were broken too, passing a positionTypeId to a route that
filters on organizationId. A TODO records where the real flag lives:
PositionConfiguration.canReceiveRecord, keyed by positionId.
Delete ActionsColumn.tsx, which had no references.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
CreatePositionTypePayload,
|
||||
PositionRequest,
|
||||
@@ -23,10 +28,22 @@ interface positionParams {
|
||||
interface UsePositionTypeManagerProps {
|
||||
id?: string;
|
||||
unitId?: string;
|
||||
organizationId?: string;
|
||||
params?: positionParams; // 👈 we expected query params to be passed like this
|
||||
}
|
||||
|
||||
/**
|
||||
* Every cache key this hook writes under. React Query matches key prefixes
|
||||
* element by element, so `["position-type"]` does NOT reach
|
||||
* `["position-types-common", ...]` — each root has to be listed. Anything that
|
||||
* mutates a position type should call this rather than hand-picking keys, or
|
||||
* the department pickers (which read the "-common" queries) go stale.
|
||||
*/
|
||||
export const invalidatePositionTypeQueries = (queryClient: QueryClient) => {
|
||||
["position-types", "position-type", "position-types-common"].forEach(
|
||||
(root) => queryClient.invalidateQueries({ queryKey: [root] }),
|
||||
);
|
||||
};
|
||||
|
||||
export const usePositionTypes = ({
|
||||
id,
|
||||
params = {
|
||||
@@ -35,11 +52,11 @@ export const usePositionTypes = ({
|
||||
orderBy: "createdAt:Desc",
|
||||
},
|
||||
unitId,
|
||||
organizationId,
|
||||
}: UsePositionTypeManagerProps = {}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const { handleError } = useErrorHandler(t);
|
||||
const invalidateAll = () => invalidatePositionTypeQueries(queryClient);
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ["position-types", params],
|
||||
queryFn: () => positionTypeService.getAll(params).then((res) => res.data),
|
||||
@@ -70,38 +87,6 @@ export const usePositionTypes = ({
|
||||
enabled: !!unitId,
|
||||
});
|
||||
|
||||
// Position types by organization ID
|
||||
const {
|
||||
data: positionTypeByOrgId,
|
||||
isLoading: isLoadingOrgPosition,
|
||||
isError: isErrorOrgPosition,
|
||||
refetch: refetchOrgPosition,
|
||||
} = useQuery<PositionTypesListResponse | undefined>({
|
||||
queryKey: ["position-type-org", organizationId, params],
|
||||
queryFn: async () => {
|
||||
if (!organizationId) return undefined;
|
||||
const res = await positionTypeService.getByOrganizationId(organizationId, params);
|
||||
return res.data as PositionTypesListResponse | undefined;
|
||||
},
|
||||
enabled: !!organizationId,
|
||||
});
|
||||
|
||||
// Common types with organization ID (includes both org-specific and common types)
|
||||
const {
|
||||
data: commonPositionTypesByOrgId,
|
||||
isLoading: isLoadingCommonOrgTypes,
|
||||
isError: isErrorCommonOrgTypes,
|
||||
refetch: refetchCommonOrgTypes,
|
||||
} = useQuery<PositionTypesListResponse | undefined>({
|
||||
queryKey: ["position-types-common-org", organizationId, params],
|
||||
queryFn: async () => {
|
||||
if (!organizationId) return undefined;
|
||||
const res = await positionTypeService.getCommonTypesByOrganizationId(organizationId, params);
|
||||
return res.data as PositionTypesListResponse | undefined;
|
||||
},
|
||||
enabled: !!organizationId,
|
||||
});
|
||||
|
||||
// Common types with unit ID (includes both unit-specific and common types)
|
||||
const {
|
||||
data: commonPositionTypes,
|
||||
@@ -123,15 +108,16 @@ export const usePositionTypes = ({
|
||||
mutationFn: (payload: CreatePositionTypePayload) =>
|
||||
positionTypeService.create(payload),
|
||||
onSuccess: () => {
|
||||
toast.success("Position type created");
|
||||
queryClient.invalidateQueries({ queryKey: ["position-types"] });
|
||||
toast.success(t("contentManagement.positionTypeCreated"));
|
||||
invalidateAll();
|
||||
},
|
||||
onError: (error) => {
|
||||
handleError(error);
|
||||
},
|
||||
});
|
||||
|
||||
// Update
|
||||
// Update. IAM answers 403 `position_type_not_allowed_to_update` for built-in
|
||||
// (isSystem) types, so the error has to reach the user.
|
||||
const updatePositionType = useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
@@ -141,11 +127,12 @@ export const usePositionTypes = ({
|
||||
data: UpdatePositionTypePayload;
|
||||
}) => positionTypeService.update(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success("Position type updated");
|
||||
queryClient.invalidateQueries({ queryKey: ["position-types"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["position-type", id] });
|
||||
toast.success(t("contentManagement.positionTypeUpdated"));
|
||||
invalidateAll();
|
||||
},
|
||||
onError: (error) => {
|
||||
handleError(error);
|
||||
},
|
||||
onError: () => {},
|
||||
});
|
||||
|
||||
//update positon from to
|
||||
@@ -153,30 +140,32 @@ export const usePositionTypes = ({
|
||||
mutationFn: ({ toId, fromId }: { toId: string; fromId: string }) =>
|
||||
positionTypeService.updateFromto(toId, fromId),
|
||||
onSuccess: () => {
|
||||
toast.success("Position type migration updated");
|
||||
queryClient.invalidateQueries({ queryKey: ["position-types-to"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["position-type", id] });
|
||||
toast.success(t("contentManagement.positionTypeMigrated"));
|
||||
invalidateAll();
|
||||
},
|
||||
onError: (error) => {
|
||||
handleError(error);
|
||||
},
|
||||
onError: () => {},
|
||||
});
|
||||
//update all postions
|
||||
const migratePositionsByPositions = useMutation({
|
||||
mutationFn: ({ id, data }: { id: string; data: PositionRequest }) =>
|
||||
positionTypeService.updateByPostion(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success("Position type migration updated");
|
||||
queryClient.invalidateQueries({ queryKey: ["position-types-migration"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["position-type", id] });
|
||||
toast.success(t("contentManagement.positionTypeMigrated"));
|
||||
invalidateAll();
|
||||
},
|
||||
onError: (error) => {
|
||||
handleError(error);
|
||||
},
|
||||
onError: () => {},
|
||||
});
|
||||
|
||||
// Delete
|
||||
// Delete. Also 403s for built-in types.
|
||||
const deletePositionType = useMutation({
|
||||
mutationFn: (id: string) => positionTypeService.delete(id),
|
||||
onSuccess: () => {
|
||||
toast.success("Position type deleted");
|
||||
queryClient.invalidateQueries({ queryKey: ["position-types"] });
|
||||
toast.success(t("contentManagement.positionTypeDeleted"));
|
||||
invalidateAll();
|
||||
},
|
||||
onError: (error) => {
|
||||
handleError(error);
|
||||
@@ -205,16 +194,6 @@ export const usePositionTypes = ({
|
||||
refetchPosition,
|
||||
isErrorPosition,
|
||||
isLoadingPosition,
|
||||
// organization-based position types
|
||||
positionTypeByOrgId,
|
||||
refetchOrgPosition,
|
||||
isErrorOrgPosition,
|
||||
isLoadingOrgPosition,
|
||||
// common types with organization ID
|
||||
commonPositionTypesByOrgId,
|
||||
refetchCommonOrgTypes,
|
||||
isErrorCommonOrgTypes,
|
||||
isLoadingCommonOrgTypes,
|
||||
// common types with unit ID
|
||||
commonPositionTypes: commonPositionTypes?.items ?? [],
|
||||
isLoadingCommonTypes,
|
||||
|
||||
Reference in New Issue
Block a user