feat: added custom design for tags

This commit is contained in:
Ebezer Igbinoba
2023-08-20 10:02:40 +01:00
parent 4a48c088df
commit 66ea3ba172
5 changed files with 225 additions and 13 deletions

View File

@ -9,6 +9,7 @@ type Props = {
className?: string;
onClose?: () => void;
color?: string;
styles?: Record<string, string>
isDisabled?: boolean;
} & VariantProps<typeof tagVariants>;
@ -35,11 +36,12 @@ export const Tag = ({
color,
isDisabled,
size = "sm",
onClose
onClose,
styles = {}
}: Props) => (
<div
className={twMerge(tagVariants({ colorSchema, className, size }))}
style={{ backgroundColor: color }}
style={{ backgroundColor: color, ...styles }}
>
{children}
{onClose && (

View File

@ -36,4 +36,17 @@ export type DeleteWsTagRes = {
createdAt: string;
user: string;
_id: string;
};
};
export type TagDesign = {
tagBackground: string;
tagLabel: string
}
export type SecretTags = {
id: string;
_id: string;
slug: string;
tagBackground: string;
tagLabel: string
}

View File

@ -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<typeof designTagSchema>;
export const DesignTagModal = ({ onDesignTag, selectedTag }: Props): JSX.Element => {
const [tagDesignObj, setTagDesignObj] = useState({
tagColor: {
bg: "",
text: ""
}
})
const {
control,
reset,
formState,
handleSubmit,
setValue
} = useForm<FormData>({
resolver: yupResolver(designTagSchema)
});
const onFormSubmit = ({ tagBackground, tagLabel }: FormData) => {
onDesignTag({ tagBackground, tagLabel });
reset();
};
const [previewTag, setPreviewTag] = useToggle(false);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>, 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 (
<form onSubmit={handleSubmit(onFormSubmit)}>
<div className="relative">
<Controller
control={control}
name="tagBackground"
defaultValue=""
render={({ field, fieldState: { error } }) => {
return (
<>
<FormControl label="Tag Background" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} type="color" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange(e, 'bg')} />
</FormControl>
</>
)
}}
/>
{/* <Tooltip content={previewTag ? "Hide Preview" : "Show Preview"}> */}
<div >
<FontAwesomeIcon icon={previewTag ? faEye : faEyeSlash} onClick={() => setPreviewTag.toggle()} className="absolute top-[2px] left-[127px] cursor-pointer" />
{previewTag && (
<Tag
styles={{
backgroundColor: tagDesignObj.tagColor.bg,
color: tagDesignObj.tagColor.text
}}
isDisabled={true}
onClose={() => void (0)}
key={selectedTag._id}
className="absolute top-[-5px] right-[-5px] cursor-pointer"
>
{selectedTag.slug}
</Tag>
)}
</div>
{/* </Tooltip> */}
</div>
<Controller
control={control}
name="tagLabel"
defaultValue="#000000"
render={({ field, fieldState: { error } }) => {
return (
<>
<FormControl label="Tag Label" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} type="color" onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleInputChange(e, 'text')} value={tagDesignObj.tagColor.text} />
</FormControl>
</>
)
}}
/>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isDisabled={formState.isSubmitting} isLoading={formState.isSubmitting}>
Save
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};

View File

@ -0,0 +1 @@
export {DesignTagModal} from "./DesignTagModal"

View File

@ -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<FormData>;
register: UseFormRegister<FormData>;
@ -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<boolean>(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<TagDesign & WsTag>({})
const [selectedTag, setSelectedTag] = useState<WsTag>({})
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 (
<tr className="group flex flex-row hover:bg-mineshaft-700" key={index}>
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
</td>
{/* Add a custom design to new tag to make visible */}
<Modal
isOpen={popUp?.designTag?.isOpen}
onOpenChange={(open: boolean) => {
handlePopUpToggle("designTag", open);
}}
>
<ModalContent
title={`Customise design for ${selectedTag.slug}`}
subTitle="Choose custom background and label text colors for the tag."
>
<DesignTagModal selectedTag={selectedTag} onDesignTag={onDesignWsTag} />
</ModalContent>
</Modal>
<Controller
control={control}
defaultValue=""
@ -326,7 +381,7 @@ export const SecretInputRow = memo(
</td>
<td className="min-w-sm flex">
<div className="flex h-8 items-center pl-2">
{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={() => {}}
>
{}
{ }
</Checkbox>
}
key={wsTag._id}