mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:28:35 +02:00
Placeholder error and fix env nav
This commit is contained in:
@@ -10,11 +10,13 @@ import { CountBadge } from './core/CountBadge';
|
|||||||
import type { DropdownItem, DropdownRef } from './core/Dropdown';
|
import type { DropdownItem, DropdownRef } from './core/Dropdown';
|
||||||
import { Dropdown } from './core/Dropdown';
|
import { Dropdown } from './core/Dropdown';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||||
|
|
||||||
export function RecentRequestsDropdown() {
|
export function RecentRequestsDropdown() {
|
||||||
const dropdownRef = useRef<DropdownRef>(null);
|
const dropdownRef = useRef<DropdownRef>(null);
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const activeWorkspaceId = useActiveWorkspaceId();
|
const activeWorkspaceId = useActiveWorkspaceId();
|
||||||
|
const activeEnvironmentId = useActiveEnvironmentId();
|
||||||
const requests = useRequests();
|
const requests = useRequests();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
const allRecentRequestIds = useRecentRequests();
|
const allRecentRequestIds = useRecentRequests();
|
||||||
@@ -65,6 +67,7 @@ export function RecentRequestsDropdown() {
|
|||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
routes.navigate('request', {
|
routes.navigate('request', {
|
||||||
requestId: request.id,
|
requestId: request.id,
|
||||||
|
environmentId: activeEnvironmentId ?? undefined,
|
||||||
workspaceId: activeWorkspaceId,
|
workspaceId: activeWorkspaceId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -77,7 +80,7 @@ export function RecentRequestsDropdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return recentRequestItems.slice(0, 20);
|
return recentRequestItems.slice(0, 20);
|
||||||
}, [activeWorkspaceId, recentRequestIds, requests, routes]);
|
}, [activeWorkspaceId, activeEnvironmentId, recentRequestIds, requests, routes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown ref={dropdownRef} items={items}>
|
<Dropdown ref={dropdownRef} items={items}>
|
||||||
|
|||||||
@@ -115,7 +115,11 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
const request = requests[selectedIndex ?? -1];
|
const request = requests[selectedIndex ?? -1];
|
||||||
if (!request || request.id === activeRequestId) return;
|
if (!request || request.id === activeRequestId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
routes.navigate('request', { requestId: request.id, workspaceId: request.workspaceId });
|
routes.navigate('request', {
|
||||||
|
requestId: request.id,
|
||||||
|
workspaceId: request.workspaceId,
|
||||||
|
environmentId: activeEnvironmentId ?? undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
@@ -161,12 +165,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
'h-full relative grid grid-rows-[auto_minmax(0,1fr)_auto]',
|
'h-full relative grid grid-rows-[auto_minmax(0,1fr)_auto]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<HStack
|
<HStack className="mt-1 pt-1 mx-2" justifyContent="between" alignItems="center" space={1}>
|
||||||
className="mt-1 pt-1 mx-2"
|
|
||||||
justifyContent="between"
|
|
||||||
alignItems="center"
|
|
||||||
space={1}
|
|
||||||
>
|
|
||||||
<WorkspaceActionsDropdown
|
<WorkspaceActionsDropdown
|
||||||
forDropdown={false}
|
forDropdown={false}
|
||||||
className="text-left mb-0"
|
className="text-left mb-0"
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { Icon } from './core/Icon';
|
|||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import { HStack } from './core/Stacks';
|
import { HStack } from './core/Stacks';
|
||||||
import { useDialog } from './DialogContext';
|
import { useDialog } from './DialogContext';
|
||||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
|
||||||
|
|
||||||
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown'>;
|
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown'>;
|
||||||
|
|
||||||
@@ -27,7 +26,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||||
const environmentId = useActiveEnvironmentId();
|
|
||||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||||
@@ -58,10 +56,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
color="gray"
|
color="gray"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
hide();
|
hide();
|
||||||
routes.navigate('workspace', {
|
routes.navigate('workspace', { workspaceId: w.id });
|
||||||
workspaceId: w.id,
|
|
||||||
environmentId: environmentId ?? undefined,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
This Window
|
This Window
|
||||||
@@ -74,10 +69,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
hide();
|
hide();
|
||||||
await invoke('new_window', {
|
await invoke('new_window', {
|
||||||
url: routes.paths.workspace({
|
url: routes.paths.workspace({ workspaceId: w.id }),
|
||||||
workspaceId: w.id,
|
|
||||||
environmentId: environmentId ?? undefined,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -152,7 +144,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
createWorkspace,
|
createWorkspace,
|
||||||
deleteWorkspace.mutate,
|
deleteWorkspace.mutate,
|
||||||
dialog,
|
dialog,
|
||||||
environmentId,
|
|
||||||
prompt,
|
prompt,
|
||||||
routes,
|
routes,
|
||||||
updateWorkspace,
|
updateWorkspace,
|
||||||
@@ -165,7 +156,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className={classNames(className, 'text-gray-800 !px-2 truncate')}
|
className={classNames(className, 'text-gray-800 !px-2 truncate')}
|
||||||
forDropdown
|
forDropdown
|
||||||
leftSlot={<img src="https://yaak.app/logo.svg" alt="Workspace logo" className="w-4 h-4 mr-1" />}
|
leftSlot={
|
||||||
|
<img src="https://yaak.app/logo.svg" alt="Workspace logo" className="w-4 h-4 mr-1" />
|
||||||
|
}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
{activeWorkspace?.name}
|
{activeWorkspace?.name}
|
||||||
|
|||||||
@@ -50,12 +50,16 @@
|
|||||||
@apply text-xs text-gray-800 dark:text-gray-900 px-1 rounded cursor-default dark:shadow;
|
@apply text-xs text-gray-800 dark:text-gray-900 px-1 rounded cursor-default dark:shadow;
|
||||||
|
|
||||||
/* NOTE: Background and border are translucent so we can see text selection through it */
|
/* NOTE: Background and border are translucent so we can see text selection through it */
|
||||||
@apply bg-gray-300/40 border border-gray-300 border-opacity-40 hover:border-opacity-80;
|
@apply bg-gray-300/40 border border-gray-300 border-opacity-40;
|
||||||
|
|
||||||
/* Bring above on hover */
|
/* Bring above on hover */
|
||||||
@apply hover:z-10 relative;
|
@apply hover:z-10 relative;
|
||||||
|
|
||||||
-webkit-text-security: none;
|
-webkit-text-security: none;
|
||||||
|
|
||||||
|
&.placeholder-widget-error {
|
||||||
|
@apply bg-red-300/40 border-red-300 border-opacity-40;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export const multiLineExtensions = [
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
|
drawSelection(),
|
||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
rectangularSelection(),
|
rectangularSelection(),
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
|
||||||
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType } from '@codemirror/view';
|
|
||||||
|
|
||||||
class PlaceholderWidget extends WidgetType {
|
|
||||||
constructor(readonly name: string) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
eq(other: PlaceholderWidget) {
|
|
||||||
return this.name == other.name;
|
|
||||||
}
|
|
||||||
toDOM() {
|
|
||||||
const elt = document.createElement('span');
|
|
||||||
elt.className = 'placeholder-widget';
|
|
||||||
elt.textContent = this.name;
|
|
||||||
return elt;
|
|
||||||
}
|
|
||||||
ignoreEvent() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
|
||||||
*/
|
|
||||||
class BetterMatchDecorator extends MatchDecorator {
|
|
||||||
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
|
|
||||||
if (!update.startState.selection.eq(update.state.selection)) {
|
|
||||||
return super.createDeco(update.view);
|
|
||||||
} else {
|
|
||||||
return super.updateDeco(update, deco);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const placeholderMatcher = new BetterMatchDecorator({
|
|
||||||
regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g,
|
|
||||||
decoration(match, view, matchStartPos) {
|
|
||||||
const matchEndPos = matchStartPos + match[0].length - 1;
|
|
||||||
|
|
||||||
// Don't decorate if the cursor is inside the match
|
|
||||||
for (const r of view.state.selection.ranges) {
|
|
||||||
if (r.from > matchStartPos && r.to <= matchEndPos) {
|
|
||||||
return Decoration.replace({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupMatch = match[1];
|
|
||||||
if (groupMatch == null) {
|
|
||||||
// Should never happen, but make TS happy
|
|
||||||
console.warn('Group match was empty', match);
|
|
||||||
return Decoration.replace({});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Decoration.replace({
|
|
||||||
inclusive: true,
|
|
||||||
widget: new PlaceholderWidget(groupMatch),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const placeholders = ViewPlugin.fromClass(
|
|
||||||
class {
|
|
||||||
placeholders: DecorationSet;
|
|
||||||
constructor(view: EditorView) {
|
|
||||||
this.placeholders = placeholderMatcher.createDeco(view);
|
|
||||||
}
|
|
||||||
update(update: ViewUpdate) {
|
|
||||||
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
decorations: (instance) => instance.placeholders,
|
|
||||||
provide: (plugin) =>
|
|
||||||
EditorView.atomicRanges.of((view) => {
|
|
||||||
return view.plugin(plugin)?.placeholders || Decoration.none;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -4,7 +4,7 @@ import { w } from '@tauri-apps/api/clipboard-79413165';
|
|||||||
const openTag = '${[ ';
|
const openTag = '${[ ';
|
||||||
const closeTag = ' ]}';
|
const closeTag = ' ]}';
|
||||||
|
|
||||||
interface TwigCompletionOption {
|
export interface TwigCompletionOption {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { LRLanguage } from '@codemirror/language';
|
|||||||
import { parseMixed } from '@lezer/common';
|
import { parseMixed } from '@lezer/common';
|
||||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||||
import { genericCompletion } from '../genericCompletion';
|
import { genericCompletion } from '../genericCompletion';
|
||||||
import { placeholders } from '../placeholder';
|
import { placeholders } from './placeholder';
|
||||||
import { textLanguageName } from '../text/extension';
|
import { textLanguageName } from '../text/extension';
|
||||||
import { twigCompletion } from './completion';
|
import { twigCompletion } from './completion';
|
||||||
import { parser as twigParser } from './twig';
|
import { parser as twigParser } from './twig';
|
||||||
@@ -29,7 +29,7 @@ export function twig(
|
|||||||
completion,
|
completion,
|
||||||
completionBase,
|
completionBase,
|
||||||
base.support,
|
base.support,
|
||||||
placeholders,
|
placeholders(variables),
|
||||||
...additionalCompletion,
|
...additionalCompletion,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
86
src-web/components/core/Editor/twig/placeholder.ts
Normal file
86
src-web/components/core/Editor/twig/placeholder.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||||
|
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||||
|
|
||||||
|
class PlaceholderWidget extends WidgetType {
|
||||||
|
constructor(
|
||||||
|
readonly name: string,
|
||||||
|
readonly isExistingVariable: boolean,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
eq(other: PlaceholderWidget) {
|
||||||
|
return this.name == other.name;
|
||||||
|
}
|
||||||
|
toDOM() {
|
||||||
|
const elt = document.createElement('span');
|
||||||
|
elt.className = `placeholder-widget ${!this.isExistingVariable ? 'placeholder-widget-error' : ''}`;
|
||||||
|
elt.textContent = this.name;
|
||||||
|
return elt;
|
||||||
|
}
|
||||||
|
ignoreEvent() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
||||||
|
*/
|
||||||
|
class BetterMatchDecorator extends MatchDecorator {
|
||||||
|
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
|
||||||
|
if (!update.startState.selection.eq(update.state.selection)) {
|
||||||
|
return super.createDeco(update.view);
|
||||||
|
} else {
|
||||||
|
return super.updateDeco(update, deco);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const placeholders = function (variables: { name: string }[]) {
|
||||||
|
const placeholderMatcher = new BetterMatchDecorator({
|
||||||
|
regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g,
|
||||||
|
decoration(match, view, matchStartPos) {
|
||||||
|
const matchEndPos = matchStartPos + match[0].length - 1;
|
||||||
|
|
||||||
|
// Don't decorate if the cursor is inside the match
|
||||||
|
for (const r of view.state.selection.ranges) {
|
||||||
|
if (r.from > matchStartPos && r.to <= matchEndPos) {
|
||||||
|
return Decoration.replace({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupMatch = match[1];
|
||||||
|
if (groupMatch == null) {
|
||||||
|
// Should never happen, but make TS happy
|
||||||
|
console.warn('Group match was empty', match);
|
||||||
|
return Decoration.replace({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decoration.replace({
|
||||||
|
inclusive: true,
|
||||||
|
widget: new PlaceholderWidget(
|
||||||
|
groupMatch,
|
||||||
|
variables.some((v) => v.name === groupMatch),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
placeholders: DecorationSet;
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.placeholders = placeholderMatcher.createDeco(view);
|
||||||
|
}
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (instance) => instance.placeholders,
|
||||||
|
provide: (plugin) =>
|
||||||
|
EditorView.atomicRanges.of((view) => {
|
||||||
|
return view.plugin(plugin)?.placeholders || Decoration.none;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,9 +4,11 @@ import type { HttpRequest } from '../lib/models';
|
|||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useAppRoutes } from './useAppRoutes';
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
import { requestsQueryKey, useRequests } from './useRequests';
|
import { requestsQueryKey, useRequests } from './useRequests';
|
||||||
|
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||||
|
|
||||||
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const workspaceId = useActiveWorkspaceId();
|
||||||
|
const activeEnvironmentId = useActiveEnvironmentId();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
const requests = useRequests();
|
const requests = useRequests();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -26,7 +28,11 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean })
|
|||||||
(requests) => [...(requests ?? []), request],
|
(requests) => [...(requests ?? []), request],
|
||||||
);
|
);
|
||||||
if (navigateAfter) {
|
if (navigateAfter) {
|
||||||
routes.navigate('request', { workspaceId: request.workspaceId, requestId: request.id });
|
routes.navigate('request', {
|
||||||
|
workspaceId: request.workspaceId,
|
||||||
|
requestId: request.id,
|
||||||
|
environmentId: activeEnvironmentId ?? undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { HttpRequest } from '../lib/models';
|
|||||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||||
import { useAppRoutes } from './useAppRoutes';
|
import { useAppRoutes } from './useAppRoutes';
|
||||||
import { requestsQueryKey } from './useRequests';
|
import { requestsQueryKey } from './useRequests';
|
||||||
|
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||||
|
|
||||||
export function useDuplicateRequest({
|
export function useDuplicateRequest({
|
||||||
id,
|
id,
|
||||||
@@ -12,7 +13,8 @@ export function useDuplicateRequest({
|
|||||||
id: string | null;
|
id: string | null;
|
||||||
navigateAfter: boolean;
|
navigateAfter: boolean;
|
||||||
}) {
|
}) {
|
||||||
const workspaceId = useActiveWorkspaceId();
|
const activeWorkspaceId = useActiveWorkspaceId();
|
||||||
|
const activeEnvironmentId = useActiveEnvironmentId();
|
||||||
const routes = useAppRoutes();
|
const routes = useAppRoutes();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<HttpRequest, string>({
|
return useMutation<HttpRequest, string>({
|
||||||
@@ -25,8 +27,12 @@ export function useDuplicateRequest({
|
|||||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||||
(requests) => [...(requests ?? []), request],
|
(requests) => [...(requests ?? []), request],
|
||||||
);
|
);
|
||||||
if (navigateAfter && workspaceId !== null) {
|
if (navigateAfter && activeWorkspaceId !== null) {
|
||||||
routes.navigate('request', { workspaceId, requestId: request.id });
|
routes.navigate('request', {
|
||||||
|
workspaceId: activeWorkspaceId,
|
||||||
|
requestId: request.id,
|
||||||
|
environmentId: activeEnvironmentId ?? undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user