IKHSAN
ENID Baca dalam Bahasa Indonesia

opendesk

Public
Branch
main
Latest commit
08d9f3328 October 2026

TeamsList.vue

Open raw
<template>
  <SettingsLayoutBase
    :title="__('Teams')"
    :description="
      __('Create and manage teams and assign agents to specific teams.')
    "
  >
    <template #header-actions>
      <Button
        :label="__('New')"
        class="rtl:flex-row-reverse"
        theme="gray"
        variant="solid"
        @click="emit('update:step', 'new-team', '')"
        icon-left="lucide-plus"
      />
    </template>
    <template
      v-if="teams.data?.length > 9 || teamsSearchQuery.length"
      #header-bottom
    >
      <div class="relative">
        <TextInput
          :model-value="teamsSearchQuery"
          @update:model-value="teamsSearchQuery = $event"
          :placeholder="__('Search')"
          type="text"
          class="focus:ring-0 border-outline-gray-2"
          :debounce="300"
        >
          <template #prefix>
            <LucideSearch class="size-4" />
          </template>
        </TextInput>
        <Button
          v-if="teamsSearchQuery"
          icon="lucide-x"
          variant="ghost"
          @click="teamsSearchQuery = ''"
          class="absolute end-1 top-1/2 -translate-y-1/2"
        />
      </div>
    </template>
    <template #content>
      <!-- List -->
      <div
        v-if="!teams.loading && teams.data?.length > 0"
        class="w-full h-full -ms-2"
      >
        <div class="flex text-sm text-ink-gray-5">
          <p class="ms-2">{{ __("Team name") }}</p>
        </div>
        <hr class="mx-2 mt-2" />
        <div v-for="(team, index) in teams.data" :key="team.name">
          <div
            class="flex items-center cursor-pointer hover:bg-surface-sidebar rounded-4 h-12.5"
          >
            <div
              class="w-full py-3 ps-2 flex gap-1 items-center"
              @click="() => emit('update:step', 'team-edit', team.name)"
            >
              <p class="text-base-medium text-ink-gray-7">
                {{ team.name }}
              </p>
              <Badge :label="__('Disabled')" v-if="team.disabled" />
            </div>
            <div class="flex justify-between items-center pe-2">
              <div>
                <Dropdown
                  align="end"
                  :options="dropdownOptions(team)"
                  @update:open="(open) => open && (isConfirmingDelete = false)"
                >
                  <Button icon="lucide-more-horizontal" variant="ghost" />
                </Dropdown>
              </div>
            </div>
          </div>
          <hr v-if="index !== teams.data.length - 1" class="mx-2" />
        </div>
        <!-- Load More Button -->
        <div class="flex justify-center">
          <Button
            v-if="!teams.loading && teams.hasNextPage"
            class="mt-3.5 p-2"
            @click="() => teams.next()"
            :loading="teams.loading"
            :label="__('Load More')"
            icon-left="lucide-refresh-cw"
          />
        </div>
      </div>
      <!-- Loading State -->
      <div
        v-if="teams.loading"
        class="flex mt-28 justify-between w-full h-full"
      >
        <Button
          :loading="teams.loading"
          variant="ghost"
          class="w-full"
          size="lg"
        />
      </div>
      <!-- Empty State -->
      <EmptyState
        v-if="!teams.loading && !teams.data?.length"
        variant="badge"
        :icon="AgentIcon"
        title="No team found"
        :description="
          teamsSearchQuery.length
            ? 'Try a different search.'
            : 'Add one to get started.'
        "
      />
    </template>
  </SettingsLayoutBase>
  <NewTeamModal
    v-model="showForm"
    @create="
      () => {
        teams.reload();
      }
    "
  />
  <RenameTeamModal v-model="showRename" @onRename="() => teams.reload()" />
</template>

<script setup lang="ts">
import EditIcon from "@/components/icons/EditIcon.vue";
import AgentIcon from "@/components/icons/AgentIcon.vue";
import SettingsLayoutBase from "@/components/layouts/SettingsLayoutBase.vue";
import EmptyState from "@/components/EmptyState.vue";
import { __ } from "@/translation";
import { TeamListResourceSymbol } from "@/types";
import { ConfirmDelete } from "@/utils";
import { Dropdown, TextInput, toast } from "frappe-ui";
import { inject, markRaw, Ref, ref, watch } from "vue";
import NewTeamModal from "../NewTeamModal.vue";
import RenameTeamModal from "./RenameTeamModal.vue";

interface E {
  (event: "update:step", step: string, team: string): void;
}

const emit = defineEmits<E>();
const teamsSearchQuery = inject<Ref>("teamsSearchQuery");

const teams = inject(TeamListResourceSymbol)!;
const showForm = ref(false);
const showRename = ref({
  show: false,
  teamName: "",
});
const isConfirmingDelete = ref(false);

const dropdownOptions = (team: any) => {
  return [
    {
      label: __("Rename"),
      icon: markRaw(EditIcon),
      onClick: () => {
        showRename.value = {
          show: true,
          teamName: team.name,
        };
      },
    },
    ...ConfirmDelete({
      onConfirmDelete: () => deleteTeam(team),
      isConfirmingDelete,
    }),
  ];
};

const deleteTeam = (team: any) => {
  if (!isConfirmingDelete.value) {
    isConfirmingDelete.value = true;
    return;
  }

  teams.delete.submit(team.name, {
    onSuccess: () => {
      toast.success(__("Team deleted successfully."));
    },
  });
};

watch(teamsSearchQuery, (newValue) => {
  teams.filters = {
    ...teams.filters,
    name: ["like", `%${newValue}%`],
  };
  teams.reload();
});
</script>
bytes
5,533
lines
198
Type
vue
Latest commit
28 October 2026