@hed-hog/core 0.0.370 → 0.0.371
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/LICENSE.md +21 -0
- package/README.md +2 -0
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +10 -7
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/app-command/app-command.module.d.ts +3 -0
- package/dist/app-command/app-command.module.d.ts.map +1 -0
- package/dist/app-command/app-command.module.js +22 -0
- package/dist/app-command/app-command.module.js.map +1 -0
- package/dist/app-command/app-command.registry.d.ts +22 -0
- package/dist/app-command/app-command.registry.d.ts.map +1 -0
- package/dist/{webhook-integration/webhook-command.registry.js → app-command/app-command.registry.js} +6 -6
- package/dist/app-command/app-command.registry.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +2 -2
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +20 -7
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +3 -3
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +10 -36
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/dto/forget.dto.d.ts +1 -0
- package/dist/auth/dto/forget.dto.d.ts.map +1 -1
- package/dist/auth/dto/forget.dto.js +5 -0
- package/dist/auth/dto/forget.dto.js.map +1 -1
- package/dist/auth/guards/auth.guard.d.ts.map +1 -1
- package/dist/auth/guards/auth.guard.js +4 -1
- package/dist/auth/guards/auth.guard.js.map +1 -1
- package/dist/challenge/challenge.service.d.ts +8 -7
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +27 -19
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/file/dto/upload.dto.d.ts +3 -0
- package/dist/file/dto/upload.dto.d.ts.map +1 -1
- package/dist/file/dto/upload.dto.js +22 -0
- package/dist/file/dto/upload.dto.js.map +1 -1
- package/dist/file/file.controller.d.ts +2 -1
- package/dist/file/file.controller.d.ts.map +1 -1
- package/dist/file/file.controller.js +29 -16
- package/dist/file/file.controller.js.map +1 -1
- package/dist/file/file.service.d.ts +51 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +250 -18
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/image-optimization.service.d.ts +17 -4
- package/dist/file/image-optimization.service.d.ts.map +1 -1
- package/dist/file/image-optimization.service.js +101 -46
- package/dist/file/image-optimization.service.js.map +1 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +15 -7
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +18 -8
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/integration/services/event-subscriber.registry.d.ts +5 -0
- package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -1
- package/dist/integration/services/event-subscriber.registry.js +15 -0
- package/dist/integration/services/event-subscriber.registry.js.map +1 -1
- package/dist/integration/services/outbox.processor.d.ts.map +1 -1
- package/dist/integration/services/outbox.processor.js +2 -4
- package/dist/integration/services/outbox.processor.js.map +1 -1
- package/dist/integration/services/outbox.service.d.ts +4 -2
- package/dist/integration/services/outbox.service.d.ts.map +1 -1
- package/dist/integration/services/outbox.service.js +26 -15
- package/dist/integration/services/outbox.service.js.map +1 -1
- package/dist/integration-profile/integration-credential-crypto.service.d.ts +43 -0
- package/dist/integration-profile/integration-credential-crypto.service.d.ts.map +1 -0
- package/dist/integration-profile/integration-credential-crypto.service.js +142 -0
- package/dist/integration-profile/integration-credential-crypto.service.js.map +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts +6 -7
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.controller.js +4 -3
- package/dist/integration-profile/integration-profile.controller.js.map +1 -1
- package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.module.js +5 -2
- package/dist/integration-profile/integration-profile.module.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts +16 -0
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -0
- package/dist/integration-profile/integration-profile.secrets.js +56 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts +125 -8
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +257 -68
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +17 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +31 -0
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/language/en.json +178 -0
- package/dist/language/pt.json +178 -0
- package/dist/mail/mail.controller.d.ts.map +1 -1
- package/dist/mail/mail.controller.js +5 -28
- package/dist/mail/mail.controller.js.map +1 -1
- package/dist/mail/mail.module.js +18 -18
- package/dist/mail/mail.module.js.map +1 -1
- package/dist/mail/mail.service.d.ts +13 -2
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +25 -15
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +6 -5
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/oauth/providers/facebook.provider.js +5 -5
- package/dist/oauth/providers/facebook.provider.js.map +1 -1
- package/dist/oauth/providers/github.provider.js +5 -5
- package/dist/oauth/providers/github.provider.js.map +1 -1
- package/dist/oauth/providers/google.provider.js +5 -5
- package/dist/oauth/providers/google.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +5 -5
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft.provider.js +5 -5
- package/dist/oauth/providers/microsoft.provider.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +4 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +16 -9
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +7 -7
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js +28 -21
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +7 -7
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +21 -25
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts +1 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +15 -2
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/token/token.service.d.ts.map +1 -1
- package/dist/token/token.service.js +2 -1
- package/dist/token/token.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +9 -0
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.controller.js +13 -0
- package/dist/user/user.controller.js.map +1 -1
- package/dist/user/user.service.d.ts +7 -0
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/user/user.service.js +19 -0
- package/dist/user/user.service.js.map +1 -1
- package/dist/utils/webauthn-origin.d.ts +11 -0
- package/dist/utils/webauthn-origin.d.ts.map +1 -0
- package/dist/utils/webauthn-origin.js +37 -0
- package/dist/utils/webauthn-origin.js.map +1 -0
- package/dist/webhook-integration/webhook-integration.controller.d.ts +13 -0
- package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.js +29 -0
- package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.js +3 -3
- package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts +18 -2
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +110 -6
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +9 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/menu.yaml +6 -0
- package/hedhog/data/role.yaml +7 -0
- package/hedhog/data/route.yaml +36 -0
- package/hedhog/data/setting_group.yaml +119 -13
- package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
- package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
- package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
- package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
- package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
- package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
- package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
- package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
- package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
- package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
- package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
- package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
- package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
- package/hedhog/frontend/messages/en.json +69 -4
- package/hedhog/frontend/messages/pt.json +69 -4
- package/hedhog/table/setting.yaml +2 -0
- package/hedhog/table/webhook_call_log.yaml +49 -46
- package/package.json +8 -3
- package/src/ai/ai.service.ts +9 -6
- package/src/app-command/app-command.module.ts +9 -0
- package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
- package/src/auth/auth.controller.ts +20 -4
- package/src/auth/auth.service.ts +10 -36
- package/src/auth/dto/forget.dto.ts +5 -1
- package/src/auth/guards/auth.guard.ts +4 -1
- package/src/challenge/challenge.service.ts +30 -19
- package/src/file/dto/upload.dto.ts +25 -1
- package/src/file/file.controller.ts +31 -18
- package/src/file/file.service.ts +341 -16
- package/src/file/image-optimization.service.ts +111 -46
- package/src/file/provider/s3.provider.ts +16 -7
- package/src/file/provider/spaces.provider.ts +15 -5
- package/src/index.ts +4 -1
- package/src/integration/services/event-subscriber.registry.ts +15 -0
- package/src/integration/services/outbox.processor.ts +2 -5
- package/src/integration/services/outbox.service.ts +35 -17
- package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
- package/src/integration-profile/integration-profile.controller.ts +2 -0
- package/src/integration-profile/integration-profile.module.ts +5 -2
- package/src/integration-profile/integration-profile.secrets.ts +51 -0
- package/src/integration-profile/integration-profile.service.ts +335 -54
- package/src/integration-profile/integration-profile.utils.ts +56 -0
- package/src/language/en.json +5 -1
- package/src/language/pt.json +5 -1
- package/src/mail/mail.controller.ts +5 -29
- package/src/mail/mail.module.ts +5 -5
- package/src/mail/mail.service.ts +37 -13
- package/src/mcp-chat/mcp-chat.service.ts +5 -5
- package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
- package/src/oauth/providers/facebook.provider.ts +3 -3
- package/src/oauth/providers/github.provider.ts +3 -3
- package/src/oauth/providers/google.provider.ts +3 -3
- package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
- package/src/oauth/providers/microsoft.provider.ts +3 -3
- package/src/pdf/pdf.service.ts +19 -6
- package/src/profile/profile.controller.ts +21 -14
- package/src/profile/profile.service.ts +23 -20
- package/src/security/security.service.spec.ts +222 -0
- package/src/setting/setting.service.ts +15 -2
- package/src/token/token.service.ts +2 -1
- package/src/user/user.controller.ts +26 -16
- package/src/user/user.service.ts +25 -0
- package/src/utils/webauthn-origin.ts +38 -0
- package/src/webhook-integration/webhook-integration.controller.ts +23 -0
- package/src/webhook-integration/webhook-integration.module.ts +3 -3
- package/src/webhook-integration/webhook-integration.service.ts +115 -5
- package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
- package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
- package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
- package/src/menu/menu.service.spec.ts +0 -247
- package/src/route/route.service.spec.ts +0 -300
- package/src/screen/screen.service.spec.ts +0 -298
- package/src/user/user.service.spec.ts +0 -294
|
@@ -124,6 +124,8 @@ function getErrorMessage(error: unknown, fallback: string) {
|
|
|
124
124
|
return fallback;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
|
|
128
|
+
|
|
127
129
|
export default function AiAgentPage() {
|
|
128
130
|
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
129
131
|
const t = useTranslations('core.AiAgentPage');
|
|
@@ -133,11 +135,41 @@ export default function AiAgentPage() {
|
|
|
133
135
|
model: z.string().optional(),
|
|
134
136
|
instructions: z.string().optional(),
|
|
135
137
|
});
|
|
138
|
+
const { data: generalSettings } = useQuery<{
|
|
139
|
+
data: Array<{ slug: string; value: string }>;
|
|
140
|
+
}>({
|
|
141
|
+
queryKey: ['setting-group-general'],
|
|
142
|
+
queryFn: async () => {
|
|
143
|
+
const response = await request<{
|
|
144
|
+
data: Array<{ slug: string; value: string }>;
|
|
145
|
+
}>({ url: '/setting/group/general', method: 'GET' });
|
|
146
|
+
return response.data;
|
|
147
|
+
},
|
|
148
|
+
staleTime: 5 * 60 * 1000,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const pageSizeOptions = useMemo(() => {
|
|
152
|
+
const setting = generalSettings?.data?.find(
|
|
153
|
+
(s) => s.slug === 'pagination-page-sizes'
|
|
154
|
+
);
|
|
155
|
+
if (!setting?.value) return DEFAULT_PAGE_SIZES;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(setting.value) as string[];
|
|
158
|
+
const sizes = parsed
|
|
159
|
+
.map(Number)
|
|
160
|
+
.filter((n) => !isNaN(n) && n > 0)
|
|
161
|
+
.sort((a, b) => a - b);
|
|
162
|
+
return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
|
|
163
|
+
} catch {
|
|
164
|
+
return DEFAULT_PAGE_SIZES;
|
|
165
|
+
}
|
|
166
|
+
}, [generalSettings]);
|
|
167
|
+
|
|
136
168
|
const [page, setPage] = useState(1);
|
|
137
169
|
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
138
170
|
storageKey: 'pagination:global:pageSize',
|
|
139
|
-
defaultValue:
|
|
140
|
-
allowedValues:
|
|
171
|
+
defaultValue: 12,
|
|
172
|
+
allowedValues: pageSizeOptions,
|
|
141
173
|
});
|
|
142
174
|
const [searchQuery, setSearchQuery] = useState('');
|
|
143
175
|
const debouncedSearch = useDebounce(searchQuery, 400);
|
|
@@ -440,6 +472,7 @@ export default function AiAgentPage() {
|
|
|
440
472
|
setPageSize(value);
|
|
441
473
|
setPage(1);
|
|
442
474
|
}}
|
|
475
|
+
pageSizeOptions={pageSizeOptions}
|
|
443
476
|
/>
|
|
444
477
|
|
|
445
478
|
<Sheet
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
InputGroup,
|
|
5
|
+
InputGroupAddon,
|
|
6
|
+
InputGroupInput,
|
|
7
|
+
} from '@/components/ui/input-group';
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from '@/components/ui/select';
|
|
15
|
+
import { cn } from '@/lib/utils';
|
|
16
|
+
import { useTranslations } from 'next-intl';
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
|
|
19
|
+
type ByteUnit = 'bytes' | 'kb' | 'mb' | 'gb' | 'tb';
|
|
20
|
+
|
|
21
|
+
// Base binária (1024) — consistente com os valores existentes (ex.: 10485760 = 10 MB).
|
|
22
|
+
const UNIT_BYTES: Record<ByteUnit, number> = {
|
|
23
|
+
bytes: 1,
|
|
24
|
+
kb: 1024,
|
|
25
|
+
mb: 1024 ** 2,
|
|
26
|
+
gb: 1024 ** 3,
|
|
27
|
+
tb: 1024 ** 4,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Da maior para a menor unidade — usado para escolher a melhor unidade de exibição.
|
|
31
|
+
const UNIT_ORDER: ByteUnit[] = ['tb', 'gb', 'mb', 'kb', 'bytes'];
|
|
32
|
+
|
|
33
|
+
const STORAGE_PREFIX = 'byte-unit:';
|
|
34
|
+
|
|
35
|
+
/** Maior unidade que divide o total exatamente (default: bytes). */
|
|
36
|
+
function pickBestUnit(totalBytes: number): ByteUnit {
|
|
37
|
+
if (totalBytes > 0) {
|
|
38
|
+
for (const unit of UNIT_ORDER) {
|
|
39
|
+
if (totalBytes % UNIT_BYTES[unit] === 0) {
|
|
40
|
+
return unit;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return 'bytes';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readSavedUnit(persistKey?: string): ByteUnit | null {
|
|
48
|
+
if (!persistKey || typeof window === 'undefined') {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const saved = window.localStorage.getItem(STORAGE_PREFIX + persistKey);
|
|
52
|
+
return saved && saved in UNIT_BYTES ? (saved as ByteUnit) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toDisplay(totalBytes: number, unit: ByteUnit): string {
|
|
56
|
+
if (!totalBytes) {
|
|
57
|
+
return '0';
|
|
58
|
+
}
|
|
59
|
+
const value = totalBytes / UNIT_BYTES[unit];
|
|
60
|
+
// Evita ruído de ponto flutuante (ex.: 0.30000000000000004).
|
|
61
|
+
return String(Math.round(value * 10000) / 10000);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ByteSizeInputProps {
|
|
65
|
+
/** Valor total em bytes (unidade canônica armazenada). */
|
|
66
|
+
value: number;
|
|
67
|
+
/** Chamado com o novo total em bytes. */
|
|
68
|
+
onChange: (totalBytes: number) => void;
|
|
69
|
+
/** Slug do setting — usado para lembrar a unidade escolhida no localStorage. */
|
|
70
|
+
persistKey?: string;
|
|
71
|
+
disabled?: boolean;
|
|
72
|
+
className?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Input numérico com seletor de unidade (Bytes/KB/MB/GB/TB, base binária 1024). O valor é
|
|
77
|
+
* sempre persistido em **bytes**; o usuário escolhe apenas como visualizar/editar e a conversão
|
|
78
|
+
* de volta para bytes acontece a cada alteração. A unidade inicial é a maior que divide o valor
|
|
79
|
+
* exatamente (ou a última escolhida pelo usuário, se houver `persistKey`).
|
|
80
|
+
*
|
|
81
|
+
* Irmão do `DurationInput` (segundos → min/horas/dias).
|
|
82
|
+
*/
|
|
83
|
+
export function ByteSizeInput({
|
|
84
|
+
value,
|
|
85
|
+
onChange,
|
|
86
|
+
persistKey,
|
|
87
|
+
disabled,
|
|
88
|
+
className,
|
|
89
|
+
}: ByteSizeInputProps) {
|
|
90
|
+
const t = useTranslations('core.Configurations');
|
|
91
|
+
const total = Math.max(0, Math.floor(Number(value) || 0));
|
|
92
|
+
|
|
93
|
+
const [unit, setUnit] = useState<ByteUnit>(
|
|
94
|
+
() => readSavedUnit(persistKey) ?? pickBestUnit(total)
|
|
95
|
+
);
|
|
96
|
+
// Texto digitado em andamento. Quando `null`, a exibição é derivada de `value` + `unit`
|
|
97
|
+
// (mantém-se sincronizado com mudanças externas sem precisar de efeito/setState).
|
|
98
|
+
const [editValue, setEditValue] = useState<string | null>(null);
|
|
99
|
+
|
|
100
|
+
const display = editValue ?? toDisplay(total, unit);
|
|
101
|
+
|
|
102
|
+
const handleInputChange = (raw: string) => {
|
|
103
|
+
setEditValue(raw);
|
|
104
|
+
const parsed = parseFloat(raw);
|
|
105
|
+
const bytes = Number.isFinite(parsed)
|
|
106
|
+
? Math.max(0, Math.round(parsed * UNIT_BYTES[unit]))
|
|
107
|
+
: 0;
|
|
108
|
+
onChange(bytes);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleBlur = () => {
|
|
112
|
+
// Volta a exibir o valor canônico (reformatado) na unidade atual.
|
|
113
|
+
setEditValue(null);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleUnitChange = (nextUnit: ByteUnit) => {
|
|
117
|
+
// Troca de unidade é só exibição: mantém os bytes constantes, não dispara onChange.
|
|
118
|
+
setUnit(nextUnit);
|
|
119
|
+
setEditValue(null);
|
|
120
|
+
if (persistKey && typeof window !== 'undefined') {
|
|
121
|
+
window.localStorage.setItem(STORAGE_PREFIX + persistKey, nextUnit);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<InputGroup className={cn('bg-background', className)}>
|
|
127
|
+
<InputGroupInput
|
|
128
|
+
type="number"
|
|
129
|
+
inputMode="decimal"
|
|
130
|
+
min={0}
|
|
131
|
+
step="any"
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
value={display}
|
|
134
|
+
onChange={(e) => handleInputChange(e.target.value)}
|
|
135
|
+
onBlur={handleBlur}
|
|
136
|
+
/>
|
|
137
|
+
<InputGroupAddon align="inline-end" className="pr-1">
|
|
138
|
+
<Select
|
|
139
|
+
value={unit}
|
|
140
|
+
onValueChange={(next) => handleUnitChange(next as ByteUnit)}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
>
|
|
143
|
+
<SelectTrigger
|
|
144
|
+
size="sm"
|
|
145
|
+
className="h-7 gap-1 border-0 bg-transparent px-2 shadow-none focus-visible:ring-0"
|
|
146
|
+
>
|
|
147
|
+
<SelectValue />
|
|
148
|
+
</SelectTrigger>
|
|
149
|
+
<SelectContent>
|
|
150
|
+
<SelectItem value="bytes">{t('byteUnitBytes')}</SelectItem>
|
|
151
|
+
<SelectItem value="kb">{t('byteUnitKB')}</SelectItem>
|
|
152
|
+
<SelectItem value="mb">{t('byteUnitMB')}</SelectItem>
|
|
153
|
+
<SelectItem value="gb">{t('byteUnitGB')}</SelectItem>
|
|
154
|
+
<SelectItem value="tb">{t('byteUnitTB')}</SelectItem>
|
|
155
|
+
</SelectContent>
|
|
156
|
+
</Select>
|
|
157
|
+
</InputGroupAddon>
|
|
158
|
+
</InputGroup>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
InputGroup,
|
|
5
|
+
InputGroupAddon,
|
|
6
|
+
InputGroupInput,
|
|
7
|
+
} from '@/components/ui/input-group';
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from '@/components/ui/select';
|
|
15
|
+
import { cn } from '@/lib/utils';
|
|
16
|
+
import { useTranslations } from 'next-intl';
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
|
|
19
|
+
type DurationUnit = 'seconds' | 'minutes' | 'hours' | 'days';
|
|
20
|
+
|
|
21
|
+
const UNIT_SECONDS: Record<DurationUnit, number> = {
|
|
22
|
+
seconds: 1,
|
|
23
|
+
minutes: 60,
|
|
24
|
+
hours: 3600,
|
|
25
|
+
days: 86400,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Da maior para a menor unidade — usado para escolher a melhor unidade de exibição.
|
|
29
|
+
const UNIT_ORDER: DurationUnit[] = ['days', 'hours', 'minutes', 'seconds'];
|
|
30
|
+
|
|
31
|
+
const STORAGE_PREFIX = 'duration-unit:';
|
|
32
|
+
|
|
33
|
+
/** Maior unidade que divide o total exatamente (default: segundos). */
|
|
34
|
+
function pickBestUnit(totalSeconds: number): DurationUnit {
|
|
35
|
+
if (totalSeconds > 0) {
|
|
36
|
+
for (const unit of UNIT_ORDER) {
|
|
37
|
+
if (totalSeconds % UNIT_SECONDS[unit] === 0) {
|
|
38
|
+
return unit;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return 'seconds';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readSavedUnit(persistKey?: string): DurationUnit | null {
|
|
46
|
+
if (!persistKey || typeof window === 'undefined') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const saved = window.localStorage.getItem(STORAGE_PREFIX + persistKey);
|
|
50
|
+
return saved && saved in UNIT_SECONDS ? (saved as DurationUnit) : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toDisplay(totalSeconds: number, unit: DurationUnit): string {
|
|
54
|
+
if (!totalSeconds) {
|
|
55
|
+
return '0';
|
|
56
|
+
}
|
|
57
|
+
const value = totalSeconds / UNIT_SECONDS[unit];
|
|
58
|
+
// Evita ruído de ponto flutuante (ex.: 0.30000000000000004).
|
|
59
|
+
return String(Math.round(value * 10000) / 10000);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DurationInputProps {
|
|
63
|
+
/** Valor total em segundos (unidade canônica armazenada). */
|
|
64
|
+
value: number;
|
|
65
|
+
/** Chamado com o novo total em segundos. */
|
|
66
|
+
onChange: (totalSeconds: number) => void;
|
|
67
|
+
/** Slug do setting — usado para lembrar a unidade escolhida no localStorage. */
|
|
68
|
+
persistKey?: string;
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Input numérico com seletor de unidade (segundos/minutos/horas/dias). O valor é sempre
|
|
75
|
+
* persistido em **segundos**; o usuário escolhe apenas como visualizar/editar e a conversão
|
|
76
|
+
* de volta para segundos acontece a cada alteração. A unidade inicial é a maior que divide
|
|
77
|
+
* o valor exatamente (ou a última escolhida pelo usuário, se houver `persistKey`).
|
|
78
|
+
*
|
|
79
|
+
* Não confundir com o `InputDuration` (campo mascarado mm:ss) usado na estrutura de cursos do LMS.
|
|
80
|
+
*/
|
|
81
|
+
export function DurationInput({
|
|
82
|
+
value,
|
|
83
|
+
onChange,
|
|
84
|
+
persistKey,
|
|
85
|
+
disabled,
|
|
86
|
+
className,
|
|
87
|
+
}: DurationInputProps) {
|
|
88
|
+
const t = useTranslations('core.Configurations');
|
|
89
|
+
const total = Math.max(0, Math.floor(Number(value) || 0));
|
|
90
|
+
|
|
91
|
+
const [unit, setUnit] = useState<DurationUnit>(
|
|
92
|
+
() => readSavedUnit(persistKey) ?? pickBestUnit(total)
|
|
93
|
+
);
|
|
94
|
+
// Texto digitado em andamento. Quando `null`, a exibição é derivada de `value` + `unit`
|
|
95
|
+
// (mantém-se sincronizado com mudanças externas sem precisar de efeito/setState).
|
|
96
|
+
const [editValue, setEditValue] = useState<string | null>(null);
|
|
97
|
+
|
|
98
|
+
const display = editValue ?? toDisplay(total, unit);
|
|
99
|
+
|
|
100
|
+
const handleInputChange = (raw: string) => {
|
|
101
|
+
setEditValue(raw);
|
|
102
|
+
const parsed = parseFloat(raw);
|
|
103
|
+
const seconds = Number.isFinite(parsed)
|
|
104
|
+
? Math.max(0, Math.round(parsed * UNIT_SECONDS[unit]))
|
|
105
|
+
: 0;
|
|
106
|
+
onChange(seconds);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleBlur = () => {
|
|
110
|
+
// Volta a exibir o valor canônico (reformatado) na unidade atual.
|
|
111
|
+
setEditValue(null);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleUnitChange = (nextUnit: DurationUnit) => {
|
|
115
|
+
// Troca de unidade é só exibição: mantém os segundos constantes, não dispara onChange.
|
|
116
|
+
setUnit(nextUnit);
|
|
117
|
+
setEditValue(null);
|
|
118
|
+
if (persistKey && typeof window !== 'undefined') {
|
|
119
|
+
window.localStorage.setItem(STORAGE_PREFIX + persistKey, nextUnit);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<InputGroup className={cn('bg-background', className)}>
|
|
125
|
+
<InputGroupInput
|
|
126
|
+
type="number"
|
|
127
|
+
inputMode="decimal"
|
|
128
|
+
min={0}
|
|
129
|
+
step="any"
|
|
130
|
+
disabled={disabled}
|
|
131
|
+
value={display}
|
|
132
|
+
onChange={(e) => handleInputChange(e.target.value)}
|
|
133
|
+
onBlur={handleBlur}
|
|
134
|
+
/>
|
|
135
|
+
<InputGroupAddon align="inline-end" className="pr-1">
|
|
136
|
+
<Select
|
|
137
|
+
value={unit}
|
|
138
|
+
onValueChange={(next) => handleUnitChange(next as DurationUnit)}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
>
|
|
141
|
+
<SelectTrigger
|
|
142
|
+
size="sm"
|
|
143
|
+
className="h-7 gap-1 border-0 bg-transparent px-2 shadow-none focus-visible:ring-0"
|
|
144
|
+
>
|
|
145
|
+
<SelectValue />
|
|
146
|
+
</SelectTrigger>
|
|
147
|
+
<SelectContent>
|
|
148
|
+
<SelectItem value="seconds">{t('durationUnitSeconds')}</SelectItem>
|
|
149
|
+
<SelectItem value="minutes">{t('durationUnitMinutes')}</SelectItem>
|
|
150
|
+
<SelectItem value="hours">{t('durationUnitHours')}</SelectItem>
|
|
151
|
+
<SelectItem value="days">{t('durationUnitDays')}</SelectItem>
|
|
152
|
+
</SelectContent>
|
|
153
|
+
</Select>
|
|
154
|
+
</InputGroupAddon>
|
|
155
|
+
</InputGroup>
|
|
156
|
+
);
|
|
157
|
+
}
|