Release v0.1.2
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import type { ApiSettings } from "../../../types";
|
||||
import Button from "../../../components/Button";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import DataGrid, { DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "../../../components/table/DataGrid";
|
||||
import ToggleSwitch from "../../../components/ToggleSwitch";
|
||||
import { ToggleSwitch } from "@govoplan/core-webui";
|
||||
import { getBool, getText } from "../utils/draftEditor";
|
||||
import { attachmentRuleZipSelection, createAttachmentRule, nextAttachmentLabel, summarizeAttachmentRules, type AttachmentBasePath, type AttachmentRule, type AttachmentZipCollection } from "../utils/attachments";
|
||||
import ManagedFileChooser, { type ManagedAttachmentSelection } from "./ManagedFileChooser";
|
||||
import { insertAfter, moveArrayItem } from "../../../utils/arrayOrder";
|
||||
import { insertAfter, moveArrayItem } from "@govoplan/core-webui";
|
||||
import { asRecord } from "../utils/campaignView";
|
||||
|
||||
export type { AttachmentBasePath, AttachmentRule } from "../utils/attachments";
|
||||
@@ -22,6 +22,7 @@ type AttachmentRulesOverlayProps = {
|
||||
emptyText?: string;
|
||||
basePaths?: AttachmentBasePath[];
|
||||
zipConfig?: AttachmentZipCollection;
|
||||
filesModuleInstalled?: boolean;
|
||||
onChange: (rules: AttachmentRule[]) => void;
|
||||
};
|
||||
|
||||
@@ -33,9 +34,9 @@ type AttachmentRulesTableProps = {
|
||||
emptyText?: string;
|
||||
basePaths?: AttachmentBasePath[];
|
||||
id?: string;
|
||||
showAddButton?: boolean;
|
||||
activeChooserRuleIndex?: number | null;
|
||||
zipConfig?: AttachmentZipCollection;
|
||||
filesModuleInstalled?: boolean;
|
||||
onOpenFileChooser?: (ruleIndex: number) => void;
|
||||
onChange: (rules: AttachmentRule[]) => void;
|
||||
};
|
||||
@@ -55,6 +56,7 @@ export default function AttachmentRulesOverlay({
|
||||
emptyText = "No attachment files or matching rules configured yet.",
|
||||
basePaths = [],
|
||||
zipConfig = { enabled: false, archives: [] },
|
||||
filesModuleInstalled = false,
|
||||
onChange
|
||||
}: AttachmentRulesOverlayProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -85,7 +87,7 @@ export default function AttachmentRulesOverlay({
|
||||
<button className="modal-close" aria-label="Cancel attachment changes" onClick={cancelOverlay}>×</button>
|
||||
</header>
|
||||
<div className="modal-body attachment-rules-body">
|
||||
<AttachmentRulesDataGrid
|
||||
<AttachmentRulesTable
|
||||
rules={draftRules}
|
||||
settings={settings}
|
||||
campaignId={campaignId}
|
||||
@@ -93,6 +95,7 @@ export default function AttachmentRulesOverlay({
|
||||
emptyText={emptyText}
|
||||
basePaths={basePaths}
|
||||
zipConfig={zipConfig}
|
||||
filesModuleInstalled={filesModuleInstalled}
|
||||
activeChooserRuleIndex={null}
|
||||
onChange={setDraftRules}
|
||||
/>
|
||||
@@ -124,26 +127,13 @@ function cloneAttachmentRules(rules: AttachmentRule[]): AttachmentRule[] {
|
||||
}
|
||||
|
||||
export function AttachmentRulesTable({
|
||||
showAddButton = true,
|
||||
onChange,
|
||||
...tableProps
|
||||
}: AttachmentRulesTableProps) {
|
||||
function addRule() {
|
||||
onChange([
|
||||
...tableProps.rules,
|
||||
createAttachmentRule(tableProps.basePaths?.[0]?.path ?? "", nextAttachmentLabel(tableProps.rules), tableProps.basePaths?.[0]?.id ?? "")
|
||||
]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="attachment-rules-editor">
|
||||
<div className="attachment-rules-main">
|
||||
<AttachmentRulesDataGrid {...tableProps} onChange={onChange} />
|
||||
{showAddButton && (
|
||||
<div className="button-row compact-actions attachment-rules-footer-actions">
|
||||
<Button variant="primary" onClick={addRule} disabled={tableProps.disabled || (tableProps.basePaths?.length ?? 0) === 0}>Add file</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -159,6 +149,7 @@ export function AttachmentRulesDataGrid({
|
||||
id = "attachment-rules",
|
||||
activeChooserRuleIndex = null,
|
||||
zipConfig = { enabled: false, archives: [] },
|
||||
filesModuleInstalled = false,
|
||||
onOpenFileChooser,
|
||||
onChange
|
||||
}: AttachmentRulesTableProps) {
|
||||
@@ -186,6 +177,7 @@ export function AttachmentRulesDataGrid({
|
||||
}
|
||||
|
||||
function openFileChooser(ruleIndex: number) {
|
||||
if (!filesModuleInstalled) return;
|
||||
if (onOpenFileChooser) {
|
||||
onOpenFileChooser(ruleIndex);
|
||||
return;
|
||||
@@ -221,14 +213,14 @@ export function AttachmentRulesDataGrid({
|
||||
<DataGrid
|
||||
id={id}
|
||||
rows={rules}
|
||||
columns={attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, activeChooserRuleIndex: activeChooserRuleIndex ?? fileChooser?.ruleIndex ?? null, patchRule, addRule, moveRule, openFileChooser, removeRule })}
|
||||
columns={attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, filesModuleInstalled, activeChooserRuleIndex: activeChooserRuleIndex ?? fileChooser?.ruleIndex ?? null, patchRule, addRule, moveRule, openFileChooser, removeRule })}
|
||||
getRowKey={(rule, index) => String(rule.id ?? index)}
|
||||
emptyText={basePaths.length === 0 ? "No attachment source is enabled for individual attachments." : emptyText}
|
||||
emptyAction={<DataGridEmptyAction onAdd={() => addRule(-1)} disabled={disabled || basePaths.length === 0} label="Add first attachment" />}
|
||||
className="attachment-rules-table-wrap attachment-rules-table"
|
||||
rowClassName={(_rule, index) => (activeChooserRuleIndex ?? fileChooser?.ruleIndex) === index ? "is-choosing-file" : undefined}
|
||||
/>
|
||||
{fileChooser && (
|
||||
{fileChooser && filesModuleInstalled && (
|
||||
<ManagedFileChooser
|
||||
open
|
||||
settings={settings}
|
||||
@@ -251,6 +243,7 @@ type AttachmentRuleColumnContext = {
|
||||
rules: AttachmentRule[];
|
||||
basePaths: AttachmentBasePath[];
|
||||
zipConfig: AttachmentZipCollection;
|
||||
filesModuleInstalled: boolean;
|
||||
activeChooserRuleIndex: number | null;
|
||||
patchRule: (index: number, patch: Partial<AttachmentRule>) => void;
|
||||
addRule: (afterIndex?: number) => void;
|
||||
@@ -259,7 +252,7 @@ type AttachmentRuleColumnContext = {
|
||||
removeRule: (index: number) => void;
|
||||
};
|
||||
|
||||
function attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, activeChooserRuleIndex: _activeChooserRuleIndex, patchRule, addRule, moveRule, openFileChooser, removeRule }: AttachmentRuleColumnContext): DataGridColumn<AttachmentRule>[] {
|
||||
function attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, filesModuleInstalled, activeChooserRuleIndex: _activeChooserRuleIndex, patchRule, addRule, moveRule, openFileChooser, removeRule }: AttachmentRuleColumnContext): DataGridColumn<AttachmentRule>[] {
|
||||
return [
|
||||
{ id: "label", header: "Label", width: 190, resizable: true, sortable: true, filterable: true, sticky: "start", render: (rule, index) => <input value={getText(rule, "label")} disabled={disabled} placeholder="Attachment label" onChange={(event) => patchRule(index, { label: event.target.value })} />, value: (rule) => getText(rule, "label") },
|
||||
{
|
||||
@@ -308,18 +301,21 @@ function attachmentRuleColumns({ disabled, rules, basePaths, zipConfig, activeCh
|
||||
className="chooser-display-input"
|
||||
value={getText(rule, "file_filter")}
|
||||
disabled={disabled || basePaths.length === 0}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
placeholder="Choose a managed file or pattern"
|
||||
onClick={() => !disabled && openFileChooser(index)}
|
||||
readOnly={filesModuleInstalled}
|
||||
tabIndex={filesModuleInstalled ? -1 : undefined}
|
||||
placeholder={filesModuleInstalled ? "Choose a managed file or pattern" : "file.pdf or **/*.pdf"}
|
||||
onChange={(event) => {
|
||||
if (!filesModuleInstalled) patchRule(index, { file_filter: event.target.value });
|
||||
}}
|
||||
onClick={() => filesModuleInstalled && !disabled && openFileChooser(index)}
|
||||
onKeyDown={(event) => {
|
||||
if (!disabled && (event.key === "Enter" || event.key === " ")) {
|
||||
if (filesModuleInstalled && !disabled && (event.key === "Enter" || event.key === " ")) {
|
||||
event.preventDefault();
|
||||
openFileChooser(index);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button onClick={() => openFileChooser(index)} disabled={disabled || basePaths.length === 0}>Choose</Button>
|
||||
{filesModuleInstalled && <Button onClick={() => openFileChooser(index)} disabled={disabled || basePaths.length === 0}>Choose</Button>}
|
||||
</div>
|
||||
),
|
||||
value: (rule) => getText(rule, "file_filter")
|
||||
|
||||
@@ -9,14 +9,13 @@ import {
|
||||
type CampaignShare,
|
||||
type CampaignShareTargets
|
||||
} from "../../../api/campaigns";
|
||||
import Button from "../../../components/Button";
|
||||
import Card from "../../../components/Card";
|
||||
import ConfirmDialog from "../../../components/ConfirmDialog";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||
import DataGrid, { type DataGridColumn } from "../../../components/table/DataGrid";
|
||||
import Dialog from "../../../components/Dialog";
|
||||
import FormField from "../../../components/FormField";
|
||||
import StatusBadge from "../../../components/StatusBadge";
|
||||
|
||||
import { Dialog } from "@govoplan/core-webui";
|
||||
import { FormField } from "@govoplan/core-webui";
|
||||
import { StatusBadge } from "@govoplan/core-webui";
|
||||
type TargetType = "user" | "group";
|
||||
|
||||
export default function CampaignAccessCard({
|
||||
@@ -143,12 +142,12 @@ export default function CampaignAccessCard({
|
||||
<DataGrid id={`campaign-${campaign.id}-shares`} rows={shares} columns={columns} getRowKey={(row) => row.id} emptyText={available === null ? "Loading campaign access…" : "This campaign has no explicit shares."} />
|
||||
</Card>
|
||||
|
||||
<Dialog open={ownerOpen} title="Change campaign owner" onClose={() => !busy && setOwnerOpen(false)} footer={<><Button onClick={() => setOwnerOpen(false)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void saveOwner()} disabled={busy || !ownerId}>Save owner</Button></>}>
|
||||
<Dialog open={ownerOpen} className="campaign-access-dialog" title="Change campaign owner" onClose={() => !busy && setOwnerOpen(false)} footer={<><Button onClick={() => setOwnerOpen(false)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void saveOwner()} disabled={busy || !ownerId}>Save owner</Button></>}>
|
||||
<FormField label="Owner type"><select value={ownerType} onChange={(event) => { const next = event.target.value as TargetType; setOwnerType(next); setOwnerId((next === "user" ? targets.users : targets.groups)[0]?.id || ""); }}><option value="user">User</option><option value="group">Group</option></select></FormField>
|
||||
<FormField label="Owner"><select value={ownerId} onChange={(event) => setOwnerId(event.target.value)}>{targetOptions.map((item) => <option key={item.id} value={item.id}>{item.name}{item.secondary ? ` · ${item.secondary}` : ""}</option>)}</select></FormField>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={shareOpen} title="Share campaign" onClose={() => !busy && setShareOpen(false)} footer={<><Button onClick={() => setShareOpen(false)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void saveShare()} disabled={busy || !shareTargetId}>Save share</Button></>}>
|
||||
<Dialog open={shareOpen} className="campaign-access-dialog" title="Share campaign" onClose={() => !busy && setShareOpen(false)} footer={<><Button onClick={() => setShareOpen(false)} disabled={busy}>Cancel</Button><Button variant="primary" onClick={() => void saveShare()} disabled={busy || !shareTargetId}>Save share</Button></>}>
|
||||
<FormField label="Target type"><select value={shareType} onChange={(event) => { const next = event.target.value as TargetType; setShareType(next); setShareTargetId((next === "user" ? targets.users : targets.groups)[0]?.id || ""); }}><option value="user">User</option><option value="group">Group</option></select></FormField>
|
||||
<FormField label="User or group"><select value={shareTargetId} onChange={(event) => setShareTargetId(event.target.value)}><option value="">Select…</option>{shareTargetOptions.map((item) => <option key={item.id} value={item.id}>{item.name}{item.secondary ? ` · ${item.secondary}` : ""}</option>)}</select></FormField>
|
||||
<FormField label="Access"><select value={sharePermission} onChange={(event) => setSharePermission(event.target.value as "read" | "write")}><option value="read">Can view</option><option value="write">Can edit and operate</option></select></FormField>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from "react";
|
||||
import type { ApiSettings } from "../../../types";
|
||||
import Button from "../../../components/Button";
|
||||
import ConfirmDialog from "../../../components/ConfirmDialog";
|
||||
import DismissibleAlert from "../../../components/DismissibleAlert";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||
import {
|
||||
forkCampaignVersion,
|
||||
lockCampaignVersionPermanently,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, File, Folder, FolderOpen, Home, Link2, Search } from "lucide-react";
|
||||
import { usePlatformUiCapability, type FilesFileExplorerUiCapability } from "@govoplan/core-webui";
|
||||
import type { ApiSettings } from "../../../types";
|
||||
import {
|
||||
listFileSpaces,
|
||||
@@ -12,10 +13,10 @@ import {
|
||||
type FileSpace,
|
||||
type ManagedFile
|
||||
} from "../../../api/files";
|
||||
import Button from "../../../components/Button";
|
||||
import ConfirmDialog from "../../../components/ConfirmDialog";
|
||||
import Dialog from "../../../components/Dialog";
|
||||
import DismissibleAlert from "../../../components/DismissibleAlert";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||
import { Dialog } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||
import { FolderTree } from "../../files/components/FileManagerComponents";
|
||||
import { useFileTreeState } from "../../files/hooks/useFileTreeState";
|
||||
import type { FolderNode, SortColumn, SortDirection } from "../../files/types";
|
||||
@@ -84,6 +85,8 @@ export default function ManagedFileChooser({
|
||||
onSelectFolder,
|
||||
onSelectAttachment
|
||||
}: ManagedFileChooserProps) {
|
||||
const filesExplorerUi = usePlatformUiCapability<FilesFileExplorerUiCapability>("files.fileExplorer");
|
||||
const FolderTreeComponent = filesExplorerUi?.FolderTree ?? FolderTree;
|
||||
const parsedSource = useMemo(() => parseManagedAttachmentSource(source), [source]);
|
||||
const normalizedBasePath = normalizeManagedBasePath(basePath);
|
||||
const storageKey = useMemo(
|
||||
@@ -345,7 +348,7 @@ export default function ManagedFileChooser({
|
||||
<span>{space.label}</span>
|
||||
</button>
|
||||
{selected && (
|
||||
<FolderTree
|
||||
<FolderTreeComponent
|
||||
nodes={treeNodes}
|
||||
activeSpaceId={selectedSpaceId}
|
||||
spaceId={space.id}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react";
|
||||
import Button from "../../../components/Button";
|
||||
import { Button, MessageDisplayPanel, type MessageDisplayAttachment } from "@govoplan/core-webui";
|
||||
|
||||
export type MessagePreviewAttachment = {
|
||||
filename?: string | null;
|
||||
// Campaign review/template/mock previews need recipient navigation, review notes,
|
||||
// raw MIME inspection and attachment grouping. Generic mailbox reading uses
|
||||
// core MessageDisplayPanel instead.
|
||||
export type CampaignMessagePreviewAttachment = MessageDisplayAttachment & {
|
||||
label?: string | null;
|
||||
detail?: string | null;
|
||||
contentType?: string | null;
|
||||
sizeBytes?: number | null;
|
||||
archiveGroup?: string | null;
|
||||
archiveLabel?: string | null;
|
||||
};
|
||||
|
||||
export type MessagePreviewMetaItem = {
|
||||
export type CampaignMessagePreviewMetaItem = {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
value: ReactNode;
|
||||
};
|
||||
|
||||
export type MessagePreviewNavigation = {
|
||||
export type CampaignMessagePreviewNavigation = {
|
||||
index: number;
|
||||
total: number;
|
||||
onFirst: () => void;
|
||||
@@ -25,24 +23,24 @@ export type MessagePreviewNavigation = {
|
||||
onLast: () => void;
|
||||
};
|
||||
|
||||
export type MessagePreviewOverlayProps = {
|
||||
export type CampaignMessagePreviewOverlayProps = {
|
||||
title?: string;
|
||||
subject?: string | null;
|
||||
bodyMode?: "text" | "html";
|
||||
text?: string | null;
|
||||
html?: string | null;
|
||||
recipientLabel?: React.ReactNode;
|
||||
recipientNote?: React.ReactNode;
|
||||
metaItems?: MessagePreviewMetaItem[];
|
||||
attachments?: MessagePreviewAttachment[];
|
||||
recipientLabel?: ReactNode;
|
||||
recipientNote?: ReactNode;
|
||||
metaItems?: CampaignMessagePreviewMetaItem[];
|
||||
attachments?: CampaignMessagePreviewAttachment[];
|
||||
raw?: string | null;
|
||||
rawLabel?: string;
|
||||
navigation?: MessagePreviewNavigation;
|
||||
navigation?: CampaignMessagePreviewNavigation;
|
||||
closeLabel?: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function MessagePreviewOverlay({
|
||||
export default function CampaignMessagePreviewOverlay({
|
||||
title = "Message preview",
|
||||
subject,
|
||||
bodyMode = "text",
|
||||
@@ -57,10 +55,9 @@ export default function MessagePreviewOverlay({
|
||||
navigation,
|
||||
closeLabel = "Close",
|
||||
onClose
|
||||
}: MessagePreviewOverlayProps) {
|
||||
}: CampaignMessagePreviewOverlayProps) {
|
||||
const shownSubject = subject?.trim() || "No subject";
|
||||
const shownText = text?.trim() || "No plain-text body to preview.";
|
||||
const shownHtml = html?.trim() || "<p>No HTML body to preview.</p>";
|
||||
const fields = metaItems.map((item) => ({ label: item.label, value: item.value }));
|
||||
|
||||
return (
|
||||
<div className="overlay-backdrop" role="dialog" aria-modal="true" aria-labelledby="message-preview-title">
|
||||
@@ -88,24 +85,16 @@ export default function MessagePreviewOverlay({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metaItems.length > 0 && (
|
||||
<div className="detail-grid message-preview-meta">
|
||||
{metaItems.map((item) => (
|
||||
<div key={item.label}><span className="muted small-note">{item.label}</span><strong>{item.value || "—"}</strong></div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="template-preview-box">
|
||||
<h3>{shownSubject}</h3>
|
||||
{bodyMode === "html" ? (
|
||||
<iframe className="template-preview-frame" title="Rendered HTML body preview" sandbox="" srcDoc={shownHtml} />
|
||||
) : (
|
||||
<pre>{shownText}</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MessagePreviewAttachmentBoxes attachments={attachments} />
|
||||
<MessageDisplayPanel
|
||||
title={shownSubject}
|
||||
fields={fields}
|
||||
bodyText={text}
|
||||
bodyHtml={html}
|
||||
preferredBodyMode={bodyMode}
|
||||
deriveTextFromHtml={false}
|
||||
attachments={attachments}
|
||||
emptyText="No message content is available."
|
||||
/>
|
||||
|
||||
{raw && (
|
||||
<details className="message-preview-raw">
|
||||
@@ -120,46 +109,8 @@ export default function MessagePreviewOverlay({
|
||||
);
|
||||
}
|
||||
|
||||
function MessagePreviewAttachmentBoxes({ attachments }: { attachments: MessagePreviewAttachment[] }) {
|
||||
const direct = attachments.filter((attachment) => !attachment.archiveGroup);
|
||||
const archiveGroups = new Map<string, MessagePreviewAttachment[]>();
|
||||
for (const attachment of attachments) {
|
||||
if (!attachment.archiveGroup) continue;
|
||||
const group = archiveGroups.get(attachment.archiveGroup) ?? [];
|
||||
group.push(attachment);
|
||||
archiveGroups.set(attachment.archiveGroup, group);
|
||||
}
|
||||
|
||||
function attachmentChip(attachment: MessagePreviewAttachment, index: number) {
|
||||
const filename = attachment.filename?.trim() || attachment.label?.trim() || "Unnamed attachment";
|
||||
const details = [attachment.detail, attachment.contentType, attachment.sizeBytes ? `${attachment.sizeBytes} bytes` : ""].filter(Boolean).join(" · ");
|
||||
return (
|
||||
<div className="attachment-file-chip" key={`${filename}:${index}`}>
|
||||
<strong>{filename}</strong>
|
||||
{details && <span>{details}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="template-preview-attachments message-preview-attachments">
|
||||
<h3>Attachments</h3>
|
||||
{attachments.length === 0 ? (
|
||||
<p className="muted small-note">No attachments are effective for this message.</p>
|
||||
) : (
|
||||
<div className="message-preview-attachment-layout">
|
||||
{direct.length > 0 && <div className="attachment-chip-grid">{direct.map(attachmentChip)}</div>}
|
||||
{[...archiveGroups.entries()].map(([groupName, items]) => (
|
||||
<section className="attachment-zip-group" key={groupName}>
|
||||
<header>
|
||||
<strong>{items[0]?.archiveLabel || groupName}</strong>
|
||||
<span>{items.length} file{items.length === 1 ? "" : "s"} inside ZIP</span>
|
||||
</header>
|
||||
<div className="attachment-chip-grid">{items.map(attachmentChip)}</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export type MessagePreviewAttachment = CampaignMessagePreviewAttachment;
|
||||
export type MessagePreviewMetaItem = CampaignMessagePreviewMetaItem;
|
||||
export type MessagePreviewNavigation = CampaignMessagePreviewNavigation;
|
||||
export type MessagePreviewOverlayProps = CampaignMessagePreviewOverlayProps;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import Button from "../../../components/Button";
|
||||
import Dialog from "../../../components/Dialog";
|
||||
import DismissibleAlert from "../../../components/DismissibleAlert";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { Dialog } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||
import {
|
||||
buildUndefinedPlaceholders,
|
||||
extractTemplatePlaceholders,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Button from "../../../components/Button";
|
||||
import Dialog from "../../../components/Dialog";
|
||||
import DismissibleAlert from "../../../components/DismissibleAlert";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { Dialog } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||
import type { TemplateNamespace, TemplatePlaceholder, UndefinedPlaceholder } from "../utils/templatePlaceholders";
|
||||
|
||||
export type TemplateFieldOption = {
|
||||
|
||||
Reference in New Issue
Block a user