From c4ab2965f7536efb24b3d0473b6ff18baf4c4da4 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 25 Nov 2025 08:45:33 -0800 Subject: [PATCH] Scrollable tables, specify multi-part filename, fix required prop in prompt, better tab padding --- src-tauri/capabilities/default.json | 1 + src-tauri/src/http_request.rs | 15 ++- src-web/commands/commands.tsx | 110 ++++++++++-------- src-web/components/FormMultipartEditor.tsx | 2 + src-web/components/SelectFile.tsx | 6 +- src-web/components/Settings/Settings.tsx | 12 +- .../components/Settings/SettingsPlugins.tsx | 6 +- src-web/components/core/Dialog.tsx | 16 +-- src-web/components/core/PairEditor.tsx | 43 ++++++- src-web/components/core/Table.tsx | 48 ++++++-- src-web/components/core/Tabs/Tabs.tsx | 2 +- src-web/components/git/GitDropdown.tsx | 1 + src-web/components/git/GitRemotesDialog.tsx | 72 ++++++------ src-web/components/git/HistoryDialog.tsx | 48 ++++---- src-web/lib/confirm.ts | 5 +- src-web/lib/prompt.ts | 2 + 16 files changed, 245 insertions(+), 144 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index ae959bbf..5132668c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -10,6 +10,7 @@ "core:event:allow-listen", "core:event:allow-unlisten", "core:path:allow-resolve-directory", + "core:path:allow-basename", "os:allow-os-type", "clipboard-manager:allow-clear", "clipboard-manager:allow-write-text", diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index fa715f6d..b2c1da93 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -398,11 +398,16 @@ pub async fn send_http_request_with_context( // Set a file path if it is not empty if !file_path.is_empty() { - let filename = PathBuf::from(file_path) - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); + let user_filename = get_str(p, "filename").to_owned(); + let filename = if user_filename.is_empty() { + PathBuf::from(file_path) + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string() + } else { + user_filename + }; part = part.file_name(filename); } diff --git a/src-web/commands/commands.tsx b/src-web/commands/commands.tsx index c75d383a..979a9ba4 100644 --- a/src-web/commands/commands.tsx +++ b/src-web/commands/commands.tsx @@ -1,9 +1,16 @@ -import type { Folder } from '@yaakapp-internal/models'; -import { createWorkspaceModel } from '@yaakapp-internal/models'; +import { createWorkspaceModel, type Folder, modelTypeLabel } from '@yaakapp-internal/models'; import { applySync, calculateSync } from '@yaakapp-internal/sync'; import { Banner } from '../components/core/Banner'; import { InlineCode } from '../components/core/InlineCode'; -import { VStack } from '../components/core/Stacks'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeaderCell, + TableRow, + TruncatedWideTableCell, +} from '../components/core/Table'; import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { createFastMutation } from '../hooks/useFastMutation'; import { showConfirm } from '../lib/confirm'; @@ -76,63 +83,72 @@ export const syncWorkspace = createFastMutation< : await showConfirm({ id: 'commit-sync', title: 'Changes Detected', + size: 'md', confirmText: 'Apply Changes', color: isDeletingWorkspace ? 'danger' : 'primary', description: ( - - {isDeletingWorkspace && ( +
+ {isDeletingWorkspace ? ( 🚨 Changes contain a workspace deletion! + ) : ( + )}

{pluralizeCount('file', dbOps.length)} in the directory{' '} {dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?

-
- - - - - - - - - {dbOps.map((op, i) => { - let name = ''; - let label = ''; - let color = ''; +
NameOperation
+ + + Type + Name + Operation + + + + {dbOps.map((op, i) => { + let name: string; + let label: string; + let color: string; + let model: string; - if (op.type === 'dbCreate') { - label = 'create'; - name = resolvedModelNameWithFolders(op.fs.model); - color = 'text-success'; - } else if (op.type === 'dbUpdate') { - label = 'update'; - name = resolvedModelNameWithFolders(op.fs.model); - color = 'text-info'; - } else if (op.type === 'dbDelete') { - label = 'delete'; - name = resolvedModelNameWithFolders(op.model); - color = 'text-danger'; - } else { - return null; - } + if (op.type === 'dbCreate') { + label = 'create'; + name = resolvedModelNameWithFolders(op.fs.model); + color = 'text-success'; + model = modelTypeLabel(op.fs.model); + } else if (op.type === 'dbUpdate') { + label = 'update'; + name = resolvedModelNameWithFolders(op.fs.model); + color = 'text-info'; + model = modelTypeLabel(op.fs.model); + } else if (op.type === 'dbDelete') { + label = 'delete'; + name = resolvedModelNameWithFolders(op.model); + color = 'text-danger'; + model = modelTypeLabel(op.model); + } else { + return null; + } - return ( - // biome-ignore lint/suspicious/noArrayIndexKey: none - - - - - ); - })} - -
{name} - {label} -
-
- + return ( + // biome-ignore lint/suspicious/noArrayIndexKey: none + + {model} + + {name} + + + {label} + + + ); + })} + + +
), }); if (confirmed) { diff --git a/src-web/components/FormMultipartEditor.tsx b/src-web/components/FormMultipartEditor.tsx index 9bed56f1..c2611d26 100644 --- a/src-web/components/FormMultipartEditor.tsx +++ b/src-web/components/FormMultipartEditor.tsx @@ -17,6 +17,7 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props name: p.name, value: p.file ?? p.value, contentType: p.contentType, + filename: p.filename, isFile: !!p.file, id: p.id, })), @@ -30,6 +31,7 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props enabled: p.enabled, name: p.name, contentType: p.contentType, + filename: p.filename, file: p.isFile ? p.value : undefined, value: p.isFile ? undefined : p.value, id: p.id, diff --git a/src-web/components/SelectFile.tsx b/src-web/components/SelectFile.tsx index 8e59adcd..0b86e7d0 100644 --- a/src-web/components/SelectFile.tsx +++ b/src-web/components/SelectFile.tsx @@ -14,6 +14,7 @@ import { HStack } from './core/Stacks'; type Props = Omit & { onChange: (value: { filePath: string | null; contentType: string | null }) => void; filePath: string | null; + nameOverride?: string | null; directory?: boolean; inline?: boolean; noun?: string; @@ -31,6 +32,7 @@ export function SelectFile({ className, directory, noun, + nameOverride, size = 'sm', label, help, @@ -88,6 +90,8 @@ export function SelectFile({ }; }, [isHovering, onChange]); + const filePathWithNameOverride = nameOverride ? `${filePath} (${nameOverride})` : filePath; + return (
{label && ( @@ -110,7 +114,7 @@ export function SelectFile({ {...props} > {rtlEscapeChar} - {inline ? filePath || selectOrChange : selectOrChange} + {inline ? filePathWithNameOverride || selectOrChange : selectOrChange} {!inline && ( diff --git a/src-web/components/Settings/Settings.tsx b/src-web/components/Settings/Settings.tsx index 99b28206..a9b8ae55 100644 --- a/src-web/components/Settings/Settings.tsx +++ b/src-web/components/Settings/Settings.tsx @@ -82,22 +82,22 @@ export default function Settings({ hide }: Props) { }), )} > - + - + - + - + - + - + diff --git a/src-web/components/Settings/SettingsPlugins.tsx b/src-web/components/Settings/SettingsPlugins.tsx index 5508ed00..22f2fb53 100644 --- a/src-web/components/Settings/SettingsPlugins.tsx +++ b/src-web/components/Settings/SettingsPlugins.tsx @@ -242,7 +242,7 @@ function PluginSearch() { defaultValue={query} /> -
+
{results.data == null ? ( @@ -250,7 +250,7 @@ function PluginSearch() { ) : (results.data.plugins ?? []).length === 0 ? ( No plugins found ) : ( - +
Name @@ -282,7 +282,7 @@ function InstalledPlugins() { ) : ( -
+
Name diff --git a/src-web/components/core/Dialog.tsx b/src-web/components/core/Dialog.tsx index f2256419..15b30352 100644 --- a/src-web/components/core/Dialog.tsx +++ b/src-web/components/core/Dialog.tsx @@ -69,7 +69,11 @@ export function Dialog({ animate={{ top: 0, scale: 1 }} className={classNames( className, - 'grid grid-rows-[auto_auto_minmax(0,1fr)]', + 'grid', + title != null && description != null && 'grid-rows-[auto_minmax(0,1fr)_minmax_(0,1fr)]', + title == null && description != null && 'grid-rows-[auto_minmax(0,1fr)]', + title != null && description == null && 'grid-rows-[auto_minmax(0,1fr)]', + title == null && description == null && 'grid-rows-[minmax(0,1fr)]', 'grid-cols-1', // must be here for inline code blocks to correctly break words 'relative bg-surface pointer-events-auto', 'rounded-lg', @@ -83,20 +87,16 @@ export function Dialog({ size === 'dynamic' && 'min-w-[20rem] max-w-[100vw]', )} > - {title ? ( + {title && ( {title} - ) : ( - )} - {description ? ( -
+ {description && ( +
{description}
- ) : ( - )}
(filename: string) => onChange?.({ ...pair, filename }), + [onChange, pair], + ); + const handleEditMultiLineValue = useCallback( () => showDialog({ @@ -614,6 +621,7 @@ export function PairEditorRow({ inline size="xs" filePath={pair.value} + nameOverride={pair.filename || null} onChange={handleChangeValueFile} /> ) : pair.value.includes('\n') ? ( @@ -659,6 +667,7 @@ export function PairEditorRow({ onChangeFile={handleChangeValueFile} onChangeText={handleChangeValueText} onChangeContentType={handleChangeValueContentType} + onChangeFilename={handleChangeValueFilename} onDelete={handleDelete} editMultiLine={handleEditMultiLineValue} /> @@ -687,6 +696,7 @@ function FileActionsDropdown({ onChangeFile, onChangeText, onChangeContentType, + onChangeFilename, onDelete, editMultiLine, }: { @@ -694,6 +704,7 @@ function FileActionsDropdown({ onChangeFile: ({ filePath }: { filePath: string | null }) => void; onChangeText: (text: string) => void; onChangeContentType: (contentType: string) => void; + onChangeFilename: (filename: string) => void; onDelete: () => void; editMultiLine: () => void; }) { @@ -731,6 +742,26 @@ function FileActionsDropdown({ onChangeContentType(contentType); }, }, + { + label: 'Set File Name', + leftSlot: , + onSelect: async () => { + console.log('PAIR', pair); + const defaultFilename = await basename(pair.value ?? ''); + const filename = await showPrompt({ + id: 'filename', + title: 'Override Filename', + label: 'Filename', + required: false, + placeholder: defaultFilename ?? 'myfile.png', + defaultValue: pair.filename, + confirmText: 'Set', + description: 'Leave blank to use the name of the selected file', + }); + if (filename == null) return; + onChangeFilename(filename); + }, + }, { label: 'Unset File', leftSlot: , @@ -747,7 +778,17 @@ function FileActionsDropdown({ color: 'danger', }, ], - [editMultiLine, onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile], + [ + editMultiLine, + onChangeContentType, + onChangeFile, + onDelete, + pair.contentType, + pair.isFile, + onChangeFilename, + pair.filename, + pair, + ], ); return ( diff --git a/src-web/components/core/Table.tsx b/src-web/components/core/Table.tsx index d72e54d8..96e4e89d 100644 --- a/src-web/components/core/Table.tsx +++ b/src-web/components/core/Table.tsx @@ -1,20 +1,50 @@ import classNames from 'classnames'; import type { ReactNode } from 'react'; -export function Table({ children }: { children: ReactNode }) { +export function Table({ + children, + className, + scrollable, +}: { + children: ReactNode; + className?: string; + scrollable?: boolean; +}) { return ( -
- {children} -
+
+ + {children} +
+
); } export function TableBody({ children }: { children: ReactNode }) { - return {children}; + return ( + + {children} + + ); } -export function TableHead({ children }: { children: ReactNode }) { - return {children}; +export function TableHead({ children, className }: { children: ReactNode; className?: string }) { + return ( + + {children} + + ); } export function TableRow({ children }: { children: ReactNode }) { @@ -42,9 +72,7 @@ export function TruncatedWideTableCell({ className?: string; }) { return ( - -
{children}
-
+ {children} ); } diff --git a/src-web/components/core/Tabs/Tabs.tsx b/src-web/components/core/Tabs/Tabs.tsx index 2ed9cfdb..fcb6e366 100644 --- a/src-web/components/core/Tabs/Tabs.tsx +++ b/src-web/components/core/Tabs/Tabs.tsx @@ -87,7 +87,7 @@ export function Tabs({ addBorders && layout === 'horizontal' && 'pl-3 -ml-1', addBorders && layout === 'vertical' && 'ml-0 mb-2', 'flex items-center hide-scrollbars', - layout === 'horizontal' && 'h-full overflow-auto p-2 -mr-2', + layout === 'horizontal' && 'h-full overflow-auto p-2', layout === 'vertical' && 'overflow-x-auto overflow-y-visible ', // Give space for button focus states within overflow boundary. !addBorders && layout === 'vertical' && 'py-1 pl-3 -ml-5 pr-1', diff --git a/src-web/components/git/GitDropdown.tsx b/src-web/components/git/GitDropdown.tsx index dfd4d22f..959a8fa1 100644 --- a/src-web/components/git/GitDropdown.tsx +++ b/src-web/components/git/GitDropdown.tsx @@ -107,6 +107,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { id: 'git-history', size: 'md', title: 'Commit History', + noPadding: true, render: () => , }); }, diff --git a/src-web/components/git/GitRemotesDialog.tsx b/src-web/components/git/GitRemotesDialog.tsx index 4665c31a..53c56300 100644 --- a/src-web/components/git/GitRemotesDialog.tsx +++ b/src-web/components/git/GitRemotesDialog.tsx @@ -15,45 +15,43 @@ export function GitRemotesDialog({ dir }: Props) { const [{ remotes }, { rmRemote }] = useGit(dir, gitCallbacks(dir)); return ( -
- - - - Name - URL - - + + + + + {remotes.data?.map((r) => ( + + {r.name} + {r.url} + + addGitRemote(dir)} - > - Add Remote - - + icon="trash" + title="Remove remote" + onClick={() => rmRemote.mutate({ name: r.name })} + /> + - - - {remotes.data?.map((r) => ( - - {r.name} - {r.url} - - rmRemote.mutate({ name: r.name })} - /> - - - ))} - -
-
+ ))} + + ); } diff --git a/src-web/components/git/HistoryDialog.tsx b/src-web/components/git/HistoryDialog.tsx index db9f6c4b..25d6711a 100644 --- a/src-web/components/git/HistoryDialog.tsx +++ b/src-web/components/git/HistoryDialog.tsx @@ -16,29 +16,31 @@ interface Props { export function HistoryDialog({ log }: Props) { return ( - - - - Message - Author - When - - - - {log.map((l) => ( - - - {l.message || No message} - - - {l.author.name || 'Unknown'} - - - {formatDistanceToNowStrict(l.when)} ago - +
+
+ + + Message + Author + When - ))} - -
+ + + {log.map((l) => ( + + + {l.message || No message} + + + {l.author.name || 'Unknown'} + + + {formatDistanceToNowStrict(l.when)} ago + + + ))} + + +
); } diff --git a/src-web/lib/confirm.ts b/src-web/lib/confirm.ts index cad3031f..bbbbb8be 100644 --- a/src-web/lib/confirm.ts +++ b/src-web/lib/confirm.ts @@ -5,20 +5,21 @@ import { showDialog } from './dialog'; type ConfirmArgs = { id: string; -} & Pick & +} & Pick & Pick; export async function showConfirm({ color, confirmText, requireTyping, + size = 'sm', ...extraProps }: ConfirmArgs) { return new Promise((onResult: ConfirmProps['onResult']) => { showDialog({ ...extraProps, hideX: true, - size: 'sm', + size, disableBackdropClose: true, // Prevent accidental dismisses render: ({ hide }) => Confirm({ onHide: hide, color, onResult, confirmText, requireTyping }), }); diff --git a/src-web/lib/prompt.ts b/src-web/lib/prompt.ts index 70fdd31f..5650ab66 100644 --- a/src-web/lib/prompt.ts +++ b/src-web/lib/prompt.ts @@ -18,11 +18,13 @@ export async function showPrompt({ description, cancelText, confirmText, + required, ...props }: PromptArgs) { const inputs: FormInput[] = [ { ...props, + optional: !required, type: 'text', name: 'value', },