mirror of
https://github.com/mountain-loop/yaak.git
synced 2025-12-05 19:17:44 -06:00
Compare commits
3 Commits
0cad8f69e2
...
8b851d4685
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b851d4685 | ||
|
|
20e1b5c00e | ||
|
|
c4ab2965f7 |
@@ -33,9 +33,10 @@ export const plugin: PluginDefinition = {
|
||||
name: 'token',
|
||||
type: 'text',
|
||||
label: '1Password Service Account Token',
|
||||
description: '',
|
||||
description:
|
||||
'Token can be generated from the 1Password website by visiting Developer > Service Accounts',
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Yaak template syntax
|
||||
defaultValue: '${[ONEPASSWORD_TOKEN]}',
|
||||
defaultValue: '${[1PASSWORD_TOKEN]}',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -398,11 +398,16 @@ pub async fn send_http_request_with_context<R: Runtime>(
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
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 { Button } from '../components/core/Button';
|
||||
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';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { jotaiStore } from '../lib/jotai';
|
||||
import { pluralizeCount } from '../lib/pluralize';
|
||||
import { showPrompt } from '../lib/prompt';
|
||||
@@ -71,72 +79,92 @@ export const syncWorkspace = createFastMutation<
|
||||
|
||||
console.log('Directory changes detected', { dbOps, ops });
|
||||
|
||||
const confirmed = force
|
||||
? true
|
||||
: await showConfirm({
|
||||
id: 'commit-sync',
|
||||
title: 'Changes Detected',
|
||||
confirmText: 'Apply Changes',
|
||||
color: isDeletingWorkspace ? 'danger' : 'primary',
|
||||
description: (
|
||||
<VStack space={3}>
|
||||
{isDeletingWorkspace && (
|
||||
<Banner color="danger">
|
||||
🚨 <strong>Changes contain a workspace deletion!</strong>
|
||||
</Banner>
|
||||
)}
|
||||
<p>
|
||||
{pluralizeCount('file', dbOps.length)} in the directory{' '}
|
||||
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?
|
||||
</p>
|
||||
<div className="overflow-y-auto max-h-[10rem]">
|
||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-1 text-left">Name</th>
|
||||
<th className="py-1 text-right pl-4">Operation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-highlight">
|
||||
{dbOps.map((op, i) => {
|
||||
let name = '';
|
||||
let label = '';
|
||||
let color = '';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||
<tr key={i} className="text-text">
|
||||
<td className="py-1">{name}</td>
|
||||
<td className="py-1 pl-4 text-right">
|
||||
<InlineCode className={color}>{label}</InlineCode>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
});
|
||||
if (confirmed) {
|
||||
if (force) {
|
||||
await applySync(workspaceId, syncDir, ops);
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog({
|
||||
id: 'commit-sync',
|
||||
title: 'Changes Detected',
|
||||
size: 'md',
|
||||
render: ({ hide }) => (
|
||||
<form
|
||||
className="h-full grid grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-3"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await applySync(workspaceId, syncDir, ops);
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{isDeletingWorkspace ? (
|
||||
<Banner color="danger">
|
||||
🚨 <strong>Changes contain a workspace deletion!</strong>
|
||||
</Banner>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<p>
|
||||
{pluralizeCount('file', dbOps.length)} in the directory{' '}
|
||||
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?
|
||||
</p>
|
||||
<Table scrollable className="my-4">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Type</TableHeaderCell>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Operation</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{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';
|
||||
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
|
||||
<TableRow key={i}>
|
||||
<TableCell className="text-text-subtle">{model}</TableCell>
|
||||
<TruncatedWideTableCell>{name}</TruncatedWideTableCell>
|
||||
<TableCell className="text-right">
|
||||
<InlineCode className={color}>{label}</InlineCode>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<footer className="py-3 flex flex-row-reverse items-center gap-3">
|
||||
<Button type="submit" color="primary">
|
||||
Apply Changes
|
||||
</Button>
|
||||
<Button onClick={hide} color="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</footer>
|
||||
</form>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { HStack } from './core/Stacks';
|
||||
type Props = Omit<ButtonProps, 'type'> & {
|
||||
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 (
|
||||
<div ref={ref} className="w-full">
|
||||
{label && (
|
||||
@@ -110,7 +114,7 @@ export function SelectFile({
|
||||
{...props}
|
||||
>
|
||||
{rtlEscapeChar}
|
||||
{inline ? filePath || selectOrChange : selectOrChange}
|
||||
{inline ? filePathWithNameOverride || selectOrChange : selectOrChange}
|
||||
</Button>
|
||||
|
||||
{!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 />
|
||||
</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 />
|
||||
</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 />
|
||||
</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 />
|
||||
</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 />
|
||||
</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 />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -242,7 +242,7 @@ function PluginSearch() {
|
||||
defaultValue={query}
|
||||
/>
|
||||
</HStack>
|
||||
<div className="w-full h-full overflow-y-auto">
|
||||
<div className="w-full h-full">
|
||||
{results.data == null ? (
|
||||
<EmptyStateText>
|
||||
<LoadingIcon size="xl" className="text-text-subtlest" />
|
||||
@@ -250,7 +250,7 @@ function PluginSearch() {
|
||||
) : (results.data.plugins ?? []).length === 0 ? (
|
||||
<EmptyStateText>No plugins found</EmptyStateText>
|
||||
) : (
|
||||
<Table>
|
||||
<Table scrollable>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
@@ -282,7 +282,7 @@ function InstalledPlugins() {
|
||||
</EmptyStateText>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<Table scrollable>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
|
||||
@@ -190,7 +190,7 @@ function InitializedTemplateFunctionDialog({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 border-t border-t-border py-3 bg-surface-highlight w-full flex flex-col gap-4">
|
||||
<div className="px-6 border-t border-t-border pt-3 pb-6 bg-surface-highlight w-full flex flex-col gap-4">
|
||||
{previewType !== 'none' ? (
|
||||
<div className="w-full grid grid-cols-1 grid-rows-[auto_auto]">
|
||||
<HStack space={0.5}>
|
||||
|
||||
@@ -88,15 +88,15 @@ export function Dialog({
|
||||
{title}
|
||||
</Heading>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
<span />
|
||||
)}
|
||||
|
||||
{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}
|
||||
</div>
|
||||
) : (
|
||||
<span aria-hidden />
|
||||
<span />
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -92,11 +92,12 @@ function templateTags(
|
||||
let option = options.find(
|
||||
(o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)),
|
||||
);
|
||||
|
||||
if (option == null) {
|
||||
const from = node.from; // Cache here so the reference doesn't change
|
||||
option = {
|
||||
invalid: true,
|
||||
type: 'variable',
|
||||
invalid: true,
|
||||
name: inner,
|
||||
value: null,
|
||||
label: inner,
|
||||
@@ -106,20 +107,20 @@ function templateTags(
|
||||
};
|
||||
}
|
||||
|
||||
let invalid = false;
|
||||
if (option.type === 'function') {
|
||||
const tokens = parseTemplate(rawTag);
|
||||
const values = collectArgumentValues(tokens, option);
|
||||
for (const arg of option.args) {
|
||||
if (!('optional' in arg)) continue;
|
||||
if (!arg.optional && values[arg.name] == null) {
|
||||
invalid = true;
|
||||
// Clone so we don't mutate the original
|
||||
option = { ...option, invalid: true };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new TemplateTagWidget({ ...option, invalid }, rawTag, node.from);
|
||||
const widget = new TemplateTagWidget(option, rawTag, node.from);
|
||||
const deco = Decoration.replace({ widget, inclusive: true });
|
||||
widgets.push(deco.range(node.from, node.to));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { basename } from '@tauri-apps/api/path';
|
||||
import classNames from 'classnames';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables';
|
||||
@@ -70,6 +71,7 @@ export type Pair = {
|
||||
name: string;
|
||||
value: string;
|
||||
contentType?: string;
|
||||
filename?: string;
|
||||
isFile?: boolean;
|
||||
readOnlyName?: boolean;
|
||||
};
|
||||
@@ -492,6 +494,11 @@ export function PairEditorRow({
|
||||
[onChange, pair],
|
||||
);
|
||||
|
||||
const handleChangeValueFilename = useMemo(
|
||||
() => (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: <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',
|
||||
leftSlot: <Icon icon="x" />,
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
{children}
|
||||
</table>
|
||||
<div className={classNames('w-full', scrollable && 'h-full overflow-y-auto')}>
|
||||
<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 }) {
|
||||
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 }) {
|
||||
return <thead>{children}</thead>;
|
||||
export function TableHead({ children, className }: { children: ReactNode; className?: string }) {
|
||||
return (
|
||||
<thead
|
||||
className={classNames(
|
||||
className,
|
||||
'bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableRow({ children }: { children: ReactNode }) {
|
||||
@@ -42,9 +72,7 @@ export function TruncatedWideTableCell({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<TableCell className={classNames(className, 'w-full relative')}>
|
||||
<div className="absolute inset-0 py-2 truncate">{children}</div>
|
||||
</TableCell>
|
||||
<TableCell className={classNames(className, 'truncate max-w-0 w-full')}>{children}</TableCell>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -107,6 +107,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
|
||||
id: 'git-history',
|
||||
size: 'md',
|
||||
title: 'Commit History',
|
||||
noPadding: true,
|
||||
render: () => <HistoryDialog log={log.data ?? []} />,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -15,45 +15,43 @@ export function GitRemotesDialog({ dir }: Props) {
|
||||
const [{ remotes }, { rmRemote }] = useGit(dir, gitCallbacks(dir));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>URL</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
<Button
|
||||
<Table scrollable>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>URL</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
<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"
|
||||
size="2xs"
|
||||
color="primary"
|
||||
title="Add remote"
|
||||
variant="border"
|
||||
onClick={() => addGitRemote(dir)}
|
||||
>
|
||||
Add Remote
|
||||
</Button>
|
||||
</TableHeaderCell>
|
||||
icon="trash"
|
||||
title="Remove remote"
|
||||
onClick={() => rmRemote.mutate({ name: r.name })}
|
||||
/>
|
||||
</TableCell>
|
||||
</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"
|
||||
icon="trash"
|
||||
title="Remove remote"
|
||||
onClick={() => rmRemote.mutate({ name: r.name })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,29 +16,31 @@ interface Props {
|
||||
|
||||
export function HistoryDialog({ log }: Props) {
|
||||
return (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Message</TableHeaderCell>
|
||||
<TableHeaderCell>Author</TableHeaderCell>
|
||||
<TableHeaderCell>When</TableHeaderCell>
|
||||
</TableRow>
|
||||
</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>
|
||||
<div className="pl-5 pr-1 pb-1">
|
||||
<Table scrollable className="px-1">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Message</TableHeaderCell>
|
||||
<TableHeaderCell>Author</TableHeaderCell>
|
||||
<TableHeaderCell>When</TableHeaderCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,21 @@ import { showDialog } from './dialog';
|
||||
|
||||
type ConfirmArgs = {
|
||||
id: string;
|
||||
} & Pick<DialogProps, 'title' | 'description'> &
|
||||
} & Pick<DialogProps, 'title' | 'description' | 'size'> &
|
||||
Pick<ConfirmProps, 'color' | 'confirmText' | 'requireTyping'>;
|
||||
|
||||
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 }),
|
||||
});
|
||||
|
||||
@@ -18,11 +18,13 @@ export async function showPrompt({
|
||||
description,
|
||||
cancelText,
|
||||
confirmText,
|
||||
required,
|
||||
...props
|
||||
}: PromptArgs) {
|
||||
const inputs: FormInput[] = [
|
||||
{
|
||||
...props,
|
||||
optional: !required,
|
||||
type: 'text',
|
||||
name: 'value',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user