feat: improve reference match, auto closing tag and reference select

This commit is contained in:
Salman
2024-03-15 02:22:09 +05:30
parent 1fc9e60254
commit 1bcc742466

View File

@@ -1,5 +1,5 @@
/* eslint-disable react/no-danger */
import React, { forwardRef, TextareaHTMLAttributes, useState } from "react";
import React, { forwardRef, TextareaHTMLAttributes, useRef, useState } from "react";
import { faChevronRight, faFolder, faKey, faRecycle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
@@ -32,7 +32,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
skipNext = true;
return (
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
&#36;&#123;<span className="ph-no-capture text-yello-200/80">{el.slice(2, -1)}</span>
&#36;&#123;<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
&#125;
</span>
);
@@ -69,21 +69,33 @@ const commonClassName = "font-mono text-sm caret-white border-none outline-none
export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
(
{ value, isVisible, containerClassName, onBlur, isDisabled, isReadOnly, onFocus, ...props },
{
value: propValue,
isVisible,
containerClassName,
onBlur,
isDisabled,
isReadOnly,
onFocus,
secretPath,
environment,
onChange,
...props
},
ref
) => {
const [isSecretFocused, setIsSecretFocused] = useToggle();
const [showReferencePopup, setShowReferencePopup] = useState<boolean>(false);
const [value, setValue] = useState<string>(propValue || "");
const { currentWorkspace } = useWorkspace();
const [listVariables, setListVariables] = useState<VariableType[]>([]);
const [lastSelectionIndex, setLastSelectionIndex] = useState<number>(0);
const childRef = useRef<HTMLTextAreaElement>(null);
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const { environment, secretPath } = props;
async function extractReference(refValue: string, refIndex: number) {
console.log({ refIndex });
async function extractReference(refValue: string) {
const isNested = refValue.includes(".");
const currentListVariable: VariableType[] = [];
@@ -101,7 +113,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
return;
}
console.log({ currentEnvironment, currentSecretPath });
// Move to react query
const [encryptSecrets, folders] = await Promise.all([
fetchProjectEncryptedSecrets({
workspaceId,
@@ -122,63 +134,148 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
currentListVariable.unshift({ name: secret.key, type: "secret" });
});
// get list of secrets, folder name and envs
// On env select get list of secrets
// on env select show list of secrets and folder
// on env or folder select replace the text and update the caret?
// fetch secrets based on current base environment and the path
setListVariables(currentListVariable);
setShowReferencePopup(true);
}
function handleVariablePopup(element: HTMLTextAreaElement) {
const { selectionStart, selectionEnd, value: elValue } = element;
if (selectionStart !== selectionEnd || selectionStart === 0) {
setShowReferencePopup(false);
return;
}
let match = null;
for (
let matches = REGEX_REFERENCE.exec(elValue);
matches !== null;
matches = REGEX_REFERENCE.exec(elValue)
) {
if (matches.index <= selectionStart && REGEX_REFERENCE.lastIndex >= selectionStart) {
match = matches?.[2];
extractReference(match, matches.index);
function findMatch(str: string, start: number) {
const matches = [...str.matchAll(REGEX_REFERENCE)];
for (let i = 0; i < matches.length; i += 1) {
const match = matches[i];
if (
match &&
typeof match.index !== "undefined" &&
match.index <= start &&
start < match.index + match[0].length
) {
return match;
}
}
setShowReferencePopup(Boolean(match));
return null;
}
function handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
// On Key up or down if the popup is open ignore it
if ((showReferencePopup && event.key === "ArrowUp") || event.key === "ArrowDown") {
event.preventDefault();
// todo: point up or down in the variable popup
// return;
function setCaretPos(caretPos: number) {
if (childRef?.current) {
childRef.current.focus();
setTimeout(() => {
if (!childRef?.current) return;
childRef.current.selectionStart = caretPos;
childRef.current.selectionEnd = caretPos;
}, 200);
}
}
function referencePopup(text: string, pos: number) {
const match = findMatch(text, pos);
if (match && typeof match.index !== "undefined") {
setLastSelectionIndex(pos);
extractReference(match?.[2]);
}
setShowReferencePopup(!!match);
}
function handleReferencePopup(element: HTMLTextAreaElement) {
const { selectionStart, selectionEnd, value: text } = element;
if (selectionStart !== selectionEnd || selectionStart === 0) {
return;
}
referencePopup(text, selectionStart);
}
function handleKeyUp(event: React.KeyboardEvent<HTMLTextAreaElement>) {
if (event.key === "Escape") {
setShowReferencePopup(false);
return;
}
if (event.key === "{") {
// auto close the tag
const currCaretPos = event.currentTarget.selectionEnd;
const isPrevDollar = value[currCaretPos - 2] === "$";
if (!isPrevDollar) return;
const newValue = `${value.slice(0, currCaretPos)}}${value.slice(currCaretPos)}`;
setValue(newValue);
if (event.currentTarget) {
setCaretPos(currCaretPos);
setTimeout(() => {
// on next tick
referencePopup(newValue, currCaretPos);
}, 200);
return;
}
}
// On Key up or down if the popup is open ignore it
if ((showReferencePopup && event.key === "ArrowUp") || event.key === "ArrowDown") {
event.preventDefault();
// todo: point up or down in the variable popup
// return;
}
handleVariablePopup(event.currentTarget);
handleReferencePopup(event.currentTarget);
}
function handleMouseClick(event: React.MouseEvent<HTMLTextAreaElement, MouseEvent>) {
handleVariablePopup(event.currentTarget);
handleReferencePopup(event.currentTarget);
}
async function handleReferenceSelect({
name,
type,
slug
}: {
name: string;
type: "folder" | "secret" | "environment";
slug?: string;
}) {
setShowReferencePopup(false);
// forward ref for parent component
if (typeof ref === "function") {
ref(childRef.current);
} else if (ref && "current" in ref) {
const refCopy = ref;
refCopy.current = childRef.current;
}
let newValue = value || "";
const match = findMatch(newValue, lastSelectionIndex);
const referenceStartIndex = match?.index || 0;
const referenceEndIndex = referenceStartIndex + (match?.[0]?.length || 0);
const [start, oldReference, end] = [
value.slice(0, referenceStartIndex),
value.slice(referenceStartIndex, referenceEndIndex),
value.slice(referenceEndIndex)
];
const oldReferenceStr = oldReference.slice(2, oldReference.length - 1); // remove template
let replaceReference = "";
let offset = 3;
switch (type) {
case "folder":
replaceReference = `${oldReferenceStr}${name}.`;
offset -= 1;
break;
case "secret":
replaceReference = `${oldReferenceStr}${name}`;
break;
case "environment":
replaceReference = `${slug}.`;
offset -= 1;
break;
default:
}
newValue = `${start}$\{${replaceReference}}${end}`;
setValue(newValue);
setCaretPos(start.length + replaceReference.length + offset);
if (type !== "secret") extractReference(replaceReference);
}
function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
setValue(event.target.value);
return onChange;
}
return (
@@ -199,32 +296,38 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
<textarea
style={{ whiteSpace: "break-spaces" }}
aria-label="secret value"
ref={ref}
className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`}
ref={childRef}
className={twMerge(
"absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0",
commonClassName
)}
onFocus={() => setIsSecretFocused.on()}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onClick={handleMouseClick}
onChange={handleChange}
disabled={isDisabled}
spellCheck={false}
onBlur={(evt) => {
onBlur?.(evt);
if (!showReferencePopup) setIsSecretFocused.off();
}}
value={value || ""}
{...props}
value={value}
readOnly={isReadOnly}
/>
</div>
</div>
{/* TODO(radix): Move to radix select component and scroll element */}
{showReferencePopup && isSecretFocused && (
<div className="absolute z-10 mt-2 w-60 rounded-md border border-mineshaft-600 bg-mineshaft-700 text-sm text-bunker-200">
<div className="fixed z-[100] mt-2 w-60 rounded-md border border-mineshaft-600 bg-mineshaft-700 text-sm text-bunker-200">
<div className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md py-4 text-white">
{listVariables.map((e, i) => {
return (
<div
className="flex items-center justify-between border-b border-mineshaft-600 px-2 py-1 last:border-b-0"
<button
className="flex items-center justify-between border-b border-mineshaft-600 px-2 py-1 text-left last:border-b-0"
key={`key-${i + 1}`}
onClick={() => handleReferenceSelect({ name: e.name, type: e.type })}
type="button"
>
{e.type === "folder" && (
<>
@@ -245,30 +348,36 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faKey} />
</div>
<div className="w-48 truncate">{e.name}</div>
<div className="w-48 truncate text-left">{e.name}</div>
</div>
)}
</div>
</button>
);
})}
<div className="flex w-full justify-center gap-2">All Secrets</div>
<div className="flex w-full justify-center gap-2 pt-1 text-xs text-bunker-300">
All Secrets
</div>
{currentWorkspace?.environments.map((env, i) => (
<div
<button
className="flex items-center justify-between border-b border-mineshaft-600 px-2 py-1 last:border-b-0"
key={`key-${i + 1}`}
onClick={() =>
handleReferenceSelect({ name: env.name, type: "environment", slug: env.slug })
}
type="button"
>
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faRecycle} />
</div>
<div className="w-48 truncate">{env.name}</div>
<div className="w-48 truncate text-left">{env.name}</div>
</div>
<div className="flex items-center text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
</div>
</button>
))}
</div>
</div>