diff --git a/packages/web/src/app/[domain]/components/configEditor.tsx b/packages/web/src/app/[domain]/components/configEditor.tsx index edfe4248d..14ed0d618 100644 --- a/packages/web/src/app/[domain]/components/configEditor.tsx +++ b/packages/web/src/app/[domain]/components/configEditor.tsx @@ -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 = (previous: T) => T; export type QuickAction = { name: string; fn: QuickActionFn; description?: string | ReactNode; + selectionText?: string; }; interface ConfigEditorProps { @@ -57,11 +59,13 @@ export function onQuickAction( options?: { focusEditor?: boolean; moveCursor?: boolean; + selectionText?: string; } ) { const { focusEditor = false, moveCursor = true, + selectionText = `""`, } = options ?? {}; let previousConfig: T; @@ -78,7 +82,6 @@ export function onQuickAction( view.focus(); } - const cursorPos = next.lastIndexOf(`""`) + 1; view.dispatch({ changes: { from: 0, @@ -87,10 +90,16 @@ export function onQuickAction( } }); - 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 = (props: ConfigEditorProps, forwardedRef: Ref) => { const { value, type, onChange, actions, schema } = props; const captureEvent = useCaptureEvent(); const editorRef = useRef(null); + const [isViewMoreActionsEnabled, setIsViewMoreActionsEnabled] = useState(false); + const [height, setHeight] = useState(224); + useImperativeHandle( forwardedRef, () => editorRef.current as ReactCodeMirrorRef @@ -117,7 +131,79 @@ const ConfigEditor = (props: ConfigEditorProps, forwardedRef: Ref - +
+ + {actions + .slice(0, isViewMoreActionsEnabled ? actions.length : DEFAULT_ACTIONS_VISIBLE) + .map(({ name, fn, description, selectionText }, index, truncatedActions) => ( +
+ + + + + + + {index !== truncatedActions.length - 1 && ( + + )} + {index === truncatedActions.length - 1 && truncatedActions.length < actions.length && ( + <> + + + + )} +
+ ))} + +
+
+ + + (props: ConfigEditorProps, forwardedRef: Ref - -
- - {actions.map(({ name, fn, description }, index) => ( -
- - - - - - - {index !== actions.length - 1 && ( - - )} -
- ))} -
-
+
{ + 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); + }} + />
) }; diff --git a/packages/web/src/app/[domain]/connections/layout.tsx b/packages/web/src/app/[domain]/connections/layout.tsx index 19c0c99ce..924a53234 100644 --- a/packages/web/src/app/[domain]/connections/layout.tsx +++ b/packages/web/src/app/[domain]/connections/layout.tsx @@ -12,7 +12,7 @@ export default function Layout({
-
{children}
+
{children}
) diff --git a/packages/web/src/app/[domain]/connections/quickActions.tsx b/packages/web/src/app/[domain]/connections/quickActions.tsx index 6f255661f..ba4e96718 100644 --- a/packages/web/src/app/[domain]/connections/quickActions.tsx +++ b/packages/web/src/app/[domain]/connections/quickActions.tsx @@ -22,10 +22,11 @@ export const githubQuickActions: QuickAction[] = [ ...previous, repos: [ ...(previous.repos ?? []), - "" + "/" ] }), - name: "Add a repo", + name: "Add a single repo", + selectionText: "/", description: (
Add a individual repository to sync with. Ensure the repository is visible to the provided token (if any). @@ -47,10 +48,11 @@ export const githubQuickActions: QuickAction[] = [ ...previous, orgs: [ ...(previous.orgs ?? []), - "" + "" ] }), name: "Add an organization", + selectionText: "", description: (
Add an organization to sync with. All repositories in the organization visible to the provided token (if any) will be synced. @@ -72,19 +74,138 @@ export const githubQuickActions: QuickAction[] = [ ...previous, users: [ ...(previous.users ?? []), - "" + "" ] }), name: "Add a user", - description: Add a user to sync with. All repositories that the user owns visible to the provided token (if any) will be synced. + selectionText: "", + description: ( +
+ Add a user to sync with. All repositories that the user owns visible to the provided token (if any) will be synced. + Examples: +
+ {[ + "jane-doe", + "torvalds", + "octocat" + ].map((org) => ( + {org} + ))} +
+
+ ) }, { 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: Set a custom GitHub host. Defaults to https://github.com. + }, + { + fn: (previous: GithubConnectionConfig) => ({ + ...previous, + exclude: { + ...previous.exclude, + repos: [ + ...(previous.exclude?.repos ?? []), + "" + ] + } + }), + name: "Exclude by repo name", + selectionText: "", + description: ( +
+ Exclude repositories from syncing by name. Glob patterns are supported. + Examples: +
+ {[ + "my-org/docs*", + "my-org/test*" + ].map((repo) => ( + {repo} + ))} +
+
+ ) + }, + { + fn: (previous: GithubConnectionConfig) => ({ + ...previous, + exclude: { + ...previous.exclude, + topics: [ + ...(previous.exclude?.topics ?? []), + "" + ] + } + }), + name: "Exclude by topic", + selectionText: "", + description: ( +
+ Exclude topics from syncing. Only repos that do not match any of the provided topics will be synced. Glob patterns are supported. + Examples: +
+ {[ + "docs", + "ci" + ].map((repo) => ( + {repo} + ))} +
+
+ ) + }, + { + fn: (previous: GithubConnectionConfig) => ({ + ...previous, + topics: [ + ...(previous.topics ?? []), + "" + ] + }), + name: "Include by topic", + selectionText: "", + description: ( +
+ Include repositories by topic. Only repos that match at least one of the provided topics will be synced. Glob patterns are supported. + Examples: +
+ {[ + "docs", + "ci" + ].map((repo) => ( + {repo} + ))} +
+
+ ) + }, + { + fn: (previous: GithubConnectionConfig) => ({ + ...previous, + exclude: { + ...previous.exclude, + archived: true, + } + }), + name: "Exclude archived repos", + description: Exclude archived repositories from syncing. + }, + { + fn: (previous: GithubConnectionConfig) => ({ + ...previous, + exclude: { + ...previous.exclude, + forks: true, + } + }), + name: "Exclude forked repos", + description: Exclude forked repositories from syncing. } ]; @@ -98,6 +219,20 @@ export const gitlabQuickActions: QuickAction[] = [ ] }), name: "Add a project", + description: ( +
+ Add a individual project to sync with. Ensure the project is visible to the provided token (if any). + Examples: +
+ {[ + "gitlab-org/gitlab", + "corp/team-project", + ].map((repo) => ( + {repo} + ))} +
+
+ ) }, { fn: (previous: GitlabConnectionConfig) => ({ @@ -108,6 +243,20 @@ export const gitlabQuickActions: QuickAction[] = [ ] }), name: "Add a user", + description: ( +
+ Add a user to sync with. All projects that the user owns visible to the provided token (if any) will be synced. + Examples: +
+ {[ + "jane-doe", + "torvalds" + ].map((org) => ( + {org} + ))} +
+
+ ) }, { fn: (previous: GitlabConnectionConfig) => ({ @@ -118,6 +267,20 @@ export const gitlabQuickActions: QuickAction[] = [ ] }), name: "Add a group", + description: ( +
+ Add a group to sync with. All projects in the group (and recursive subgroups) visible to the provided token (if any) will be synced. + Examples: +
+ {[ + "my-group", + "path/to/subgroup" + ].map((org) => ( + {org} + ))} +
+
+ ) }, { fn: (previous: GitlabConnectionConfig) => ({ @@ -125,16 +288,43 @@ export const gitlabQuickActions: QuickAction[] = [ url: previous.url ?? "", }), name: "Set a custom url", + description: Set a custom GitLab host. Defaults to https://gitlab.com. }, { fn: (previous: GitlabConnectionConfig) => ({ ...previous, - token: previous.token ?? { - secret: "", - }, + all: true, }), - name: "Add a secret", + name: "Sync all projects", + description: Sync all projects visible to the provided token (if any). Only available when using a self-hosted GitLab instance. }, + { + fn: (previous: GitlabConnectionConfig) => ({ + ...previous, + exclude: { + ...previous.exclude, + projects: [ + ...(previous.exclude?.projects ?? []), + "" + ] + } + }), + name: "Exclude a project", + description: ( +
+ List of projects to exclude from syncing. Glob patterns are supported. + Examples: +
+ {[ + "docs/**", + "**/tests/**", + ].map((repo) => ( + {repo} + ))} +
+
+ ) + } ] export const giteaQuickActions: QuickAction[] = [ diff --git a/packages/web/src/app/[domain]/settings/layout.tsx b/packages/web/src/app/[domain]/settings/layout.tsx index f4a9f7565..8a0a577af 100644 --- a/packages/web/src/app/[domain]/settings/layout.tsx +++ b/packages/web/src/app/[domain]/settings/layout.tsx @@ -36,8 +36,8 @@ export default function SettingsLayout({ return (
-
-
+
+

Settings