1<template>
2 <SettingsLayoutBase
3 :title="__('Teams')"
4 :description="
5 __('Create and manage teams and assign agents to specific teams.')
6 "
7 >
8 <template #header-actions>
9 <Button
10 :label="__('New')"
11 class="rtl:flex-row-reverse"
12 theme="gray"
13 variant="solid"
14 @click="emit('update:step', 'new-team', '')"
15 icon-left="lucide-plus"
16 />
17 </template>
18 <template
19 v-if="teams.data?.length > 9 || teamsSearchQuery.length"
20 #header-bottom
21 >
22 <div class="relative">
23 <TextInput
24 :model-value="teamsSearchQuery"
25 @update:model-value="teamsSearchQuery = $event"
26 :placeholder="__('Search')"
27 type="text"
28 class="focus:ring-0 border-outline-gray-2"
29 :debounce="300"
30 >
31 <template #prefix>
32 <LucideSearch class="size-4" />
33 </template>
34 </TextInput>
35 <Button
36 v-if="teamsSearchQuery"
37 icon="lucide-x"
38 variant="ghost"
39 @click="teamsSearchQuery = ''"
40 class="absolute end-1 top-1/2 -translate-y-1/2"
41 />
42 </div>
43 </template>
44 <template #content>
45 <!-- List -->
46 <div
47 v-if="!teams.loading && teams.data?.length > 0"
48 class="w-full h-full -ms-2"
49 >
50 <div class="flex text-sm text-ink-gray-5">
51 <p class="ms-2">{{ __("Team name") }}</p>
52 </div>
53 <hr class="mx-2 mt-2" />
54 <div v-for="(team, index) in teams.data" :key="team.name">
55 <div
56 class="flex items-center cursor-pointer hover:bg-surface-sidebar rounded-4 h-12.5"
57 >
58 <div
59 class="w-full py-3 ps-2 flex gap-1 items-center"
60 @click="() => emit('update:step', 'team-edit', team.name)"
61 >
62 <p class="text-base-medium text-ink-gray-7">
63 {{ team.name }}
64 </p>
65 <Badge :label="__('Disabled')" v-if="team.disabled" />
66 </div>
67 <div class="flex justify-between items-center pe-2">
68 <div>
69 <Dropdown
70 align="end"
71 :options="dropdownOptions(team)"
72 @update:open="(open) => open && (isConfirmingDelete = false)"
73 >
74 <Button icon="lucide-more-horizontal" variant="ghost" />
75 </Dropdown>
76 </div>
77 </div>
78 </div>
79 <hr v-if="index !== teams.data.length - 1" class="mx-2" />
80 </div>
81 <!-- Load More Button -->
82 <div class="flex justify-center">
83 <Button
84 v-if="!teams.loading && teams.hasNextPage"
85 class="mt-3.5 p-2"
86 @click="() => teams.next()"
87 :loading="teams.loading"
88 :label="__('Load More')"
89 icon-left="lucide-refresh-cw"
90 />
91 </div>
92 </div>
93 <!-- Loading State -->
94 <div
95 v-if="teams.loading"
96 class="flex mt-28 justify-between w-full h-full"
97 >
98 <Button
99 :loading="teams.loading"
100 variant="ghost"
101 class="w-full"
102 size="lg"
103 />
104 </div>
105 <!-- Empty State -->
106 <EmptyState
107 v-if="!teams.loading && !teams.data?.length"
108 variant="badge"
109 :icon="AgentIcon"
110 title="No team found"
111 :description="
112 teamsSearchQuery.length
113 ? 'Try a different search.'
114 : 'Add one to get started.'
115 "
116 />
117 </template>
118 </SettingsLayoutBase>
119 <NewTeamModal
120 v-model="showForm"
121 @create="
122 () => {
123 teams.reload();
124 }
125 "
126 />
127 <RenameTeamModal v-model="showRename" @onRename="() => teams.reload()" />
128</template>
129
130<script setup lang="ts">
131import EditIcon from "@/components/icons/EditIcon.vue";
132import AgentIcon from "@/components/icons/AgentIcon.vue";
133import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
134import EmptyState from "@/components/EmptyState.vue";
135import { __ } from "@/translation";
136import { TeamListResourceSymbol } from "@/types";
137import { ConfirmDelete } from "@/utils";
138import { Dropdown, TextInput, toast } from "frappe-ui";
139import { inject, markRaw, Ref, ref, watch } from "vue";
140import NewTeamModal from "../NewTeamModal.vue";
141import RenameTeamModal from "./RenameTeamModal.vue";
142
143interface E {
144 (event: "update:step", step: string, team: string): void;
145}
146
147const emit = defineEmits<E>();
148const teamsSearchQuery = inject<Ref>("teamsSearchQuery");
149
150const teams = inject(TeamListResourceSymbol)!;
151const showForm = ref(false);
152const showRename = ref({
153 show: false,
154 teamName: "",
155});
156const isConfirmingDelete = ref(false);
157
158const dropdownOptions = (team: any) => {
159 return [
160 {
161 label: __("Rename"),
162 icon: markRaw(EditIcon),
163 onClick: () => {
164 showRename.value = {
165 show: true,
166 teamName: team.name,
167 };
168 },
169 },
170 ...ConfirmDelete({
171 onConfirmDelete: () => deleteTeam(team),
172 isConfirmingDelete,
173 }),
174 ];
175};
176
177const deleteTeam = (team: any) => {
178 if (!isConfirmingDelete.value) {
179 isConfirmingDelete.value = true;
180 return;
181 }
182
183 teams.delete.submit(team.name, {
184 onSuccess: () => {
185 toast.success(__("Team deleted successfully."));
186 },
187 });
188};
189
190watch(teamsSearchQuery, (newValue) => {
191 teams.filters = {
192 ...teams.filters,
193 name: ["like", `%${newValue}%`],
194 };
195 teams.reload();
196});
197</script>
198