Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7eb3130
feat(query-key): create reference query key composable & improve quer…
piggggggggy Mar 14, 2025
053af79
fix(service-query): apply query invalidation (admin <-> workspace)
piggggggggy Mar 14, 2025
63ba054
refactor(query-key): refactor service/reference query key composable …
piggggggggy Mar 16, 2025
138387b
feat(query-client): create and register base query client
piggggggggy Mar 16, 2025
17f3385
feat(invalidator): create service/reference query global invalidator
piggggggggy Mar 16, 2025
1b0f7b6
refactor: improve query key type (service/reference)
piggggggggy Mar 17, 2025
a52f2e2
fix(admin-toggle): apply global query invalidation helper
piggggggggy Mar 17, 2025
6c0690f
feat(api-keys): create api base query-key
piggggggggy Mar 18, 2025
7e5a609
feat(query-key): create new api query-key generator with factory pattern
piggggggggy Mar 18, 2025
87af863
feat(ops-flow): create base query-key
piggggggggy Mar 18, 2025
f68d0ec
refactor(query-composable): improve query composable
piggggggggy Mar 18, 2025
87f275c
feat(reference-query): create reference query key composable
piggggggggy Mar 18, 2025
98500e1
feat(reference-query): create reference query composable
piggggggggy Mar 18, 2025
e6c17ed
feat(reference-query): refactor reference query
piggggggggy Mar 20, 2025
923e706
feat(reference-sync): create reference query sync composable
piggggggggy Mar 20, 2025
03d501f
fix(invalidator): refactor query invalidators
piggggggggy Mar 20, 2025
21311f8
feat(query-key): apply immutable object to api query key
piggggggggy Mar 20, 2025
2635d86
chore: small fix
piggggggggy Mar 21, 2025
edba5e0
chore: remove all index in resource keys
piggggggggy Mar 21, 2025
80bf23f
feat(use-api-query-key): optimize composable performance
piggggggggy Mar 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_modules.nosync/
# Dev tools
.DS_Store
.vscode
.cursor
.idea
*.swp
*.bak
Expand Down
100 changes: 78 additions & 22 deletions apps/web/src/api-clients/_common/composables/use-api-query-key.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,86 @@
import { computed, reactive } from 'vue';
import type { ComputedRef } from 'vue';
import { reactive, computed } from 'vue';

import type { QueryKey } from '@tanstack/vue-query';

import { API_QUERY_KEY_MAP } from '@/api-clients/_common/constants/api-query-key-map';
import type {
ResourceName, ServiceName, Verb,
APIQueryKeyMapService, APIQueryKeyMapResource, APIQueryKeyMapVerb, ServiceName, ResourceName, Verb,
} from '@/api-clients/_common/types/query-key-type';
import { useQueryKeyAppContext } from '@/query/_composables/use-query-key-app-context';
import { createImmutableObject } from '@/query/_helper/immutable-key-helper';
import type { QueryKeyArray } from '@/query/_types/query-key-type';

import { useAppContextStore } from '@/store/app-context/app-context-store';
import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store';

/**
* Generates a computed query key for API requests, incorporating global parameters.
*
* @param service - The service name, representing the API service scope (e.g., 'dashboard').
* @param resource - The resource name, specifying the target API resource (e.g., 'public-data-table').
* @param verb - The API action verb, defining the type of request (e.g., 'get', 'list', 'update').
* @param additionalGlobalParams - Optional additional global parameters (e.g., workspace ID, admin mode).
* @returns A computed reference to the query key array, structured as `[service, resource, verb, { globalParams }]`.
*
* ### Example Usage:
* ```ts
* const queryKey = useAPIQueryKey('dashboard', 'public-data-table', 'get');
* ```
* The generated query key ensures:
* - **Type safety**: Prevents invalid API calls by enforcing a valid `service/resource/verb` combination.
* - **Auto-completion**: Provides intelligent suggestions based on predefined API structure.
* - **Cache management**: Enables precise cache invalidation and data synchronization.
*/

type QueryKeyArrayWithDep = QueryKeyArray & {
addDep: (deps: Record<string, unknown>) => QueryKeyArray;
};
type ExtractParams<T> = T extends (params: infer P) => any ? P : never;

type VerbFunction<T> = {
(params?: ExtractParams<T>): QueryKeyArrayWithDep;
};

type MapVerbToReturnType<T> = T extends (params: any) => any
? VerbFunction<T>
: never;

