Run oxfmt across repo, add format script and docs

Add .oxfmtignore to skip generated bindings and wasm-pack output.
Add npm format script, update DEVELOPMENT.md for Vite+ toolchain,
and format all non-generated files with oxfmt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-03-13 10:15:49 -07:00
parent 45262edfbd
commit b4a1c418bb
664 changed files with 13638 additions and 13492 deletions

View File

@@ -1,16 +1,16 @@
import type { AlertProps } from '../components/core/Alert';
import { Alert } from '../components/core/Alert';
import type { DialogProps } from '../components/core/Dialog';
import { showDialog } from './dialog';
import type { AlertProps } from "../components/core/Alert";
import { Alert } from "../components/core/Alert";
import type { DialogProps } from "../components/core/Dialog";
import { showDialog } from "./dialog";
interface AlertArgs {
id: string;
title: DialogProps['title'];
body: AlertProps['body'];
size?: DialogProps['size'];
title: DialogProps["title"];
body: AlertProps["body"];
size?: DialogProps["size"];
}
export function showAlert({ id, title, body, size = 'sm' }: AlertArgs) {
export function showAlert({ id, title, body, size = "sm" }: AlertArgs) {
showDialog({
id,
title,
@@ -23,7 +23,7 @@ export function showAlert({ id, title, body, size = 'sm' }: AlertArgs) {
export function showSimpleAlert(title: string, message: string) {
showAlert({
id: 'simple-alert',
id: "simple-alert",
body: message,
title: title,
});

View File

@@ -1,5 +1,5 @@
import { getIdentifier } from '@tauri-apps/api/app';
import { invokeCmd } from './tauri';
import { getIdentifier } from "@tauri-apps/api/app";
import { invokeCmd } from "./tauri";
export interface AppInfo {
isDev: boolean;
@@ -16,8 +16,8 @@ export interface AppInfo {
}
export const appInfo = {
...(await invokeCmd('cmd_metadata')),
...(await invokeCmd("cmd_metadata")),
identifier: await getIdentifier(),
} as AppInfo;
console.log('App info', appInfo);
console.log("App info", appInfo);

View File

@@ -1,10 +1,10 @@
import deepEqual from '@gilbarbara/deep-equal';
import type { UpdateInfo } from '@yaakapp-internal/tauri';
import type { Atom } from 'jotai';
import { atom } from 'jotai';
import { selectAtom } from 'jotai/utils';
import type { SplitLayoutLayout } from '../components/core/SplitLayout';
import { atomWithKVStorage } from './atoms/atomWithKVStorage';
import deepEqual from "@gilbarbara/deep-equal";
import type { UpdateInfo } from "@yaakapp-internal/tauri";
import type { Atom } from "jotai";
import { atom } from "jotai";
import { selectAtom } from "jotai/utils";
import type { SplitLayoutLayout } from "../components/core/SplitLayout";
import { atomWithKVStorage } from "./atoms/atomWithKVStorage";
export function deepEqualAtom<T>(a: Atom<T>) {
return selectAtom(
@@ -15,8 +15,8 @@ export function deepEqualAtom<T>(a: Atom<T>) {
}
export const workspaceLayoutAtom = atomWithKVStorage<SplitLayoutLayout>(
'workspace_layout',
'horizontal',
"workspace_layout",
"horizontal",
);
export const updateAvailableAtom = atom<Omit<UpdateInfo, 'replyEventId'> | null>(null);
export const updateAvailableAtom = atom<Omit<UpdateInfo, "replyEventId"> | null>(null);

View File

@@ -1,10 +1,10 @@
import { atom } from 'jotai';
import { getKeyValue, setKeyValue } from '../keyValueStore';
import { atom } from "jotai";
import { getKeyValue, setKeyValue } from "../keyValueStore";
export function atomWithKVStorage<T extends object | boolean | number | string | null>(
key: string | string[],
fallback: T,
namespace = 'global',
namespace = "global",
) {
const baseAtom = atom<T>(fallback);
@@ -15,7 +15,7 @@ export function atomWithKVStorage<T extends object | boolean | number | string |
const derivedAtom = atom<T, [T | ((prev: T) => T)], void>(
(get) => get(baseAtom),
(get, set, update) => {
const nextValue = typeof update === 'function' ? update(get(baseAtom)) : update;
const nextValue = typeof update === "function" ? update(get(baseAtom)) : update;
set(baseAtom, nextValue);
setKeyValue({ namespace, key, value: nextValue }).catch(console.error);
},

View File

@@ -1,6 +1,6 @@
export function capitalize(str: string): string {
return str
.split(' ')
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
.join(" ");
}

View File

@@ -1,4 +1,4 @@
import type { Color } from '@yaakapp-internal/plugins';
import type { Color } from "@yaakapp-internal/plugins";
const colors: Record<Color, boolean> = {
primary: true,

View File

@@ -1,21 +1,21 @@
import type { ConfirmProps } from '../components/core/Confirm';
import { Confirm } from '../components/core/Confirm';
import type { DialogProps } from '../components/core/Dialog';
import { showDialog } from './dialog';
import type { ConfirmProps } from "../components/core/Confirm";
import { Confirm } from "../components/core/Confirm";
import type { DialogProps } from "../components/core/Dialog";
import { showDialog } from "./dialog";
type ConfirmArgs = {
id: string;
} & Pick<DialogProps, 'title' | 'description' | 'size'> &
Pick<ConfirmProps, 'color' | 'confirmText' | 'requireTyping'>;
} & Pick<DialogProps, "title" | "description" | "size"> &
Pick<ConfirmProps, "color" | "confirmText" | "requireTyping">;
export async function showConfirm({
color,
confirmText,
requireTyping,
size = 'sm',
size = "sm",
...extraProps
}: ConfirmArgs) {
return new Promise((onResult: ConfirmProps['onResult']) => {
return new Promise((onResult: ConfirmProps["onResult"]) => {
showDialog({
...extraProps,
hideX: true,
@@ -28,8 +28,8 @@ export async function showConfirm({
export async function showConfirmDelete({ confirmText, color, ...extraProps }: ConfirmArgs) {
return showConfirm({
color: color ?? 'danger',
confirmText: confirmText ?? 'Delete',
color: color ?? "danger",
confirmText: confirmText ?? "Delete",
...extraProps,
});
}

View File

@@ -1,4 +1,4 @@
export const HEADER_SIZE_MD = '30px';
export const HEADER_SIZE_LG = '40px';
export const HEADER_SIZE_MD = "30px";
export const HEADER_SIZE_LG = "40px";
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
export const WINDOW_CONTROLS_WIDTH = "10.5rem";

View File

@@ -1,62 +1,62 @@
import MimeType from 'whatwg-mimetype';
import type { EditorProps } from '../components/core/Editor/Editor';
import MimeType from "whatwg-mimetype";
import type { EditorProps } from "../components/core/Editor/Editor";
export function languageFromContentType(
contentType: string | null,
content: string | null = null,
): EditorProps['language'] {
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
if (justContentType.includes('json')) {
return 'json';
): EditorProps["language"] {
const justContentType = contentType?.split(";")[0] ?? contentType ?? "";
if (justContentType.includes("json")) {
return "json";
}
if (justContentType.includes('xml')) {
return 'xml';
if (justContentType.includes("xml")) {
return "xml";
}
if (justContentType.includes('html')) {
if (justContentType.includes("html")) {
const detected = languageFromContent(content);
if (detected === 'xml') {
if (detected === "xml") {
// If it's detected as XML, but is already HTML, don't change it
return 'html';
return "html";
}
return detected;
}
if (justContentType.includes('javascript')) {
if (justContentType.includes("javascript")) {
// Sometimes `application/javascript` returns JSON, so try detecting that
return languageFromContent(content, 'javascript');
return languageFromContent(content, "javascript");
}
if (justContentType.includes('markdown')) {
return 'markdown';
if (justContentType.includes("markdown")) {
return "markdown";
}
return languageFromContent(content, 'text');
return languageFromContent(content, "text");
}
export function languageFromContent(
content: string | null,
fallback?: EditorProps['language'],
): EditorProps['language'] {
if (content == null) return 'text';
fallback?: EditorProps["language"],
): EditorProps["language"] {
if (content == null) return "text";
const firstBytes = content.slice(0, 20).trim();
if (firstBytes.startsWith('{') || firstBytes.startsWith('[')) {
return 'json';
if (firstBytes.startsWith("{") || firstBytes.startsWith("[")) {
return "json";
}
if (
firstBytes.toLowerCase().startsWith('<!doctype') ||
firstBytes.toLowerCase().startsWith('<html')
firstBytes.toLowerCase().startsWith("<!doctype") ||
firstBytes.toLowerCase().startsWith("<html")
) {
return 'html';
return "html";
}
if (firstBytes.startsWith('<')) {
return 'xml';
if (firstBytes.startsWith("<")) {
return "xml";
}
return fallback;
}
export function isJSON(content: string | null | undefined): boolean {
if (typeof content !== 'string') return false;
if (typeof content !== "string") return false;
try {
JSON.parse(content);
@@ -73,20 +73,20 @@ export function isProbablyTextContentType(contentType: string | null): boolean {
const normalized = mimeType.toLowerCase();
// Check if it starts with "text/"
if (normalized.startsWith('text/')) {
if (normalized.startsWith("text/")) {
return true;
}
// Common text mimetypes and suffixes
return [
'application/json',
'application/xml',
'application/javascript',
'application/yaml',
'+json',
'+xml',
'+yaml',
'+text',
"application/json",
"application/xml",
"application/javascript",
"application/yaml",
"+json",
"+xml",
"+yaml",
"+text",
].some((textType) => normalized === textType || normalized.endsWith(textType));
}
@@ -94,6 +94,6 @@ export function getMimeTypeFromContentType(contentType: string): MimeType {
try {
return new MimeType(contentType);
} catch {
return new MimeType('text/plain');
return new MimeType("text/plain");
}
}

View File

@@ -1,5 +1,5 @@
import { clear, writeText } from '@tauri-apps/plugin-clipboard-manager';
import { showToast } from './toast';
import { clear, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { showToast } from "./toast";
export function copyToClipboard(
text: string | null,
@@ -11,12 +11,12 @@ export function copyToClipboard(
writeText(text).catch(console.error);
}
if (text !== '' && !disableToast) {
if (text !== "" && !disableToast) {
showToast({
id: 'copied',
color: 'success',
icon: 'copy',
message: 'Copied to clipboard',
id: "copied",
color: "success",
icon: "copy",
message: "Copied to clipboard",
});
}
}

View File

@@ -1,12 +1,12 @@
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { createWorkspaceModel } from '@yaakapp-internal/models';
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { jotaiStore } from './jotai';
import { router } from './router';
import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
import { createWorkspaceModel } from "@yaakapp-internal/models";
import { activeRequestAtom } from "../hooks/useActiveRequest";
import { jotaiStore } from "./jotai";
import { router } from "./router";
export async function createRequestAndNavigate<
T extends HttpRequest | GrpcRequest | WebsocketRequest,
>(patch: Partial<T> & Pick<T, 'model' | 'workspaceId'>) {
>(patch: Partial<T> & Pick<T, "model" | "workspaceId">) {
const activeRequest = jotaiStore.get(activeRequestAtom);
if (patch.sortPriority === undefined) {
@@ -23,7 +23,7 @@ export async function createRequestAndNavigate<
const newId = await createWorkspaceModel(patch);
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId: patch.workspaceId },
search: (prev) => ({ ...prev, request_id: newId }),
});

View File

@@ -1,121 +1,121 @@
export const charsets = [
'utf-8',
'us-ascii',
'950',
'ASMO-708',
'CP1026',
'CP870',
'DOS-720',
'DOS-862',
'EUC-CN',
'IBM437',
'Johab',
'Windows-1252',
'X-EBCDIC-Spain',
'big5',
'cp866',
'csISO2022JP',
'ebcdic-cp-us',
'euc-kr',
'gb2312',
'hz-gb-2312',
'ibm737',
'ibm775',
'ibm850',
'ibm852',
'ibm857',
'ibm861',
'ibm869',
'iso-2022-jp',
'iso-2022-jp',
'iso-2022-kr',
'iso-8859-1',
'iso-8859-15',
'iso-8859-2',
'iso-8859-3',
'iso-8859-4',
'iso-8859-5',
'iso-8859-6',
'iso-8859-7',
'iso-8859-8',
'iso-8859-8-i',
'iso-8859-9',
'koi8-r',
'koi8-u',
'ks_c_5601-1987',
'macintosh',
'shift_jis',
'unicode',
'unicodeFFFE',
'utf-7',
'windows-1250',
'windows-1251',
'windows-1253',
'windows-1254',
'windows-1255',
'windows-1256',
'windows-1257',
'windows-1258',
'windows-874',
'x-Chinese-CNS',
'x-Chinese-Eten',
'x-EBCDIC-Arabic',
'x-EBCDIC-CyrillicRussian',
'x-EBCDIC-CyrillicSerbianBulgarian',
'x-EBCDIC-DenmarkNorway',
'x-EBCDIC-FinlandSweden',
'x-EBCDIC-Germany',
'x-EBCDIC-Greek',
'x-EBCDIC-GreekModern',
'x-EBCDIC-Hebrew',
'x-EBCDIC-Icelandic',
'x-EBCDIC-Italy',
'x-EBCDIC-JapaneseAndJapaneseLatin',
'x-EBCDIC-JapaneseAndKana',
'x-EBCDIC-JapaneseAndUSCanada',
'x-EBCDIC-JapaneseKatakana',
'x-EBCDIC-KoreanAndKoreanExtended',
'x-EBCDIC-KoreanExtended',
'x-EBCDIC-SimplifiedChinese',
'x-EBCDIC-Thai',
'x-EBCDIC-TraditionalChinese',
'x-EBCDIC-Turkish',
'x-EBCDIC-UK',
'x-Europa',
'x-IA5',
'x-IA5-German',
'x-IA5-Norwegian',
'x-IA5-Swedish',
'x-ebcdic-cp-us-euro',
'x-ebcdic-denmarknorway-euro',
'x-ebcdic-finlandsweden-euro',
'x-ebcdic-finlandsweden-euro',
'x-ebcdic-france-euro',
'x-ebcdic-germany-euro',
'x-ebcdic-icelandic-euro',
'x-ebcdic-international-euro',
'x-ebcdic-italy-euro',
'x-ebcdic-spain-euro',
'x-ebcdic-uk-euro',
'x-euc-jp',
'x-iscii-as',
'x-iscii-be',
'x-iscii-de',
'x-iscii-gu',
'x-iscii-ka',
'x-iscii-ma',
'x-iscii-or',
'x-iscii-pa',
'x-iscii-ta',
'x-iscii-te',
'x-mac-arabic',
'x-mac-ce',
'x-mac-chinesesimp',
'x-mac-cyrillic',
'x-mac-greek',
'x-mac-hebrew',
'x-mac-icelandic',
'x-mac-japanese',
'x-mac-korean',
'x-mac-turkish',
"utf-8",
"us-ascii",
"950",
"ASMO-708",
"CP1026",
"CP870",
"DOS-720",
"DOS-862",
"EUC-CN",
"IBM437",
"Johab",
"Windows-1252",
"X-EBCDIC-Spain",
"big5",
"cp866",
"csISO2022JP",
"ebcdic-cp-us",
"euc-kr",
"gb2312",
"hz-gb-2312",
"ibm737",
"ibm775",
"ibm850",
"ibm852",
"ibm857",
"ibm861",
"ibm869",
"iso-2022-jp",
"iso-2022-jp",
"iso-2022-kr",
"iso-8859-1",
"iso-8859-15",
"iso-8859-2",
"iso-8859-3",
"iso-8859-4",
"iso-8859-5",
"iso-8859-6",
"iso-8859-7",
"iso-8859-8",
"iso-8859-8-i",
"iso-8859-9",
"koi8-r",
"koi8-u",
"ks_c_5601-1987",
"macintosh",
"shift_jis",
"unicode",
"unicodeFFFE",
"utf-7",
"windows-1250",
"windows-1251",
"windows-1253",
"windows-1254",
"windows-1255",
"windows-1256",
"windows-1257",
"windows-1258",
"windows-874",
"x-Chinese-CNS",
"x-Chinese-Eten",
"x-EBCDIC-Arabic",
"x-EBCDIC-CyrillicRussian",
"x-EBCDIC-CyrillicSerbianBulgarian",
"x-EBCDIC-DenmarkNorway",
"x-EBCDIC-FinlandSweden",
"x-EBCDIC-Germany",
"x-EBCDIC-Greek",
"x-EBCDIC-GreekModern",
"x-EBCDIC-Hebrew",
"x-EBCDIC-Icelandic",
"x-EBCDIC-Italy",
"x-EBCDIC-JapaneseAndJapaneseLatin",
"x-EBCDIC-JapaneseAndKana",
"x-EBCDIC-JapaneseAndUSCanada",
"x-EBCDIC-JapaneseKatakana",
"x-EBCDIC-KoreanAndKoreanExtended",
"x-EBCDIC-KoreanExtended",
"x-EBCDIC-SimplifiedChinese",
"x-EBCDIC-Thai",
"x-EBCDIC-TraditionalChinese",
"x-EBCDIC-Turkish",
"x-EBCDIC-UK",
"x-Europa",
"x-IA5",
"x-IA5-German",
"x-IA5-Norwegian",
"x-IA5-Swedish",
"x-ebcdic-cp-us-euro",
"x-ebcdic-denmarknorway-euro",
"x-ebcdic-finlandsweden-euro",
"x-ebcdic-finlandsweden-euro",
"x-ebcdic-france-euro",
"x-ebcdic-germany-euro",
"x-ebcdic-icelandic-euro",
"x-ebcdic-international-euro",
"x-ebcdic-italy-euro",
"x-ebcdic-spain-euro",
"x-ebcdic-uk-euro",
"x-euc-jp",
"x-iscii-as",
"x-iscii-be",
"x-iscii-de",
"x-iscii-gu",
"x-iscii-ka",
"x-iscii-ma",
"x-iscii-or",
"x-iscii-pa",
"x-iscii-ta",
"x-iscii-te",
"x-mac-arabic",
"x-mac-ce",
"x-mac-chinesesimp",
"x-mac-cyrillic",
"x-mac-greek",
"x-mac-hebrew",
"x-mac-icelandic",
"x-mac-japanese",
"x-mac-korean",
"x-mac-turkish",
];

View File

@@ -1 +1 @@
export const connections = ['close', 'keep-alive'];
export const connections = ["close", "keep-alive"];

View File

@@ -1 +1 @@
export const encodings = ['*', 'gzip', 'compress', 'deflate', 'br', 'zstd', 'identity'];
export const encodings = ["*", "gzip", "compress", "deflate", "br", "zstd", "identity"];

View File

@@ -1,70 +1,70 @@
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
export const headerNames: (GenericCompletionOption | string)[] = [
{
type: 'constant',
label: 'Content-Type',
info: 'The original media type of the resource (prior to any content encoding applied for sending)',
type: "constant",
label: "Content-Type",
info: "The original media type of the resource (prior to any content encoding applied for sending)",
},
{
type: 'constant',
label: 'Content-Length',
info: 'The size of the message body, in bytes, sent to the recipient',
type: "constant",
label: "Content-Length",
info: "The size of the message body, in bytes, sent to the recipient",
},
{
type: 'constant',
label: 'Accept',
type: "constant",
label: "Accept",
info:
'The content types, expressed as MIME types, the client is able to understand. ' +
'The server uses content negotiation to select one of the proposals and informs ' +
'the client of the choice with the Content-Type response header. Browsers set required ' +
'values for this header based on the context of the request. For example, a browser uses ' +
'different values in a request when fetching a CSS stylesheet, image, video, or a script.',
"The content types, expressed as MIME types, the client is able to understand. " +
"The server uses content negotiation to select one of the proposals and informs " +
"the client of the choice with the Content-Type response header. Browsers set required " +
"values for this header based on the context of the request. For example, a browser uses " +
"different values in a request when fetching a CSS stylesheet, image, video, or a script.",
},
{
type: 'constant',
label: 'Accept-Encoding',
type: "constant",
label: "Accept-Encoding",
info:
'The content encoding (usually a compression algorithm) that the client can understand. ' +
'The server uses content negotiation to select one of the proposals and informs the client ' +
'of that choice with the Content-Encoding response header.',
"The content encoding (usually a compression algorithm) that the client can understand. " +
"The server uses content negotiation to select one of the proposals and informs the client " +
"of that choice with the Content-Encoding response header.",
},
{
type: 'constant',
label: 'Accept-Language',
type: "constant",
label: "Accept-Language",
info:
'The natural language and locale that the client prefers. The server uses content ' +
'negotiation to select one of the proposals and informs the client of the choice with ' +
'the Content-Language response header.',
"The natural language and locale that the client prefers. The server uses content " +
"negotiation to select one of the proposals and informs the client of the choice with " +
"the Content-Language response header.",
},
{
type: 'constant',
label: 'Authorization',
info: 'Provide credentials that authenticate a user agent with a server, allowing access to a protected resource.',
type: "constant",
label: "Authorization",
info: "Provide credentials that authenticate a user agent with a server, allowing access to a protected resource.",
},
'Cache-Control',
'Cookie',
'Connection',
'Content-MD5',
'Date',
'Expect',
'Forwarded',
'From',
'Host',
'If-Match',
'If-Modified-Since',
'If-None-Match',
'If-Range',
'If-Unmodified-Since',
'Max-Forwards',
'Origin',
'Pragma',
'Proxy-Authorization',
'Range',
'Referer',
'TE',
'User-Agent',
'Upgrade',
'Via',
'Warning',
"Cache-Control",
"Cookie",
"Connection",
"Content-MD5",
"Date",
"Expect",
"Forwarded",
"From",
"Host",
"If-Match",
"If-Modified-Since",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
"Max-Forwards",
"Origin",
"Pragma",
"Proxy-Authorization",
"Range",
"Referer",
"TE",
"User-Agent",
"Upgrade",
"Via",
"Warning",
];

View File

@@ -1,213 +1,213 @@
export const mimeTypes = [
'application/json',
'application/xml',
'application/x-www-form-urlencoded',
'multipart/form-data',
'multipart/byteranges',
'application/octet-stream',
'text/plain',
'application/javascript',
'application/pdf',
'text/html',
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'text/css',
'application/x-pkcs12',
'application/xhtml+xml',
'application/andrew-inset',
'application/applixware',
'application/atom+xml',
'application/atomcat+xml',
'application/atomsvc+xml',
'application/bdoc',
'application/cu-seeme',
'application/davmount+xml',
'application/docbook+xml',
'application/dssc+xml',
'application/ecmascript',
'application/epub+zip',
'application/exi',
'application/font-tdpfr',
'application/font-woff',
'application/font-woff2',
'application/geo+json',
'application/graphql',
'application/java-serialized-object',
'application/json5',
'application/jsonml+json',
'application/ld+json',
'application/lost+xml',
'application/manifest+json',
'application/mp4',
'application/msword',
'application/mxf',
'application/n-triples',
'application/n-quads',
'application/oda',
'application/ogg',
'application/pgp-encrypted',
'application/pgp-signature',
'application/pics-rules',
'application/pkcs10',
'application/pkcs7-mime',
'application/pkcs7-signature',
'application/pkcs8',
'application/postscript',
'application/pskc+xml',
'application/rdf+xml',
'application/resource-lists+xml',
'application/resource-lists-diff+xml',
'application/rls-services+xml',
'application/rsd+xml',
'application/rss+xml',
'application/rtf',
'application/sdp',
'application/shf+xml',
'application/timestamped-data',
'application/trig',
'application/vnd.android.package-archive',
'application/vnd.api+json',
'application/vnd.apple.installer+xml',
'application/vnd.apple.mpegurl',
'application/vnd.apple.pkpass',
'application/vnd.bmi',
'application/vnd.curl.car',
'application/vnd.curl.pcurl',
'application/vnd.dna',
'application/vnd.google-apps.document',
'application/vnd.google-apps.presentation',
'application/vnd.google-apps.spreadsheet',
'application/vnd.hal+xml',
'application/vnd.handheld-entertainment+xml',
'application/vnd.macports.portpkg',
'application/vnd.unity',
'application/vnd.zul',
'application/widget',
'application/wsdl+xml',
'application/x-7z-compressed',
'application/x-ace-compressed',
'application/x-bittorrent',
'application/x-bzip',
'application/x-bzip2',
'application/x-cfs-compressed',
'application/x-chrome-extension',
'application/x-cocoa',
'application/x-envoy',
'application/x-eva',
'font/opentype',
'application/x-gca-compressed',
'application/x-gtar',
'application/x-hdf',
'application/x-httpd-php',
'application/x-install-instructions',
'application/x-latex',
'application/x-lua-bytecode',
'application/x-lzh-compressed',
'application/x-ms-application',
'application/x-ms-shortcut',
'application/x-ndjson',
'application/x-perl',
'application/x-pkcs7-certificates',
'application/x-pkcs7-certreqresp',
'application/x-rar-compressed',
'application/x-sh',
'application/x-sql',
'application/x-subrip',
'application/x-t3vm-image',
'application/x-tads',
'application/x-tar',
'application/x-tcl',
'application/x-tex',
'application/x-x509-ca-cert',
'application/xop+xml',
'application/xslt+xml',
'application/zip',
'audio/3gpp',
'audio/adpcm',
'audio/basic',
'audio/midi',
'audio/mpeg',
'audio/mp4',
'audio/ogg',
'audio/silk',
'audio/wave',
'audio/webm',
'audio/x-aac',
'audio/x-aiff',
'audio/x-caf',
'audio/x-flac',
'audio/xm',
'image/bmp',
'image/cgm',
'image/sgi',
'image/svg+xml',
'image/tiff',
'image/x-3ds',
'image/x-freehand',
'image/x-icon',
'image/x-jng',
'image/x-mrsid-image',
'image/x-pcx',
'image/x-pict',
'image/x-rgb',
'image/x-tga',
'message/rfc822',
'text/cache-manifest',
'text/calendar',
'text/coffeescript',
'text/csv',
'text/hjson',
'text/jade',
'text/jsx',
'text/less',
'text/mathml',
'text/n3',
'text/richtext',
'text/sgml',
'text/slim',
'text/stylus',
'text/tab-separated-values',
'text/turtle',
'text/uri-list',
'text/vcard',
'text/vnd.curl',
'text/vnd.fly',
'text/vtt',
'text/x-asm',
'text/x-c',
'text/x-component',
'text/x-fortran',
'text/x-handlebars-template',
'text/x-java-source',
'text/x-lua',
'text/x-markdown',
'text/x-nfo',
'text/x-opml',
'text/x-pascal',
'text/x-processing',
'text/x-sass',
'text/x-scss',
'text/x-vcalendar',
'text/xml',
'text/yaml',
'video/3gpp',
'video/3gpp2',
'video/h261',
'video/h263',
'video/h264',
'video/jpeg',
'video/jpm',
'video/mj2',
'video/mp2t',
'video/mp4',
'video/mpeg',
'video/ogg',
'video/quicktime',
'video/webm',
'video/x-f4v',
'video/x-fli',
'video/x-flv',
'video/x-m4v',
"application/json",
"application/xml",
"application/x-www-form-urlencoded",
"multipart/form-data",
"multipart/byteranges",
"application/octet-stream",
"text/plain",
"application/javascript",
"application/pdf",
"text/html",
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"text/css",
"application/x-pkcs12",
"application/xhtml+xml",
"application/andrew-inset",
"application/applixware",
"application/atom+xml",
"application/atomcat+xml",
"application/atomsvc+xml",
"application/bdoc",
"application/cu-seeme",
"application/davmount+xml",
"application/docbook+xml",
"application/dssc+xml",
"application/ecmascript",
"application/epub+zip",
"application/exi",
"application/font-tdpfr",
"application/font-woff",
"application/font-woff2",
"application/geo+json",
"application/graphql",
"application/java-serialized-object",
"application/json5",
"application/jsonml+json",
"application/ld+json",
"application/lost+xml",
"application/manifest+json",
"application/mp4",
"application/msword",
"application/mxf",
"application/n-triples",
"application/n-quads",
"application/oda",
"application/ogg",
"application/pgp-encrypted",
"application/pgp-signature",
"application/pics-rules",
"application/pkcs10",
"application/pkcs7-mime",
"application/pkcs7-signature",
"application/pkcs8",
"application/postscript",
"application/pskc+xml",
"application/rdf+xml",
"application/resource-lists+xml",
"application/resource-lists-diff+xml",
"application/rls-services+xml",
"application/rsd+xml",
"application/rss+xml",
"application/rtf",
"application/sdp",
"application/shf+xml",
"application/timestamped-data",
"application/trig",
"application/vnd.android.package-archive",
"application/vnd.api+json",
"application/vnd.apple.installer+xml",
"application/vnd.apple.mpegurl",
"application/vnd.apple.pkpass",
"application/vnd.bmi",
"application/vnd.curl.car",
"application/vnd.curl.pcurl",
"application/vnd.dna",
"application/vnd.google-apps.document",
"application/vnd.google-apps.presentation",
"application/vnd.google-apps.spreadsheet",
"application/vnd.hal+xml",
"application/vnd.handheld-entertainment+xml",
"application/vnd.macports.portpkg",
"application/vnd.unity",
"application/vnd.zul",
"application/widget",
"application/wsdl+xml",
"application/x-7z-compressed",
"application/x-ace-compressed",
"application/x-bittorrent",
"application/x-bzip",
"application/x-bzip2",
"application/x-cfs-compressed",
"application/x-chrome-extension",
"application/x-cocoa",
"application/x-envoy",
"application/x-eva",
"font/opentype",
"application/x-gca-compressed",
"application/x-gtar",
"application/x-hdf",
"application/x-httpd-php",
"application/x-install-instructions",
"application/x-latex",
"application/x-lua-bytecode",
"application/x-lzh-compressed",
"application/x-ms-application",
"application/x-ms-shortcut",
"application/x-ndjson",
"application/x-perl",
"application/x-pkcs7-certificates",
"application/x-pkcs7-certreqresp",
"application/x-rar-compressed",
"application/x-sh",
"application/x-sql",
"application/x-subrip",
"application/x-t3vm-image",
"application/x-tads",
"application/x-tar",
"application/x-tcl",
"application/x-tex",
"application/x-x509-ca-cert",
"application/xop+xml",
"application/xslt+xml",
"application/zip",
"audio/3gpp",
"audio/adpcm",
"audio/basic",
"audio/midi",
"audio/mpeg",
"audio/mp4",
"audio/ogg",
"audio/silk",
"audio/wave",
"audio/webm",
"audio/x-aac",
"audio/x-aiff",
"audio/x-caf",
"audio/x-flac",
"audio/xm",
"image/bmp",
"image/cgm",
"image/sgi",
"image/svg+xml",
"image/tiff",
"image/x-3ds",
"image/x-freehand",
"image/x-icon",
"image/x-jng",
"image/x-mrsid-image",
"image/x-pcx",
"image/x-pict",
"image/x-rgb",
"image/x-tga",
"message/rfc822",
"text/cache-manifest",
"text/calendar",
"text/coffeescript",
"text/csv",
"text/hjson",
"text/jade",
"text/jsx",
"text/less",
"text/mathml",
"text/n3",
"text/richtext",
"text/sgml",
"text/slim",
"text/stylus",
"text/tab-separated-values",
"text/turtle",
"text/uri-list",
"text/vcard",
"text/vnd.curl",
"text/vnd.fly",
"text/vtt",
"text/x-asm",
"text/x-c",
"text/x-component",
"text/x-fortran",
"text/x-handlebars-template",
"text/x-java-source",
"text/x-lua",
"text/x-markdown",
"text/x-nfo",
"text/x-opml",
"text/x-pascal",
"text/x-processing",
"text/x-sass",
"text/x-scss",
"text/x-vcalendar",
"text/xml",
"text/yaml",
"video/3gpp",
"video/3gpp2",
"video/h261",
"video/h263",
"video/h264",
"video/jpeg",
"video/jpm",
"video/mj2",
"video/mp2t",
"video/mp4",
"video/mpeg",
"video/ogg",
"video/quicktime",
"video/webm",
"video/x-f4v",
"video/x-fli",
"video/x-flv",
"video/x-m4v",
];

View File

@@ -1,8 +1,8 @@
import type { HttpRequestHeader } from '@yaakapp-internal/models';
import { invokeCmd } from './tauri';
import type { HttpRequestHeader } from "@yaakapp-internal/models";
import { invokeCmd } from "./tauri";
/**
* Global default headers fetched from the backend.
* These are static and fetched once on module load.
*/
export const defaultHeaders: HttpRequestHeader[] = await invokeCmd('cmd_default_headers');
export const defaultHeaders: HttpRequestHeader[] = await invokeCmd("cmd_default_headers");

View File

@@ -1,17 +1,17 @@
import type { AnyModel } from '@yaakapp-internal/models';
import { deleteModel, modelTypeLabel } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { Prose } from '../components/Prose';
import { showConfirmDelete } from './confirm';
import { pluralizeCount } from './pluralize';
import { resolvedModelName } from './resolvedModelName';
import type { AnyModel } from "@yaakapp-internal/models";
import { deleteModel, modelTypeLabel } from "@yaakapp-internal/models";
import { InlineCode } from "../components/core/InlineCode";
import { Prose } from "../components/Prose";
import { showConfirmDelete } from "./confirm";
import { pluralizeCount } from "./pluralize";
import { resolvedModelName } from "./resolvedModelName";
export async function deleteModelWithConfirm(
model: AnyModel | AnyModel[] | null,
options: { confirmName?: string } = {},
): Promise<boolean> {
if (model == null) {
console.warn('Tried to delete null model');
console.warn("Tried to delete null model");
return false;
}
const models = Array.isArray(model) ? model : [model];
@@ -19,14 +19,14 @@ export async function deleteModelWithConfirm(
if (firstModel == null) return false;
const descriptor =
models.length === 1 ? modelTypeLabel(firstModel) : pluralizeCount('Item', models.length);
models.length === 1 ? modelTypeLabel(firstModel) : pluralizeCount("Item", models.length);
const confirmed = await showConfirmDelete({
id: `delete-model-${models.map((m) => m.id).join(',')}`,
id: `delete-model-${models.map((m) => m.id).join(",")}`,
title: `Delete ${descriptor}`,
requireTyping: options.confirmName,
description: (
<>
Permanently delete{' '}
Permanently delete{" "}
{models.length === 1 ? (
<>
<InlineCode>{resolvedModelName(firstModel)}</InlineCode>?
@@ -45,7 +45,7 @@ export async function deleteModelWithConfirm(
</Prose>
</>
) : (
`all ${pluralizeCount('item', models.length)}?`
`all ${pluralizeCount("item", models.length)}?`
)}
</>
),

View File

@@ -1,6 +1,6 @@
import { atom } from 'jotai';
import type { DialogInstance } from '../components/Dialogs';
import { jotaiStore } from './jotai';
import { atom } from "jotai";
import type { DialogInstance } from "../components/Dialogs";
import { jotaiStore } from "./jotai";
export const dialogsAtom = atom<DialogInstance[]>([]);

View File

@@ -1,12 +1,12 @@
import type { SyncModel } from '@yaakapp-internal/git';
import { stringify } from 'yaml';
import type { SyncModel } from "@yaakapp-internal/git";
import { stringify } from "yaml";
/**
* Convert a SyncModel to a clean YAML string for diffing.
* Removes noisy fields like updatedAt that change on every edit.
*/
export function modelToYaml(model: SyncModel | null): string {
if (!model) return '';
if (!model) return "";
return stringify(model, {
indent: 2,

View File

@@ -1,10 +1,10 @@
import type { DragMoveEvent } from '@dnd-kit/core';
import type { DragMoveEvent } from "@dnd-kit/core";
export function computeSideForDragMove(
id: string,
e: DragMoveEvent,
orientation: 'vertical' | 'horizontal' = 'vertical',
): 'before' | 'after' | null {
orientation: "vertical" | "horizontal" = "vertical",
): "before" | "after" | null {
if (e.over == null || e.over.id !== id) {
return null;
}
@@ -12,7 +12,7 @@ export function computeSideForDragMove(
const overRect = e.over.rect;
if (orientation === 'horizontal') {
if (orientation === "horizontal") {
// For horizontal layouts (tabs side-by-side), use left/right logic
const activeLeft =
e.active.rect.current.translated?.left ?? e.active.rect.current.initial.left + e.delta.x;
@@ -22,7 +22,7 @@ export function computeSideForDragMove(
const hoverRight = overRect.right;
const hoverMiddleX = hoverLeft + (hoverRight - hoverLeft) / 2;
return pointerX < hoverMiddleX ? 'before' : 'after'; // 'before' = left, 'after' = right
return pointerX < hoverMiddleX ? "before" : "after"; // 'before' = left, 'after' = right
} else {
// For vertical layouts, use top/bottom logic
const activeTop =
@@ -34,6 +34,6 @@ export function computeSideForDragMove(
const hoverMiddleY = (hoverBottom - hoverTop) / 2;
const hoverClientY = pointerY - hoverTop;
return hoverClientY < hoverMiddleY ? 'before' : 'after';
return hoverClientY < hoverMiddleY ? "before" : "after";
}
}

View File

@@ -1,19 +1,19 @@
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
import { duplicateModel } from '@yaakapp-internal/models';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { jotaiStore } from './jotai';
import { navigateToRequestOrFolderOrWorkspace } from './setWorkspaceSearchParams';
import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
import { duplicateModel } from "@yaakapp-internal/models";
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { jotaiStore } from "./jotai";
import { navigateToRequestOrFolderOrWorkspace } from "./setWorkspaceSearchParams";
export async function duplicateRequestOrFolderAndNavigate(
model: Folder | HttpRequest | GrpcRequest | WebsocketRequest | null,
) {
if (model == null) {
throw new Error('Cannot duplicate null item');
throw new Error("Cannot duplicate null item");
}
const newId = await duplicateModel(model);
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (workspaceId == null || model.model === 'folder') return;
if (workspaceId == null || model.model === "folder") return;
navigateToRequestOrFolderOrWorkspace(newId, model.model);
}

View File

@@ -1,12 +1,12 @@
import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models';
import { updateModel } from '@yaakapp-internal/models';
import { openFolderSettings } from '../commands/openFolderSettings';
import type { PairEditorHandle } from '../components/core/PairEditor';
import { ensurePairId } from '../components/core/PairEditor.util';
import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog';
import { environmentsBreakdownAtom } from '../hooks/useEnvironmentsBreakdown';
import { toggleDialog } from './dialog';
import { jotaiStore } from './jotai';
import type { Environment, EnvironmentVariable } from "@yaakapp-internal/models";
import { updateModel } from "@yaakapp-internal/models";
import { openFolderSettings } from "../commands/openFolderSettings";
import type { PairEditorHandle } from "../components/core/PairEditor";
import { ensurePairId } from "../components/core/PairEditor.util";
import { EnvironmentEditDialog } from "../components/EnvironmentEditDialog";
import { environmentsBreakdownAtom } from "../hooks/useEnvironmentsBreakdown";
import { toggleDialog } from "./dialog";
import { jotaiStore } from "./jotai";
interface Options {
addOrFocusVariable?: EnvironmentVariable;
@@ -16,8 +16,8 @@ export async function editEnvironment(
initialEnvironment: Environment | null,
options: Options = {},
) {
if (initialEnvironment?.parentModel === 'folder' && initialEnvironment.parentId != null) {
openFolderSettings(initialEnvironment.parentId, 'variables');
if (initialEnvironment?.parentModel === "folder" && initialEnvironment.parentId != null) {
openFolderSettings(initialEnvironment.parentId, "variables");
} else {
const { addOrFocusVariable } = options;
const { baseEnvironment } = jotaiStore.get(environmentsBreakdownAtom);
@@ -42,10 +42,10 @@ export async function editEnvironment(
let didFocusVariable = false;
toggleDialog({
id: 'environment-editor',
id: "environment-editor",
noPadding: true,
size: 'lg',
className: 'h-[90vh] max-h-[60rem]',
size: "lg",
className: "h-[90vh] max-h-[60rem]",
render: () => (
<EnvironmentEditDialog
initialEnvironmentId={environment?.id ?? null}

View File

@@ -1,22 +1,22 @@
import { parseTemplate } from '@yaakapp-internal/templates';
import { activeEnvironmentIdAtom } from '../hooks/useActiveEnvironment';
import { activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { jotaiStore } from './jotai';
import { invokeCmd } from './tauri';
import { parseTemplate } from "@yaakapp-internal/templates";
import { activeEnvironmentIdAtom } from "../hooks/useActiveEnvironment";
import { activeWorkspaceIdAtom } from "../hooks/useActiveWorkspace";
import { jotaiStore } from "./jotai";
import { invokeCmd } from "./tauri";
export function analyzeTemplate(template: string): 'global_secured' | 'local_secured' | 'insecure' {
export function analyzeTemplate(template: string): "global_secured" | "local_secured" | "insecure" {
let secureTags = 0;
let insecureTags = 0;
let totalTags = 0;
for (const t of parseTemplate(template).tokens) {
if (t.type === 'eof') continue;
if (t.type === "eof") continue;
totalTags++;
if (t.type === 'tag' && t.val.type === 'fn' && t.val.name === 'secure') {
if (t.type === "tag" && t.val.type === "fn" && t.val.name === "secure") {
secureTags++;
} else if (t.type === 'tag' && t.val.type === 'var') {
} else if (t.type === "tag" && t.val.type === "var") {
// Variables are secure
} else if (t.type === 'tag' && t.val.type === 'bool') {
} else if (t.type === "tag" && t.val.type === "bool") {
// Booleans are secure
} else {
insecureTags++;
@@ -24,34 +24,34 @@ export function analyzeTemplate(template: string): 'global_secured' | 'local_sec
}
if (secureTags === 1 && totalTags === 1) {
return 'global_secured';
return "global_secured";
}
if (insecureTags === 0) {
return 'local_secured';
return "local_secured";
}
return 'insecure';
return "insecure";
}
export async function convertTemplateToInsecure(template: string) {
if (template === '') {
return '';
if (template === "") {
return "";
}
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? 'n/a';
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
return invokeCmd<string>('cmd_decrypt_template', { template, workspaceId, environmentId });
return invokeCmd<string>("cmd_decrypt_template", { template, workspaceId, environmentId });
}
export async function convertTemplateToSecure(template: string): Promise<string> {
if (template === '') {
return '';
if (template === "") {
return "";
}
if (analyzeTemplate(template) === 'global_secured') {
if (analyzeTemplate(template) === "global_secured") {
return template;
}
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? 'n/a';
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom) ?? "n/a";
const environmentId = jotaiStore.get(activeEnvironmentIdAtom) ?? null;
return invokeCmd<string>('cmd_secure_template', { template, workspaceId, environmentId });
return invokeCmd<string>("cmd_secure_template", { template, workspaceId, environmentId });
}

View File

@@ -1,4 +1,4 @@
import { showErrorToast } from './toast';
import { showErrorToast } from "./toast";
/**
* Handles a fire-and-forget promise by catching and reporting errors
@@ -6,10 +6,10 @@ import { showErrorToast } from './toast';
*/
export function fireAndForget(promise: Promise<unknown>) {
promise.catch((err: unknown) => {
console.error('Unhandled async error:', err);
console.error("Unhandled async error:", err);
showErrorToast({
id: 'async-error',
title: 'Unexpected Error',
id: "async-error",
title: "Unexpected Error",
message: err instanceof Error ? err.message : String(err),
});
});

View File

@@ -1,44 +1,44 @@
import vkBeautify from 'vkbeautify';
import { invokeCmd } from './tauri';
import vkBeautify from "vkbeautify";
import { invokeCmd } from "./tauri";
export async function tryFormatJson(text: string): Promise<string> {
if (text === '') return text;
if (text === "") return text;
try {
const result = await invokeCmd<string>('cmd_format_json', { text });
const result = await invokeCmd<string>("cmd_format_json", { text });
return result;
} catch (err) {
console.warn('Failed to format JSON', err);
console.warn("Failed to format JSON", err);
}
try {
return JSON.stringify(JSON.parse(text), null, 2);
} catch (err) {
console.log('JSON beautify failed', err);
console.log("JSON beautify failed", err);
}
return text;
}
export async function tryFormatGraphql(text: string): Promise<string> {
if (text === '') return text;
if (text === "") return text;
try {
return await invokeCmd<string>('cmd_format_graphql', { text });
return await invokeCmd<string>("cmd_format_graphql", { text });
} catch (err) {
console.warn('Failed to format GraphQL', err);
console.warn("Failed to format GraphQL", err);
}
return text;
}
export async function tryFormatXml(text: string): Promise<string> {
if (text === '') return text;
if (text === "") return text;
try {
return vkBeautify.xml(text, ' ');
return vkBeautify.xml(text, " ");
} catch (err) {
console.warn('Failed to format XML', err);
console.warn("Failed to format XML", err);
}
return text;

View File

@@ -1,6 +1,6 @@
import { customAlphabet } from 'nanoid';
import { customAlphabet } from "nanoid";
const nanoid = customAlphabet('023456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ', 10);
const nanoid = customAlphabet("023456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ", 10);
export function generateId(): string {
return nanoid();

View File

@@ -1,22 +1,22 @@
import type { ReactNode } from 'react';
import type { ReactNode } from "react";
/**
* Get the text content from a ReactNode
* https://stackoverflow.com/questions/50428910/get-text-content-from-node-in-react
*/
export function getNodeText(node: ReactNode): string {
if (typeof node === 'string' || typeof node === 'number') {
if (typeof node === "string" || typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) {
return node.map(getNodeText).join('');
return node.map(getNodeText).join("");
}
if (typeof node === 'object' && node) {
if (typeof node === "object" && node) {
// oxlint-disable-next-line no-explicit-any
return getNodeText((node as any).props.children);
}
return '';
return "";
}

View File

@@ -1,33 +1,33 @@
import type { BatchUpsertResult } from '@yaakapp-internal/models';
import { Button } from '../components/core/Button';
import { FormattedError } from '../components/core/FormattedError';
import { VStack } from '../components/core/Stacks';
import { ImportDataDialog } from '../components/ImportDataDialog';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
import { createFastMutation } from '../hooks/useFastMutation';
import { showAlert } from './alert';
import { showDialog } from './dialog';
import { jotaiStore } from './jotai';
import { pluralizeCount } from './pluralize';
import { router } from './router';
import { invokeCmd } from './tauri';
import type { BatchUpsertResult } from "@yaakapp-internal/models";
import { Button } from "../components/core/Button";
import { FormattedError } from "../components/core/FormattedError";
import { VStack } from "../components/core/Stacks";
import { ImportDataDialog } from "../components/ImportDataDialog";
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
import { createFastMutation } from "../hooks/useFastMutation";
import { showAlert } from "./alert";
import { showDialog } from "./dialog";
import { jotaiStore } from "./jotai";
import { pluralizeCount } from "./pluralize";
import { router } from "./router";
import { invokeCmd } from "./tauri";
export const importData = createFastMutation({
mutationKey: ['import_data'],
mutationKey: ["import_data"],
onError: (err: string) => {
showAlert({
id: 'import-failed',
title: 'Import Failed',
size: 'md',
id: "import-failed",
title: "Import Failed",
size: "md",
body: <FormattedError>{err}</FormattedError>,
});
},
mutationFn: async () => {
return new Promise<void>((resolve, reject) => {
showDialog({
id: 'import',
title: 'Import Data',
size: 'sm',
id: "import",
title: "Import Data",
size: "sm",
render: ({ hide }) => {
const importAndHide = async (filePath: string) => {
try {
@@ -51,7 +51,7 @@ export const importData = createFastMutation({
async function performImport(filePath: string): Promise<boolean> {
const activeWorkspace = jotaiStore.get(activeWorkspaceAtom);
const imported = await invokeCmd<BatchUpsertResult>('cmd_import_data', {
const imported = await invokeCmd<BatchUpsertResult>("cmd_import_data", {
filePath,
workspaceId: activeWorkspace?.id,
});
@@ -59,31 +59,31 @@ async function performImport(filePath: string): Promise<boolean> {
const importedWorkspace = imported.workspaces[0];
showDialog({
id: 'import-complete',
title: 'Import Complete',
size: 'sm',
id: "import-complete",
title: "Import Complete",
size: "sm",
hideX: true,
render: ({ hide }) => {
return (
<VStack space={3} className="pb-4">
<ul className="list-disc pl-6">
{imported.workspaces.length > 0 && (
<li>{pluralizeCount('Workspace', imported.workspaces.length)}</li>
<li>{pluralizeCount("Workspace", imported.workspaces.length)}</li>
)}
{imported.environments.length > 0 && (
<li>{pluralizeCount('Environment', imported.environments.length)}</li>
<li>{pluralizeCount("Environment", imported.environments.length)}</li>
)}
{imported.folders.length > 0 && (
<li>{pluralizeCount('Folder', imported.folders.length)}</li>
<li>{pluralizeCount("Folder", imported.folders.length)}</li>
)}
{imported.httpRequests.length > 0 && (
<li>{pluralizeCount('HTTP Request', imported.httpRequests.length)}</li>
<li>{pluralizeCount("HTTP Request", imported.httpRequests.length)}</li>
)}
{imported.grpcRequests.length > 0 && (
<li>{pluralizeCount('GRPC Request', imported.grpcRequests.length)}</li>
<li>{pluralizeCount("GRPC Request", imported.grpcRequests.length)}</li>
)}
{imported.websocketRequests.length > 0 && (
<li>{pluralizeCount('Websocket Request', imported.websocketRequests.length)}</li>
<li>{pluralizeCount("Websocket Request", imported.websocketRequests.length)}</li>
)}
</ul>
<div>
@@ -99,7 +99,7 @@ async function performImport(filePath: string): Promise<boolean> {
if (importedWorkspace != null) {
const environmentId = imported.environments[0]?.id ?? null;
await router.navigate({
to: '/workspaces/$workspaceId',
to: "/workspaces/$workspaceId",
params: { workspaceId: importedWorkspace.id },
search: { environment_id: environmentId },
});

View File

@@ -1,50 +1,50 @@
import { emit } from '@tauri-apps/api/event';
import { openUrl } from '@tauri-apps/plugin-opener';
import { debounce } from '@yaakapp-internal/lib';
import { emit } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import { debounce } from "@yaakapp-internal/lib";
import type {
FormInput,
InternalEvent,
JsonPrimitive,
ShowToastRequest,
} from '@yaakapp-internal/plugins';
import { updateAllPlugins } from '@yaakapp-internal/plugins';
} from "@yaakapp-internal/plugins";
import { updateAllPlugins } from "@yaakapp-internal/plugins";
import type {
PluginUpdateNotification,
UpdateInfo,
UpdateResponse,
YaakNotification,
} from '@yaakapp-internal/tauri';
import { openSettings } from '../commands/openSettings';
import { Button } from '../components/core/Button';
import { ButtonInfiniteLoading } from '../components/core/ButtonInfiniteLoading';
import { Icon } from '../components/core/Icon';
import { HStack, VStack } from '../components/core/Stacks';
} from "@yaakapp-internal/tauri";
import { openSettings } from "../commands/openSettings";
import { Button } from "../components/core/Button";
import { ButtonInfiniteLoading } from "../components/core/ButtonInfiniteLoading";
import { Icon } from "../components/core/Icon";
import { HStack, VStack } from "../components/core/Stacks";
// Listen for toasts
import { listenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { fireAndForget } from './fireAndForget';
import { updateAvailableAtom } from './atoms';
import { stringToColor } from './color';
import { generateId } from './generateId';
import { jotaiStore } from './jotai';
import { showPrompt } from './prompt';
import { showPromptForm } from './prompt-form';
import { invokeCmd } from './tauri';
import { showToast } from './toast';
import { listenToTauriEvent } from "../hooks/useListenToTauriEvent";
import { fireAndForget } from "./fireAndForget";
import { updateAvailableAtom } from "./atoms";
import { stringToColor } from "./color";
import { generateId } from "./generateId";
import { jotaiStore } from "./jotai";
import { showPrompt } from "./prompt";
import { showPromptForm } from "./prompt-form";
import { invokeCmd } from "./tauri";
import { showToast } from "./toast";
export function initGlobalListeners() {
listenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
listenToTauriEvent<ShowToastRequest>("show_toast", (event) => {
showToast({ ...event.payload });
});
listenToTauriEvent('settings', () => openSettings.mutate(null));
listenToTauriEvent("settings", () => openSettings.mutate(null));
// Track active dynamic form dialogs so follow-up input updates can reach them
const activeForms = new Map<string, (inputs: FormInput[]) => void>();
// Listen for plugin events
listenToTauriEvent<InternalEvent>('plugin_event', async ({ payload: event }) => {
if (event.payload.type === 'prompt_text_request') {
listenToTauriEvent<InternalEvent>("plugin_event", async ({ payload: event }) => {
if (event.payload.type === "prompt_text_request") {
const value = await showPrompt(event.payload);
const result: InternalEvent = {
id: generateId(),
@@ -53,12 +53,12 @@ export function initGlobalListeners() {
pluginRefId: event.pluginRefId,
context: event.context,
payload: {
type: 'prompt_text_response',
type: "prompt_text_response",
value,
},
};
await emit(event.id, result);
} else if (event.payload.type === 'prompt_form_request') {
} else if (event.payload.type === "prompt_form_request") {
if (event.replyId != null) {
// Follow-up update from plugin runtime — update the active dialog's inputs
const updateInputs = activeForms.get(event.replyId);
@@ -77,7 +77,7 @@ export function initGlobalListeners() {
pluginRefId: event.pluginRefId,
context: event.context,
payload: {
type: 'prompt_form_response',
type: "prompt_form_response",
values,
done,
},
@@ -103,68 +103,70 @@ export function initGlobalListeners() {
}
});
listenToTauriEvent<string>('update_installed', async ({ payload: version }) => {
console.log('Got update installed event', version);
listenToTauriEvent<string>("update_installed", async ({ payload: version }) => {
console.log("Got update installed event", version);
showUpdateInstalledToast(version);
});
// Listen for update events
listenToTauriEvent<UpdateInfo>('update_available', async ({ payload }) => {
console.log('Got update available', payload);
listenToTauriEvent<UpdateInfo>("update_available", async ({ payload }) => {
console.log("Got update available", payload);
fireAndForget(showUpdateAvailableToast(payload));
});
listenToTauriEvent<YaakNotification>('notification', ({ payload }) => {
console.log('Got notification event', payload);
listenToTauriEvent<YaakNotification>("notification", ({ payload }) => {
console.log("Got notification event", payload);
showNotificationToast(payload);
});
// Listen for plugin update events
listenToTauriEvent<PluginUpdateNotification>('plugin_updates_available', ({ payload }) => {
console.log('Got plugin updates event', payload);
listenToTauriEvent<PluginUpdateNotification>("plugin_updates_available", ({ payload }) => {
console.log("Got plugin updates event", payload);
showPluginUpdatesToast(payload);
});
// Check for plugin initialization errors
fireAndForget(invokeCmd<[string, string][]>('cmd_plugin_init_errors').then((errors) => {
for (const [dir, message] of errors) {
const dirBasename = dir.split('/').pop() ?? dir;
showToast({
id: `plugin-init-error-${dirBasename}`,
color: 'warning',
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Plugin failed to load</h2>
<p className="text-text-subtle text-sm">
{dirBasename}: {message}
</p>
</VStack>
),
action: ({ hide }) => (
<Button
size="xs"
color="warning"
variant="border"
onClick={() => {
hide();
openSettings.mutate('plugins:installed');
}}
>
View Plugins
</Button>
),
});
}
}));
fireAndForget(
invokeCmd<[string, string][]>("cmd_plugin_init_errors").then((errors) => {
for (const [dir, message] of errors) {
const dirBasename = dir.split("/").pop() ?? dir;
showToast({
id: `plugin-init-error-${dirBasename}`,
color: "warning",
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Plugin failed to load</h2>
<p className="text-text-subtle text-sm">
{dirBasename}: {message}
</p>
</VStack>
),
action: ({ hide }) => (
<Button
size="xs"
color="warning"
variant="border"
onClick={() => {
hide();
openSettings.mutate("plugins:installed");
}}
>
View Plugins
</Button>
),
});
}
}),
);
}
function showUpdateInstalledToast(version: string) {
const UPDATE_TOAST_ID = 'update-info';
const UPDATE_TOAST_ID = "update-info";
showToast({
id: UPDATE_TOAST_ID,
color: 'primary',
color: "primary",
timeout: null,
message: (
<VStack>
@@ -180,7 +182,7 @@ function showUpdateInstalledToast(version: string) {
loadingChildren="Restarting..."
onClick={() => {
hide();
setTimeout(() => invokeCmd('cmd_restart', {}), 200);
setTimeout(() => invokeCmd("cmd_restart", {}), 200);
}}
>
Relaunch Yaak
@@ -190,23 +192,23 @@ function showUpdateInstalledToast(version: string) {
}
async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
const UPDATE_TOAST_ID = 'update-info';
const UPDATE_TOAST_ID = "update-info";
const { version, replyEventId, downloaded } = updateInfo;
jotaiStore.set(updateAvailableAtom, { version, downloaded });
// Acknowledge the event, so we don't time out and try the fallback update logic
await emit<UpdateResponse>(replyEventId, { type: 'ack' });
await emit<UpdateResponse>(replyEventId, { type: "ack" });
showToast({
id: UPDATE_TOAST_ID,
color: 'info',
color: "info",
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">Yaak {version} is available</h2>
<p className="text-text-subtle text-sm">
{downloaded ? 'Do you want to install' : 'Download and install'} the update?
{downloaded ? "Do you want to install" : "Download and install"} the update?
</p>
</VStack>
),
@@ -216,12 +218,12 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
size="xs"
color="info"
className="min-w-[10rem]"
loadingChildren={downloaded ? 'Installing...' : 'Downloading...'}
loadingChildren={downloaded ? "Installing..." : "Downloading..."}
onClick={async () => {
await emit<UpdateResponse>(replyEventId, { type: 'action', action: 'install' });
await emit<UpdateResponse>(replyEventId, { type: "action", action: "install" });
}}
>
{downloaded ? 'Install Now' : 'Download and Install'}
{downloaded ? "Install Now" : "Download and Install"}
</ButtonInfiniteLoading>
<Button
size="xs"
@@ -240,23 +242,23 @@ async function showUpdateAvailableToast(updateInfo: UpdateInfo) {
}
function showPluginUpdatesToast(updateInfo: PluginUpdateNotification) {
const PLUGIN_UPDATE_TOAST_ID = 'plugin-updates';
const PLUGIN_UPDATE_TOAST_ID = "plugin-updates";
const count = updateInfo.updateCount;
const pluginNames = updateInfo.plugins.map((p: { name: string }) => p.name);
showToast({
id: PLUGIN_UPDATE_TOAST_ID,
color: 'info',
color: "info",
timeout: null,
message: (
<VStack>
<h2 className="font-semibold">
{count === 1 ? '1 plugin update' : `${count} plugin updates`} available
{count === 1 ? "1 plugin update" : `${count} plugin updates`} available
</h2>
<p className="text-text-subtle text-sm">
{count === 1
? pluginNames[0]
: `${pluginNames.slice(0, 2).join(', ')}${count > 2 ? `, and ${count - 2} more` : ''}`}
: `${pluginNames.slice(0, 2).join(", ")}${count > 2 ? `, and ${count - 2} more` : ""}`}
</p>
</VStack>
),
@@ -272,8 +274,8 @@ function showPluginUpdatesToast(updateInfo: PluginUpdateNotification) {
hide();
if (updated.length > 0) {
showToast({
color: 'success',
message: `Successfully updated ${updated.length} plugin${updated.length === 1 ? '' : 's'}`,
color: "success",
message: `Successfully updated ${updated.length} plugin${updated.length === 1 ? "" : "s"}`,
});
}
}}
@@ -286,7 +288,7 @@ function showPluginUpdatesToast(updateInfo: PluginUpdateNotification) {
variant="border"
onClick={() => {
hide();
openSettings.mutate('plugins:installed');
openSettings.mutate("plugins:installed");
}}
>
View Updates
@@ -310,7 +312,7 @@ function showNotificationToast(n: YaakNotification) {
</VStack>
),
onClose: () => {
invokeCmd('cmd_dismiss_notification', { notificationId: n.id }).catch(console.error);
invokeCmd("cmd_dismiss_notification", { notificationId: n.id }).catch(console.error);
},
action: ({ hide }) => {
return actionLabel && actionUrl ? (

View File

@@ -1,3 +1,3 @@
import { createStore } from 'jotai';
import { createStore } from "jotai";
export const jotaiStore = createStore();

View File

@@ -10,7 +10,7 @@ export function textLikelyContainsJsonComments(text: string): boolean {
if (inString) {
if (ch === '"') {
inString = false;
} else if (ch === '\\') {
} else if (ch === "\\") {
i++; // skip escaped char
}
continue;
@@ -19,9 +19,9 @@ export function textLikelyContainsJsonComments(text: string): boolean {
inString = true;
continue;
}
if (ch === '/' && i + 1 < text.length) {
if (ch === "/" && i + 1 < text.length) {
const next = text[i + 1];
if (next === '/' || next === '*') {
if (next === "/" || next === "*") {
return true;
}
}

View File

@@ -1,9 +1,9 @@
import type { KeyValue } from '@yaakapp-internal/models';
import { createGlobalModel, keyValuesAtom, patchModel } from '@yaakapp-internal/models';
import { jotaiStore } from './jotai';
import type { KeyValue } from "@yaakapp-internal/models";
import { createGlobalModel, keyValuesAtom, patchModel } from "@yaakapp-internal/models";
import { jotaiStore } from "./jotai";
export async function setKeyValue<T>({
namespace = 'global',
namespace = "global",
key: keyOrKeys,
value: rawValue,
}: {
@@ -18,12 +18,12 @@ export async function setKeyValue<T>({
if (kv) {
await patchModel(kv, { namespace, key, value });
} else {
await createGlobalModel({ model: 'key_value', namespace, key, value });
await createGlobalModel({ model: "key_value", namespace, key, value });
}
}
export function getKeyValueRaw({
namespace = 'global',
namespace = "global",
key: keyOrKeys,
}: {
namespace?: string;
@@ -36,7 +36,7 @@ export function getKeyValueRaw({
}
export function getKeyValue<T>({
namespace = 'global',
namespace = "global",
key,
fallback,
}: {
@@ -53,7 +53,7 @@ export function extractKeyValue<T>(kv: KeyValue | null): T | undefined {
try {
return JSON.parse(kv.value) as T;
} catch (err) {
console.log('Failed to parse kv value', kv.value, err);
console.log("Failed to parse kv value", kv.value, err);
return undefined;
}
}
@@ -65,6 +65,6 @@ export function extractKeyValueOrFallback<T>(kv: KeyValue | null, fallback: T):
}
export function buildKeyValueKey(key: string | string[]): string {
if (typeof key === 'string') return key;
return key.join('::');
if (typeof key === "string") return key;
return key.join("::");
}

View File

@@ -1,8 +1,8 @@
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import { unified } from 'unified';
import rehypeStringify from "rehype-stringify";
import remarkGfm from "remark-gfm";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
const renderer = unified()
.use(remarkParse)
@@ -21,7 +21,7 @@ export async function renderMarkdown(md: string): Promise<string> {
const r = await renderer.process(md);
return r.toString();
} catch (err) {
console.log('FAILED TO RENDER MARKDOWN', err);
return 'error';
console.log("FAILED TO RENDER MARKDOWN", err);
return "error";
}
}

View File

@@ -1,4 +1,4 @@
import { sleep } from './sleep';
import { sleep } from "./sleep";
/** Ensures a promise takes at least a certain number of milliseconds to resolve */
export async function minPromiseMillis<T>(promise: Promise<T>, millis = 300) {

View File

@@ -1,94 +1,90 @@
import type { HttpResponseEvent } from '@yaakapp-internal/models';
import { describe, expect, test } from 'vite-plus/test';
import { getCookieCounts } from './model_util';
import type { HttpResponseEvent } from "@yaakapp-internal/models";
import { describe, expect, test } from "vite-plus/test";
import { getCookieCounts } from "./model_util";
function makeEvent(
type: string,
name: string,
value: string,
): HttpResponseEvent {
function makeEvent(type: string, name: string, value: string): HttpResponseEvent {
return {
id: 'test',
model: 'http_response_event',
responseId: 'resp',
workspaceId: 'ws',
id: "test",
model: "http_response_event",
responseId: "resp",
workspaceId: "ws",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
event: { type, name, value } as HttpResponseEvent['event'],
event: { type, name, value } as HttpResponseEvent["event"],
};
}
describe('getCookieCounts', () => {
test('returns zeros for undefined events', () => {
describe("getCookieCounts", () => {
test("returns zeros for undefined events", () => {
expect(getCookieCounts(undefined)).toEqual({ sent: 0, received: 0 });
});
test('returns zeros for empty events', () => {
test("returns zeros for empty events", () => {
expect(getCookieCounts([])).toEqual({ sent: 0, received: 0 });
});
test('counts single sent cookie', () => {
const events = [makeEvent('header_up', 'Cookie', 'session=abc123')];
test("counts single sent cookie", () => {
const events = [makeEvent("header_up", "Cookie", "session=abc123")];
expect(getCookieCounts(events)).toEqual({ sent: 1, received: 0 });
});
test('counts multiple sent cookies in one header', () => {
const events = [makeEvent('header_up', 'Cookie', 'a=1; b=2; c=3')];
test("counts multiple sent cookies in one header", () => {
const events = [makeEvent("header_up", "Cookie", "a=1; b=2; c=3")];
expect(getCookieCounts(events)).toEqual({ sent: 3, received: 0 });
});
test('counts single received cookie', () => {
const events = [makeEvent('header_down', 'Set-Cookie', 'session=abc123; Path=/')];
test("counts single received cookie", () => {
const events = [makeEvent("header_down", "Set-Cookie", "session=abc123; Path=/")];
expect(getCookieCounts(events)).toEqual({ sent: 0, received: 1 });
});
test('counts multiple received cookies from multiple headers', () => {
test("counts multiple received cookies from multiple headers", () => {
const events = [
makeEvent('header_down', 'Set-Cookie', 'a=1; Path=/'),
makeEvent('header_down', 'Set-Cookie', 'b=2; HttpOnly'),
makeEvent('header_down', 'Set-Cookie', 'c=3; Secure'),
makeEvent("header_down", "Set-Cookie", "a=1; Path=/"),
makeEvent("header_down", "Set-Cookie", "b=2; HttpOnly"),
makeEvent("header_down", "Set-Cookie", "c=3; Secure"),
];
expect(getCookieCounts(events)).toEqual({ sent: 0, received: 3 });
});
test('deduplicates sent cookies by name', () => {
test("deduplicates sent cookies by name", () => {
const events = [
makeEvent('header_up', 'Cookie', 'session=old'),
makeEvent('header_up', 'Cookie', 'session=new'),
makeEvent("header_up", "Cookie", "session=old"),
makeEvent("header_up", "Cookie", "session=new"),
];
expect(getCookieCounts(events)).toEqual({ sent: 1, received: 0 });
});
test('deduplicates received cookies by name', () => {
test("deduplicates received cookies by name", () => {
const events = [
makeEvent('header_down', 'Set-Cookie', 'token=abc; Path=/'),
makeEvent('header_down', 'Set-Cookie', 'token=xyz; Path=/'),
makeEvent("header_down", "Set-Cookie", "token=abc; Path=/"),
makeEvent("header_down", "Set-Cookie", "token=xyz; Path=/"),
];
expect(getCookieCounts(events)).toEqual({ sent: 0, received: 1 });
});
test('counts both sent and received cookies', () => {
test("counts both sent and received cookies", () => {
const events = [
makeEvent('header_up', 'Cookie', 'a=1; b=2; c=3'),
makeEvent('header_down', 'Set-Cookie', 'x=10; Path=/'),
makeEvent('header_down', 'Set-Cookie', 'y=20; Path=/'),
makeEvent('header_down', 'Set-Cookie', 'z=30; Path=/'),
makeEvent("header_up", "Cookie", "a=1; b=2; c=3"),
makeEvent("header_down", "Set-Cookie", "x=10; Path=/"),
makeEvent("header_down", "Set-Cookie", "y=20; Path=/"),
makeEvent("header_down", "Set-Cookie", "z=30; Path=/"),
];
expect(getCookieCounts(events)).toEqual({ sent: 3, received: 3 });
});
test('ignores non-cookie headers', () => {
test("ignores non-cookie headers", () => {
const events = [
makeEvent('header_up', 'Content-Type', 'application/json'),
makeEvent('header_down', 'Content-Length', '123'),
makeEvent("header_up", "Content-Type", "application/json"),
makeEvent("header_down", "Content-Length", "123"),
];
expect(getCookieCounts(events)).toEqual({ sent: 0, received: 0 });
});
test('handles case-insensitive header names', () => {
test("handles case-insensitive header names", () => {
const events = [
makeEvent('header_up', 'COOKIE', 'a=1'),
makeEvent('header_down', 'SET-COOKIE', 'b=2; Path=/'),
makeEvent("header_up", "COOKIE", "a=1"),
makeEvent("header_down", "SET-COOKIE", "b=2; Path=/"),
];
expect(getCookieCounts(events)).toEqual({ sent: 1, received: 1 });
});

View File

@@ -4,46 +4,46 @@ import type {
Environment,
HttpResponseEvent,
HttpResponseHeader,
} from '@yaakapp-internal/models';
import { getMimeTypeFromContentType } from './contentType';
} from "@yaakapp-internal/models";
import { getMimeTypeFromContentType } from "./contentType";
export const BODY_TYPE_NONE = null;
export const BODY_TYPE_GRAPHQL = 'graphql';
export const BODY_TYPE_JSON = 'application/json';
export const BODY_TYPE_BINARY = 'binary';
export const BODY_TYPE_OTHER = 'other';
export const BODY_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const BODY_TYPE_FORM_MULTIPART = 'multipart/form-data';
export const BODY_TYPE_XML = 'text/xml';
export const BODY_TYPE_GRAPHQL = "graphql";
export const BODY_TYPE_JSON = "application/json";
export const BODY_TYPE_BINARY = "binary";
export const BODY_TYPE_OTHER = "other";
export const BODY_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded";
export const BODY_TYPE_FORM_MULTIPART = "multipart/form-data";
export const BODY_TYPE_XML = "text/xml";
export function cookieDomain(cookie: Cookie): string {
if (cookie.domain === 'NotPresent' || cookie.domain === 'Empty') {
return 'n/a';
if (cookie.domain === "NotPresent" || cookie.domain === "Empty") {
return "n/a";
}
if ('HostOnly' in cookie.domain) {
if ("HostOnly" in cookie.domain) {
return cookie.domain.HostOnly;
}
if ('Suffix' in cookie.domain) {
if ("Suffix" in cookie.domain) {
return cookie.domain.Suffix;
}
return 'unknown';
return "unknown";
}
export function modelsEq(a: AnyModel, b: AnyModel) {
if (a.model !== b.model) {
return false;
}
if (a.model === 'key_value' && b.model === 'key_value') {
if (a.model === "key_value" && b.model === "key_value") {
return a.key === b.key && a.namespace === b.namespace;
}
if ('id' in a && 'id' in b) {
if ("id" in a && "id" in b) {
return a.id === b.id;
}
return false;
}
export function getContentTypeFromHeaders(headers: HttpResponseHeader[] | null): string | null {
return headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? null;
return headers?.find((h) => h.name.toLowerCase() === "content-type")?.value ?? null;
}
export function getCharsetFromContentType(headers: HttpResponseHeader[]): string | null {
@@ -51,24 +51,25 @@ export function getCharsetFromContentType(headers: HttpResponseHeader[]): string
if (contentType == null) return null;
const mimeType = getMimeTypeFromContentType(contentType);
return mimeType.parameters.get('charset') ?? null;
return mimeType.parameters.get("charset") ?? null;
}
export function isBaseEnvironment(environment: Environment): boolean {
return environment.parentModel === 'workspace';
return environment.parentModel === "workspace";
}
export function isSubEnvironment(environment: Environment): boolean {
return environment.parentModel === 'environment';
return environment.parentModel === "environment";
}
export function isFolderEnvironment(environment: Environment): boolean {
return environment.parentModel === 'folder';
return environment.parentModel === "folder";
}
export function getCookieCounts(
events: HttpResponseEvent[] | undefined,
): { sent: number; received: number } {
export function getCookieCounts(events: HttpResponseEvent[] | undefined): {
sent: number;
received: number;
} {
if (!events) return { sent: 0, received: 0 };
// Use Sets to deduplicate by cookie name
@@ -77,15 +78,15 @@ export function getCookieCounts(
for (const event of events) {
const e = event.event;
if (e.type === 'header_up' && e.name.toLowerCase() === 'cookie') {
if (e.type === "header_up" && e.name.toLowerCase() === "cookie") {
// Parse "Cookie: name=value; name2=value2" format
for (const pair of e.value.split(';')) {
const name = pair.split('=')[0]?.trim();
for (const pair of e.value.split(";")) {
const name = pair.split("=")[0]?.trim();
if (name) sentNames.add(name);
}
} else if (e.type === 'header_down' && e.name.toLowerCase() === 'set-cookie') {
} else if (e.type === "header_down" && e.name.toLowerCase() === "set-cookie") {
// Parse "Set-Cookie: name=value; ..." - first part before ; is name=value
const name = e.value.split(';')[0]?.split('=')[0]?.trim();
const name = e.value.split(";")[0]?.split("=")[0]?.trim();
if (name) receivedNames.add(name);
}
}

View File

@@ -1,12 +1,12 @@
import type { HttpUrlParameter } from '@yaakapp-internal/models';
import { generateId } from './generateId';
import type { HttpUrlParameter } from "@yaakapp-internal/models";
import { generateId } from "./generateId";
export function prepareImportQuerystring(
url: string,
): { url: string; urlParameters: HttpUrlParameter[] } | null {
const split = url.split(/\?(.*)/s);
const baseUrl = split[0] ?? '';
const querystring = split[1] ?? '';
const baseUrl = split[0] ?? "";
const querystring = split[1] ?? "";
// No querystring in url
if (!querystring) {

View File

@@ -1,11 +1,11 @@
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
import type { DialogProps } from '../components/core/Dialog';
import type { PromptProps } from '../components/core/Prompt';
import { Prompt } from '../components/core/Prompt';
import { showDialog } from './dialog';
import type { FormInput, JsonPrimitive } from "@yaakapp-internal/plugins";
import type { DialogProps } from "../components/core/Dialog";
import type { PromptProps } from "../components/core/Prompt";
import { Prompt } from "../components/core/Prompt";
import { showDialog } from "./dialog";
type FormArgs = Pick<DialogProps, 'title' | 'description' | 'size'> &
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & {
type FormArgs = Pick<DialogProps, "title" | "description" | "size"> &
Omit<PromptProps, "onClose" | "onCancel" | "onResult"> & {
id: string;
onValuesChange?: (values: Record<string, JsonPrimitive>) => void;
onInputsUpdated?: (cb: (inputs: FormInput[]) => void) => void;
@@ -20,13 +20,13 @@ export async function showPromptForm({
onInputsUpdated,
...props
}: FormArgs) {
return new Promise((resolve: PromptProps['onResult']) => {
return new Promise((resolve: PromptProps["onResult"]) => {
showDialog({
id,
title,
description,
hideX: true,
size: size ?? 'sm',
size: size ?? "sm",
disableBackdropClose: true, // Prevent accidental dismisses
onClose: () => {
// Click backdrop, close, or escape

View File

@@ -1,16 +1,16 @@
import type { FormInput, PromptTextRequest } from '@yaakapp-internal/plugins';
import type { ReactNode } from 'react';
import type { DialogProps } from '../components/core/Dialog';
import { showPromptForm } from './prompt-form';
import type { FormInput, PromptTextRequest } from "@yaakapp-internal/plugins";
import type { ReactNode } from "react";
import type { DialogProps } from "../components/core/Dialog";
import { showPromptForm } from "./prompt-form";
type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
type PromptProps = Omit<PromptTextRequest, "id" | "title" | "description"> & {
description?: ReactNode;
onCancel: () => void;
onResult: (value: string | null) => void;
};
type PromptArgs = Pick<DialogProps, 'title' | 'description'> &
Omit<PromptProps, 'onClose' | 'onCancel' | 'onResult'> & { id: string };
type PromptArgs = Pick<DialogProps, "title" | "description"> &
Omit<PromptProps, "onClose" | "onCancel" | "onResult"> & { id: string };
export async function showPrompt({
id,
@@ -25,8 +25,8 @@ export async function showPrompt({
{
...props,
optional: !required,
type: 'text',
name: 'value',
type: "text",
name: "value",
},
];
@@ -40,6 +40,6 @@ export async function showPrompt({
});
if (result == null) return null; // Cancelled
if (typeof result.value === 'string') return result.value;
return props.defaultValue ?? '';
if (typeof result.value === "string") return result.value;
return props.defaultValue ?? "";
}

View File

@@ -1,15 +1,15 @@
import { QueryCache, QueryClient } from '@tanstack/react-query';
import { QueryCache, QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (err, query) => {
console.log('Query client error', { err, query });
console.log("Query client error", { err, query });
},
}),
defaultOptions: {
queries: {
retry: false,
networkMode: 'always',
networkMode: "always",
refetchOnWindowFocus: true,
refetchOnReconnect: false,
refetchOnMount: false, // Don't refetch when a hook mounts

View File

@@ -1,29 +1,29 @@
import type { AnyModel } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { InlineCode } from '../components/core/InlineCode';
import { showPrompt } from './prompt';
import type { AnyModel } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import { InlineCode } from "../components/core/InlineCode";
import { showPrompt } from "./prompt";
export async function renameModelWithPrompt(model: Extract<AnyModel, { name: string }> | null) {
if (model == null) {
throw new Error('Tried to rename null model');
throw new Error("Tried to rename null model");
}
const name = await showPrompt({
id: 'rename-request',
title: 'Rename Request',
id: "rename-request",
title: "Rename Request",
required: false,
description:
model.name === '' ? (
'Enter a new name'
model.name === "" ? (
"Enter a new name"
) : (
<>
Enter a new name for <InlineCode>{model.name}</InlineCode>
</>
),
label: 'Name',
placeholder: 'New Name',
label: "Name",
placeholder: "New Name",
defaultValue: model.name,
confirmText: 'Save',
confirmText: "Save",
});
if (name == null) return;

View File

@@ -1,45 +1,45 @@
import type { AnyModel } from '@yaakapp-internal/models';
import { foldersAtom } from '@yaakapp-internal/models';
import { jotaiStore } from './jotai';
import type { AnyModel } from "@yaakapp-internal/models";
import { foldersAtom } from "@yaakapp-internal/models";
import { jotaiStore } from "./jotai";
export function resolvedModelName(r: AnyModel | null): string {
if (r == null) return '';
if (r == null) return "";
if (!('url' in r) || r.model === 'plugin') {
return 'name' in r ? r.name : '';
if (!("url" in r) || r.model === "plugin") {
return "name" in r ? r.name : "";
}
// Return name if it has one
if ('name' in r && r.name) {
if ("name" in r && r.name) {
return r.name;
}
// Replace variable syntax with variable name
const withoutVariables = r.url.replace(/\$\{\[\s*([^\]\s]+)\s*]}/g, '$1');
if (withoutVariables.trim() === '') {
return r.model === 'http_request'
? r.bodyType && r.bodyType === 'graphql'
? 'GraphQL Request'
: 'HTTP Request'
: r.model === 'websocket_request'
? 'WebSocket Request'
: 'gRPC Request';
const withoutVariables = r.url.replace(/\$\{\[\s*([^\]\s]+)\s*]}/g, "$1");
if (withoutVariables.trim() === "") {
return r.model === "http_request"
? r.bodyType && r.bodyType === "graphql"
? "GraphQL Request"
: "HTTP Request"
: r.model === "websocket_request"
? "WebSocket Request"
: "gRPC Request";
}
// GRPC gets nice short names
if (r.model === 'grpc_request' && r.service != null && r.method != null) {
const shortService = r.service.split('.').pop();
if (r.model === "grpc_request" && r.service != null && r.method != null) {
const shortService = r.service.split(".").pop();
return `${shortService}/${r.method}`;
}
// Strip unnecessary protocol
const withoutProto = withoutVariables.replace(/^(http|https|ws|wss):\/\//, '');
const withoutProto = withoutVariables.replace(/^(http|https|ws|wss):\/\//, "");
return withoutProto;
}
export function resolvedModelNameWithFolders(model: AnyModel | null): string {
return resolvedModelNameWithFoldersArray(model).join(' / ');
return resolvedModelNameWithFoldersArray(model).join(" / ");
}
export function resolvedModelNameWithFoldersArray(model: AnyModel | null): string[] {
@@ -48,7 +48,7 @@ export function resolvedModelNameWithFoldersArray(model: AnyModel | null): strin
const getParents = (m: AnyModel, names: string[]) => {
let newNames = [...names, resolvedModelName(m)];
if ('folderId' in m) {
if ("folderId" in m) {
const parent = folders.find((f) => f.id === m.folderId);
if (parent) {
newNames = [...resolvedModelNameWithFoldersArray(parent), ...newNames];

View File

@@ -1,8 +1,8 @@
import { readFile } from '@tauri-apps/plugin-fs';
import type { HttpResponse } from '@yaakapp-internal/models';
import type { FilterResponse } from '@yaakapp-internal/plugins';
import type { ServerSentEvent } from '@yaakapp-internal/sse';
import { invokeCmd } from './tauri';
import { readFile } from "@tauri-apps/plugin-fs";
import type { HttpResponse } from "@yaakapp-internal/models";
import type { FilterResponse } from "@yaakapp-internal/plugins";
import type { ServerSentEvent } from "@yaakapp-internal/sse";
import { invokeCmd } from "./tauri";
export async function getResponseBodyText({
response,
@@ -11,7 +11,7 @@ export async function getResponseBodyText({
response: HttpResponse;
filter: string | null;
}): Promise<string | null> {
const result = await invokeCmd<FilterResponse>('cmd_http_response_body', {
const result = await invokeCmd<FilterResponse>("cmd_http_response_body", {
response,
filter,
});
@@ -27,7 +27,7 @@ export async function getResponseBodyEventSource(
response: HttpResponse,
): Promise<ServerSentEvent[]> {
if (!response.bodyPath) return [];
return invokeCmd<ServerSentEvent[]>('cmd_get_sse_events', {
return invokeCmd<ServerSentEvent[]>("cmd_get_sse_events", {
filePath: response.bodyPath,
});
}

View File

@@ -1,9 +1,9 @@
import { type } from '@tauri-apps/plugin-os';
import { type } from "@tauri-apps/plugin-os";
const os = type();
export const revealInFinderText =
os === 'macos'
? 'Reveal in Finder'
: os === 'windows'
? 'Show in Explorer'
: 'Show in File Manager';
os === "macos"
? "Reveal in Finder"
: os === "windows"
? "Show in Explorer"
: "Show in File Manager";

View File

@@ -1,11 +1,11 @@
// Create a new router instance
import { createRouter } from '@tanstack/react-router';
import { routeTree } from '../routeTree.gen';
import { createRouter } from "@tanstack/react-router";
import { routeTree } from "../routeTree.gen";
export const router = createRouter({ routeTree });
// Register the router instance for type safety
declare module '@tanstack/react-router' {
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}

View File

@@ -1,3 +1,3 @@
export function isSidebarFocused() {
return document.activeElement?.closest('.x-theme-sidebar') != null;
return document.activeElement?.closest(".x-theme-sidebar") != null;
}

View File

@@ -1,6 +1,6 @@
import type { HttpRequest, HttpResponse } from '@yaakapp-internal/models';
import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
import { invokeCmd } from './tauri';
import type { HttpRequest, HttpResponse } from "@yaakapp-internal/models";
import { getActiveCookieJar } from "../hooks/useActiveCookieJar";
import { invokeCmd } from "./tauri";
export async function sendEphemeralRequest(
request: HttpRequest,
@@ -8,7 +8,7 @@ export async function sendEphemeralRequest(
): Promise<HttpResponse> {
// Remove some things that we don't want to associate
const newRequest = { ...request };
return invokeCmd('cmd_send_ephemeral_request', {
return invokeCmd("cmd_send_ephemeral_request", {
request: newRequest,
environmentId,
cookieJarId: getActiveCookieJar()?.id,

View File

@@ -1,6 +1,6 @@
import type { Folder, GrpcRequest, WebsocketRequest, Workspace } from '@yaakapp-internal/models';
import type { HttpRequest } from '@yaakapp-internal/sync';
import { router } from './router.js';
import type { Folder, GrpcRequest, WebsocketRequest, Workspace } from "@yaakapp-internal/models";
import type { HttpRequest } from "@yaakapp-internal/sync";
import { router } from "./router.js";
/**
* Setting search params using "from" on the global router instance in tanstack router does not
@@ -35,11 +35,11 @@ export function setWorkspaceSearchParams(
export function navigateToRequestOrFolderOrWorkspace(
id: string,
model: (Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest)['model'],
model: (Workspace | Folder | HttpRequest | GrpcRequest | WebsocketRequest)["model"],
) {
if (model === 'workspace') {
if (model === "workspace") {
setWorkspaceSearchParams({ request_id: null, folder_id: null });
} else if (model === 'folder') {
} else if (model === "folder") {
setWorkspaceSearchParams({ request_id: null, folder_id: id });
} else {
setWorkspaceSearchParams({ request_id: id, folder_id: null });

View File

@@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api/core';
import type { Settings } from '@yaakapp-internal/models';
import { invoke } from "@tauri-apps/api/core";
import type { Settings } from "@yaakapp-internal/models";
export function getSettings(): Promise<Settings> {
return invoke<Settings>('models_get_settings');
return invoke<Settings>("models_get_settings");
}

View File

@@ -1,8 +1,8 @@
import { VStack } from '../components/core/Stacks';
import { WorkspaceEncryptionSetting } from '../components/WorkspaceEncryptionSetting';
import { activeWorkspaceMetaAtom } from '../hooks/useActiveWorkspace';
import { showDialog } from './dialog';
import { jotaiStore } from './jotai';
import { VStack } from "../components/core/Stacks";
import { WorkspaceEncryptionSetting } from "../components/WorkspaceEncryptionSetting";
import { activeWorkspaceMetaAtom } from "../hooks/useActiveWorkspace";
import { showDialog } from "./dialog";
import { jotaiStore } from "./jotai";
export function setupOrConfigureEncryption() {
setupOrConfigure();
@@ -20,9 +20,9 @@ export function withEncryptionEnabled(callback?: () => void) {
function setupOrConfigure(onEnable?: () => void) {
showDialog({
id: 'workspace-encryption',
title: 'Workspace Encryption',
size: 'md',
id: "workspace-encryption",
title: "Workspace Encryption",
size: "md",
render: ({ hide }) => (
<VStack space={3} className="pb-2" alignItems="end">
<WorkspaceEncryptionSetting expanded onDone={hide} onEnabledEncryption={onEnable} />

View File

@@ -1,13 +1,13 @@
import type { Environment } from '@yaakapp-internal/models';
import { patchModel } from '@yaakapp-internal/models';
import { EnvironmentColorPicker } from '../components/EnvironmentColorPicker';
import { showDialog } from './dialog';
import type { Environment } from "@yaakapp-internal/models";
import { patchModel } from "@yaakapp-internal/models";
import { EnvironmentColorPicker } from "../components/EnvironmentColorPicker";
import { showDialog } from "./dialog";
export function showColorPicker(environment: Environment) {
showDialog({
title: 'Environment Color',
id: 'color-picker',
size: 'sm',
title: "Environment Color",
id: "color-picker",
size: "sm",
render: ({ hide }) => {
return (
<EnvironmentColorPicker

View File

@@ -1,64 +1,64 @@
import type { InvokeArgs } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core';
import type { InvokeArgs } from "@tauri-apps/api/core";
import { invoke } from "@tauri-apps/api/core";
type TauriCmd =
| 'cmd_call_grpc_request_action'
| 'cmd_call_http_authentication_action'
| 'cmd_call_http_request_action'
| 'cmd_call_websocket_request_action'
| 'cmd_call_workspace_action'
| 'cmd_call_folder_action'
| 'cmd_check_for_updates'
| 'cmd_curl_to_request'
| 'cmd_decrypt_template'
| 'cmd_default_headers'
| 'cmd_delete_all_grpc_connections'
| 'cmd_delete_all_http_responses'
| 'cmd_delete_send_history'
| 'cmd_dismiss_notification'
| 'cmd_export_data'
| 'cmd_format_graphql'
| 'cmd_format_json'
| 'cmd_get_http_authentication_config'
| 'cmd_get_http_authentication_summaries'
| 'cmd_get_http_response_events'
| 'cmd_get_sse_events'
| 'cmd_get_themes'
| 'cmd_get_workspace_meta'
| 'cmd_git_add_credential'
| 'cmd_git_clone'
| 'cmd_grpc_go'
| 'cmd_grpc_reflect'
| 'cmd_grpc_request_actions'
| 'cmd_http_request_actions'
| 'cmd_websocket_request_actions'
| 'cmd_workspace_actions'
| 'cmd_folder_actions'
| 'cmd_http_request_body'
| 'cmd_http_response_body'
| 'cmd_import_data'
| 'cmd_metadata'
| 'cmd_restart'
| 'cmd_new_child_window'
| 'cmd_new_main_window'
| 'cmd_plugin_info'
| 'cmd_plugin_init_errors'
| 'cmd_reload_plugins'
| 'cmd_render_template'
| 'cmd_save_response'
| 'cmd_secure_template'
| 'cmd_send_ephemeral_request'
| 'cmd_send_http_request'
| 'cmd_template_function_summaries'
| 'cmd_template_function_config'
| 'cmd_template_tokens_to_string';
| "cmd_call_grpc_request_action"
| "cmd_call_http_authentication_action"
| "cmd_call_http_request_action"
| "cmd_call_websocket_request_action"
| "cmd_call_workspace_action"
| "cmd_call_folder_action"
| "cmd_check_for_updates"
| "cmd_curl_to_request"
| "cmd_decrypt_template"
| "cmd_default_headers"
| "cmd_delete_all_grpc_connections"
| "cmd_delete_all_http_responses"
| "cmd_delete_send_history"
| "cmd_dismiss_notification"
| "cmd_export_data"
| "cmd_format_graphql"
| "cmd_format_json"
| "cmd_get_http_authentication_config"
| "cmd_get_http_authentication_summaries"
| "cmd_get_http_response_events"
| "cmd_get_sse_events"
| "cmd_get_themes"
| "cmd_get_workspace_meta"
| "cmd_git_add_credential"
| "cmd_git_clone"
| "cmd_grpc_go"
| "cmd_grpc_reflect"
| "cmd_grpc_request_actions"
| "cmd_http_request_actions"
| "cmd_websocket_request_actions"
| "cmd_workspace_actions"
| "cmd_folder_actions"
| "cmd_http_request_body"
| "cmd_http_response_body"
| "cmd_import_data"
| "cmd_metadata"
| "cmd_restart"
| "cmd_new_child_window"
| "cmd_new_main_window"
| "cmd_plugin_info"
| "cmd_plugin_init_errors"
| "cmd_reload_plugins"
| "cmd_render_template"
| "cmd_save_response"
| "cmd_secure_template"
| "cmd_send_ephemeral_request"
| "cmd_send_http_request"
| "cmd_template_function_summaries"
| "cmd_template_function_config"
| "cmd_template_tokens_to_string";
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
// console.log('RUN COMMAND', cmd, args);
try {
return await invoke(cmd, args);
} catch (err) {
console.warn('Tauri command error', cmd, err);
console.warn("Tauri command error", cmd, err);
throw err;
}
}

View File

@@ -1,10 +1,10 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { fireAndForget } from '../fireAndForget';
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { fireAndForget } from "../fireAndForget";
export type Appearance = 'light' | 'dark';
export type Appearance = "light" | "dark";
export function getCSSAppearance(): Appearance {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
export async function getWindowAppearance(): Promise<Appearance> {
@@ -23,13 +23,15 @@ export function subscribeToWindowAppearanceChange(
unsubscribe: () => {},
};
fireAndForget(getCurrentWebviewWindow()
.onThemeChanged((t) => {
cb(t.payload);
})
.then((l) => {
container.unsubscribe = l;
}));
fireAndForget(
getCurrentWebviewWindow()
.onThemeChanged((t) => {
cb(t.payload);
})
.then((l) => {
container.unsubscribe = l;
}),
);
return () => container.unsubscribe();
}
@@ -38,8 +40,8 @@ export function resolveAppearance(
preferredAppearance: Appearance,
appearanceSetting: string,
): Appearance {
const appearance = appearanceSetting === 'system' ? preferredAppearance : appearanceSetting;
return appearance === 'dark' ? 'dark' : 'light';
const appearance = appearanceSetting === "system" ? preferredAppearance : appearanceSetting;
return appearance === "dark" ? "dark" : "light";
}
export function subscribeToPreferredAppearance(cb: (a: Appearance) => void) {

View File

@@ -1,10 +1,10 @@
import type { GetThemesResponse } from '@yaakapp-internal/plugins';
import { invokeCmd } from '../tauri';
import type { Appearance } from './appearance';
import { resolveAppearance } from './appearance';
import type { GetThemesResponse } from "@yaakapp-internal/plugins";
import { invokeCmd } from "../tauri";
import type { Appearance } from "./appearance";
import { resolveAppearance } from "./appearance";
export async function getThemes() {
const themes = (await invokeCmd<GetThemesResponse[]>('cmd_get_themes')).flatMap((t) => t.themes);
const themes = (await invokeCmd<GetThemesResponse[]>("cmd_get_themes")).flatMap((t) => t.themes);
themes.sort((a, b) => a.label.localeCompare(b.label));
// Remove duplicates, in case multiple plugins provide the same theme
const uniqueThemes = Array.from(new Map(themes.map((t) => [t.id, t])).values());
@@ -26,82 +26,82 @@ export async function getResolvedTheme(
const dark = darkThemes.find((t) => t.id === themeDark) ?? darkThemes[0] ?? yaakDark;
const light = lightThemes.find((t) => t.id === themeLight) ?? lightThemes[0] ?? yaakLight;
const active = appearance === 'dark' ? dark : light;
const active = appearance === "dark" ? dark : light;
return { dark, light, active };
}
const yaakDark = {
id: 'yaak-dark',
label: 'Yaak',
id: "yaak-dark",
label: "Yaak",
dark: true,
base: {
surface: 'hsl(244,23%,14%)',
surfaceHighlight: 'hsl(244,23%,20%)',
text: 'hsl(245,23%,85%)',
textSubtle: 'hsl(245,18%,58%)',
textSubtlest: 'hsl(245,18%,45%)',
border: 'hsl(244,23%,25%)',
primary: 'hsl(266,100%,79%)',
secondary: 'hsl(245,23%,60%)',
info: 'hsl(206,100%,63%)',
success: 'hsl(150,99%,44%)',
notice: 'hsl(48,80%,63%)',
warning: 'hsl(28,100%,61%)',
danger: 'hsl(342,90%,68%)',
surface: "hsl(244,23%,14%)",
surfaceHighlight: "hsl(244,23%,20%)",
text: "hsl(245,23%,85%)",
textSubtle: "hsl(245,18%,58%)",
textSubtlest: "hsl(245,18%,45%)",
border: "hsl(244,23%,25%)",
primary: "hsl(266,100%,79%)",
secondary: "hsl(245,23%,60%)",
info: "hsl(206,100%,63%)",
success: "hsl(150,99%,44%)",
notice: "hsl(48,80%,63%)",
warning: "hsl(28,100%,61%)",
danger: "hsl(342,90%,68%)",
},
components: {
button: {
primary: 'hsl(266,100%,71.1%)',
secondary: 'hsl(244,23%,54%)',
info: 'hsl(206,100%,56.7%)',
success: 'hsl(150,99%,37.4%)',
notice: 'hsl(48,80%,50.4%)',
warning: 'hsl(28,100%,54.9%)',
danger: 'hsl(342,90%,61.2%)',
primary: "hsl(266,100%,71.1%)",
secondary: "hsl(244,23%,54%)",
info: "hsl(206,100%,56.7%)",
success: "hsl(150,99%,37.4%)",
notice: "hsl(48,80%,50.4%)",
warning: "hsl(28,100%,54.9%)",
danger: "hsl(342,90%,61.2%)",
},
dialog: {
border: 'hsl(244,23%,24%)',
border: "hsl(244,23%,24%)",
},
sidebar: {
surface: 'hsl(243,23%,16%)',
border: 'hsl(244,23%,22%)',
surface: "hsl(243,23%,16%)",
border: "hsl(244,23%,22%)",
},
responsePane: {
surface: 'hsl(243,23%,16%)',
border: 'hsl(246,23%,23%)',
surface: "hsl(243,23%,16%)",
border: "hsl(246,23%,23%)",
},
appHeader: {
surface: 'hsl(244,23%,12%)',
border: 'hsl(244,23%,21%)',
surface: "hsl(244,23%,12%)",
border: "hsl(244,23%,21%)",
},
},
};
const yaakLight = {
id: 'yaak-light',
label: 'Yaak',
id: "yaak-light",
label: "Yaak",
dark: false,
base: {
surface: 'hsl(0,0%,100%)',
surfaceHighlight: 'hsl(218,24%,87%)',
text: 'hsl(217,24%,10%)',
textSubtle: 'hsl(217,24%,40%)',
textSubtlest: 'hsl(217,24%,58%)',
border: 'hsl(217,22%,90%)',
primary: 'hsl(266,100%,60%)',
secondary: 'hsl(220,24%,50%)',
info: 'hsl(206,100%,40%)',
success: 'hsl(139,66%,34%)',
notice: 'hsl(45,100%,34%)',
warning: 'hsl(30,100%,36%)',
danger: 'hsl(335,75%,48%)',
surface: "hsl(0,0%,100%)",
surfaceHighlight: "hsl(218,24%,87%)",
text: "hsl(217,24%,10%)",
textSubtle: "hsl(217,24%,40%)",
textSubtlest: "hsl(217,24%,58%)",
border: "hsl(217,22%,90%)",
primary: "hsl(266,100%,60%)",
secondary: "hsl(220,24%,50%)",
info: "hsl(206,100%,40%)",
success: "hsl(139,66%,34%)",
notice: "hsl(45,100%,34%)",
warning: "hsl(30,100%,36%)",
danger: "hsl(335,75%,48%)",
},
components: {
sidebar: {
surface: 'hsl(220,20%,98%)',
border: 'hsl(217,22%,88%)',
surfaceHighlight: 'hsl(217,25%,90%)',
surface: "hsl(220,20%,98%)",
border: "hsl(217,22%,88%)",
surfaceHighlight: "hsl(217,25%,90%)",
},
},
};

View File

@@ -1,6 +1,6 @@
import type { Theme, ThemeComponentColors } from '@yaakapp-internal/plugins';
import { defaultDarkTheme, defaultLightTheme } from './themes';
import { YaakColor } from './yaakColor';
import type { Theme, ThemeComponentColors } from "@yaakapp-internal/plugins";
import { defaultDarkTheme, defaultLightTheme } from "./themes";
import { YaakColor } from "./yaakColor";
export type YaakColors = {
surface: YaakColor;
@@ -50,7 +50,7 @@ export type YaakTheme = {
export type YaakColorKey = keyof ThemeComponentColors;
type ComponentName = keyof NonNullable<YaakTheme['components']>;
type ComponentName = keyof NonNullable<YaakTheme["components"]>;
type CSSVariables = Record<YaakColorKey, string | undefined>;
@@ -155,7 +155,7 @@ function buttonSolidColorVariables(
if (color == null) return {};
const theme: Partial<ThemeComponentColors> = {
text: 'white',
text: "white",
surface: color.lower(0.3).css(),
surfaceHighlight: color.lower(0.1).css(),
border: color.css(),
@@ -204,7 +204,7 @@ function variablesToCSS(
const css = Object.entries(vars ?? {})
.filter(([, value]) => value)
.map(([name, value]) => `--${name}: ${value};`)
.join('\n');
.join("\n");
return selector == null ? css : `${selector} {\n${indent(css)}\n}`;
}
@@ -231,7 +231,7 @@ function buttonCSS(
return [
variablesToCSS(`.x-theme-button--solid--${color}`, buttonSolidColorVariables(yaakColor)),
variablesToCSS(`.x-theme-button--border--${color}`, buttonBorderColorVariables(yaakColor)),
].join('\n\n');
].join("\n\n");
}
function bannerCSS(
@@ -245,7 +245,7 @@ function bannerCSS(
}
return [variablesToCSS(`.x-theme-banner--${color}`, bannerColorVariables(yaakColor))].join(
'\n\n',
"\n\n",
);
}
@@ -255,7 +255,7 @@ function toastCSS(theme: Theme, color: YaakColorKey, colors?: ThemeComponentColo
return null;
}
return [variablesToCSS(`.x-theme-toast--${color}`, toastColorVariables(yaakColor))].join('\n\n');
return [variablesToCSS(`.x-theme-toast--${color}`, toastColorVariables(yaakColor))].join("\n\n");
}
function templateTagCSS(
@@ -270,7 +270,7 @@ function templateTagCSS(
return [
variablesToCSS(`.x-theme-templateTag--${color}`, templateTagColorVariables(yaakColor)),
].join('\n\n');
].join("\n\n");
}
export function getThemeCSS(theme: Theme): string {
@@ -283,18 +283,18 @@ export function getThemeCSS(theme: Theme): string {
return { ...prev, [key]: theme.base[key as YaakColorKey] };
}, {}) as ThemeComponentColors;
let themeCSS = '';
let themeCSS = "";
try {
const baseCss = variablesToCSS(null, themeVariables(theme));
themeCSS = [
baseCss,
...Object.keys(components ?? {}).map((key) => componentCSS(theme, key as ComponentName)),
variablesToCSS(
'.x-theme-button--solid--default',
".x-theme-button--solid--default",
buttonSolidColorVariables(yc(theme, theme.base.surface), true),
),
variablesToCSS(
'.x-theme-button--border--default',
".x-theme-button--border--default",
buttonBorderColorVariables(yc(theme, theme.base.surface), true),
),
...Object.keys(colors ?? {}).map((key) =>
@@ -309,46 +309,46 @@ export function getThemeCSS(theme: Theme): string {
...Object.keys(colors ?? {}).map((key) =>
templateTagCSS(theme, key as YaakColorKey, theme.components?.templateTag ?? colors),
),
].join('\n\n');
].join("\n\n");
} catch (err) {
console.error('Failed to generate CSS', err);
console.error("Failed to generate CSS", err);
}
return [`/* ${label} */`, `[data-theme="${id}"] {`, indent(themeCSS), '}'].join('\n');
return [`/* ${label} */`, `[data-theme="${id}"] {`, indent(themeCSS), "}"].join("\n");
}
export function addThemeStylesToDocument(rawTheme: Theme | null) {
if (rawTheme == null) {
console.error('Failed to add theme styles: theme is null');
console.error("Failed to add theme styles: theme is null");
return;
}
const theme = completeTheme(rawTheme);
let styleEl = document.head.querySelector('style[data-theme]');
let styleEl = document.head.querySelector("style[data-theme]");
if (!styleEl) {
styleEl = document.createElement('style');
styleEl = document.createElement("style");
document.head.appendChild(styleEl);
}
styleEl.setAttribute('data-theme', theme.id);
styleEl.setAttribute('data-updated-at', new Date().toISOString());
styleEl.setAttribute("data-theme", theme.id);
styleEl.setAttribute("data-updated-at", new Date().toISOString());
styleEl.textContent = getThemeCSS(theme);
}
export function setThemeOnDocument(theme: Theme | null) {
if (theme == null) {
console.error('Failed to set theme: theme is null');
console.error("Failed to set theme: theme is null");
return;
}
document.documentElement.setAttribute('data-theme', theme.id);
document.documentElement.setAttribute("data-theme", theme.id);
}
export function indent(text: string, space = ' '): string {
export function indent(text: string, space = " "): string {
return text
.split('\n')
.split("\n")
.map((line) => space + line)
.join('\n');
.join("\n");
}
function yc<T extends string | null | undefined>(
@@ -356,7 +356,7 @@ function yc<T extends string | null | undefined>(
s: T,
): T extends string ? YaakColor : null {
if (s == null) return null as never;
return new YaakColor(s, theme.dark ? 'dark' : 'light') as never;
return new YaakColor(s, theme.dark ? "dark" : "light") as never;
}
export function completeTheme(theme: Theme): Theme {

View File

@@ -1,37 +1,37 @@
import parseColor from 'parse-color';
import parseColor from "parse-color";
export class YaakColor {
private readonly appearance: 'dark' | 'light' = 'light';
private readonly appearance: "dark" | "light" = "light";
private hue = 0;
private saturation = 0;
private lightness = 0;
private alpha = 1;
constructor(cssColor: string, appearance: 'dark' | 'light' = 'light') {
constructor(cssColor: string, appearance: "dark" | "light" = "light") {
try {
this.set(cssColor);
this.appearance = appearance;
} catch (err) {
console.log('Failed to parse CSS color', cssColor, err);
console.log("Failed to parse CSS color", cssColor, err);
}
}
static transparent(): YaakColor {
return new YaakColor('rgb(0,0,0)', 'light').translucify(1);
return new YaakColor("rgb(0,0,0)", "light").translucify(1);
}
static white(): YaakColor {
return new YaakColor('rgb(0,0,0)', 'light').lower(1);
return new YaakColor("rgb(0,0,0)", "light").lower(1);
}
static black(): YaakColor {
return new YaakColor('rgb(0,0,0)', 'light').lift(1);
return new YaakColor("rgb(0,0,0)", "light").lift(1);
}
set(cssColor: string): YaakColor {
let fixedCssColor = cssColor;
if (cssColor.startsWith('#') && cssColor.length === 9) {
if (cssColor.startsWith("#") && cssColor.length === 9) {
const [r, g, b, a] = hexToRgba(cssColor);
fixedCssColor = `rgba(${r},${g},${b},${a})`;
}
@@ -48,11 +48,11 @@ export class YaakColor {
}
lower(mod: number): YaakColor {
return this.appearance === 'dark' ? this._darken(mod) : this._lighten(mod);
return this.appearance === "dark" ? this._darken(mod) : this._lighten(mod);
}
lift(mod: number): YaakColor {
return this.appearance === 'dark' ? this._lighten(mod) : this._darken(mod);
return this.appearance === "dark" ? this._lighten(mod) : this._darken(mod);
}
minLightness(n: number): YaakColor {
@@ -132,7 +132,7 @@ function rgbaToHex(r: number, g: number, b: number, a: number): string {
const hex = Number(Math.round(n)).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
return `#${[toHex(r), toHex(g), toHex(b), toHex(a * 255)].join('').toUpperCase()}`;
return `#${[toHex(r), toHex(g), toHex(b), toHex(a * 255)].join("").toUpperCase()}`;
}
function rgbaToHexNoAlpha(r: number, g: number, b: number): string {
@@ -140,12 +140,12 @@ function rgbaToHexNoAlpha(r: number, g: number, b: number): string {
const hex = Number(Math.round(n)).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
return `#${[toHex(r), toHex(g), toHex(b)].join('').toUpperCase()}`;
return `#${[toHex(r), toHex(g), toHex(b)].join("").toUpperCase()}`;
}
function hexToRgba(hex: string): [number, number, number, number] {
const fromHex = (h: string): number => {
if (h === '') return 255;
if (h === "") return 255;
return Number(`0x${h}`);
};

View File

@@ -1,7 +1,7 @@
import { atom } from 'jotai';
import type { ToastInstance } from '../components/Toasts';
import { generateId } from './generateId';
import { jotaiStore } from './jotai';
import { atom } from "jotai";
import type { ToastInstance } from "../components/Toasts";
import { generateId } from "./generateId";
import { jotaiStore } from "./jotai";
export const toastsAtom = atom<ToastInstance[]>([]);
@@ -9,9 +9,9 @@ export function showToast({
id,
timeout = 5000,
...props
}: Omit<ToastInstance, 'id' | 'timeout' | 'uniqueKey'> & {
id?: ToastInstance['id'];
timeout?: ToastInstance['timeout'];
}: Omit<ToastInstance, "id" | "timeout" | "uniqueKey"> & {
id?: ToastInstance["id"];
timeout?: ToastInstance["timeout"];
}) {
id = id ?? generateId();
const uniqueKey = generateId();
@@ -56,7 +56,7 @@ export function showErrorToast<T>({
}) {
return showToast({
id,
color: 'danger',
color: "danger",
timeout: null,
message: (
<div className="w-full">