Compare commits

...

6 Commits

Author SHA1 Message Date
Gregory Schier
64f5d973eb Fix env editor switching 2025-05-23 21:44:34 -07:00
Gregory Schier
041298b3f8 Detect JSON language if application/javascript returns JSON 2025-05-21 11:05:20 -07:00
Gregory Schier
b400940f0e Fix import curl 2025-05-21 11:04:57 -07:00
Gregory Schier
2e144f064d Fix syntax highlighting 2025-05-21 08:26:15 -07:00
Gregory Schier
d8b1cadae6 Fix model deletion 2025-05-21 08:25:12 -07:00
Gregory Schier
c2f9760d08 Fix template parsing 2025-05-21 08:18:09 -07:00
12 changed files with 51 additions and 115 deletions

View File

@@ -72,7 +72,7 @@ function mixLanguage(base: LanguageSupport): LRLanguage {
return {
parser: base.language.parser,
overlay: (node) => node.type.name === 'PlainText',
overlay: (node) => node.type.name === 'Text',
};
}),
});

View File

@@ -1,14 +1,7 @@
import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({
"${[": t.bracket,
"]}": t.bracket,
"(": t.bracket,
")": t.bracket,
"=": t.bracket,
",": t.bracket,
Tag: t.keyword,
Identifier: t.variableName,
ChainedIdentifier: t.variableName,
Boolean: t.bool,
TagOpen: t.bracket,
TagClose: t.bracket,
TagContent: t.keyword,
});

View File

