@greatapps/common 1.1.187 → 1.1.190

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.
@@ -1,294 +1,261 @@
1
- "use client";
2
-
3
- "use client";
4
-
5
- import { useEffect, useState } from "react";
6
- import { Plus, Settings, User, Users } from "lucide-react";
7
- import { UserAvatar } from "../ui/data-display/UserAvatar";
8
- import { useQueryClient, useMutation } from "@tanstack/react-query";
9
- import { Popover, PopoverContent, PopoverTrigger } from "../ui/overlay/Popover";
10
- import { Button } from "../ui/buttons/Button";
11
- import {
12
- Command,
13
- CommandEmpty,
14
- CommandInput,
15
- CommandItem,
16
- CommandList,
17
- } from "../ui/overlay/Command";
18
- import { Checkbox } from "../ui/form/Checkbox";
19
- import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip";
20
- import { cn } from "../../infra/utils/clsx";
21
- import {
22
- useListProjectUsers,
23
- LIST_PROJECT_USERS_BASE_KEY,
24
- } from "../../modules/projects/hooks/list-project-users.hook";
25
- import {
26
- useListAvailableUsers,
27
- LIST_AVAILABLE_USERS_BASE_KEY,
28
- } from "../../modules/projects/hooks/list-available-users.hook";
29
- import { addProjectUserAction } from "../../modules/projects/actions/add-project-user.action";
30
- import { removeProjectUserAction } from "../../modules/projects/actions/remove-project-user.action";
31
-
32
- export interface UsersSelectorPopoverProps {
33
- projectId?: number;
34
- side?: "top" | "bottom" | "left" | "right";
35
- align?: "start" | "center" | "end";
36
- contentClassName?: string;
37
- }
38
-
39
- function UserItemSkeleton() {
40
- return (
41
- <div className="flex items-center justify-between px-2 py-1.5 gap-2">
42
- <div className="flex items-center gap-2 flex-1">
43
- <div className="skeleton size-8 rounded-full shrink-0" />
44
- <div className="skeleton h-3.5 w-28 rounded" />
45
- </div>
46
- <div className="skeleton size-4 rounded shrink-0" />
47
- </div>
48
- );
49
- }
50
-
51
- const GAPPS_URL = process.env.NEXT_PUBLIC_GREAT_APPS ?? "";
52
-
53
- export function UsersSelectorPopover({
54
- projectId,
55
- side,
56
- align,
57
- contentClassName,
58
- }: UsersSelectorPopoverProps) {
59
- const queryClient = useQueryClient();
60
- const [open, setOpen] = useState(false);
61
- const [hoveredId, setHoveredId] = useState<number | null>(null);
62
- const [searchInput, setSearchInput] = useState("");
63
- const [search, setSearch] = useState("");
64
-
65
- const id = projectId ?? 0;
66
-
67
- useEffect(() => {
68
- const timer = setTimeout(() => setSearch(searchInput), 300);
69
- return () => clearTimeout(timer);
70
- }, [searchInput]);
71
-
72
- const projectUsers = useListProjectUsers(id, search);
73
- const availableUsers = useListAvailableUsers(id, search);
74
-
75
- const inProject = projectUsers.data?.pages.flatMap((p) => p.data) ?? [];
76
- const notInProject = availableUsers.data?.pages.flatMap((p) => p.data) ?? [];
77
- const isLoading = projectUsers.isLoading || availableUsers.isLoading;
78
- const isFetchingMore =
79
- projectUsers.isFetchingNextPage || availableUsers.isFetchingNextPage;
80
- const totalInProject = projectUsers.data?.pages[0]?.total ?? inProject.length;
81
-
82
- const invalidate = () => {
83
- queryClient.invalidateQueries({
84
- queryKey: LIST_PROJECT_USERS_BASE_KEY(id),
85
- });
86
- queryClient.invalidateQueries({
87
- queryKey: LIST_AVAILABLE_USERS_BASE_KEY(id),
88
- });
89
- };
90
-
91
- const addMutation = useMutation({
92
- mutationFn: (userId: number) =>
93
- addProjectUserAction({ projectId: id, users: [userId] }),
94
- onSuccess: invalidate,
95
- });
96
-
97
- const removeMutation = useMutation({
98
- mutationFn: (userId: number) =>
99
- removeProjectUserAction({ projectId: id, users: [userId] }),
100
- onSuccess: invalidate,
101
- });
102
-
103
- const allUsers = [
104
- ...inProject.map((u) => ({
105
- id: u.id_user,
106
- name: u.name,
107
- photo: u.photo ?? null,
108
- checked: true as const,
109
- removable: u.profile !== "owner" && u.profile !== "admin",
110
- })),
111
- ...notInProject.map((u) => ({
112
- id: u.id,
113
- name: u.name,
114
- photo: u.photo ?? null,
115
- checked: false as const,
116
- removable: true,
117
- })),
118
- ];
119
-
120
- const toggleUser = (id: number, checked: boolean, removable: boolean) => {
121
- if (checked) {
122
- if (!removable) return;
123
- removeMutation.mutate(id);
124
- } else {
125
- addMutation.mutate(id);
126
- }
127
- };
128
-
129
- const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
130
- const target = e.currentTarget;
131
- const nearBottom =
132
- target.scrollTop + target.clientHeight >= target.scrollHeight - 50;
133
- if (!nearBottom) return;
134
- if (projectUsers.hasNextPage && !projectUsers.isFetchingNextPage) {
135
- projectUsers.fetchNextPage();
136
- } else if (
137
- availableUsers.hasNextPage &&
138
- !availableUsers.isFetchingNextPage
139
- ) {
140
- availableUsers.fetchNextPage();
141
- }
142
- };
143
-
144
- return (
145
- <Popover open={open} onOpenChange={setOpen}>
146
- <PopoverTrigger className="cursor-pointer">
147
- <div className="flex items-center gap-2 text-gray-950 px-3 py-2 border border-gray-200 rounded-full hover:border-gray-950">
148
- <span className="paragraph-small-semibold">{totalInProject}</span>
149
- <User size={16} />
150
- </div>
151
- </PopoverTrigger>
152
-
153
- <PopoverContent
154
- className={cn(
155
- "absolute right-[-88px] w-[273px] p-0 bg-white rounded-2xl shadow-md border border-gray-200",
156
- contentClassName,
157
- )}
158
- side={side}
159
- align={align}
160
- sideOffset={8}
161
- >
162
- <div className="flex flex-col p-3 gap-2">
163
- <div className="flex items-center justify-between">
164
- <span className="paragraph-small-semibold">Usuários</span>
165
- <div className="flex items-center gap-1.5">
166
- <Button
167
- variant="secondary"
168
- className="h-8 paragraph-small-semibold text-gray-950"
169
- onClick={() => {
170
- window.location.href = `${GAPPS_URL}/my-teams?createNew=true`;
171
- }}
172
- >
173
- <Plus size={16} />
174
- Adicionar
175
- </Button>
176
- <Tooltip>
177
- <TooltipTrigger asChild>
178
- <Button
179
- variant="secondary"
180
- className="p-0 size-8"
181
- onClick={() => {
182
- window.location.href = `${GAPPS_URL}/my-teams`;
183
- }}
184
- >
185
- <Settings size={18} />
186
- </Button>
187
- </TooltipTrigger>
188
- <TooltipContent className="z-[1002]">
189
- Visualizar todos usuários
190
- </TooltipContent>
191
- </Tooltip>
192
- </div>
193
- </div>
194
-
195
- <Command shouldFilter={false} className="gap-2" value="">
196
- <CommandInput
197
- placeholder="Busque aqui"
198
- value={searchInput}
199
- onValueChange={setSearchInput}
200
- />
201
- <CommandList
202
- className="custom-scrollbar paragraph-small-medium text-gray-600 h-[162px]"
203
- onScroll={handleScroll}
204
- >
205
- {isLoading ? (
206
- <>
207
- <UserItemSkeleton />
208
- <UserItemSkeleton />
209
- <UserItemSkeleton />
210
- </>
211
- ) : allUsers.length === 0 ? (
212
- <div className="flex flex-col items-center justify-center gap-2 h-full text-gray-400 py-4">
213
- <Users size={28} className="text-gray-300" />
214
- <span className="paragraph-small-medium text-center text-gray-500">
215
- Nenhum usuário neste projeto.
216
- <br />
217
- Adicione um usuário para começar.
218
- </span>
219
- </div>
220
- ) : (
221
- <>
222
- <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
223
- {allUsers.map((user) => (
224
- <CommandItem
225
- key={user.id}
226
- value={user.name || String(user.id)}
227
- autoFocus={false}
228
- onPointerEnter={() => setHoveredId(user.id)}
229
- onPointerLeave={() => setHoveredId(null)}
230
- onFocus={() => setHoveredId(user.id)}
231
- onBlur={() => setHoveredId(null)}
232
- onSelect={() =>
233
- toggleUser(user.id, user.checked, user.removable)
234
- }
235
- className={
236
- user.removable ? "cursor-pointer" : "cursor-default"
237
- }
238
- >
239
- <div className="flex items-center justify-between w-full">
240
- <div className="flex items-center gap-2 min-w-0">
241
- <UserAvatar
242
- photo={user.photo}
243
- name={user.name ?? undefined}
244
- size={32}
245
- />
246
- <span className="truncate">{user.name}</span>
247
- </div>
248
- <Tooltip open={user.removable && hoveredId === user.id}>
249
- <TooltipTrigger
250
- onClick={(e) => e.stopPropagation()}
251
- onPointerDown={(e) => e.stopPropagation()}
252
- >
253
- <Checkbox
254
- checked={user.checked}
255
- disabled={!user.removable}
256
- onCheckedChange={() =>
257
- toggleUser(
258
- user.id,
259
- user.checked,
260
- user.removable,
261
- )
262
- }
263
- className={cn(
264
- user.removable && hoveredId === user.id
265
- ? "border-gray-950"
266
- : "",
267
- "disabled:opacity-10",
268
- )}
269
- />
270
- </TooltipTrigger>
271
- <TooltipContent className="z-[1002]">
272
- {user.checked
273
- ? "Remover do projeto"
274
- : "Adicionar ao projeto"}
275
- </TooltipContent>
276
- </Tooltip>
277
- </div>
278
- </CommandItem>
279
- ))}
280
- {isFetchingMore && (
281
- <>
282
- <UserItemSkeleton />
283
- <UserItemSkeleton />
284
- </>
285
- )}
286
- </>
287
- )}
288
- </CommandList>
289
- </Command>
290
- </div>
291
- </PopoverContent>
292
- </Popover>
293
- );
294
- }
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Plus, Settings, User, Users } from "lucide-react";
5
+ import { UserAvatar } from "../ui/data-display/UserAvatar";
6
+ import { useQueryClient, useMutation } from "@tanstack/react-query";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/overlay/Popover";
8
+ import { Button } from "../ui/buttons/Button";
9
+ import {
10
+ Command,
11
+ CommandEmpty,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ } from "../ui/overlay/Command";
16
+ import { Checkbox } from "../ui/form/Checkbox";
17
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/overlay/Tooltip";
18
+ import { cn } from "../../infra/utils/clsx";
19
+ import {
20
+ useListProjectUsers,
21
+ LIST_PROJECT_USERS_BASE_KEY,
22
+ } from "../../modules/projects/hooks/list-project-users.hook";
23
+ import {
24
+ useListAllAccountUsers,
25
+ LIST_ALL_ACCOUNT_USERS_BASE_KEY,
26
+ } from "../../modules/projects/hooks/list-all-account-users.hook";
27
+ import { removeProjectUserAction } from "../../modules/projects/actions/remove-project-user.action";
28
+ import { addProjectUserAction } from "../../modules/projects/actions/add-project-user.action";
29
+
30
+ export interface UsersSelectorPopoverProps {
31
+ projectId?: number;
32
+ side?: "top" | "bottom" | "left" | "right";
33
+ align?: "start" | "center" | "end";
34
+ contentClassName?: string;
35
+ }
36
+
37
+ function UserItemSkeleton() {
38
+ return (
39
+ <div className="flex items-center justify-between px-2 py-1.5 gap-2">
40
+ <div className="flex items-center gap-2 flex-1">
41
+ <div className="skeleton size-8 rounded-full shrink-0" />
42
+ <div className="skeleton h-3.5 w-28 rounded" />
43
+ </div>
44
+ <div className="skeleton size-4 rounded shrink-0" />
45
+ </div>
46
+ );
47
+ }
48
+
49
+ const GAPPS_URL = process.env.NEXT_PUBLIC_GREAT_APPS ?? "";
50
+
51
+ export function UsersSelectorPopover({
52
+ projectId,
53
+ side,
54
+ align,
55
+ contentClassName,
56
+ }: UsersSelectorPopoverProps) {
57
+ const queryClient = useQueryClient();
58
+ const [open, setOpen] = useState(false);
59
+ const [hoveredId, setHoveredId] = useState<number | null>(null);
60
+ const [searchInput, setSearchInput] = useState("");
61
+ const [search, setSearch] = useState("");
62
+
63
+ const id = projectId ?? 0;
64
+
65
+ useEffect(() => {
66
+ const timer = setTimeout(() => setSearch(searchInput), 300);
67
+ return () => clearTimeout(timer);
68
+ }, [searchInput]);
69
+
70
+ const accountUsers = useListAllAccountUsers(search, id || undefined);
71
+ const projectUsers = useListProjectUsers(id, "");
72
+
73
+ const allUsers = accountUsers.data?.pages.flatMap((p) => p.data) ?? [];
74
+ const isLoading = accountUsers.isLoading;
75
+ const isFetchingMore = accountUsers.isFetchingNextPage;
76
+ const totalInProject = projectUsers.data?.pages[0]?.total ?? 0;
77
+
78
+ const invalidate = () => {
79
+ queryClient.invalidateQueries({ queryKey: LIST_PROJECT_USERS_BASE_KEY(id) });
80
+ queryClient.invalidateQueries({ queryKey: LIST_ALL_ACCOUNT_USERS_BASE_KEY() });
81
+ };
82
+
83
+ const removeMutation = useMutation({
84
+ mutationFn: (userId: number) =>
85
+ removeProjectUserAction({ projectId: id, users: [userId] }),
86
+ onSuccess: invalidate,
87
+ });
88
+
89
+ const addMutation = useMutation({
90
+ mutationFn: (userId: number) =>
91
+ addProjectUserAction({ projectId: id, users: [userId] }),
92
+ onSuccess: invalidate,
93
+ });
94
+
95
+ const toggleUser = (userId: number, inProject: boolean, removable: boolean) => {
96
+ if (inProject) {
97
+ if (!removable) return;
98
+ removeMutation.mutate(userId);
99
+ } else {
100
+ addMutation.mutate(userId);
101
+ }
102
+ };
103
+
104
+ const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
105
+ const target = e.currentTarget;
106
+ const nearBottom =
107
+ target.scrollTop + target.clientHeight >= target.scrollHeight - 50;
108
+ if (!nearBottom) return;
109
+ if (accountUsers.hasNextPage && !accountUsers.isFetchingNextPage) {
110
+ accountUsers.fetchNextPage();
111
+ }
112
+ };
113
+
114
+ return (
115
+ <Popover open={open} onOpenChange={setOpen}>
116
+ <PopoverTrigger className="cursor-pointer">
117
+ <div className="flex items-center gap-2 text-gray-950 px-3 py-2 border border-gray-200 rounded-full hover:border-gray-950">
118
+ <span className="paragraph-small-semibold">{totalInProject}</span>
119
+ <User size={16} />
120
+ </div>
121
+ </PopoverTrigger>
122
+
123
+ <PopoverContent
124
+ className={cn(
125
+ "absolute right-[-88px] w-[273px] p-0 bg-white rounded-2xl shadow-md border border-gray-200",
126
+ contentClassName,
127
+ )}
128
+ side={side}
129
+ align={align}
130
+ sideOffset={8}
131
+ >
132
+ <div className="flex flex-col p-3 gap-2">
133
+ <div className="flex items-center justify-between">
134
+ <span className="paragraph-small-semibold">Usuários</span>
135
+ <div className="flex items-center gap-1.5">
136
+ <Button
137
+ variant="secondary"
138
+ className="h-8 paragraph-small-semibold text-gray-950"
139
+ onClick={() => {
140
+ window.location.href = `${GAPPS_URL}/my-teams?createNew=true`;
141
+ }}
142
+ >
143
+ <Plus size={16} />
144
+ Adicionar
145
+ </Button>
146
+ <Tooltip>
147
+ <TooltipTrigger asChild>
148
+ <Button
149
+ variant="secondary"
150
+ className="p-0 size-8"
151
+ onClick={() => {
152
+ window.location.href = `${GAPPS_URL}/my-teams`;
153
+ }}
154
+ >
155
+ <Settings size={18} />
156
+ </Button>
157
+ </TooltipTrigger>
158
+ <TooltipContent className="z-[1002]">
159
+ Visualizar todos usuários
160
+ </TooltipContent>
161
+ </Tooltip>
162
+ </div>
163
+ </div>
164
+
165
+ <Command shouldFilter={false} className="gap-2" value="">
166
+ <CommandInput
167
+ placeholder="Busque aqui"
168
+ value={searchInput}
169
+ onValueChange={setSearchInput}
170
+ />
171
+ <CommandList
172
+ className="custom-scrollbar paragraph-small-medium text-gray-600 h-[162px]"
173
+ onScroll={handleScroll}
174
+ >
175
+ {isLoading ? (
176
+ <>
177
+ <UserItemSkeleton />
178
+ <UserItemSkeleton />
179
+ <UserItemSkeleton />
180
+ </>
181
+ ) : allUsers.length === 0 ? (
182
+ <div className="flex flex-col items-center justify-center gap-2 h-full text-gray-400 py-4">
183
+ <Users size={28} className="text-gray-300" />
184
+ <span className="paragraph-small-medium text-center text-gray-500">
185
+ Nenhum usuário encontrado.
186
+ </span>
187
+ </div>
188
+ ) : (
189
+ <>
190
+ <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
191
+ {allUsers.map((user) => {
192
+ const inProject = user.in_project ?? false;
193
+ const removable =
194
+ inProject &&
195
+ user.profile !== "owner" &&
196
+ user.profile !== "admin";
197
+ const interactive = !inProject || removable;
198
+
199
+ return (
200
+ <CommandItem
201
+ key={user.id}
202
+ value={user.name || String(user.id)}
203
+ autoFocus={false}
204
+ onPointerEnter={() => setHoveredId(user.id)}
205
+ onPointerLeave={() => setHoveredId(null)}
206
+ onFocus={() => setHoveredId(user.id)}
207
+ onBlur={() => setHoveredId(null)}
208
+ onSelect={() => toggleUser(user.id, inProject, removable)}
209
+ className={interactive ? "cursor-pointer" : "cursor-default"}
210
+ >
211
+ <div className="flex items-center justify-between w-full">
212
+ <div className="flex items-center gap-2 min-w-0">
213
+ <UserAvatar
214
+ photo={user.photo}
215
+ name={user.name ?? undefined}
216
+ size={32}
217
+ />
218
+ <span className="truncate">{user.name}</span>
219
+ </div>
220
+ <Tooltip open={interactive && hoveredId === user.id}>
221
+ <TooltipTrigger
222
+ onClick={(e) => e.stopPropagation()}
223
+ onPointerDown={(e) => e.stopPropagation()}
224
+ >
225
+ <Checkbox
226
+ checked={inProject}
227
+ disabled={!interactive}
228
+ onCheckedChange={() =>
229
+ toggleUser(user.id, inProject, removable)
230
+ }
231
+ className={cn(
232
+ interactive && hoveredId === user.id
233
+ ? "border-gray-950"
234
+ : "",
235
+ "disabled:opacity-10",
236
+ )}
237
+ />
238
+ </TooltipTrigger>
239
+ <TooltipContent className="z-[1002]">
240
+ {inProject ? "Remover do projeto" : "Adicionar ao projeto"}
241
+ </TooltipContent>
242
+ </Tooltip>
243
+ </div>
244
+ </CommandItem>
245
+ );
246
+ })}
247
+ {isFetchingMore && (
248
+ <>
249
+ <UserItemSkeleton />
250
+ <UserItemSkeleton />
251
+ </>
252
+ )}
253
+ </>
254
+ )}
255
+ </CommandList>
256
+ </Command>
257
+ </div>
258
+ </PopoverContent>
259
+ </Popover>
260
+ );
261
+ }
package/src/index.ts CHANGED
@@ -256,6 +256,10 @@ export {
256
256
  TooltipProvider,
257
257
  } from "./components/ui/overlay/Tooltip";
