From c17c3fc1a1d65962be346d715676060449a800bd Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 7 Feb 2025 07:57:11 -0800 Subject: [PATCH] Better flows around setting up FS sync and Git --- src-tauri/yaak-git/index.ts | 9 +- src-tauri/yaak-git/src/branch.rs | 10 +- src-tauri/yaak-git/src/git.rs | 7 +- src-tauri/yaak-git/src/merge.rs | 6 +- src-web/commands/openSettings.ts | 27 + src-web/commands/openWorkspaceSettings.tsx | 25 + src-web/components/CommandPaletteDialog.tsx | 6 +- src-web/components/CreateWorkspaceDialog.tsx | 22 +- src-web/components/GitDropdown.tsx | 497 +++++++++++------- src-web/components/LicenseBadge.tsx | 9 +- src-web/components/SettingsDropdown.tsx | 15 +- .../components/SyncToFilesystemSetting.tsx | 28 +- src-web/components/Toasts.tsx | 27 +- .../components/WorkspaceActionsDropdown.tsx | 13 +- .../components/WorkspaceSettingsDialog.tsx | 12 +- src-web/components/core/Dropdown.tsx | 24 +- src-web/components/core/Icon.tsx | 2 + src-web/hooks/useNotificationToast.tsx | 4 +- src-web/hooks/useOpenSettings.tsx | 30 -- src-web/lib/toast.ts | 25 +- 20 files changed, 499 insertions(+), 299 deletions(-) create mode 100644 src-web/commands/openSettings.ts create mode 100644 src-web/commands/openWorkspaceSettings.tsx delete mode 100644 src-web/hooks/useOpenSettings.tsx diff --git a/src-tauri/yaak-git/index.ts b/src-tauri/yaak-git/index.ts index ae8d4400..388c1dfd 100644 --- a/src-tauri/yaak-git/index.ts +++ b/src-tauri/yaak-git/index.ts @@ -61,11 +61,6 @@ export function useGit(dir: string) { mutationFn: () => invoke('plugin:yaak-git|pull', { dir }), onSuccess, }), - init: useMutation({ - mutationKey: ['git', 'initialize', dir], - mutationFn: () => invoke('plugin:yaak-git|initialize', { dir }), - onSuccess, - }), unstage: useMutation({ mutationKey: ['git', 'unstage', dir], mutationFn: (args) => invoke('plugin:yaak-git|unstage', { dir, ...args }), @@ -74,3 +69,7 @@ export function useGit(dir: string) { }, ] as const; } + +export async function gitInit(dir: string) { + await invoke('plugin:yaak-git|initialize', { dir }); +} diff --git a/src-tauri/yaak-git/src/branch.rs b/src-tauri/yaak-git/src/branch.rs index 164d950d..f26eb569 100644 --- a/src-tauri/yaak-git/src/branch.rs +++ b/src-tauri/yaak-git/src/branch.rs @@ -41,7 +41,15 @@ pub(crate) fn git_checkout_branch(dir: &Path, branch: &str, force: bool) -> Resu pub(crate) fn git_create_branch(dir: &Path, name: &str) -> Result<()> { let repo = open_repo(dir)?; - let head = repo.head()?.peel_to_commit()?; + let head = match repo.head() { + Ok(h) => h, + Err(e) if e.code() == git2::ErrorCode::UnbornBranch => { + let msg = "Cannot create branch when there are no commits"; + return Err(GenericError(msg.into())); + } + Err(e) => return Err(e.into()), + }; + let head = head.peel_to_commit()?; repo.branch(name, &head, false)?; diff --git a/src-tauri/yaak-git/src/git.rs b/src-tauri/yaak-git/src/git.rs index e46e1396..e61ae414 100644 --- a/src-tauri/yaak-git/src/git.rs +++ b/src-tauri/yaak-git/src/git.rs @@ -65,6 +65,10 @@ pub struct GitAuthor { pub fn git_init(dir: &Path) -> Result<()> { git2::Repository::init(dir)?; + let repo = open_repo(dir)?; + // Default to main instead of master, to align with + // the official Git and GitHub behavior + repo.set_head("refs/heads/main")?; info!("Initialized {dir:?}"); Ok(()) } @@ -134,7 +138,8 @@ pub fn git_commit(dir: &Path, message: &str) -> Result<()> { pub fn git_log(dir: &Path) -> Result> { let repo = open_repo(dir)?; - if repo.is_empty()? { + // Return empty if empty repo or no head (new repo) + if repo.is_empty()? || repo.head().is_err() { return Ok(vec![]); } diff --git a/src-tauri/yaak-git/src/merge.rs b/src-tauri/yaak-git/src/merge.rs index 60389d84..2c621141 100644 --- a/src-tauri/yaak-git/src/merge.rs +++ b/src-tauri/yaak-git/src/merge.rs @@ -1,9 +1,8 @@ use crate::error::Error::MergeConflicts; use crate::util::bytes_to_string; -use git2::{AnnotatedCommit, Branch, IndexEntry, MergeOptions, Reference, Repository}; +use git2::{AnnotatedCommit, Branch, IndexEntry, Reference, Repository}; use log::{debug, info}; - pub(crate) fn do_merge( repo: &Repository, local_branch: &Branch, @@ -46,7 +45,6 @@ pub(crate) fn do_merge( Ok(()) } - pub(crate) fn merge_fast_forward( repo: &Repository, local_reference: &mut Reference, @@ -79,7 +77,7 @@ pub(crate) fn merge_normal( let local_tree = repo.find_commit(local.id())?.tree()?; let remote_tree = repo.find_commit(remote.id())?.tree()?; let ancestor = repo.find_commit(repo.merge_base(local.id(), remote.id())?)?.tree()?; - + let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?; if idx.has_conflicts() { diff --git a/src-web/commands/openSettings.ts b/src-web/commands/openSettings.ts new file mode 100644 index 00000000..5a47fb3c --- /dev/null +++ b/src-web/commands/openSettings.ts @@ -0,0 +1,27 @@ +import { SettingsTab } from '../components/Settings/SettingsTab'; +import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace'; +import { createFastMutation } from '../hooks/useFastMutation'; +import { trackEvent } from '../lib/analytics'; +import { router } from '../lib/router'; +import { invokeCmd } from '../lib/tauri'; + +export const openSettings = createFastMutation({ + mutationKey: ['open_settings'], + mutationFn: async function (tab) { + const workspaceId = getActiveWorkspaceId(); + if (workspaceId == null) return; + + trackEvent('dialog', 'show', { id: 'settings', tab: `${tab}` }); + const location = router.buildLocation({ + to: '/workspaces/$workspaceId/settings', + params: { workspaceId }, + search: { tab: tab ?? SettingsTab.General }, + }); + await invokeCmd('cmd_new_child_window', { + url: location.href, + label: 'settings', + title: 'Yaak Settings', + innerSize: [750, 600], + }); + }, +}); diff --git a/src-web/commands/openWorkspaceSettings.tsx b/src-web/commands/openWorkspaceSettings.tsx new file mode 100644 index 00000000..0b90e70d --- /dev/null +++ b/src-web/commands/openWorkspaceSettings.tsx @@ -0,0 +1,25 @@ +import { WorkspaceSettingsDialog } from '../components/WorkspaceSettingsDialog'; +import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace'; +import { createFastMutation } from '../hooks/useFastMutation'; +import { showDialog } from '../lib/dialog'; + +export const openWorkspaceSettings = createFastMutation({ + mutationKey: ['open_workspace_settings'], + async mutationFn({ openSyncMenu }) { + const workspaceId = getActiveWorkspaceId(); + showDialog({ + id: 'workspace-settings', + title: 'Workspace Settings', + size: 'md', + render({ hide }) { + return ( + + ); + }, + }); + }, +}); diff --git a/src-web/components/CommandPaletteDialog.tsx b/src-web/components/CommandPaletteDialog.tsx index efedbac3..eaf3980f 100644 --- a/src-web/components/CommandPaletteDialog.tsx +++ b/src-web/components/CommandPaletteDialog.tsx @@ -3,6 +3,7 @@ import { fuzzyFilter } from 'fuzzbunny'; import type { KeyboardEvent, ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createFolder } from '../commands/commands'; +import { openSettings } from '../commands/openSettings'; import { switchWorkspace } from '../commands/switchWorkspace'; import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; @@ -17,7 +18,6 @@ import { useEnvironments } from '../hooks/useEnvironments'; import type { HotkeyAction } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey'; import { useHttpRequestActions } from '../hooks/useHttpRequestActions'; -import { useOpenSettings } from '../hooks/useOpenSettings'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; @@ -71,7 +71,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { const [recentRequests] = useRecentRequests(); const [, setSidebarHidden] = useSidebarHidden(); const { baseEnvironment } = useEnvironments(); - const { mutate: openSettings } = useOpenSettings(); const { mutate: createHttpRequest } = useCreateHttpRequest(); const { mutate: createGrpcRequest } = useCreateGrpcRequest(); const { mutate: createEnvironment } = useCreateEnvironment(); @@ -85,7 +84,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { key: 'settings.open', label: 'Open Settings', action: 'settings.show', - onSelect: openSettings, + onSelect: () => openSettings.mutate(null), }, { key: 'app.create', @@ -193,7 +192,6 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) { createWorkspace, deleteRequest, httpRequestActions, - openSettings, renameRequest, sendRequest, setSidebarHidden, diff --git a/src-web/components/CreateWorkspaceDialog.tsx b/src-web/components/CreateWorkspaceDialog.tsx index 2ccbf4c7..2b80f4db 100644 --- a/src-web/components/CreateWorkspaceDialog.tsx +++ b/src-web/components/CreateWorkspaceDialog.tsx @@ -1,9 +1,11 @@ +import { gitInit } from '@yaakapp-internal/git'; import type { WorkspaceMeta } from '@yaakapp-internal/models'; import { useState } from 'react'; import { upsertWorkspace } from '../commands/upsertWorkspace'; import { upsertWorkspaceMeta } from '../commands/upsertWorkspaceMeta'; import { router } from '../lib/router'; import { invokeCmd } from '../lib/tauri'; +import { showErrorToast } from '../lib/toast'; import { Button } from './core/Button'; import { PlainInput } from './core/PlainInput'; import { VStack } from './core/Stacks'; @@ -15,7 +17,10 @@ interface Props { export function CreateWorkspaceDialog({ hide }: Props) { const [name, setName] = useState(''); - const [settingSyncDir, setSettingSyncDir] = useState(null); + const [syncConfig, setSyncConfig] = useState<{ + filePath: string | null; + initGit?: boolean; + }>({ filePath: null, initGit: true }); return ( ('cmd_get_workspace_meta', { workspaceId: workspace.id, }); - upsertWorkspaceMeta.mutate({ ...workspaceMeta, settingSyncDir }); + await upsertWorkspaceMeta.mutateAsync({ + ...workspaceMeta, + settingSyncDir: syncConfig.filePath, + }); + + if (syncConfig.initGit && syncConfig.filePath) { + gitInit(syncConfig.filePath).catch((err) => { + showErrorToast('git-init-error', String(err)); + }); + } // Navigate to workspace await router.navigate({ @@ -47,8 +61,8 @@ export function CreateWorkspaceDialog({ hide }: Props) { + ]} + > + +
+ +
Setup Git
+
+
); } diff --git a/src-web/components/LicenseBadge.tsx b/src-web/components/LicenseBadge.tsx index e6df70dd..06dfedc6 100644 --- a/src-web/components/LicenseBadge.tsx +++ b/src-web/components/LicenseBadge.tsx @@ -3,12 +3,12 @@ import type { LicenseCheckStatus } from '@yaakapp-internal/license'; import { useLicense } from '@yaakapp-internal/license'; import type { ReactNode } from 'react'; import { appInfo } from '../hooks/useAppInfo'; -import { useOpenSettings } from '../hooks/useOpenSettings'; import type { ButtonProps } from './core/Button'; import { Button } from './core/Button'; -import { HStack } from './core/Stacks'; -import { SettingsTab } from './Settings/SettingsTab'; import { Icon } from './core/Icon'; +import { HStack } from './core/Stacks'; +import { openSettings } from '../commands/openSettings'; +import {SettingsTab} from "./Settings/SettingsTab"; const details: Record< LicenseCheckStatus['type'] | 'dev' | 'beta', @@ -31,7 +31,6 @@ const details: Record< }; export function LicenseBadge() { - const openSettings = useOpenSettings(SettingsTab.License); const { check } = useLicense(); if (check.data == null) { @@ -57,7 +56,7 @@ export function LicenseBadge() { if (checkType === 'beta') { await openUrl('https://feedback.yaak.app'); } else { - openSettings.mutate(); + openSettings.mutate(SettingsTab.License); } }} color={detail.color} diff --git a/src-web/components/SettingsDropdown.tsx b/src-web/components/SettingsDropdown.tsx index 1b2df14d..492e0faa 100644 --- a/src-web/components/SettingsDropdown.tsx +++ b/src-web/components/SettingsDropdown.tsx @@ -1,11 +1,11 @@ import { openUrl } from '@tauri-apps/plugin-opener'; import { useRef } from 'react'; +import { openSettings } from '../commands/openSettings'; import { useAppInfo } from '../hooks/useAppInfo'; import { useCheckForUpdates } from '../hooks/useCheckForUpdates'; import { useExportData } from '../hooks/useExportData'; import { useImportData } from '../hooks/useImportData'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; -import { useOpenSettings } from '../hooks/useOpenSettings'; import { showDialog } from '../lib/dialog'; import type { DropdownRef } from './core/Dropdown'; import { Dropdown } from './core/Dropdown'; @@ -19,9 +19,8 @@ export function SettingsDropdown() { const appInfo = useAppInfo(); const dropdownRef = useRef(null); const checkForUpdates = useCheckForUpdates(); - const openSettings = useOpenSettings(); - useListenToTauriEvent('settings', () => openSettings.mutate()); + useListenToTauriEvent('settings', () => openSettings.mutate(null)); return ( , - onSelect: openSettings.mutate, + onSelect: () => openSettings.mutate(null), }, { label: 'Keyboard shortcuts', @@ -76,7 +75,13 @@ export function SettingsDropdown() { }, ]} > - + ); } diff --git a/src-web/components/SyncToFilesystemSetting.tsx b/src-web/components/SyncToFilesystemSetting.tsx index b6015c67..f331d52f 100644 --- a/src-web/components/SyncToFilesystemSetting.tsx +++ b/src-web/components/SyncToFilesystemSetting.tsx @@ -1,38 +1,38 @@ import { readDir } from '@tauri-apps/plugin-fs'; import { useState } from 'react'; import { Banner } from './core/Banner'; +import { Checkbox } from './core/Checkbox'; import { VStack } from './core/Stacks'; import { SelectFile } from './SelectFile'; export interface SyncToFilesystemSettingProps { - onChange: (filePath: string | null) => void; - value: string | null; + onChange: (args: { filePath: string | null; initGit?: boolean }) => void; + value: { filePath: string | null; initGit?: boolean }; allowNonEmptyDirectory?: boolean; + forceOpen?: boolean; } export function SyncToFilesystemSetting({ onChange, value, allowNonEmptyDirectory, + forceOpen, }: SyncToFilesystemSettingProps) { const [error, setError] = useState(null); - return ( -
- Sync to filesystem +
+ Data directory {typeof value.initGit === 'boolean' && ' and Git'} - When enabled, workspace data syncs to the chosen folder as text files, ideal for backup - and Git collaboration. + Sync workspace data to folder as plain text files, ideal for backup and Git collaboration. {error &&
{error}
} { if (filePath != null) { const files = await readDir(filePath); @@ -42,9 +42,17 @@ export function SyncToFilesystemSetting({ } } - onChange(filePath); + onChange({ ...value, filePath }); }} /> + + {value.filePath && typeof value.initGit === 'boolean' && ( + onChange({ ...value, initGit })} + title="Initialize Git Repo" + /> + )}
); diff --git a/src-web/components/Toasts.tsx b/src-web/components/Toasts.tsx index a0d0b4fa..292c783a 100644 --- a/src-web/components/Toasts.tsx +++ b/src-web/components/Toasts.tsx @@ -19,18 +19,21 @@ export const Toasts = () => {
- {toasts.map(({ message, uniqueKey, ...props }: ToastInstance) => ( - hideToast(uniqueKey)} - > - {message} - - ))} + {toasts.map((toast: ToastInstance) => { + const { message, uniqueKey, ...props } = toast; + return ( + hideToast(toast)} + > + {message} + + ); + })}
diff --git a/src-web/components/WorkspaceActionsDropdown.tsx b/src-web/components/WorkspaceActionsDropdown.tsx index dddb340a..15449cb7 100644 --- a/src-web/components/WorkspaceActionsDropdown.tsx +++ b/src-web/components/WorkspaceActionsDropdown.tsx @@ -2,6 +2,7 @@ import { revealItemInDir } from '@tauri-apps/plugin-opener'; import classNames from 'classnames'; import { memo, useCallback, useMemo } from 'react'; import { openWorkspaceFromSyncDir } from '../commands/openWorkspaceFromSyncDir'; +import { openWorkspaceSettings } from '../commands/openWorkspaceSettings'; import { switchWorkspace } from '../commands/switchWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useCreateWorkspace } from '../hooks/useCreateWorkspace'; @@ -19,7 +20,6 @@ import { Icon } from './core/Icon'; import type { RadioDropdownItem } from './core/RadioDropdown'; import { RadioDropdown } from './core/RadioDropdown'; import { SwitchWorkspaceDialog } from './SwitchWorkspaceDialog'; -import { WorkspaceSettingsDialog } from './WorkspaceSettingsDialog'; type Props = Pick; @@ -49,16 +49,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ label: 'Workspace Settings', leftSlot: , hotKeyAction: 'workspace_settings.show', - onSelect: async () => { - showDialog({ - id: 'workspace-settings', - title: 'Workspace Settings', - size: 'md', - render: ({ hide }) => ( - - ), - }); - }, + onSelect: () => openWorkspaceSettings.mutate({}), }, { label: revealInFinderText, diff --git a/src-web/components/WorkspaceSettingsDialog.tsx b/src-web/components/WorkspaceSettingsDialog.tsx index 2c2b139c..05b406b4 100644 --- a/src-web/components/WorkspaceSettingsDialog.tsx +++ b/src-web/components/WorkspaceSettingsDialog.tsx @@ -15,9 +15,10 @@ import { SyncToFilesystemSetting } from './SyncToFilesystemSetting'; interface Props { workspaceId: string | null; hide: () => void; + openSyncMenu?: boolean; } -export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) { +export function WorkspaceSettingsDialog({ workspaceId, hide, openSyncMenu }: Props) { const workspaces = useWorkspaces(); const workspace = workspaces.find((w) => w.id === workspaceId); const workspaceMeta = useWorkspaceMeta(); @@ -60,10 +61,11 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) { - upsertWorkspaceMeta.mutate({ ...workspaceMeta, settingSyncDir }) - } + value={{ filePath: workspaceMeta.settingSyncDir }} + forceOpen={openSyncMenu} + onChange={({ filePath }) => { + upsertWorkspaceMeta.mutate({ ...workspaceMeta, settingSyncDir: filePath }); + }} />