@escalated-dev/escalated 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +125 -2
- package/package.json +40 -5
- package/src/components/ActiveChatsPanel.vue +246 -0
- package/src/components/ActivityTimeline.stories.js +55 -0
- package/src/components/ActivityTimeline.vue +83 -66
- package/src/components/AdminDashboard.stories.js +1423 -0
- package/src/components/AgentLoadIndicator.stories.js +37 -0
- package/src/components/AgentLoadIndicator.vue +49 -0
- package/src/components/AgentPerformanceCard.vue +44 -0
- package/src/components/ArticleEditor.vue +82 -0
- package/src/components/AssigneeSelect.vue +37 -23
- package/src/components/AttachmentList.vue +65 -37
- package/src/components/AuditLogEntry.vue +95 -0
- package/src/components/BulkActionBar.vue +166 -97
- package/src/components/CategoryTree.vue +89 -0
- package/src/components/ChartWidget.vue +46 -0
- package/src/components/ChatActionBar.vue +226 -0
- package/src/components/ChatBubble.vue +162 -0
- package/src/components/ChatComposer.vue +308 -0
- package/src/components/ChatQueue.vue +222 -0
- package/src/components/ChatThread.vue +197 -0
- package/src/components/CollisionWarning.stories.js +30 -0
- package/src/components/CollisionWarning.vue +68 -0
- package/src/components/ConditionalFieldRules.vue +135 -0
- package/src/components/ContextPanel.vue +66 -0
- package/src/components/ContextPanelSection.vue +65 -0
- package/src/components/CustomFieldRenderer.vue +159 -0
- package/src/components/EscalatedLayout.vue +424 -70
- package/src/components/FileDropzone.vue +57 -41
- package/src/components/FollowButton.stories.js +30 -0
- package/src/components/FollowButton.vue +88 -63
- package/src/components/KeyboardShortcutHelp.vue +213 -123
- package/src/components/KnowledgePanel.vue +122 -0
- package/src/components/KpiCard.stories.js +32 -0
- package/src/components/KpiCard.vue +88 -0
- package/src/components/MacroDropdown.vue +150 -90
- package/src/components/MentionDropdown.vue +154 -0
- package/src/components/MetricComparison.vue +89 -0
- package/src/components/PercentileChart.vue +67 -0
- package/src/components/PeriodSelector.vue +98 -0
- package/src/components/PermissionMatrix.vue +107 -0
- package/src/components/PinnedNotes.vue +129 -90
- package/src/components/PluginSlot.vue +68 -62
- package/src/components/PresenceIndicator.vue +138 -28
- package/src/components/PriorityBadge.stories.js +33 -0
- package/src/components/PriorityBadge.vue +46 -39
- package/src/components/QuickFilters.vue +65 -49
- package/src/components/ReplyComposer.vue +207 -30
- package/src/components/ReplyThread.vue +111 -21
- package/src/components/ReportChart.vue +395 -0
- package/src/components/ReportExportButton.vue +108 -0
- package/src/components/SatisfactionRating.vue +180 -128
- package/src/components/SaveViewDialog.vue +172 -0
- package/src/components/SavedViewSidebar.vue +208 -0
- package/src/components/ScheduleEditor.vue +104 -0
- package/src/components/SideConversation.vue +165 -0
- package/src/components/SideConversationList.vue +147 -0
- package/src/components/SkillTagManager.vue +79 -0
- package/src/components/SlaComplianceChart.vue +46 -0
- package/src/components/SlaTimer.stories.js +35 -0
- package/src/components/SlaTimer.vue +62 -49
- package/src/components/SnoozeButton.vue +247 -0
- package/src/components/StatsCard.stories.js +24 -0
- package/src/components/StatsCard.vue +18 -6
- package/src/components/StatusBadge.stories.js +53 -0
- package/src/components/StatusBadge.vue +78 -46
- package/src/components/TagSelect.vue +84 -53
- package/src/components/TicketFilters.vue +247 -46
- package/src/components/TicketLinkPanel.vue +193 -0
- package/src/components/TicketList.vue +536 -74
- package/src/components/TicketMergeDialog.vue +157 -0
- package/src/components/TicketSidebar.vue +167 -99
- package/src/components/TicketSplitDialog.vue +98 -0
- package/src/components/TicketTypeSelector.vue +21 -0
- package/src/components/TotpInput.stories.js +9 -0
- package/src/components/TotpInput.vue +81 -0
- package/src/components/TwoFactorSetup.vue +113 -0
- package/src/components/VariableTokenInput.vue +111 -0
- package/src/components/WorkflowActionConfig.vue +287 -0
- package/src/components/WorkflowActionList.vue +350 -0
- package/src/components/WorkflowConditionBuilder.vue +265 -0
- package/src/components/WorkflowTriggerSelector.vue +125 -0
- package/src/composables/useChat.js +187 -0
- package/src/composables/useDebouncedSearch.js +38 -0
- package/src/composables/useI18n.js +80 -80
- package/src/composables/useKeyboardShortcuts.js +46 -46
- package/src/composables/useMentions.js +134 -0
- package/src/composables/usePluginExtensions.js +159 -67
- package/src/composables/usePluginHooks.js +65 -0
- package/src/composables/useRealtime.js +182 -0
- package/src/defineEscalatedPlugin.js +76 -50
- package/src/index.js +77 -28
- package/src/locales/ar.json +727 -0
- package/src/locales/de.json +678 -430
- package/src/locales/en.json +717 -430
- package/src/locales/es.json +679 -430
- package/src/locales/fr.json +680 -431
- package/src/locales/index.js +12 -2
- package/src/locales/it.json +727 -0
- package/src/locales/ja.json +727 -0
- package/src/locales/ko.json +727 -0
- package/src/locales/nl.json +727 -0
- package/src/locales/pl.json +727 -0
- package/src/locales/pt-BR.json +727 -0
- package/src/locales/ru.json +727 -0
- package/src/locales/tr.json +727 -0
- package/src/locales/zh-CN.json +727 -0
- package/src/pages/Admin/ApiTokens/Index.vue +381 -217
- package/src/pages/Admin/AuditLog/Index.vue +154 -0
- package/src/pages/Admin/BusinessHours/Form.vue +172 -0
- package/src/pages/Admin/BusinessHours/Index.vue +113 -0
- package/src/pages/Admin/CannedResponses/Index.vue +82 -25
- package/src/pages/Admin/Capacity/Index.vue +134 -0
- package/src/pages/Admin/CustomFields/Form.vue +195 -0
- package/src/pages/Admin/CustomFields/Index.vue +134 -0
- package/src/pages/Admin/CustomObjects/Form.vue +202 -0
- package/src/pages/Admin/CustomObjects/Index.vue +127 -0
- package/src/pages/Admin/CustomObjects/Records.vue +228 -0
- package/src/pages/Admin/Departments/Form.vue +35 -11
- package/src/pages/Admin/Departments/Index.vue +74 -21
- package/src/pages/Admin/EscalationRules/Form.vue +86 -25
- package/src/pages/Admin/EscalationRules/Index.vue +158 -37
- package/src/pages/Admin/Import/Connect.vue +95 -0
- package/src/pages/Admin/Import/Index.vue +102 -0
- package/src/pages/Admin/Import/Mapping.vue +110 -0
- package/src/pages/Admin/Import/Progress.vue +153 -0
- package/src/pages/Admin/Import/Review.vue +76 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
- package/src/pages/Admin/Macros/Index.vue +448 -287
- package/src/pages/Admin/Plugins/Index.vue +327 -197
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
- package/src/pages/Admin/Reports/Cohorts.vue +180 -0
- package/src/pages/Admin/Reports/Comparison.vue +141 -0
- package/src/pages/Admin/Reports/CsatReport.vue +127 -0
- package/src/pages/Admin/Reports/Dashboard.vue +226 -0
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
- package/src/pages/Admin/Reports/SlaReport.vue +162 -0
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
- package/src/pages/Admin/Reports.vue +206 -101
- package/src/pages/Admin/Roles/Form.vue +71 -0
- package/src/pages/Admin/Roles/Index.vue +122 -0
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
- package/src/pages/Admin/Settings/DataRetention.vue +182 -0
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
- package/src/pages/Admin/Settings/Sandbox.vue +135 -0
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
- package/src/pages/Admin/Settings.vue +847 -99
- package/src/pages/Admin/Skills/Form.vue +53 -0
- package/src/pages/Admin/Skills/Index.vue +94 -0
- package/src/pages/Admin/SlaPolicies/Form.vue +62 -19
- package/src/pages/Admin/SlaPolicies/Index.vue +85 -23
- package/src/pages/Admin/Statuses/Form.vue +114 -0
- package/src/pages/Admin/Statuses/Index.vue +153 -0
- package/src/pages/Admin/Tags/Index.vue +121 -35
- package/src/pages/Admin/Tickets/Index.vue +96 -9
- package/src/pages/Admin/Tickets/Show.vue +130 -37
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
- package/src/pages/Admin/Webhooks/Form.vue +110 -0
- package/src/pages/Admin/Webhooks/Index.vue +136 -0
- package/src/pages/Admin/Workflows/Builder.vue +207 -0
- package/src/pages/Admin/Workflows/Index.vue +235 -0
- package/src/pages/Admin/Workflows/Logs.vue +286 -0
- package/src/pages/Agent/Dashboard.vue +311 -186
- package/src/pages/Agent/TicketIndex.vue +80 -6
- package/src/pages/Agent/TicketShow.vue +399 -60
- package/src/pages/Customer/Create.vue +37 -10
- package/src/pages/Customer/Index.vue +9 -2
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -0
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
- package/src/pages/Customer/Show.vue +69 -63
- package/src/pages/Guest/Create.vue +40 -15
- package/src/pages/Guest/Show.vue +124 -93
- package/src/plugin.js +111 -1
- package/src/utils/constants.js +30 -0
- package/src/utils/formatting.js +35 -0
- package/src/utils/mentionHighlight.js +24 -0
- package/src/utils/sanitizeHtml.js +78 -64
- package/src/widget/EscalatedWidget.vue +804 -0
- package/src/widget/index.js +254 -0
- package/src/pages/Admin/Plugins.vue +0 -129
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue';
|
|
3
|
+
import WorkflowActionConfig from './WorkflowActionConfig.vue';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
modelValue: { type: Array, default: () => [] },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits(['update:modelValue']);
|
|
10
|
+
|
|
11
|
+
const actionTypes = [
|
|
12
|
+
{
|
|
13
|
+
value: 'assign_agent',
|
|
14
|
+
label: 'Assign Agent',
|
|
15
|
+
icon: 'M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z',
|
|
16
|
+
color: 'text-blue-400 bg-blue-500/10',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: 'change_status',
|
|
20
|
+
label: 'Change Status',
|
|
21
|
+
icon: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
22
|
+
color: 'text-emerald-400 bg-emerald-500/10',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
value: 'change_priority',
|
|
26
|
+
label: 'Change Priority',
|
|
27
|
+
icon: 'M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12',
|
|
28
|
+
color: 'text-red-400 bg-red-500/10',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
value: 'add_tag',
|
|
32
|
+
label: 'Add Tag',
|
|
33
|
+
icon: 'M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z',
|
|
34
|
+
color: 'text-violet-400 bg-violet-500/10',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
value: 'remove_tag',
|
|
38
|
+
label: 'Remove Tag',
|
|
39
|
+
icon: 'M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z',
|
|
40
|
+
color: 'text-pink-400 bg-pink-500/10',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: 'move_department',
|
|
44
|
+
label: 'Move Department',
|
|
45
|
+
icon: 'M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 01-1.125-1.125v-3.75z',
|
|
46
|
+
color: 'text-cyan-400 bg-cyan-500/10',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
value: 'add_note',
|
|
50
|
+
label: 'Add Internal Note',
|
|
51
|
+
icon: 'M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z',
|
|
52
|
+
color: 'text-amber-400 bg-amber-500/10',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: 'send_email',
|
|
56
|
+
label: 'Send Email Notification',
|
|
57
|
+
icon: 'M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75',
|
|
58
|
+
color: 'text-sky-400 bg-sky-500/10',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
value: 'send_webhook',
|
|
62
|
+
label: 'Send Webhook',
|
|
63
|
+
icon: 'M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244',
|
|
64
|
+
color: 'text-indigo-400 bg-indigo-500/10',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
value: 'delay',
|
|
68
|
+
label: 'Delay',
|
|
69
|
+
icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
70
|
+
color: 'text-orange-400 bg-orange-500/10',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
value: 'apply_macro',
|
|
74
|
+
label: 'Apply Macro',
|
|
75
|
+
icon: 'M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z',
|
|
76
|
+
color: 'text-yellow-400 bg-yellow-500/10',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
value: 'close_ticket',
|
|
80
|
+
label: 'Close Ticket',
|
|
81
|
+
icon: 'M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
82
|
+
color: 'text-gray-400 bg-gray-500/10',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
value: 'snooze',
|
|
86
|
+
label: 'Snooze',
|
|
87
|
+
icon: 'M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0',
|
|
88
|
+
color: 'text-teal-400 bg-teal-500/10',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const showTypeSelector = ref(false);
|
|
93
|
+
const expandedIndex = ref(null);
|
|
94
|
+
const dragIndex = ref(null);
|
|
95
|
+
|
|
96
|
+
const actions = computed(() => props.modelValue);
|
|
97
|
+
|
|
98
|
+
function getActionType(value) {
|
|
99
|
+
return actionTypes.find((t) => t.value === value) || actionTypes[0];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addAction(typeValue) {
|
|
103
|
+
const newActions = [...actions.value, { type: typeValue, config: {} }];
|
|
104
|
+
emit('update:modelValue', newActions);
|
|
105
|
+
showTypeSelector.value = false;
|
|
106
|
+
expandedIndex.value = newActions.length - 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function removeAction(index) {
|
|
110
|
+
const newActions = actions.value.filter((_, i) => i !== index);
|
|
111
|
+
emit('update:modelValue', newActions);
|
|
112
|
+
if (expandedIndex.value === index) expandedIndex.value = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function updateConfig(index, config) {
|
|
116
|
+
const newActions = actions.value.map((a, i) => (i === index ? { ...a, config } : a));
|
|
117
|
+
emit('update:modelValue', newActions);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toggleExpand(index) {
|
|
121
|
+
expandedIndex.value = expandedIndex.value === index ? null : index;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getActionSummary(action) {
|
|
125
|
+
const c = action.config || {};
|
|
126
|
+
switch (action.type) {
|
|
127
|
+
case 'assign_agent':
|
|
128
|
+
return c.mode === 'round_robin'
|
|
129
|
+
? 'Round robin'
|
|
130
|
+
: c.mode === 'least_busy'
|
|
131
|
+
? 'Least busy'
|
|
132
|
+
: c.agent || 'Not configured';
|
|
133
|
+
case 'change_status':
|
|
134
|
+
return c.status || 'Not configured';
|
|
135
|
+
case 'change_priority':
|
|
136
|
+
return c.priority || 'Not configured';
|
|
137
|
+
case 'add_tag':
|
|
138
|
+
case 'remove_tag':
|
|
139
|
+
return c.tag || 'Not configured';
|
|
140
|
+
case 'move_department':
|
|
141
|
+
return c.department || 'Not configured';
|
|
142
|
+
case 'add_note':
|
|
143
|
+
return c.body ? c.body.substring(0, 40) + (c.body.length > 40 ? '...' : '') : 'Not configured';
|
|
144
|
+
case 'send_email':
|
|
145
|
+
return c.to || 'Not configured';
|
|
146
|
+
case 'send_webhook':
|
|
147
|
+
return c.url || 'Not configured';
|
|
148
|
+
case 'delay':
|
|
149
|
+
return `Wait ${c.duration || 1} ${c.unit || 'hours'}`;
|
|
150
|
+
case 'apply_macro':
|
|
151
|
+
return c.macro || 'Not configured';
|
|
152
|
+
case 'close_ticket':
|
|
153
|
+
return 'Close immediately';
|
|
154
|
+
case 'snooze':
|
|
155
|
+
return `${c.duration || 1} ${c.unit || 'hours'}`;
|
|
156
|
+
default:
|
|
157
|
+
return 'Not configured';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Drag-and-drop reordering
|
|
162
|
+
function onDragStart(index) {
|
|
163
|
+
dragIndex.value = index;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function onDragOver(event, index) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
if (dragIndex.value === null || dragIndex.value === index) return;
|
|
169
|
+
const newActions = [...actions.value];
|
|
170
|
+
const [moved] = newActions.splice(dragIndex.value, 1);
|
|
171
|
+
newActions.splice(index, 0, moved);
|
|
172
|
+
emit('update:modelValue', newActions);
|
|
173
|
+
dragIndex.value = index;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function onDragEnd() {
|
|
177
|
+
dragIndex.value = null;
|
|
178
|
+
}
|
|
179
|
+
</script>
|
|
180
|
+
|
|
181
|
+
<template>
|
|
182
|
+
<div class="space-y-0">
|
|
183
|
+
<div v-for="(action, i) in actions" :key="i" class="relative">
|
|
184
|
+
<!-- Connector line -->
|
|
185
|
+
<div v-if="i > 0" class="flex justify-center">
|
|
186
|
+
<div class="h-6 w-px bg-[var(--esc-panel-border)]" />
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Delay divider -->
|
|
190
|
+
<div
|
|
191
|
+
v-if="action.type === 'delay'"
|
|
192
|
+
class="my-1 flex items-center gap-3"
|
|
193
|
+
draggable="true"
|
|
194
|
+
@dragstart="onDragStart(i)"
|
|
195
|
+
@dragover="onDragOver($event, i)"
|
|
196
|
+
@dragend="onDragEnd"
|
|
197
|
+
>
|
|
198
|
+
<div class="h-px flex-1 border-t border-dashed border-orange-500/40" />
|
|
199
|
+
<div
|
|
200
|
+
class="flex items-center gap-2 rounded-full border border-orange-500/30 bg-orange-500/10 px-4 py-1.5"
|
|
201
|
+
>
|
|
202
|
+
<svg
|
|
203
|
+
class="h-4 w-4 text-orange-400"
|
|
204
|
+
fill="none"
|
|
205
|
+
stroke="currentColor"
|
|
206
|
+
stroke-width="1.5"
|
|
207
|
+
viewBox="0 0 24 24"
|
|
208
|
+
>
|
|
209
|
+
<path
|
|
210
|
+
stroke-linecap="round"
|
|
211
|
+
stroke-linejoin="round"
|
|
212
|
+
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
213
|
+
/>
|
|
214
|
+
</svg>
|
|
215
|
+
<span class="text-xs font-medium text-orange-400">{{ getActionSummary(action) }}</span>
|
|
216
|
+
<button type="button" class="ml-1 text-orange-400/60 hover:text-rose-400" @click="removeAction(i)">
|
|
217
|
+
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
218
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
219
|
+
</svg>
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="h-px flex-1 border-t border-dashed border-orange-500/40" />
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Regular action card -->
|
|
226
|
+
<div
|
|
227
|
+
v-else
|
|
228
|
+
class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] transition-all"
|
|
229
|
+
:class="expandedIndex === i ? 'shadow-lg' : ''"
|
|
230
|
+
draggable="true"
|
|
231
|
+
@dragstart="onDragStart(i)"
|
|
232
|
+
@dragover="onDragOver($event, i)"
|
|
233
|
+
@dragend="onDragEnd"
|
|
234
|
+
>
|
|
235
|
+
<!-- Action header -->
|
|
236
|
+
<div class="flex cursor-pointer items-center gap-3 px-4 py-3" @click="toggleExpand(i)">
|
|
237
|
+
<!-- Drag handle -->
|
|
238
|
+
<div class="cursor-grab text-[var(--esc-panel-text-muted)]">
|
|
239
|
+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
|
240
|
+
<circle cx="9" cy="6" r="1.5" />
|
|
241
|
+
<circle cx="15" cy="6" r="1.5" />
|
|
242
|
+
<circle cx="9" cy="12" r="1.5" />
|
|
243
|
+
<circle cx="15" cy="12" r="1.5" />
|
|
244
|
+
<circle cx="9" cy="18" r="1.5" />
|
|
245
|
+
<circle cx="15" cy="18" r="1.5" />
|
|
246
|
+
</svg>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Icon -->
|
|
250
|
+
<div
|
|
251
|
+
class="flex h-8 w-8 items-center justify-center rounded-lg"
|
|
252
|
+
:class="getActionType(action.type).color"
|
|
253
|
+
>
|
|
254
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
255
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="getActionType(action.type).icon" />
|
|
256
|
+
</svg>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<!-- Label and summary -->
|
|
260
|
+
<div class="min-w-0 flex-1">
|
|
261
|
+
<div class="text-sm font-medium text-[var(--esc-panel-text-secondary)]">
|
|
262
|
+
{{ getActionType(action.type).label }}
|
|
263
|
+
</div>
|
|
264
|
+
<div class="truncate text-xs text-[var(--esc-panel-text-muted)]">
|
|
265
|
+
{{ getActionSummary(action) }}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<!-- Expand/collapse chevron -->
|
|
270
|
+
<svg
|
|
271
|
+
class="h-4 w-4 text-[var(--esc-panel-text-muted)] transition-transform"
|
|
272
|
+
:class="expandedIndex === i ? 'rotate-180' : ''"
|
|
273
|
+
fill="none"
|
|
274
|
+
stroke="currentColor"
|
|
275
|
+
stroke-width="2"
|
|
276
|
+
viewBox="0 0 24 24"
|
|
277
|
+
>
|
|
278
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
279
|
+
</svg>
|
|
280
|
+
|
|
281
|
+
<!-- Remove -->
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
class="flex h-7 w-7 items-center justify-center rounded-lg text-[var(--esc-panel-text-muted)] transition-colors hover:bg-rose-500/10 hover:text-rose-400"
|
|
285
|
+
@click.stop="removeAction(i)"
|
|
286
|
+
>
|
|
287
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
288
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
289
|
+
</svg>
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<!-- Expanded config -->
|
|
294
|
+
<div v-if="expandedIndex === i" class="border-t border-[var(--esc-panel-border)] px-4 pb-4">
|
|
295
|
+
<WorkflowActionConfig
|
|
296
|
+
:action-type="action.type"
|
|
297
|
+
:model-value="action.config"
|
|
298
|
+
@update:model-value="updateConfig(i, $event)"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<!-- Connector to add button -->
|
|
305
|
+
<div v-if="actions.length" class="flex justify-center">
|
|
306
|
+
<div class="h-6 w-px bg-[var(--esc-panel-border)]" />
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<!-- Add action -->
|
|
310
|
+
<div class="relative">
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
class="flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-[var(--esc-panel-border)] px-4 py-3 text-sm text-[var(--esc-panel-text-muted)] transition-all hover:border-emerald-500/50 hover:text-emerald-400"
|
|
314
|
+
@click="showTypeSelector = !showTypeSelector"
|
|
315
|
+
>
|
|
316
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
317
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
318
|
+
</svg>
|
|
319
|
+
Add Action
|
|
320
|
+
</button>
|
|
321
|
+
|
|
322
|
+
<!-- Action type selector dropdown -->
|
|
323
|
+
<div
|
|
324
|
+
v-if="showTypeSelector"
|
|
325
|
+
class="absolute left-0 z-20 mt-2 w-full rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] py-2 shadow-2xl"
|
|
326
|
+
>
|
|
327
|
+
<button
|
|
328
|
+
v-for="at in actionTypes"
|
|
329
|
+
:key="at.value"
|
|
330
|
+
type="button"
|
|
331
|
+
class="flex w-full items-center gap-3 px-4 py-2 text-left transition-colors hover:bg-[var(--esc-panel-hover)]"
|
|
332
|
+
@click="addAction(at.value)"
|
|
333
|
+
>
|
|
334
|
+
<div class="flex h-7 w-7 items-center justify-center rounded-lg" :class="at.color">
|
|
335
|
+
<svg
|
|
336
|
+
class="h-3.5 w-3.5"
|
|
337
|
+
fill="none"
|
|
338
|
+
stroke="currentColor"
|
|
339
|
+
stroke-width="1.5"
|
|
340
|
+
viewBox="0 0 24 24"
|
|
341
|
+
>
|
|
342
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="at.icon" />
|
|
343
|
+
</svg>
|
|
344
|
+
</div>
|
|
345
|
+
<span class="text-sm text-[var(--esc-panel-text-secondary)]">{{ at.label }}</span>
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</template>
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
modelValue: {
|
|
6
|
+
type: Object,
|
|
7
|
+
default: () => ({ match: 'all', conditions: [] }),
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits(['update:modelValue']);
|
|
12
|
+
|
|
13
|
+
const fields = [
|
|
14
|
+
{
|
|
15
|
+
value: 'status',
|
|
16
|
+
label: 'Status',
|
|
17
|
+
type: 'enum',
|
|
18
|
+
options: [
|
|
19
|
+
'open',
|
|
20
|
+
'in_progress',
|
|
21
|
+
'waiting_on_customer',
|
|
22
|
+
'waiting_on_agent',
|
|
23
|
+
'escalated',
|
|
24
|
+
'resolved',
|
|
25
|
+
'closed',
|
|
26
|
+
'reopened',
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{ value: 'priority', label: 'Priority', type: 'enum', options: ['low', 'medium', 'high', 'urgent', 'critical'] },
|
|
30
|
+
{
|
|
31
|
+
value: 'type',
|
|
32
|
+
label: 'Type',
|
|
33
|
+
type: 'enum',
|
|
34
|
+
options: ['question', 'problem', 'incident', 'task', 'feature_request'],
|
|
35
|
+
},
|
|
36
|
+
{ value: 'channel', label: 'Channel', type: 'enum', options: ['web', 'email', 'chat', 'api', 'phone'] },
|
|
37
|
+
{ value: 'tags', label: 'Tags', type: 'text' },
|
|
38
|
+
{ value: 'department', label: 'Department', type: 'text' },
|
|
39
|
+
{ value: 'assigned_agent', label: 'Assigned Agent', type: 'text' },
|
|
40
|
+
{ value: 'subject', label: 'Subject', type: 'text' },
|
|
41
|
+
{ value: 'hours_since_created', label: 'Hours Since Created', type: 'number' },
|
|
42
|
+
{ value: 'hours_since_updated', label: 'Hours Since Updated', type: 'number' },
|
|
43
|
+
{ value: 'reply_count', label: 'Reply Count', type: 'number' },
|
|
44
|
+
{ value: 'custom_field', label: 'Custom Field', type: 'text' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const operatorsByType = {
|
|
48
|
+
enum: [
|
|
49
|
+
{ value: 'equals', label: 'equals' },
|
|
50
|
+
{ value: 'not_equals', label: 'not equals' },
|
|
51
|
+
{ value: 'in', label: 'in' },
|
|
52
|
+
],
|
|
53
|
+
text: [
|
|
54
|
+
{ value: 'equals', label: 'equals' },
|
|
55
|
+
{ value: 'not_equals', label: 'not equals' },
|
|
56
|
+
{ value: 'contains', label: 'contains' },
|
|
57
|
+
{ value: 'is_empty', label: 'is empty' },
|
|
58
|
+
{ value: 'matches', label: 'matches' },
|
|
59
|
+
],
|
|
60
|
+
number: [
|
|
61
|
+
{ value: 'equals', label: 'equals' },
|
|
62
|
+
{ value: 'not_equals', label: 'not equals' },
|
|
63
|
+
{ value: 'greater_than', label: 'greater than' },
|
|
64
|
+
{ value: 'less_than', label: 'less than' },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const data = computed(() => props.modelValue);
|
|
69
|
+
|
|
70
|
+
function update(patch) {
|
|
71
|
+
emit('update:modelValue', { ...data.value, ...patch });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toggleMatch() {
|
|
75
|
+
update({ match: data.value.match === 'all' ? 'any' : 'all' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addCondition() {
|
|
79
|
+
const conditions = [...(data.value.conditions || []), { field: 'status', operator: 'equals', value: '' }];
|
|
80
|
+
update({ conditions });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function removeCondition(index) {
|
|
84
|
+
const conditions = data.value.conditions.filter((_, i) => i !== index);
|
|
85
|
+
update({ conditions });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function updateCondition(index, patch) {
|
|
89
|
+
const conditions = data.value.conditions.map((c, i) => (i === index ? { ...c, ...patch } : c));
|
|
90
|
+
update({ conditions });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getFieldConfig(fieldValue) {
|
|
94
|
+
return fields.find((f) => f.value === fieldValue) || fields[0];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getOperators(fieldValue) {
|
|
98
|
+
const field = getFieldConfig(fieldValue);
|
|
99
|
+
return operatorsByType[field.type] || operatorsByType.text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function onFieldChange(index, newField) {
|
|
103
|
+
const operators = getOperators(newField);
|
|
104
|
+
updateCondition(index, { field: newField, operator: operators[0].value, value: '' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const previewText = computed(() => {
|
|
108
|
+
if (!data.value.conditions?.length) return '';
|
|
109
|
+
const joiner = data.value.match === 'all' ? ' AND ' : ' OR ';
|
|
110
|
+
return data.value.conditions
|
|
111
|
+
.map((c) => {
|
|
112
|
+
const field = getFieldConfig(c.field);
|
|
113
|
+
const opLabel =
|
|
114
|
+
(operatorsByType[field.type] || []).find((o) => o.value === c.operator)?.label || c.operator;
|
|
115
|
+
if (c.operator === 'is_empty') return `${field.label} is empty`;
|
|
116
|
+
const val = Array.isArray(c.value) ? `[${c.value.join(', ')}]` : c.value;
|
|
117
|
+
return `${field.label} ${opLabel} ${val}`;
|
|
118
|
+
})
|
|
119
|
+
.join(joiner);
|
|
120
|
+
});
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<div class="space-y-4">
|
|
125
|
+
<!-- Match type toggle -->
|
|
126
|
+
<div class="flex items-center gap-3">
|
|
127
|
+
<span class="text-sm text-[var(--esc-panel-text-muted)]">Match:</span>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
class="rounded-lg px-3 py-1.5 text-xs font-semibold transition-colors"
|
|
131
|
+
:class="
|
|
132
|
+
data.match === 'all'
|
|
133
|
+
? 'bg-amber-500/20 text-amber-400'
|
|
134
|
+
: 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]'
|
|
135
|
+
"
|
|
136
|
+
@click="toggleMatch"
|
|
137
|
+
>
|
|
138
|
+
All conditions (AND)
|
|
139
|
+
</button>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
class="rounded-lg px-3 py-1.5 text-xs font-semibold transition-colors"
|
|
143
|
+
:class="
|
|
144
|
+
data.match === 'any'
|
|
145
|
+
? 'bg-amber-500/20 text-amber-400'
|
|
146
|
+
: 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text-secondary)]'
|
|
147
|
+
"
|
|
148
|
+
@click="toggleMatch"
|
|
149
|
+
>
|
|
150
|
+
Any condition (OR)
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- Condition rows -->
|
|
155
|
+
<div class="space-y-2">
|
|
156
|
+
<div
|
|
157
|
+
v-for="(condition, i) in data.conditions"
|
|
158
|
+
:key="i"
|
|
159
|
+
class="flex flex-wrap items-center gap-2 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-3"
|
|
160
|
+
>
|
|
161
|
+
<!-- Field -->
|
|
162
|
+
<select
|
|
163
|
+
:value="condition.field"
|
|
164
|
+
class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none"
|
|
165
|
+
@change="onFieldChange(i, $event.target.value)"
|
|
166
|
+
>
|
|
167
|
+
<option v-for="f in fields" :key="f.value" :value="f.value">{{ f.label }}</option>
|
|
168
|
+
</select>
|
|
169
|
+
|
|
170
|
+
<!-- Operator -->
|
|
171
|
+
<select
|
|
172
|
+
:value="condition.operator"
|
|
173
|
+
class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none"
|
|
174
|
+
@change="updateCondition(i, { operator: $event.target.value })"
|
|
175
|
+
>
|
|
176
|
+
<option v-for="op in getOperators(condition.field)" :key="op.value" :value="op.value">
|
|
177
|
+
{{ op.label }}
|
|
178
|
+
</option>
|
|
179
|
+
</select>
|
|
180
|
+
|
|
181
|
+
<!-- Value (contextual) -->
|
|
182
|
+
<template v-if="condition.operator !== 'is_empty'">
|
|
183
|
+
<!-- Enum select for enum fields -->
|
|
184
|
+
<select
|
|
185
|
+
v-if="getFieldConfig(condition.field).type === 'enum' && condition.operator !== 'in'"
|
|
186
|
+
:value="condition.value"
|
|
187
|
+
class="rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none"
|
|
188
|
+
@change="updateCondition(i, { value: $event.target.value })"
|
|
189
|
+
>
|
|
190
|
+
<option value="">Select...</option>
|
|
191
|
+
<option v-for="opt in getFieldConfig(condition.field).options" :key="opt" :value="opt">
|
|
192
|
+
{{ opt }}
|
|
193
|
+
</option>
|
|
194
|
+
</select>
|
|
195
|
+
|
|
196
|
+
<!-- Multi-select for "in" operator on enum fields -->
|
|
197
|
+
<select
|
|
198
|
+
v-else-if="getFieldConfig(condition.field).type === 'enum' && condition.operator === 'in'"
|
|
199
|
+
:value="condition.value"
|
|
200
|
+
multiple
|
|
201
|
+
class="min-h-[60px] rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none"
|
|
202
|
+
@change="
|
|
203
|
+
updateCondition(i, { value: Array.from($event.target.selectedOptions, (o) => o.value) })
|
|
204
|
+
"
|
|
205
|
+
>
|
|
206
|
+
<option v-for="opt in getFieldConfig(condition.field).options" :key="opt" :value="opt">
|
|
207
|
+
{{ opt }}
|
|
208
|
+
</option>
|
|
209
|
+
</select>
|
|
210
|
+
|
|
211
|
+
<!-- Number input -->
|
|
212
|
+
<input
|
|
213
|
+
v-else-if="getFieldConfig(condition.field).type === 'number'"
|
|
214
|
+
type="number"
|
|
215
|
+
:value="condition.value"
|
|
216
|
+
placeholder="0"
|
|
217
|
+
class="w-24 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
218
|
+
@input="updateCondition(i, { value: $event.target.value })"
|
|
219
|
+
/>
|
|
220
|
+
|
|
221
|
+
<!-- Text input (default) -->
|
|
222
|
+
<input
|
|
223
|
+
v-else
|
|
224
|
+
type="text"
|
|
225
|
+
:value="condition.value"
|
|
226
|
+
placeholder="Value"
|
|
227
|
+
class="flex-1 rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-2 py-1.5 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none focus:ring-1 focus:ring-[var(--esc-panel-border-input)]"
|
|
228
|
+
@input="updateCondition(i, { value: $event.target.value })"
|
|
229
|
+
/>
|
|
230
|
+
</template>
|
|
231
|
+
|
|
232
|
+
<!-- Remove button -->
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
class="ml-auto flex h-7 w-7 items-center justify-center rounded-lg text-rose-400 transition-colors hover:bg-rose-500/10 hover:text-rose-300"
|
|
236
|
+
@click="removeCondition(i)"
|
|
237
|
+
>
|
|
238
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
239
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
240
|
+
</svg>
|
|
241
|
+
</button>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
<!-- Add condition button -->
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
class="flex items-center gap-1.5 rounded-lg border border-dashed border-[var(--esc-panel-border)] px-3 py-2 text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:border-amber-500/50 hover:text-amber-400"
|
|
249
|
+
@click="addCondition"
|
|
250
|
+
>
|
|
251
|
+
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
252
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
253
|
+
</svg>
|
|
254
|
+
Add Condition
|
|
255
|
+
</button>
|
|
256
|
+
|
|
257
|
+
<!-- Preview -->
|
|
258
|
+
<div
|
|
259
|
+
v-if="previewText"
|
|
260
|
+
class="rounded-lg border border-amber-500/20 bg-amber-500/5 px-4 py-2.5 text-xs text-amber-300"
|
|
261
|
+
>
|
|
262
|
+
<span class="font-medium">Preview:</span> When {{ previewText }}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</template>
|