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) => (