- {filterColumn ? (
+
+ {searchCandidateIds.length > 0 ? (
table.setGlobalFilter(value)}
placeholder={filterPlaceholder}
- table={table}
+ query={effectiveQuery}
/>
) : null}
+ {isMultiMode && searchCandidateIds.length > 1 ? (
+
+
+
+
+
+ {searchCandidateIds.map((id) => {
+ const column = table.getColumn(id);
+
+ if (!column) {
+ return null;
+ }
+
+ const isChecked = searchColumns.length === 0 ? true : searchColumns.includes(id);
+
+ return (
+ {
+ setSearchColumns((prev) => {
+ // Treat the empty-selection
+ // sentinel as "all candidates"
+ // before mutating, so the user
+ // never lands in a state where
+ // unchecking one box silently
+ // re-enables every column.
+ const base = prev.length === 0 ? [...searchCandidateIds] : prev;
+
+ return value
+ ? Array.from(new Set([id, ...base]))
+ : base.filter((x) => x !== id);
+ });
+ }}
+ onSelect={(event) => event.preventDefault()}
+ >
+ {columnPickerLabel(column)}
+
+ );
+ })}
+
+
+ ) : null}
-
- Page {pagination.pageIndex + 1} of {table.getPageCount()}
-
+ {pageCount > 0 ? (
+
+ Page {safePageIndex + 1} of {pageCount}
+
+ ) : (
+
+ )}
-
- {
* (re-pulling a file the user already has) without an extra REST round-trip.
* Nested conflicts (the user pulls `/etc/` while only `/etc/nginx.conf` is
* cached) still surface server-side as a 409 and are auto-redialed by the
- * caller through the same `OverwriteConfirmDialog` flow.
+ * caller through the same `OverwriteDialog` flow.
*/
export const findPullConflicts = (
pullTargets: readonly string[],
diff --git a/frontend/src/features/flows/files/flow-files-promote-dialog.tsx b/frontend/src/features/flows/files/flow-files-promote-dialog.tsx
index f6f6e3b8..7c1f2c1e 100644
--- a/frontend/src/features/flows/files/flow-files-promote-dialog.tsx
+++ b/frontend/src/features/flows/files/flow-files-promote-dialog.tsx
@@ -4,11 +4,9 @@ import { useEffect, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import type { FileNode } from '@/components/shared/file-manager';
-import type { OverwriteConflict } from '@/components/shared/overwrite-confirm-dialog';
+import type { OverwriteConflict } from '@/components/shared/overwrite';
-import { OverwriteConfirmDialog } from '@/components/shared/overwrite-confirm-dialog';
-import { OverwriteCtaButtons } from '@/components/shared/overwrite-cta-buttons';
-import { useOverwriteAction } from '@/components/shared/use-overwrite-action';
+import { OverwriteButtons, OverwriteDialog, useOverwrite } from '@/components/shared/overwrite';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
@@ -139,7 +137,7 @@ const FlowFilesPromoteDialogForm = ({ files, flowId, onClose }: FlowFilesPromote
* with a single atomic batch request. Backend handles `sources[]` in one
* DB transaction (all-or-nothing) — no per-source aggregation needed here.
*/
- const overwriteAction = useOverwriteAction({
+ const overwriteAction = useOverwrite({
execute: (plan, force) => promote(plan.sources, plan.destination, force),
// Local preflight against the resource library snapshot — flags the
// exact destinations already taken so the dialog can name them.
@@ -236,7 +234,7 @@ const FlowFilesPromoteDialogForm = ({ files, flowId, onClose }: FlowFilesPromote
>
Cancel
- {
@@ -252,7 +250,7 @@ const FlowFilesPromoteDialogForm = ({ files, flowId, onClose }: FlowFilesPromote
- {
* is open so closing it discards every transient field without an imperative reset.
*
* The actual overwrite orchestration (preflight → execute → ConflictDialog
- * fallback) is delegated to {@link useOverwriteAction}; this component only
+ * fallback) is delegated to {@link useOverwrite}; this component only
* owns the listing browser UI and the per-action plan derivation.
*/
const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: FlowFilesPullDialogFormProps) => {
@@ -169,7 +167,7 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
* close-on-success — this dialog just provides the plan (paths) and the
* three pure helpers (find / execute / synthesize).
*/
- const overwriteAction = useOverwriteAction({
+ const overwriteAction = useOverwrite({
execute: (paths, force) => pull(paths, force),
findConflicts: (paths) => findPullConflicts(paths, cachedFiles),
onSuccess: onClose,
@@ -430,7 +428,7 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
>
Cancel
- {
@@ -446,7 +444,7 @@ const FlowFilesPullDialogForm = ({ cachedFiles, flowId, onClose, onSuccess }: Fl
- ('templates');
const fileInputRef = useRef(null);
@@ -323,6 +330,158 @@ export const FlowForm = ({
}
}, [pendingTemplate, setValue]);
+ // Templates and resources share the same dropdown via tabs — both picker
+ // bodies are kept as render functions so each can be mounted directly
+ // inside its `` without duplicating the search-input +
+ // scrolled-list layout.
+ const renderTemplatePickerInner = () => (
+ <>
+
+
+ setTemplateSearch(event.target.value)}
+ onClick={(event) => event.stopPropagation()}
+ onKeyDown={(event) => event.stopPropagation()}
+ placeholder="Search..."
+ value={templateSearch}
+ />
+ {templateSearch && (
+
+ {
+ event.stopPropagation();
+ setTemplateSearch('');
+ }}
+ >
+
+
+
+ )}
+
+
+
+
+ {!filteredTemplates.length ? (
+
+ {templateSearch ? 'No results found' : 'No available templates'}
+
+ ) : (
+ filteredTemplates.map((template) => (
+ {
+ if (isFormDisabled) {
+ return;
+ }
+
+ handleApplyTemplate(template);
+ }}
+ >
+ {template.title}
+
+ ))
+ )}
+
+ >
+ );
+
+ const renderResourcePickerInner = () => (
+ <>
+
+
+ setResourceSearch(event.target.value)}
+ onClick={(event) => event.stopPropagation()}
+ onKeyDown={(event) => event.stopPropagation()}
+ placeholder="Search..."
+ value={resourceSearch}
+ />
+ {resourceSearch && (
+
+ {
+ event.stopPropagation();
+ setResourceSearch('');
+ }}
+ >
+
+
+
+ )}
+
+
+
+
+ {!filteredResources.length ? (
+
+ {resourceSearch ? 'No results found' : 'No available resources'}
+
+ ) : (
+ filteredResources.map((resource) => {
+ const resourceId = String(resource.id);
+ const isSelected = resourceIds.includes(resourceId);
+ const Icon = resource.isDir ? Folder : FileText;
+ // Depth derived from the path's slash count; ignored while a
+ // search query is active so matches don't appear orphaned
+ // beneath hidden ancestors.
+ const depth = isResourceSearchActive ? 0 : resource.path.split('/').length - 1;
+
+ return (
+ {
+ event.preventDefault();
+
+ if (isFormDisabled) {
+ return;
+ }
+
+ handleToggleAttachment(resourceId);
+ }}
+ style={{ paddingLeft: `${0.5 + depth * 0.875}rem` }}
+ >
+
+
+
+ {resource.name}
+ {isResourceSearchActive && resource.path !== resource.name && (
+
+ {resource.path}
+
+ )}
+
+ {isSelected &&
}
+
+
+ );
+ })
+ )}
+
+
+ {
+ event.preventDefault();
+
+ if (isFormDisabled) {
+ return;
+ }
+
+ handleAttachClick();
+ }}
+ >
+ {upload.isUploading ? : }
+ {upload.isUploading ? 'Uploading…' : 'Upload files'}
+
+ >
+ );
+
return (