mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
Quick action tweaks (#218)
This commit is contained in:
parent
cdfcb5a88b
commit
7685d9cf66
4 changed files with 317 additions and 69 deletions
|
|
@ -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>
|
||||
)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>[] = [
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue