Migrate Files surfaces to interface patterns

This commit is contained in:
2026-08-03 09:35:52 +02:00
parent 6baf2a421b
commit d8ae506ff8
9 changed files with 229 additions and 54 deletions
@@ -6,9 +6,9 @@ import {
ConnectionTree,
ConfirmDialog,
CredentialPanel,
DisabledActionTooltip,
DismissibleAlert,
Dialog,
DocumentationHelpLink,
FormField,
ActionBlockerHint,
AdvancedOptionsPanel,
@@ -149,6 +149,16 @@ const ENDPOINT_PLACEHOLDERS: Record<Provider, string> = {
generic: "https://files.example.test"
};
const CONNECTOR_DOCUMENTATION = {
topicId: "files.governed-connectors-and-provenance",
documentationType: "admin"
} as const;
const CONNECTOR_RECOVERY_DOCUMENTATION = {
topicId: "files.reference.integrity-recovery-and-fail-closed-transports",
documentationType: "admin"
} as const;
export default function FileConnectorSettingsPanel({
settings,
scopeType,
@@ -796,10 +806,14 @@ export default function FileConnectorSettingsPanel({
const credentialSaveDisabled = !credentialDraft || saving || credentialMissingRequired;
const profileSaveTooltip = requiredFieldsTooltip(profileMissingFields);
const credentialSaveTooltip = requiredFieldsTooltip(credentialMissingFields);
const profileSaveDisabledReason = saving ? "Wait for the current connector change to finish." : profileSaveTooltip;
const credentialSaveDisabledReason = saving ? "Wait for the current credential change to finish." : credentialSaveTooltip;
const canDiscoverProfileEndpoint = Boolean(draft && (draft.provider === "webdav" || draft.provider === "nextcloud"));
const credentialTestUnsupported = Boolean(credentialTestProfile && credentialTestProfile.provider !== "webdav" && credentialTestProfile.provider !== "nextcloud");
const credentialTestDisabled = !credentialDraft || saving || testingCredentialLogin || !credentialTestProfile || credentialTestUnsupported;
const credentialTestTooltip = credentialTestHelpText(credentialTestProfile, credentialTestUnsupported);
const credentialTestTooltip = saving ? "Wait for the current connector change to finish." :
testingCredentialLogin ? "The credential test is already running." :
credentialTestHelpText(credentialTestProfile, credentialTestUnsupported);
const profileCredentialOptions = draft ? credentialOptionsForProfileDraft(selectableCredentials, draft) : [];
const profileCredentialSelectValue = draft ? credentialProfileIdForDraft(draft, selectableCredentials) : "";
const connectorColumns: ConnectionTreeColumn<ConnectorTreeRow>[] = [
@@ -846,18 +860,23 @@ export default function FileConnectorSettingsPanel({
function renderConnectorActions(row: ConnectorTreeRow) {
if (row.kind === "credential") {
const readOnly = row.credential.source_kind !== "database";
const mutationBlocker = connectorMutationBlocker(canWrite, readOnly, saving);
return <TableActionGroup actions={[
{ id: "edit", label: i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.credential.label }), icon: <Edit3 size={15} />, disabled: !canWrite || readOnly || saving, onClick: () => startCredentialEdit(row.credential) },
{ id: "disable", label: i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.credential.label }), icon: <Trash2 size={15} />, variant: "danger", applicable: row.credential.enabled, disabled: !canWrite || readOnly || saving, onClick: () => setPendingCredentialDeactivate(row.credential) }
{ id: "edit", label: i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.credential.label }), icon: <Edit3 size={15} />, disabled: Boolean(mutationBlocker), disabledReason: mutationBlocker, onClick: () => startCredentialEdit(row.credential) },
{ id: "disable", label: i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.credential.label }), icon: <Trash2 size={15} />, variant: "danger", disabled: Boolean(mutationBlocker) || !row.credential.enabled, disabledReason: mutationBlocker || (!row.credential.enabled ? "This credential is already disabled." : ""), onClick: () => setPendingCredentialDeactivate(row.credential) }
]} />;
}
const readOnly = row.profile.source_kind !== "database";
const mutationBlocker = connectorMutationBlocker(canWrite, readOnly, saving);
const testBlocker = !row.profile.enabled ? "Enable this connection before testing it." :
saving ? "Wait for the current connector change to finish." :
testingProfileId === row.profile.id ? "This connection test is already running." : "";
return <TableActionGroup actions={[
{ id: "test", label: i18nMessage("i18n:govoplan-files.test_value.6a5d10a5", { value0: row.profile.label }), icon: <RefreshCw size={15} />, applicable: row.profile.enabled, disabled: saving || testingProfileId === row.profile.id, onClick: () => void testBrowse(row.profile) },
{ id: "add-credential", label: i18nMessage("i18n:govoplan-files.add_credential_for_value.0fa9c1fe", { value0: row.profile.label }), icon: <UserRoundKey size={15} />, variant: "primary", disabled: !canWrite || saving, onClick: () => startCredentialCreate(row.profile) },
{ id: "edit", label: i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.profile.label }), icon: <Edit3 size={15} />, disabled: !canWrite || readOnly || saving, onClick: () => startEdit(row.profile) },
{ id: "disable", label: i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.profile.label }), icon: <Trash2 size={15} />, variant: "danger", applicable: row.profile.enabled, disabled: !canWrite || readOnly || saving, onClick: () => setPendingDeactivate(row.profile) }
{ id: "test", label: i18nMessage("i18n:govoplan-files.test_value.6a5d10a5", { value0: row.profile.label }), icon: <RefreshCw size={15} />, disabled: Boolean(testBlocker), disabledReason: testBlocker, onClick: () => void testBrowse(row.profile) },
{ id: "add-credential", label: i18nMessage("i18n:govoplan-files.add_credential_for_value.0fa9c1fe", { value0: row.profile.label }), icon: <UserRoundKey size={15} />, variant: "primary", disabled: Boolean(mutationBlocker), disabledReason: mutationBlocker, onClick: () => startCredentialCreate(row.profile) },
{ id: "edit", label: i18nMessage("i18n:govoplan-files.edit_value.fad75899", { value0: row.profile.label }), icon: <Edit3 size={15} />, disabled: Boolean(mutationBlocker), disabledReason: mutationBlocker, onClick: () => startEdit(row.profile) },
{ id: "disable", label: i18nMessage("i18n:govoplan-files.disable_value.09485f7f", { value0: row.profile.label }), icon: <Trash2 size={15} />, variant: "danger", disabled: Boolean(mutationBlocker) || !row.profile.enabled, disabledReason: mutationBlocker || (!row.profile.enabled ? "This connection is already disabled." : ""), onClick: () => setPendingDeactivate(row.profile) }
]} />;
}
@@ -876,7 +895,8 @@ export default function FileConnectorSettingsPanel({
requiredAction: "Ask an administrator to grant the required permission or make the change at a higher scope.",
actor: "System or tenant administrator",
target: "Administration > GLOBAL or TENANT > File Connectors"
}} />
}}
documentation={CONNECTOR_DOCUMENTATION} />
}
{targetSelectionRequired && !hasSelectableTarget &&
@@ -888,7 +908,8 @@ export default function FileConnectorSettingsPanel({
requiredAction: "Create or select a user or group first.",
actor: "Access administrator",
target: targetPluralLabel(scopeType, targetLabel)
}} />
}}
documentation={CONNECTOR_DOCUMENTATION} />
}
{targetSelectionRequired &&
@@ -911,10 +932,11 @@ export default function FileConnectorSettingsPanel({
actions={
canWrite ?
<div className="button-row compact-actions">
<Button onClick={() => void loadProfiles()} disabled={loading}>{loading ? "i18n:govoplan-files.loading.b04ba49f" : "i18n:govoplan-files.reload.cce71553"}</Button>
<Button variant="primary" onClick={startCreate} disabled={saving}><Plus size={16} aria-hidden="true" /> i18n:govoplan-files.new_connection.ac979fe4</Button>
<DocumentationHelpLink reference={CONNECTOR_DOCUMENTATION} />
<Button onClick={() => void loadProfiles()} disabled={loading || saving} disabledReason={loading ? "File connections are already loading." : saving ? "Wait for the current connector change to finish." : undefined}>{loading ? "i18n:govoplan-files.loading.b04ba49f" : "i18n:govoplan-files.reload.cce71553"}</Button>
<Button variant="primary" onClick={startCreate} disabled={saving} disabledReason={saving ? "Wait for the current connector change to finish." : undefined}><Plus size={16} aria-hidden="true" /> i18n:govoplan-files.new_connection.ac979fe4</Button>
</div> :
null
<DocumentationHelpLink reference={CONNECTOR_DOCUMENTATION} />
}>
<LoadingFrame loading={loading} label="i18n:govoplan-files.loading_file_connections.bd68f224">
@@ -931,10 +953,13 @@ export default function FileConnectorSettingsPanel({
<Card
title={i18nMessage("i18n:govoplan-files.value_connector_policy.0bf7f53b", { value0: scopeLabel(scopeType) })}
actions={canWrite ?
<Button variant="primary" onClick={() => void savePolicyDraft()} disabled={saving || loading}>
<ShieldCheck size={16} aria-hidden="true" /> {saving ? "i18n:govoplan-files.saving.ae7e8875" : "i18n:govoplan-files.save_policy.77d67ce3"}
</Button> :
null}>
<div className="button-row compact-actions">
<DocumentationHelpLink reference={CONNECTOR_DOCUMENTATION} />
<Button variant="primary" onClick={() => void savePolicyDraft()} disabled={saving || loading} disabledReason={saving ? "Wait for the current connector change to finish." : loading ? "Wait until the effective connector policy has loaded." : undefined}>
<ShieldCheck size={16} aria-hidden="true" /> {saving ? "i18n:govoplan-files.saving.ae7e8875" : "i18n:govoplan-files.save_policy.77d67ce3"}
</Button>
</div> :
<DocumentationHelpLink reference={CONNECTOR_DOCUMENTATION} />}>
<LoadingFrame loading={loading} label="i18n:govoplan-files.loading_connector_policy.f12ab285">
<>
@@ -1060,11 +1085,9 @@ export default function FileConnectorSettingsPanel({
footer={credentialDraft ?
<>
<Button onClick={closeCredentialDialog} disabled={saving}>i18n:govoplan-files.cancel.77dfd213</Button>
<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>
</DisabledActionTooltip>
<Button variant="primary" onClick={() => void saveCredentialDraft()} disabled={credentialSaveDisabled} disabledReason={credentialSaveDisabledReason}>
<Save size={16} aria-hidden="true" /> {saving ? "i18n:govoplan-files.saving.ae7e8875" : "i18n:govoplan-files.save_credential.2c02a6d2"}
</Button>
</> :
null}>
@@ -1105,7 +1128,7 @@ export default function FileConnectorSettingsPanel({
/>
</FormField>
</div>
<FormField label="i18n:govoplan-files.credential_mode.23fdd899">
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION}>
<select value={credentialDraft.credentialMode} disabled={saving} onChange={(event) => patchCredentialDraft({ credentialMode: event.target.value as CredentialMode })}>
<option value="none">i18n:govoplan-files.none.6eef6648</option>
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
@@ -1173,11 +1196,9 @@ export default function FileConnectorSettingsPanel({
</div>
}
<div className="button-row compact-actions">
<DisabledActionTooltip reason={credentialTestDisabled ? credentialTestTooltip : ""}>
<Button type="button" onClick={() => void testCredentialLogin()} disabled={credentialTestDisabled}>
<RefreshCw size={16} aria-hidden="true" /> {testingCredentialLogin ? "Testing login" : "Test login"}
</Button>
</DisabledActionTooltip>
<Button type="button" onClick={() => void testCredentialLogin()} disabled={credentialTestDisabled} disabledReason={credentialTestTooltip}>
<RefreshCw size={16} aria-hidden="true" /> {testingCredentialLogin ? "Testing login" : "Test login"}
</Button>
</div>
{credentialLoginResult &&
<DismissibleAlert tone={credentialLoginResult.ok ? "success" : "warning"} resetKey={credentialLoginResult.message}>
@@ -1192,7 +1213,7 @@ export default function FileConnectorSettingsPanel({
<p>Limit where lower levels may use this credential.</p>
</header>
<div className="form-grid two">
<FormField label="i18n:govoplan-files.policy.bb9cf141">
<FormField label="i18n:govoplan-files.policy.bb9cf141" documentation={CONNECTOR_DOCUMENTATION}>
<select value={credentialDraft.policyMode} disabled={saving} onChange={(event) => patchCredentialDraft({ policyMode: event.target.value as PolicyMode })}>
<option value="allow-provider">i18n:govoplan-files.can_use_this_credential.f4a41971</option>
<option value="allowed-paths">i18n:govoplan-files.must_stay_in_allowed_paths.dc5b4eb4</option>
@@ -1209,7 +1230,7 @@ export default function FileConnectorSettingsPanel({
</FormField>
</div>
<AdvancedOptionsPanel title="Credential metadata JSON" summary="For diagnostics or provider-specific compatibility flags. Normal setup should not need this.">
<FormField label="i18n:govoplan-files.metadata_json.b0e4c283">
<FormField label="i18n:govoplan-files.metadata_json.b0e4c283" documentation={CONNECTOR_RECOVERY_DOCUMENTATION}>
<textarea value={credentialDraft.metadataJson} disabled={saving} onChange={(event) => patchCredentialDraft({ metadataJson: event.target.value })} rows={5} spellCheck={false} />
</FormField>
</AdvancedOptionsPanel>
@@ -1228,11 +1249,9 @@ export default function FileConnectorSettingsPanel({
footer={draft ?
<>
<Button onClick={closeProfileDialog} disabled={saving}>i18n:govoplan-files.cancel.77dfd213</Button>
<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>
</DisabledActionTooltip>
<Button variant="primary" onClick={() => void saveDraft()} disabled={profileSaveDisabled} disabledReason={profileSaveDisabledReason}>
<Save size={16} aria-hidden="true" /> {saving ? "i18n:govoplan-files.saving.ae7e8875" : "i18n:govoplan-files.save_connection.07796d38"}
</Button>
</> :
null}>
@@ -1277,13 +1296,13 @@ export default function FileConnectorSettingsPanel({
<p>Only fields relevant for the selected provider and credential mode are shown.</p>
</header>
<div className="form-grid two">
<FormField label="i18n:govoplan-files.endpoint_url.65aaaa45">
<FormField label="i18n:govoplan-files.endpoint_url.65aaaa45" documentation={CONNECTOR_DOCUMENTATION}>
<input value={draft.endpointUrl} disabled={saving} onChange={(event) => patchDraft({ endpointUrl: event.target.value })} placeholder={ENDPOINT_PLACEHOLDERS[draft.provider]} />
</FormField>
<FormField label="i18n:govoplan-files.base_path.6a4867ca">
<input value={draft.basePath} disabled={saving} onChange={(event) => patchDraft({ basePath: event.target.value })} placeholder="GovOPlaN" />
</FormField>
<FormField label="i18n:govoplan-files.credential_mode.23fdd899">
<FormField label="i18n:govoplan-files.credential_mode.23fdd899" documentation={CONNECTOR_DOCUMENTATION}>
<select value={draft.credentialMode} disabled={saving} onChange={(event) => patchDraft({ credentialMode: event.target.value as CredentialMode })}>
<option value="none">i18n:govoplan-files.none.6eef6648</option>
<option value="anonymous">i18n:govoplan-files.anonymous.9bed5104</option>
@@ -1300,9 +1319,9 @@ export default function FileConnectorSettingsPanel({
</FormField>
</div>
<div className="button-row compact-actions">
<Button type="button" onClick={() => patchDraft({ endpointUrl: ENDPOINT_PLACEHOLDERS[draft.provider] })} disabled={saving}>Use provider example</Button>
<Button type="button" onClick={() => patchDraft({ endpointUrl: ENDPOINT_PLACEHOLDERS[draft.provider] })} disabled={saving} disabledReason={saving ? "Wait for the current connector change to finish." : undefined}>Use provider example</Button>
{canDiscoverProfileEndpoint &&
<Button type="button" onClick={() => void discoverProfileEndpoint()} disabled={saving || discoveringProfile || !draft.endpointUrl.trim()}>
<Button type="button" onClick={() => void discoverProfileEndpoint()} disabled={saving || discoveringProfile || !draft.endpointUrl.trim()} disabledReason={saving ? "Wait for the current connector change to finish." : discoveringProfile ? "Endpoint discovery is already running." : !draft.endpointUrl.trim() ? "Enter a Nextcloud or WebDAV URL before discovery." : undefined}>
<RefreshCw size={16} aria-hidden="true" /> {discoveringProfile ? "Checking endpoint" : "Discover endpoint"}
</Button>
}
@@ -1348,7 +1367,7 @@ export default function FileConnectorSettingsPanel({
<ToggleSwitch label="i18n:govoplan-files.sync.905f6309" checked={draft.canSync} disabled={saving} onChange={(canSync) => patchDraft({ canSync })} />
</div>
<div className="form-grid two">
<FormField label="i18n:govoplan-files.policy.bb9cf141">
<FormField label="i18n:govoplan-files.policy.bb9cf141" documentation={CONNECTOR_DOCUMENTATION}>
<select value={draft.policyMode} disabled={saving} onChange={(event) => patchDraft({ policyMode: event.target.value as PolicyMode })}>
<option value="allow-provider">i18n:govoplan-files.can_use_this_connection.5091a74c</option>
<option value="allowed-paths">i18n:govoplan-files.must_stay_in_allowed_paths.dc5b4eb4</option>
@@ -1365,7 +1384,7 @@ export default function FileConnectorSettingsPanel({
</FormField>
</div>
<AdvancedOptionsPanel title="Connection metadata JSON" summary="For diagnostics or provider-specific compatibility flags. Normal setup should not need this.">
<FormField label="i18n:govoplan-files.metadata_json.b0e4c283">
<FormField label="i18n:govoplan-files.metadata_json.b0e4c283" documentation={CONNECTOR_RECOVERY_DOCUMENTATION}>
<textarea value={draft.metadataJson} disabled={saving} onChange={(event) => patchDraft({ metadataJson: event.target.value })} rows={5} spellCheck={false} />
</FormField>
</AdvancedOptionsPanel>
@@ -1485,6 +1504,13 @@ function targetPluralLabel(scopeType: ConnectorScope, fallback: string): string
return fallback;
}
function connectorMutationBlocker(canWrite: boolean, readOnly: boolean, saving: boolean): string {
if (!canWrite) return "Connector administration permission is required for this action.";
if (readOnly) return "This entry is deployment-managed. Change its bootstrap configuration and restart the service.";
if (saving) return "Wait for the current connector change to finish.";
return "";
}
function emptyDraft(scopeType: ConnectorScope): ConnectorProfileDraft {
return {
id: `${scopeType}-webdav`,
+31 -12
View File
@@ -3,6 +3,7 @@ import { ArrowUp, ChevronRight, Copy, Download, File, Folder, Home, KeyRound, Li
import {
Button,
ConfirmDialog,
DocumentationHelpLink,
DismissibleAlert,
FieldLabel,
FileDropZone,
@@ -107,6 +108,10 @@ type FileAccessExplanationTarget = {
const DEFAULT_FILE_LIST_ROW_HEIGHT = 58;
const FILE_LIST_OVERSCAN_ROWS = 8;
const ARCHIVE_FILENAME_PATTERN = /\.(?:zip|tar|tar\.gz|tgz|tar\.bz2|tbz2|tar\.xz|txz)$/i;
const FILES_WORKFLOW_DOCUMENTATION = {
topicId: "files.workflow.organize-managed-files",
documentationType: "user"
} as const;
export default function FilesPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
const canDownload = hasScope(auth, "files:download");
@@ -2071,31 +2076,45 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
message === "i18n:govoplan-files.unpacking_zip_upload.35019691" ||
message === "i18n:govoplan-files.finalizing_upload.bcce936d" ? "info" : "success";
const managedSpaceBlocker = !activeSpace ? "Select a file space first." :
activeSpaceIsConnector ? "This action writes managed storage and is unavailable in a read-only connector space." : "";
const workingBlocker = busy ? "Wait for the current file operation to finish." : "";
const uploadBlocker = workingBlocker || managedSpaceBlocker || (!canUpload ? "File upload permission is required." : "");
const organizeBlocker = workingBlocker || managedSpaceBlocker || (!canOrganize ? "File organization permission is required." : "");
const selectionBlocker = !hasSelection ? "Select at least one file or folder first." : "";
const downloadBlocker = workingBlocker || managedSpaceBlocker || (!canDownload ? "File download permission is required." : "") || (selectedDownloadFileIds.length === 0 ? "Select at least one downloadable file first." : "");
const shareBlocker = workingBlocker || managedSpaceBlocker || (!canShare ? "File sharing permission is required." : "") || (!shareManageableFile ? "Select one manageable file first." : "");
const accessExplanationBlocker = workingBlocker || (activeSpaceIsConnector ? "Access explanations apply to managed files and folders." : "") || (!canExplainResourceAccess ? "Permission to inspect resource access is required." : "") || (!accessExplainableTarget ? "Select one file or folder first." : "");
const deleteBlocker = workingBlocker || managedSpaceBlocker || (!canDelete ? "File deletion permission is required." : "") || selectionBlocker;
const syncBlocker = workingBlocker || (!activeSpace ? "Select a file space first." : "") || (!canUpload ? "File upload permission is required to import synchronized content." : "") || (activeSpaceIsConnector && connectorSpaceSelectedItem?.kind !== "file" ? "Select one remote file to synchronize." : "");
const toolbar =
<div className="file-manager-toolbar" aria-label="i18n:govoplan-files.file_actions.9e1b94c5">
<Button variant="primary" onClick={() => openDialog("upload", toolbarTarget())} disabled={busy || !activeSpace || activeSpaceIsConnector || !canUpload}><UploadCloud size={16} aria-hidden="true" /> i18n:govoplan-files.upload.8bdf057f</Button>
<Button variant="primary" onClick={() => openDialog("upload", toolbarTarget())} disabled={Boolean(uploadBlocker)} disabledReason={uploadBlocker}><UploadCloud size={16} aria-hidden="true" /> i18n:govoplan-files.upload.8bdf057f</Button>
<Button
onClick={() => activeSpaceIsConnector ? void syncConnectorSpaceSelection() : void openConnectorSyncDialog(toolbarTarget())}
disabled={busy || !activeSpace || !canUpload || activeSpaceIsConnector && connectorSpaceSelectedItem?.kind !== "file"}>
disabled={Boolean(syncBlocker)}
disabledReason={syncBlocker}>
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.sync.905f6309
</Button>
<Button onClick={() => void openConnectorSpaceDialog()} disabled={busy || !canOrganize}>
<Button onClick={() => void openConnectorSpaceDialog()} disabled={busy || !canOrganize} disabledReason={workingBlocker || (!canOrganize ? "File organization permission is required to add a connector space." : "")}>
<Link2 size={16} aria-hidden="true" /> i18n:govoplan-files.add_space.e4d674d4
</Button>
<Button onClick={() => void downloadSelection()} disabled={busy || activeSpaceIsConnector || !canDownload || selectedDownloadFileIds.length === 0}><Download size={16} aria-hidden="true" /> {downloadLabel}</Button>
<Button onClick={() => shareManageableFile && setShareDialogFile(shareManageableFile)} disabled={busy || activeSpaceIsConnector || !shareManageableFile}><Share2 size={16} aria-hidden="true" /> Manage shares</Button>
<Button onClick={() => openDialog("create-folder", toolbarTarget())} disabled={busy || !activeSpace || activeSpaceIsConnector || !canOrganize}><Plus size={16} aria-hidden="true" /> i18n:govoplan-files.create_folder.97bafaba</Button>
<Button onClick={() => openTransferDialog("move")} disabled={busy || activeSpaceIsConnector || !canOrganize || !hasSelection}><MoveRight size={16} aria-hidden="true" /> i18n:govoplan-files.move.76cdb950</Button>
<Button onClick={() => openTransferDialog("copy")} disabled={busy || activeSpaceIsConnector || !canOrganize || !hasSelection}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
{hasSelection && <Button onClick={openRenameDialog} disabled={busy || activeSpaceIsConnector || !canOrganize}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>}
<Button onClick={() => accessExplainableTarget && void openAccessExplanation(accessExplainableTarget)} disabled={busy || activeSpaceIsConnector || !canExplainResourceAccess || !accessExplainableTarget}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
<Button variant="danger" onClick={() => void deleteSelected()} disabled={busy || activeSpaceIsConnector || !canDelete || !hasSelection}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
<Button onClick={() => void downloadSelection()} disabled={Boolean(downloadBlocker)} disabledReason={downloadBlocker}><Download size={16} aria-hidden="true" /> {downloadLabel}</Button>
<Button onClick={() => shareManageableFile && setShareDialogFile(shareManageableFile)} disabled={Boolean(shareBlocker)} disabledReason={shareBlocker}><Share2 size={16} aria-hidden="true" /> Manage shares</Button>
<Button onClick={() => openDialog("create-folder", toolbarTarget())} disabled={Boolean(organizeBlocker)} disabledReason={organizeBlocker}><Plus size={16} aria-hidden="true" /> i18n:govoplan-files.create_folder.97bafaba</Button>
<Button onClick={() => openTransferDialog("move")} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><MoveRight size={16} aria-hidden="true" /> i18n:govoplan-files.move.76cdb950</Button>
<Button onClick={() => openTransferDialog("copy")} disabled={Boolean(organizeBlocker || selectionBlocker)} disabledReason={organizeBlocker || selectionBlocker}><Copy size={16} aria-hidden="true" /> i18n:govoplan-files.copy.af74f7c5</Button>
{hasSelection && <Button onClick={openRenameDialog} disabled={Boolean(organizeBlocker)} disabledReason={organizeBlocker}>{selectedEntryCount === 1 ? "i18n:govoplan-files.rename.d3f4cb89" : "i18n:govoplan-files.bulk_rename.7dcaa624"}</Button>}
<Button onClick={() => accessExplainableTarget && void openAccessExplanation(accessExplainableTarget)} disabled={Boolean(accessExplanationBlocker)} disabledReason={accessExplanationBlocker}><KeyRound size={16} aria-hidden="true" /> i18n:govoplan-files.explain_access.4d5fac37</Button>
<Button variant="danger" onClick={() => void deleteSelected()} disabled={Boolean(deleteBlocker)} disabledReason={deleteBlocker}><Trash2 size={16} aria-hidden="true" /> i18n:govoplan-files.delete.f6fdbe48</Button>
{activeSpaceIsConnector &&
<Button onClick={() => activeSpace && void loadConnectorSpaceContents(activeSpace)} disabled={busy || connectorSpaceLoading || !activeSpace}>
<Button onClick={() => activeSpace && void loadConnectorSpaceContents(activeSpace)} disabled={busy || connectorSpaceLoading || !activeSpace} disabledReason={workingBlocker || (connectorSpaceLoading ? "This connector space is already refreshing." : !activeSpace ? "Select a connector space first." : "")}>
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-files.refresh.56e3badc
</Button>
}
<DocumentationHelpLink reference={FILES_WORKFLOW_DOCUMENTATION} />
</div>;