Release v0.1.4

This commit is contained in:
2026-07-02 14:59:52 +02:00
parent ab2343aa88
commit fb6fb67d45
27 changed files with 1437 additions and 1785 deletions

View File

@@ -1,8 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { Pencil } from "lucide-react";
import { usePlatformModuleInstalled } from "@govoplan/core-webui";
import { usePlatformModuleInstalled, usePlatformUiCapability, type FilesFileExplorerUiCapability, type FilesFileSpace } from "@govoplan/core-webui";
import type { ApiSettings } from "../../types";
import { listFileSpaces, type FileSpace } from "../../api/files";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
@@ -18,7 +17,6 @@ import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
import { updateNested } from "./utils/draftEditor";
import { AttachmentRulesDataGrid } from "./components/AttachmentRulesOverlay";
import ManagedFileChooser from "./components/ManagedFileChooser";
import TemplateExpressionEditorDialog from "./components/TemplateExpressionEditorDialog";
import { countIndividualAttachmentRules, countIndividualAttachmentRulesForBasePath, createAttachmentBasePath, ensureAttachmentBasePaths, normalizeAttachmentBasePaths, normalizeAttachmentRules, normalizeAttachmentZipCollection, createAttachmentZipArchive, parseManagedAttachmentSource, removeIndividualAttachmentRulesForBasePath, summarizeAttachmentRules, type AttachmentBasePath, type AttachmentZipArchive, type AttachmentZipCollection } from "./utils/attachments";
import { insertAfter, moveArrayItem } from "@govoplan/core-webui";
@@ -30,9 +28,13 @@ type IndividualDisableState = { index: number; usageCount: number };
export default function AttachmentsDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
const filesModuleInstalled = usePlatformModuleInstalled("files");
const filesFileExplorer = usePlatformUiCapability<FilesFileExplorerUiCapability>("files.fileExplorer");
const ManagedFileChooser = filesModuleInstalled ? filesFileExplorer?.ManagedFileChooser : null;
const managedFilesAvailable = Boolean(ManagedFileChooser);
const listManagedFileSpaces = filesModuleInstalled ? filesFileExplorer?.listFileSpaces : undefined;
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId);
const [pathChooser, setPathChooser] = useState<PathChooserState | null>(null);
const [fileSpaces, setFileSpaces] = useState<FileSpace[]>([]);
const [fileSpaces, setFileSpaces] = useState<FilesFileSpace[]>([]);
const [individualDisable, setIndividualDisable] = useState<IndividualDisableState | null>(null);
const [zipNameEditorIndex, setZipNameEditorIndex] = useState<number | null>(null);
const version = data.currentVersion;
@@ -71,16 +73,16 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
);
useEffect(() => {
if (!filesModuleInstalled) {
if (!listManagedFileSpaces) {
setFileSpaces([]);
return;
}
let cancelled = false;
void listFileSpaces(settings)
void listManagedFileSpaces(settings)
.then((response) => { if (!cancelled) setFileSpaces(response.spaces); })
.catch(() => { if (!cancelled) setFileSpaces([]); });
return () => { cancelled = true; };
}, [filesModuleInstalled, settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
}, [listManagedFileSpaces, settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
function patchBasePaths(paths: AttachmentBasePath[]) {
if (locked) return;
@@ -251,7 +253,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
<DataGrid
id={`campaign-${campaignId}-attachment-sources`}
rows={basePaths}
columns={attachmentSourceColumns({ locked, basePaths, fileSpaces, filesModuleInstalled, patchBasePath, setIndividualEligibility, addBasePath, moveBasePath, removeBasePath, setPathChooser })}
columns={attachmentSourceColumns({ locked, basePaths, fileSpaces, managedFilesAvailable, patchBasePath, setIndividualEligibility, addBasePath, moveBasePath, removeBasePath, setPathChooser })}
getRowKey={(basePath) => basePath.id}
emptyText="No attachment sources configured."
emptyAction={<DataGridEmptyAction onAdd={() => addBasePath(-1)} disabled={locked} label="Add first attachment source" />}
@@ -310,7 +312,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
settings={settings}
campaignId={campaignId}
zipConfig={zipConfig}
filesModuleInstalled={filesModuleInstalled}
filesModuleInstalled={managedFilesAvailable}
previewContext={attachmentPreviewContext}
onChange={(rules) => patch(["attachments", "global"], rules)}
/>
@@ -321,9 +323,13 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
<div><dt>Base paths</dt><dd>{basePaths.length}</dd></div>
<div><dt>Global attachments</dt><dd>direct: {globalSummary.direct} / rules: {globalSummary.rules}</dd></div>
<div><dt>Per-recipient patterns</dt><dd>{individualRulesCount}</dd></div>
<div><dt>Upload support</dt><dd>{filesModuleInstalled ? "Connected through Files" : "Manual paths"}</dd></div>
<div><dt>Upload support</dt><dd>{managedFilesAvailable ? "Connected through Files" : "Manual paths"}</dd></div>
</dl>
<p className="muted small-note">{filesModuleInstalled ? "Files are managed in the top-level Files module. Upload there, share files with campaigns, then use these rules to resolve concrete attachments during review/build." : "The Files module is not installed. Use manual paths and patterns; managed file browsing and sharing are unavailable."}</p>
<p className="muted small-note">{managedFilesAvailable
? "Files are managed in the top-level Files module. Upload there, share files with campaigns, then use these rules to resolve concrete attachments during review/build."
: filesModuleInstalled
? "Managed file browsing is unavailable. Use manual paths and patterns until the Files module exposes the file explorer capability."
: "The Files module is not installed. Use manual paths and patterns; managed file browsing and sharing are unavailable."}</p>
</Card>
</>
@@ -357,11 +363,11 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
canAddField={(name) => !getDraftFields(displayDraft).some((field) => field.name === name)}
/>
{pathChooser && filesModuleInstalled && (
{pathChooser && ManagedFileChooser && (
<ManagedFileChooser
open
settings={settings}
campaignId={campaignId}
storageScope={campaignId}
mode="folder"
source={basePaths[pathChooser.index]?.source}
basePath={basePaths[pathChooser.index]?.path}
@@ -562,8 +568,8 @@ function uniqueStrings(values: string[]): string[] {
type AttachmentSourceColumnContext = {
locked: boolean;
basePaths: AttachmentBasePath[];
fileSpaces: FileSpace[];
filesModuleInstalled: boolean;
fileSpaces: FilesFileSpace[];
managedFilesAvailable: boolean;
patchBasePath: (index: number, patch: Partial<AttachmentBasePath>) => void;
setIndividualEligibility: (index: number, checked: boolean) => void;
addBasePath: (afterIndex?: number) => void;
@@ -572,7 +578,7 @@ type AttachmentSourceColumnContext = {
setPathChooser: (state: PathChooserState | null) => void;
};
function attachmentSourceColumns({ locked, basePaths, fileSpaces, filesModuleInstalled, patchBasePath, setIndividualEligibility, addBasePath, moveBasePath, removeBasePath, setPathChooser }: AttachmentSourceColumnContext): DataGridColumn<AttachmentBasePath>[] {
function attachmentSourceColumns({ locked, basePaths, fileSpaces, managedFilesAvailable, patchBasePath, setIndividualEligibility, addBasePath, moveBasePath, removeBasePath, setPathChooser }: AttachmentSourceColumnContext): DataGridColumn<AttachmentBasePath>[] {
return [
{ id: "name", header: "Name", width: 220, resizable: true, sortable: true, filterable: true, sticky: "start", render: (basePath, index) => <input value={basePath.name} disabled={locked} placeholder="Campaign files" onChange={(event) => patchBasePath(index, { name: event.target.value })} />, value: (basePath) => basePath.name },
{
@@ -586,23 +592,23 @@ function attachmentSourceColumns({ locked, basePaths, fileSpaces, filesModuleIns
<div className="field-with-action split-field-action">
<input
className="chooser-display-input"
value={filesModuleInstalled ? formatAttachmentSourcePath(basePath, fileSpaces) : basePath.path}
value={managedFilesAvailable ? formatAttachmentSourcePath(basePath, fileSpaces) : basePath.path}
disabled={locked}
readOnly={filesModuleInstalled}
tabIndex={filesModuleInstalled ? -1 : undefined}
readOnly={managedFilesAvailable}
tabIndex={managedFilesAvailable ? -1 : undefined}
placeholder="attachments"
onChange={(event) => {
if (!filesModuleInstalled) patchBasePath(index, { path: event.target.value, source: "" });
if (!managedFilesAvailable) patchBasePath(index, { path: event.target.value, source: "" });
}}
onClick={() => filesModuleInstalled && !locked && setPathChooser({ index })}
onClick={() => managedFilesAvailable && !locked && setPathChooser({ index })}
onKeyDown={(event) => {
if (filesModuleInstalled && !locked && (event.key === "Enter" || event.key === " ")) {
if (managedFilesAvailable && !locked && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
setPathChooser({ index });
}
}}
/>
{filesModuleInstalled && <Button onClick={() => setPathChooser({ index })} disabled={locked}>Choose folder</Button>}
{managedFilesAvailable && <Button onClick={() => setPathChooser({ index })} disabled={locked}>Choose folder</Button>}
</div>
),
value: (basePath) => formatAttachmentSourcePath(basePath, fileSpaces)
@@ -631,7 +637,7 @@ function attachmentSourceColumns({ locked, basePaths, fileSpaces, filesModuleIns
}
];
}
function formatAttachmentSourcePath(basePath: AttachmentBasePath, spaces: FileSpace[]): string {
function formatAttachmentSourcePath(basePath: AttachmentBasePath, spaces: FilesFileSpace[]): string {
const parsedSource = parseManagedAttachmentSource(basePath.source);
const matchingSpace = parsedSource
? spaces.find((space) => space.owner_type === parsedSource.ownerType && space.owner_id === parsedSource.ownerId)