type UseAPIQueryResult = {
[S in APIQueryKeyMapService]: {
[R in APIQueryKeyMapResource<S>]: {
[V in APIQueryKeyMapVerb<S, R>]: MapVerbToReturnType<(typeof API_QUERY_KEY_MAP)[S][R][V]>;
};
};
};

type APIQueryKeyValue = (params?: Record<string, unknown>) => QueryKeyArray;


export const _useAPIQueryKey = (): ComputedRef<UseAPIQueryResult> => {
const queryKeyAppContext = useQueryKeyAppContext('service');
const globalContext = computed(() => queryKeyAppContext.value);


const apiStructure = Object.entries(API_QUERY_KEY_MAP).reduce<Record<APIQueryKeyMapService, any>>((result, [serviceName, resources]) => {
result[serviceName as APIQueryKeyMapService] = Object.entries(resources).reduce((resourceResult, [resourceName, verbs]) => {
resourceResult[resourceName] = Object.entries(verbs).reduce((verbResult, [verb, queryKeyValue]) => {
const staticKey = createImmutableObject([serviceName, resourceName, verb]);

const verbFunction = <T extends APIQueryKeyValue>(params?: ExtractParams<T>) => {
const baseKey = computed(() => (queryKeyValue as T)(params));
const queryKey = [
...globalContext.value,
...staticKey,
...createImmutableObject(baseKey.value),
] as QueryKeyArray;

(queryKey as QueryKeyArrayWithDep).addDep = (deps: Record<string, unknown>): QueryKeyArray => [
...globalContext.value,
...staticKey,
...createImmutableObject(baseKey.value),
createImmutableObject(deps),
];

return queryKey as QueryKeyArrayWithDep;
};

verbResult[verb] = verbFunction;
return verbResult;
}, {});
return resourceResult;
}, {});
return result;
}, {} as Record<APIQueryKeyMapService, any>);


return computed(() => apiStructure as UseAPIQueryResult);
};




interface GlobalQueryParams {
workspaceId?: string;
Expand All @@ -35,7 +91,7 @@ export const useAPIQueryKey = <S extends ServiceName, R extends ResourceName<S>,
resource: R,
verb: V,
additionalGlobalParams?: Partial<GlobalQueryParams>,
) => {
): ComputedRef<QueryKey> => {
const appContextStore = useAppContextStore();
const userWorkspaceStore = useUserWorkspaceStore();

Expand All @@ -50,5 +106,5 @@ export const useAPIQueryKey = <S extends ServiceName, R extends ResourceName<S>,
...additionalGlobalParams,
});

