@hed-hog/core 0.0.401 → 0.0.404

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.
Files changed (114) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/authorize/authorize.controller.d.ts +1 -0
  6. package/dist/authorize/authorize.controller.d.ts.map +1 -1
  7. package/dist/authorize/authorize.service.d.ts +1 -0
  8. package/dist/authorize/authorize.service.d.ts.map +1 -1
  9. package/dist/authorize/authorize.service.js +3 -0
  10. package/dist/authorize/authorize.service.js.map +1 -1
  11. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  12. package/dist/file/file.controller.d.ts +3 -3
  13. package/dist/file/file.service.d.ts +121 -12
  14. package/dist/file/file.service.d.ts.map +1 -1
  15. package/dist/file/file.service.js +245 -3
  16. package/dist/file/file.service.js.map +1 -1
  17. package/dist/file/provider/abstract.provider.d.ts +33 -0
  18. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  19. package/dist/file/provider/abstract.provider.js +9 -0
  20. package/dist/file/provider/abstract.provider.js.map +1 -1
  21. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  22. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  23. package/dist/file/provider/s3-multipart.util.js +88 -0
  24. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  25. package/dist/file/provider/s3.provider.d.ts +9 -1
  26. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  27. package/dist/file/provider/s3.provider.js +16 -0
  28. package/dist/file/provider/s3.provider.js.map +1 -1
  29. package/dist/file/provider/spaces.provider.d.ts +10 -1
  30. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  31. package/dist/file/provider/spaces.provider.js +17 -0
  32. package/dist/file/provider/spaces.provider.js.map +1 -1
  33. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  34. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  35. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  36. package/dist/integration-profile/integration-profile.secrets.js +7 -2
  37. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  38. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  39. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  40. package/dist/integration-profile/integration-profile.service.js +3 -3
  41. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  42. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  43. package/dist/integration-profile/testers/http-probe.js +5 -3
  44. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  45. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  46. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  47. package/dist/mail/mail.service.d.ts.map +1 -1
  48. package/dist/mail/mail.service.js +16 -1
  49. package/dist/mail/mail.service.js.map +1 -1
  50. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  51. package/dist/mcp/mcp-server.service.js +10 -1
  52. package/dist/mcp/mcp-server.service.js.map +1 -1
  53. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -1
  54. package/dist/pdf/document/pdf-document.css.js +88 -0
  55. package/dist/pdf/document/pdf-document.css.js.map +1 -1
  56. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -1
  57. package/dist/pdf/document/pdf-document.renderer.js +73 -0
  58. package/dist/pdf/document/pdf-document.renderer.js.map +1 -1
  59. package/dist/pdf/document/pdf-document.types.d.ts +56 -0
  60. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -1
  61. package/dist/pdf/pdf.service.d.ts +6 -0
  62. package/dist/pdf/pdf.service.d.ts.map +1 -1
  63. package/dist/pdf/pdf.service.js +3 -0
  64. package/dist/pdf/pdf.service.js.map +1 -1
  65. package/dist/setting/setting.service.d.ts.map +1 -1
  66. package/dist/setting/setting.service.js +3 -0
  67. package/dist/setting/setting.service.js.map +1 -1
  68. package/dist/task/task.service.d.ts +6 -0
  69. package/dist/task/task.service.d.ts.map +1 -1
  70. package/dist/task/task.service.js +44 -13
  71. package/dist/task/task.service.js.map +1 -1
  72. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  73. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  74. package/dist/whatsapp-sender/whatsapp-sender.service.js +83 -21
  75. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  76. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  77. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  78. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  79. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  80. package/hedhog/frontend/messages/en.json +46 -20
  81. package/hedhog/frontend/messages/pt.json +46 -20
  82. package/hedhog/table/file.yaml +3 -1
  83. package/package.json +3 -3
  84. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  85. package/src/access-log/access-log.interceptor.ts +18 -0
  86. package/src/authorize/authorize.service.spec.ts +1 -0
  87. package/src/authorize/authorize.service.ts +2 -0
  88. package/src/file/file-direct-upload.spec.ts +344 -0
  89. package/src/file/file.service.ts +363 -2
  90. package/src/file/provider/abstract.provider.ts +57 -0
  91. package/src/file/provider/s3-multipart.spec.ts +123 -0
  92. package/src/file/provider/s3-multipart.util.ts +126 -0
  93. package/src/file/provider/s3.provider.ts +65 -1
  94. package/src/file/provider/spaces.provider.ts +67 -1
  95. package/src/integration-profile/integration-profile.secrets.ts +7 -2
  96. package/src/integration-profile/integration-profile.service.ts +1 -0
  97. package/src/integration-profile/testers/http-probe.ts +5 -3
  98. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  99. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  100. package/src/integration-profile/testers/tester-coverage.spec.ts +2 -0
  101. package/src/mail/mail.service.spec.ts +25 -0
  102. package/src/mail/mail.service.ts +18 -1
  103. package/src/mcp/mcp-server.service.spec.ts +16 -0
  104. package/src/mcp/mcp-server.service.ts +10 -1
  105. package/src/pdf/document/pdf-document.css.ts +88 -0
  106. package/src/pdf/document/pdf-document.renderer.spec.ts +43 -0
  107. package/src/pdf/document/pdf-document.renderer.ts +102 -0
  108. package/src/pdf/document/pdf-document.types.ts +52 -0
  109. package/src/pdf/pdf.service.ts +11 -0
  110. package/src/setting/setting.service.ts +3 -0
  111. package/src/task/task.service.spec.ts +77 -1
  112. package/src/task/task.service.ts +58 -14
  113. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +191 -1
  114. package/src/whatsapp-sender/whatsapp-sender.service.ts +122 -14
