Placeholder error and fix env nav

This commit is contained in:
Gregory Schier
2023-10-28 19:08:31 -07:00
parent 23e77dfec1
commit 613081728d
11 changed files with 126 additions and 106 deletions

View File

@@ -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}>

View File

@@ -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"

View File

@@ -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}

View File

@@ -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;
}
} }
} }

View File

@@ -148,6 +148,7 @@ export const multiLineExtensions = [
}, },
}), }),
EditorState.allowMultipleSelections.of(true), EditorState.allowMultipleSelections.of(true),
drawSelection(),
indentOnInput(), indentOnInput(),
closeBrackets(), closeBrackets(),
rectangularSelection(), rectangularSelection(),

View File

@@ -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;
}),
},
);

View File

@@ -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;
} }

View File

@@ -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,
]; ];
} }

View 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;
}),
},
);
};

View File

@@ -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,
});
} }
}, },
}); });

View File

@@ -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,
});
} }
}, },
}); });