Compare commits

..

9 Commits

Author SHA1 Message Date
Gregory Schier
74f14a8392 Tweak some things for launch 2025-02-18 21:28:03 -08:00
Gregory Schier
ccbc8d4e18 Update 2025-02-15 12:04:35 -08:00
Gregory Schier
e4cc11aec5 Update 2025-02-15 07:29:50 -08:00
Gregory Schier
7fdf6f2798 Update 2025-02-15 07:12:12 -08:00
Gregory Schier
2aa27f7003 Create FUNDING.yml 2025-02-15 05:22:39 -08:00
Gregory Schier
3aaa0355e1 Show folders in sync confirm dialog 2025-02-09 08:35:29 -08:00
Gregory Schier
325c88f251 Show push errors in commit dialog 2025-02-07 22:50:12 -08:00
Gregory Schier
83ab93cebf Show push errors in commit dialog 2025-02-07 22:20:39 -08:00
Gregory Schier
c6289f13c1 Handle external files 2025-02-07 22:14:40 -08:00
31 changed files with 190 additions and 95 deletions

View File

@@ -5,7 +5,7 @@ export type GitAuthor = { name: string | null, email: string | null, };
export type GitCommit = { author: GitAuthor, when: string, message: string | null, };
export type GitStatus = "added" | "conflict" | "current" | "modified" | "removed" | "renamed" | "type_change";
export type GitStatus = "untracked" | "conflict" | "current" | "modified" | "removed" | "renamed" | "type_change";
export type GitStatusEntry = { relaPath: string, status: GitStatus, staged: boolean, prev: SyncModel | null, next: SyncModel | null, };

View File