@@ -24,48 +24,22 @@ vi.mock('next-intl', () => ({
24
24
  useTranslations: () => (key: string) => key,
25
25
  }));
26
26
 
27
- // O Select real é Radix e precisa de APIs de ponteiro que o jsdom não tem. O
28
- // stub vira um <select> nativo, mesmo padrão de commerce-form-sheet.test.tsx.
29
- vi.mock('@/components/ui/select', () => ({
30
- Select: ({
31
- children,
32
- value,
33
- onValueChange,
34
- }: {
35
- children: ReactNode;
36
- value?: string;
37
- onValueChange?: (value: string) => void;
38
- }) => (
39
- <select
40
- aria-label="select-app"
41
- value={value}
42
- onChange={(event) => onValueChange?.(event.target.value)}
43
- >
44
- {children}
45
- </select>
46
- ),
47
- SelectContent: ({ children }: { children: ReactNode }) => <>{children}</>,
48
- SelectItem: ({ children, value }: { children: ReactNode; value: string }) => (
49
- <option value={value}>{children}</option>
50
- ),
51
- SelectTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
52
- SelectValue: ({ placeholder }: { placeholder?: string }) => (
53
- <option value="" disabled>
54
- {placeholder ?? 'Selecione'}
55
- </option>
56
- ),
57
- }));
58
-
59
- // O Dialog real também é Radix (portal + focus trap); aqui só interessa o
27
+ // O Sheet real é Radix (portal + focus trap) e o ResizableSheetContent depende
28
+ // de matchMedia/ResizeObserver, que o jsdom não tem; aqui só interessa o
60
29
  // conteúdo renderizado.
