mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 09:48:28 +02:00
Scrollable tables, specify multi-part filename, fix required prop in prompt, better tab padding
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
"core:event:allow-listen",
|
"core:event:allow-listen",
|
||||||
"core:event:allow-unlisten",
|
"core:event:allow-unlisten",
|
||||||
"core:path:allow-resolve-directory",
|
"core:path:allow-resolve-directory",
|
||||||
|
"core:path:allow-basename",
|
||||||
"os:allow-os-type",
|
"os:allow-os-type",
|
||||||
"clipboard-manager:allow-clear",
|
"clipboard-manager:allow-clear",
|
||||||
"clipboard-manager:allow-write-text",
|
"clipboard-manager:allow-write-text",
|
||||||
|
|||||||
@@ -398,11 +398,16 @@ pub async fn send_http_request_with_context<R: Runtime>(
|
|||||||
|
|
||||||
// Set a file path if it is not empty
|
// Set a file path if it is not empty
|
||||||
if !file_path.is_empty() {
|
if !file_path.is_empty() {
|
||||||
let filename = PathBuf::from(file_path)
|
let user_filename = get_str(p, "filename").to_owned();
|
||||||
.file_name()
|
let filename = if user_filename.is_empty() {
|
||||||
.unwrap_or_default()
|
PathBuf::from(file_path)
|
||||||
.to_string_lossy()
|
.file_name()
|
||||||
.to_string();
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
user_filename
|
||||||
|
};
|
||||||
part = part.file_name(filename);
|
part = part.file_name(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import type { Folder } from '@yaakapp-internal/models';
|
import { createWorkspaceModel, type Folder, modelTypeLabel } from '@yaakapp-internal/models';
|
||||||
import { createWorkspaceModel } from '@yaakapp-internal/models';
|
|
||||||
import { applySync, calculateSync } from '@yaakapp-internal/sync';
|
import { applySync, calculateSync } from '@yaakapp-internal/sync';
|
||||||
import { Banner } from '../components/core/Banner';
|
import { Banner } from '../components/core/Banner';
|
||||||
import { InlineCode } from '../components/core/InlineCode';
|
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 { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
|
||||||
import { createFastMutation } from '../hooks/useFastMutation';
|
import { createFastMutation } from '../hooks/useFastMutation';
|
||||||
import { showConfirm } from '../lib/confirm';
|
import { showConfirm } from '../lib/confirm';
|
||||||
@@ -76,63 +83,72 @@ export const syncWorkspace = createFastMutation<
|
|||||||
: await showConfirm({
|
: await showConfirm({
|
||||||
id: 'commit-sync',
|
id: 'commit-sync',
|
||||||
title: 'Changes Detected',
|
title: 'Changes Detected',
|
||||||
|
size: 'md',
|
||||||
confirmText: 'Apply Changes',
|
confirmText: 'Apply Changes',
|
||||||
color: isDeletingWorkspace ? 'danger' : 'primary',
|
color: isDeletingWorkspace ? 'danger' : 'primary',
|
||||||
description: (
|
description: (
|
||||||
<VStack space={3}>
|
<div className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)] gap-3">
|
||||||
{isDeletingWorkspace && (
|
{isDeletingWorkspace ? (
|
||||||
<Banner color="danger">
|
<Banner color="danger">
|
||||||
🚨 <strong>Changes contain a workspace deletion!</strong>
|
🚨 <strong>Changes contain a workspace deletion!</strong>
|
||||||
</Banner>
|
</Banner>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
)}
|
)}
|
||||||
<p>
|
<p>
|
||||||
{pluralizeCount('file', dbOps.length)} in the directory{' '}
|
{pluralizeCount('file', dbOps.length)} in the directory{' '}
|
||||||
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?
|
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?
|
||||||
</p>
|
</p>
|
||||||
<div className="overflow-y-auto max-h-[10rem]">
|
<Table scrollable>
|
||||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
<TableHead>
|
||||||
<thead>
|
<TableRow>
|
||||||
<tr>
|
<TableHeaderCell>Type</TableHeaderCell>
|
||||||
<th className="py-1 text-left">Name</th>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<th className="py-1 text-right pl-4">Operation</th>
|
<TableHeaderCell>Operation</TableHeaderCell>
|
||||||
</tr>
|
</TableRow>
|
||||||
</thead>
|
</TableHead>
|
||||||
<tbody className="divide-y divide-surface-highlight">
|
<TableBody>
|
||||||
{dbOps.map((op, i) => {
|
{dbOps.map((op, i) => {
|
||||||
let name = '';
|
let name: string;
|
||||||
let label = '';
|
let label: string;
|
||||||
let color = '';
|
let color: string;
|
||||||
|
let model: string;
|
||||||
|
|
||||||
if (op.type === 'dbCreate') {
|
if (op.type === 'dbCreate') {
|
||||||
label = 'create';
|
label = 'create';
|
||||||
name = resolvedModelNameWithFolders(op.fs.model);
|
name = resolvedModelNameWithFolders(op.fs.model);
|
||||||
color = 'text-success';
|
color = 'text-success';
|
||||||
} else if (op.type === 'dbUpdate') {
|
model = modelTypeLabel(op.fs.model);
|
||||||
label = 'update';
|
} else if (op.type === 'dbUpdate') {
|
||||||
name = resolvedModelNameWithFolders(op.fs.model);
|
label = 'update';
|
||||||
color = 'text-info';
|
name = resolvedModelNameWithFolders(op.fs.model);
|
||||||
} else if (op.type === 'dbDelete') {
|
color = 'text-info';
|
||||||
label = 'delete';
|
model = modelTypeLabel(op.fs.model);
|
||||||
name = resolvedModelNameWithFolders(op.model);
|
} else if (op.type === 'dbDelete') {
|
||||||
color = 'text-danger';
|
label = 'delete';
|
||||||
} else {
|
name = resolvedModelNameWithFolders(op.model);
|
||||||
return null;
|
color = 'text-danger';
|
||||||
}
|
model = modelTypeLabel(op.model);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||||
<tr key={i} className="text-text">
|
<TableRow key={i}>
|
||||||
<td className="py-1">{name}</td>
|
<TableCell>{model}</TableCell>
|
||||||
<td className="py-1 pl-4 text-right">
|
<TruncatedWideTableCell className="text-text">
|
||||||
<InlineCode className={color}>{label}</InlineCode>
|
{name}
|
||||||
</td>
|
</TruncatedWideTableCell>
|
||||||
</tr>
|
<TableCell className="text-right">
|
||||||
);
|
<InlineCode className={color}>{label}</InlineCode>
|
||||||
})}
|
</TableCell>
|
||||||
</tbody>
|
</TableRow>
|
||||||
</table>
|
);
|
||||||
</div>
|
})}
|
||||||
</VStack>
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props
|
|||||||
name: p.name,
|
name: p.name,
|
||||||
value: p.file ?? p.value,
|
value: p.file ?? p.value,
|
||||||
contentType: p.contentType,
|
contentType: p.contentType,
|
||||||
|
filename: p.filename,
|
||||||
isFile: !!p.file,
|
isFile: !!p.file,
|
||||||
id: p.id,
|
id: p.id,
|
||||||
})),
|
})),
|
||||||
@@ -30,6 +31,7 @@ export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props
|
|||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
contentType: p.contentType,
|
contentType: p.contentType,
|
||||||
|
filename: p.filename,
|
||||||
file: p.isFile ? p.value : undefined,
|
file: p.isFile ? p.value : undefined,
|
||||||
value: p.isFile ? undefined : p.value,
|
value: p.isFile ? undefined : p.value,
|
||||||
id: p.id,
|
id: p.id,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { HStack } from './core/Stacks';
|
|||||||
type Props = Omit<ButtonProps, 'type'> & {
|
type Props = Omit<ButtonProps, 'type'> & {
|
||||||
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
|
onChange: (value: { filePath: string | null; contentType: string | null }) => void;
|
||||||
filePath: string | null;
|
filePath: string | null;
|
||||||
|
nameOverride?: string | null;
|
||||||
directory?: boolean;
|
directory?: boolean;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
noun?: string;
|
noun?: string;
|
||||||
@@ -31,6 +32,7 @@ export function SelectFile({
|
|||||||
className,
|
className,
|
||||||
directory,
|
directory,
|
||||||
noun,
|
noun,
|
||||||
|
nameOverride,
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
label,
|
label,
|
||||||
help,
|
help,
|
||||||
@@ -88,6 +90,8 @@ export function SelectFile({
|
|||||||
};
|
};
|
||||||
}, [isHovering, onChange]);
|
}, [isHovering, onChange]);
|
||||||
|
|
||||||
|
const filePathWithNameOverride = nameOverride ? `${filePath} (${nameOverride})` : filePath;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="w-full">
|
<div ref={ref} className="w-full">
|
||||||
{label && (
|
{label && (
|
||||||
@@ -110,7 +114,7 @@ export function SelectFile({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{rtlEscapeChar}
|
{rtlEscapeChar}
|
||||||
{inline ? filePath || selectOrChange : selectOrChange}
|
{inline ? filePathWithNameOverride || selectOrChange : selectOrChange}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{!inline && (
|
{!inline && (
|
||||||
|
|||||||
@@ -82,22 +82,22 @@ export default function Settings({ hide }: Props) {
|
|||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-8 !py-4">
|
<TabContent value={TAB_GENERAL} className="overflow-y-auto h-full px-6 !py-4">
|
||||||
<SettingsGeneral />
|
<SettingsGeneral />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-8 !py-4">
|
<TabContent value={TAB_INTERFACE} className="overflow-y-auto h-full px-6 !py-4">
|
||||||
<SettingsInterface />
|
<SettingsInterface />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_THEME} className="overflow-y-auto h-full px-8 !py-4">
|
<TabContent value={TAB_THEME} className="overflow-y-auto h-full px-6 !py-4">
|
||||||
<SettingsTheme />
|
<SettingsTheme />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-8 !py-4">
|
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-6 !py-4">
|
||||||
<SettingsPlugins />
|
<SettingsPlugins />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-8 !py-4">
|
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
|
||||||
<SettingsProxy />
|
<SettingsProxy />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-8 !py-4">
|
<TabContent value={TAB_LICENSE} className="overflow-y-auto h-full px-6 !py-4">
|
||||||
<SettingsLicense />
|
<SettingsLicense />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ function PluginSearch() {
|
|||||||
defaultValue={query}
|
defaultValue={query}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<div className="w-full h-full overflow-y-auto">
|
<div className="w-full h-full">
|
||||||
{results.data == null ? (
|
{results.data == null ? (
|
||||||
<EmptyStateText>
|
<EmptyStateText>
|
||||||
<LoadingIcon size="xl" className="text-text-subtlest" />
|
<LoadingIcon size="xl" className="text-text-subtlest" />
|
||||||
@@ -250,7 +250,7 @@ function PluginSearch() {
|
|||||||
) : (results.data.plugins ?? []).length === 0 ? (
|
) : (results.data.plugins ?? []).length === 0 ? (
|
||||||
<EmptyStateText>No plugins found</EmptyStateText>
|
<EmptyStateText>No plugins found</EmptyStateText>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table scrollable>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
@@ -282,7 +282,7 @@ function InstalledPlugins() {
|
|||||||
</EmptyStateText>
|
</EmptyStateText>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table scrollable>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
|
|||||||
@@ -69,7 +69,11 @@ export function Dialog({
|
|||||||
animate={{ top: 0, scale: 1 }}
|
animate={{ top: 0, scale: 1 }}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
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
|
'grid-cols-1', // must be here for inline code blocks to correctly break words
|
||||||
'relative bg-surface pointer-events-auto',
|
'relative bg-surface pointer-events-auto',
|
||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
@@ -83,20 +87,16 @@ export function Dialog({
|
|||||||
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw]',
|
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title ? (
|
{title && (
|
||||||
<Heading className="px-6 mt-4 mb-2" level={1} id={titleId}>
|
<Heading className="px-6 mt-4 mb-2" level={1} id={titleId}>
|
||||||
{title}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
) : (
|
|
||||||
<span aria-hidden />
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{description ? (
|
{description && (
|
||||||
<div className="px-6 text-text-subtle mb-3" id={descriptionId}>
|
<div className="min-h-0 px-6 text-text-subtle mb-3" id={descriptionId}>
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<span aria-hidden />
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
|
import { basename } from '@tauri-apps/api/path';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables';
|
import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables';
|
||||||
@@ -70,6 +71,7 @@ export type Pair = {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
|
filename?: string;
|
||||||
isFile?: boolean;
|
isFile?: boolean;
|
||||||
readOnlyName?: boolean;
|
readOnlyName?: boolean;
|
||||||
};
|
};
|
||||||
@@ -492,6 +494,11 @@ export function PairEditorRow({
|
|||||||
[onChange, pair],
|
[onChange, pair],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeValueFilename = useMemo(
|
||||||
|
() => (filename: string) => onChange?.({ ...pair, filename }),
|
||||||
|
[onChange, pair],
|
||||||
|
);
|
||||||
|
|
||||||
const handleEditMultiLineValue = useCallback(
|
const handleEditMultiLineValue = useCallback(
|
||||||
() =>
|
() =>
|
||||||
showDialog({
|
showDialog({
|
||||||
@@ -614,6 +621,7 @@ export function PairEditorRow({
|
|||||||
inline
|
inline
|
||||||
size="xs"
|
size="xs"
|
||||||
filePath={pair.value}
|
filePath={pair.value}
|
||||||
|
nameOverride={pair.filename || null}
|
||||||
onChange={handleChangeValueFile}
|
onChange={handleChangeValueFile}
|
||||||
/>
|
/>
|
||||||
) : pair.value.includes('\n') ? (
|
) : pair.value.includes('\n') ? (
|
||||||
@@ -659,6 +667,7 @@ export function PairEditorRow({
|
|||||||
onChangeFile={handleChangeValueFile}
|
onChangeFile={handleChangeValueFile}
|
||||||
onChangeText={handleChangeValueText}
|
onChangeText={handleChangeValueText}
|
||||||
onChangeContentType={handleChangeValueContentType}
|
onChangeContentType={handleChangeValueContentType}
|
||||||
|
onChangeFilename={handleChangeValueFilename}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
editMultiLine={handleEditMultiLineValue}
|
editMultiLine={handleEditMultiLineValue}
|
||||||
/>
|
/>
|
||||||
@@ -687,6 +696,7 @@ function FileActionsDropdown({
|
|||||||
onChangeFile,
|
onChangeFile,
|
||||||
onChangeText,
|
onChangeText,
|
||||||
onChangeContentType,
|
onChangeContentType,
|
||||||
|
onChangeFilename,
|
||||||
onDelete,
|
onDelete,
|
||||||
editMultiLine,
|
editMultiLine,
|
||||||
}: {
|
}: {
|
||||||
@@ -694,6 +704,7 @@ function FileActionsDropdown({
|
|||||||
onChangeFile: ({ filePath }: { filePath: string | null }) => void;
|
onChangeFile: ({ filePath }: { filePath: string | null }) => void;
|
||||||
onChangeText: (text: string) => void;
|
onChangeText: (text: string) => void;
|
||||||
onChangeContentType: (contentType: string) => void;
|
onChangeContentType: (contentType: string) => void;
|
||||||
|
onChangeFilename: (filename: string) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
editMultiLine: () => void;
|
editMultiLine: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -731,6 +742,26 @@ function FileActionsDropdown({
|
|||||||
onChangeContentType(contentType);
|
onChangeContentType(contentType);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Set File Name',
|
||||||
|
leftSlot: <Icon icon="file_code" />,
|
||||||
|
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',
|
label: 'Unset File',
|
||||||
leftSlot: <Icon icon="x" />,
|
leftSlot: <Icon icon="x" />,
|
||||||
@@ -747,7 +778,17 @@ function FileActionsDropdown({
|
|||||||
color: 'danger',
|
color: 'danger',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[editMultiLine, onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
|
[
|
||||||
|
editMultiLine,
|
||||||
|
onChangeContentType,
|
||||||
|
onChangeFile,
|
||||||
|
onDelete,
|
||||||
|
pair.contentType,
|
||||||
|
pair.isFile,
|
||||||
|
onChangeFilename,
|
||||||
|
pair.filename,
|
||||||
|
pair,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,20 +1,50 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export function Table({ children }: { children: ReactNode }) {
|
export function Table({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
scrollable,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
scrollable?: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
<div className={classNames('w-full', scrollable && 'h-full overflow-y-auto')}>
|
||||||
{children}
|
<table
|
||||||
</table>
|
className={classNames(
|
||||||
|
className,
|
||||||
|
'w-full text-sm mb-auto min-w-full max-w-full',
|
||||||
|
'border-separate border-spacing-0',
|
||||||
|
scrollable && '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableBody({ children }: { children: ReactNode }) {
|
export function TableBody({ children }: { children: ReactNode }) {
|
||||||
return <tbody className="divide-y divide-surface-highlight">{children}</tbody>;
|
return (
|
||||||
|
<tbody className="[&>tr:not(:last-child)>td]:border-b [&>tr:not(:last-child)>td]:border-b-surface-highlight">
|
||||||
|
{children}
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableHead({ children }: { children: ReactNode }) {
|
export function TableHead({ children, className }: { children: ReactNode; className?: string }) {
|
||||||
return <thead>{children}</thead>;
|
return (
|
||||||
|
<thead
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
'bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableRow({ children }: { children: ReactNode }) {
|
export function TableRow({ children }: { children: ReactNode }) {
|
||||||
@@ -42,9 +72,7 @@ export function TruncatedWideTableCell({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TableCell className={classNames(className, 'w-full relative')}>
|
<TableCell className={classNames(className, 'truncate max-w-0 w-full')}>{children}</TableCell>
|
||||||
<div className="absolute inset-0 py-2 truncate">{children}</div>
|
|
||||||
</TableCell>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function Tabs({
|
|||||||
addBorders && layout === 'horizontal' && 'pl-3 -ml-1',
|
addBorders && layout === 'horizontal' && 'pl-3 -ml-1',
|
||||||
addBorders && layout === 'vertical' && 'ml-0 mb-2',
|
addBorders && layout === 'vertical' && 'ml-0 mb-2',
|
||||||
'flex items-center hide-scrollbars',
|
'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 ',
|
layout === 'vertical' && 'overflow-x-auto overflow-y-visible ',
|
||||||
// Give space for button focus states within overflow boundary.
|
// Give space for button focus states within overflow boundary.
|
||||||
!addBorders && layout === 'vertical' && 'py-1 pl-3 -ml-5 pr-1',
|
!addBorders && layout === 'vertical' && 'py-1 pl-3 -ml-5 pr-1',
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
|
|||||||
id: 'git-history',
|
id: 'git-history',
|
||||||
size: 'md',
|
size: 'md',
|
||||||
title: 'Commit History',
|
title: 'Commit History',
|
||||||
|
noPadding: true,
|
||||||
render: () => <HistoryDialog log={log.data ?? []} />,
|
render: () => <HistoryDialog log={log.data ?? []} />,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,45 +15,43 @@ export function GitRemotesDialog({ dir }: Props) {
|
|||||||
const [{ remotes }, { rmRemote }] = useGit(dir, gitCallbacks(dir));
|
const [{ remotes }, { rmRemote }] = useGit(dir, gitCallbacks(dir));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Table scrollable>
|
||||||
<Table>
|
<TableHead>
|
||||||
<TableHead>
|
<TableRow>
|
||||||
<TableRow>
|
<TableHeaderCell>Name</TableHeaderCell>
|
||||||
<TableHeaderCell>Name</TableHeaderCell>
|
<TableHeaderCell>URL</TableHeaderCell>
|
||||||
<TableHeaderCell>URL</TableHeaderCell>
|
<TableHeaderCell>
|
||||||
<TableHeaderCell>
|
<Button
|
||||||
<Button
|
className="text-text-subtle ml-auto"
|
||||||
|
size="2xs"
|
||||||
|
color="primary"
|
||||||
|
title="Add remote"
|
||||||
|
variant="border"
|
||||||
|
onClick={() => addGitRemote(dir)}
|
||||||
|
>
|
||||||
|
Add Remote
|
||||||
|
</Button>
|
||||||
|
</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{remotes.data?.map((r) => (
|
||||||
|
<TableRow key={r.name + r.url}>
|
||||||
|
<TableCell>{r.name}</TableCell>
|
||||||
|
<TableCell>{r.url}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
className="text-text-subtle ml-auto"
|
className="text-text-subtle ml-auto"
|
||||||
size="2xs"
|
icon="trash"
|
||||||
color="primary"
|
title="Remove remote"
|
||||||
title="Add remote"
|
onClick={() => rmRemote.mutate({ name: r.name })}
|
||||||
variant="border"
|
/>
|
||||||
onClick={() => addGitRemote(dir)}
|
</TableCell>
|
||||||
>
|
|
||||||
Add Remote
|
|
||||||
</Button>
|
|
||||||
</TableHeaderCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
))}
|
||||||
<TableBody>
|
</TableBody>
|
||||||
{remotes.data?.map((r) => (
|
</Table>
|
||||||
<TableRow key={r.name + r.url}>
|
|
||||||
<TableCell>{r.name}</TableCell>
|
|
||||||
<TableCell>{r.url}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
className="text-text-subtle ml-auto"
|
|
||||||
icon="trash"
|
|
||||||
title="Remove remote"
|
|
||||||
onClick={() => rmRemote.mutate({ name: r.name })}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,29 +16,31 @@ interface Props {
|
|||||||
|
|
||||||
export function HistoryDialog({ log }: Props) {
|
export function HistoryDialog({ log }: Props) {
|
||||||
return (
|
return (
|
||||||
<Table>
|
<div className="pl-5 pr-1 pb-1">
|
||||||
<TableHead>
|
<Table scrollable className="px-1">
|
||||||
<TableRow>
|
<TableHead>
|
||||||
<TableHeaderCell>Message</TableHeaderCell>
|
<TableRow>
|
||||||
<TableHeaderCell>Author</TableHeaderCell>
|
<TableHeaderCell>Message</TableHeaderCell>
|
||||||
<TableHeaderCell>When</TableHeaderCell>
|
<TableHeaderCell>Author</TableHeaderCell>
|
||||||
</TableRow>
|
<TableHeaderCell>When</TableHeaderCell>
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{log.map((l) => (
|
|
||||||
<TableRow key={l.author + (l.message ?? 'n/a') + l.when}>
|
|
||||||
<TruncatedWideTableCell>
|
|
||||||
{l.message || <em className="text-text-subtle">No message</em>}
|
|
||||||
</TruncatedWideTableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span title={`Email: ${l.author.email}`}>{l.author.name || 'Unknown'}</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-text-subtle">
|
|
||||||
<span title={l.when}>{formatDistanceToNowStrict(l.when)} ago</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHead>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{log.map((l) => (
|
||||||
|
<TableRow key={l.author + (l.message ?? 'n/a') + l.when}>
|
||||||
|
<TruncatedWideTableCell>
|
||||||
|
{l.message || <em className="text-text-subtle">No message</em>}
|
||||||
|
</TruncatedWideTableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span title={`Email: ${l.author.email}`}>{l.author.name || 'Unknown'}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-text-subtle">
|
||||||
|
<span title={l.when}>{formatDistanceToNowStrict(l.when)} ago</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,21 @@ import { showDialog } from './dialog';
|
|||||||
|
|
||||||
type ConfirmArgs = {
|
type ConfirmArgs = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Pick<DialogProps, 'title' | 'description'> &
|
} & Pick<DialogProps, 'title' | 'description' | 'size'> &
|
||||||
Pick<ConfirmProps, 'color' | 'confirmText' | 'requireTyping'>;
|
Pick<ConfirmProps, 'color' | 'confirmText' | 'requireTyping'>;
|
||||||
|
|
||||||
export async function showConfirm({
|
export async function showConfirm({
|
||||||
color,
|
color,
|
||||||
confirmText,
|
confirmText,
|
||||||
requireTyping,
|
requireTyping,
|
||||||
|
size = 'sm',
|
||||||
...extraProps
|
...extraProps
|
||||||
}: ConfirmArgs) {
|
}: ConfirmArgs) {
|
||||||
return new Promise((onResult: ConfirmProps['onResult']) => {
|
return new Promise((onResult: ConfirmProps['onResult']) => {
|
||||||
showDialog({
|
showDialog({
|
||||||
...extraProps,
|
...extraProps,
|
||||||
hideX: true,
|
hideX: true,
|
||||||
size: 'sm',
|
size,
|
||||||
disableBackdropClose: true, // Prevent accidental dismisses
|
disableBackdropClose: true, // Prevent accidental dismisses
|
||||||
render: ({ hide }) => Confirm({ onHide: hide, color, onResult, confirmText, requireTyping }),
|
render: ({ hide }) => Confirm({ onHide: hide, color, onResult, confirmText, requireTyping }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ export async function showPrompt({
|
|||||||
description,
|
description,
|
||||||
cancelText,
|
cancelText,
|
||||||
confirmText,
|
confirmText,
|
||||||
|
required,
|
||||||
...props
|
...props
|
||||||
}: PromptArgs) {
|
}: PromptArgs) {
|
||||||
const inputs: FormInput[] = [
|
const inputs: FormInput[] = [
|
||||||
{
|
{
|
||||||
...props,
|
...props,
|
||||||
|
optional: !required,
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'value',
|
name: 'value',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user