From 66ea3ba17267d38b788e267c4dbe394d4e214145 Mon Sep 17 00:00:00 2001 From: Ebezer Igbinoba Date: Sun, 20 Aug 2023 10:02:40 +0100 Subject: [PATCH 01/16] feat: added custom design for tags --- frontend/src/components/v2/Tag/Tag.tsx | 6 +- frontend/src/hooks/api/tags/types.ts | 15 +- .../DesignTagModal/DesignTagModal.tsx | 138 ++++++++++++++++++ .../components/DesignTagModal/index.tsx | 1 + .../SecretInputRow/SecretInputRow.tsx | 78 ++++++++-- 5 files changed, 225 insertions(+), 13 deletions(-) create mode 100644 frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx create mode 100644 frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index 13672e7e9..cbb833b30 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -9,6 +9,7 @@ type Props = { className?: string; onClose?: () => void; color?: string; + styles?: Record isDisabled?: boolean; } & VariantProps; @@ -35,11 +36,12 @@ export const Tag = ({ color, isDisabled, size = "sm", - onClose + onClose, + styles = {} }: Props) => (
{children} {onClose && ( diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 56b7171bf..87486ee03 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -36,4 +36,17 @@ export type DeleteWsTagRes = { createdAt: string; user: string; _id: string; -}; \ No newline at end of file +}; + +export type TagDesign = { + tagBackground: string; + tagLabel: string +} + +export type SecretTags = { + id: string; + _id: string; + slug: string; + tagBackground: string; + tagLabel: string +} \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx new file mode 100644 index 000000000..6993471e9 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx @@ -0,0 +1,138 @@ +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { Button, FormControl, Input, ModalClose, Tooltip, IconButton, Tag } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; +import { TagDesign } from "~/hooks/api/tags/types"; + +import { + faEye, + faEyeSlash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useState } from 'react'; +import { WsTag } from '../../../../hooks/api/tags/types'; + +type TagData = { + tagBackground: string; + tagLabel: string +} + +type Props = { + onDesignTag: (tagData: TagData) => void; + selectedTag: WsTag +}; + +const designTagSchema = yup.object({ + tagBackground: yup.string().required().trim().label("Tag Background"), + tagLabel: yup.string().required().trim().label("Tag Label"), +}); +type FormData = yup.InferType; + + +export const DesignTagModal = ({ onDesignTag, selectedTag }: Props): JSX.Element => { + const [tagDesignObj, setTagDesignObj] = useState({ + tagColor: { + bg: "", + text: "" + } + }) + + const { + control, + reset, + formState, + handleSubmit, + setValue + } = useForm({ + resolver: yupResolver(designTagSchema) + }); + + const onFormSubmit = ({ tagBackground, tagLabel }: FormData) => { + onDesignTag({ tagBackground, tagLabel }); + reset(); + }; + + const [previewTag, setPreviewTag] = useToggle(false); + + const handleInputChange = (e: React.ChangeEvent, type: string) => { + setTagDesignObj((prev: { tagColor: { bg: string, text: string }; }) => ({ + tagColor: { + ...prev.tagColor, + [type]: e.target.value + } + })) + if (type === 'bg') { + setValue('tagBackground', e.target.value) + } else { + setValue('tagLabel', e.target.value) + } + } + + return ( +
+
+ { + return ( + <> + + ) => handleInputChange(e, 'bg')} /> + + + ) + }} + /> + {/* */} +
+ setPreviewTag.toggle()} className="absolute top-[2px] left-[127px] cursor-pointer" /> + {previewTag && ( + void (0)} + key={selectedTag._id} + className="absolute top-[-5px] right-[-5px] cursor-pointer" + > + {selectedTag.slug} + + )} + +
+ {/*
*/} +
+ + { + return ( + <> + + ) => handleInputChange(e, 'text')} value={tagDesignObj.tagColor.text} /> + + + ) + }} + /> +
+ + + + +
+ + ); +}; diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx new file mode 100644 index 000000000..165e8e3a3 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx @@ -0,0 +1 @@ +export {DesignTagModal} from "./DesignTagModal" \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 65e5436b7..f58360dcb 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -38,12 +38,17 @@ import { SecretInput, Tag, TextArea, - Tooltip + Tooltip, + Modal, + ModalContent, } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; + import { WsTag } from "@app/hooks/api/types"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; +import { SecretTags, TagDesign } from "~/hooks/api/tags/types"; +import { DesignTagModal } from "../../components/DesignTagModal"; +import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; const tagColors = [ { bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" }, @@ -73,6 +78,7 @@ type Props = { // tag props wsTags?: WsTag[]; onCreateTagOpen: () => void; + onDesignTagOpen: (selectedTag: WsTag, selectedFieldIndex: number) => void; // rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering control: Control; register: UseFormRegister; @@ -80,6 +86,9 @@ type Props = { isKeyError?: boolean; keyError?: string; autoCapitalization?: boolean; + designObj: TagDesign & WsTag; + updateDesign: boolean; + selectedFieldIndex: number }; export const SecretInputRow = memo( @@ -92,6 +101,8 @@ export const SecretInputRow = memo( isAddOnly, wsTags, onCreateTagOpen, + onDesignTagOpen, + designObj, onSecretDelete, searchTerm, control, @@ -100,7 +111,9 @@ export const SecretInputRow = memo( isKeyError, keyError, secUniqId, - autoCapitalization + autoCapitalization, + updateDesign, + selectedFieldIndex }: Props): JSX.Element => { const isKeySubDisabled = useRef(false); // comment management in a row @@ -113,7 +126,7 @@ export const SecretInputRow = memo( const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]])) // display the tags in alphabetical order - secretTags.sort((a, b) => a.name.localeCompare(b.name)) + secretTags.sort((a, b) => a?.name?.localeCompare(b?.name)) // to get details on a secret const overrideAction = useWatch({ @@ -145,7 +158,10 @@ export const SecretInputRow = memo( // when secret is override by personal values const isOverridden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; + const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue); + const [tagDesignObj, setTagDesignObj] = useState({}) + const [selectedTag, setSelectedTag] = useState({}) const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true }); const tags = @@ -174,6 +190,20 @@ export const SecretInputRow = memo( setInviteLinkCopied.on(); }; + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "secretDetails", + "addTag", + "secretSnapshots", + "uploadedSecOpts", + "compareSecrets", + "folderForm", + "deleteFolder", + "upgradePlan", + "addSecretImport", + "deleteSecretImport", + "designTag" + ] as const); + const onSecretOverride = () => { if (isOverridden) { // when user created a new override but then removes @@ -193,14 +223,22 @@ export const SecretInputRow = memo( }; const onSelectTag = (selectedTag: WsTag) => { + const checkBoxSelected = !selectedTagIds[selectedTag.slug] + checkBoxSelected && handlePopUpOpen('designTag') + setSelectedTag(selectedTag) + }; + + const onDesignWsTag = (_tagDesignObj: TagDesign) => { + setTagDesignObj(() => (_tagDesignObj)) + handlePopUpClose("designTag"); const shouldAppend = !selectedTagIds[selectedTag.slug]; if (shouldAppend) { - append(selectedTag); + append({...selectedTag, ..._tagDesignObj}); } else { - const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug); + const pos = tags.findIndex(({ slug }: {slug: string}) => selectedTag.slug === slug); remove(pos); } - }; + } const isCreatedSecret = !secId; const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly; @@ -223,11 +261,28 @@ export const SecretInputRow = memo( return <>; } + + return (
{index + 1}
+ {/* Add a custom design to new tag to make visible */} + { + handlePopUpToggle("designTag", open); + }} + > + + + + +
- {secretTags.map(({ id, _id, slug }, i) => { + {secretTags.map(({ id, _id, slug, tagBackground, tagLabel }: SecretTags, i: number) => { // This map lookup shouldn't ever fail, but if it does we default to the first color const tagColor = tagColorByTagId.get(_id) || tagColors[0] return ( @@ -335,6 +390,10 @@ export const SecretInputRow = memo( tagColor.bg, tagColor.text )} + styles={{ + backgroundColor: tagBackground, + color: tagLabel + }} isDisabled={isReadOnly || isAddOnly || isRollbackMode} onClose={() => remove(i)} key={id} @@ -395,9 +454,8 @@ export const SecretInputRow = memo( className="mr-0 data-[state=checked]:bg-primary" id="autoCapitalization" isChecked={selectedTagIds?.[wsTag.slug]} - onCheckedChange={() => {}} > - {} + { } } key={wsTag._id} From 9a1b453c863959facd7f31652b71c72c381fbb81 Mon Sep 17 00:00:00 2001 From: Ebezer Igbinoba Date: Tue, 22 Aug 2023 05:12:23 +0100 Subject: [PATCH 02/16] Feat: added tag color widgt and changed tag popover design --- backend/spec.json | 3 + backend/src/controllers/v2/tagController.ts | 3 +- backend/src/models/tag.ts | 6 + backend/src/routes/v2/tags.ts | 1 + docs/spec.yaml | 2 + .../components/utilities/isValidHexColor.ts | 5 + frontend/src/components/v2/Tag/Tag.tsx | 14 +- frontend/src/const.ts | 66 +++++++ frontend/src/hooks/api/tags/queries.tsx | 13 +- frontend/src/hooks/api/tags/types.ts | 23 ++- frontend/src/styles/globals.css | 25 +++ .../src/views/DashboardPage/DashboardPage.tsx | 3 +- .../CreateTagModal/CreateTagModal.tsx | 176 +++++++++++++++--- .../SecretInputRow/SecretInputRow.tsx | 163 ++++++++-------- 14 files changed, 368 insertions(+), 135 deletions(-) create mode 100644 frontend/src/components/utilities/isValidHexColor.ts diff --git a/backend/spec.json b/backend/spec.json index 013b5fd4d..1afbc8dce 100644 --- a/backend/spec.json +++ b/backend/spec.json @@ -3203,6 +3203,9 @@ "name": { "example": "any" }, + "tagColor": { + "example": "any" + }, "slug": { "example": "any" } diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 0d945c3e5..fa21fc971 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -6,10 +6,11 @@ import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; export const createWorkspaceTag = async (req: Request, res: Response) => { const { workspaceId } = req.params; - const { name, slug } = req.body; + const { name, slug, tagColor } = req.body; const tagToCreate = { name, + tagColor, workspace: new Types.ObjectId(workspaceId), slug, user: new Types.ObjectId(req.user._id), diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts index 53bf085d3..ec649e254 100644 --- a/backend/src/models/tag.ts +++ b/backend/src/models/tag.ts @@ -3,6 +3,7 @@ import { Schema, Types, model } from "mongoose"; export interface ITag { _id: Types.ObjectId; name: string; + tagColor: string; slug: string; user: Types.ObjectId; workspace: Types.ObjectId; @@ -15,6 +16,11 @@ const tagSchema = new Schema( required: true, trim: true, }, + tagColor: { + type: String, + required: false, + trim: true, + }, slug: { type: String, required: true, diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index 7ccfd17cd..aca1b6d8c 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -48,6 +48,7 @@ router.post( }), param("workspaceId").exists().trim(), body("name").exists().trim(), + body("tagColor").exists().trim(), body("slug").exists().trim(), validateRequest, tagController.createWorkspaceTag diff --git a/docs/spec.yaml b/docs/spec.yaml index 799c7f6f4..5cf129be3 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -1949,6 +1949,8 @@ paths: properties: name: example: any + tagColor: + example: any slug: example: any /api/v2/workspace/tags/{tagId}: diff --git a/frontend/src/components/utilities/isValidHexColor.ts b/frontend/src/components/utilities/isValidHexColor.ts new file mode 100644 index 000000000..86c14b142 --- /dev/null +++ b/frontend/src/components/utilities/isValidHexColor.ts @@ -0,0 +1,5 @@ +export const isValidHexColor = (hexColor: string) => { + const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/; + + return hexColorPattern.test(hexColor); +} \ No newline at end of file diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index cbb833b30..a2de200ef 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -11,10 +11,11 @@ type Props = { color?: string; styles?: Record isDisabled?: boolean; + tagColor: string; } & VariantProps; const tagVariants = cva( - "inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200", + "inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 rounded-[30px] text-gray-400 ", { variants: { colorSchema: { @@ -41,18 +42,7 @@ export const Tag = ({ }: Props) => (
{children} - {onClose && ( - - )}
); diff --git a/frontend/src/const.ts b/frontend/src/const.ts index f2309df31..7f24f8fc5 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -51,3 +51,69 @@ const plansProd: Mapping = { export const plans = plansProd || plansDev; export const leaveConfirmDefaultMessage = "Your changes will be lost if you leave the page. Are you sure you want to continue?"; + +export const secretTagsColors = [ + { + id: 1, + hex: '#bec2c8', + rgba: "rgb(128,128,128, 0.8)", + name: 'Grey', + selected: true + }, + { + id: 2, + hex: '#95a2b3', + rgba: "rgb(0,0,255, 0.8)", + name: 'blue', + selected: false + }, + { + id: 3, + hex: '#5e6ad2', + rgba: "rgb(128,0,128, 0.8)", + name: 'Purple', + selected: false + }, + { + id: 4, + hex: '#26b5ce', + rgba: "rgb(0,128,128, 0.8)", + name: 'Teal', + selected: false + }, + { + id: 5, + hex: '#4cb782', + rgba: "rgb(0,128,0, 0.8)", + name: 'Green', + selected: false + }, + { + id: 6, + hex: '#f2c94c', + rgba: "rgb(255,255,0, 0.8)", + name: 'Yellow', + selected: false + }, + { + id: 7, + hex: '#f2994a', + rgba: "rgb(128,128,0, 0.8)", + name: 'Orange', + selected: false + }, + { + id: 8, + hex: '#f7c8c1', + rgba: "rgb(128,0,0, 0.8)", + name: 'Pink', + selected: false + }, + { + id: 9, + hex: '#eb5757', + rgba: "rgb(255,0,0, 0.8)", + name: 'Red', + selected: false + }, +] \ No newline at end of file diff --git a/frontend/src/hooks/api/tags/queries.tsx b/frontend/src/hooks/api/tags/queries.tsx index 74900da0e..b0216828c 100644 --- a/frontend/src/hooks/api/tags/queries.tsx +++ b/frontend/src/hooks/api/tags/queries.tsx @@ -7,7 +7,9 @@ import { CreateTagRes, DeleteTagDTO, DeleteWsTagRes, - UserWsTags + QueryTag, + UserWsTags, + WsTag } from "./types"; const workspaceTags = { @@ -34,14 +36,15 @@ export const useCreateWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ workspaceID, tagName, tagSlug }) => { + mutationFn: async ({ workspaceID, tagName, tagColor, tagSlug }: QueryTag) => { const { data } = await apiRequest.post(`/api/v2/workspace/${workspaceID}/tags`, { name: tagName, + tagColor: tagColor, slug: tagSlug }) return data; }, - onSuccess: (tagData) => { + onSuccess: (tagData: WsTag) => { queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace)); } }); @@ -51,11 +54,11 @@ export const useDeleteWsTag = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async ({ tagID }) => { + mutationFn: async ({ tagID }: {tagID: string}) => { const { data } = await apiRequest.delete(`/api/v2/workspace/tags/${tagID}`); return data }, - onSuccess: (tagData) => { + onSuccess: (tagData: WsTag) => { queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace)); } }); diff --git a/frontend/src/hooks/api/tags/types.ts b/frontend/src/hooks/api/tags/types.ts index 87486ee03..db162415e 100644 --- a/frontend/src/hooks/api/tags/types.ts +++ b/frontend/src/hooks/api/tags/types.ts @@ -38,15 +38,24 @@ export type DeleteWsTagRes = { _id: string; }; -export type TagDesign = { - tagBackground: string; - tagLabel: string -} - export type SecretTags = { id: string; _id: string; slug: string; - tagBackground: string; - tagLabel: string + tagColor: string; +} + +export type TagColor = { + id: number; + hex: string + rgba: string + name: string + selected: boolean +} + +export type QueryTag = { + workspaceID: string; + tagName: string; + tagColor: string; + tagSlug: string } \ No newline at end of file diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index ae8d17427..6d5070fe0 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -107,6 +107,31 @@ @apply bg-primary-400; } } +.tags-conic-bg { + background: conic-gradient(rgb(235, 87, 87), rgb(242, 201, 76), rgb(76, 183, 130), rgb(78, 167, 252), rgb(250, 96, 122)); +} + +.show-tags { + transform: translateY(10px); + transition: all 0.2s; + opacity: 1; +} +.hide-tags { + transform: translateY(-20px); + transition: all 0.2s; + opacity: 0; +} + +.show-hex-input { + transform: translateY(-33px); + transition: all 0.2s; + opacity: 1; +} +.hide-hex-input { + transform: translateY(20px); + transition: all 0.2s; + opacity: 0; +} @import "@fontsource/inter/400.css"; @import "@fontsource/inter/500.css"; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index b83cb839d..4c84aab84 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -513,11 +513,12 @@ export const DashboardPage = () => { }, []); const onCreateWsTag = useCallback( - async (tagName: string) => { + async (tagName: string, tagColor: string) => { try { await createWsTag({ workspaceID: workspaceId, tagName, + tagColor, tagSlug: tagName.replace(" ", "_") }); handlePopUpClose("addTag"); diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx index 22973c1a6..c378bf54c 100644 --- a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx +++ b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx @@ -1,11 +1,21 @@ +import { useState, useEffect } from 'react'; import { Controller, useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; +import { secretTagsColors } from "~/const" +import { + faCheck +} from "@fortawesome/free-solid-svg-icons"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Button, FormControl, Input, ModalClose, Tooltip } from "@app/components/v2"; +import { isValidHexColor } from "~/components/utilities/isValidHexColor"; +import { TagColor } from '~/hooks/api/tags/types'; -import { Button, FormControl, Input, ModalClose } from "@app/components/v2"; type Props = { - onCreateTag: (tagName: string) => Promise; + onCreateTag: (tagName: string, tagColor: string) => Promise; }; const createTagSchema = yup.object({ @@ -23,33 +33,151 @@ export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { resolver: yupResolver(createTagSchema) }); + const [tagsColors, setTagsColors] = useState(secretTagsColors) + const [selectedTagColor, setSelectedTagColor] = useState({}) + const [showHexInput, setShowHexInput] = useState(false) + const [tagColor, setTagColor] = useState("") + + const onFormSubmit = async ({ name }: FormData) => { - await onCreateTag(name); + await onCreateTag(name, tagColor); reset(); }; + useEffect(() => { + const clonedTagColors = [...tagsColors] + for (const tagColor of clonedTagColors) { + if (tagColor.selected) { + setSelectedTagColor(tagColor) + setTagColor(tagColor.hex) + break + } + } + }, []) + + useEffect(() => { + const tagsList = document.querySelector(".secret-tags-wrapper") + const tagsHexWrapper = document.querySelector(".tags-hex-wrapper") + + if (showHexInput) { + tagsList?.classList.add('hide-tags') + tagsList?.classList.remove('show-tags') + tagsHexWrapper?.classList.add('show-hex-input') + tagsHexWrapper?.classList.remove('hide-hex-input') + } else { + tagsList?.classList.remove('hide-tags') + tagsList?.classList.add('show-tags') + tagsHexWrapper?.classList.remove('show-hex-input') + tagsHexWrapper?.classList.add('hide-hex-input') + } + }, [showHexInput]) + + const handleColorChange = (tagColor: TagColor) => { + const clonedTagColors = [...tagsColors] + const tagColorIndex = clonedTagColors.findIndex(_tagColor => _tagColor.id === tagColor.id) + const _selectedTagColor = clonedTagColors[tagColorIndex] + clonedTagColors.forEach(tagColor => { + tagColor.selected = false + }) + if (selectedTagColor.id !== tagColor.id) { + _selectedTagColor.selected = !_selectedTagColor.selected + setSelectedTagColor(_selectedTagColor) + setTagColor(_selectedTagColor.hex) + } + } + return ( -
- ( - - - - )} - /> -
- - - - -
- + + + +
+ + ); }; diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index f58360dcb..9b70b936e 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -46,9 +46,8 @@ import { import { WsTag } from "@app/hooks/api/types"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; -import { SecretTags, TagDesign } from "~/hooks/api/tags/types"; -import { DesignTagModal } from "../../components/DesignTagModal"; -import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks"; +import { SecretTags } from "~/hooks/api/tags/types"; +import { useToggle } from "@app/hooks"; const tagColors = [ { bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" }, @@ -86,9 +85,6 @@ type Props = { isKeyError?: boolean; keyError?: string; autoCapitalization?: boolean; - designObj: TagDesign & WsTag; - updateDesign: boolean; - selectedFieldIndex: number }; export const SecretInputRow = memo( @@ -102,7 +98,6 @@ export const SecretInputRow = memo( wsTags, onCreateTagOpen, onDesignTagOpen, - designObj, onSecretDelete, searchTerm, control, @@ -112,8 +107,6 @@ export const SecretInputRow = memo( keyError, secUniqId, autoCapitalization, - updateDesign, - selectedFieldIndex }: Props): JSX.Element => { const isKeySubDisabled = useRef(false); // comment management in a row @@ -160,12 +153,10 @@ export const SecretInputRow = memo( overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue); - const [tagDesignObj, setTagDesignObj] = useState({}) - const [selectedTag, setSelectedTag] = useState({}) const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true }); - const tags = - useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || []; + const tags = useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || []; + const selectedTagIds = tags.reduce>( (prev, curr) => ({ ...prev, [curr.slug]: true }), {} @@ -190,20 +181,6 @@ export const SecretInputRow = memo( setInviteLinkCopied.on(); }; - const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "secretDetails", - "addTag", - "secretSnapshots", - "uploadedSecOpts", - "compareSecrets", - "folderForm", - "deleteFolder", - "upgradePlan", - "addSecretImport", - "deleteSecretImport", - "designTag" - ] as const); - const onSecretOverride = () => { if (isOverridden) { // when user created a new override but then removes @@ -223,23 +200,14 @@ export const SecretInputRow = memo( }; const onSelectTag = (selectedTag: WsTag) => { - const checkBoxSelected = !selectedTagIds[selectedTag.slug] - checkBoxSelected && handlePopUpOpen('designTag') - setSelectedTag(selectedTag) - }; - - const onDesignWsTag = (_tagDesignObj: TagDesign) => { - setTagDesignObj(() => (_tagDesignObj)) - handlePopUpClose("designTag"); const shouldAppend = !selectedTagIds[selectedTag.slug]; if (shouldAppend) { - append({...selectedTag, ..._tagDesignObj}); + append(selectedTag); } else { - const pos = tags.findIndex(({ slug }: {slug: string}) => selectedTag.slug === slug); + const pos = tags.findIndex(({ slug }: { slug: string }) => selectedTag.slug === slug); remove(pos); } - } - + }; const isCreatedSecret = !secId; const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly; @@ -261,27 +229,13 @@ export const SecretInputRow = memo( return <>; } - + return (
{index + 1}
- {/* Add a custom design to new tag to make visible */} - { - handlePopUpToggle("designTag", open); - }} - > - - - -
- {secretTags.map(({ id, _id, slug, tagBackground, tagLabel }: SecretTags, i: number) => { - // This map lookup shouldn't ever fail, but if it does we default to the first color - const tagColor = tagColorByTagId.get(_id) || tagColors[0] + {secretTags.map(({ id, _id, slug, tagColor }: SecretTags, i: number) => { return ( - remove(i)} - key={id} - > - {slug} - ) + + +
+ remove(i)} + key={id} + className="cursor-pointer" + > +
+
+ {slug} +
+ +
+
+
+ +
+ Add tags to {secKey || "this secret"} +
+
+ {wsTags?.map((wsTag) => ( + + ))} + +
+
+
+ + ) })}
@@ -433,25 +430,22 @@ export const SecretInputRow = memo( -
+
Add tags to {secKey || "this secret"}
-
+
{wsTags?.map((wsTag) => ( + + - - - -
- - + +
+ ); }; diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx deleted file mode 100644 index 6993471e9..000000000 --- a/frontend/src/views/DashboardPage/components/DesignTagModal/DesignTagModal.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { yupResolver } from "@hookform/resolvers/yup"; -import * as yup from "yup"; - -import { Button, FormControl, Input, ModalClose, Tooltip, IconButton, Tag } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; -import { TagDesign } from "~/hooks/api/tags/types"; - -import { - faEye, - faEyeSlash -} from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useState } from 'react'; -import { WsTag } from '../../../../hooks/api/tags/types'; - -type TagData = { - tagBackground: string; - tagLabel: string -} - -type Props = { - onDesignTag: (tagData: TagData) => void; - selectedTag: WsTag -}; - -const designTagSchema = yup.object({ - tagBackground: yup.string().required().trim().label("Tag Background"), - tagLabel: yup.string().required().trim().label("Tag Label"), -}); -type FormData = yup.InferType; - - -export const DesignTagModal = ({ onDesignTag, selectedTag }: Props): JSX.Element => { - const [tagDesignObj, setTagDesignObj] = useState({ - tagColor: { - bg: "", - text: "" - } - }) - - const { - control, - reset, - formState, - handleSubmit, - setValue - } = useForm({ - resolver: yupResolver(designTagSchema) - }); - - const onFormSubmit = ({ tagBackground, tagLabel }: FormData) => { - onDesignTag({ tagBackground, tagLabel }); - reset(); - }; - - const [previewTag, setPreviewTag] = useToggle(false); - - const handleInputChange = (e: React.ChangeEvent, type: string) => { - setTagDesignObj((prev: { tagColor: { bg: string, text: string }; }) => ({ - tagColor: { - ...prev.tagColor, - [type]: e.target.value - } - })) - if (type === 'bg') { - setValue('tagBackground', e.target.value) - } else { - setValue('tagLabel', e.target.value) - } - } - - return ( -
-
- { - return ( - <> - - ) => handleInputChange(e, 'bg')} /> - - - ) - }} - /> - {/* */} -
- setPreviewTag.toggle()} className="absolute top-[2px] left-[127px] cursor-pointer" /> - {previewTag && ( - void (0)} - key={selectedTag._id} - className="absolute top-[-5px] right-[-5px] cursor-pointer" - > - {selectedTag.slug} - - )} - -
- {/*
*/} -
- - { - return ( - <> - - ) => handleInputChange(e, 'text')} value={tagDesignObj.tagColor.text} /> - - - ) - }} - /> -
- - - - -
- - ); -}; diff --git a/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx b/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx deleted file mode 100644 index 165e8e3a3..000000000 --- a/frontend/src/views/DashboardPage/components/DesignTagModal/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {DesignTagModal} from "./DesignTagModal" \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx index 9b70b936e..cdb95864a 100644 --- a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx +++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/jsx-no-useless-fragment */ -import { memo, useEffect, useRef, useState } from "react"; +import { memo, useEffect,useRef, useState } from "react"; import { Control, Controller, @@ -15,7 +15,6 @@ import { faCopy, faEllipsis, faInfoCircle, - faPlus, faTags, faXmark } from "@fortawesome/free-solid-svg-icons"; @@ -24,41 +23,21 @@ import { cx } from "cva"; import { twMerge } from "tailwind-merge"; import { - Button, - Checkbox, - FormControl, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, Input, Popover, - PopoverContent, PopoverTrigger, SecretInput, Tag, - TextArea, - Tooltip, - Modal, - ModalContent, -} from "@app/components/v2"; - + Tooltip} from "@app/components/v2"; +import { useToggle } from "@app/hooks"; import { WsTag } from "@app/hooks/api/types"; +import AddTagPopoverContent from "../../../../components/AddTagPopoverContent/AddTagPopoverContent"; import { FormData, SecretActionType } from "../../DashboardPage.utils"; -import { SecretTags } from "~/hooks/api/tags/types"; -import { useToggle } from "@app/hooks"; - -const tagColors = [ - { bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" }, - { bg: "bg-[#cb1c8d]/40", text: "text-[#f2c6e3]/70" }, - { bg: "bg-[#badc58]/40", text: "text-[#eef6d5]/70" }, - { bg: "bg-[#ff5400]/40", text: "text-[#ffddcc]/70" }, - { bg: "bg-[#3AB0FF]/40", text: "text-[#f0fffd]/70" }, - { bg: "bg-[#6F1AB6]/40", text: "text-[#FFE5F1]/70" }, - { bg: "bg-[#C40B13]/40", text: "text-[#FFDEDE]/70" }, - { bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" } -]; type Props = { index: number; @@ -77,7 +56,6 @@ type Props = { // tag props wsTags?: WsTag[]; onCreateTagOpen: () => void; - onDesignTagOpen: (selectedTag: WsTag, selectedFieldIndex: number) => void; // rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering control: Control; register: UseFormRegister; @@ -97,11 +75,10 @@ export const SecretInputRow = memo( isAddOnly, wsTags, onCreateTagOpen, - onDesignTagOpen, onSecretDelete, searchTerm, control, - register, + // register, setValue, isKeyError, keyError, @@ -116,8 +93,6 @@ export const SecretInputRow = memo( append } = useFieldArray({ control, name: `secrets.${index}.tags` }); - const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]])) - // display the tags in alphabetical order secretTags.sort((a, b) => a?.name?.localeCompare(b?.name)) @@ -152,7 +127,19 @@ export const SecretInputRow = memo( const isOverridden = overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; + const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue); + const [hoveredTag, setHoveredTag] = useState(null); + + const handleTagOnMouseEnter = (wsTag: WsTag) => { + setHoveredTag(wsTag); + } + + const handleTagOnMouseLeave = () => { + setHoveredTag(null); + } + + const checkIfTagIsVisible = (wsTag: WsTag) => wsTag._id === hoveredTag?._id; const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true }); const tags = useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || []; @@ -164,6 +151,7 @@ export const SecretInputRow = memo( const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false); + useEffect(() => { let timer: NodeJS.Timeout; if (isInviteLinkCopied) { @@ -172,6 +160,7 @@ export const SecretInputRow = memo( return () => clearTimeout(timer); }, [isInviteLinkCopied]); + useEffect(() => { setEditorRef(isOverridden ? secValueOverride : secValue); }, [isOverridden]); @@ -202,7 +191,8 @@ export const SecretInputRow = memo( const onSelectTag = (selectedTag: WsTag) => { const shouldAppend = !selectedTagIds[selectedTag.slug]; if (shouldAppend) { - append(selectedTag); + const {_id: id, name, slug, tagColor} = selectedTag + append({_id: id, name, slug, tagColor}); } else { const pos = tags.findIndex(({ slug }: { slug: string }) => selectedTag.slug === slug); remove(pos); @@ -229,8 +219,6 @@ export const SecretInputRow = memo( return <>; } - - return ( @@ -335,67 +323,37 @@ export const SecretInputRow = memo(
- {secretTags.map(({ id, _id, slug, tagColor }: SecretTags, i: number) => { + {secretTags.map(({ id, slug, tagColor}) => { return ( - + <> +
remove(i)} + // isDisabled={isReadOnly || isAddOnly || isRollbackMode} + // onClose={() => remove(i)} key={id} className="cursor-pointer" > -
-
+
+
{slug}
-
- -
- Add tags to {secKey || "this secret"} -
-
- {wsTags?.map((wsTag) => ( - - ))} - -
-
+ onSelectTag(wsTag)} + handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} + handleTagOnMouseLeave={() => handleTagOnMouseLeave()} + checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} + handleOnCreateTagOpen={() => onCreateTagOpen()} + /> - + ) })}
@@ -428,46 +386,16 @@ export const SecretInputRow = memo(
- -
- Add tags to {secKey || "this secret"} -
-
- {wsTags?.map((wsTag) => ( - - ))} - -
-
+ onSelectTag(wsTag)} + handleTagOnMouseEnter={(wsTag: WsTag) => handleTagOnMouseEnter(wsTag)} + handleTagOnMouseLeave={() => handleTagOnMouseLeave()} + checkIfTagIsVisible={(wsTag: WsTag) => checkIfTagIsVisible(wsTag)} + handleOnCreateTagOpen={() => onCreateTagOpen()} + />
)} @@ -511,20 +439,16 @@ export const SecretInputRow = memo( - - -