Skip to content

Commit c5eef87

Browse files
feat : Added create tag option in dropdown (ohcnetwork#11296)
Co-authored-by: Jacob John Jeevan <mail@jacobjeevan.me>
1 parent 08c9bc3 commit c5eef87

File tree

7 files changed

+628
-412
lines changed

7 files changed

+628
-412
lines changed

public/locale/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,7 @@
828828
"create_schedule_template": "Create Schedule Template",
829829
"create_summaries": "Create Summaries",
830830
"create_tag": "Create Tag",
831+
"create_tag_description": "Create a new tag for the questionnaire",
831832
"create_template": "Create Template",
832833
"create_user": "Create User",
833834
"create_user_and_add_to_org": "Create a new user and add them to the organization.",
@@ -1711,6 +1712,7 @@
17111712
"my_profile": "My Profile",
17121713
"my_schedules": "My Schedules",
17131714
"name": "Name",
1715+
"name_and_slug_are_required": "Name and slug are required",
17141716
"name_is_required": "Name is required",
17151717
"name_of_hospital": "Name of Hospital",
17161718
"name_of_shifting_approving_facility": "Name of shifting approving facility",
@@ -2630,8 +2632,10 @@
26302632
"system": "System",
26312633
"systolic": "Systolic",
26322634
"tachycardia": "Tachycardia",
2635+
"tag_created_successfully": "Tag created successfully",
26332636
"tag_name": "Tag Name",
26342637
"tag_slug": "Tag Slug",
2638+
"tag_updated_successfully": "Tag updated successfully",
26352639
"tags": "Tags",
26362640
"taken": "Taken",
26372641
"taper_titrate_dosage": "Taper & Titrate Dosage",

src/components/Questionnaire/CloneQuestionnaireSheet.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export default function CloneQuestionnaireSheet({
9595
status: "draft" as const,
9696
title: `${questionnaire.title} (Clone)`,
9797
organizations: selectedIds,
98+
tags: questionnaire.tags.map((tag) => tag.id),
9899
};
99100

100101
cloneQuestionnaire(clonedQuestionnaire);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
import { Loader2, Plus } from "lucide-react";
3+
import { useState } from "react";
4+
import { useTranslation } from "react-i18next";
5+
import { toast } from "sonner";
6+
7+
import { Button } from "@/components/ui/button";
8+
import { Input } from "@/components/ui/input";
9+
import { Label } from "@/components/ui/label";
10+
import {
11+
Sheet,
12+
SheetContent,
13+
SheetDescription,
14+
SheetHeader,
15+
SheetTitle,
16+
SheetTrigger,
17+
} from "@/components/ui/sheet";
18+
19+
import mutate from "@/Utils/request/mutate";
20+
import questionnaireApi from "@/types/questionnaire/questionnaireApi";
21+
import { QuestionnaireTagModel } from "@/types/questionnaire/tags";
22+
23+
interface Props {
24+
trigger: React.ReactNode;
25+
onTagCreated: (tag: QuestionnaireTagModel) => void;
26+
}
27+
28+
export default function CreateQuestionnaireTagSheet({
29+
trigger,
30+
onTagCreated,
31+
}: Props) {
32+
const queryClient = useQueryClient();
33+
const { t } = useTranslation();
34+
const [open, setOpen] = useState(false);
35+
const [newTagName, setNewTagName] = useState("");
36+
const [newTagSlug, setNewTagSlug] = useState("");
37+
38+
const { mutate: createTag, isPending: isCreating } = useMutation({
39+
mutationFn: mutate(questionnaireApi.tags.create),
40+
onSuccess: (data: unknown) => {
41+
const tagData = data as QuestionnaireTagModel;
42+
queryClient.invalidateQueries({
43+
queryKey: ["questionnaire_tags"],
44+
});
45+
setNewTagName("");
46+
setNewTagSlug("");
47+
setOpen(false);
48+
onTagCreated?.(tagData);
49+
toast.success(t("tag_created_successfully"));
50+
},
51+
});
52+
53+
const handleCreateTag = () => {
54+
if (!newTagName.trim() || !newTagSlug.trim()) {
55+
toast.error(t("name_and_slug_are_required"));
56+
return;
57+
}
58+
59+
createTag({
60+
name: newTagName.trim(),
61+
slug: newTagSlug.trim(),
62+
});
63+
};
64+
65+
return (
66+
<Sheet open={open} onOpenChange={setOpen}>
67+
<SheetTrigger asChild>
68+
{trigger || (
69+
<Button variant="outline" size="sm">
70+
<Plus className="mr-2 h-4 w-4" />
71+
{t("create_tag")}
72+
</Button>
73+
)}
74+
</SheetTrigger>
75+
<SheetContent>
76+
<SheetHeader>
77+
<SheetTitle>{t("create_tag")}</SheetTitle>
78+
<SheetDescription>{t("create_tag_description")}</SheetDescription>
79+
</SheetHeader>
80+
81+
<div className="space-y-6 py-4">
82+
<div className="space-y-4">
83+
<div className="space-y-2">
84+
<Label htmlFor="tag-name">{t("tag_name")}</Label>
85+
<Input
86+
id="tag-name"
87+
value={newTagName}
88+
onChange={(e) => setNewTagName(e.target.value)}
89+
placeholder={t("enter_tag_name")}
90+
/>
91+
</div>
92+
<div className="space-y-2">
93+
<Label htmlFor="tag-slug">{t("tag_slug")}</Label>
94+
<Input
95+
id="tag-slug"
96+
value={newTagSlug}
97+
onChange={(e) => setNewTagSlug(e.target.value)}
98+
placeholder={t("enter_tag_slug")}
99+
/>
100+
</div>
101+
<Button
102+
onClick={handleCreateTag}
103+
disabled={isCreating || !newTagName || !newTagSlug}
104+
className="w-full"
105+
>
106+
{isCreating ? (
107+
<>
108+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
109+
{t("creating")}
110+
</>
111+
) : (
112+
t("create_tag")
113+
)}
114+
</Button>
115+
</div>
116+
</div>
117+
</SheetContent>
118+
</Sheet>
119+
);
120+
}

src/components/Questionnaire/ManageQuestionnaireTagsSheet.tsx

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22
import { Check, Hash, Loader2, Plus, X } from "lucide-react";
3-
import { useEffect, useState } from "react";
3+
import { useEffect, useMemo, useState } from "react";
44
import { useTranslation } from "react-i18next";
55
import { toast } from "sonner";
66

@@ -56,18 +56,19 @@ export default function ManageQuestionnaireTagsSheet({
5656
const queryClient = useQueryClient();
5757
const { t } = useTranslation();
5858
const [open, setOpen] = useState(false);
59+
const [popOverOpen, setPopOverOpen] = useState(false);
5960
const [searchQuery, setSearchQuery] = useState("");
60-
const [selectedSlugs, setSelectedSlugs] = useState<string[]>([]);
6161
const [isCreateOpen, setIsCreateOpen] = useState(false);
6262
const [newTagName, setNewTagName] = useState("");
6363
const [newTagSlug, setNewTagSlug] = useState("");
64+
const [selectedTags, setSelectedTags] = useState<QuestionnaireTagModel[]>([]);
6465

6566
const { data: availableTags, isLoading } = useQuery({
6667
queryKey: ["questionnaire_tags", searchQuery],
6768
queryFn: query.debounced(questionnaireApi.tags.list, {
6869
queryParams: searchQuery !== "" ? { name: searchQuery } : undefined,
6970
}),
70-
enabled: open,
71+
enabled: popOverOpen,
7172
});
7273

7374
const { mutate: setTags, isPending: isUpdating } = useMutation({
@@ -78,7 +79,7 @@ export default function ManageQuestionnaireTagsSheet({
7879
queryClient.invalidateQueries({
7980
queryKey: ["questionnaireDetail", questionnaire.slug],
8081
});
81-
toast.success("Tags updated successfully");
82+
toast.success(t("tag_updated_successfully"));
8283
setOpen(false);
8384
},
8485
});
@@ -90,36 +91,56 @@ export default function ManageQuestionnaireTagsSheet({
9091
queryClient.invalidateQueries({
9192
queryKey: ["questionnaire_tags"],
9293
});
93-
setSelectedSlugs((current) => [...current, tagData.slug]);
94+
setSelectedTags((current) => [...current, tagData]);
9495
setNewTagName("");
9596
setNewTagSlug("");
9697
setIsCreateOpen(false);
97-
toast.success("Tag created successfully");
98+
toast.success(t("tag_created_successfully"));
9899
},
99100
});
100101

101-
// Initialize selected slugs from questionnaire tags
102+
// Initialize selected tags from questionnaire tags
102103
useEffect(() => {
103104
if (questionnaire.tags) {
104-
setSelectedSlugs(questionnaire.tags.map((tag) => tag.slug));
105+
setSelectedTags(questionnaire.tags);
105106
}
106107
}, [questionnaire.tags]);
107108

108-
const handleToggleTag = (tagSlug: string) => {
109-
setSelectedSlugs((current) =>
110-
current.includes(tagSlug)
111-
? current.filter((slug) => slug !== tagSlug)
112-
: [...current, tagSlug],
109+
// Simple merge of selected tags with available tags
110+
const tagOptions = useMemo(() => {
111+
if (!availableTags?.results) return selectedTags;
112+
if (searchQuery) return availableTags.results;
113+
114+
const availableSlugs = new Set(
115+
availableTags.results.map((tag) => tag.slug),
116+
);
117+
118+
// Add selected tags that aren't in availableTags
119+
const selectedNotInAvailable = selectedTags.filter(
120+
(selectedTag) => !availableSlugs.has(selectedTag.slug),
113121
);
122+
123+
return [...availableTags.results, ...selectedNotInAvailable];
124+
}, [availableTags, selectedTags, searchQuery]);
125+
126+
const handleToggleTag = (tagSlug: string) => {
127+
setSelectedTags((current) => {
128+
const newTag = tagOptions?.find((tag) => tag.slug === tagSlug);
129+
return current.some((tag) => tag.slug === tagSlug)
130+
? current.filter((tag) => tag.slug !== tagSlug)
131+
: newTag
132+
? [...current, newTag]
133+
: current;
134+
});
114135
};
115136

116137
const handleSave = () => {
117-
setTags({ tags: selectedSlugs });
138+
setTags({ tags: selectedTags.map((tag) => tag.slug) });
118139
};
119140

120141
const handleCreateTag = () => {
121142
if (!newTagName.trim() || !newTagSlug.trim()) {
122-
toast.error("Name and slug are required");
143+
toast.error(t("name_and_slug_are_required"));
123144
return;
124145
}
125146

@@ -129,14 +150,12 @@ export default function ManageQuestionnaireTagsSheet({
129150
});
130151
};
131152

132-
const selectedTags = availableTags?.results.filter((tag) =>
133-
selectedSlugs.includes(tag.slug),
134-
);
135-
136153
const hasChanges =
137154
new Set(questionnaire.tags.map((tag) => tag.slug)).size !==
138-
new Set(selectedSlugs).size ||
139-
!questionnaire.tags.every((tag) => selectedSlugs.includes(tag.slug));
155+
new Set(selectedTags).size ||
156+
!questionnaire.tags.every((tag) =>
157+
selectedTags.some((st) => st.slug === tag.slug),
158+
);
140159

141160
return (
142161
<Sheet open={open} onOpenChange={setOpen}>
@@ -186,7 +205,16 @@ export default function ManageQuestionnaireTagsSheet({
186205
{/* Tag Selector */}
187206
<div className="space-y-4">
188207
<h3 className="text-sm font-medium">{t("add_tags")}</h3>
189-
<Popover modal={true}>
208+
<Popover
209+
modal={true}
210+
open={popOverOpen}
211+
onOpenChange={(open) => {
212+
if (!open) {
213+
setSearchQuery("");
214+
}
215+
setPopOverOpen(open);
216+
}}
217+
>
190218
<PopoverTrigger asChild>
191219
<Button
192220
variant="outline"
@@ -214,7 +242,7 @@ export default function ManageQuestionnaireTagsSheet({
214242
<Loader2 className="h-6 w-6 animate-spin" />
215243
</div>
216244
) : (
217-
availableTags?.results?.map((tag) => (
245+
tagOptions?.map((tag) => (
218246
<CommandItem
219247
key={tag.slug}
220248
value={tag.slug}
@@ -224,7 +252,7 @@ export default function ManageQuestionnaireTagsSheet({
224252
<Hash className="h-4 w-4" />
225253
<span>{tag.name}</span>
226254
</div>
227-
{selectedSlugs.includes(tag.slug) && (
255+
{selectedTags.some((t) => t.slug === tag.slug) && (
228256
<Check className="h-4 w-4" />
229257
)}
230258
</CommandItem>
@@ -301,7 +329,7 @@ export default function ManageQuestionnaireTagsSheet({
301329
<Button
302330
variant="outline"
303331
onClick={() => {
304-
setSelectedSlugs(questionnaire.tags.map((tag) => tag.slug));
332+
setSelectedTags(questionnaire.tags);
305333
setOpen(false);
306334
}}
307335
>

0 commit comments

Comments
 (0)