return computed(() => [service, resource, verb, { ...globalQueryParams }]);
return computed<QueryKey>(() => [service, resource, verb, { ...globalQueryParams }]);
};
22 changes: 9 additions & 13 deletions apps/web/src/api-clients/_common/composables/use-scoped-query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/**
* useScopedQuery - A custom wrapper around `useQuery` to enforce scope-based API fetching.
*
Expand All @@ -10,8 +8,8 @@
*
* ## Functionality
* - Extends `useQuery` with **grant scope validation**.
* - Runs queries only when **the users scope is valid** and the app is **ready**.
* - Uses Vues **reactivity** to dynamically compute the `enabled` state.
* - Runs queries only when **the user's scope is valid** and the app is **ready**.
* - Uses Vue's **reactivity** to dynamically compute the `enabled` state.
* - Supports both **static and reactive `enabled` values**.
*
* ## Parameters:
Expand Down Expand Up @@ -43,7 +41,7 @@

import type { MaybeRef } from '@vueuse/core';
import { toValue } from '@vueuse/core';
import { computed, reactive } from 'vue';
import { computed } from 'vue';

import type {
UseQueryOptions,
Expand All @@ -57,21 +55,19 @@ import { useUserStore } from '@/store/user/user-store';

export const useScopedQuery = <TQueryFnData = unknown, TError = unknown, TData = TQueryFnData>(
options: UseQueryOptions<TQueryFnData, TError, TData>,
requiredScopes: GrantScope[] = [],
requiredScopes: GrantScope[],
) => {
const appContextStore = useAppContextStore();
const userStore = useUserStore();

const _state = reactive({
currentGrantScope: computed<GrantScope>(() => userStore.state.currentGrantInfo?.scope || 'USER'),
isLoading: computed(() => appContextStore.getters.globalGrantLoading),
isValidScope: computed(() => requiredScopes.includes(_state.currentGrantScope)),
});
const currentGrantScope = computed<GrantScope>(() => userStore.state.currentGrantInfo?.scope || 'USER');
const isLoading = computed(() => appContextStore.getters.globalGrantLoading);
const isValidScope = computed(() => requiredScopes.includes(currentGrantScope.value));

const queryEnabled = computed<boolean>(() => {
const _inheritedEnabled = options?.enabled as MaybeRef<boolean> | undefined;
const _inheritedEnabled = ('enabled' in options ? options.enabled : undefined) as MaybeRef<boolean> | undefined;
if (_inheritedEnabled !== undefined && !toValue(_inheritedEnabled)) return false;
return _state.isValidScope && !_state.isLoading;
return isValidScope.value && !isLoading.value;
});

return useQuery<TQueryFnData, TError, TData>({
Expand Down
34 changes: 34 additions & 0 deletions apps/web/src/api-clients/_common/constants/api-query-key-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { privateDashboardKeys } from '@/api-clients/dashboard/private-dashboard/keys';
import { privateDataTableKeys } from '@/api-clients/dashboard/private-data-table/keys';
import { privateFolderKeys } from '@/api-clients/dashboard/private-folder/keys';
import { privateWidgetKeys } from '@/api-clients/dashboard/private-widget/keys';
import { publicDashboardKeys } from '@/api-clients/dashboard/public-dashboard/keys';
import { publicDataTableKeys } from '@/api-clients/dashboard/public-data-table/keys';
import { publicFolderKeys } from '@/api-clients/dashboard/public-folder/keys';
import { publicWidgetKeys } from '@/api-clients/dashboard/public-widget/keys';
import { commentKeys } from '@/api-clients/opsflow/comment/keys';
import { eventKeys } from '@/api-clients/opsflow/event/keys';
import { taskCategoryKeys } from '@/api-clients/opsflow/task-category/keys';
import { taskTypeKeys } from '@/api-clients/opsflow/task-type/keys';
import { taskKeys } from '@/api-clients/opsflow/task/keys';

export const API_QUERY_KEY_MAP = {
dashboard: {
publicFolder: publicFolderKeys,
publicDashboard: publicDashboardKeys,
publicDataTable: publicDataTableKeys,
publicWidget: publicWidgetKeys,
privateFolder: privateFolderKeys,
privateDashboard: privateDashboardKeys,
privateDataTable: privateDataTableKeys,
privateWidget: privateWidgetKeys,
},
opsflow: {
comment: commentKeys,
task: taskKeys,
taskCategory: taskCategoryKeys,
taskType: taskTypeKeys,
event: eventKeys,
},
} as const;

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SERVICE_PREFIX = 'service' as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { queryClient } from '@/query';

export const invalidateServiceQuery = async (mode: 'admin' | 'workspace') => {
await queryClient.invalidateQueries({
queryKey: ['service', mode],
});
};
14 changes: 14 additions & 0 deletions apps/web/src/api-clients/_common/types/query-key-type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { API_DOC } from '@/api-clients/_common/constants/api-doc';
// import type { QueryContext } from '@/query/_types/query-key-type';

import type { API_QUERY_KEY_MAP } from '../constants/api-query-key-map';



/**
Expand All @@ -7,3 +11,13 @@ import type { API_DOC } from '@/api-clients/_common/constants/api-doc';
export type ServiceName = keyof typeof API_DOC;
export type ResourceName<S extends ServiceName> = keyof (typeof API_DOC)[S];
export type Verb<S extends ServiceName, R extends ResourceName<S>> = Extract<(typeof API_DOC)[S][R], string[]>[number] | string;

export type ServiceQueryKey<S extends ServiceName, R extends ResourceName<S>, V extends Verb<S, R>> = [
S,
R,
V,
];

export type APIQueryKeyMapService = keyof typeof API_QUERY_KEY_MAP;
export type APIQueryKeyMapResource<S extends APIQueryKeyMapService> = keyof (typeof API_QUERY_KEY_MAP)[S];
export type APIQueryKeyMapVerb<S extends APIQueryKeyMapService, R extends APIQueryKeyMapResource<S>> = keyof (typeof API_QUERY_KEY_MAP)[S][R];
9 changes: 9 additions & 0 deletions apps/web/src/api-clients/dashboard/private-dashboard/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { PrivateDashboardGetParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/get';
import type { PrivateDashboardListParameters } from '@/api-clients/dashboard/private-dashboard/schema/api-verbs/list';



export const privateDashboardKeys = {
list: (params: PrivateDashboardListParameters) => ['private-dashboard', 'list', params] as const,
get: (idParam: PrivateDashboardGetParameters['dashboard_id']) => ['private-dashboard', 'get', idParam] as const,
};
9 changes: 9 additions & 0 deletions apps/web/src/api-clients/dashboard/private-data-table/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { DataTableGetParameters } from '@/api-clients/dashboard/private-data-table/schema/api-verbs/get';
import type { DataTableListParameters } from '@/api-clients/dashboard/private-data-table/schema/api-verbs/list';
import type { DataTableLoadParameters } from '@/api-clients/dashboard/private-data-table/schema/api-verbs/load';

export const privateDataTableKeys = {
list: (params: DataTableListParameters) => ['private-data-table', 'list', params] as const,
get: (idParam: DataTableGetParameters['data_table_id']) => ['private-data-table', 'get', idParam] as const,
load: (idParam: DataTableLoadParameters['data_table_id']) => ['private-data-table', 'load', idParam] as const,
};
7 changes: 7 additions & 0 deletions apps/web/src/api-clients/dashboard/private-folder/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PrivateFolderGetParameters } from '@/api-clients/dashboard/private-folder/schema/api-verbs/get';
import type { PrivateFolderListParameters } from '@/api-clients/dashboard/private-folder/schema/api-verbs/list';

export const privateFolderKeys = {
list: (params: PrivateFolderListParameters) => ['private-folder', 'list', params] as const,
get: (idParam: PrivateFolderGetParameters['folder_id']) => ['private-folder', 'get', idParam] as const,
};
11 changes: 11 additions & 0 deletions apps/web/src/api-clients/dashboard/private-widget/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { PrivateWidgetGetParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/get';
import type { PrivateWidgetListParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/list';
import type { PrivateWidgetLoadParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/load';
import type { PrivateWidgetLoadSumParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/load-sum';

export const privateWidgetKeys = {
list: (params: PrivateWidgetListParameters) => ['private-widget', 'list', params] as const,
get: (idParam: PrivateWidgetGetParameters['widget_id']) => ['private-widget', 'get', idParam] as const,
load: (idParam: PrivateWidgetLoadParameters['widget_id']) => ['private-widget', 'load', idParam] as const,
loadSum: (idParam: PrivateWidgetLoadSumParameters['widget_id']) => ['private-widget', 'load-sum', idParam] as const,
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { PublicDashboardShareParameters } from '@/api-clients/dashboard/pub
import type { PublicDashboardUnshareParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/unshare';
import type { PublicDashboardUpdateParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/update';
import type { PublicDashboardModel } from '@/api-clients/dashboard/public-dashboard/schema/model';
import { useReferenceQuerySync } from '@/query/reference/_composables/use-reference-query-sync';

interface UsePublicDashboardApiReturn {
publicDashboardGetQueryKey: ComputedRef<QueryKey>;
Expand All @@ -34,13 +35,14 @@ interface UsePublicDashboardApiReturn {
export const usePublicDashboardApi = (): UsePublicDashboardApiReturn => {
const publicDashboardGetQueryKey = useAPIQueryKey('dashboard', 'public-dashboard', 'get');
const publicDashboardListQueryKey = useAPIQueryKey('dashboard', 'public-dashboard', 'list');
const { withReferenceUpdate, withReferenceRefresh } = useReferenceQuerySync<PublicDashboardModel>('publicDashboard');

const actions = {
async create(params: PublicDashboardCreateParameters) {
return SpaceConnector.clientV2.dashboard.publicDashboard.create<PublicDashboardCreateParameters, PublicDashboardModel>(params);
return withReferenceUpdate(() => SpaceConnector.clientV2.dashboard.publicDashboard.create<PublicDashboardCreateParameters, PublicDashboardModel>(params));
},
async update(params: PublicDashboardUpdateParameters) {
return SpaceConnector.clientV2.dashboard.publicDashboard.update<PublicDashboardUpdateParameters, PublicDashboardModel>(params);
return withReferenceUpdate(() => SpaceConnector.clientV2.dashboard.publicDashboard.update<PublicDashboardUpdateParameters, PublicDashboardModel>(params));
},
async changeFolder(params: PublicDashboardChangeFolderParameters) {
return SpaceConnector.clientV2.dashboard.publicDashboard.changeFolder<PublicDashboardChangeFolderParameters, PublicDashboardModel>(params);
Expand All @@ -52,7 +54,7 @@ export const usePublicDashboardApi = (): UsePublicDashboardApiReturn => {
return SpaceConnector.clientV2.dashboard.publicDashboard.unshare<PublicDashboardUnshareParameters, PublicDashboardModel>(params);
},
async delete(params: PublicDashboardDeleteParameters) {
return SpaceConnector.clientV2.dashboard.publicDashboard.delete<PublicDashboardUnshareParameters>(params);
return withReferenceRefresh(() => SpaceConnector.clientV2.dashboard.publicDashboard.delete<PublicDashboardUnshareParameters>(params));
},
async get(params: PublicDashboardGetParameters) {
return SpaceConnector.clientV2.dashboard.publicDashboard.get<PublicDashboardGetParameters, PublicDashboardModel>(params);
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/api-clients/dashboard/public-dashboard/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PublicDashboardGetParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/get';
import type { PublicDashboardListParameters } from '@/api-clients/dashboard/public-dashboard/schema/api-verbs/list';

export const publicDashboardKeys = {
list: (params: PublicDashboardListParameters) => ['public-dashboard', 'list', params] as const,
get: (idParam: PublicDashboardGetParameters['dashboard_id']) => ['public-dashboard', 'get', idParam] as const,
};
9 changes: 9 additions & 0 deletions apps/web/src/api-clients/dashboard/public-data-table/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { DataTableGetParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/get';
import type { DataTableListParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/list';
import type { DataTableLoadParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/load';

export const publicDataTableKeys = {
list: (params: DataTableListParameters) => ['public-data-table', 'list', params] as const,
get: (idParam: DataTableGetParameters['data_table_id']) => ['public-data-table', 'get', idParam] as const,
load: (idParam: DataTableLoadParameters['data_table_id']) => ['public-data-table', 'load', idParam] as const,
};
7 changes: 7 additions & 0 deletions apps/web/src/api-clients/dashboard/public-folder/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PublicFolderGetParameters } from '@/api-clients/dashboard/public-folder/schema/api-verbs/get';
import type { PublicFolderListParameters } from '@/api-clients/dashboard/public-folder/schema/api-verbs/list';

export const publicFolderKeys = {
list: (params: PublicFolderListParameters) => ['public-folder', 'list', params] as const,
get: (idParam: PublicFolderGetParameters['folder_id']) => ['public-folder', 'get', idParam] as const,
};
11 changes: 11 additions & 0 deletions apps/web/src/api-clients/dashboard/public-widget/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { PublicWidgetGetParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/get';
import type { PublicWidgetListParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/list';
import type { PublicWidgetLoadParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/load';
import type { PublicWidgetLoadSumParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/load-sum';

export const publicWidgetKeys = {
list: (params: PublicWidgetListParameters) => ['public-widget', 'list', params] as const,
get: (idParam: PublicWidgetGetParameters['widget_id']) => ['public-widget', 'get', idParam] as const,
load: (idParam: PublicWidgetLoadParameters['widget_id']) => ['public-widget', 'load', idParam] as const,
loadSum: (idParam: PublicWidgetLoadSumParameters['widget_id']) => ['public-widget', 'load-sum', idParam] as const,
};
8 changes: 8 additions & 0 deletions apps/web/src/api-clients/opsflow/comment/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CommentGetParameters } from '@/api-clients/opsflow/comment/schema/api-verbs/get';
import type { CommentListParameters } from '@/api-clients/opsflow/comment/schema/api-verbs/list';

export const commentKeys = {
list: (params: CommentListParameters) => ['comment', 'list', params] as const,
get: (idParam: CommentGetParameters['comment_id']) => ['comment', 'get', idParam] as const,
};

6 changes: 6 additions & 0 deletions apps/web/src/api-clients/opsflow/event/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { EventListParameters } from '@/api-clients/opsflow/event/schema/api-verbs/list';

export const eventKeys = {
list: (params: EventListParameters) => ['event', 'list', params] as const,
};

8 changes: 8 additions & 0 deletions apps/web/src/api-clients/opsflow/task-category/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { TaskCategoryGetParameters } from '@/api-clients/opsflow/task-category/schema/api-verbs/get';
import type { TaskCategoryListParameters } from '@/api-clients/opsflow/task-category/schema/api-verbs/list';

export const taskCategoryKeys = {
list: (params: TaskCategoryListParameters) => ['task-category', 'list', params] as const,
get: (idParam: TaskCategoryGetParameters['category_id']) => ['task-category', 'get', idParam] as const,
};

Loading
Loading