1<template>
2 <component
3 :is="config.doctype ? Link : Combobox"
4 v-bind="controlProps"
5 :disabled="!isEditable"
6 :model-value="action.value"
7 :placeholder="__('Select {0}', label.toLowerCase())"
8 @update:model-value="pick(($event ?? '') as string)"
9 >
10 <template #trigger="{ setOpen }">
11 <!-- The anchor forwards its click to this element, so it has to be a plain
12 one: Tooltip and Combobox both drop inherited attrs -->
13 <div
14 class="inline-flex"
15 :role="isEditable ? 'button' : undefined"
16 :tabindex="isEditable ? 0 : undefined"
17 @keydown.enter.self.prevent="setOpen(true)"
18 >
19 <Tooltip :text="value ? `${label}: ${value}` : label">
20 <Badge
21 class="bg-surface-base transition-colors"
22 :class="isEditable ? 'cursor-pointer hover:bg-surface-gray-2' : ''"
23 theme="gray"
24 variant="outline"
25 size="lg"
26 >
27 <template #prefix>
28 <component
29 :is="config.icon"
30 class="size-3 shrink-0 text-ink-gray-6"
31 />
32 </template>
33 <span class="flex min-w-0 items-center gap-1">
34 <span v-if="value" class="shrink-0 text-ink-gray-6">
35 {{ label }}
36 </span>
37 <span class="max-w-40 truncate text-ink-gray-8">
38 {{ value || label }}
39 </span>
40 </span>
41 <template #suffix>
42 <!-- Removing is not picking: the click stops here -->
43 <button
44 type="button"
45 :aria-label="__('Remove {0}', label)"
46 class="flex items-center rounded-1 text-ink-gray-6 transition-colors hover:text-ink-gray-9 focus-visible:ring-2 focus-visible:ring-outline-gray-3 mt-[1px]"
47 @click.stop="emit('remove')"
48 @pointerdown.stop
49 >
50 <LucideX class="size-3" />
51 </button>
52 </template>
53 </Badge>
54 </Tooltip>
55 </div>
56 </template>
57 </component>
58</template>
59
60<script setup lang="ts">
61import { ACTION_TYPES } from "@/components/Settings/SavedReplies/components/actionTypes";
62import { useSavedReplyActionOptions } from "@/composables/useSavedReplyActionOptions";
63import { Link } from "@framework/ui";
64import { __ } from "@/translation";
65import { SavedReplyAction } from "@/types";
66import { Badge, Combobox, Tooltip } from "frappe-ui";
67import { computed } from "vue";
68import LucideX from "~icons/lucide/x";
69
70const props = defineProps<{
71 action: SavedReplyAction;
72 /** Values the other chips hold, so a repoint can't collide with them. */
73 taken?: string[];
74}>();
75
76const emit = defineEmits<{
77 update: [action: SavedReplyAction];
78 remove: [];
79}>();
80
81const { valueOptions, linkFilters } = useSavedReplyActionOptions();
82
83const config = computed(() => ACTION_TYPES[props.action.action_type]);
84const label = computed(() => config.value.chipLabel);
85// Empty for no-value actions like "Assign to Me"; the noun then stands alone
86const value = computed(() => props.action.label || props.action.value);
87
88// A tag chip holds one tag, so it picks like the rest. Excluded rather than
89// listed, so a new control type is editable by default instead of silently
90// read-only: no-value actions have nothing to pick, and a comment is too long
91// for a chip.
92const isEditable = computed(
93 () => !["none", "editor"].includes(config.value.control)
94);
95
96// Combobox takes string values; the shared options allow numbers too
97const pickerOptions = computed(() =>
98 valueOptions(props.action.action_type)
99 .filter((option) => !props.taken?.includes(String(option.value)))
100 .map((option) => ({
101 label: option.label,
102 value: String(option.value),
103 }))
104);
105
106// A #trigger slot puts Combobox in button mode, and Link forwards that slot,
107// so the badge below serves both controls.
108const controlProps = computed(() =>
109 config.value.doctype
110 ? {
111 doctype: config.value.doctype,
112 filters: linkFilters(props.action.action_type),
113 title: value.value,
114 }
115 : { options: pickerOptions.value }
116);
117
118/** `label` carries the display name (an agent's, not their id), so both move. */
119function pick(picked: string) {
120 // A link picker offers no option list to read a label from; for these
121 // doctypes the record name is the label.
122 if (config.value.doctype) {
123 emit("update", { ...props.action, value: picked, label: picked });
124 return;
125 }
126 const option = pickerOptions.value.find(
127 (candidate) => candidate.value === picked
128 );
129 if (!option) return;
130 emit("update", { ...props.action, value: option.value, label: option.label });
131}
132</script>
133