mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Merge branch 'V2' into feature/sign-placement-ui
This commit is contained in:
commit
306117ed4b
74
README.md
74
README.md
@ -115,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
@ -4678,6 +4678,12 @@
|
||||
}
|
||||
},
|
||||
"apiKeys": {
|
||||
"intro": "Use your API key to programmatically access Stirling PDF's processing capabilities.",
|
||||
"docsTitle": "API Documentation",
|
||||
"docsDescription": "Learn more about integrating with Stirling PDF:",
|
||||
"docsLink": "API Documentation",
|
||||
"schemaLink": "API Schema Reference",
|
||||
"usage": "Include this key in the X-API-KEY header with all API requests.",
|
||||
"description": "Your API key for accessing Stirling's suite of PDF tools. Copy it to your project or refresh to generate a new one.",
|
||||
"publicKeyAriaLabel": "Public API key",
|
||||
"copyKeyAriaLabel": "Copy API key",
|
||||
|
||||
@ -64,17 +64,19 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
headerBorder: 'var(--modal-header-border)',
|
||||
}), []);
|
||||
|
||||
// Get isAdmin and runningEE from app config
|
||||
// Get isAdmin, runningEE, and loginEnabled from app config
|
||||
const isAdmin = config?.isAdmin ?? false;
|
||||
const runningEE = config?.runningEE ?? false;
|
||||
const loginEnabled = config?.enableLogin ?? false;
|
||||
|
||||
// Left navigation structure and icons
|
||||
const configNavSections = useMemo(() =>
|
||||
createConfigNavSections(
|
||||
isAdmin,
|
||||
runningEE
|
||||
runningEE,
|
||||
loginEnabled
|
||||
),
|
||||
[isAdmin, runningEE]
|
||||
[isAdmin, runningEE, loginEnabled]
|
||||
);
|
||||
|
||||
const activeLabel = useMemo(() => {
|
||||
|
||||
@ -41,7 +41,8 @@ export interface ConfigColors {
|
||||
|
||||
export const createConfigNavSections = (
|
||||
isAdmin: boolean = false,
|
||||
runningEE: boolean = false
|
||||
runningEE: boolean = false,
|
||||
_loginEnabled: boolean = false
|
||||
): ConfigNavSection[] => {
|
||||
const sections: ConfigNavSection[] = [
|
||||
{
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Group, Loader, Stack, Text } from '@mantine/core';
|
||||
import { useMemo, useRef, useEffect } from 'react';
|
||||
import { useMemo, useRef, useEffect, useState } from 'react';
|
||||
import type { PagePreview } from '@app/types/compare';
|
||||
import type { TokenBoundingBox, CompareDocumentPaneProps } from '@app/types/compare';
|
||||
import { mergeConnectedRects, normalizeRotation, groupWordRects, computePageLayoutMetrics } from '@app/components/tools/compare/compare';
|
||||
@ -53,6 +53,8 @@ const CompareDocumentPane = ({
|
||||
|
||||
// Track which page images have finished loading to avoid flashing between states
|
||||
const imageLoadedRef = useRef<Map<number, boolean>>(new Map());
|
||||
// Force a re-render when an image load state changes (refs don't trigger renders)
|
||||
const [, setImageLoadedTick] = useState(0);
|
||||
const visiblePageRafRef = useRef<number | null>(null);
|
||||
const lastReportedVisiblePageRef = useRef<number | null>(null);
|
||||
const pageNodesRef = useRef<HTMLElement[] | null>(null);
|
||||
@ -252,6 +254,7 @@ const CompareDocumentPane = ({
|
||||
onLoad={() => {
|
||||
if (!imageLoadedRef.current.get(page.pageNumber)) {
|
||||
imageLoadedRef.current.set(page.pageNumber, true);
|
||||
setImageLoadedTick((v) => v + 1); // refs don't trigger renders
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -279,6 +279,15 @@
|
||||
--modal-content-bg: #ffffff;
|
||||
--modal-header-border: rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* API Keys section colors (light mode) */
|
||||
--api-keys-card-bg: #ffffff;
|
||||
--api-keys-card-border: #e0e0e0;
|
||||
--api-keys-card-shadow: rgba(0, 0, 0, 0.06);
|
||||
--api-keys-input-bg: #f8f8f8;
|
||||
--api-keys-input-border: #e0e0e0;
|
||||
--api-keys-button-bg: #f5f5f5;
|
||||
--api-keys-button-color: #333333;
|
||||
|
||||
/* PDF Report Colors (always light) */
|
||||
--pdf-light-header-bg: 239 246 255;
|
||||
--pdf-light-accent: 59 130 246;
|
||||
@ -539,6 +548,15 @@
|
||||
--modal-content-bg: #2A2F36;
|
||||
--modal-header-border: rgba(255, 255, 255, 0.08);
|
||||
|
||||
/* API Keys section colors (dark mode) */
|
||||
--api-keys-card-bg: #2A2F36;
|
||||
--api-keys-card-border: #3A4047;
|
||||
--api-keys-card-shadow: none;
|
||||
--api-keys-input-bg: #1F2329;
|
||||
--api-keys-input-border: #3A4047;
|
||||
--api-keys-button-bg: #3A4047;
|
||||
--api-keys-button-color: #D0D6DC;
|
||||
|
||||
/* Code token colors (dark mode - Cursor-like) */
|
||||
--code-kw-color: #C792EA; /* purple */
|
||||
--code-str-color: #C3E88D; /* green */
|
||||
|
||||
@ -26,6 +26,57 @@ const Merge = (props: BaseToolProps) => {
|
||||
props,
|
||||
{ minFiles: 2 }
|
||||
);
|
||||
const naturalCompare = useCallback((a: string, b: string): number => {
|
||||
const isDigit = (char: string) => char >= '0' && char <= '9';
|
||||
|
||||
const getChunk = (s: string, length: number, marker: number): { chunk: string; newMarker: number } => {
|
||||
let chunk = '';
|
||||
const c = s.charAt(marker);
|
||||
chunk += c;
|
||||
marker++;
|
||||
|
||||
if (isDigit(c)) {
|
||||
while (marker < length && isDigit(s.charAt(marker))) {
|
||||
chunk += s.charAt(marker);
|
||||
marker++;
|
||||
}
|
||||
} else {
|
||||
while (marker < length && !isDigit(s.charAt(marker))) {
|
||||
chunk += s.charAt(marker);
|
||||
marker++;
|
||||
}
|
||||
}
|
||||
return { chunk, newMarker: marker };
|
||||
};
|
||||
|
||||
const len1 = a.length;
|
||||
const len2 = b.length;
|
||||
let marker1 = 0;
|
||||
let marker2 = 0;
|
||||
|
||||
while (marker1 < len1 && marker2 < len2) {
|
||||
const { chunk: chunk1, newMarker: newMarker1 } = getChunk(a, len1, marker1);
|
||||
marker1 = newMarker1;
|
||||
|
||||
const { chunk: chunk2, newMarker: newMarker2 } = getChunk(b, len2, marker2);
|
||||
marker2 = newMarker2;
|
||||
|
||||
let result: number;
|
||||
if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
|
||||
const num1 = parseInt(chunk1, 10);
|
||||
const num2 = parseInt(chunk2, 10);
|
||||
result = num1 - num2;
|
||||
} else {
|
||||
result = chunk1.localeCompare(chunk2);
|
||||
}
|
||||
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return len1 - len2;
|
||||
}, []);
|
||||
|
||||
// Custom file sorting logic for merge tool
|
||||
const sortFiles = useCallback((sortType: 'filename' | 'dateModified', ascending: boolean = true) => {
|
||||
@ -33,7 +84,7 @@ const Merge = (props: BaseToolProps) => {
|
||||
let comparison = 0;
|
||||
switch (sortType) {
|
||||
case 'filename':
|
||||
comparison = stubA.name.localeCompare(stubB.name);
|
||||
comparison = naturalCompare(stubA.name, stubB.name);
|
||||
break;
|
||||
case 'dateModified':
|
||||
comparison = stubA.lastModified - stubB.lastModified;
|
||||
@ -45,7 +96,7 @@ const Merge = (props: BaseToolProps) => {
|
||||
const selectedIds = sortedStubs.map(record => record.id);
|
||||
const deselectedIds = fileIds.filter(id => !selectedIds.includes(id));
|
||||
reorderFiles([...selectedIds, ...deselectedIds]);
|
||||
}, [selectedFileStubs, fileIds, reorderFiles]);
|
||||
}, [selectedFileStubs, fileIds, reorderFiles, naturalCompare]);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
|
||||
@ -2,13 +2,15 @@ import React from 'react';
|
||||
import { createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
|
||||
import PeopleSection from '@app/components/shared/config/configSections/PeopleSection';
|
||||
import TeamsSection from '@app/components/shared/config/configSections/TeamsSection';
|
||||
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
|
||||
|
||||
/**
|
||||
* Proprietary extension of createConfigNavSections that adds workspace sections
|
||||
*/
|
||||
export const createConfigNavSections = (
|
||||
isAdmin: boolean = false,
|
||||
runningEE: boolean = false
|
||||
runningEE: boolean = false,
|
||||
loginEnabled: boolean = false
|
||||
): ConfigNavSection[] => {
|
||||
// Get the core sections
|
||||
const sections = createCoreConfigNavSections(isAdmin, runningEE);
|
||||
@ -37,6 +39,25 @@ export const createConfigNavSections = (
|
||||
sections.splice(1, 0, workspaceSection);
|
||||
}
|
||||
|
||||
// Add Developer section if login is enabled
|
||||
if (loginEnabled) {
|
||||
const developerSection: ConfigNavSection = {
|
||||
title: 'Developer',
|
||||
items: [
|
||||
{
|
||||
key: 'api-keys',
|
||||
label: 'API Keys',
|
||||
icon: 'key-rounded',
|
||||
component: <ApiKeys />
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Add Developer section after Preferences (or Workspace if it exists)
|
||||
const insertIndex = isAdmin ? 2 : 1;
|
||||
sections.splice(insertIndex, 0, developerSection);
|
||||
}
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
import React, { useState } from "react";
|
||||
import { Anchor, Group, Stack, Text, Paper, Skeleton } from "@mantine/core";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import ApiKeySection from "./apiKeys/ApiKeySection";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import RefreshModal from "./apiKeys/RefreshModal";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import useApiKey from "./apiKeys/hooks/useApiKey";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
|
||||
export default function ApiKeys() {
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
const [showRefreshModal, setShowRefreshModal] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { apiKey, isLoading: apiKeyLoading, refresh, isRefreshing, error: apiKeyError, refetch } = useApiKey();
|
||||
|
||||
const copy = async (text: string, tag: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(tag);
|
||||
setTimeout(() => setCopied(null), 1600);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshKeys = async () => {
|
||||
try {
|
||||
await refresh();
|
||||
} finally {
|
||||
setShowRefreshModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={20} p={0}>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('config.apiKeys.intro', 'Use your API key to programmatically access Stirling PDF\'s processing capabilities.')}
|
||||
</Text>
|
||||
|
||||
<Paper
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
background: "var(--bg-muted)",
|
||||
border: "1px solid var(--border-subtle)"
|
||||
}}
|
||||
>
|
||||
<Group gap="xs" wrap="nowrap" align="flex-start">
|
||||
<LocalIcon icon="info-rounded" width={18} height={18} style={{ marginTop: 2, flexShrink: 0, opacity: 0.7 }} />
|
||||
<Stack gap={8} style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{t('config.apiKeys.docsTitle', 'API Documentation')}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('config.apiKeys.docsDescription', 'Learn more about integrating with Stirling PDF:')}
|
||||
</Text>
|
||||
<Stack gap={4}>
|
||||
<Text size="sm">
|
||||
<Anchor
|
||||
href="https://docs.stirlingpdf.com/API"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
{t('config.apiKeys.docsLink', 'API Documentation')}
|
||||
<LocalIcon icon="open-in-new-rounded" width={14} height={14} />
|
||||
</Anchor>
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<Anchor
|
||||
href="https://registry.scalar.com/@stirlingpdf/apis/stirling-pdf-processing-api/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
|
||||
>
|
||||
{t('config.apiKeys.schemaLink', 'API Schema Reference')}
|
||||
<LocalIcon icon="open-in-new-rounded" width={14} height={14} />
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{apiKeyError && (
|
||||
<Text size="sm" c="red.5">
|
||||
{t('config.apiKeys.generateError', "We couldn't generate your API key.")} {" "}
|
||||
<Anchor component="button" underline="always" onClick={refetch} c="red.4">
|
||||
{t('common.retry', 'Retry')}
|
||||
</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{apiKeyLoading ? (
|
||||
<div style={{ padding: 18, borderRadius: 12, background: "var(--api-keys-card-bg)", border: "1px solid var(--api-keys-card-border)", boxShadow: "0 2px 8px var(--api-keys-card-shadow)" }}>
|
||||
<Group align="center" gap={12} wrap="nowrap">
|
||||
<Skeleton height={36} style={{ flex: 1 }} />
|
||||
<Skeleton height={32} width={76} />
|
||||
<Skeleton height={32} width={92} />
|
||||
</Group>
|
||||
</div>
|
||||
) : (
|
||||
<ApiKeySection
|
||||
publicKey={apiKey ?? ""}
|
||||
copied={copied}
|
||||
onCopy={copy}
|
||||
onRefresh={() => setShowRefreshModal(true)}
|
||||
disabled={isRefreshing}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text size="sm" c="dimmed" style={{ marginTop: -8 }}>
|
||||
{t('config.apiKeys.usage', 'Include this key in the X-API-KEY header with all API requests.')}
|
||||
</Text>
|
||||
|
||||
<RefreshModal
|
||||
opened={showRefreshModal}
|
||||
onClose={() => setShowRefreshModal(false)}
|
||||
onConfirm={refreshKeys}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Paper,
|
||||
} from "@mantine/core";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
import FitText from "@app/components/shared/FitText";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ApiKeySectionProps {
|
||||
publicKey: string;
|
||||
copied: string | null;
|
||||
onCopy: (text: string, tag: string) => void;
|
||||
onRefresh: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ApiKeySection({
|
||||
publicKey,
|
||||
copied,
|
||||
onCopy,
|
||||
onRefresh,
|
||||
disabled,
|
||||
}: ApiKeySectionProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Paper radius="md" p={18} style={{ background: "var(--api-keys-card-bg)", border: "1px solid var(--api-keys-card-border)", boxShadow: "0 2px 8px var(--api-keys-card-shadow)" }}>
|
||||
<Group align="flex-end" wrap="nowrap">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Box
|
||||
style={{
|
||||
background: "var(--api-keys-input-bg)",
|
||||
border: "1px solid var(--api-keys-input-border)",
|
||||
borderRadius: 8,
|
||||
padding: "8px 12px",
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: 14,
|
||||
minHeight: 36,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
aria-label={t('config.apiKeys.publicKeyAriaLabel', 'Public API key')}
|
||||
>
|
||||
<FitText text={publicKey} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
onClick={() => onCopy(publicKey, "public")}
|
||||
leftSection={<LocalIcon icon="content-copy-rounded" width={14} height={14} />}
|
||||
styles={{ root: { background: "var(--api-keys-button-bg)", color: "var(--api-keys-button-color)", border: "none", marginLeft: 12 } }}
|
||||
aria-label={t('config.apiKeys.copyKeyAriaLabel', 'Copy API key')}
|
||||
>
|
||||
{copied === "public" ? t('common.copied', 'Copied!') : t('common.copy', 'Copy')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
onClick={onRefresh}
|
||||
leftSection={<LocalIcon icon="refresh-rounded" width={14} height={14} />}
|
||||
styles={{ root: { background: "var(--api-keys-button-bg)", color: "var(--api-keys-button-color)", border: "none", marginLeft: 8 } }}
|
||||
disabled={disabled}
|
||||
aria-label={t('config.apiKeys.refreshAriaLabel', 'Refresh API key')}
|
||||
>
|
||||
{t('common.refresh', 'Refresh')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
Group,
|
||||
Button,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex";
|
||||
|
||||
interface RefreshModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export default function RefreshModal({ opened, onClose, onConfirm }: RefreshModalProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={t('config.apiKeys.refreshModal.title', 'Refresh API Keys')}
|
||||
centered
|
||||
size="sm"
|
||||
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="red">
|
||||
{t('config.apiKeys.refreshModal.warning', '⚠️ Warning: This action will generate new API keys and make your previous keys invalid.')}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
{t('config.apiKeys.refreshModal.impact', 'Any applications or services currently using these keys will stop working until you update them with the new keys.')}
|
||||
</Text>
|
||||
<Text size="sm" fw={500}>
|
||||
{t('config.apiKeys.refreshModal.confirmPrompt', 'Are you sure you want to continue?')}
|
||||
</Text>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={onClose}>
|
||||
{t('common.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button color="red" onClick={onConfirm}>
|
||||
{t('config.apiKeys.refreshModal.confirmCta', 'Refresh Keys')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import apiClient from "@app/services/apiClient";
|
||||
|
||||
export function useApiKey() {
|
||||
const [apiKey, setApiKey] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [hasAttempted, setHasAttempted] = useState<boolean>(false);
|
||||
|
||||
const fetchKey = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
// Backend is POST for get and update
|
||||
const res = await apiClient.post("/api/v1/user/get-api-key");
|
||||
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
||||
if (typeof value === "string") setApiKey(value);
|
||||
} catch (e: any) {
|
||||
// If not found, try to create one by calling update endpoint
|
||||
if (e?.response?.status === 404) {
|
||||
try {
|
||||
const createRes = await apiClient.post("/api/v1/user/update-api-key");
|
||||
const created =
|
||||
typeof createRes.data === "string"
|
||||
? createRes.data
|
||||
: createRes.data?.apiKey;
|
||||
if (typeof created === "string") setApiKey(created);
|
||||
} catch (createErr: any) {
|
||||
setError(createErr);
|
||||
}
|
||||
} else {
|
||||
setError(e);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setHasAttempted(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await apiClient.post("/api/v1/user/update-api-key");
|
||||
const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
|
||||
if (typeof value === "string") setApiKey(value);
|
||||
} catch (e: any) {
|
||||
setError(e);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasAttempted) {
|
||||
fetchKey();
|
||||
}
|
||||
}, [hasAttempted, fetchKey]);
|
||||
|
||||
return { apiKey, isLoading, isRefreshing, error, refetch: fetchKey, refresh, hasAttempted } as const;
|
||||
}
|
||||
|
||||
export default useApiKey;
|
||||
@ -242,7 +242,6 @@ ignore = [
|
||||
'team.status',
|
||||
'text',
|
||||
'update.version',
|
||||
'validateSignature.cert.bits',
|
||||
'validateSignature.cert.version',
|
||||
'validateSignature.status',
|
||||
'watermark.type.1',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user