mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 23:31:21 +02:00
A bit more chaining cleanup
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { TemplateFunctionCheckboxArg } from "./TemplateFunctionCheckboxArg";
|
||||||
import type { TemplateFunctionHttpRequestArg } from "./TemplateFunctionHttpRequestArg";
|
import type { TemplateFunctionHttpRequestArg } from "./TemplateFunctionHttpRequestArg";
|
||||||
import type { TemplateFunctionSelectArg } from "./TemplateFunctionSelectArg";
|
import type { TemplateFunctionSelectArg } from "./TemplateFunctionSelectArg";
|
||||||
import type { TemplateFunctionTextArg } from "./TemplateFunctionTextArg";
|
import type { TemplateFunctionTextArg } from "./TemplateFunctionTextArg";
|
||||||
|
|
||||||
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg;
|
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type TemplateFunctionCheckboxArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
|
||||||
@@ -56,6 +56,7 @@ export * from './gen/ShowToastRequest';
|
|||||||
export * from './gen/TemplateFunction';
|
export * from './gen/TemplateFunction';
|
||||||
export * from './gen/TemplateFunctionArg';
|
export * from './gen/TemplateFunctionArg';
|
||||||
export * from './gen/TemplateFunctionBaseArg';
|
export * from './gen/TemplateFunctionBaseArg';
|
||||||
|
export * from './gen/TemplateFunctionCheckboxArg';
|
||||||
export * from './gen/TemplateFunctionHttpRequestArg';
|
export * from './gen/TemplateFunctionHttpRequestArg';
|
||||||
export * from './gen/TemplateFunctionSelectArg';
|
export * from './gen/TemplateFunctionSelectArg';
|
||||||
export * from './gen/TemplateFunctionSelectOption';
|
export * from './gen/TemplateFunctionSelectOption';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Context,
|
||||||
FindHttpResponsesResponse,
|
FindHttpResponsesResponse,
|
||||||
GetHttpRequestByIdResponse,
|
GetHttpRequestByIdResponse,
|
||||||
HttpRequestAction,
|
HttpRequestAction,
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
SendHttpRequestResponse,
|
SendHttpRequestResponse,
|
||||||
TemplateFunction,
|
TemplateFunction,
|
||||||
} from '@yaakapp/api';
|
} from '@yaakapp/api';
|
||||||
import { Context } from '@yaakapp/api';
|
|
||||||
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction';
|
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction';
|
||||||
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
|
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
|
||||||
import interceptStdout from 'intercept-stdout';
|
import interceptStdout from 'intercept-stdout';
|
||||||
@@ -18,7 +18,6 @@ import { readFileSync } from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import * as util from 'node:util';
|
import * as util from 'node:util';
|
||||||
import { parentPort, workerData } from 'node:worker_threads';
|
import { parentPort, workerData } from 'node:worker_threads';
|
||||||
import { text } from '../../src-web/components/core/Editor/text/extension';
|
|
||||||
|
|
||||||
new Promise<void>(async (resolve, reject) => {
|
new Promise<void>(async (resolve, reject) => {
|
||||||
const { pluginDir, pluginRefId } = workerData;
|
const { pluginDir, pluginRefId } = workerData;
|
||||||
@@ -244,7 +243,6 @@ new Promise<void>(async (resolve, reject) => {
|
|||||||
const action = mod.plugin.templateFunctions.find((a) => a.name === payload.name);
|
const action = mod.plugin.templateFunctions.find((a) => a.name === payload.name);
|
||||||
if (typeof action?.onRender === 'function') {
|
if (typeof action?.onRender === 'function') {
|
||||||
const result = await action.onRender(ctx, payload.args);
|
const result = await action.onRender(ctx, payload.args);
|
||||||
console.log('GOT VALUE', result);
|
|
||||||
sendPayload({ type: 'call_template_function_response', value: result ?? null }, replyId);
|
sendPayload({ type: 'call_template_function_response', value: result ?? null }, replyId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub enum InternalEventPayload {
|
|||||||
|
|
||||||
GetHttpRequestByIdRequest(GetHttpRequestByIdRequest),
|
GetHttpRequestByIdRequest(GetHttpRequestByIdRequest),
|
||||||
GetHttpRequestByIdResponse(GetHttpRequestByIdResponse),
|
GetHttpRequestByIdResponse(GetHttpRequestByIdResponse),
|
||||||
|
|
||||||
FindHttpResponsesRequest(FindHttpResponsesRequest),
|
FindHttpResponsesRequest(FindHttpResponsesRequest),
|
||||||
FindHttpResponsesResponse(FindHttpResponsesResponse),
|
FindHttpResponsesResponse(FindHttpResponsesResponse),
|
||||||
|
|
||||||
@@ -210,6 +210,7 @@ pub struct TemplateFunction {
|
|||||||
pub enum TemplateFunctionArg {
|
pub enum TemplateFunctionArg {
|
||||||
Text(TemplateFunctionTextArg),
|
Text(TemplateFunctionTextArg),
|
||||||
Select(TemplateFunctionSelectArg),
|
Select(TemplateFunctionSelectArg),
|
||||||
|
Checkbox(TemplateFunctionCheckboxArg),
|
||||||
HttpRequest(TemplateFunctionHttpRequestArg),
|
HttpRequest(TemplateFunctionHttpRequestArg),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +254,14 @@ pub struct TemplateFunctionSelectArg {
|
|||||||
pub options: Vec<TemplateFunctionSelectOption>,
|
pub options: Vec<TemplateFunctionSelectOption>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct TemplateFunctionCheckboxArg {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub base: TemplateFunctionBaseArg,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ impl Display for FnArg {
|
|||||||
pub enum Val {
|
pub enum Val {
|
||||||
Str { text: String },
|
Str { text: String },
|
||||||
Var { name: String },
|
Var { name: String },
|
||||||
|
Bool { value: bool },
|
||||||
Fn { name: String, args: Vec<FnArg> },
|
Fn { name: String, args: Vec<FnArg> },
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,7 @@ impl Display for Val {
|
|||||||
let str = match self {
|
let str = match self {
|
||||||
Val::Str { text } => format!(r#""{}""#, text.to_string().replace(r#"""#, r#"\""#)),
|
Val::Str { text } => format!(r#""{}""#, text.to_string().replace(r#"""#, r#"\""#)),
|
||||||
Val::Var { name } => name.to_string(),
|
Val::Var { name } => name.to_string(),
|
||||||
|
Val::Bool { value } => value.to_string(),
|
||||||
Val::Fn { name, args } => {
|
Val::Fn { name, args } => {
|
||||||
format!(
|
format!(
|
||||||
"{name}({})",
|
"{name}({})",
|
||||||
@@ -176,6 +178,10 @@ impl Parser {
|
|||||||
} else if let Some(v) = self.parse_ident() {
|
} else if let Some(v) = self.parse_ident() {
|
||||||
if v == "null" {
|
if v == "null" {
|
||||||
Some(Val::Null)
|
Some(Val::Null)
|
||||||
|
} else if v == "true" {
|
||||||
|
Some(Val::Bool { value: true })
|
||||||
|
} else if v == "false" {
|
||||||
|
Some(Val::Bool { value: false })
|
||||||
} else {
|
} else {
|
||||||
Some(Val::Var { name: v })
|
Some(Val::Var { name: v })
|
||||||
}
|
}
|
||||||
@@ -397,6 +403,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn var_boolean() {
|
||||||
|
let mut p = Parser::new("${[ true ]}${[ false ]}");
|
||||||
|
assert_eq!(
|
||||||
|
p.parse().tokens,
|
||||||
|
vec![
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Bool { value: true },
|
||||||
|
},
|
||||||
|
Token::Tag {
|
||||||
|
val: Val::Bool { value: false },
|
||||||
|
},
|
||||||
|
Token::Eof
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn var_multiple_names_invalid() {
|
fn var_multiple_names_invalid() {
|
||||||
let mut p = Parser::new("${[ foo bar ]}");
|
let mut p = Parser::new("${[ foo bar ]}");
|
||||||
@@ -516,7 +539,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fn_mixed_args() {
|
fn fn_mixed_args() {
|
||||||
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb="baz \"hi\"", c=qux ) ]}"#);
|
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb="baz \"hi\"", c=qux, z=true ) ]}"#);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.parse().tokens,
|
p.parse().tokens,
|
||||||
vec![
|
vec![
|
||||||
@@ -538,6 +561,10 @@ mod tests {
|
|||||||
name: "c".into(),
|
name: "c".into(),
|
||||||
value: Val::Var { name: "qux".into() }
|
value: Val::Var { name: "qux".into() }
|
||||||
},
|
},
|
||||||
|
FnArg {
|
||||||
|
name: "z".into(),
|
||||||
|
value: Val::Bool { value: true }
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ async fn render_tag<T: TemplateCallback>(
|
|||||||
Some(v) => v.to_string(),
|
Some(v) => v.to_string(),
|
||||||
None => "".into(),
|
None => "".into(),
|
||||||
},
|
},
|
||||||
|
Val::Bool { value } => value.to_string(),
|
||||||
Val::Fn { name, args } => {
|
Val::Fn { name, args } => {
|
||||||
let empty = "".to_string();
|
let empty = "".to_string();
|
||||||
let mut resolved_args: HashMap<String, String> = HashMap::new();
|
let mut resolved_args: HashMap<String, String> = HashMap::new();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
TemplateFunction,
|
TemplateFunction,
|
||||||
TemplateFunctionArg,
|
TemplateFunctionArg,
|
||||||
|
TemplateFunctionCheckboxArg,
|
||||||
TemplateFunctionHttpRequestArg,
|
TemplateFunctionHttpRequestArg,
|
||||||
TemplateFunctionSelectArg,
|
TemplateFunctionSelectArg,
|
||||||
TemplateFunctionTextArg,
|
TemplateFunctionTextArg,
|
||||||
@@ -14,6 +15,7 @@ import { useRenderTemplate } from '../hooks/useRenderTemplate';
|
|||||||
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
|
import { useTemplateTokensToString } from '../hooks/useTemplateTokensToString';
|
||||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
|
import { Checkbox } from './core/Checkbox';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import { Select } from './core/Select';
|
import { Select } from './core/Select';
|
||||||
@@ -29,7 +31,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TemplateFunctionDialog({ templateFunction, hide, initialTokens, onChange }: Props) {
|
export function TemplateFunctionDialog({ templateFunction, hide, initialTokens, onChange }: Props) {
|
||||||
const [argValues, setArgValues] = useState<Record<string, string>>(() => {
|
const [argValues, setArgValues] = useState<Record<string, string | boolean>>(() => {
|
||||||
const initial: Record<string, string> = {};
|
const initial: Record<string, string> = {};
|
||||||
const initialArgs =
|
const initialArgs =
|
||||||
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
||||||
@@ -48,20 +50,20 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
|
|||||||
return initial;
|
return initial;
|
||||||
});
|
});
|
||||||
|
|
||||||
const setArgValue = useCallback((name: string, value: string) => {
|
const setArgValue = useCallback((name: string, value: string | boolean) => {
|
||||||
setArgValues((v) => ({ ...v, [name]: value }));
|
setArgValues((v) => ({ ...v, [name]: value }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const tokens: Tokens = useMemo(() => {
|
const tokens: Tokens = useMemo(() => {
|
||||||
|
console.log('HELLO', argValues);
|
||||||
const argTokens: FnArg[] = Object.keys(argValues).map((name) => ({
|
const argTokens: FnArg[] = Object.keys(argValues).map((name) => ({
|
||||||
name,
|
name,
|
||||||
value:
|
value:
|
||||||
argValues[name] === NULL_ARG
|
argValues[name] === NULL_ARG
|
||||||
? { type: 'null' }
|
? { type: 'null' }
|
||||||
: {
|
: typeof argValues[name] === 'boolean'
|
||||||
type: 'str',
|
? { type: 'bool', value: argValues[name] }
|
||||||
text: argValues[name] ?? '',
|
: { type: 'str', text: argValues[name] ?? '' },
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -101,7 +103,7 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
|
|||||||
key={i}
|
key={i}
|
||||||
arg={a}
|
arg={a}
|
||||||
onChange={(v) => setArgValue(a.name, v)}
|
onChange={(v) => setArgValue(a.name, v)}
|
||||||
value={argValues[a.name] ?? '__ERROR__'}
|
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'text':
|
case 'text':
|
||||||
@@ -110,7 +112,16 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
|
|||||||
key={i}
|
key={i}
|
||||||
arg={a}
|
arg={a}
|
||||||
onChange={(v) => setArgValue(a.name, v)}
|
onChange={(v) => setArgValue(a.name, v)}
|
||||||
value={argValues[a.name] ?? '__ERROR__'}
|
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'checkbox':
|
||||||
|
return (
|
||||||
|
<CheckboxArg
|
||||||
|
key={i}
|
||||||
|
arg={a}
|
||||||
|
onChange={(v) => setArgValue(a.name, v)}
|
||||||
|
value={argValues[a.name] !== undefined ? argValues[a.name] === true : false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'http_request':
|
case 'http_request':
|
||||||
@@ -119,7 +130,7 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
|
|||||||
key={i}
|
key={i}
|
||||||
arg={a}
|
arg={a}
|
||||||
onChange={(v) => setArgValue(a.name, v)}
|
onChange={(v) => setArgValue(a.name, v)}
|
||||||
value={argValues[a.name] ?? '__ERROR__'}
|
value={argValues[a.name] ? String(argValues[a.name]) : '__ERROR__'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -211,3 +222,22 @@ function HttpRequestArg({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CheckboxArg({
|
||||||
|
arg,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
arg: TemplateFunctionCheckboxArg;
|
||||||
|
value: boolean;
|
||||||
|
onChange: (v: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
onChange={onChange}
|
||||||
|
checked={value}
|
||||||
|
title={arg.label ?? arg.name}
|
||||||
|
hideLabel={arg.label == null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { EnvironmentVariable } from '@yaakapp/api';
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import type { Tokens } from '../gen/Tokens';
|
import type { Tokens } from '../gen/Tokens';
|
||||||
import { useActiveEnvironmentVariables } from '../hooks/useActiveEnvironmentVariables';
|
import { useActiveEnvironmentVariables } from '../hooks/useActiveEnvironmentVariables';
|
||||||
@@ -10,7 +9,6 @@ import { Select } from './core/Select';
|
|||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
definition: EnvironmentVariable;
|
|
||||||
initialTokens: Tokens;
|
initialTokens: Tokens;
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
onChange: (rawTag: string) => void;
|
onChange: (rawTag: string) => void;
|
||||||
|
|||||||
@@ -180,7 +180,30 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onClickVariable = useCallback(
|
const onClickVariable = useCallback(
|
||||||
async (v: EnvironmentVariable, tagValue: string, startPos: number) => {
|
async (_v: EnvironmentVariable, tagValue: string, startPos: number) => {
|
||||||
|
const initialTokens = await parseTemplate(tagValue);
|
||||||
|
dialog.show({
|
||||||
|
size: 'dynamic',
|
||||||
|
id: 'template-variable',
|
||||||
|
title: 'Configure Variable',
|
||||||
|
render: ({ hide }) => (
|
||||||
|
<TemplateVariableDialog
|
||||||
|
hide={hide}
|
||||||
|
initialTokens={initialTokens}
|
||||||
|
onChange={(insert) => {
|
||||||
|
cm.current?.view.dispatch({
|
||||||
|
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dialog],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickMissingVariable = useCallback(
|
||||||
|
async (_name: string, tagValue: string, startPos: number) => {
|
||||||
const initialTokens = await parseTemplate(tagValue);
|
const initialTokens = await parseTemplate(tagValue);
|
||||||
dialog.show({
|
dialog.show({
|
||||||
size: 'dynamic',
|
size: 'dynamic',
|
||||||
@@ -188,7 +211,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
title: 'Configure Variable',
|
title: 'Configure Variable',
|
||||||
render: ({ hide }) => (
|
render: ({ hide }) => (
|
||||||
<TemplateVariableDialog
|
<TemplateVariableDialog
|
||||||
definition={v}
|
|
||||||
hide={hide}
|
hide={hide}
|
||||||
initialTokens={initialTokens}
|
initialTokens={initialTokens}
|
||||||
onChange={(insert) => {
|
onChange={(insert) => {
|
||||||
@@ -215,6 +237,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
templateFunctions,
|
templateFunctions,
|
||||||
onClickFunction,
|
onClickFunction,
|
||||||
onClickVariable,
|
onClickVariable,
|
||||||
|
onClickMissingVariable,
|
||||||
});
|
});
|
||||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||||
}, [
|
}, [
|
||||||
@@ -225,6 +248,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
templateFunctions,
|
templateFunctions,
|
||||||
onClickFunction,
|
onClickFunction,
|
||||||
onClickVariable,
|
onClickVariable,
|
||||||
|
onClickMissingVariable,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Initialize the editor when ref mounts
|
// Initialize the editor when ref mounts
|
||||||
@@ -247,6 +271,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
templateFunctions,
|
templateFunctions,
|
||||||
onClickVariable,
|
onClickVariable,
|
||||||
onClickFunction,
|
onClickFunction,
|
||||||
|
onClickMissingVariable,
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
@@ -358,7 +383,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
justifyContent="end"
|
justifyContent="end"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'absolute bottom-2 left-0 right-0',
|
'absolute bottom-2 left-0 right-0',
|
||||||
'pointer-events-none', // No pointer events so we don't block the editor
|
'pointer-events-none', // No pointer events, so we don't block the editor
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{decoratedActions}
|
{decoratedActions}
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ import {
|
|||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { tags as t } from '@lezer/highlight';
|
import { tags as t } from '@lezer/highlight';
|
||||||
import type { EnvironmentVariable } from '@yaakapp/api';
|
import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api';
|
||||||
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
|
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
|
||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
import type { TemplateFunction } from '../../../hooks/useTemplateFunctions';
|
|
||||||
import type { EditorProps } from './index';
|
import type { EditorProps } from './index';
|
||||||
import { pairs } from './pairs/extension';
|
import { pairs } from './pairs/extension';
|
||||||
import { text } from './text/extension';
|
import { text } from './text/extension';
|
||||||
@@ -84,11 +83,13 @@ export function getLanguageExtension({
|
|||||||
templateFunctions,
|
templateFunctions,
|
||||||
onClickVariable,
|
onClickVariable,
|
||||||
onClickFunction,
|
onClickFunction,
|
||||||
|
onClickMissingVariable,
|
||||||
}: {
|
}: {
|
||||||
environmentVariables: EnvironmentVariable[];
|
environmentVariables: EnvironmentVariable[];
|
||||||
templateFunctions: TemplateFunction[];
|
templateFunctions: TemplateFunction[];
|
||||||
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
||||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||||
|
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||||
} & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
|
} & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) {
|
||||||
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
|
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
|
||||||
if (justContentType === 'application/graphql') {
|
if (justContentType === 'application/graphql') {
|
||||||
@@ -106,6 +107,7 @@ export function getLanguageExtension({
|
|||||||
autocomplete,
|
autocomplete,
|
||||||
onClickFunction,
|
onClickFunction,
|
||||||
onClickVariable,
|
onClickVariable,
|
||||||
|
onClickMissingVariable,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,22 @@ import type { CompletionContext } from '@codemirror/autocomplete';
|
|||||||
const openTag = '${[ ';
|
const openTag = '${[ ';
|
||||||
const closeTag = ' ]}';
|
const closeTag = ' ]}';
|
||||||
|
|
||||||
export interface TwigCompletionOption {
|
export type TwigCompletionOptionVariable = {
|
||||||
|
type: 'variable';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TwigCompletionOptionFunction = {
|
||||||
|
args: { name: string }[];
|
||||||
|
type: 'function';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TwigCompletionOption = (TwigCompletionOptionFunction | TwigCompletionOptionVariable) & {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: 'function' | 'variable' | 'unknown';
|
onClick: (rawTag: string, startPos: number) => void;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
onClick?: (rawTag: string, startPos: number) => void;
|
invalid?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface TwigCompletionConfig {
|
export interface TwigCompletionConfig {
|
||||||
options: TwigCompletionOption[];
|
options: TwigCompletionOption[];
|
||||||
@@ -46,10 +55,9 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
|||||||
options: options
|
options: options
|
||||||
.filter((v) => v.name.trim())
|
.filter((v) => v.name.trim())
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
const innerLabel = v.type === 'function' ? `${v.name}()` : v.name;
|
const tagSyntax = openTag + v.label + closeTag;
|
||||||
const tagSyntax = openTag + innerLabel + closeTag;
|
|
||||||
return {
|
return {
|
||||||
label: innerLabel,
|
label: v.label,
|
||||||
apply: tagSyntax,
|
apply: tagSyntax,
|
||||||
type: v.type === 'variable' ? 'variable' : 'function',
|
type: v.type === 'variable' ? 'variable' : 'function',
|
||||||
matchLen: matchLen,
|
matchLen: matchLen,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { LanguageSupport } from '@codemirror/language';
|
import type { LanguageSupport } from '@codemirror/language';
|
||||||
import { LRLanguage } from '@codemirror/language';
|
import { LRLanguage } from '@codemirror/language';
|
||||||
import { parseMixed } from '@lezer/common';
|
import { parseMixed } from '@lezer/common';
|
||||||
import type { EnvironmentVariable } from '@yaakapp/api';
|
import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api';
|
||||||
import type { TemplateFunction } from '../../../../hooks/useTemplateFunctions';
|
|
||||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||||
import { genericCompletion } from '../genericCompletion';
|
import { genericCompletion } from '../genericCompletion';
|
||||||
import { textLanguageName } from '../text/extension';
|
import { textLanguageName } from '../text/extension';
|
||||||
@@ -18,6 +17,7 @@ export function twig({
|
|||||||
autocomplete,
|
autocomplete,
|
||||||
onClickFunction,
|
onClickFunction,
|
||||||
onClickVariable,
|
onClickVariable,
|
||||||
|
onClickMissingVariable,
|
||||||
}: {
|
}: {
|
||||||
base: LanguageSupport;
|
base: LanguageSupport;
|
||||||
environmentVariables: EnvironmentVariable[];
|
environmentVariables: EnvironmentVariable[];
|
||||||
@@ -25,6 +25,7 @@ export function twig({
|
|||||||
autocomplete?: GenericCompletionConfig;
|
autocomplete?: GenericCompletionConfig;
|
||||||
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
||||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||||
|
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const language = mixLanguage(base);
|
const language = mixLanguage(base);
|
||||||
|
|
||||||
@@ -35,14 +36,23 @@ export function twig({
|
|||||||
label: v.name,
|
label: v.name,
|
||||||
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
const functionOptions: TwigCompletionOption[] =
|
const functionOptions: TwigCompletionOption[] =
|
||||||
templateFunctions.map((fn) => ({
|
templateFunctions.map((fn) => {
|
||||||
name: fn.name,
|
const shortArgs =
|
||||||
type: 'function',
|
fn.args
|
||||||
value: null,
|
.slice(0, 2)
|
||||||
label: fn.name + '(' + fn.args.length + ')',
|
.map((a) => a.name)
|
||||||
onClick: (rawTag: string, startPos: number) => onClickFunction(fn, rawTag, startPos),
|
.join(', ') + (fn.args.length > 2 ? ', …' : '');
|
||||||
})) ?? [];
|
return {
|
||||||
|
name: fn.name,
|
||||||
|
type: 'function',
|
||||||
|
args: fn.args.map((a) => ({ name: a.name })),
|
||||||
|
value: null,
|
||||||
|
label: `${fn.name}(${shortArgs})`,
|
||||||
|
onClick: (rawTag: string, startPos: number) => onClickFunction(fn, rawTag, startPos),
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
|
||||||
const options = [...variableOptions, ...functionOptions];
|
const options = [...variableOptions, ...functionOptions];
|
||||||
|
|
||||||
@@ -51,7 +61,7 @@ export function twig({
|
|||||||
return [
|
return [
|
||||||
language,
|
language,
|
||||||
base.support,
|
base.support,
|
||||||
templateTags(options),
|
templateTags(options, onClickMissingVariable),
|
||||||
language.data.of({ autocomplete: completions }),
|
language.data.of({ autocomplete: completions }),
|
||||||
base.language.data.of({ autocomplete: completions }),
|
base.language.data.of({ autocomplete: completions }),
|
||||||
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||||
import { truncate } from '../../../../lib/truncate';
|
|
||||||
import { BetterMatchDecorator } from '../BetterMatchDecorator';
|
import { BetterMatchDecorator } from '../BetterMatchDecorator';
|
||||||
import type { TwigCompletionOption } from './completion';
|
import type { TwigCompletionOption } from './completion';
|
||||||
|
|
||||||
const TAG_TRUNCATE_LEN = 30;
|
|
||||||
|
|
||||||
class TemplateTagWidget extends WidgetType {
|
class TemplateTagWidget extends WidgetType {
|
||||||
readonly #clickListenerCallback: () => void;
|
readonly #clickListenerCallback: () => void;
|
||||||
|
|
||||||
@@ -32,17 +29,15 @@ class TemplateTagWidget extends WidgetType {
|
|||||||
toDOM() {
|
toDOM() {
|
||||||
const elt = document.createElement('span');
|
const elt = document.createElement('span');
|
||||||
elt.className = `x-theme-templateTag template-tag ${
|
elt.className = `x-theme-templateTag template-tag ${
|
||||||
this.option.type === 'unknown'
|
this.option.invalid
|
||||||
? 'x-theme-templateTag--danger'
|
? 'x-theme-templateTag--danger'
|
||||||
: this.option.type === 'variable'
|
: this.option.type === 'variable'
|
||||||
? 'x-theme-templateTag--primary'
|
? 'x-theme-templateTag--primary'
|
||||||
: 'x-theme-templateTag--info'
|
: 'x-theme-templateTag--info'
|
||||||
}`;
|
}`;
|
||||||
elt.title = this.option.type === 'unknown' ? '__NOT_FOUND__' : this.option.value ?? '';
|
elt.title = this.option.invalid ? 'Not Found' : this.option.value ?? '';
|
||||||
elt.textContent = truncate(
|
elt.setAttribute('data-tag-type', this.option.type);
|
||||||
this.rawTag.replace('${[', '').replace(']}', '').trim(),
|
elt.textContent = this.option.label;
|
||||||
TAG_TRUNCATE_LEN,
|
|
||||||
);
|
|
||||||
elt.addEventListener('click', this.#clickListenerCallback);
|
elt.addEventListener('click', this.#clickListenerCallback);
|
||||||
return elt;
|
return elt;
|
||||||
}
|
}
|
||||||
@@ -57,7 +52,10 @@ class TemplateTagWidget extends WidgetType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function templateTags(options: TwigCompletionOption[]) {
|
export function templateTags(
|
||||||
|
options: TwigCompletionOption[],
|
||||||
|
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
|
||||||
|
) {
|
||||||
const templateTagMatcher = new BetterMatchDecorator({
|
const templateTagMatcher = new BetterMatchDecorator({
|
||||||
regexp: /\$\{\[\s*(.+)(?!]})\s*]}/g,
|
regexp: /\$\{\[\s*(.+)(?!]})\s*]}/g,
|
||||||
decoration(match, view, matchStartPos) {
|
decoration(match, view, matchStartPos) {
|
||||||
@@ -82,7 +80,14 @@ export function templateTags(options: TwigCompletionOption[]) {
|
|||||||
|
|
||||||
let option = options.find((v) => v.name === name);
|
let option = options.find((v) => v.name === name);
|
||||||
if (option == null) {
|
if (option == null) {
|
||||||
option = { type: 'unknown', name: innerTagMatch, value: null, label: innerTagMatch };
|
option = {
|
||||||
|
invalid: true,
|
||||||
|
type: 'variable',
|
||||||
|
name: innerTagMatch,
|
||||||
|
value: null,
|
||||||
|
label: innerTagMatch,
|
||||||
|
onClick: () => onClickMissingVariable(name, match[0], matchStartPos),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return Decoration.replace({
|
return Decoration.replace({
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export function Select<T extends string>({
|
|||||||
const osInfo = useOsInfo();
|
const osInfo = useOsInfo();
|
||||||
const [focused, setFocused] = useState<boolean>(false);
|
const [focused, setFocused] = useState<boolean>(false);
|
||||||
const id = `input-${name}`;
|
const id = `input-${name}`;
|
||||||
|
const isInvalidSelection = options.find((o) => 'value' in o && o.value === value) == null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -67,6 +69,7 @@ export function Select<T extends string>({
|
|||||||
'pl-2',
|
'pl-2',
|
||||||
'border',
|
'border',
|
||||||
focused ? 'border-border-focus' : 'border-border',
|
focused ? 'border-border-focus' : 'border-border',
|
||||||
|
isInvalidSelection && 'border-danger',
|
||||||
size === 'xs' && 'h-xs',
|
size === 'xs' && 'h-xs',
|
||||||
size === 'sm' && 'h-sm',
|
size === 'sm' && 'h-sm',
|
||||||
size === 'md' && 'h-md',
|
size === 'md' && 'h-md',
|
||||||
@@ -81,6 +84,7 @@ export function Select<T extends string>({
|
|||||||
onBlur={() => setFocused(false)}
|
onBlur={() => setFocused(false)}
|
||||||
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
||||||
>
|
>
|
||||||
|
{isInvalidSelection && <option value={'__NONE__'}>-- Select an Option --</option>}
|
||||||
{options.map((o) => {
|
{options.map((o) => {
|
||||||
if (o.type === 'separator') return null;
|
if (o.type === 'separator') return null;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
import type { FnArg } from "./FnArg";
|
import type { FnArg } from "./FnArg";
|
||||||
|
|
||||||
export type Val = { "type": "str", text: string, } | { "type": "var", name: string, } | { "type": "fn", name: string, args: Array<FnArg>, } | { "type": "null" };
|
export type Val = { "type": "str", text: string, } | { "type": "var", name: string, } | { "type": "bool", value: boolean, } | { "type": "fn", name: string, args: Array<FnArg>, } | { "type": "null" };
|
||||||
|
|||||||
Reference in New Issue
Block a user