intermittent commit

This commit is contained in:
2026-07-14 13:22:11 +02:00
parent b8b395e8b5
commit f3210234d3
23 changed files with 2027 additions and 555 deletions

View File

@@ -150,7 +150,7 @@ export type FileConnectorDiscoveryResponse = {
export type FileConnectorProfilePayload = {
id: string;
label: string;
provider: "seafile" | "nextcloud" | "webdav" | "smb" | "nfs" | "dms" | "generic";
provider: "seafile" | "nextcloud" | "webdav" | "smb" | "s3" | "sharepoint" | "onedrive" | "nfs" | "dms" | "generic";
endpoint_url?: string | null;
base_path?: string | null;
enabled?: boolean;
@@ -172,7 +172,7 @@ export type FileConnectorProfileUpdatePayload = Partial<Omit<FileConnectorProfil
export type FileConnectorCredentialPayload = {
id: string;
label: string;
provider?: "seafile" | "nextcloud" | "webdav" | "smb" | "nfs" | "dms" | "generic" | null;
provider?: "seafile" | "nextcloud" | "webdav" | "smb" | "s3" | "sharepoint" | "onedrive" | "nfs" | "dms" | "generic" | null;
enabled?: boolean;
scope_type?: "system" | "tenant" | "user" | "group" | "campaign";
scope_id?: string | null;
@@ -206,6 +206,8 @@ export type FileConnectorBrowseResponse = {
path: string;
library_id?: string | null;
read_only: boolean;
next_continuation_token?: string | null;
has_more: boolean;
decision: FileConnectorPolicyDecision;
items: FileConnectorBrowseItem[];
};
@@ -808,11 +810,12 @@ export function deactivateFileConnectorProfile(settings: ApiSettings, profileId:
export function browseFileConnectorProfile(
settings: ApiSettings,
profileId: string,
params: {path?: string;library_id?: string;campaign_id?: string;} = {})
params: {path?: string;library_id?: string;continuation_token?: string;campaign_id?: string;} = {})
: Promise<FileConnectorBrowseResponse> {
const search = new URLSearchParams();
if (params.path) search.set("path", params.path);
if (params.library_id) search.set("library_id", params.library_id);
if (params.continuation_token) search.set("continuation_token", params.continuation_token);
if (params.campaign_id) search.set("campaign_id", params.campaign_id);
const suffix = search.toString() ? `?${search.toString()}` : "";
return apiFetch<FileConnectorBrowseResponse>(settings, `/api/v1/files/connectors/profiles/${encodeURIComponent(profileId)}/browse${suffix}`);

View File

@@ -5,6 +5,8 @@ import {
Card,
ConnectionTree,
ConfirmDialog,
CredentialPanel,
DisabledActionTooltip,
DismissibleAlert,
Dialog,
FormField,
@@ -123,6 +125,9 @@ const PROVIDERS: Array<{id: Provider;label: string;}> = [
{ id: "nextcloud", label: "i18n:govoplan-files.nextcloud.aab73a3d" },
{ id: "seafile", label: "i18n:govoplan-files.seafile.600192cc" },
{ id: "smb", label: "i18n:govoplan-files.smb.515d2f88" },
{ id: "s3", label: "S3" },
{ id: "sharepoint", label: "SharePoint" },
{ id: "onedrive", label: "OneDrive" },
{ id: "nfs", label: "i18n:govoplan-files.nfs.db121865" },
{ id: "dms", label: "i18n:govoplan-files.dms.477e5652" },
{ id: "generic", label: "i18n:govoplan-files.generic.ff7613e5" }];
@@ -133,6 +138,9 @@ const ENDPOINT_PLACEHOLDERS: Record<Provider, string> = {
nextcloud: "http://127.0.0.1:9081/remote.php/dav/files/admin/",
seafile: "http://127.0.0.1:9082/",
smb: "smb://127.0.0.1:1445/files",
s3: "http://127.0.0.1:9000",
sharepoint: "https://graph.microsoft.com/v1.0/sites/{site-id}/drives/{drive-id}",
onedrive: "https://graph.microsoft.com/v1.0/drives/{drive-id}",
nfs: "nfs://127.0.0.1/export",
dms: "https://dms.example.test",
generic: "https://files.example.test"
@@ -340,6 +348,17 @@ export default function FileConnectorSettingsPanel({
setCredentialDraft((current) => current ? { ...current, ...patch } : current);
}
function patchCredentialValues(patch: {username?: string | null;password?: string | null;}) {
const next: Partial<ConnectorCredentialDraft> = {};
if (patch.username !== undefined) next.username = String(patch.username ?? "");
if (patch.password !== undefined) next.password = String(patch.password ?? "");
patchCredentialDraft(next);
}
function patchCredentialToken(patch: {password?: string | null;}) {
if (patch.password !== undefined) patchCredentialDraft({ token: String(patch.password ?? "") });
}
function patchPolicyDraft(patch: Partial<CentralConnectorPolicyDraft>) {
setPolicyDraft((current) => ({ ...current, ...patch }));
}
@@ -906,11 +925,11 @@ export default function FileConnectorSettingsPanel({
footer={credentialDraft ?
<>
<Button onClick={closeCredentialDialog} disabled={saving}>i18n:govoplan-files.cancel.77dfd213</Button>
<span className="disabled-action-tooltip" data-tooltip={credentialSaveTooltip}>
<DisabledActionTooltip reason={credentialSaveTooltip}>
<Button variant="primary" onClick={() => void saveCredentialDraft()} disabled={credentialSaveDisabled}>
<Save size={16} aria-hidden="true" /> {saving ? "i18n:govoplan-files.saving.ae7e8875" : "i18n:govoplan-files.save_credential.2c02a6d2"}
</Button>
</span>
</DisabledActionTooltip>
</> :
null}>
@@ -983,35 +1002,47 @@ export default function FileConnectorSettingsPanel({
actor: "Connector administrator",
target: "Credential mode"
}} /> :
<div className="form-grid two-column-form-grid">
{(credentialDraft.credentialMode === "basic" || credentialDraft.credentialMode === "secret_ref") &&
<FormField label="i18n:govoplan-files.username.84c29015">
<input value={credentialDraft.username} disabled={saving} onChange={(event) => patchCredentialDraft({ username: event.target.value })} autoComplete="username" />
</FormField>
}
<div className="file-connector-secret-fields">
{credentialDraft.credentialMode === "basic" &&
<FormField label="i18n:govoplan-files.password.8be3c943">
<input value={credentialDraft.password} disabled={saving} onChange={(event) => patchCredentialDraft({ password: event.target.value })} type="password" autoComplete="new-password" placeholder={editingCredentialId ? "i18n:govoplan-files.leave_empty_to_keep_saved_password.6ec39f5e" : ""} />
</FormField>
}
{credentialDraft.credentialMode === "token" &&
<FormField label="i18n:govoplan-files.token.a1141eb9">
<input value={credentialDraft.token} disabled={saving} onChange={(event) => patchCredentialDraft({ token: event.target.value })} type="password" placeholder={editingCredentialId ? "i18n:govoplan-files.leave_empty_to_keep_saved_token.e725857b" : ""} />
</FormField>
<CredentialPanel
values={{ username: credentialDraft.username, password: credentialDraft.password }}
onChange={patchCredentialValues}
disabled={saving}
savedPassword={Boolean(editingCredentialId) && !credentialDraft.clearPassword}
savedPasswordPlaceholder="i18n:govoplan-files.leave_empty_to_keep_saved_password.6ec39f5e" />
}
{credentialDraft.credentialMode === "secret_ref" &&
<FormField label="i18n:govoplan-files.secret_reference.04ed2221">
<input value={credentialDraft.secretRef} disabled={saving} onChange={(event) => patchCredentialDraft({ secretRef: event.target.value })} />
</FormField>
<>
<CredentialPanel
values={{ username: credentialDraft.username }}
onChange={patchCredentialValues}
disabled={saving}
showPassword={false} />
<div className="form-grid two-column-form-grid">
<FormField label="i18n:govoplan-files.secret_reference.04ed2221">
<input value={credentialDraft.secretRef} disabled={saving} onChange={(event) => patchCredentialDraft({ secretRef: event.target.value })} />
</FormField>
</div>
</>
}
{credentialDraft.credentialMode === "token" &&
<CredentialPanel
values={{ password: credentialDraft.token }}
onChange={patchCredentialToken}
disabled={saving}
showUsername={false}
passwordLabel="i18n:govoplan-files.token.a1141eb9"
savedPassword={Boolean(editingCredentialId) && !credentialDraft.clearToken}
savedPasswordPlaceholder="i18n:govoplan-files.leave_empty_to_keep_saved_token.e725857b" />
}
</div>
}
<div className="button-row compact-actions">
<span className="disabled-action-tooltip" data-tooltip={credentialTestDisabled ? credentialTestTooltip : ""}>
<DisabledActionTooltip reason={credentialTestDisabled ? credentialTestTooltip : ""}>
<Button type="button" onClick={() => void testCredentialLogin()} disabled={credentialTestDisabled}>
<RefreshCw size={16} aria-hidden="true" /> {testingCredentialLogin ? "Testing login" : "Test login"}
</Button>
</span>
</DisabledActionTooltip>
</div>
{credentialLoginResult &&
<DismissibleAlert tone={credentialLoginResult.ok ? "success" : "warning"} resetKey={credentialLoginResult.message}>
@@ -1072,11 +1103,11 @@ export default function FileConnectorSettingsPanel({
footer={draft ?
<>
<Button onClick={closeProfileDialog} disabled={saving}>i18n:govoplan-files.cancel.77dfd213</Button>
<span className="disabled-action-tooltip" data-tooltip={profileSaveTooltip}>
<DisabledActionTooltip reason={profileSaveTooltip}>
<Button variant="primary" onClick={() => void saveDraft()} disabled={profileSaveDisabled}>
<Save size={16} aria-hidden="true" /> {saving ? "i18n:govoplan-files.saving.ae7e8875" : "i18n:govoplan-files.save_connection.07796d38"}
</Button>
</span>
</DisabledActionTooltip>
</> :
null}>

View File

@@ -182,6 +182,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
}, [visibleFiles, folders, currentFolder, searchActive, sortColumn, sortDirection]);
const fileListViewportRef = useRef<HTMLDivElement | null>(null);
const fileListMeasureRowRef = useRef<HTMLDivElement | null>(null);
const managedSpaceLoadsInFlightRef = useRef<Set<string>>(new Set());
const [fileListViewportHeight, setFileListViewportHeight] = useState(0);
const [fileListScrollTop, setFileListScrollTop] = useState(0);
const [fileListRowHeight, setFileListRowHeight] = useState(DEFAULT_FILE_LIST_ROW_HEIGHT);
@@ -291,8 +292,9 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
try {
const response = await listFileSpaces(settings);
setSpaces(response.spaces);
setActiveSpaceId((current) => current || response.spaces[0]?.id || "");
await Promise.all(response.spaces.filter((space) => !isConnectorSpace(space)).map((space) => loadSpaceContents(space, { silent: true })));
const nextActiveSpace = response.spaces.find((space) => space.id === activeSpaceId) ?? response.spaces[0] ?? null;
setActiveSpaceId(nextActiveSpace?.id || "");
if (nextActiveSpace && !isConnectorSpace(nextActiveSpace)) await loadSpaceContents(nextActiveSpace, { silent: true });
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
@@ -331,6 +333,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
await loadConnectorSpaceContents(space, { silent: options.silent, folderPath: "" });
return;
}
if (managedSpaceLoadsInFlightRef.current.has(space.id)) return;
managedSpaceLoadsInFlightRef.current.add(space.id);
if (!options.silent) {
setBusy(true);
setError("");
@@ -372,6 +376,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
managedSpaceLoadsInFlightRef.current.delete(space.id);
if (!options.silent) setBusy(false);
}
}
@@ -411,6 +416,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
const nextSpace = spaces.find((space) => space.id === activeSpaceId);
if (nextSpace && isConnectorSpace(nextSpace)) {
void loadConnectorSpaceContents(nextSpace, { folderPath: "", silent: true });
} else if (nextSpace && filesBySpace[nextSpace.id] === undefined && foldersBySpace[nextSpace.id] === undefined) {
void loadSpaceContents(nextSpace, { silent: true });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeSpaceId, spaces]);

View File

@@ -14,7 +14,7 @@
gap: 8px;
flex-wrap: wrap;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
background: var(--panel-header);
}
@@ -29,7 +29,7 @@
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
min-height: 0;
height: 100%;
border: 1px solid var(--line);
border: var(--border-line);
border-radius: 0;
overflow: hidden;
background: var(--panel);
@@ -46,7 +46,7 @@
.file-list-sticky {
flex: 0 0 auto;
z-index: 2;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
background: var(--panel-soft);
}
@@ -97,7 +97,7 @@
gap: 12px;
flex-wrap: wrap;
padding: 8px 14px;
border-top: 1px solid var(--line);
border-top: var(--border-line);
color: var(--muted);
font-size: 12px;
}
@@ -130,7 +130,7 @@
.file-list-table-head {
padding: 10px 14px;
border-top: 1px solid var(--line);
border-top: var(--border-line);
background: var(--panel);
color: var(--muted);
font-size: 12px;
@@ -161,7 +161,7 @@
.file-list-row {
min-height: 58px;
padding: 9px 14px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
cursor: default;
user-select: none;
}
@@ -244,7 +244,7 @@
}
.folder-row .file-row-icon {
color: #b6791d;
color: var(--warning-deep);
}
.file-row-tail {
@@ -274,7 +274,7 @@
display: grid;
min-width: 190px;
overflow: hidden;
border: 1px solid var(--line-dark);
border: var(--border-line-dark);
border-radius: 12px;
background: var(--panel);
box-shadow: var(--shadow-popover);
@@ -299,7 +299,7 @@
.file-context-menu button:hover,
.file-context-menu button:focus-visible {
background: rgba(13, 110, 253, .08);
background: var(--primary-soft);
outline: none;
}
@@ -315,14 +315,14 @@
display: grid;
place-items: center;
padding: 22px;
background: rgba(28, 25, 22, .38);
background: var(--dialog-backdrop);
}
.file-dialog {
width: min(620px, 100%);
max-height: min(720px, calc(100vh - 44px));
overflow: auto;
border: 1px solid var(--line-dark);
border: var(--border-line-dark);
border-radius: 16px;
background: var(--panel);
box-shadow: var(--shadow-popover);
@@ -334,7 +334,7 @@
justify-content: space-between;
gap: 12px;
padding: 15px 18px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
background: var(--panel-soft);
}
@@ -397,7 +397,7 @@
overflow-y: auto;
scrollbar-gutter: stable;
padding: 8px;
border: 1px solid var(--line);
border: var(--border-line);
border-radius: 12px;
background: var(--panel-soft);
}
@@ -428,13 +428,13 @@
.file-folder-selector-node:hover:not(:disabled),
.file-folder-selector-node:focus-visible,
.file-folder-selector-node.is-selected {
background: rgba(13, 110, 253, .09);
background: var(--primary-soft);
outline: none;
}
.file-folder-selector-node.is-selected {
color: var(--text-strong);
box-shadow: inset 0 0 0 1px rgba(13, 110, 253, .25);
box-shadow: var(--primary-inset-ring);
}
.file-folder-selector-empty {
@@ -462,7 +462,7 @@
grid-template-rows: auto auto minmax(0, 1fr);
min-height: 320px;
overflow: hidden;
border: 1px solid var(--line);
border: var(--border-line);
border-radius: 12px;
background: var(--panel);
}
@@ -473,7 +473,7 @@
gap: 10px;
align-items: center;
padding: 10px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
background: var(--panel-soft);
}
@@ -577,7 +577,7 @@
gap: 3px;
min-width: 0;
padding: 10px 12px;
border: 1px solid var(--line);
border: var(--border-line);
border-radius: 10px;
background: var(--panel-soft);
}
@@ -625,7 +625,7 @@
gap: 12px;
align-items: center;
padding: 10px 12px;
border: 1px solid var(--line);
border: var(--border-line);
border-radius: 8px;
background: var(--panel-soft);
}
@@ -698,6 +698,11 @@
white-space: nowrap;
}
.file-connector-secret-fields {
display: grid;
gap: 18px;
}
@media (max-width: 760px) {
.file-connector-profile-row {
grid-template-columns: 1fr;
@@ -726,7 +731,7 @@
.file-tree-panel {
max-height: 260px;
border-right: 0;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
}
.file-list-table-head {
@@ -760,7 +765,7 @@
z-index: 35;
display: grid;
place-items: center;
background: rgba(255, 255, 255, .12);
background: var(--panel-glass);
backdrop-filter: blur(1px);
}
@@ -768,7 +773,7 @@
display: grid;
gap: 3px;
padding: 12px 14px;
border: 1px solid var(--line);
border: var(--border-line);
border-radius: 12px;
background: var(--panel-soft);
}
@@ -791,7 +796,7 @@
gap: 10px;
align-items: center;
padding: 10px;
border: 1px solid var(--line);
border: var(--border-line);
border-radius: 12px;
background: var(--panel-soft);
}
@@ -842,8 +847,8 @@
.rename-preview-more:hover,
.rename-preview-more:focus-visible {
border-color: rgba(13, 110, 253, .45);
background: rgba(13, 110, 253, .08);
border-color: var(--primary-border);
background: var(--primary-soft);
color: var(--text-strong);
outline: none;
}
@@ -860,7 +865,7 @@
.managed-file-chooser-dialog > .dialog-header {
padding: 16px 18px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
background: var(--panel);
}
@@ -891,9 +896,9 @@
.managed-file-chooser-footer {
flex: 0 0 auto;
padding: 12px 18px;
border-top: 1px solid var(--line);
border-top: var(--border-line);
background: var(--panel);
box-shadow: 0 -8px 24px rgba(15, 23, 42, .06);
box-shadow: var(--shadow-footer);
}
.managed-file-chooser-layout {
@@ -911,7 +916,7 @@
.managed-file-chooser-spaces {
min-width: 0;
padding: 16px;
border-right: 1px solid var(--line);
border-right: var(--border-line);
background: var(--panel-soft);
overflow: auto;
}
@@ -1014,7 +1019,7 @@
gap: 12px;
align-items: center;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
}
.managed-file-breadcrumb {
@@ -1046,7 +1051,7 @@
display: grid;
gap: 8px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
background: var(--panel-soft);
}
@@ -1075,7 +1080,7 @@
align-items: center;
min-height: 38px;
padding: 0 20px;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
background: var(--panel-soft);
color: var(--muted);
font-size: 12px;
@@ -1206,7 +1211,7 @@
.managed-file-chooser-note {
margin: 0;
padding: 10px 14px;
border-top: 1px solid var(--line);
border-top: var(--border-line);
background: var(--panel-soft);
}
@@ -1313,7 +1318,7 @@
.managed-file-chooser-spaces {
border-right: 0;
border-bottom: 1px solid var(--line);
border-bottom: var(--border-line);
max-height: 160px;
}