@hed-hog/core 0.0.401 → 0.0.402
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/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +121 -12
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +245 -3
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +7 -2
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -1
- package/dist/pdf/document/pdf-document.css.js +88 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -1
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -1
- package/dist/pdf/document/pdf-document.renderer.js +73 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -1
- package/dist/pdf/document/pdf-document.types.d.ts +56 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -1
- package/dist/pdf/pdf.service.d.ts +6 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +3 -0
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +83 -21
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +46 -20
- package/hedhog/frontend/messages/pt.json +46 -20
- package/hedhog/table/file.yaml +3 -1
- package/package.json +4 -4
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.ts +363 -2
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/integration-profile/integration-profile.secrets.ts +7 -2
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/tester-coverage.spec.ts +2 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/pdf/document/pdf-document.css.ts +88 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +43 -0
- package/src/pdf/document/pdf-document.renderer.ts +102 -0
- package/src/pdf/document/pdf-document.types.ts +52 -0
- package/src/pdf/pdf.service.ts +11 -0
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +191 -1
- 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
|
|
28
|
-
//
|
|
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/
|
|
62
|
-
|
|
30
|
+
vi.mock('@/components/ui/sheet', () => ({
|
|
31
|
+
Sheet: ({ children, open }: { children: ReactNode; open?: boolean }) =>
|
|
63
32
|
open ? <div>{children}</div> : null,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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:
|
|
91
|
-
//
|
|
92
|
-
await screen.
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from '@/components/ui/
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
</
|
|
117
|
-
<
|
|
175
|
+
</SheetTitle>
|
|
176
|
+
<SheetDescription>
|
|
118
177
|
{t('dialogDescription', { name: user?.name ?? '' })}
|
|
119
|
-
</
|
|
120
|
-
</
|
|
178
|
+
</SheetDescription>
|
|
179
|
+
</SheetHeader>
|
|
180
|
+
|
|
181
|
+
<div className="w-full border-t" />
|
|
121
182
|
|
|
122
|
-
<div className="space-y-
|
|
183
|
+
<div className="flex-1 space-y-6 overflow-y-auto px-6 py-5">
|
|
123
184
|
<div className="space-y-2">
|
|
124
|
-
<Label
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
</
|
|
136
|
-
|
|
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
|
-
<
|
|
250
|
+
<SheetFooter className="border-t px-6">
|
|
160
251
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
161
252
|
{t('cancel')}
|
|
162
253
|
</Button>
|
|
163
|
-
<Button
|
|
164
|
-
{
|
|
254
|
+
<Button
|
|
255
|
+
onClick={handleConfirm}
|
|
256
|
+
disabled={!app}
|
|
257
|
+
loading={submitting}
|
|
258
|
+
loadingLabel={t('opening')}
|
|
259
|
+
>
|
|
260
|
+
{t('confirm')}
|
|
165
261
|
</Button>
|
|
166
|
-
</
|
|
167
|
-
</
|
|
168
|
-
</
|
|
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": "
|
|
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
|
|
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": "
|
|
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": "
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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
|
|
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",
|