@greatapps/common 1.1.498 → 1.1.500
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/navigation/ProjectSelectorPopover.mjs.map +1 -1
- package/dist/infra/utils/array.mjs +34 -0
- package/dist/infra/utils/array.mjs.map +1 -0
- package/dist/modules/projects/hooks/create-project.hook.mjs +53 -22
- package/dist/modules/projects/hooks/create-project.hook.mjs.map +1 -1
- package/dist/modules/projects/hooks/delete-project.hook.mjs +5 -6
- package/dist/modules/projects/hooks/delete-project.hook.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/navigation/ProjectSelectorPopover.tsx +3 -3
- package/src/infra/utils/array.ts +54 -0
- package/src/modules/projects/hooks/create-project.hook.ts +70 -27
- package/src/modules/projects/hooks/delete-project.hook.ts +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/navigation/ProjectSelectorPopover.tsx"],"sourcesContent":["'use client';\n\nimport { Dispatch, RefObject, SetStateAction, UIEvent, useCallback } from 'react';\nimport {\n Button,\n Command,\n CommandEmpty,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n UserAvatar,\n} from '../../index';\nimport { IconCheck, IconChevronDown, IconFolderOpen } from '@tabler/icons-react';\nimport type { Project } from '../../modules/projects/types';\n\nexport interface ProjectSelectorPopoverProps {\n open: boolean;\n setOpen: Dispatch<SetStateAction<boolean>>;\n titleRef?: RefObject<HTMLSpanElement | null>;\n projects:
|
|
1
|
+
{"version":3,"sources":["../../../src/components/navigation/ProjectSelectorPopover.tsx"],"sourcesContent":["'use client';\n\nimport { Dispatch, RefObject, SetStateAction, UIEvent, useCallback } from 'react';\nimport {\n Button,\n Command,\n CommandEmpty,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n UserAvatar,\n} from '../../index';\nimport { IconCheck, IconChevronDown, IconFolderOpen } from '@tabler/icons-react';\nimport type { Project } from '../../modules/projects/types';\n\nexport interface ProjectSelectorPopoverProps {\n open: boolean;\n setOpen: Dispatch<SetStateAction<boolean>>;\n titleRef?: RefObject<HTMLSpanElement | null>;\n projects: Project[];\n selectedProject: Project | null;\n isLoading?: boolean;\n isFetchingNextPage?: boolean;\n hasNextPage?: boolean;\n onLoadMore?: () => void | Promise<unknown>;\n onSelectProject: (project: Project) => void;\n onManageProjects?: () => void;\n children?: React.ReactNode;\n}\n\nfunction ProjectItemSkeleton() {\n return (\n <div className=\"flex items-center justify-between px-2 py-2 gap-2\">\n <div className=\"flex items-center gap-2 flex-1\">\n <div className=\"skeleton size-9 rounded-lg shrink-0\" />\n <div className=\"skeleton h-4 w-36 rounded\" />\n </div>\n <div className=\"skeleton size-4 rounded-full shrink-0\" />\n </div>\n );\n}\n\nexport function ProjectSelectorPopover({\n open,\n setOpen,\n titleRef,\n projects,\n selectedProject,\n isLoading,\n isFetchingNextPage,\n hasNextPage,\n onLoadMore,\n onSelectProject,\n onManageProjects,\n children,\n}: ProjectSelectorPopoverProps) {\n const handleListScroll = useCallback((event: UIEvent<HTMLDivElement>) => {\n if (!hasNextPage || isFetchingNextPage || !onLoadMore) return;\n\n const target = event.currentTarget;\n const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight;\n\n if (distanceToBottom <= 80) {\n onLoadMore();\n }\n }, [hasNextPage, isFetchingNextPage, onLoadMore]);\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n {children ?? (\n <button className=\"cursor-pointer\">\n <IconChevronDown\n size={20}\n className={`transition-transform duration-200 ${open ? 'rotate-180 text-gray-950' : 'text-gray-400'}`}\n />\n </button>\n )}\n </PopoverTrigger>\n <PopoverContent\n className=\"w-[296px] p-0 bg-white rounded-2xl shadow-md border border-gray-200\"\n align=\"start\"\n sideOffset={8}\n onInteractOutside={(event: Event) => {\n if (titleRef?.current?.contains(event.target as Node)) {\n event.preventDefault();\n }\n }}\n >\n <div className=\"flex flex-col p-3 gap-2\">\n <div className=\"flex items-center justify-between\">\n <span className=\"paragraph-small-semibold\">Projetos</span>\n </div>\n <Command className=\"gap-2\" defaultValue=\" \">\n <CommandInput placeholder=\"Busque aqui\" />\n <CommandList\n className=\"custom-scrollbar paragraph-small-medium text-gray-600 h-[227px]\"\n onScroll={handleListScroll}\n >\n {isLoading ? (\n <>\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n </>\n ) : projects.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center gap-2 h-full text-gray-400 py-4\">\n <IconFolderOpen size={28} className=\"text-gray-300\" />\n <span className=\"paragraph-small-medium text-center text-gray-500\">\n Nenhum projeto encontrado.\n <br />\n Crie um projeto para começar.\n </span>\n </div>\n ) : (\n <>\n <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>\n {projects.map((project) => {\n const isSelected = selectedProject?.id === project.id;\n return (\n <CommandItem\n key={project.id}\n value={`${project.title}-${project.id}`}\n keywords={[project.title]}\n className={isSelected ? 'bg-gray-50 min-h-[39px]' : 'min-h-[39px]'}\n onSelect={() => onSelectProject(project)}\n >\n <div className=\"flex items-center justify-between w-full cursor-pointer\">\n <div className=\"flex items-center gap-2 w-full\">\n <UserAvatar\n photo={project.photo}\n name={project.title}\n size={36}\n />\n {project.title}\n </div>\n {isSelected && (\n <IconCheck className=\"size-4 text-gray-950\" />\n )}\n </div>\n </CommandItem>\n );\n })}\n {isFetchingNextPage && (\n <>\n <ProjectItemSkeleton />\n <ProjectItemSkeleton />\n </>\n )}\n </>\n )}\n </CommandList>\n </Command>\n {onManageProjects && (\n <>\n <div className=\"border-t border-gray-200\" />\n <Button\n variant=\"ghost\"\n className=\"h-10! paragraph-small-medium text-gray-600 hover:text-gray-950 hover:bg-zinc-50 w-full justify-center\"\n onClick={onManageProjects}\n >\n Gerenciar projetos\n </Button>\n </>\n )}\n </div>\n </PopoverContent>\n </Popover>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function insertSorted(data, newItem, compareFn) {
|
|
2
|
+
const index = data.findIndex((item) => compareFn(newItem, item) < 0);
|
|
3
|
+
if (index === -1) return [...data, newItem];
|
|
4
|
+
return [...data.slice(0, index), newItem, ...data.slice(index)];
|
|
5
|
+
}
|
|
6
|
+
function buildCompareFn(sort) {
|
|
7
|
+
const fields = sort.split(",").map((f) => f.trim()).filter(Boolean);
|
|
8
|
+
const parsedFields = fields.map((field) => {
|
|
9
|
+
const desc = field.startsWith("-");
|
|
10
|
+
const key = desc ? field.slice(1) : field;
|
|
11
|
+
return { key, desc };
|
|
12
|
+
});
|
|
13
|
+
return (a, b) => {
|
|
14
|
+
for (const { key, desc } of parsedFields) {
|
|
15
|
+
const aVal = a[key];
|
|
16
|
+
const bVal = b[key];
|
|
17
|
+
let result;
|
|
18
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
19
|
+
result = aVal.localeCompare(bVal);
|
|
20
|
+
} else if (typeof aVal === "number" && typeof bVal === "number") {
|
|
21
|
+
result = aVal - bVal;
|
|
22
|
+
} else {
|
|
23
|
+
result = String(aVal ?? "").localeCompare(String(bVal ?? ""));
|
|
24
|
+
}
|
|
25
|
+
if (result !== 0) return desc ? -result : result;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
buildCompareFn,
|
|
32
|
+
insertSorted
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=array.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/infra/utils/array.ts"],"sourcesContent":["/**\n * Insere um item em um array já ordenado, mantendo a ordenação\n * com base na função de comparação fornecida.\n */\nexport function insertSorted<T>(\n data: T[],\n newItem: T,\n compareFn: (a: T, b: T) => number\n): T[] {\n const index = data.findIndex((item) => compareFn(newItem, item) < 0);\n if (index === -1) return [...data, newItem];\n return [...data.slice(0, index), newItem, ...data.slice(index)];\n}\n\n/**\n * Cria uma função de comparação a partir de uma string de sort da API.\n * Suporta múltiplos campos separados por vírgula e prefixo `-` para desc.\n *\n * Exemplos:\n * - \"title\" → ascendente por title\n * - \"-title\" → descendente por title\n * - \"title,-datetime_add\" → ascendente por title, depois descendente por datetime_add\n */\nexport function buildCompareFn<T extends Record<string, unknown>>(\n sort: string\n): (a: T, b: T) => number {\n const fields = sort.split(',').map((f) => f.trim()).filter(Boolean);\n\n const parsedFields = fields.map((field) => {\n const desc = field.startsWith('-');\n const key = desc ? field.slice(1) : field;\n return { key, desc };\n });\n\n return (a: T, b: T) => {\n for (const { key, desc } of parsedFields) {\n const aVal = a[key];\n const bVal = b[key];\n\n let result: number;\n\n if (typeof aVal === 'string' && typeof bVal === 'string') {\n result = aVal.localeCompare(bVal);\n } else if (typeof aVal === 'number' && typeof bVal === 'number') {\n result = aVal - bVal;\n } else {\n result = String(aVal ?? '').localeCompare(String(bVal ?? ''));\n }\n\n if (result !== 0) return desc ? -result : result;\n }\n return 0;\n };\n}\n"],"mappings":"AAIO,SAAS,aACd,MACA,SACA,WACK;AACL,QAAM,QAAQ,KAAK,UAAU,CAAC,SAAS,UAAU,SAAS,IAAI,IAAI,CAAC;AACnE,MAAI,UAAU,GAAI,QAAO,CAAC,GAAG,MAAM,OAAO;AAC1C,SAAO,CAAC,GAAG,KAAK,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,MAAM,KAAK,CAAC;AAChE;AAWO,SAAS,eACd,MACwB;AACxB,QAAM,SAAS,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAElE,QAAM,eAAe,OAAO,IAAI,CAAC,UAAU;AACzC,UAAM,OAAO,MAAM,WAAW,GAAG;AACjC,UAAM,MAAM,OAAO,MAAM,MAAM,CAAC,IAAI;AACpC,WAAO,EAAE,KAAK,KAAK;AAAA,EACrB,CAAC;AAED,SAAO,CAAC,GAAM,MAAS;AACrB,eAAW,EAAE,KAAK,KAAK,KAAK,cAAc;AACxC,YAAM,OAAO,EAAE,GAAG;AAClB,YAAM,OAAO,EAAE,GAAG;AAElB,UAAI;AAEJ,UAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU;AACxD,iBAAS,KAAK,cAAc,IAAI;AAAA,MAClC,WAAW,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU;AAC/D,iBAAS,OAAO;AAAA,MAClB,OAAO;AACL,iBAAS,OAAO,QAAQ,EAAE,EAAE,cAAc,OAAO,QAAQ,EAAE,CAAC;AAAA,MAC9D;AAEA,UAAI,WAAW,EAAG,QAAO,OAAO,CAAC,SAAS;AAAA,IAC5C;AACA,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -3,34 +3,65 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
3
3
|
import { createProjectAction } from "../actions/create-project.action";
|
|
4
4
|
import { withAction } from "../../../utils/withAction";
|
|
5
5
|
import { useAuthQueryKey } from "../../../hooks/useAuthQueryKey";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { PROJECTS_BASE_KEY } from "./list-projects.hook";
|
|
7
|
+
import { insertSorted, buildCompareFn } from "../../../infra/utils/array";
|
|
8
|
+
function extractSort(query) {
|
|
9
|
+
const key = query.queryKey;
|
|
10
|
+
for (let i = key.length - 1; i >= 0; i--) {
|
|
11
|
+
const segment = key[i];
|
|
12
|
+
if (segment && typeof segment === "object" && "sort" in segment) {
|
|
13
|
+
return segment.sort;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
8
18
|
function useCreateProject() {
|
|
9
19
|
const queryClient = useQueryClient();
|
|
10
|
-
const
|
|
11
|
-
const listKey = useAuthQueryKey([...PROJECTS_QUERY_KEY()]);
|
|
20
|
+
const projectsKey = useAuthQueryKey([...PROJECTS_BASE_KEY]);
|
|
12
21
|
return useMutation({
|
|
13
22
|
mutationFn: (data) => withAction(createProjectAction)(data),
|
|
14
23
|
onSuccess: (newProject) => {
|
|
15
|
-
queryClient.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
const cache = queryClient.getQueryCache();
|
|
25
|
+
const queries = cache.findAll({ queryKey: projectsKey });
|
|
26
|
+
for (const query of queries) {
|
|
27
|
+
const sort = extractSort(query);
|
|
28
|
+
const isInfinite = query.queryKey.includes("infinite");
|
|
29
|
+
if (isInfinite) {
|
|
30
|
+
queryClient.setQueryData(
|
|
31
|
+
query.queryKey,
|
|
32
|
+
(old) => {
|
|
33
|
+
if (!old?.pages.length) return old;
|
|
34
|
+
const pageSize = old.pages[0].data.length || 20;
|
|
35
|
+
const allItems = old.pages.flatMap((page) => page.data);
|
|
36
|
+
const withNew = sort ? insertSorted(allItems, newProject, buildCompareFn(sort)) : [newProject, ...allItems];
|
|
37
|
+
const newTotal = (old.pages[0].total ?? 0) + 1;
|
|
38
|
+
return {
|
|
39
|
+
...old,
|
|
40
|
+
pages: old.pages.map((page, i) => ({
|
|
41
|
+
...page,
|
|
42
|
+
data: withNew.slice(i * pageSize, (i + 1) * pageSize),
|
|
43
|
+
total: newTotal
|
|
44
|
+
}))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
queryClient.setQueryData(
|
|
50
|
+
query.queryKey,
|
|
51
|
+
(old) => {
|
|
52
|
+
if (!old) return old;
|
|
53
|
+
const pageSize = old.data.length || 20;
|
|
54
|
+
const withNew = sort ? insertSorted(old.data, newProject, buildCompareFn(sort)) : [newProject, ...old.data];
|
|
55
|
+
return {
|
|
56
|
+
...old,
|
|
57
|
+
data: withNew.slice(0, pageSize),
|
|
58
|
+
total: old.total + 1
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
);
|
|
32
62
|
}
|
|
33
|
-
|
|
63
|
+
}
|
|
64
|
+
queryClient.invalidateQueries({ queryKey: projectsKey });
|
|
34
65
|
}
|
|
35
66
|
});
|
|
36
67
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/projects/hooks/create-project.hook.ts"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient, InfiniteData } from '@tanstack/react-query';\nimport { createProjectAction } from '../actions/create-project.action';\nimport { withAction } from '../../../utils/withAction';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/projects/hooks/create-project.hook.ts"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient, InfiniteData, Query } from '@tanstack/react-query';\nimport { createProjectAction } from '../actions/create-project.action';\nimport { withAction } from '../../../utils/withAction';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\nimport { PROJECTS_BASE_KEY } from './list-projects.hook';\nimport { insertSorted, buildCompareFn } from '../../../infra/utils/array';\nimport type { CreateProjectRequest, Project, ProjectsPage } from '../types';\n\nfunction extractSort(query: Query): string | undefined {\n const key = query.queryKey;\n for (let i = key.length - 1; i >= 0; i--) {\n const segment = key[i];\n if (segment && typeof segment === 'object' && 'sort' in segment) {\n return (segment as Record<string, unknown>).sort as string | undefined;\n }\n }\n return undefined;\n}\n\nexport function useCreateProject() {\n const queryClient = useQueryClient();\n const projectsKey = useAuthQueryKey([...PROJECTS_BASE_KEY]);\n\n return useMutation({\n mutationFn: (data: CreateProjectRequest) =>\n withAction(createProjectAction)(data),\n\n onSuccess: (newProject: Project) => {\n const cache = queryClient.getQueryCache();\n const queries = cache.findAll({ queryKey: projectsKey });\n\n for (const query of queries) {\n const sort = extractSort(query);\n const isInfinite = query.queryKey.includes('infinite');\n\n if (isInfinite) {\n queryClient.setQueryData<InfiniteData<ProjectsPage>>(\n query.queryKey,\n (old) => {\n if (!old?.pages.length) return old;\n\n const pageSize = old.pages[0].data.length || 20;\n const allItems = old.pages.flatMap((page) => page.data);\n\n const withNew = sort\n ? insertSorted(allItems, newProject, buildCompareFn<Project>(sort))\n : [newProject, ...allItems];\n\n const newTotal = (old.pages[0].total ?? 0) + 1;\n\n return {\n ...old,\n pages: old.pages.map((page, i) => ({\n ...page,\n data: withNew.slice(i * pageSize, (i + 1) * pageSize),\n total: newTotal,\n })),\n };\n },\n );\n } else {\n queryClient.setQueryData<ProjectsPage>(\n query.queryKey,\n (old) => {\n if (!old) return old;\n\n const pageSize = old.data.length || 20;\n\n const withNew = sort\n ? insertSorted(old.data, newProject, buildCompareFn<Project>(sort))\n : [newProject, ...old.data];\n\n return {\n ...old,\n data: withNew.slice(0, pageSize),\n total: old.total + 1,\n };\n },\n );\n }\n }\n\n queryClient.invalidateQueries({ queryKey: projectsKey });\n },\n });\n}\n"],"mappings":";AAEA,SAAS,aAAa,sBAA2C;AACjE,SAAS,2BAA2B;AACpC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,cAAc,sBAAsB;AAG7C,SAAS,YAAY,OAAkC;AACrD,QAAM,MAAM,MAAM;AAClB,WAAS,IAAI,IAAI,SAAS,GAAG,KAAK,GAAG,KAAK;AACxC,UAAM,UAAU,IAAI,CAAC;AACrB,QAAI,WAAW,OAAO,YAAY,YAAY,UAAU,SAAS;AAC/D,aAAQ,QAAoC;AAAA,IAC9C;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,mBAAmB;AACjC,QAAM,cAAc,eAAe;AACnC,QAAM,cAAc,gBAAgB,CAAC,GAAG,iBAAiB,CAAC;AAE1D,SAAO,YAAY;AAAA,IACjB,YAAY,CAAC,SACX,WAAW,mBAAmB,EAAE,IAAI;AAAA,IAEtC,WAAW,CAAC,eAAwB;AAClC,YAAM,QAAQ,YAAY,cAAc;AACxC,YAAM,UAAU,MAAM,QAAQ,EAAE,UAAU,YAAY,CAAC;AAEvD,iBAAW,SAAS,SAAS;AAC3B,cAAM,OAAO,YAAY,KAAK;AAC9B,cAAM,aAAa,MAAM,SAAS,SAAS,UAAU;AAErD,YAAI,YAAY;AACd,sBAAY;AAAA,YACV,MAAM;AAAA,YACN,CAAC,QAAQ;AACP,kBAAI,CAAC,KAAK,MAAM,OAAQ,QAAO;AAE/B,oBAAM,WAAW,IAAI,MAAM,CAAC,EAAE,KAAK,UAAU;AAC7C,oBAAM,WAAW,IAAI,MAAM,QAAQ,CAAC,SAAS,KAAK,IAAI;AAEtD,oBAAM,UAAU,OACZ,aAAa,UAAU,YAAY,eAAwB,IAAI,CAAC,IAChE,CAAC,YAAY,GAAG,QAAQ;AAE5B,oBAAM,YAAY,IAAI,MAAM,CAAC,EAAE,SAAS,KAAK;AAE7C,qBAAO;AAAA,gBACL,GAAG;AAAA,gBACH,OAAO,IAAI,MAAM,IAAI,CAAC,MAAM,OAAO;AAAA,kBACjC,GAAG;AAAA,kBACH,MAAM,QAAQ,MAAM,IAAI,WAAW,IAAI,KAAK,QAAQ;AAAA,kBACpD,OAAO;AAAA,gBACT,EAAE;AAAA,cACJ;AAAA,YACF;AAAA,UACF;AAAA,QACF,OAAO;AACL,sBAAY;AAAA,YACV,MAAM;AAAA,YACN,CAAC,QAAQ;AACP,kBAAI,CAAC,IAAK,QAAO;AAEjB,oBAAM,WAAW,IAAI,KAAK,UAAU;AAEpC,oBAAM,UAAU,OACZ,aAAa,IAAI,MAAM,YAAY,eAAwB,IAAI,CAAC,IAChE,CAAC,YAAY,GAAG,IAAI,IAAI;AAE5B,qBAAO;AAAA,gBACL,GAAG;AAAA,gBACH,MAAM,QAAQ,MAAM,GAAG,QAAQ;AAAA,gBAC/B,OAAO,IAAI,QAAQ;AAAA,cACrB;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,kBAAY,kBAAkB,EAAE,UAAU,YAAY,CAAC;AAAA,IACzD;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -3,17 +3,15 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
3
3
|
import { deleteProjectAction } from "../actions/delete-project.action";
|
|
4
4
|
import { withAction } from "../../../utils/withAction";
|
|
5
5
|
import { useAuthQueryKey } from "../../../hooks/useAuthQueryKey";
|
|
6
|
-
import {
|
|
7
|
-
import { INFINITE_PROJECTS_QUERY_KEY } from "./list-infinite-projects.hook";
|
|
6
|
+
import { PROJECTS_BASE_KEY } from "./list-projects.hook";
|
|
8
7
|
function useDeleteProject() {
|
|
9
8
|
const queryClient = useQueryClient();
|
|
10
|
-
const
|
|
11
|
-
const listKey = useAuthQueryKey([...PROJECTS_QUERY_KEY()]);
|
|
9
|
+
const projectsKey = useAuthQueryKey([...PROJECTS_BASE_KEY]);
|
|
12
10
|
return useMutation({
|
|
13
11
|
mutationFn: ({ projectId, moveToProjectId }) => withAction(deleteProjectAction)(projectId, moveToProjectId),
|
|
14
12
|
onSuccess: (_, { projectId }) => {
|
|
15
13
|
queryClient.setQueriesData(
|
|
16
|
-
{ queryKey:
|
|
14
|
+
{ queryKey: projectsKey, exact: false },
|
|
17
15
|
(old) => {
|
|
18
16
|
if (!old?.pages.length) return old;
|
|
19
17
|
return {
|
|
@@ -27,7 +25,7 @@ function useDeleteProject() {
|
|
|
27
25
|
}
|
|
28
26
|
);
|
|
29
27
|
queryClient.setQueriesData(
|
|
30
|
-
{ queryKey:
|
|
28
|
+
{ queryKey: projectsKey, exact: false },
|
|
31
29
|
(old) => {
|
|
32
30
|
if (!old) return old;
|
|
33
31
|
return {
|
|
@@ -37,6 +35,7 @@ function useDeleteProject() {
|
|
|
37
35
|
};
|
|
38
36
|
}
|
|
39
37
|
);
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: projectsKey });
|
|
40
39
|
}
|
|
41
40
|
});
|
|
42
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/projects/hooks/delete-project.hook.ts"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient, InfiniteData } from '@tanstack/react-query';\nimport { deleteProjectAction } from '../actions/delete-project.action';\nimport { withAction } from '../../../utils/withAction';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/projects/hooks/delete-project.hook.ts"],"sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient, InfiniteData } from '@tanstack/react-query';\nimport { deleteProjectAction } from '../actions/delete-project.action';\nimport { withAction } from '../../../utils/withAction';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\nimport { PROJECTS_BASE_KEY } from './list-projects.hook';\nimport type { ProjectsPage } from '../types';\n\nexport function useDeleteProject() {\n const queryClient = useQueryClient();\n const projectsKey = useAuthQueryKey([...PROJECTS_BASE_KEY]);\n\n return useMutation({\n mutationFn: ({ projectId, moveToProjectId }: { projectId: number; moveToProjectId: number }) =>\n withAction(deleteProjectAction)(projectId, moveToProjectId),\n\n onSuccess: (_, { projectId }) => {\n queryClient.setQueriesData<InfiniteData<ProjectsPage>>(\n { queryKey: projectsKey, exact: false },\n (old) => {\n if (!old?.pages.length) return old;\n return {\n ...old,\n pages: old.pages.map((page) => ({\n ...page,\n data: page.data.filter((p) => p.id !== projectId),\n total: page.total - 1,\n })),\n };\n },\n );\n\n queryClient.setQueriesData<ProjectsPage>(\n { queryKey: projectsKey, exact: false },\n (old) => {\n if (!old) return old;\n return {\n ...old,\n data: old.data.filter((p) => p.id !== projectId),\n total: old.total - 1,\n };\n },\n );\n\n queryClient.invalidateQueries({ queryKey: projectsKey });\n },\n });\n}\n"],"mappings":";AAEA,SAAS,aAAa,sBAAoC;AAC1D,SAAS,2BAA2B;AACpC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAG3B,SAAS,mBAAmB;AACjC,QAAM,cAAc,eAAe;AACnC,QAAM,cAAc,gBAAgB,CAAC,GAAG,iBAAiB,CAAC;AAE1D,SAAO,YAAY;AAAA,IACjB,YAAY,CAAC,EAAE,WAAW,gBAAgB,MACxC,WAAW,mBAAmB,EAAE,WAAW,eAAe;AAAA,IAE5D,WAAW,CAAC,GAAG,EAAE,UAAU,MAAM;AAC/B,kBAAY;AAAA,QACV,EAAE,UAAU,aAAa,OAAO,MAAM;AAAA,QACtC,CAAC,QAAQ;AACP,cAAI,CAAC,KAAK,MAAM,OAAQ,QAAO;AAC/B,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,OAAO,IAAI,MAAM,IAAI,CAAC,UAAU;AAAA,cAC9B,GAAG;AAAA,cACH,MAAM,KAAK,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,SAAS;AAAA,cAChD,OAAO,KAAK,QAAQ;AAAA,YACtB,EAAE;AAAA,UACJ;AAAA,QACF;AAAA,MACF;AAEA,kBAAY;AAAA,QACV,EAAE,UAAU,aAAa,OAAO,MAAM;AAAA,QACtC,CAAC,QAAQ;AACP,cAAI,CAAC,IAAK,QAAO;AACjB,iBAAO;AAAA,YACL,GAAG;AAAA,YACH,MAAM,IAAI,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,SAAS;AAAA,YAC/C,OAAO,IAAI,QAAQ;AAAA,UACrB;AAAA,QACF;AAAA,MACF;AAEA,kBAAY,kBAAkB,EAAE,UAAU,YAAY,CAAC;AAAA,IACzD;AAAA,EACF,CAAC;AACH;","names":[]}
|
package/package.json
CHANGED
|
@@ -20,13 +20,13 @@ export interface ProjectSelectorPopoverProps {
|
|
|
20
20
|
open: boolean;
|
|
21
21
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
|
22
22
|
titleRef?: RefObject<HTMLSpanElement | null>;
|
|
23
|
-
projects:
|
|
24
|
-
selectedProject:
|
|
23
|
+
projects: Project[];
|
|
24
|
+
selectedProject: Project | null;
|
|
25
25
|
isLoading?: boolean;
|
|
26
26
|
isFetchingNextPage?: boolean;
|
|
27
27
|
hasNextPage?: boolean;
|
|
28
28
|
onLoadMore?: () => void | Promise<unknown>;
|
|
29
|
-
onSelectProject: (project:
|
|
29
|
+
onSelectProject: (project: Project) => void;
|
|
30
30
|
onManageProjects?: () => void;
|
|
31
31
|
children?: React.ReactNode;
|
|
32
32
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insere um item em um array já ordenado, mantendo a ordenação
|
|
3
|
+
* com base na função de comparação fornecida.
|
|
4
|
+
*/
|
|
5
|
+
export function insertSorted<T>(
|
|
6
|
+
data: T[],
|
|
7
|
+
newItem: T,
|
|
8
|
+
compareFn: (a: T, b: T) => number
|
|
9
|
+
): T[] {
|
|
10
|
+
const index = data.findIndex((item) => compareFn(newItem, item) < 0);
|
|
11
|
+
if (index === -1) return [...data, newItem];
|
|
12
|
+
return [...data.slice(0, index), newItem, ...data.slice(index)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cria uma função de comparação a partir de uma string de sort da API.
|
|
17
|
+
* Suporta múltiplos campos separados por vírgula e prefixo `-` para desc.
|
|
18
|
+
*
|
|
19
|
+
* Exemplos:
|
|
20
|
+
* - "title" → ascendente por title
|
|
21
|
+
* - "-title" → descendente por title
|
|
22
|
+
* - "title,-datetime_add" → ascendente por title, depois descendente por datetime_add
|
|
23
|
+
*/
|
|
24
|
+
export function buildCompareFn<T extends Record<string, unknown>>(
|
|
25
|
+
sort: string
|
|
26
|
+
): (a: T, b: T) => number {
|
|
27
|
+
const fields = sort.split(',').map((f) => f.trim()).filter(Boolean);
|
|
28
|
+
|
|
29
|
+
const parsedFields = fields.map((field) => {
|
|
30
|
+
const desc = field.startsWith('-');
|
|
31
|
+
const key = desc ? field.slice(1) : field;
|
|
32
|
+
return { key, desc };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return (a: T, b: T) => {
|
|
36
|
+
for (const { key, desc } of parsedFields) {
|
|
37
|
+
const aVal = a[key];
|
|
38
|
+
const bVal = b[key];
|
|
39
|
+
|
|
40
|
+
let result: number;
|
|
41
|
+
|
|
42
|
+
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
43
|
+
result = aVal.localeCompare(bVal);
|
|
44
|
+
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
45
|
+
result = aVal - bVal;
|
|
46
|
+
} else {
|
|
47
|
+
result = String(aVal ?? '').localeCompare(String(bVal ?? ''));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (result !== 0) return desc ? -result : result;
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -1,45 +1,88 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useMutation, useQueryClient, InfiniteData } from '@tanstack/react-query';
|
|
3
|
+
import { useMutation, useQueryClient, InfiniteData, Query } from '@tanstack/react-query';
|
|
4
4
|
import { createProjectAction } from '../actions/create-project.action';
|
|
5
5
|
import { withAction } from '../../../utils/withAction';
|
|
6
6
|
import { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { PROJECTS_BASE_KEY } from './list-projects.hook';
|
|
8
|
+
import { insertSorted, buildCompareFn } from '../../../infra/utils/array';
|
|
9
9
|
import type { CreateProjectRequest, Project, ProjectsPage } from '../types';
|
|
10
10
|
|
|
11
|
+
function extractSort(query: Query): string | undefined {
|
|
12
|
+
const key = query.queryKey;
|
|
13
|
+
for (let i = key.length - 1; i >= 0; i--) {
|
|
14
|
+
const segment = key[i];
|
|
15
|
+
if (segment && typeof segment === 'object' && 'sort' in segment) {
|
|
16
|
+
return (segment as Record<string, unknown>).sort as string | undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export function useCreateProject() {
|
|
12
23
|
const queryClient = useQueryClient();
|
|
13
|
-
const
|
|
14
|
-
const listKey = useAuthQueryKey([...PROJECTS_QUERY_KEY()]);
|
|
24
|
+
const projectsKey = useAuthQueryKey([...PROJECTS_BASE_KEY]);
|
|
15
25
|
|
|
16
26
|
return useMutation({
|
|
17
27
|
mutationFn: (data: CreateProjectRequest) =>
|
|
18
28
|
withAction(createProjectAction)(data),
|
|
19
29
|
|
|
20
30
|
onSuccess: (newProject: Project) => {
|
|
21
|
-
queryClient.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
const cache = queryClient.getQueryCache();
|
|
32
|
+
const queries = cache.findAll({ queryKey: projectsKey });
|
|
33
|
+
|
|
34
|
+
for (const query of queries) {
|
|
35
|
+
const sort = extractSort(query);
|
|
36
|
+
const isInfinite = query.queryKey.includes('infinite');
|
|
37
|
+
|
|
38
|
+
if (isInfinite) {
|
|
39
|
+
queryClient.setQueryData<InfiniteData<ProjectsPage>>(
|
|
40
|
+
query.queryKey,
|
|
41
|
+
(old) => {
|
|
42
|
+
if (!old?.pages.length) return old;
|
|
43
|
+
|
|
44
|
+
const pageSize = old.pages[0].data.length || 20;
|
|
45
|
+
const allItems = old.pages.flatMap((page) => page.data);
|
|
46
|
+
|
|
47
|
+
const withNew = sort
|
|
48
|
+
? insertSorted(allItems, newProject, buildCompareFn<Project>(sort))
|
|
49
|
+
: [newProject, ...allItems];
|
|
50
|
+
|
|
51
|
+
const newTotal = (old.pages[0].total ?? 0) + 1;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...old,
|
|
55
|
+
pages: old.pages.map((page, i) => ({
|
|
56
|
+
...page,
|
|
57
|
+
data: withNew.slice(i * pageSize, (i + 1) * pageSize),
|
|
58
|
+
total: newTotal,
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
queryClient.setQueryData<ProjectsPage>(
|
|
65
|
+
query.queryKey,
|
|
66
|
+
(old) => {
|
|
67
|
+
if (!old) return old;
|
|
68
|
+
|
|
69
|
+
const pageSize = old.data.length || 20;
|
|
70
|
+
|
|
71
|
+
const withNew = sort
|
|
72
|
+
? insertSorted(old.data, newProject, buildCompareFn<Project>(sort))
|
|
73
|
+
: [newProject, ...old.data];
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
...old,
|
|
77
|
+
data: withNew.slice(0, pageSize),
|
|
78
|
+
total: old.total + 1,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
queryClient.invalidateQueries({ queryKey: projectsKey });
|
|
43
86
|
},
|
|
44
87
|
});
|
|
45
88
|
}
|
|
@@ -4,14 +4,12 @@ import { useMutation, useQueryClient, InfiniteData } from '@tanstack/react-query
|
|
|
4
4
|
import { deleteProjectAction } from '../actions/delete-project.action';
|
|
5
5
|
import { withAction } from '../../../utils/withAction';
|
|
6
6
|
import { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';
|
|
7
|
-
import {
|
|
8
|
-
import { INFINITE_PROJECTS_QUERY_KEY } from './list-infinite-projects.hook';
|
|
7
|
+
import { PROJECTS_BASE_KEY } from './list-projects.hook';
|
|
9
8
|
import type { ProjectsPage } from '../types';
|
|
10
9
|
|
|
11
10
|
export function useDeleteProject() {
|
|
12
11
|
const queryClient = useQueryClient();
|
|
13
|
-
const
|
|
14
|
-
const listKey = useAuthQueryKey([...PROJECTS_QUERY_KEY()]);
|
|
12
|
+
const projectsKey = useAuthQueryKey([...PROJECTS_BASE_KEY]);
|
|
15
13
|
|
|
16
14
|
return useMutation({
|
|
17
15
|
mutationFn: ({ projectId, moveToProjectId }: { projectId: number; moveToProjectId: number }) =>
|
|
@@ -19,7 +17,7 @@ export function useDeleteProject() {
|
|
|
19
17
|
|
|
20
18
|
onSuccess: (_, { projectId }) => {
|
|
21
19
|
queryClient.setQueriesData<InfiniteData<ProjectsPage>>(
|
|
22
|
-
{ queryKey:
|
|
20
|
+
{ queryKey: projectsKey, exact: false },
|
|
23
21
|
(old) => {
|
|
24
22
|
if (!old?.pages.length) return old;
|
|
25
23
|
return {
|
|
@@ -34,7 +32,7 @@ export function useDeleteProject() {
|
|
|
34
32
|
);
|
|
35
33
|
|
|
36
34
|
queryClient.setQueriesData<ProjectsPage>(
|
|
37
|
-
{ queryKey:
|
|
35
|
+
{ queryKey: projectsKey, exact: false },
|
|
38
36
|
(old) => {
|
|
39
37
|
if (!old) return old;
|
|
40
38
|
return {
|
|
@@ -44,6 +42,8 @@ export function useDeleteProject() {
|
|
|
44
42
|
};
|
|
45
43
|
},
|
|
46
44
|
);
|
|
45
|
+
|
|
46
|
+
queryClient.invalidateQueries({ queryKey: projectsKey });
|
|
47
47
|
},
|
|
48
48
|
});
|
|
49
49
|
}
|