diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1fa72b81a..abf46cadb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "@hcaptcha/react-hcaptcha": "^1.11.0", "@headlessui/react": "^1.7.19", "@hookform/resolvers": "^3.9.1", + "@lexical/react": "^0.29.0", "@lottiefiles/dotlottie-react": "^0.12.0", "@octokit/rest": "^21.0.2", "@peculiar/x509": "^1.12.3", @@ -66,6 +67,7 @@ "jspdf": "^2.5.2", "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", + "lexical": "^0.29.0", "ms": "^2.1.3", "nprogress": "^0.2.0", "picomatch": "^4.0.2", @@ -1570,6 +1572,260 @@ } } }, + "node_modules/@lexical/clipboard": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.29.0.tgz", + "integrity": "sha512-llxZosYCwH13p2GfPfhAinukdvAZYxWuwf5md107X80hsE8TQJj25unjqTwRKQ+w/wD+hpmBMziU8+K/WTitWQ==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.29.0", + "@lexical/list": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/code": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.29.0.tgz", + "integrity": "sha512-yKGzoKpyIO39Xf7OKLPpoCE5V8mTDCM3l3CDHZR3X1gM/VZQzf4jAiO3b06y9YkQ2fM8kqwchYu87wGvs8/iIQ==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.29.0", + "lexical": "0.29.0", + "prismjs": "^1.30.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.29.0.tgz", + "integrity": "sha512-uUq0m9ql/7mthp7Ho1vnG7Id6imQ5kD5mxUhX2lmgHretS+yAHGsGsGiPIVHdPWeVmUb2n4IVDJ+cJbUsUjQJw==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.29.0", + "@lexical/link": "0.29.0", + "@lexical/mark": "0.29.0", + "@lexical/table": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/dragon": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.29.0.tgz", + "integrity": "sha512-Zaky2jd/Pp1blAZqPeGNdyhxnVL4lwVjbWPxhfS1gbW4Q5CBQ3aD3B0T4ljiKfmRNJm004LJ9q7KjhlRbREvZA==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.29.0.tgz", + "integrity": "sha512-fa7s0Yi2RKz/GvgT5XU9fborx6VPU3VtvvEPaIXgyd6zXZRiOhD9rGypwB3oj4fMK1ndx2dX0m7SwhMJo48D8w==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/history": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.29.0.tgz", + "integrity": "sha512-OrCwZycp/yaq63mw511NutkwAB+W6WSchG1xTxlLh6nbc8jnbvKhCf4CGbnrvlhD7hTuzxJ8FI9/2M/2zv/mNQ==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/html": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.29.0.tgz", + "integrity": "sha512-+jV6ijppOpxpUGeXkGssXJbsAmFALfeLrgbM0xuZbxZ7RgYZ+5Atn00WjSno7+JV5EOuRkYmCNtS1tiHtXMY1g==", + "license": "MIT", + "dependencies": { + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/link": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.29.0.tgz", + "integrity": "sha512-wGbKRF0x/6ZQHuCfr8m8qD1J0R1kFmWINBG2A1hUXPDf7UY5qm/nS2oKNDGpjiDMGwkVZ7n7WfzeBGO+KRe/Lg==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/list": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.29.0.tgz", + "integrity": "sha512-sWiof+i2ff8rL7KxJ3dxHLwyJfX423e1EVLmAdQEOPhyZJiNbeLTSNhNGsZ8FjFoBwvTTEDwuQZm3iT3hliKOg==", + "license": "MIT", + "dependencies": { + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/mark": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.29.0.tgz", + "integrity": "sha512-UB3x6pyUdpZHRqF4tiajLnC1+Umvt7x8Rkkdi29aNNvzIWniVwGkBOlmvFus7x+4dOV1D1fydwiP4m38nGgLDw==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.29.0.tgz", + "integrity": "sha512-4Od8WoDoviv9DxJZVgrIORTIAzyoGOpztbGbIBXguGmwvy7NnHQDh9fZYIYRrdI1Awp1VVGdJ3ku/7KTgSOoRw==", + "license": "MIT", + "dependencies": { + "@lexical/code": "0.29.0", + "@lexical/link": "0.29.0", + "@lexical/list": "0.29.0", + "@lexical/rich-text": "0.29.0", + "@lexical/text": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/offset": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.29.0.tgz", + "integrity": "sha512-VyD2Ff3rBJpo++Fxvi3MNYmDELa+9nA0EgXqGRNb3MvRehRjHbaDbymtLMMHIwvbkF5lnra+ubStcTRQmoQxXw==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.29.0.tgz", + "integrity": "sha512-IzH3M652Ej2gB2sK65N3yTgyiQAa3I3tqKbSnBRiXu/+isxHoCy/qRr9/kL63uy7zhGvgV+EYsoffQCawIFt8Q==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.29.0.tgz", + "integrity": "sha512-F5C3meDb2HmO0NmKJBVRkjmX9PNln6O1jXU/APJuSFBdvfcIWSY58ncHR4zy2M5LF1Q5PQMWyIay9p+SqOtY5A==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/react": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.29.0.tgz", + "integrity": "sha512-YMlnljW/jxmwSzsRv5UPatfOoMZXqxFmRIEltTUIQfrOFdqn+ssUtCpjE6xRD1oxD6KpSIekakzLs+y/8+7CuQ==", + "license": "MIT", + "dependencies": { + "@lexical/devtools-core": "0.29.0", + "@lexical/dragon": "0.29.0", + "@lexical/hashtag": "0.29.0", + "@lexical/history": "0.29.0", + "@lexical/link": "0.29.0", + "@lexical/list": "0.29.0", + "@lexical/mark": "0.29.0", + "@lexical/markdown": "0.29.0", + "@lexical/overflow": "0.29.0", + "@lexical/plain-text": "0.29.0", + "@lexical/rich-text": "0.29.0", + "@lexical/table": "0.29.0", + "@lexical/text": "0.29.0", + "@lexical/utils": "0.29.0", + "@lexical/yjs": "0.29.0", + "lexical": "0.29.0", + "react-error-boundary": "^3.1.4" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.29.0.tgz", + "integrity": "sha512-fSKgXGxJUOWo7dwSTUYFVBNNk4pPN8norsZfdmKM1kGDS1/GKuVzlzHLKZ7rQb8RLD5a43p4ifEL+28P+q0Qqg==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/selection": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.29.0.tgz", + "integrity": "sha512-lX9CRrXgKte65cozTHFXwUJ2fvZD92OEtos+YU+U40GJjf3NdheGeKDxDfOpF4AXrYRSszY7E0CzmIvuEs0p4A==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/table": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.29.0.tgz", + "integrity": "sha512-Jdj32kBDeJh/0dGaZB14JggnEIS956/cN7grnLr7cmhhVzDicvLMBENSXQVEJAQVcSIU4G9EvxC7GJZ9VgqDnA==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.29.0", + "@lexical/utils": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/text": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.29.0.tgz", + "integrity": "sha512-QnNGr6ickTLk76o3PdxJjPwt//dpuh8idVfR73WdCIoAwkhiEPUxxTZERoMsudXj6O/lJ+/HhI61wVjLckYr3A==", + "license": "MIT", + "dependencies": { + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/utils": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.29.0.tgz", + "integrity": "sha512-y2hhWQDjcXdplsAaQMuZx6ht9u1I4BV5NynA+WKoQ3h8vKxzeDnpCxVOK/zxU1R5dhM/nilnFu7uhvrSeEn+TQ==", + "license": "MIT", + "dependencies": { + "@lexical/list": "0.29.0", + "@lexical/selection": "0.29.0", + "@lexical/table": "0.29.0", + "lexical": "0.29.0" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.29.0.tgz", + "integrity": "sha512-6IXWWlGkVJEzWP/+LcuKYJ9jmcFp8k7TT/jmz4V5gBD9Ut3swOGsIA/sQCtB9y7jad10csaDVmFdFzGNWKVH9A==", + "license": "MIT", + "dependencies": { + "@lexical/offset": "0.29.0", + "@lexical/selection": "0.29.0", + "lexical": "0.29.0" + }, + "peerDependencies": { + "yjs": ">=13.5.22" + } + }, "node_modules/@lottiefiles/dotlottie-react": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.12.0.tgz", @@ -8871,6 +9127,17 @@ "node": ">=10" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/iterator.prototype": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", @@ -9100,6 +9367,34 @@ "node": ">= 0.8.0" } }, + "node_modules/lexical": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.29.0.tgz", + "integrity": "sha512-eoBHUEn0LmExKeK6x2cFKU0FPaMk2Bc5HgiCzTiv5ymKtwWw7LeKcxaNPmLxRRdQpcWV1IMKjayAbw7Lt/Gu7w==", + "license": "MIT" + }, + "node_modules/lib0": { + "version": "0.2.102", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.102.tgz", + "integrity": "sha512-g70kydI0I1sZU0ChO8mBbhw0oUW/8U0GHzygpvEIx8k+jgOpqnTSb/E+70toYVqHxBhrERD21TwD5QcZJQ40ZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -10857,6 +11152,15 @@ } } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -11142,6 +11446,22 @@ "react": "^18.3.1" } }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -13587,9 +13907,9 @@ } }, "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "version": "5.4.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.16.tgz", + "integrity": "sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14131,6 +14451,24 @@ "node": ">=8" } }, + "node_modules/yjs": { + "version": "13.6.24", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.24.tgz", + "integrity": "sha512-xn/pYLTZa3uD1uDG8lpxfLRo5SR/rp0frdASOl2a71aYNvUXdWcLtVL91s2y7j+Q8ppmjZ9H3jsGVgoFMbT2VA==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 95ad59c7d..e750783b2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@hcaptcha/react-hcaptcha": "^1.11.0", "@headlessui/react": "^1.7.19", "@hookform/resolvers": "^3.9.1", + "@lexical/react": "^0.29.0", "@lottiefiles/dotlottie-react": "^0.12.0", "@octokit/rest": "^21.0.2", "@peculiar/x509": "^1.12.3", @@ -70,6 +71,7 @@ "jspdf": "^2.5.2", "jsrp": "^0.2.4", "jwt-decode": "^4.0.0", + "lexical": "^0.29.0", "ms": "^2.1.3", "nprogress": "^0.2.0", "picomatch": "^4.0.2", diff --git a/frontend/src/components/v2/Editor/Editor.tsx b/frontend/src/components/v2/Editor/Editor.tsx new file mode 100644 index 000000000..e9c49cbac --- /dev/null +++ b/frontend/src/components/v2/Editor/Editor.tsx @@ -0,0 +1,159 @@ +/* eslint-disable no-underscore-dangle */ +import { forwardRef, InputHTMLAttributes } from "react"; +import { InitialConfigType, LexicalComposer } from "@lexical/react/LexicalComposer"; +import { ContentEditable } from "@lexical/react/LexicalContentEditable"; +import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"; +import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"; +import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin"; +import { ReactNode } from "@tanstack/react-router"; +import { cva, VariantProps } from "cva"; +import { EditorState, LexicalEditor } from "lexical"; +import { twMerge } from "tailwind-merge"; + +import { HighlightNode } from "./EditorHighlight"; +import { EditorPlaceholderPlugin } from "./EditorPlaceholderPlugin"; + +// Catch any errors that occur during Lexical updates and log them +// or throw them as needed. If you don't throw them, Lexical will +// try to recover gracefully without losing user data. +function onError(error: Error) { + console.error(error); +} + +const inputVariants = cva( + "input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100", + { + variants: { + size: { + xs: ["text-xs"], + sm: ["text-sm"], + md: ["text-md"], + lg: ["text-lg"] + }, + isRounded: { + true: ["rounded-md"], + false: "" + }, + variant: { + filled: ["bg-mineshaft-900", "text-gray-400"], + outline: ["bg-transparent"], + plain: "bg-transparent outline-none" + }, + isError: { + true: "focus:ring-red/50 placeholder-red-300", + false: "focus:ring-primary-400/50 focus:ring-1" + } + }, + compoundVariants: [] + } +); + +const inputParentContainerVariants = cva("inline-flex font-inter items-center border relative", { + variants: { + isRounded: { + true: ["rounded-md"], + false: "" + }, + isError: { + true: "border-red", + false: "border-mineshaft-500" + }, + isFullWidth: { + true: "w-full", + false: "" + }, + variant: { + filled: ["bg-bunker-800", "text-gray-400"], + outline: ["bg-transparent"], + plain: "border-none" + } + } +}); + +type Props = Omit< + InputHTMLAttributes, + "size" | "onChange" | "placeholder" | "aria-placeholder" +> & + VariantProps & { + children?: ReactNode; + namespace?: string; + placeholder?: string; + isFullWidth?: boolean; + isRequired?: boolean; + leftIcon?: ReactNode; + rightIcon?: ReactNode; + isDisabled?: boolean; + isReadOnly?: boolean; + containerClassName?: string; + onChange: (editorState: EditorState, editor: LexicalEditor, tags: Set) => void; + initialValue?: string; + }; + +export const Editor = forwardRef( + ( + { + children, + namespace = "infisical-editor", + className, + containerClassName, + isRounded = true, + isFullWidth = true, + isDisabled, + isError = false, + isRequired, + leftIcon, + rightIcon, + variant = "filled", + size = "md", + isReadOnly, + placeholder, + onChange, + ...props + }, + ref + ) => { + const initialConfig: InitialConfigType = { + namespace, + onError, + nodes: [HighlightNode] + }; + + return ( +
+ {leftIcon && {leftIcon}} + + + } + ErrorBoundary={LexicalErrorBoundary} + /> + + + {children} + + {rightIcon && {rightIcon}} +
+ ); + } +); diff --git a/frontend/src/components/v2/Editor/EditorHighlight.tsx b/frontend/src/components/v2/Editor/EditorHighlight.tsx new file mode 100644 index 000000000..bb57828fb --- /dev/null +++ b/frontend/src/components/v2/Editor/EditorHighlight.tsx @@ -0,0 +1,127 @@ +/* eslint-disable no-underscore-dangle,@typescript-eslint/class-methods-use-this */ +import { useCallback, useEffect } from "react"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { useLexicalTextEntity } from "@lexical/react/useLexicalTextEntity"; +import { + $applyNodeReplacement, + EditorConfig, + LexicalNode, + SerializedTextNode, + Spread, + TextNode +} from "lexical"; + +type HighlightTheme = { contentClassName: string }; +type Trigger = { startTrigger: string; endTrigger: string }; + +export type SerializedHighlightNode = Spread< + { + __highlightTheme: HighlightTheme; + __trigger: Trigger; + }, + SerializedTextNode +>; + +export class HighlightNode extends TextNode { + __highlightTheme: HighlightTheme; + __trigger: Trigger; + + constructor( + text: string, + highlightTheme: HighlightTheme = { + contentClassName: "ph-no-capture text-yellow-200/80" + }, + trigger: Trigger = { startTrigger: "${", endTrigger: "}" }, + key?: string + ) { + super(text, key); + this.__highlightTheme = highlightTheme; + this.__trigger = trigger; + } + + static getType(): string { + return "highlight"; + } + + static clone(node: HighlightNode): HighlightNode { + return new HighlightNode(node.__text, node.__highlightTheme, node.__trigger, node.__key); + } + + static importJSON(serializedNode: SerializedHighlightNode): HighlightNode { + return $applyNodeReplacement(new HighlightNode("")).updateFromJSON(serializedNode); + } + + createDOM(config: EditorConfig): HTMLElement { + const dom = super.createDOM(config); + dom.style.cursor = "default"; + dom.className = this.__highlightTheme.contentClassName; + return dom; + } + + canInsertTextBefore(): boolean { + return false; + } + + canInsertTextAfter(): boolean { + return false; + } + + isTextEntity(): true { + return true; + } +} + +export function $createKeywordNode(keyword: string = ""): HighlightNode { + return $applyNodeReplacement(new HighlightNode(keyword)); +} + +export function $isKeywordNode(node: LexicalNode | null | undefined): boolean { + return node instanceof HighlightNode; +} + +type Props = { + contentClassName?: string; + startTrigger?: string; + endTrigger?: string; +}; + +export const EditorHighlightPlugin = ({ + endTrigger = "}", + startTrigger = "${", + contentClassName = "ph-no-capture text-yellow-200/80" +}: Props) => { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!editor.hasNodes([HighlightNode])) { + throw new Error("HighlightsPlugin: HighlightsNode not registered on editor"); + } + }, [editor]); + + const createKeywordNode = useCallback((textNode: TextNode): HighlightNode => { + return $applyNodeReplacement( + new HighlightNode( + textNode.getTextContent(), + { contentClassName }, + { startTrigger, endTrigger } + ) + ); + }, []); + + const getKeywordMatch = useCallback((text: string) => { + for (let i = 0; i < text.length; i += 1) { + if (text.slice(i, i + 2) === startTrigger) { + const closingBracketIndex = text.indexOf(endTrigger, i + 2); + if (closingBracketIndex !== -1) { + return { start: i, end: closingBracketIndex + 1 }; + } + return null; + } + } + return null; + }, []); + + useLexicalTextEntity(getKeywordMatch, HighlightNode, createKeywordNode); + + return null; +}; diff --git a/frontend/src/components/v2/Editor/EditorPlaceholderPlugin.tsx b/frontend/src/components/v2/Editor/EditorPlaceholderPlugin.tsx new file mode 100644 index 000000000..7bddf17de --- /dev/null +++ b/frontend/src/components/v2/Editor/EditorPlaceholderPlugin.tsx @@ -0,0 +1,22 @@ +import { useEffect } from "react"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { useLexicalIsTextContentEmpty } from "@lexical/react/useLexicalIsTextContentEmpty"; + +export const EditorPlaceholderPlugin = ({ placeholder }: { placeholder: string | undefined }) => { + const [editor] = useLexicalComposerContext(); + const isEmpty = useLexicalIsTextContentEmpty(editor); + + /* Set the placeholder on root. */ + useEffect(() => { + const rootElement = editor.getRootElement() as HTMLElement; + if (rootElement) { + if (isEmpty && placeholder) { + rootElement.setAttribute("placeholder", placeholder); + } else { + rootElement.removeAttribute("placeholder"); + } + } + }, [editor, isEmpty]); // eslint-disable-line + + return null; +}; diff --git a/frontend/src/components/v2/Editor/index.tsx b/frontend/src/components/v2/Editor/index.tsx new file mode 100644 index 000000000..6da88bf75 --- /dev/null +++ b/frontend/src/components/v2/Editor/index.tsx @@ -0,0 +1,2 @@ +export { Editor } from "./Editor"; +export { EditorHighlightPlugin } from "./EditorHighlight"; diff --git a/frontend/src/components/v2/index.tsx b/frontend/src/components/v2/index.tsx index 092fd6697..9dcf72e40 100644 --- a/frontend/src/components/v2/index.tsx +++ b/frontend/src/components/v2/index.tsx @@ -11,6 +11,7 @@ export * from "./DatePicker"; export * from "./DeleteActionModal"; export * from "./Drawer"; export * from "./Dropdown"; +export * from "./Editor"; export * from "./EmailServiceSetupModal"; export * from "./EmptyState"; export * from "./FilterableSelect"; diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index 261556752..f3b9fa012 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -46,5 +46,6 @@ export { useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets + useGetIdentityUniversalAuthClientSecrets, + useSearchIdentities } from "./queries"; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 005882359..6b8a1d1cc 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -15,11 +15,13 @@ import { IdentityMembershipOrg, IdentityOidcAuth, IdentityTokenAuth, - IdentityUniversalAuth + IdentityUniversalAuth, + TSearchIdentitiesDTO } from "./types"; export const identitiesKeys = { getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const, + searchIdentities: (dto: TSearchIdentitiesDTO) => ["identity", "search", dto] as const, getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => @@ -53,6 +55,26 @@ export const useGetIdentityById = (identityId: string) => { }); }; +export const useSearchIdentities = (dto: TSearchIdentitiesDTO) => { + const { limit, search, offset, orderBy, orderDirection } = dto; + return useQuery({ + queryKey: identitiesKeys.searchIdentities(dto), + queryFn: async () => { + const { data } = await apiRequest.post<{ + identities: IdentityMembershipOrg[]; + totalCount: number; + }>("/api/v1/identities/search", { + limit, + offset, + orderBy, + orderDirection, + search + }); + return data; + } + }); +}; + export const useGetIdentityProjectMemberships = (identityId: string) => { return useQuery({ enabled: Boolean(identityId), diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 35a9870bc..ca06219aa 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -1,3 +1,5 @@ +import { OrderByDirection } from "../generic/types"; +import { OrgIdentityOrderBy } from "../organization/types"; import { TOrgRole } from "../roles/types"; import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types"; import { IdentityAuthMethod, IdentityJwtConfigurationType } from "./enums"; @@ -540,3 +542,14 @@ export type TProjectIdentitiesList = { identityMemberships: IdentityMembership[]; totalCount: number; }; + +export type TSearchIdentitiesDTO = { + limit?: number; + offset?: number; + orderBy?: OrgIdentityOrderBy; + orderDirection?: OrderByDirection; + search: { + name?: { $contains: string }; + role?: { $in: string[] }; + }; +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index 3c9610c04..1c5b0c465 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -191,3 +191,10 @@ html { #nprogress .bar { @apply bg-primary-400; } + +[contentEditable="true"]:before { + content: attr(placeholder); + position: absolute; + top: 0.5rem; + @apply text-sm text-gray-500 opacity-50; +} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index cf57aebbb..053b2c875 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -1,7 +1,10 @@ +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; import { faArrowDown, faArrowUp, faEllipsis, + faFilter, faMagnifyingGlass, faServer } from "@fortawesome/free-solid-svg-icons"; @@ -12,14 +15,19 @@ import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { + Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, EmptyState, + FormControl, IconButton, Input, Pagination, + Popover, + PopoverContent, + PopoverTrigger, Select, SelectItem, Spinner, @@ -30,11 +38,12 @@ import { Td, Th, THead, + Tooltip, Tr } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { usePagination, useResetPageHelper } from "@app/hooks"; -import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; +import { useGetOrgRoles, useSearchIdentities, useUpdateIdentity } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -68,22 +77,22 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { page, setPerPage } = usePagination(OrgIdentityOrderBy.Name); + const [filteredRoles, setFilteredRoles] = useState([]); const organizationId = currentOrg?.id || ""; const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); - const { data, isPending, isFetching } = useGetIdentityMembershipOrgs( - { - organizationId, - offset, - limit, - orderDirection, - orderBy, - search: debouncedSearch - }, - { placeholderData: (prevData) => prevData } - ); + const { data, isPending, isFetching } = useSearchIdentities({ + offset, + limit, + orderDirection, + orderBy, + search: { + name: debouncedSearch ? { $contains: debouncedSearch } : undefined, + role: filteredRoles?.length ? { $in: filteredRoles } : undefined + } + }); const { totalCount = 0 } = data ?? {}; useResetPageHelper({ @@ -91,6 +100,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { offset, setPage }); + const filterForm = useForm<{ roles: string }>(); const { data: roles } = useGetOrgRoles(organizationId); @@ -132,13 +142,78 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { return (
- setSearch(e.target.value)} - leftIcon={} - placeholder="Search identities by name..." - /> +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search identities by name..." + /> +
+ + + + + + + + + +
+ Advance Filter +
+
{ + setFilteredRoles(el.roles?.split(",")?.filter(Boolean) || []); + })} + > + ( + + + + )} + /> +
+ + {Boolean(filteredRoles.length) && ( + + )} +
+ +
+
+
+
@@ -190,7 +265,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { {isPending && } {!isPending && - data?.identityMemberships.map(({ identity: { id, name }, role, customRole }) => { + data?.identities?.map(({ identity: { id, name }, role, customRole }) => { return ( { onChangePerPage={(newPerPage) => setPerPage(newPerPage)} /> )} - {!isPending && data && data?.identityMemberships.length === 0 && ( + {!isPending && data && data?.identities.length === 0 && ( 0