Improve Tag feature with better error handling and animations (#449)

* fix: adds animation on duplicate tag

* fix: fixes error data flow

* fix: fixes tag getting animated on all errors

* fix: changes icon to heroicons

* fix: fixes error being thrown when adding duplicate tag

* fix: fixes responses not getting refetched
This commit is contained in:
Anshuman Pandey
2023-06-30 21:10:25 +05:30
committed by GitHub
parent f7aea59f80
commit 46b7183161
6 changed files with 56 additions and 52 deletions

View File

@@ -1,10 +1,13 @@
"use client";
import TagsCombobox from "@/app/environments/[environmentId]/surveys/[surveyId]/responses/TagsCombobox";
import { useResponses } from "@/lib/responses/responses";
import { removeTagFromResponse, useAddTagToResponse, useCreateTag } from "@/lib/tags/mutateTags";
import { useTagsForEnvironment } from "@/lib/tags/tags";
import React, { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { Tag } from "./Tag";
import { ExclamationCircleIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
interface ResponseTagsWrapperProps {
tags: {
@@ -22,13 +25,13 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
responseId,
surveyId,
}) => {
const router = useRouter();
const [searchValue, setSearchValue] = useState("");
const [open, setOpen] = React.useState(false);
const [tagsState, setTagsState] = useState(tags);
const [tagIdToHighlight, setTagIdToHighlight] = useState("");
const { createTag } = useCreateTag(environmentId);
const { mutateResponses } = useResponses(environmentId, surveyId);
const { data: environmentTags, mutate: refetchEnvironmentTags } = useTagsForEnvironment(environmentId);
const { addTagToRespone } = useAddTagToResponse(environmentId, surveyId, responseId);
@@ -36,9 +39,10 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
try {
await removeTagFromResponse(environmentId, surveyId, responseId, tagId);
mutateResponses();
router.refresh();
} catch (e) {
console.log(e);
toast.error("An error occurred deleting the tag");
router.refresh();
}
};
@@ -96,27 +100,36 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
onSuccess: () => {
setSearchValue("");
setOpen(false);
mutateResponses();
refetchEnvironmentTags();
router.refresh();
},
}
);
},
onError: (err) => {
toast.error(err?.message ?? "Something went wrong", {
duration: 2000,
});
if (err?.cause === "DUPLICATE_RECORD") {
toast.error(err?.message ?? "Something went wrong", {
duration: 2000,
icon: <ExclamationCircleIcon className="h-5 w-5 text-orange-500" />,
});
const tag = tags.find((tag) => tag.tagName === tagName?.trim() ?? "");
setTagIdToHighlight(tag?.tagId ?? "");
} else {
toast.error(err?.message ?? "Something went wrong", {
duration: 2000,
});
}
setSearchValue("");
setOpen(false);
mutateResponses();
const tag = tags.find((tag) => tag.tagName === tagName?.trim() ?? "");
setTagIdToHighlight(tag?.tagId ?? "");
refetchEnvironmentTags();
router.refresh();
},
throwOnError: false,
}
);
}}
@@ -137,9 +150,9 @@ const ResponseTagsWrapper: React.FC<ResponseTagsWrapperProps> = ({
onSuccess: () => {
setSearchValue("");
setOpen(false);
mutateResponses();
refetchEnvironmentTags();
router.refresh();
},
}
);

View File

@@ -35,7 +35,7 @@ export function Tag({
key={tagId}
className={cn(
"relative flex items-center justify-between gap-2 rounded-full border bg-slate-600 px-2 py-1 text-slate-100",
highlight && "border-2 border-green-600"
highlight && "animate-shake"
)}>
<div className="flex items-center gap-2">
<span className="text-sm">{tagName}</span>

View File

@@ -15,6 +15,13 @@ export const useCreateTag = (environmentId: string) => {
if (!response.ok) {
const errorData = await response.json();
if (errorData?.duplicateRecord) {
throw new Error("Tag already assigned", {
cause: "DUPLICATE_RECORD",
});
}
throw new Error(errorData.message);
}

View File

@@ -1,7 +1,5 @@
import { hasEnvironmentAccess, getSessionUser } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database/src/client";
import { TTag } from "@formbricks/types/v1/tags";
import { Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
@@ -25,9 +23,9 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.status(403).json({ message: "You are not authorized to access this environment! " });
}
// GET /api/environments/[environmentId]/tags
// GET /api/environments/[environmentId]/tags/count
// Get all tags for an environment
// Get the count of tags on responses
if (req.method === "GET") {
let tagsCounts;
@@ -46,38 +44,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.json(tagsCounts);
}
// POST /api/environments/[environmentId]/tags
// Create a new tag for a product
if (req.method === "POST") {
const name = req.body.name;
if (!name) {
return res.status(400).json({ message: "Invalid name" });
}
let tag: TTag;
try {
tag = await prisma.tag.create({
data: {
name,
environmentId,
},
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
return res.status(400).json({ message: "Tag already exists" });
}
}
return res.status(500).json({ message: "Internal Server Error" });
}
return res.json(tag);
}
// Unknown HTTP Method
else {
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);

View File

@@ -70,7 +70,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
return res.status(400).json({ message: "Tag already exists" });
return res.status(400).json({ duplicateRecord: true });
}
}
return res.status(500).json({ message: "Internal Server Error" });

View File

@@ -12,6 +12,7 @@ module.exports = {
extend: {
animation: {
"ping-slow": "ping 2s cubic-bezier(0, 0, 0.2, 1) infinite",
shake: "shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both",
},
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
@@ -31,6 +32,23 @@ module.exports = {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
shake: {
"10%, 90%": {
transform: "translate3d(-1px, 0, 0)",
},
"20%, 80%": {
transform: "translate3d(2px, 0, 0),",
},
"30%, 50%, 70%": {
transform: "translate3d(-4px, 0, 0)",
},
"40%, 60%": {
transform: "translate3d(4px, 0, 0)",
},
},
},
maxWidth: {
"8xl": "88rem",