mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 17:26:19 +00:00
feat: added custom design for tags
This commit is contained in:
@@ -9,6 +9,7 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
styles?: Record<string, string>
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
} & VariantProps<typeof tagVariants>;
|
} & VariantProps<typeof tagVariants>;
|
||||||
|
|
||||||
@@ -35,11 +36,12 @@ export const Tag = ({
|
|||||||
color,
|
color,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
onClose
|
onClose,
|
||||||
|
styles = {}
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div
|
<div
|
||||||
className={twMerge(tagVariants({ colorSchema, className, size }))}
|
className={twMerge(tagVariants({ colorSchema, className, size }))}
|
||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: color, ...styles }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{onClose && (
|
{onClose && (
|
||||||
|
|||||||
@@ -37,3 +37,16 @@ export type DeleteWsTagRes = {
|
|||||||
user: string;
|
user: string;
|
||||||
_id: string;
|
_id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TagDesign = {
|
||||||
|
tagBackground: string;
|
||||||
|
tagLabel: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SecretTags = {
|
||||||
|
id: string;
|
||||||
|
_id: string;
|
||||||
|
slug: string;
|
||||||
|
tagBackground: string;
|
||||||
|
tagLabel: string
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {DesignTagModal} from "./DesignTagModal"
|
||||||
@@ -38,12 +38,17 @@ import {
|
|||||||
SecretInput,
|
SecretInput,
|
||||||
Tag,
|
Tag,
|
||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useToggle } from "@app/hooks";
|
|
||||||
import { WsTag } from "@app/hooks/api/types";
|
import { WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
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 = [
|
const tagColors = [
|
||||||
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
||||||
@@ -73,6 +78,7 @@ type Props = {
|
|||||||
// tag props
|
// tag props
|
||||||
wsTags?: WsTag[];
|
wsTags?: WsTag[];
|
||||||
onCreateTagOpen: () => void;
|
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
|
// rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering
|
||||||
control: Control<FormData>;
|
control: Control<FormData>;
|
||||||
register: UseFormRegister<FormData>;
|
register: UseFormRegister<FormData>;
|
||||||
@@ -80,6 +86,9 @@ type Props = {
|
|||||||
isKeyError?: boolean;
|
isKeyError?: boolean;
|
||||||
keyError?: string;
|
keyError?: string;
|
||||||
autoCapitalization?: boolean;
|
autoCapitalization?: boolean;
|
||||||
|
designObj: TagDesign & WsTag;
|
||||||
|
updateDesign: boolean;
|
||||||
|
selectedFieldIndex: number
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretInputRow = memo(
|
export const SecretInputRow = memo(
|
||||||
@@ -92,6 +101,8 @@ export const SecretInputRow = memo(
|
|||||||
isAddOnly,
|
isAddOnly,
|
||||||
wsTags,
|
wsTags,
|
||||||
onCreateTagOpen,
|
onCreateTagOpen,
|
||||||
|
onDesignTagOpen,
|
||||||
|
designObj,
|
||||||
onSecretDelete,
|
onSecretDelete,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
control,
|
control,
|
||||||
@@ -100,7 +111,9 @@ export const SecretInputRow = memo(
|
|||||||
isKeyError,
|
isKeyError,
|
||||||
keyError,
|
keyError,
|
||||||
secUniqId,
|
secUniqId,
|
||||||
autoCapitalization
|
autoCapitalization,
|
||||||
|
updateDesign,
|
||||||
|
selectedFieldIndex
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const isKeySubDisabled = useRef<boolean>(false);
|
const isKeySubDisabled = useRef<boolean>(false);
|
||||||
// comment management in a row
|
// 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]]))
|
const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]]))
|
||||||
|
|
||||||
// display the tags in alphabetical order
|
// 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
|
// to get details on a secret
|
||||||
const overrideAction = useWatch({
|
const overrideAction = useWatch({
|
||||||
@@ -145,7 +158,10 @@ export const SecretInputRow = memo(
|
|||||||
// when secret is override by personal values
|
// when secret is override by personal values
|
||||||
const isOverridden =
|
const isOverridden =
|
||||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||||
|
|
||||||
const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue);
|
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 secId = useWatch({ control, name: `secrets.${index}._id`, exact: true });
|
||||||
const tags =
|
const tags =
|
||||||
@@ -174,6 +190,20 @@ export const SecretInputRow = memo(
|
|||||||
setInviteLinkCopied.on();
|
setInviteLinkCopied.on();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"secretDetails",
|
||||||
|
"addTag",
|
||||||
|
"secretSnapshots",
|
||||||
|
"uploadedSecOpts",
|
||||||
|
"compareSecrets",
|
||||||
|
"folderForm",
|
||||||
|
"deleteFolder",
|
||||||
|
"upgradePlan",
|
||||||
|
"addSecretImport",
|
||||||
|
"deleteSecretImport",
|
||||||
|
"designTag"
|
||||||
|
] as const);
|
||||||
|
|
||||||
const onSecretOverride = () => {
|
const onSecretOverride = () => {
|
||||||
if (isOverridden) {
|
if (isOverridden) {
|
||||||
// when user created a new override but then removes
|
// when user created a new override but then removes
|
||||||
@@ -193,14 +223,22 @@ export const SecretInputRow = memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSelectTag = (selectedTag: WsTag) => {
|
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];
|
const shouldAppend = !selectedTagIds[selectedTag.slug];
|
||||||
if (shouldAppend) {
|
if (shouldAppend) {
|
||||||
append(selectedTag);
|
append({...selectedTag, ..._tagDesignObj});
|
||||||
} else {
|
} else {
|
||||||
const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug);
|
const pos = tags.findIndex(({ slug }: {slug: string}) => selectedTag.slug === slug);
|
||||||
remove(pos);
|
remove(pos);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const isCreatedSecret = !secId;
|
const isCreatedSecret = !secId;
|
||||||
const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
|
const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
|
||||||
@@ -223,11 +261,28 @@ export const SecretInputRow = memo(
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="group flex flex-row hover:bg-mineshaft-700" key={index}>
|
<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">
|
<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>
|
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||||
</td>
|
</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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -326,7 +381,7 @@ export const SecretInputRow = memo(
|
|||||||
</td>
|
</td>
|
||||||
<td className="min-w-sm flex">
|
<td className="min-w-sm flex">
|
||||||
<div className="flex h-8 items-center pl-2">
|
<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
|
// This map lookup shouldn't ever fail, but if it does we default to the first color
|
||||||
const tagColor = tagColorByTagId.get(_id) || tagColors[0]
|
const tagColor = tagColorByTagId.get(_id) || tagColors[0]
|
||||||
return (
|
return (
|
||||||
@@ -335,6 +390,10 @@ export const SecretInputRow = memo(
|
|||||||
tagColor.bg,
|
tagColor.bg,
|
||||||
tagColor.text
|
tagColor.text
|
||||||
)}
|
)}
|
||||||
|
styles={{
|
||||||
|
backgroundColor: tagBackground,
|
||||||
|
color: tagLabel
|
||||||
|
}}
|
||||||
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||||
onClose={() => remove(i)}
|
onClose={() => remove(i)}
|
||||||
key={id}
|
key={id}
|
||||||
@@ -395,9 +454,8 @@ export const SecretInputRow = memo(
|
|||||||
className="mr-0 data-[state=checked]:bg-primary"
|
className="mr-0 data-[state=checked]:bg-primary"
|
||||||
id="autoCapitalization"
|
id="autoCapitalization"
|
||||||
isChecked={selectedTagIds?.[wsTag.slug]}
|
isChecked={selectedTagIds?.[wsTag.slug]}
|
||||||
onCheckedChange={() => {}}
|
|
||||||
>
|
>
|
||||||
{}
|
{ }
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
}
|
}
|
||||||
key={wsTag._id}
|
key={wsTag._id}
|
||||||
|
|||||||
Reference in New Issue
Block a user