Quick action tweaks (#218)

This commit is contained in:
Brendan Kellam 2025-02-28 10:12:32 -08:00 committed by GitHub
parent cdfcb5a88b
commit 7685d9cf66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 317 additions and 69 deletions

View file

@ -14,18 +14,20 @@ import {
jsonSchemaLinter,
stateExtensions
} from "codemirror-json-schema";
import { useRef, forwardRef, useImperativeHandle, Ref, ReactNode } from "react";
import { useRef, forwardRef, useImperativeHandle, Ref, ReactNode, useState } from "react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Schema } from "ajv";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import useCaptureEvent from "@/hooks/useCaptureEvent";
import { CodeHostType } from "@/lib/utils";
export type QuickActionFn<T> = (previous: T) => T;
export type QuickAction<T> = {
name: string;
fn: QuickActionFn<T>;
description?: string | ReactNode;
selectionText?: string;
};
interface ConfigEditorProps<T> {
@ -57,11 +59,13 @@ export function onQuickAction<T>(
options?: {
focusEditor?: boolean;
moveCursor?: boolean;
selectionText?: string;
}
) {
const {
focusEditor = false,
moveCursor = true,
selectionText = `""`,
} = options ?? {};
let previousConfig: T;
@ -78,7 +82,6 @@ export function onQuickAction<T>(
view.focus();
}
const cursorPos = next.lastIndexOf(`""`) + 1;
view.dispatch({
changes: {
from: 0,
@ -87,10 +90,16 @@ export function onQuickAction<T>(
}
});
if (moveCursor) {
view.dispatch({
selection: { anchor: cursorPos, head: cursorPos }
});
if (moveCursor && selectionText) {
const cursorPos = next.lastIndexOf(selectionText);
if (cursorPos >= 0) {
view.dispatch({
selection: {
anchor: cursorPos,
head: cursorPos + selectionText.length
}
});
}
}
}
@ -103,10 +112,15 @@ export const isConfigValidJson = (config: string) => {
}
}
const DEFAULT_ACTIONS_VISIBLE = 4;
const ConfigEditor = <T,>(props: ConfigEditorProps<T>, forwardedRef: Ref<ReactCodeMirrorRef>) => {
const { value, type, onChange, actions, schema } = props;
const captureEvent = useCaptureEvent();
const editorRef = useRef<ReactCodeMirrorRef>(null);
const [isViewMoreActionsEnabled, setIsViewMoreActionsEnabled] = useState(false);
const [height, setHeight] = useState(224);
useImperativeHandle(
forwardedRef,
() => editorRef.current as ReactCodeMirrorRef
@ -117,7 +131,79 @@ const ConfigEditor = <T,>(props: ConfigEditorProps<T>, forwardedRef: Ref<ReactCo
return (
<div className="border rounded-md">
<ScrollArea className="p-1 overflow-auto flex-1 h-56">
<div className="flex flex-row items-center flex-wrap p-1">
<TooltipProvider>
{actions
.slice(0, isViewMoreActionsEnabled ? actions.length : DEFAULT_ACTIONS_VISIBLE)
.map(({ name, fn, description, selectionText }, index, truncatedActions) => (
<div
key={index}
className="flex flex-row items-center"
>
<Tooltip
delayDuration={100}
>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="disabled:opacity-100 disabled:pointer-events-auto disabled:cursor-not-allowed text-sm font-mono tracking-tight"
size="sm"
disabled={!isConfigValidJson(value)}
onClick={(e) => {
e.preventDefault();
captureEvent('wa_config_editor_quick_action_pressed', {
name,
type,
});
if (editorRef.current?.view) {
onQuickAction(fn, value, editorRef.current.view, {
focusEditor: true,
selectionText,
});
}
}}
>
{name}
</Button>
</TooltipTrigger>
<TooltipContent
hidden={!description}
className="max-w-xs"
>
{description}
</TooltipContent>
</Tooltip>
{index !== truncatedActions.length - 1 && (
<Separator
orientation="vertical" className="h-4 mx-1"
/>
)}
{index === truncatedActions.length - 1 && truncatedActions.length < actions.length && (
<>
<Separator
orientation="vertical" className="h-4 mx-1"
/>
<Button
variant="link"
size="sm"
className="text-xs text-muted-foreground"
onClick={(e) => {
e.preventDefault();
setIsViewMoreActionsEnabled(!isViewMoreActionsEnabled);
}}
>
+{actions.length - truncatedActions.length} more
</Button>
</>
)}
</div>
))}
</TooltipProvider>
</div>
<Separator />
<ScrollArea className="p-1 overflow-auto flex-1" style={{ height }}>
<CodeMirror
ref={editorRef}
value={value}
@ -142,55 +228,27 @@ const ConfigEditor = <T,>(props: ConfigEditorProps<T>, forwardedRef: Ref<ReactCo
theme={theme === "dark" ? "dark" : "light"}
/>
</ScrollArea>
<Separator />
<div className="flex flex-row items-center flex-wrap w-full p-1">
<TooltipProvider>
{actions.map(({ name, fn, description }, index) => (
<div
key={index}
className="flex flex-row items-center"
>
<Tooltip
delayDuration={100}
>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="disabled:opacity-100 disabled:pointer-events-auto disabled:cursor-not-allowed text-sm font-mono tracking-tight"
size="sm"
disabled={!isConfigValidJson(value)}
onClick={(e) => {
e.preventDefault();
captureEvent('wa_config_editor_quick_action_pressed', {
name,
type,
});
if (editorRef.current?.view) {
onQuickAction(fn, value, editorRef.current.view, {
focusEditor: true,
});
}
}}
>
{name}
</Button>
</TooltipTrigger>
<TooltipContent
hidden={!description}
className="max-w-xs"
>
{description}
</TooltipContent>
</Tooltip>
{index !== actions.length - 1 && (
<Separator
orientation="vertical" className="h-4 mx-1"
/>
)}
</div>
))}
</TooltipProvider>
</div>
<div
className="h-1 cursor-ns-resize bg-border rounded-md hover:bg-primary/50 transition-colors"
onMouseDown={(e) => {
e.preventDefault();
const startY = e.clientY;
const startHeight = height;
function onMouseMove(e: MouseEvent) {
const delta = e.clientY - startY;
setHeight(Math.max(112, startHeight + delta));
}
function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}}
/>
</div>
)
};

View file

@ -12,7 +12,7 @@ export default function Layout({
<div className="min-h-screen flex flex-col">
<NavigationMenu domain={domain} />
<main className="flex-grow flex justify-center p-4 bg-backgroundSecondary relative">
<div className="w-full max-w-6xl rounded-lg p-6">{children}</div>
<div className="w-full max-w-6xl p-6">{children}</div>
</main>
</div>
)

View file

@ -22,10 +22,11 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
...previous,
repos: [
...(previous.repos ?? []),
""
"<owner>/<repo name>"
]
}),
name: "Add a repo",
name: "Add a single repo",
selectionText: "<owner>/<repo name>",
description: (
<div className="flex flex-col">
<span>Add a individual repository to sync with. Ensure the repository is visible to the provided <Code>token</Code> (if any).</span>
@ -47,10 +48,11 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
...previous,
orgs: [
...(previous.orgs ?? []),
""
"<organization name>"
]
}),
name: "Add an organization",
selectionText: "<organization name>",
description: (
<div className="flex flex-col">
<span>Add an organization to sync with. All repositories in the organization visible to the provided <Code>token</Code> (if any) will be synced.</span>
@ -72,19 +74,138 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
...previous,
users: [
...(previous.users ?? []),
""
"<username>"
]
}),
name: "Add a user",
description: <span>Add a user to sync with. All repositories that the user owns visible to the provided <Code>token</Code> (if any) will be synced.</span>
selectionText: "<username>",
description: (
<div className="flex flex-col">
<span>Add a user to sync with. All repositories that the user owns visible to the provided <Code>token</Code> (if any) will be synced.</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-row gap-1 items-center">
{[
"jane-doe",
"torvalds",
"octocat"
].map((org) => (
<Code key={org}>{org}</Code>
))}
</div>
</div>
)
},
{
fn: (previous: GithubConnectionConfig) => ({
...previous,
url: previous.url ?? "",
url: previous.url ?? "https://github.example.com",
}),
name: "Set a custom url",
selectionText: "https://github.example.com",
description: <span>Set a custom GitHub host. Defaults to <Code>https://github.com</Code>.</span>
},
{
fn: (previous: GithubConnectionConfig) => ({
...previous,
exclude: {
...previous.exclude,
repos: [
...(previous.exclude?.repos ?? []),
"<glob pattern>"
]
}
}),
name: "Exclude by repo name",
selectionText: "<glob pattern>",
description: (
<div className="flex flex-col">
<span>Exclude repositories from syncing by name. Glob patterns are supported.</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-col gap-1">
{[
"my-org/docs*",
"my-org/test*"
].map((repo) => (
<Code key={repo}>{repo}</Code>
))}
</div>
</div>
)
},
{
fn: (previous: GithubConnectionConfig) => ({
...previous,
exclude: {
...previous.exclude,
topics: [
...(previous.exclude?.topics ?? []),
"<topic>"
]
}
}),
name: "Exclude by topic",
selectionText: "<topic>",
description: (
<div className="flex flex-col">
<span>Exclude topics from syncing. Only repos that do not match any of the provided topics will be synced. Glob patterns are supported.</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-col gap-1">
{[
"docs",
"ci"
].map((repo) => (
<Code key={repo}>{repo}</Code>
))}
</div>
</div>
)
},
{
fn: (previous: GithubConnectionConfig) => ({
...previous,
topics: [
...(previous.topics ?? []),
"<topic>"
]
}),
name: "Include by topic",
selectionText: "<topic>",
description: (
<div className="flex flex-col">
<span>Include repositories by topic. Only repos that match at least one of the provided topics will be synced. Glob patterns are supported.</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-col gap-1">
{[
"docs",
"ci"
].map((repo) => (
<Code key={repo}>{repo}</Code>
))}
</div>
</div>
)
},
{
fn: (previous: GithubConnectionConfig) => ({
...previous,
exclude: {
...previous.exclude,
archived: true,
}
}),
name: "Exclude archived repos",
description: <span>Exclude archived repositories from syncing.</span>
},
{
fn: (previous: GithubConnectionConfig) => ({
...previous,
exclude: {
...previous.exclude,
forks: true,
}
}),
name: "Exclude forked repos",
description: <span>Exclude forked repositories from syncing.</span>
}
];
@ -98,6 +219,20 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
]
}),
name: "Add a project",
description: (
<div className="flex flex-col">
<span>Add a individual project to sync with. Ensure the project is visible to the provided <Code>token</Code> (if any).</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-col gap-1">
{[
"gitlab-org/gitlab",
"corp/team-project",
].map((repo) => (
<Code key={repo}>{repo}</Code>
))}
</div>
</div>
)
},
{
fn: (previous: GitlabConnectionConfig) => ({
@ -108,6 +243,20 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
]
}),
name: "Add a user",
description: (
<div className="flex flex-col">
<span>Add a user to sync with. All projects that the user owns visible to the provided <Code>token</Code> (if any) will be synced.</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-row gap-1 items-center">
{[
"jane-doe",
"torvalds"
].map((org) => (
<Code key={org}>{org}</Code>
))}
</div>
</div>
)
},
{
fn: (previous: GitlabConnectionConfig) => ({
@ -118,6 +267,20 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
]
}),
name: "Add a group",
description: (
<div className="flex flex-col">
<span>Add a group to sync with. All projects in the group (and recursive subgroups) visible to the provided <Code>token</Code> (if any) will be synced.</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-col gap-1">
{[
"my-group",
"path/to/subgroup"
].map((org) => (
<Code key={org}>{org}</Code>
))}
</div>
</div>
)
},
{
fn: (previous: GitlabConnectionConfig) => ({
@ -125,16 +288,43 @@ export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
url: previous.url ?? "",
}),
name: "Set a custom url",
description: <span>Set a custom GitLab host. Defaults to <Code>https://gitlab.com</Code>.</span>
},
{
fn: (previous: GitlabConnectionConfig) => ({
...previous,
token: previous.token ?? {
secret: "",
},
all: true,
}),
name: "Add a secret",
name: "Sync all projects",
description: <span>Sync all projects visible to the provided <Code>token</Code> (if any). Only available when using a self-hosted GitLab instance.</span>
},
{
fn: (previous: GitlabConnectionConfig) => ({
...previous,
exclude: {
...previous.exclude,
projects: [
...(previous.exclude?.projects ?? []),
""
]
}
}),
name: "Exclude a project",
description: (
<div className="flex flex-col">
<span>List of projects to exclude from syncing. Glob patterns are supported.</span>
<span className="text-sm mt-2 mb-1">Examples:</span>
<div className="flex flex-col gap-1">
{[
"docs/**",
"**/tests/**",
].map((repo) => (
<Code key={repo}>{repo}</Code>
))}
</div>
</div>
)
}
]
export const giteaQuickActions: QuickAction<GiteaConnectionConfig>[] = [

View file

@ -36,8 +36,8 @@ export default function SettingsLayout({
return (
<div className="min-h-screen flex flex-col">
<NavigationMenu domain={domain} />
<div className="flex-grow flex justify-center p-4 bg-[#fafafa] dark:bg-background relative">
<div className="w-full max-w-6xl">
<div className="flex-grow flex justify-center p-4 bg-backgroundSecondary relative">
<div className="w-full max-w-6xl p-6">
<Header className="w-full">
<h1 className="text-3xl">Settings</h1>
</Header>