@@ -80,6 +80,10 @@ function templateTags(
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner;
if (inner.includes('\n')) {
return;
}
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
if (name === 'Response') {

View File

@@ -1,70 +1,17 @@
@precedence {
top,
mid,
lesser
@top Template { (Tag | Text)* }
@local tokens {
TagClose { "]}" }
@else TagContent
}
@top Template { (Tag | PlainText)* }
@skip { space }
Tag { "${[" expression* "]}" }
commaSep<content> { "" | content ("," content?)* }
expression {
Function |
Assignment |
Identifier |
ChainedIdentifier |
basicType |
hashStructure
@skip { } {
TagOpen { "${[" }
Tag { TagOpen (TagContent)+ TagClose }
}
basicType {
String |
Boolean |
Number
}
functionParam {
Identifier |
Assignment |
basicType
}
FunctionParamList {
"(" commaSep<functionParam> ")"
}
Assignment { Identifier "=" expression }
Function { (Identifier | ChainedIdentifier) !top FunctionParamList}
@tokens {
PlainText { ![$] PlainText? | "$" (@eof | ![{[] PlainText?) }
Identifier { $[a-zA-Z_\-0-9]+ }
ChainedIdentifier { Identifier ("." Identifier)+ }
hashStructure { "{" | "}" | ":" | ","}
Boolean { "true" | "false" }
String { ("'" (![\\'] | "\\" _)* "'"?) | ("\"" (![\\"] | "\\" _)* "\""?) }
Number { '-'? int frac? }
int { '0' | $[1-9] @digit* }
frac { '.' @digit+ }
@precedence { PlainText, Boolean, Number, ChainedIdentifier, Identifier, space, String }
space { $[ \t\n\r]+ }
"${[" "]}"
"(" ")"
"="
","
Text { ![$] Text? | "$" (@eof | ![{] Text?) }
}
@external propSource highlight from "./highlight"
@detectDelim

View File

@@ -1,13 +1,8 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Template = 1,
Tag = 4,
Function = 5,
Identifier = 6,
ChainedIdentifier = 7,
FunctionParamList = 10,
Assignment = 11,
String = 13,
Boolean = 14,
Number = 15,
PlainText = 17
Tag = 2,
TagOpen = 3,
TagContent = 4,
TagClose = 5,
Text = 6

View File

@@ -1,22 +1,18 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {LRParser, LocalTokenGroup} from "@lezer/lr"
import {highlight} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: "$zQVQPOOOsQQO'#C`OOQO'#Cn'#CnQVQPOOO!fQQO'#CtOOQO'#Cw'#CwOzQQO'#CtOOQO'#Ct'#CtOOQO'#Co'#CoO!mQQO,58zOOQO,58z,58zOOQO-E6l-E6lO_QQO,59RO!tQQO'#CfOOQO,58{,58{OOQO-E6m-E6mOOQO1G.f1G.fOOQO1G.m1G.mO#VQSO'#CvOOQO'#Cv'#CvO#bQSO'#CuO#jQQO,59QO#oQSO'#CpO$TQSO,59aOOQO1G.l1G.lO$]QSO'#CtO$kQSO'#CtOOQO,59[,59[OOQO-E6n-E6nO$vQQO,59R",
stateData: "%e~OgOS~ORPOaQO~OUSOVUO]TO^TO_TOlVO~OQYO~P_OX]OQhXUhXVhX]hX^hX_hXlhX~O[[O~PzOQ`O~P_OUbO]TO^TO_TOWiP~O[mOWjX`jX~O`fOWiX~OWhO~OUbO]TO^TO_TOWdX`dX~O`fOWia~OX]O[mOWhX`hX~OX]OWhX`hX~OUiOVjO]TO^TO_TOlVO~Oa^_VUg]V~",
goto: "!|lPPPPmqPPPPw}PPPPPP!X!_!ePPP!k!s!v}TQORXVPX[mX^SUijWVPX[mTc]fQRORZRQXPR_XQgdRlgSWPXTa[mRe]Qd]Rkf",
nodeNames: "⚠ Template ]} ${[ Tag Function Identifier ChainedIdentifier ) ( FunctionParamList Assignment = String Boolean Number , PlainText",
maxTerm: 28,
nodeProps: [
["openedBy", 2,"${[",8,"("],
["closedBy", 3,"]}",9,")"]
],
states: "!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
stateData: "g~OUROYPO~OSTO~OSTOTXO~O",
goto: "nXPPY^PPPbhTROSTQOSQSORVSQUQRWU",
nodeNames: "⚠ Template Tag TagOpen TagContent TagClose Text",
maxTerm: 10,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 3,
tokenData: "Fe~RzOX#uXY%OYZ%OZ]#u]^%O^p#upq%Oqr#urs%{st#utu+Ouw#uwx+vxy0^yz0tz|#u|}1[}!O1t!O!Q#u!Q!R6h!R![:S![!];_!]!_#u!_!`;u!`!c#u!c!}3Q!}#P#u#P#Q<]#Q#R#u#R#S3Q#S#T#u#T#Y3Q#Y#Z=_#Z#h3Q#h#iCu#i#o3Q#o#p;_#p#q#u#q#r;_#r;'S#u;'S;=`$s<%lO#uP#zTaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uP$^VO!}#u#O#o#u#p;'S#u;'S;=`$s<%l~#u~O#u~~$yP$vP;=`<%l#uP%OOaP~%V[aPg~OX#uXY%OYZ%OZ]#u]^%O^p#upq%Oqt#utu$Zu;'S#u;'S;=`$s<%lO#u~&SXaP]~Or%{rs&ost%{tu'Vu#O%{#O#P)r#P;'S%{;'S;=`*x<%lO%{~&vTaP]~Ot#utu$Zu;'S#u;'S;=`$s<%lO#u~'[[]~Or%{rs&os!}%{!}#O(Q#O#P)r#P#o%{#o#p(Q#p;'S%{;'S;=`*x<%l~%{~O%{~~$y~(VV]~Or(Qrs(ls#O(Q#O#P(q#P;'S(Q;'S;=`)l<%lO(Q~(qO]~~(tRO;'S(Q;'S;=`(};=`O(Q~)SW]~Or(Qrs(ls#O(Q#O#P(q#P;'S(Q;'S;=`)l;=`<%l(Q<%lO(Q~)oP;=`<%l(Q~)wUaPOt%{tu'Vu;'S%{;'S;=`*Z;=`<%l(Q<%lO%{~*`W]~Or(Qrs(ls#O(Q#O#P(q#P;'S(Q;'S;=`)l;=`<%l%{<%lO(Q~*{P;=`<%l%{~+RWO!}#u#O#o#u#o#p+k#p;'S#u;'S;=`$s<%l~#u~O#u~~$y~+nP!}#O+q~+vOR~~+}XaP]~Ot+vtu,juw+vwx&ox#O+v#O#P/Q#P;'S+v;'S;=`0W<%lO+v~,o[]~Ow+vwx&ox!}+v!}#O-e#O#P/Q#P#o+v#o#p-e#p;'S+v;'S;=`0W<%l~+v~O+v~~$y~-jV]~Ow-ewx(lx#O-e#O#P.P#P;'S-e;'S;=`.z<%lO-e~.SRO;'S-e;'S;=`.];=`O-e~.bW]~Ow-ewx(lx#O-e#O#P.P#P;'S-e;'S;=`.z;=`<%l-e<%lO-e~.}P;=`<%l-e~/VUaPOt+vtu,ju;'S+v;'S;=`/i;=`<%l-e<%lO+v~/nW]~Ow-ewx(lx#O-e#O#P.P#P;'S-e;'S;=`.z;=`<%l+v<%lO-e~0ZP;=`<%l+vV0eTXUaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uV0{TWUaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uV1eT`SlQaPOt#utu$Zu;'S#u;'S;=`$s<%lO#u~1{aaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q!R6h!R![:S![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~3X`aPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~4`_aPOt#utu$Zu}#u}!O5_!O!Q#u!Q![5_![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~5f`aPV~Ot#utu$Zu}#u}!O5_!O!P4Z!P!Q#u!Q![5_![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~6q`aP_~U~Ot#utu$Zu}#u}!O3Q!O!P7s!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~7x_aPOt#utu$Zu}#u}!O5_!O!Q#u!Q![8w![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~9Q`aP_~V~Ot#utu$Zu}#u}!O5_!O!P4Z!P!Q#u!Q![8w![!c#u!c!}5_!}#R#u#R#S5_#S#T#u#T#o5_#o;'S#u;'S;=`$s<%lO#u~:]`aP_~U~Ot#utu$Zu}#u}!O3Q!O!P7s!P!Q#u!Q![:S![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#uR;fTlQaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uV;|T[UaPOt#utu$Zu;'S#u;'S;=`$s<%lO#uR<bVaPOt#utu$Zu#q#u#q#r<w#r;'S#u;'S;=`$s<%lO#uR=OTQQaPOt#utu$Zu;'S#u;'S;=`$s<%lO#u~=faaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#U>k#U#o3Q#o;'S#u;'S;=`$s<%lO#u~>rbaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#`3Q#`#a?z#a#o3Q#o;'S#u;'S;=`$s<%lO#u~@RbaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#g3Q#g#hAZ#h#o3Q#o;'S#u;'S;=`$s<%lO#u~AbbaPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#X3Q#X#YBj#Y#o3Q#o;'S#u;'S;=`$s<%lO#u~Bs`aP^~U~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#o3Q#o;'S#u;'S;=`$s<%lO#u~C|baPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#f3Q#f#gEU#g#o3Q#o;'S#u;'S;=`$s<%lO#u~E]baPU~Ot#utu$Zu}#u}!O3Q!O!P4Z!P!Q#u!Q![3Q![!c#u!c!}3Q!}#R#u#R#S3Q#S#T#u#T#i3Q#i#jAZ#j#o3Q#o;'S#u;'S;=`$s<%lO#u",
tokenizers: [0, 1, 2],
repeatNodeCount: 2,
tokenData: "#]~RTOtbtu!hu;'Sb;'S;=`!]<%lOb~gTU~Otbtuvu;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOU~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TP!}#O#W~#]OY~",
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
topRules: {"Template":[0,1]},
tokenPrec: 196
tokenPrec: 0
})

View File

@@ -174,15 +174,12 @@ const BaseInput = forwardRef<EditorView, InputProps>(function InputBase(
);
const isValid = useMemo(() => {
console.log('CHECKING VALIDITY', validate);
if (required && !validateRequire(defaultValue ?? '')) return false;
if (typeof validate === 'boolean') return validate;
if (typeof validate === 'function' && !validate(defaultValue ?? '')) return false;
return true;
}, [required, defaultValue, validate]);
console.log('IS VALID', isValid, defaultValue);
const handleChange = useCallback(
(value: string) => {
onChange?.(value);

View File

@@ -12,6 +12,7 @@ import {
} from 'react';
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useRandomKey } from '../../hooks/useRandomKey';
import { useToggle } from '../../hooks/useToggle';
import { languageFromContentType } from '../../lib/contentType';
import { showDialog } from '../../lib/dialog';
@@ -107,6 +108,9 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [pairs, setPairs] = useState<PairWithId[]>([]);
const [showAll, toggleShowAll] = useToggle(false);
// NOTE: Use local force update key because we trigger an effect on forceUpdateKey change. If
// we simply pass forceUpdateKey to the editor, the data set by useEffect will be stale.
const [localForceUpdateKey, regenerateLocalForceUpdateKey] = useRandomKey();
useImperativeHandle(
ref,
@@ -136,6 +140,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
}
setPairs(newPairs);
regenerateLocalForceUpdateKey();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [forceUpdateKey]);
@@ -240,7 +245,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
forcedEnvironmentId={forcedEnvironmentId}
forceFocusNamePairId={forceFocusNamePairId}
forceFocusValuePairId={forceFocusValuePairId}
forceUpdateKey={forceUpdateKey}
forceUpdateKey={localForceUpdateKey}
index={i}
isLast={isLast}
nameAutocomplete={nameAutocomplete}

View File

@@ -1,9 +1,4 @@
import {
deleteModelById,
duplicateModelById,
getModel,
workspacesAtom,
} from '@yaakapp-internal/models';
import { duplicateModelById, getModel, workspacesAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React, { useMemo } from 'react';
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
@@ -134,7 +129,9 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
hotKeyAction: 'sidebar.delete_selected_item',
hotKeyLabelOnly: true,
leftSlot: <Icon icon="trash" />,
onSelect: async () => deleteModelById(child.model, child.id),
onSelect: async () => {
await deleteModelWithConfirm(getModel(child.model, child.id));
},
},
];
}

View File

@@ -1,5 +1,6 @@
import type { HttpRequest} from '@yaakapp-internal/models';
import { createWorkspaceModel, patchModelById } from '@yaakapp-internal/models';
import type { HttpRequest } from '@yaakapp-internal/models';
import { patchModelById } from '@yaakapp-internal/models';
import { createRequestAndNavigate } from '../lib/createRequestAndNavigate';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri';
import { showToast } from '../lib/toast';
@@ -26,7 +27,7 @@ export function useImportCurl() {
let verb;
if (overwriteRequestId == null) {
verb = 'Created';
await createWorkspaceModel(importedRequest);
await createRequestAndNavigate(importedRequest);
} else {
verb = 'Updated';
await patchModelById(importedRequest.model, overwriteRequestId, (r: HttpRequest) => ({

View File

@@ -11,7 +11,7 @@ export function languageFromContentType(
} else if (justContentType.includes('xml')) {
return 'xml';
} else if (justContentType.includes('html')) {
const detected = detectFromContent(content, 'html');
const detected = detectFromContent(content);
if (detected === 'xml') {
// If it's detected as XML, but is already HTML, don't change it
return 'html';
@@ -19,7 +19,8 @@ export function languageFromContentType(
return detected;
}
} else if (justContentType.includes('javascript')) {
return 'javascript';
// Sometimes `application/javascript` returns JSON, so try detecting that
return detectFromContent(content, 'javascript');
}
return detectFromContent(content, 'text');
@@ -27,7 +28,7 @@ export function languageFromContentType(
function detectFromContent(
content: string | null,
fallback: EditorProps['language'],
fallback?: EditorProps['language'],
): EditorProps['language'] {
if (content == null) return 'text';

View File

@@ -11,7 +11,7 @@ export async function deleteModelWithConfirm(model: AnyModel | null): Promise<bo
}
const confirmed = await showConfirmDelete({
id: 'delete-model-' + model.model,
id: 'delete-model-' + model.id,
title: 'Delete ' + modelTypeLabel(model),
description: (
<>