61
- vi.mock('@/components/ui/dialog', () => ({
62
- Dialog: ({ children, open }: { children: ReactNode; open?: boolean }) =>
30
+ vi.mock('@/components/ui/sheet', () => ({
31
+ Sheet: ({ children, open }: { children: ReactNode; open?: boolean }) =>
63
32
  open ? <div>{children}</div> : null,
64
- DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
65
- DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
66
- DialogFooter: ({ children }: { children: ReactNode }) => <div>{children}</div>,
67
- DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
68
- DialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
33
+ SheetDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
34
+ SheetFooter: ({ children }: { children: ReactNode }) => <div>{children}</div>,
35
+ SheetHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>,
36
+ SheetTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
37
+ }));
38
+
39
+ vi.mock('@/components/ui/resizable-sheet-content', () => ({
40
+ ResizableSheetContent: ({ children }: { children: ReactNode }) => (
41
+ <div>{children}</div>
42
+ ),
69
43
  }));
70
44
 
71
45
  import { SimulateAccessDialog } from './simulate-access-dialog';
@@ -87,10 +61,9 @@ const renderDialog = () => {
87
61
  };
88
62
 
89
63
  const chooseApp = async (value: string) => {
90
- // Espera a lista de apps chegar: mudar o valor antes de a <option> existir é
91
- // um no-op silencioso e o teste passaria a testar nada.
92
- await screen.findByText(value);
93
- fireEvent.change(screen.getByLabelText('select-app'), { target: { value } });
64
+ // Espera a lista de apps chegar: clicar antes de o card existir falharia, e o
65
+ // teste passaria a testar nada.
66
+ fireEvent.click(await screen.findByRole('radio', { name: new RegExp(`apps.${value}`) }));
94
67
  };
95
68
 
96
69
  describe('SimulateAccessDialog', () => {
@@ -114,10 +87,9 @@ describe('SimulateAccessDialog', () => {
114
87
  it('lista apenas os apps que o backend devolve', async () => {
115
88
  renderDialog();
116
89
 
117
- expect(await screen.findByText('admin')).toBeInTheDocument();
118
- expect(screen.getByText('class')).toBeInTheDocument();
119
- // `training` tem autenticação própria e ficou fora desta fase.
120
- expect(screen.queryByText('training')).not.toBeInTheDocument();
90
+ expect(await screen.findByText('apps.admin')).toBeInTheDocument();
91
+ expect(screen.getByText('apps.class')).toBeInTheDocument();
92
+ expect(screen.queryByText('apps.training')).not.toBeInTheDocument();
121
93
  });
122
94
 
123
95
  it('exige escolher o app antes de confirmar', async () => {
@@ -130,7 +102,13 @@ describe('SimulateAccessDialog', () => {
130
102
 
131
103
  it('abre a aba no gesto do clique, antes do await, e só depois aponta a URL', async () => {
132
104
  // Abrir a aba depois do await cai no bloqueador de pop-up de todo navegador.
133
- const tab = { location: { href: '' }, close: vi.fn() };
105
+ // Sem `noopener`: com ele o navegador devolve `null` e a URL iria para a
106
+ // aba do admin, deixando a nova em branco.
107
+ const tab = {
108
+ location: { href: '' },
109
+ opener: window as Window | null,
110
+ close: vi.fn(),
111
+ };
134
112
  const openSpy = vi
135
113
  .spyOn(window, 'open')
136
114
  .mockReturnValue(tab as unknown as Window);
@@ -139,12 +117,13 @@ describe('SimulateAccessDialog', () => {
139
117
  await chooseApp('class');
140
118
  fireEvent.click(screen.getByRole('button', { name: 'confirm' }));
141
119
 
142
- expect(openSpy).toHaveBeenCalledWith('', '_blank', 'noopener');
120
+ expect(openSpy).toHaveBeenCalledWith('', '_blank');
143
121
  await waitFor(() =>
144
122
  expect(tab.location.href).toBe(
145
123
  'https://class.example.com/simulacao#code=abc'
146
124
  )
147
125
  );
126
+ expect(tab.opener).toBeNull();
148
127
  });
149
128
 
150
129
  it('envia o alvo, o app e o motivo para o backend', async () => {
@@ -1,32 +1,78 @@
1
1
  'use client';
2
2
 
3
3
  import { Button } from '@/components/ui/button';
4
- import {
5
- Dialog,
6
- DialogContent,
7
- DialogDescription,
8
- DialogFooter,
9
- DialogHeader,
10
- DialogTitle,
11
- } from '@/components/ui/dialog';
12
4
  import { Label } from '@/components/ui/label';
5
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
13
6
  import {
14
- Select,
15
- SelectContent,
16
- SelectItem,
17
- SelectTrigger,
18
- SelectValue,
19
- } from '@/components/ui/select';
7
+ Sheet,
8
+ SheetDescription,
9
+ SheetFooter,
10
+ SheetHeader,
11
+ SheetTitle,
12
+ } from '@/components/ui/sheet';
13
+ import { Skeleton } from '@/components/ui/skeleton';
20
14
  import { Textarea } from '@/components/ui/textarea';
15
+ import { cn } from '@/lib/utils';
21
16
  import { User } from '@hed-hog/api-types';
22
17
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
23
- import { VenetianMask } from 'lucide-react';
18
+ import {
19
+ AppWindow,
20
+ CheckCircle2,
21
+ GraduationCap,
22
+ Handshake,
23
+ LayoutDashboard,
24
+ LucideIcon,
25
+ Presentation,
26
+ VenetianMask,
27
+ } from 'lucide-react';
24
28
  import { useTranslations } from 'next-intl';
25
29
  import { useEffect, useState } from 'react';
26
30
  import { toast } from 'sonner';
27
31
 
28
32
  type AppOption = { key: string; url: string };
29
33
 
34
+ // O ícone é o do próprio app, servido pela rota de ícone do Next de cada um
35
+ // (`app/icon.*`). O Lucide só aparece se a imagem não carregar.
36
+ const APP_META: Record<string, { iconPath: string; fallback: LucideIcon }> = {
37
+ admin: { iconPath: '/icon', fallback: LayoutDashboard },
38
+ class: { iconPath: '/icon.png', fallback: GraduationCap },
39
+ partners: { iconPath: '/icon.svg', fallback: Handshake },
40
+ training: { iconPath: '/icon.png', fallback: Presentation },
41
+ };
42
+
43
+ function resolveUrl(path: string, base: string) {
44
+ try {
45
+ return new URL(path, base);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function AppIcon({ option }: { option: AppOption }) {
52
+ const meta = APP_META[option.key];
53
+ const Fallback = meta?.fallback ?? AppWindow;
54
+ const src = meta ? resolveUrl(meta.iconPath, option.url)?.href : undefined;
55
+ const [failed, setFailed] = useState(false);
56
+
57
+ if (!src || failed) {
58
+ return (
59
+ <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-muted">
60
+ <Fallback className="h-5 w-5 text-muted-foreground" />
61
+ </span>
62
+ );
63
+ }
64
+
65
+ return (
66
+ // eslint-disable-next-line @next/next/no-img-element
67
+ <img
68
+ src={src}
69
+ alt=""
70
+ className="h-10 w-10 shrink-0 rounded-lg object-contain"
71
+ onError={() => setFailed(true)}
72
+ />
73
+ );
74
+ }
75
+
30
76
  export function SimulateAccessDialog({
31
77
  open,
32
78
  onOpenChange,
@@ -59,6 +105,9 @@ export function SimulateAccessDialog({
59
105
 
60
106
  const options = apps?.apps ?? [];
61
107
 
108
+ const appLabel = (key: string) =>
109
+ APP_META[key] ? t(`apps.${key}` as 'apps.admin') : key;
110
+
62
111
  useEffect(() => {
63
112
  if (!open) {
64
113
  setApp('');
@@ -71,8 +120,10 @@ export function SimulateAccessDialog({
71
120
  if (!user || !app) return;
72
121
 
73
122
  // A aba precisa ser aberta AGORA, no gesto do clique. Abrir depois do await
74
- // cai no bloqueador de pop-up de todo navegador.
75
- const tab = window.open('', '_blank', 'noopener');
123
+ // cai no bloqueador de pop-up de todo navegador. Sem `noopener`: com ele o
124
+ // `window.open` devolve `null` mesmo abrindo a aba, e a URL acabaria na aba
125
+ // do admin. O vínculo com o admin é cortado à mão antes de navegar.
126
+ const tab = window.open('', '_blank');
76
127
 
77
128
  setSubmitting(true);
78
129
  try {
@@ -84,6 +135,7 @@ export function SimulateAccessDialog({
84
135
  });
85
136
 
86
137
  if (tab) {
138
+ tab.opener = null;
87
139
  tab.location.href = data.url;
88
140
  } else {
89
141
  // Pop-up bloqueado apesar do gesto: o código vale 60s, então mandar o
@@ -92,14 +144,15 @@ export function SimulateAccessDialog({
92
144
  }
93
145
 
94
146
  onOpenChange(false);
95
- } catch (error: any) {
147
+ } catch (error) {
96
148
  tab?.close();
97
149
  // A mensagem do backend é específica (alvo administrador, conta
98
150
  // desativada, app sem URL...) e vale mais que um texto genérico.
151
+ const backendMessage = (
152
+ error as { response?: { data?: { message?: unknown } } }
153
+ )?.response?.data?.message;
99
154
  const message =
100
- typeof error?.response?.data?.message === 'string'
101
- ? error.response.data.message
102
- : t('startFailure');
155
+ typeof backendMessage === 'string' ? backendMessage : t('startFailure');
103
156
  toast.error(message);
104
157
  } finally {
105
158
  setSubmitting(false);
@@ -107,35 +160,73 @@ export function SimulateAccessDialog({
107
160
  };
108
161
 
109
162
  return (
110
- <Dialog open={open} onOpenChange={onOpenChange}>
111
- <DialogContent>
112
- <DialogHeader>
113
- <DialogTitle className="flex items-center gap-2">
114
- <VenetianMask className="h-4 w-4 text-amber-600" />
163
+ <Sheet open={open} onOpenChange={onOpenChange}>
164
+ <ResizableSheetContent
165
+ sheetId="core-users-simulate-access-sheet"
166
+ defaultWidth={520}
167
+ minWidth={420}
168
+ maxWidth={800}
169
+ className="w-full sm:max-w-xl gap-0"
170
+ >
171
+ <SheetHeader className="px-6 pt-6 pb-4">
172
+ <SheetTitle className="flex items-center gap-2">
173
+ <VenetianMask className="h-5 w-5 text-amber-600" />
115
174
  {t('dialogTitle')}
116
- </DialogTitle>
117
- <DialogDescription>
175
+ </SheetTitle>
176
+ <SheetDescription>
118
177
  {t('dialogDescription', { name: user?.name ?? '' })}
119
- </DialogDescription>
120
- </DialogHeader>
178
+ </SheetDescription>
179
+ </SheetHeader>
180
+
181
+ <div className="w-full border-t" />
121
182
 
122
- <div className="space-y-4">
183
+ <div className="flex-1 space-y-6 overflow-y-auto px-6 py-5">
123
184
  <div className="space-y-2">
124
- <Label htmlFor="impersonation-app">{t('targetApp')}</Label>
125
- <Select value={app} onValueChange={setApp} disabled={isLoadingApps}>
126
- <SelectTrigger id="impersonation-app" className="w-full">
127
- <SelectValue placeholder={t('targetAppPlaceholder')} />
128
- </SelectTrigger>
129
- <SelectContent>
130
- {options.map((option) => (
131
- <SelectItem key={option.key} value={option.key}>
132
- {option.key}
133
- </SelectItem>
185
+ <Label id="impersonation-app-label">{t('targetApp')}</Label>
186
+ {isLoadingApps ? (
187
+ <div className="space-y-2">
188
+ {[0, 1, 2].map((index) => (
189
+ <Skeleton key={index} className="h-[62px] w-full rounded-lg" />
134
190
  ))}
135
- </SelectContent>
136
- </Select>
137
- {!isLoadingApps && options.length === 0 && (
191
+ </div>
192
+ ) : options.length === 0 ? (
138
193
  <p className="text-xs text-destructive">{t('noAppsConfigured')}</p>
194
+ ) : (
195
+ <div
196
+ role="radiogroup"
197
+ aria-labelledby="impersonation-app-label"
198
+ className="space-y-2"
199
+ >
200
+ {options.map((option) => {
201
+ const selected = option.key === app;
202
+ return (
203
+ <button
204
+ key={option.key}
205
+ type="button"
206
+ role="radio"
207
+ aria-checked={selected}
208
+ onClick={() => setApp(option.key)}
209
+ className={cn(
210
+ 'flex w-full cursor-pointer items-center gap-3 rounded-lg border bg-card px-3 py-2.5 text-left transition-colors hover:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
211
+ selected && 'border-primary bg-primary/5 hover:border-primary'
212
+ )}
213
+ >
214
+ <AppIcon option={option} />
215
+ <div className="min-w-0 flex-1">
216
+ <p className="truncate text-sm font-medium">
217
+ {appLabel(option.key)}
218
+ </p>
219
+ <p className="truncate text-xs text-muted-foreground">
220
+ {resolveUrl('/', option.url)?.host ?? option.url}
221
+ </p>
222
+ </div>
223
+ {selected && (
224
+ <CheckCircle2 className="h-5 w-5 shrink-0 text-primary" />
225
+ )}
226
+ </button>
227
+ );
228
+ })}
229
+ </div>
139
230
  )}
140
231
  </div>
141
232
 
@@ -156,15 +247,20 @@ export function SimulateAccessDialog({
156
247
  </div>
157
248
  </div>
158
249
 
159
- <DialogFooter>
250
+ <SheetFooter className="border-t px-6">
160
251
  <Button variant="outline" onClick={() => onOpenChange(false)}>
161
252
  {t('cancel')}
162
253
  </Button>
163
- <Button onClick={handleConfirm} disabled={!app || submitting}>
164
- {submitting ? t('opening') : t('confirm')}
254
+ <Button
255
+ onClick={handleConfirm}
256
+ disabled={!app}
257
+ loading={submitting}
258
+ loadingLabel={t('opening')}
259
+ >
260
+ {t('confirm')}
165
261
  </Button>
166
- </DialogFooter>
167
- </DialogContent>
168
- </Dialog>
262
+ </SheetFooter>
263
+ </ResizableSheetContent>
264
+ </Sheet>
169
265
  );
170
266
  }
@@ -2626,6 +2626,11 @@
2626
2626
  "testConnectionSuccess": "Connection validated successfully.",
2627
2627
  "testConnectionOkWithInfo": "Connection validated: {info}",
2628
2628
  "testConnectionWarning": "Credentials are valid, but: {warning}",
2629
+ "testConnectionWarningCodes": {
2630
+ "google_oauth_testing": "The OAuth consent screen is in Testing: Google refresh tokens expire after 7 days, so channels must be reconnected every week. Publish the app (In production) to keep them connected.",
2631
+ "youtube_audit_pending": "Until the YouTube API audit is approved for this Google Cloud project, uploaded videos stay private.",
2632
+ "google_business_access_pending": "The Business Profile APIs require approved access (Basic API Access): until Google approves this project the quota is 0 and connections fail."
2633
+ },
2629
2634
  "testConnectionError": "Connection test failed.",
2630
2635
  "testConnectionErrorFallback": "Could not validate the credentials.",
2631
2636
  "testConnectionNotPersisted": "Save the profile to record the result.",
@@ -2671,7 +2676,9 @@
2671
2676
  "fieldWabaId": "WhatsApp Business Account ID (WABA)",
2672
2677
  "fieldVerifyToken": "Webhook Verify Token",
2673
2678
  "fieldGraphApiVersion": "Graph API Version",
2674
- "fieldLinkedInApiVersion": "LinkedIn API version (YYYYMM)",
2679
+ "fieldLinkedInApiVersion": "LinkedIn API version (YYYYMM)",
2680
+ "fieldXClientId": "Client ID (OAuth 2.0)",
2681
+ "fieldXClientSecret": "Client Secret (OAuth 2.0)",
2675
2682
  "fieldEnvironmentTest": "Test",
2676
2683
  "fieldEnvironmentProduction": "Production",
2677
2684
  "copiedToClipboard": "Copied to clipboard",
@@ -2840,32 +2847,42 @@
2840
2847
  "metaSocialSectionTitle": "Social network connection",
2841
2848
  "metaSocialDescription": "This profile is the HedHog app on Meta. Pages and Instagram accounts are connected per brand, under Social → Brands → Channels.",
2842
2849
  "metaSocialRedirectUriLabel": "OAuth redirect URI",
2843
- "metaSocialRedirectUriUnavailable": "Set NEXT_PUBLIC_API_BASE_URL to show the URI",
2850
+ "metaSocialRedirectUriUnavailable": "Fill in the API URL setting (api-url) to show the URI",
2844
2851
  "metaSocialStepsTitle": "How to configure it in the Meta dashboard",
2845
2852
  "metaSocialStep1": "Create a Business app and add Facebook Login for Business and the Instagram API with Facebook Login.",
2846
- "metaSocialStep2": "Create a Login Configuration of type \"User access token\" with the publishing permissions and paste its ID above.",
2853
+ "metaSocialStep2": "Create a Login Configuration of type \"User access token\" with the permissions pages_show_list, business_management, pages_read_engagement, pages_manage_posts, instagram_basic and instagram_content_publish, and paste its ID in the Login Configuration ID field.",
2847
2854
  "metaSocialStep3": "Add the URI above to \"Valid OAuth Redirect URIs\".",
2848
- "metaSocialStep4": "Until App Review is approved, use Sandbox mode or only the app administrators and testers.",
2855
+ "metaSocialStep4": "In App settings > Basic, fill in the privacy policy, the terms of service and the data deletion instructions URL.",
2849
2856
  "instagramSocialSectionTitle": "Instagram connection",
2850
2857
  "instagramSocialDescription": "This profile is the HedHog app on the Instagram API with Instagram Login. Professional Instagram accounts (Business or Creator) are connected per brand, under Social → Brands → Channels, with or without a Facebook Page.",
2851
2858
  "instagramSocialRedirectUriLabel": "OAuth redirect URI",
2852
- "instagramSocialRedirectUriUnavailable": "Set NEXT_PUBLIC_API_BASE_URL to show the URI",
2859
+ "instagramSocialRedirectUriUnavailable": "Fill in the API URL setting (api-url) to show the URI",
2853
2860
  "instagramSocialStepsTitle": "How to configure it in the Meta dashboard",
2854
2861
  "instagramSocialStep1": "In the Meta app, add the Instagram product and open \"API setup with Instagram login\".",
2855
2862
  "instagramSocialStep2": "Copy the Instagram app ID and Instagram app secret shown there and paste them above. They are not the Meta app ID and secret.",
2856
2863
  "instagramSocialStep3": "Under \"Set up Instagram business login\" > Business login settings, add the URI above to \"OAuth redirect URIs\" exactly as shown, with no trailing slash.",
2857
2864
  "instagramSocialStep4": "In the same settings, fill in the Deauthorize callback URL and the Data deletion request URL with the page of your privacy instructions.",
2858
- "instagramSocialStep5": "Until App Review is approved, add the accounts you will connect as Instagram testers of the app, or use Sandbox mode.",
2859
- "linkedinSocialSectionTitle": "LinkedIn connection",
2860
- "linkedinSocialDescription": "This profile is the HedHog app on the LinkedIn Community Management API. Company pages are connected per brand, under Social → Brands → Channels, by a member who is an administrator or content administrator of the page.",
2861
- "linkedinSocialRedirectUriLabel": "Authorized redirect URL",
2862
- "linkedinSocialRedirectUriUnavailable": "Set NEXT_PUBLIC_API_BASE_URL to show the URL",
2863
- "linkedinSocialStepsTitle": "How to configure it in the LinkedIn Developer Portal",
2864
- "linkedinSocialStep1": "Create a dedicated app, with no other products, linked to the company page and verified by a super admin of that LinkedIn Page.",
2865
- "linkedinSocialStep2": "In the Products tab, request the Community Management API. Access starts in the Development tier.",
2866
- "linkedinSocialStep3": "In the Auth tab, add the URL above to \"Authorized redirect URLs for your app\" exactly as shown, then copy the Client ID and Primary Client Secret and paste them above.",
2867
- "linkedinSocialStep4": "Before going to production, request the Standard tier, which requires a screencast of each use case of the app.",
2868
- "linkedinSocialStep5": "Until the Standard tier is approved, use Sandbox mode.",
2865
+ "instagramSocialStep5": "Until App Review is approved, add the accounts you will connect as Instagram testers of the app and keep the Production mode: Sandbox is a local simulator and is refused in production.",
2866
+ "linkedinSocialSectionTitle": "LinkedIn connection",
2867
+ "linkedinSocialDescription": "This profile is the HedHog app on the LinkedIn Community Management API. Company pages are connected per brand, under Social → Brands → Channels, by a member who is an administrator or content administrator of the page.",
2868
+ "linkedinSocialRedirectUriLabel": "Authorized redirect URL",
2869
+ "linkedinSocialRedirectUriUnavailable": "Fill in the API URL setting (api-url) to show the URL",
2870
+ "linkedinSocialStepsTitle": "How to configure it in the LinkedIn Developer Portal",
2871
+ "linkedinSocialStep1": "Create a dedicated app, with no other products, linked to the company page and verified by a super admin of that LinkedIn Page.",
2872
+ "linkedinSocialStep2": "In the Products tab, request the Community Management API. Access starts in the Development tier.",
2873
+ "linkedinSocialStep3": "In the Auth tab, add the URL above to \"Authorized redirect URLs for your app\" exactly as shown, then copy the Client ID and Primary Client Secret and paste them above.",
2874
+ "linkedinSocialStep4": "Before going to production, request the Standard tier, which requires a screencast of each use case of the app.",
2875
+ "linkedinSocialStep5": "Use the Production mode: it already calls LinkedIn in the Development tier, within its limits. Sandbox is a local simulator and is refused in production.",
2876
+ "xSocialSectionTitle": "X connection",
2877
+ "xSocialSectionDescription": "This profile is the HedHog app on the pay-per-use X API. X accounts are connected per brand, in Social → Brands → Channels, by someone who signs in to X with the brand account.",
2878
+ "xSocialRedirectUriLabel": "Callback URI",
2879
+ "xSocialRedirectUriUnavailable": "Fill in the API URL setting (api-url) to show the URL",
2880
+ "xSocialStepsTitle": "How to set it up in the X Developer Console",
2881
+ "xSocialStep1": "In the Developer Console, create a Project and an App. Under User authentication settings, turn on OAuth 2.0 with the Web App (confidential client) type and the Read and write permissions.",
2882
+ "xSocialStep2": "Register the URL above as the Callback URI, exactly as shown, in every environment.",
2883
+ "xSocialStep3": "Copy the OAuth 2.0 Client ID and Client Secret into the fields above (not the API Key and API Secret).",
2884
+ "xSocialStep4": "Under Billing, buy credits and set a spending limit: the API is pay-per-use (US$ 0.015 per post, US$ 0.20 with a link).",
2885
+ "xSocialStep5": "Use the Production mode to connect real accounts. Sandbox is a local simulator and is refused in production.",
2869
2886
  "fieldOauthAppStatus": "OAuth consent screen status",
2870
2887
  "fieldOauthAppStatusTesting": "Testing (the refresh token expires in 7 days and the channel must be reconnected)",
2871
2888
  "fieldOauthAppStatusProduction": "In production (the refresh token does not expire in 7 days)",
@@ -2873,10 +2890,10 @@
2873
2890
  "youtubeSocialDescription": "This profile is the HedHog project on Google Cloud for YouTube. Channels are connected per brand, under Social → Brands → Channels.",
2874
2891
  "googleBusinessSocialDescription": "This profile is the HedHog project on Google Cloud for Google Business Profile. Locations are connected per brand, under Social → Brands → Channels.",
2875
2892
  "googleSocialRedirectUriLabel": "OAuth authorized redirect URI",
2876
- "googleSocialRedirectUriUnavailable": "Set NEXT_PUBLIC_API_BASE_URL to show the URI",
2893
+ "googleSocialRedirectUriUnavailable": "Fill in the API URL setting (api-url) to show the URI",
2877
2894
  "googleSocialStepsTitle": "How to configure it in the Google Cloud Console",
2878
2895
  "youtubeSocialStep1": "Create a Google Cloud project just for YouTube and enable the YouTube Data API v3.",
2879
- "youtubeSocialStep2": "Configure the OAuth consent screen with the youtube.upload and youtube.readonly scopes. Since they are sensitive scopes, the app must pass Google verification before it is published.",
2896
+ "youtubeSocialStep2": "Configure the OAuth consent screen with the youtube.upload, youtube.readonly and youtube.force-ssl scopes (sensitive, they require Google verification before the app is published) and yt-analytics.readonly (non-sensitive).",
2880
2897
  "youtubeSocialStep3": "Create an OAuth client ID of type \"Web application\", add the URI above to \"Authorized redirect URIs\" and paste the Client ID and Client Secret above.",
2881
2898
  "youtubeSocialStep4": "Request the YouTube API compliance audit: until it is approved, every video uploaded by the project stays private.",
2882
2899
  "youtubeSocialStep5": "The default project quota allows about 100 video uploads per day. When it runs out, publishing is rescheduled for after the reset (midnight Pacific Time).",
@@ -2885,7 +2902,10 @@
2885
2902
  "googleBusinessSocialStep3": "Enable the My Business Account Management API, the My Business Business Information API and the Google My Business API. Until access is approved the quota is 0 and every call fails.",
2886
2903
  "googleBusinessSocialStep4": "Configure the OAuth consent screen with the business.manage scope.",
2887
2904
  "googleBusinessSocialStep5": "Create an OAuth client ID of type \"Web application\", add the URI above to \"Authorized redirect URIs\" and paste the Client ID and Client Secret above.",
2888
- "googleSocialTestingStep": "While the consent screen is in \"Testing\", choose \"Testing\" in the status above and use Sandbox mode to develop without waiting for Google approvals."
2905
+ "googleSocialTestingStep": "While the consent screen is in \"Testing\", choose \"Testing\" in the status above and add the Google accounts that will be connected as test users.",
2906
+ "metaSocialStep5": "Complete Business Verification and request App Review for the permissions. Until it is approved, only the app administrators, developers and testers can connect.",
2907
+ "socialRedirectUriMismatch": "The API URL setting ({setting}) differs from the API this panel uses ({env}). The URI above follows the setting, which is what the backend sends to the network.",
2908
+ "socialGuideLink": "Open the full setup guide"
2889
2909
  },
2890
2910
  "SystemPagesPage": {
2891
2911
  "title": "Pages",
@@ -3301,7 +3321,13 @@
3301
3321
  "redeemInvalidDescription": "This link works only once and expires after a minute. Ask the operator to start a new simulation.",
3302
3322
  "redeemMissingCode": "No simulation code found in the link.",
3303
3323
  "noAdminAccessTitle": "This user has no access to the admin panel",
3304
- "noAdminAccessDescription": "{name} has no administrative role, so there is nothing to show here. Simulate them in the application they actually use."
3324
+ "noAdminAccessDescription": "{name} has no administrative role, so there is nothing to show here. Simulate them in the application they actually use.",
3325
+ "apps": {
3326
+ "admin": "Hub (administration)",
3327
+ "class": "Hcode Class",
3328
+ "partners": "Partner Portal",
3329
+ "training": "Hcode Training"
3330
+ }
3305
3331
  },
3306
3332
  "MessageTemplatesPage": {
3307
3333
  "title": "Message templates",