mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 02:10:12 -06:00
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:
@@ -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();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user