@@ -38,7 +38,7 @@ pub struct GitStatusEntry {
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_git.ts")]
pub enum GitStatus {
Added,
Untracked,
Conflict,
Current,
Modified,
@@ -217,7 +217,7 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
let index_status = match status {
// Note: order matters here, since we're checking a bitmap!
s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict,
s if s.contains(git2::Status::INDEX_NEW) => GitStatus::Added,
s if s.contains(git2::Status::INDEX_NEW) => GitStatus::Untracked,
s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified,
s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Removed,
s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed,
@@ -232,7 +232,7 @@ pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
let worktree_status = match status {
// Note: order matters here, since we're checking a bitmap!
s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict,
s if s.contains(git2::Status::WT_NEW) => GitStatus::Added,
s if s.contains(git2::Status::WT_NEW) => GitStatus::Untracked,
s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified,
s if s.contains(git2::Status::WT_DELETED) => GitStatus::Removed,
s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed,

View File

@@ -11,7 +11,7 @@ use yaak_models::queries::UpdateSource;
const KV_NAMESPACE: &str = "license";
const KV_ACTIVATION_ID_KEY: &str = "activation_id";
const TRIAL_SECONDS: u64 = 3600 * 24 * 14;
const TRIAL_SECONDS: u64 = 3600 * 24 * 30;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]

View File

@@ -1,4 +1,4 @@
use crate::error::Error::{InvalidSyncFile, UnknownModel};
use crate::error::Error::UnknownModel;
use crate::error::Result;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
@@ -23,26 +23,29 @@ pub enum SyncModel {
}
impl SyncModel {
pub fn from_bytes(
content: Vec<u8>,
file_path: &Path,
) -> Result<Option<(SyncModel, Vec<u8>, String)>> {
pub fn from_bytes(content: Vec<u8>, file_path: &Path) -> Result<Option<(SyncModel, String)>> {
let mut hasher = Sha1::new();
hasher.update(&content);
let checksum = hex::encode(hasher.finalize());
let content_str = String::from_utf8(content.clone()).unwrap_or_default();
// Check for some strings that will be in a model file for sure. If these strings
// don't exist, then it's probably not a Yaak file.
if !content_str.contains("model") || !content_str.contains("id") {
return Ok(None);
}
let ext = file_path.extension().unwrap_or_default();
if ext == "yml" || ext == "yaml" {
Ok(Some((serde_yaml::from_slice(content.as_slice())?, content, checksum)))
Ok(Some((serde_yaml::from_str(&content_str)?, checksum)))
} else if ext == "json" {
Ok(Some((serde_json::from_reader(content.as_slice())?, content, checksum)))
Ok(Some((serde_json::from_str(&content_str)?, checksum)))
} else {
let p = file_path.to_str().unwrap().to_string();
Err(InvalidSyncFile(format!("Unknown file extension {p}")))
Ok(None)
}
}
pub fn from_file(file_path: &Path) -> Result<Option<(SyncModel, Vec<u8>, String)>> {
pub fn from_file(file_path: &Path) -> Result<Option<(SyncModel, String)>> {
let content = match fs::read(file_path) {
Ok(c) => c,
Err(_) => return Ok(None),

View File

@@ -163,7 +163,7 @@ pub(crate) async fn get_fs_candidates(dir: &Path) -> Result<Vec<FsCandidate>> {
};
let path = dir_entry.path();
let (model, _, checksum) = match SyncModel::from_file(&path) {
let (model, checksum) = match SyncModel::from_file(&path) {
Ok(Some(m)) => m,
Ok(None) => continue,
Err(e) => {

View File

@@ -7,7 +7,7 @@ import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { showConfirm } from '../lib/confirm';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
import { pluralizeCount } from '../lib/pluralize';
import { showPrompt } from '../lib/prompt';
import { invokeCmd } from '../lib/tauri';
@@ -70,14 +70,15 @@ export const syncWorkspace = createFastMutation<
(o) => o.type === 'dbDelete' && o.model.model === 'workspace',
);
console.log('Filesystem changes detected', { dbOps, ops });
console.log('Directory changes detected', { dbOps, ops });
const confirmed = force
? true
: await showConfirm({
id: 'commit-sync',
title: 'Filesystem Changes Detected',
title: 'Changes Detected',
confirmText: 'Apply Changes',
color: isDeletingWorkspace ? 'danger' : 'primary',
description: (
<VStack space={3}>
{isDeletingWorkspace && (
@@ -86,8 +87,8 @@ export const syncWorkspace = createFastMutation<
</Banner>
)}
<p>
{pluralizeCount('file', dbOps.length)} in the directory have changed. Do you want to
apply the updates to your workspace?
{pluralizeCount('file', dbOps.length)} in the directory{' '}
{dbOps.length === 1 ? 'has' : 'have'} changed. Do you want to update your workspace?
</p>
<div className="overflow-y-auto max-h-[10rem]">
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
@@ -105,15 +106,15 @@ export const syncWorkspace = createFastMutation<
if (op.type === 'dbCreate') {
label = 'create';
name = fallbackRequestName(op.fs.model);
name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-success';
} else if (op.type === 'dbUpdate') {
label = 'update';
name = fallbackRequestName(op.fs.model);
name = resolvedModelNameWithFolders(op.fs.model);
color = 'text-info';
} else if (op.type === 'dbDelete') {
label = 'delete';
name = fallbackRequestName(op.model);
name = resolvedModelNameWithFolders(op.model);
color = 'text-danger';
} else {
return null;

View File

@@ -4,7 +4,7 @@ import { InlineCode } from '../components/core/InlineCode';
import { createFastMutation } from '../hooks/useFastMutation';
import { trackEvent } from '../lib/analytics';
import { showConfirmDelete } from '../lib/confirm';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
export const deleteWebsocketRequest = createFastMutation({
mutationKey: ['delete_websocket_request'],
@@ -14,7 +14,7 @@ export const deleteWebsocketRequest = createFastMutation({
title: 'Delete WebSocket Request',
description: (
<>
Permanently delete <InlineCode>{fallbackRequestName(request)}</InlineCode>?
Permanently delete <InlineCode>{resolvedModelName(request)}</InlineCode>?
</>
),
});

View File

@@ -28,7 +28,7 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { showDialog, toggleDialog } from '../lib/dialog';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelNameWithFolders } from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import { CookieDialog } from './CookieDialog';
@@ -270,11 +270,11 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
for (const r of sortedRequests) {
requestGroup.items.push({
key: `switch-request-${r.id}`,
searchText: fallbackRequestName(r),
searchText: resolvedModelNameWithFolders(r),
label: (
<HStack space={2}>
<HttpMethodTag className="text-text-subtlest" request={r} />
<div className="truncate">{fallbackRequestName(r)}</div>
<div className="truncate">{resolvedModelNameWithFolders(r)}</div>
</HStack>
),
onSelect: async () => {

View File

@@ -15,7 +15,7 @@ import { useActiveRequest } from '../hooks/useActiveRequest';
import { useFolders } from '../hooks/useFolders';
import { useHttpRequests } from '../hooks/useHttpRequests';
import { capitalize } from '../lib/capitalize';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Banner } from './core/Banner';
import { Checkbox } from './core/Checkbox';
import { Editor } from './core/Editor/Editor';
@@ -386,7 +386,7 @@ function buildRequestBreadcrumbs(request: HttpRequest, folders: Folder[]): strin
};
next();
return ancestors.map((a) => (a.model === 'folder' ? a.name : fallbackRequestName(a)));
return ancestors.map((a) => (a.model === 'folder' ? a.name : resolvedModelName(a)));
}
function CheckboxArg({

View File

@@ -11,8 +11,8 @@ import type {
import classNames from 'classnames';
import { useMemo, useState } from 'react';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { showToast } from '../lib/toast';
import { resolvedModelName } from '../lib/resolvedModelName';
import { showErrorToast, showToast } from '../lib/toast';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
import type { CheckboxProps } from './core/Checkbox';
@@ -20,6 +20,7 @@ import { Checkbox } from './core/Checkbox';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { Input } from './core/Input';
import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout';
import { HStack } from './core/Stacks';
import { EmptyStateText } from './EmptyStateText';
@@ -42,29 +43,46 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
const [message, setMessage] = useState<string>('');
const handleCreateCommit = async () => {
await commit.mutateAsync({ message });
onDone();
try {
await commit.mutateAsync({ message });
onDone();
} catch (err) {
showErrorToast('git-commit-error', String(err));
}
};
const handleCreateCommitAndPush = async () => {
await commit.mutateAsync({ message });
await push.mutateAsync();
showToast({ id: 'git-push-success', message: 'Pushed changes', color: 'success' });
onDone();
try {
await commit.mutateAsync({ message });
await push.mutateAsync();
showToast({ id: 'git-push-success', message: 'Pushed changes', color: 'success' });
onDone();
} catch (err) {
showErrorToast('git-commit-and-push-error', String(err));
}
};
const entries = status.data?.entries ?? null;
const { internalEntries, externalEntries, allEntries } = useMemo(() => {
const allEntries = [];
const yaakEntries = [];
const externalEntries = [];
for (const entry of status.data?.entries ?? []) {
allEntries.push(entry);
if (entry.next == null && entry.prev == null) {
externalEntries.push(entry);
} else {
yaakEntries.push(entry);
}
}
return { internalEntries: yaakEntries, externalEntries, allEntries };
}, [status.data?.entries]);
const hasAddedAnything = entries?.find((s) => s.staged) != null;
const hasAnythingToAdd = entries?.find((s) => s.status !== 'current') != null;
const hasAddedAnything = allEntries.find((e) => e.staged) != null;
const hasAnythingToAdd = allEntries.find((e) => e.status !== 'current') != null;
const tree: TreeNode | null = useMemo(() => {
if (entries == null) {
return null;
}
const next = (model: TreeNode['model'], ancestors: TreeNode[]): TreeNode | null => {
const statusEntry = entries?.find((s) => s.relaPath.includes(model.id));
const statusEntry = internalEntries?.find((s) => s.relaPath.includes(model.id));
if (statusEntry == null) {
return null;
}
@@ -76,9 +94,11 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
ancestors,
};
for (const entry of entries) {
for (const entry of internalEntries) {
const childModel = entry.next ?? entry.prev;
if (childModel == null) return null; // TODO: Is this right?
// Should never happen because we're iterating internalEntries
if (childModel == null) continue;
// TODO: Figure out why not all of these show up
if ('folderId' in childModel && childModel.folderId != null) {
@@ -96,8 +116,9 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
return node;
};
return next(workspace, []);
}, [entries, workspace]);
}, [workspace, internalEntries]);
if (tree == null) {
return null;
@@ -114,6 +135,11 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
// TODO: Also ensure parents are added properly
};
const checkEntry = (entry: GitStatusEntry) => {
if (entry.staged) unstage.mutate({ relaPaths: [entry.relaPath] });
else add.mutate({ relaPaths: [entry.relaPath] });
};
return (
<div className="grid grid-rows-1 h-full">
<SplitLayout
@@ -123,6 +149,18 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
firstSlot={({ style }) => (
<div style={style} className="h-full overflow-y-auto -ml-1 pb-3">
<TreeNodeChildren node={tree} depth={0} onCheck={checkNode} />
{externalEntries.find((e) => e.status !== 'current') && (
<>
<Separator className="mt-3 mb-1">External file changes</Separator>
{externalEntries.map((entry) => (
<ExternalTreeNode
key={entry.relaPath + entry.status}
entry={entry}
onCheck={checkEntry}
/>
))}
</>
)}
</div>
)}
secondSlot={({ style }) => (
@@ -211,17 +249,13 @@ function TreeNodeChildren({
) : (
<span aria-hidden />
)}
<div className="truncate">
{fallbackRequestName(node.model)}
{/*({node.model.model})*/}
{/*({node.status.staged ? 'Y' : 'N'})*/}
</div>
<div className="truncate">{resolvedModelName(node.model)}</div>
{node.status.status !== 'current' && (
<InlineCode
className={classNames(
'py-0 ml-auto bg-transparent w-[6rem] text-center',
node.status.status === 'modified' && 'text-info',
node.status.status === 'added' && 'text-success',
node.status.status === 'untracked' && 'text-success',
node.status.status === 'removed' && 'text-danger',
)}
>
@@ -247,6 +281,43 @@ function TreeNodeChildren({
);
}
function ExternalTreeNode({
entry,
onCheck,
}: {
entry: GitStatusEntry;
onCheck: (entry: GitStatusEntry) => void;
}) {
if (entry.status === 'current') {
return null;
}
return (
<Checkbox
fullWidth
className="h-xs w-full hover:bg-surface-highlight rounded px-1 group"
checked={entry.staged}
onChange={() => onCheck(entry)}
title={
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] gap-1 w-full items-center">
<Icon color="secondary" icon="file_code" />
<div className="truncate">{entry.relaPath}</div>
<InlineCode
className={classNames(
'py-0 ml-auto bg-transparent w-[6rem] text-center',
entry.status === 'modified' && 'text-info',
entry.status === 'untracked' && 'text-success',
entry.status === 'removed' && 'text-danger',
)}
>
{entry.status}
</InlineCode>
</div>
}
/>
);
}
function nodeCheckedStatus(root: TreeNode): CheckboxProps['checked'] {
let numVisited = 0;
let numChecked = 0;

View File

@@ -9,7 +9,7 @@ import type { ReflectResponseService } from '../hooks/useGrpc';
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { Button } from './core/Button';
import { CountBadge } from './core/CountBadge';
import { Icon } from './core/Icon';
@@ -343,7 +343,7 @@ export function GrpcConnectionSetupPane({
defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0"
containerClassName="border-0"
placeholder={fallbackRequestName(activeRequest)}
placeholder={resolvedModelName(activeRequest)}
onChange={(name) => updateRequest.mutate({ id: activeRequest.id, update: { name } })}
/>
<MarkdownEditor

View File

@@ -58,7 +58,7 @@ export function HttpAuthenticationEditor({ request }: Props) {
onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })}
title="Enabled"
/>
{authConfig.data.actions && (
{authConfig.data.actions && authConfig.data.actions.length > 0 && (
<Dropdown
items={authConfig.data.actions.map(
(a): DropdownItem => ({

View File

@@ -20,7 +20,7 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { generateId } from '../lib/generateId';
import {
BODY_TYPE_BINARY,
@@ -468,7 +468,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0"
containerClassName="border-0"
placeholder={fallbackRequestName(activeRequest)}
placeholder={resolvedModelName(activeRequest)}
onChange={(name) => updateRequest({ id: activeRequestId, update: { name } })}
/>
<MarkdownEditor

View File

@@ -4,7 +4,7 @@ import { upsertWebsocketRequest } from '../commands/upsertWebsocketRequest';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { router } from '../lib/router';
import { showToast } from '../lib/toast';
import { Button } from './core/Button';
@@ -59,7 +59,7 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr
id: 'workspace-moved',
message: (
<>
<InlineCode>{fallbackRequestName(request)}</InlineCode> moved to{' '}
<InlineCode>{resolvedModelName(request)}</InlineCode> moved to{' '}
<InlineCode>
{workspaces.find((w) => w.id === selectedWorkspaceId)?.name ?? 'unknown'}
</InlineCode>

View File

@@ -53,7 +53,7 @@ export function RecentGrpcConnectionsDropdown({
<IconButton
title="Show connection history"
icon={activeConnection?.id === latestConnectionId ? 'chevron_down' : 'pin'}
className="m-0.5"
className="m-0.5 text-text-subtle"
size="sm"
iconSize="md"
/>

View File

@@ -81,7 +81,7 @@ export const RecentHttpResponsesDropdown = function ResponsePane({
<IconButton
title="Show response history"
icon={activeResponse?.id === latestResponseId ? 'chevron_down' : 'pin'}
className="m-0.5"
className="m-0.5 text-text-subtle"
size="sm"
iconSize="md"
/>

View File

@@ -6,7 +6,7 @@ import { useHotKey } from '../hooks/useHotKey';
import { useKeyboardEvent } from '../hooks/useKeyboardEvent';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { requestsAtom } from '../hooks/useRequests';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { jotaiStore } from '../lib/jotai';
import { router } from '../lib/router';
import { Button } from './core/Button';
@@ -57,7 +57,7 @@ export function RecentRequestsDropdown({ className }: Props) {
if (request === undefined) continue;
recentRequestItems.push({
label: fallbackRequestName(request),
label: resolvedModelName(request),
leftSlot: <HttpMethodTag request={request} />,
onSelect: async () => {
await router.navigate({
@@ -94,7 +94,7 @@ export function RecentRequestsDropdown({ className }: Props) {
activeRequest == null && 'text-text-subtlest italic',
)}
>
{fallbackRequestName(activeRequest)}
{resolvedModelName(activeRequest)}
</Button>
</Dropdown>
);

View File

@@ -60,7 +60,7 @@ export function RecentWebsocketConnectionsDropdown({
<IconButton
title="Show connection history"
icon={activeConnection?.id === latestConnectionId ? 'chevron_down' : 'pin'}
className="m-0.5"
className="m-0.5 text-text-subtle"
size="sm"
iconSize="md"
/>

View File

@@ -1,5 +1,5 @@
import { useLicense } from '@yaakapp-internal/license';
import { formatDistanceToNow } from 'date-fns';
import { formatDistanceToNowStrict } from 'date-fns';
import React, { useState } from 'react';
import { useToggle } from '../../hooks/useToggle';
import { Banner } from '../core/Banner';
@@ -30,7 +30,7 @@ export function SettingsLicense() {
{check.data?.type === 'trialing' && (
<p className="select-text">
<strong>
You have {formatDistanceToNow(check.data.end)} remaining on your trial.
You have {formatDistanceToNowStrict(check.data.end)} remaining on your trial.
</strong>
</p>
)}
@@ -40,11 +40,6 @@ export function SettingsLicense() {
Learn More
</Link>
</p>
<p className="select-text">
This supports future development and ensures continued growth and improvement. Personal
use and running the open-source code directly require no license.
</p>
<p>~ Gregory</p>
</Banner>
)}

View File

@@ -21,7 +21,7 @@ import { useLatestWebsocketConnection } from '../hooks/useWebsocketConnections';
import { trackEvent } from '../lib/analytics';
import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { generateId } from '../lib/generateId';
import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/Editor';
@@ -303,7 +303,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
defaultValue={activeRequest.name}
className="font-sans !text-xl !px-0"
containerClassName="border-0"
placeholder={fallbackRequestName(activeRequest)}
placeholder={resolvedModelName(activeRequest)}
onChange={(name) => upsertWebsocketRequest.mutate({ ...activeRequest, name })}
/>
<MarkdownEditor

View File

@@ -14,8 +14,8 @@ export function Banner({ children, className, color }: Props) {
className,
`x-theme-banner--${color}`,
'whitespace-pre-wrap',
'border border-dashed border-border bg-surface',
'px-3 py-2 rounded select-auto',
'border border-border bg-surface',
'px-4 py-3 rounded-lg select-auto',
'overflow-auto h-auto mb-auto text-text',
)}
>

View File

@@ -136,7 +136,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
{children}
</div>
{rightSlot && <div className="ml-1">{rightSlot}</div>}
{forDropdown && <Icon icon="chevron_down" size={size} className="ml-1 -mr-1" />}
{forDropdown && <Icon icon="check" size={size} className="ml-1 -mr-1" />}
</button>
);
});

View File

@@ -121,7 +121,10 @@ export function Tabs({
<Icon
size="sm"
icon="chevron_down"
className={classNames('ml-1', !isActive && 'opacity-50')}
className={classNames(
'ml-1',
isActive ? 'text-text-subtle' : 'text-text-subtlest',
)}
/>
</button>
</RadioDropdown>

View File

@@ -27,8 +27,8 @@ export function HistoryDialog({ log }: Props) {
<TableBody>
{log.map((l, i) => (
<TableRow key={i}>
<TruncatedWideTableCell>{l.message}</TruncatedWideTableCell>
<TableCell className="font-bold">{l.author.name ?? 'Unknown'}</TableCell>
<TruncatedWideTableCell>{l.message || <em className="text-text-subtle">No message</em>}</TruncatedWideTableCell>
<TableCell>{l.author.name ?? 'Unknown'}</TableCell>
<TableCell className="text-text-subtle">
<span title={l.when}>{formatDistanceToNowStrict(l.when)} ago</span>
</TableCell>

View File

@@ -6,7 +6,7 @@ import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { foldersAtom } from '../../hooks/useFolders';
import { requestsAtom } from '../../hooks/useRequests';
import { deepEqualAtom } from '../../lib/atoms';
import { fallbackRequestName } from '../../lib/fallbackRequestName';
import { resolvedModelName } from '../../lib/resolvedModelName';
import type { SidebarTreeNode } from './Sidebar';
export const sidebarSelectedIdAtom = atom<string | null>(null);
@@ -18,7 +18,7 @@ const allPotentialChildrenAtom = atom((get) => {
id: v.id,
model: v.model,
folderId: v.folderId,
name: fallbackRequestName(v),
name: resolvedModelName(v),
workspaceId: v.workspaceId,
sortPriority: v.sortPriority,
}));

View File

@@ -2,7 +2,7 @@ import type { GrpcRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirmDelete } from '../lib/confirm';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
@@ -15,7 +15,7 @@ export function useDeleteAnyGrpcRequest() {
title: 'Delete Request',
description: (
<>
Permanently delete <InlineCode>{fallbackRequestName(request)}</InlineCode>?
Permanently delete <InlineCode>{resolvedModelName(request)}</InlineCode>?
</>
),
});

View File

@@ -2,7 +2,7 @@ import type { HttpRequest } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import { showConfirmDelete } from '../lib/confirm';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { invokeCmd } from '../lib/tauri';
import { useFastMutation } from './useFastMutation';
@@ -15,7 +15,7 @@ export function useDeleteAnyHttpRequest() {
title: 'Delete Request',
description: (
<>
Permanently delete <InlineCode>{fallbackRequestName(request)}</InlineCode>?
Permanently delete <InlineCode>{resolvedModelName(request)}</InlineCode>?
</>
),
});

View File

@@ -1,7 +1,7 @@
import { emit } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { resolvedModelName } from '../lib/resolvedModelName';
import { useActiveEnvironment } from './useActiveEnvironment';
import { getActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace';
@@ -26,7 +26,7 @@ export function useSyncWorkspaceRequestTitle() {
const activeRequest = getActiveRequest();
if (activeRequest) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
newTitle += ` ${fallbackRequestName(activeRequest)}`;
newTitle += ` ${resolvedModelName(activeRequest)}`;
}
if (appInfo.isDev) {

View File

@@ -10,6 +10,7 @@ import { jotaiStore } from '../lib/jotai';
export function initSync() {
initModelListeners();
initFileChangeListeners();
sync().catch(console.error);
}
export async function sync({ force }: { force?: boolean } = {}) {
@@ -53,6 +54,7 @@ function initFileChangeListeners() {
await unsub?.(); // Unsub to previous
const workspaceMeta = jotaiStore.get(workspaceMetaAtom);
if (workspaceMeta == null || workspaceMeta.settingSyncDir == null) return;
debouncedSync(); // Perform an initial sync when switching workspace
unsub = watchWorkspaceFiles(
workspaceMeta.workspaceId,
workspaceMeta.settingSyncDir,

View File

@@ -1,6 +1,8 @@
import type { AnyModel } from '@yaakapp-internal/models';
import { foldersAtom } from '../hooks/useFolders';
import { jotaiStore } from './jotai';
export function fallbackRequestName(r: AnyModel | null): string {
export function resolvedModelName(r: AnyModel | null): string {
if (r == null) return '';
if (!('url' in r) || r.model === 'plugin') {
@@ -33,3 +35,21 @@ export function fallbackRequestName(r: AnyModel | null): string {
return withoutProto;
}
export function resolvedModelNameWithFolders(model: AnyModel | null): string {
if (model == null) return '';
const folders = jotaiStore.get(foldersAtom) ?? [];
const getParents = (m: AnyModel, names: string[]) => {
names = [...names, resolvedModelName(m)];
if ('folderId' in m) {
const parent = folders.find((f) => f.id === m.folderId);
if (parent) {
names = [resolvedModelName(parent), ...names];
}
}
return names;
};
return getParents(model, []).join(' / ');
}

View File

@@ -113,8 +113,8 @@ function bannerColorVariables(color: YaakColor): Partial<CSSVariables> {
text: color.lift(0.8),
textSubtle: color.translucify(0.3),
textSubtlest: color,
surface: color.translucify(0.9),
border: color.lift(0.3).translucify(0.4),
surface: color.translucify(0.95),
border: color.lift(0.3).translucify(0.8),
};
}