Placeholder error and fix env nav

This commit is contained in:
Gregory Schier
2023-10-28 19:08:31 -07:00
parent 68fbb1a3ff
commit 73190958fa
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 { Dropdown } from './core/Dropdown';
import classNames from 'classnames';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
export function RecentRequestsDropdown() {
const dropdownRef = useRef<DropdownRef>(null);
const activeRequest = useActiveRequest();
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const requests = useRequests();
const routes = useAppRoutes();
const allRecentRequestIds = useRecentRequests();
@@ -65,6 +67,7 @@ export function RecentRequestsDropdown() {
onSelect: () => {
routes.navigate('request', {
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
workspaceId: activeWorkspaceId,
});
},
@@ -77,7 +80,7 @@ export function RecentRequestsDropdown() {
}
return recentRequestItems.slice(0, 20);
}, [activeWorkspaceId, recentRequestIds, requests, routes]);
}, [activeWorkspaceId, activeEnvironmentId, recentRequestIds, requests, routes]);
return (
<Dropdown ref={dropdownRef} items={items}>

View File

@@ -115,7 +115,11 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
const request = requests[selectedIndex ?? -1];
if (!request || request.id === activeRequestId) return;
e.preventDefault();
routes.navigate('request', { requestId: request.id, workspaceId: request.workspaceId });
routes.navigate('request', {
requestId: request.id,
workspaceId: request.workspaceId,
environmentId: activeEnvironmentId ?? undefined,
});
});
useKey(
@@ -161,12 +165,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
'h-full relative grid grid-rows-[auto_minmax(0,1fr)_auto]',
)}
>
<HStack
className="mt-1 pt-1 mx-2"
justifyContent="between"
alignItems="center"
space={1}
>
<HStack className="mt-1 pt-1 mx-2" justifyContent="between" alignItems="center" space={1}>
<WorkspaceActionsDropdown
forDropdown={false}
className="text-left mb-0"

View File

@@ -16,7 +16,6 @@ import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { HStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown'>;
@@ -27,7 +26,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = activeWorkspace?.id ?? null;
const environmentId = useActiveEnvironmentId();
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
@@ -58,10 +56,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
color="gray"
onClick={() => {
hide();
routes.navigate('workspace', {
workspaceId: w.id,
environmentId: environmentId ?? undefined,
});
routes.navigate('workspace', { workspaceId: w.id });
}}
>
This Window
@@ -74,10 +69,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
onClick={async () => {
hide();
await invoke('new_window', {
url: routes.paths.workspace({
workspaceId: w.id,
environmentId: environmentId ?? undefined,
}),
url: routes.paths.workspace({ workspaceId: w.id }),
});
}}
>
@@ -152,7 +144,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
createWorkspace,
deleteWorkspace.mutate,
dialog,
environmentId,
prompt,
routes,
updateWorkspace,
@@ -165,7 +156,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
size="sm"
className={classNames(className, 'text-gray-800 !px-2 truncate')}
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}
>
{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;
/* 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 */
@apply hover:z-10 relative;
-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),
drawSelection(),
indentOnInput(),
closeBrackets(),
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 closeTag = ' ]}';
interface TwigCompletionOption {
export interface TwigCompletionOption {
name: string;
}

View File

@@ -3,7 +3,7 @@ import { LRLanguage } from '@codemirror/language';
import { parseMixed } from '@lezer/common';
import type { GenericCompletionConfig } from '../genericCompletion';
import { genericCompletion } from '../genericCompletion';
import { placeholders } from '../placeholder';
import { placeholders } from './placeholder';
import { textLanguageName } from '../text/extension';
import { twigCompletion } from './completion';
import { parser as twigParser } from './twig';
@@ -29,7 +29,7 @@ export function twig(
completion,
completionBase,
base.support,
placeholders,
placeholders(variables),
...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 { useAppRoutes } from './useAppRoutes';
import { requestsQueryKey, useRequests } from './useRequests';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
const workspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const routes = useAppRoutes();
const requests = useRequests();
const queryClient = useQueryClient();
@@ -26,7 +28,11 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean })
(requests) => [...(requests ?? []), request],
);
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 { useAppRoutes } from './useAppRoutes';
import { requestsQueryKey } from './useRequests';
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
export function useDuplicateRequest({
id,
@@ -12,7 +13,8 @@ export function useDuplicateRequest({
id: string | null;
navigateAfter: boolean;
}) {
const workspaceId = useActiveWorkspaceId();
const activeWorkspaceId = useActiveWorkspaceId();
const activeEnvironmentId = useActiveEnvironmentId();
const routes = useAppRoutes();
const queryClient = useQueryClient();
return useMutation<HttpRequest, string>({
@@ -25,8 +27,12 @@ export function useDuplicateRequest({
requestsQueryKey({ workspaceId: request.workspaceId }),
(requests) => [...(requests ?? []), request],
);
if (navigateAfter && workspaceId !== null) {
routes.navigate('request', { workspaceId, requestId: request.id });
if (navigateAfter && activeWorkspaceId !== null) {
routes.navigate('request', {
workspaceId: activeWorkspaceId,
requestId: request.id,
environmentId: activeEnvironmentId ?? undefined,
});
}
},
});