258
258
 
259
+ // Embeds
260
+ export { FrillEmbed } from "./components/embeds/FrillEmbed";
261
+ export { CrispEmbed, openCrispHelpdesk } from "./components/embeds/CrispEmbed";
262
+
259
263
  // Store
260
264
  export { useMdSidebarStore } from "./store/useMdSidebarStore";
261
265
  export { useModalManager } from "./store/useModalManager";
@@ -0,0 +1,9 @@
1
+ 'use server';
2
+
3
+ import { safeServerAction } from '../../../utils/safeServerAction';
4
+ import { projectUsersService } from '../services/project-users.service';
5
+ import type { ListUsersParams } from '../types';
6
+
7
+ export async function listAllAccountUsersAction(params: ListUsersParams = {}) {
8
+ return safeServerAction(() => projectUsersService.listAllAccountUsers(params));
9
+ }
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { useInfiniteQuery } from '@tanstack/react-query';
4
+ import { listAllAccountUsersAction } from '../actions/list-all-account-users.action';
5
+ import { withAction } from '../../../utils/withAction';
6
+
7
+ const PAGE_SIZE = 20;
8
+
9
+ export const LIST_ALL_ACCOUNT_USERS_BASE_KEY = () => ['account-users', 'all'];
10
+ export const LIST_ALL_ACCOUNT_USERS_QUERY_KEY = (search: string = '', projectId?: number) => [
11
+ ...LIST_ALL_ACCOUNT_USERS_BASE_KEY(),
12
+ search,
13
+ projectId,
14
+ ];
15
+
16
+ export function useListAllAccountUsers(search: string = '', projectId?: number) {
17
+ return useInfiniteQuery({
18
+ queryKey: LIST_ALL_ACCOUNT_USERS_QUERY_KEY(search, projectId),
19
+ queryFn: ({ pageParam }) =>
20
+ withAction(() =>
21
+ listAllAccountUsersAction({
22
+ search: search || undefined,
23
+ page: pageParam as number,
24
+ limit: PAGE_SIZE,
25
+ inProject: projectId,
26
+ })
27
+ )(),
28
+ getNextPageParam: (lastPage, allPages) => {
29
+ const loaded = allPages.reduce((sum, p) => sum + p.data.length, 0);
30
+ return loaded < lastPage.total ? allPages.length + 1 : undefined;
31
+ },
32
+ initialPageParam: 1,
33
+ });
34
+ }