From d0fde99b1c11a8b7884ad5a72fcf12d229a2d91d Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 7 Jun 2025 18:21:54 -0700 Subject: [PATCH] Environment colors (#225) --- package-lock.json | 11 +++++ .../20250611120000_environment-color.sql | 1 + src-tauri/yaak-git/bindings/gen_models.ts | 2 +- src-tauri/yaak-models/bindings/gen_models.ts | 2 +- src-tauri/yaak-models/bindings/gen_util.ts | 7 +--- src-tauri/yaak-models/src/models.rs | 4 ++ src-tauri/yaak-plugins/bindings/gen_events.ts | 8 +--- src-tauri/yaak-plugins/bindings/gen_models.ts | 2 +- src-tauri/yaak-sync/bindings/gen_models.ts | 2 +- src-tauri/yaak-sync/bindings/gen_sync.ts | 3 +- .../components/EnvironmentActionsDropdown.tsx | 5 ++- src-web/components/EnvironmentColorCircle.tsx | 29 ++++++++++++++ src-web/components/EnvironmentColorPicker.tsx | 26 ++++++++++++ src-web/components/EnvironmentEditDialog.tsx | 12 +++++- src-web/components/Workspace.tsx | 26 ++++++++++-- src-web/components/core/ColorPicker.tsx | 40 +++++++++++++++++++ src-web/components/core/Icon.tsx | 3 +- src-web/lib/showColorPicker.tsx | 23 +++++++++++ src-web/package.json | 1 + 19 files changed, 182 insertions(+), 25 deletions(-) create mode 100644 src-tauri/migrations/20250611120000_environment-color.sql create mode 100644 src-web/components/EnvironmentColorCircle.tsx create mode 100644 src-web/components/EnvironmentColorPicker.tsx create mode 100644 src-web/components/core/ColorPicker.tsx create mode 100644 src-web/lib/showColorPicker.tsx diff --git a/package-lock.json b/package-lock.json index d1df32b3..79ce9ac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12946,6 +12946,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -17626,6 +17636,7 @@ "papaparse": "^5.4.1", "parse-color": "^1.0.0", "react": "^18.3.1", + "react-colorful": "^5.6.1", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", diff --git a/src-tauri/migrations/20250611120000_environment-color.sql b/src-tauri/migrations/20250611120000_environment-color.sql new file mode 100644 index 00000000..e7071102 --- /dev/null +++ b/src-tauri/migrations/20250611120000_environment-color.sql @@ -0,0 +1 @@ +ALTER TABLE environments ADD COLUMN color TEXT; \ No newline at end of file diff --git a/src-tauri/yaak-git/bindings/gen_models.ts b/src-tauri/yaak-git/bindings/gen_models.ts index b51a6b75..b8341f92 100644 --- a/src-tauri/yaak-git/bindings/gen_models.ts +++ b/src-tauri/yaak-git/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-models/bindings/gen_models.ts b/src-tauri/yaak-models/bindings/gen_models.ts index cd0906f1..c441825f 100644 --- a/src-tauri/yaak-models/bindings/gen_models.ts +++ b/src-tauri/yaak-models/bindings/gen_models.ts @@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs"; export type EncryptedKey = { encryptedKey: string, }; -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-models/bindings/gen_util.ts b/src-tauri/yaak-models/bindings/gen_util.ts index 482d1902..f87c220c 100644 --- a/src-tauri/yaak-models/bindings/gen_util.ts +++ b/src-tauri/yaak-models/bindings/gen_util.ts @@ -1,9 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Environment } from "./gen_models.js"; -import type { Folder } from "./gen_models.js"; -import type { GrpcRequest } from "./gen_models.js"; -import type { HttpRequest } from "./gen_models.js"; -import type { WebsocketRequest } from "./gen_models.js"; -import type { Workspace } from "./gen_models.js"; +import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models.js"; export type BatchUpsertResult = { workspaces: Array, environments: Array, folders: Array, httpRequests: Array, grpcRequests: Array, websocketRequests: Array, }; diff --git a/src-tauri/yaak-models/src/models.rs b/src-tauri/yaak-models/src/models.rs index 323aae14..8cd337b6 100644 --- a/src-tauri/yaak-models/src/models.rs +++ b/src-tauri/yaak-models/src/models.rs @@ -520,6 +520,7 @@ pub struct Environment { pub public: bool, pub base: bool, pub variables: Vec, + pub color: Option, } impl UpsertModelInfo for Environment { @@ -553,6 +554,7 @@ impl UpsertModelInfo for Environment { (UpdatedAt, upsert_date(source, self.updated_at)), (WorkspaceId, self.workspace_id.into()), (Base, self.base.into()), + (Color, self.color.into()), (Name, self.name.trim().into()), (Public, self.public.into()), (Variables, serde_json::to_string(&self.variables)?.into()), @@ -563,6 +565,7 @@ impl UpsertModelInfo for Environment { vec![ EnvironmentIden::UpdatedAt, EnvironmentIden::Base, + EnvironmentIden::Color, EnvironmentIden::Name, EnvironmentIden::Public, EnvironmentIden::Variables, @@ -581,6 +584,7 @@ impl UpsertModelInfo for Environment { created_at: row.get("created_at")?, updated_at: row.get("updated_at")?, base: row.get("base")?, + color: row.get("color")?, name: row.get("name")?, public: row.get("public")?, variables: serde_json::from_str(variables.as_str()).unwrap_or_default(), diff --git a/src-tauri/yaak-plugins/bindings/gen_events.ts b/src-tauri/yaak-plugins/bindings/gen_events.ts index 3363706d..1a82fb1b 100644 --- a/src-tauri/yaak-plugins/bindings/gen_events.ts +++ b/src-tauri/yaak-plugins/bindings/gen_events.ts @@ -1,12 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Environment } from "./gen_models.js"; -import type { Folder } from "./gen_models.js"; -import type { GrpcRequest } from "./gen_models.js"; -import type { HttpRequest } from "./gen_models.js"; -import type { HttpResponse } from "./gen_models.js"; +import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js"; import type { JsonValue } from "./serde_json/JsonValue.js"; -import type { WebsocketRequest } from "./gen_models.js"; -import type { Workspace } from "./gen_models.js"; export type BootRequest = { dir: string, watch: boolean, }; diff --git a/src-tauri/yaak-plugins/bindings/gen_models.ts b/src-tauri/yaak-plugins/bindings/gen_models.ts index 8d95d78f..0880935d 100644 --- a/src-tauri/yaak-plugins/bindings/gen_models.ts +++ b/src-tauri/yaak-plugins/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-sync/bindings/gen_models.ts b/src-tauri/yaak-sync/bindings/gen_models.ts index 3828f880..8e0417b5 100644 --- a/src-tauri/yaak-sync/bindings/gen_models.ts +++ b/src-tauri/yaak-sync/bindings/gen_models.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, }; +export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array, color: string | null, }; export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; diff --git a/src-tauri/yaak-sync/bindings/gen_sync.ts b/src-tauri/yaak-sync/bindings/gen_sync.ts index 67ccd6ff..1142ab51 100644 --- a/src-tauri/yaak-sync/bindings/gen_sync.ts +++ b/src-tauri/yaak-sync/bindings/gen_sync.ts @@ -1,6 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SyncModel } from "./gen_models.js"; -import type { SyncState } from "./gen_models.js"; +import type { SyncModel, SyncState } from "./gen_models.js"; export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, }; diff --git a/src-web/components/EnvironmentActionsDropdown.tsx b/src-web/components/EnvironmentActionsDropdown.tsx index 9a75268e..83ed080a 100644 --- a/src-web/components/EnvironmentActionsDropdown.tsx +++ b/src-web/components/EnvironmentActionsDropdown.tsx @@ -4,11 +4,12 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown'; import { toggleDialog } from '../lib/dialog'; import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams'; -import type { ButtonProps } from './core/Button'; import { Button } from './core/Button'; +import type { ButtonProps } from './core/Button'; import type { DropdownItem } from './core/Dropdown'; import { Dropdown } from './core/Dropdown'; import { Icon } from './core/Icon'; +import { EnvironmentColorCircle } from './EnvironmentColorCircle'; import { EnvironmentEditDialog } from './EnvironmentEditDialog'; type Props = { @@ -38,6 +39,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo (e) => ({ key: e.id, label: e.name, + rightSlot: , leftSlot: e.id === activeEnvironment?.id ? : , onSelect: async () => { if (e.id !== activeEnvironment?.id) { @@ -80,6 +82,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined} {...buttonProps} > + {activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')} diff --git a/src-web/components/EnvironmentColorCircle.tsx b/src-web/components/EnvironmentColorCircle.tsx new file mode 100644 index 00000000..e9f0c7b2 --- /dev/null +++ b/src-web/components/EnvironmentColorCircle.tsx @@ -0,0 +1,29 @@ +import type { Environment } from '@yaakapp-internal/models'; +import classNames from 'classnames'; +import { showColorPicker } from '../lib/showColorPicker'; + +export function EnvironmentColorCircle({ + environment, + clickToEdit, +}: { + environment: Environment | null; + clickToEdit?: boolean; +}) { + if (environment?.color == null) return null; + + const style = { backgroundColor: environment.color }; + const className = + 'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent'; + + if (clickToEdit) { + return ( + + + + + ); +} diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index f8d772ad..a6d7575e 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -17,6 +17,7 @@ import { setupOrConfigureEncryption, withEncryptionEnabled, } from '../lib/setupOrConfigureEncryption'; +import { showColorPicker } from '../lib/showColorPicker'; import { BadgeButton } from './core/BadgeButton'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; @@ -35,6 +36,7 @@ import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { Separator } from './core/Separator'; import { SplitLayout } from './core/SplitLayout'; import { VStack } from './core/Stacks'; +import { EnvironmentColorCircle } from './EnvironmentColorCircle'; interface Props { initialEnvironment: Environment | null; @@ -123,7 +125,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { ))} {subEnvironments.length > 0 && (
- +
)} {subEnvironments.map((e) => ( @@ -237,6 +239,7 @@ const EnvironmentEditor = function ({ return ( +
{selectedEnvironment?.name}
{isEncryptionEnabled ? ( promptToEncrypt ? ( @@ -344,6 +347,7 @@ function SidebarButton({ onContextMenu={handleContextMenu} rightSlot={rightSlot} > + {children} {outerRightSlot} @@ -385,6 +389,12 @@ function SidebarButton({ }, ] : []) as DropdownItem[]), + { + label: 'Set Color', + leftSlot: , + hidden: environment.base, + onSelect: async () => showColorPicker(environment), + }, { label: `Make ${environment.public ? 'Private' : 'Sharable'}`, leftSlot: , diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index af176234..64c4dc5f 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -8,7 +8,10 @@ import { useEnsureActiveCookieJar, useSubscribeActiveCookieJarId, } from '../hooks/useActiveCookieJar'; -import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment'; +import { + activeEnvironmentAtom, + useSubscribeActiveEnvironmentId, +} from '../hooks/useActiveEnvironment'; import { activeRequestAtom } from '../hooks/useActiveRequest'; import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId'; import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace'; @@ -32,6 +35,7 @@ import { HotKeyList } from './core/HotKeyList'; import { FeedbackLink } from './core/Link'; import { HStack } from './core/Stacks'; import { CreateDropdown } from './CreateDropdown'; +import { ErrorBoundary } from './ErrorBoundary'; import { GrpcConnectionLayout } from './GrpcConnectionLayout'; import { HeaderSize } from './HeaderSize'; import { HttpRequestLayout } from './HttpRequestLayout'; @@ -41,7 +45,6 @@ import { Sidebar } from './sidebar/Sidebar'; import { SidebarActions } from './sidebar/SidebarActions'; import { WebsocketRequestLayout } from './WebsocketRequestLayout'; import { WorkspaceHeader } from './WorkspaceHeader'; -import { ErrorBoundary } from './ErrorBoundary'; const side = { gridArea: 'side' }; const head = { gridArea: 'head' }; @@ -56,6 +59,7 @@ export function Workspace() { const { setWidth, width, resetWidth } = useSidebarWidth(); const [sidebarHidden, setSidebarHidden] = useSidebarHidden(); const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden(); + const activeEnvironment = useAtomValue(activeEnvironmentAtom); const floating = useShouldFloatSidebar(); const [isResizing, setIsResizing] = useState(false); const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( @@ -117,6 +121,12 @@ export function Workspace() { [sideWidth, floating], ); + const environmentBg = useMemo(() => { + if (activeEnvironment?.color == null) return undefined; + const background = `linear-gradient(to right, ${activeEnvironment.color} 15%, transparent 40%)`; + return { background }; + }, [activeEnvironment?.color ?? 'n/a']); + // We're loading still if (workspaces.length === 0) { return null; @@ -175,9 +185,19 @@ export function Workspace() { +
+
+
+
diff --git a/src-web/components/core/ColorPicker.tsx b/src-web/components/core/ColorPicker.tsx new file mode 100644 index 00000000..7fb0e9f2 --- /dev/null +++ b/src-web/components/core/ColorPicker.tsx @@ -0,0 +1,40 @@ +import { useState } from 'react'; +import { HexColorPicker } from 'react-colorful'; +import { useRandomKey } from '../../hooks/useRandomKey'; +import { PlainInput } from './PlainInput'; + +interface Props { + onChange: (value: string | null) => void; + color: string | null; +} + +export function ColorPicker({ onChange, color: defaultColor }: Props) { + const [updateKey, regenerateKey] = useRandomKey(); + const [color, setColor] = useState(defaultColor); + return ( +
{ + e.preventDefault(); + onChange(color); + }} + > + { + setColor(color.toUpperCase()); + regenerateKey(); + }} + /> + setColor(c.toUpperCase())} + validate={(color) => color.match(/#[0-9a-fA-F]{6}$/) !== null} + /> + + ); +} diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index aba15f74..c95c1660 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -26,9 +26,9 @@ const icons = { cake: lucide.CakeIcon, chat: lucide.MessageSquare, check: lucide.CheckIcon, + check_circle: lucide.CheckCircleIcon, check_square_checked: lucide.SquareCheckIcon, check_square_unchecked: lucide.SquareIcon, - check_circle: lucide.CheckCircleIcon, chevron_down: lucide.ChevronDownIcon, chevron_right: lucide.ChevronRightIcon, circle_alert: lucide.CircleAlertIcon, @@ -78,6 +78,7 @@ const icons = { minus_circle: lucide.MinusCircleIcon, moon: lucide.MoonIcon, more_vertical: lucide.MoreVerticalIcon, + palette: lucide.PaletteIcon, paste: lucide.ClipboardPasteIcon, pencil: lucide.PencilIcon, pin: lucide.PinIcon, diff --git a/src-web/lib/showColorPicker.tsx b/src-web/lib/showColorPicker.tsx new file mode 100644 index 00000000..eac79b69 --- /dev/null +++ b/src-web/lib/showColorPicker.tsx @@ -0,0 +1,23 @@ +import type { Environment } from '@yaakapp-internal/models'; +import { patchModel } from '@yaakapp-internal/models'; +import { showDialog } from './dialog'; +import { EnvironmentColorPicker } from '../components/EnvironmentColorPicker'; + +export function showColorPicker(environment: Environment) { + showDialog({ + title: 'Environment Color', + id: 'color-picker', + size: 'dynamic', + render: ({ hide }) => { + return ( + { + await patchModel(environment, { color }); + hide(); + }} + /> + ); + }, + }); +} diff --git a/src-web/package.json b/src-web/package.json index 1407fbc5..e20cc067 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -54,6 +54,7 @@ "papaparse": "^5.4.1", "parse-color": "^1.0.0", "react": "^18.3.1", + "react-colorful": "^5.6.1", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1",