@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,113 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import TotpInput from './TotpInput.vue';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
qrUri: { type: String, default: '' },
|
|
7
|
+
recoveryCodes: { type: Array, default: () => [] },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits(['confirm', 'cancel']);
|
|
11
|
+
|
|
12
|
+
const step = ref(1);
|
|
13
|
+
const code = ref('');
|
|
14
|
+
const error = ref('');
|
|
15
|
+
|
|
16
|
+
function onCodeComplete(val) {
|
|
17
|
+
code.value = val;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function confirmCode() {
|
|
21
|
+
if (code.value.length !== 6) {
|
|
22
|
+
error.value = 'Please enter the full 6-digit code.';
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
error.value = '';
|
|
26
|
+
emit('confirm', code.value);
|
|
27
|
+
step.value = 3;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function copyRecoveryCodes() {
|
|
31
|
+
const text = props.recoveryCodes.join('\n');
|
|
32
|
+
navigator.clipboard?.writeText(text);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="space-y-6">
|
|
38
|
+
<!-- Step 1: QR Code -->
|
|
39
|
+
<div v-if="step === 1">
|
|
40
|
+
<h3 class="mb-4 text-sm font-semibold text-white">Step 1: Scan QR Code</h3>
|
|
41
|
+
<p class="mb-4 text-sm text-neutral-400">
|
|
42
|
+
Scan the code below with your authenticator app (Google Authenticator, Authy, etc.), or manually enter
|
|
43
|
+
the setup key.
|
|
44
|
+
</p>
|
|
45
|
+
<div class="rounded-lg border border-white/[0.06] bg-neutral-950 p-4">
|
|
46
|
+
<p class="mb-2 text-xs font-medium text-neutral-500">Setup URI (enter manually if needed):</p>
|
|
47
|
+
<code class="block break-all text-xs text-cyan-400">{{ qrUri }}</code>
|
|
48
|
+
</div>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class="mt-4 rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-5 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400"
|
|
52
|
+
@click="step = 2"
|
|
53
|
+
>
|
|
54
|
+
Next: Verify Code
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Step 2: Verify TOTP -->
|
|
59
|
+
<div v-if="step === 2">
|
|
60
|
+
<h3 class="mb-4 text-sm font-semibold text-white">Step 2: Verify Code</h3>
|
|
61
|
+
<p class="mb-4 text-sm text-neutral-400">
|
|
62
|
+
Enter the 6-digit code from your authenticator app to confirm setup.
|
|
63
|
+
</p>
|
|
64
|
+
<TotpInput v-model="code" @complete="onCodeComplete" />
|
|
65
|
+
<p v-if="error" class="mt-2 text-sm text-rose-400">{{ error }}</p>
|
|
66
|
+
<div class="mt-4 flex gap-3">
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
class="rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-5 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400"
|
|
70
|
+
@click="confirmCode"
|
|
71
|
+
>
|
|
72
|
+
Confirm & Enable 2FA
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="rounded-lg border border-white/10 px-4 py-2 text-sm text-neutral-400 hover:text-neutral-200"
|
|
77
|
+
@click="emit('cancel')"
|
|
78
|
+
>
|
|
79
|
+
Cancel
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Step 3: Recovery Codes -->
|
|
85
|
+
<div v-if="step === 3">
|
|
86
|
+
<h3 class="mb-4 text-sm font-semibold text-white">Step 3: Save Recovery Codes</h3>
|
|
87
|
+
<p class="mb-4 text-sm text-neutral-400">
|
|
88
|
+
Save these recovery codes in a safe place. Each code can only be used once to access your account if you
|
|
89
|
+
lose your authenticator device.
|
|
90
|
+
</p>
|
|
91
|
+
<div class="rounded-lg border border-white/[0.06] bg-neutral-950 p-4">
|
|
92
|
+
<div class="grid grid-cols-2 gap-2">
|
|
93
|
+
<code
|
|
94
|
+
v-for="(rc, idx) in recoveryCodes"
|
|
95
|
+
:key="idx"
|
|
96
|
+
class="rounded bg-neutral-900 px-3 py-1.5 text-center text-sm font-mono text-neutral-200"
|
|
97
|
+
>
|
|
98
|
+
{{ rc }}
|
|
99
|
+
</code>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="mt-4 flex gap-3">
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
class="rounded-lg border border-white/10 px-4 py-2 text-sm text-neutral-400 hover:text-neutral-200"
|
|
106
|
+
@click="copyRecoveryCodes"
|
|
107
|
+
>
|
|
108
|
+
Copy Codes
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
modelValue: { type: String, default: '' },
|
|
6
|
+
placeholder: { type: String, default: '' },
|
|
7
|
+
multiline: { type: Boolean, default: false },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits(['update:modelValue']);
|
|
11
|
+
|
|
12
|
+
const showDropdown = ref(false);
|
|
13
|
+
const inputRef = ref(null);
|
|
14
|
+
|
|
15
|
+
const variables = [
|
|
16
|
+
{ token: '{{ticket.reference}}', label: 'Ticket Reference' },
|
|
17
|
+
{ token: '{{ticket.subject}}', label: 'Ticket Subject' },
|
|
18
|
+
{ token: '{{ticket.status}}', label: 'Ticket Status' },
|
|
19
|
+
{ token: '{{agent.name}}', label: 'Agent Name' },
|
|
20
|
+
{ token: '{{requester.name}}', label: 'Requester Name' },
|
|
21
|
+
{ token: '{{requester.email}}', label: 'Requester Email' },
|
|
22
|
+
{ token: '{{department.name}}', label: 'Department Name' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function insertVariable(token) {
|
|
26
|
+
const el = inputRef.value;
|
|
27
|
+
if (!el) {
|
|
28
|
+
emit('update:modelValue', (props.modelValue || '') + token);
|
|
29
|
+
showDropdown.value = false;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const start = el.selectionStart || 0;
|
|
33
|
+
const end = el.selectionEnd || 0;
|
|
34
|
+
const text = props.modelValue || '';
|
|
35
|
+
const newText = text.substring(0, start) + token + text.substring(end);
|
|
36
|
+
emit('update:modelValue', newText);
|
|
37
|
+
showDropdown.value = false;
|
|
38
|
+
// Restore cursor position after Vue updates
|
|
39
|
+
requestAnimationFrame(() => {
|
|
40
|
+
el.focus();
|
|
41
|
+
const pos = start + token.length;
|
|
42
|
+
el.setSelectionRange(pos, pos);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onInput(event) {
|
|
47
|
+
emit('update:modelValue', event.target.value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const inputClass =
|
|
51
|
+
'w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 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)]';
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<div class="relative">
|
|
56
|
+
<textarea
|
|
57
|
+
v-if="multiline"
|
|
58
|
+
ref="inputRef"
|
|
59
|
+
:value="modelValue"
|
|
60
|
+
:placeholder="placeholder"
|
|
61
|
+
rows="3"
|
|
62
|
+
:class="inputClass + ' resize-y'"
|
|
63
|
+
@input="onInput"
|
|
64
|
+
/>
|
|
65
|
+
<input
|
|
66
|
+
v-else
|
|
67
|
+
ref="inputRef"
|
|
68
|
+
:value="modelValue"
|
|
69
|
+
:placeholder="placeholder"
|
|
70
|
+
type="text"
|
|
71
|
+
:class="inputClass"
|
|
72
|
+
@input="onInput"
|
|
73
|
+
/>
|
|
74
|
+
|
|
75
|
+
<!-- Insert Variable button -->
|
|
76
|
+
<div class="mt-1.5 flex justify-end">
|
|
77
|
+
<div class="relative">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
class="flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium text-[var(--esc-panel-text-muted)] transition-colors hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]"
|
|
81
|
+
@click="showDropdown = !showDropdown"
|
|
82
|
+
>
|
|
83
|
+
<svg class="h-3 w-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
84
|
+
<path
|
|
85
|
+
stroke-linecap="round"
|
|
86
|
+
stroke-linejoin="round"
|
|
87
|
+
d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"
|
|
88
|
+
/>
|
|
89
|
+
</svg>
|
|
90
|
+
Insert Variable
|
|
91
|
+
</button>
|
|
92
|
+
<div
|
|
93
|
+
v-if="showDropdown"
|
|
94
|
+
class="absolute right-0 bottom-full z-10 mb-1 w-48 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] py-1 shadow-xl"
|
|
95
|
+
>
|
|
96
|
+
<button
|
|
97
|
+
v-for="v in variables"
|
|
98
|
+
:key="v.token"
|
|
99
|
+
type="button"
|
|
100
|
+
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors hover:bg-[var(--esc-panel-hover)]"
|
|
101
|
+
@click="insertVariable(v.token)"
|
|
102
|
+
>
|
|
103
|
+
<span class="rounded bg-cyan-500/10 px-1.5 py-0.5 font-mono text-[10px] text-cyan-400">{{
|
|
104
|
+
v.token
|
|
105
|
+
}}</span>
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import VariableTokenInput from './VariableTokenInput.vue';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
actionType: { type: String, required: true },
|
|
7
|
+
modelValue: { type: Object, default: () => ({}) },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits(['update:modelValue']);
|
|
11
|
+
|
|
12
|
+
const headersPlaceholder = '{"Authorization": "Bearer ..."}';
|
|
13
|
+
const config = computed(() => props.modelValue || {});
|
|
14
|
+
|
|
15
|
+
function update(key, value) {
|
|
16
|
+
emit('update:modelValue', { ...config.value, [key]: value });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const statusOptions = [
|
|
20
|
+
'open',
|
|
21
|
+
'in_progress',
|
|
22
|
+
'waiting_on_customer',
|
|
23
|
+
'waiting_on_agent',
|
|
24
|
+
'escalated',
|
|
25
|
+
'resolved',
|
|
26
|
+
'closed',
|
|
27
|
+
];
|
|
28
|
+
const priorityOptions = ['low', 'medium', 'high', 'urgent', 'critical'];
|
|
29
|
+
const assignModes = [
|
|
30
|
+
{ value: 'specific', label: 'Specific agent' },
|
|
31
|
+
{ value: 'least_busy', label: 'Least busy agent' },
|
|
32
|
+
{ value: 'round_robin', label: 'Round robin' },
|
|
33
|
+
];
|
|
34
|
+
const delayUnits = [
|
|
35
|
+
{ value: 'minutes', label: 'Minutes' },
|
|
36
|
+
{ value: 'hours', label: 'Hours' },
|
|
37
|
+
{ value: 'days', label: 'Days' },
|
|
38
|
+
];
|
|
39
|
+
const webhookMethods = ['POST', 'PUT', 'PATCH'];
|
|
40
|
+
|
|
41
|
+
const inputClass =
|
|
42
|
+
'w-full rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 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)]';
|
|
43
|
+
const selectClass =
|
|
44
|
+
'rounded-lg border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-surface-alt)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-border-input)] focus:outline-none';
|
|
45
|
+
const labelClass = 'block text-xs font-medium text-[var(--esc-panel-text-muted)] mb-1';
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div class="space-y-3 pt-2">
|
|
50
|
+
<!-- Assign Agent -->
|
|
51
|
+
<template v-if="actionType === 'assign_agent'">
|
|
52
|
+
<div>
|
|
53
|
+
<label :class="labelClass">Assignment mode</label>
|
|
54
|
+
<select
|
|
55
|
+
:class="selectClass"
|
|
56
|
+
:value="config.mode || 'specific'"
|
|
57
|
+
@change="update('mode', $event.target.value)"
|
|
58
|
+
>
|
|
59
|
+
<option v-for="m in assignModes" :key="m.value" :value="m.value">{{ m.label }}</option>
|
|
60
|
+
</select>
|
|
61
|
+
</div>
|
|
62
|
+
<div v-if="(config.mode || 'specific') === 'specific'">
|
|
63
|
+
<label :class="labelClass">Agent</label>
|
|
64
|
+
<input
|
|
65
|
+
:class="inputClass"
|
|
66
|
+
:value="config.agent || ''"
|
|
67
|
+
placeholder="Agent ID or email"
|
|
68
|
+
@input="update('agent', $event.target.value)"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<!-- Change Status -->
|
|
74
|
+
<template v-else-if="actionType === 'change_status'">
|
|
75
|
+
<div>
|
|
76
|
+
<label :class="labelClass">New status</label>
|
|
77
|
+
<select
|
|
78
|
+
:class="selectClass"
|
|
79
|
+
:value="config.status || ''"
|
|
80
|
+
@change="update('status', $event.target.value)"
|
|
81
|
+
>
|
|
82
|
+
<option value="">Select status...</option>
|
|
83
|
+
<option v-for="s in statusOptions" :key="s" :value="s">{{ s }}</option>
|
|
84
|
+
</select>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<!-- Change Priority -->
|
|
89
|
+
<template v-else-if="actionType === 'change_priority'">
|
|
90
|
+
<div>
|
|
91
|
+
<label :class="labelClass">New priority</label>
|
|
92
|
+
<select
|
|
93
|
+
:class="selectClass"
|
|
94
|
+
:value="config.priority || ''"
|
|
95
|
+
@change="update('priority', $event.target.value)"
|
|
96
|
+
>
|
|
97
|
+
<option value="">Select priority...</option>
|
|
98
|
+
<option v-for="p in priorityOptions" :key="p" :value="p">{{ p }}</option>
|
|
99
|
+
</select>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<!-- Add / Remove Tag -->
|
|
104
|
+
<template v-else-if="actionType === 'add_tag' || actionType === 'remove_tag'">
|
|
105
|
+
<div>
|
|
106
|
+
<label :class="labelClass">Tag name</label>
|
|
107
|
+
<input
|
|
108
|
+
:class="inputClass"
|
|
109
|
+
:value="config.tag || ''"
|
|
110
|
+
placeholder="Enter tag name"
|
|
111
|
+
@input="update('tag', $event.target.value)"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<!-- Move Department -->
|
|
117
|
+
<template v-else-if="actionType === 'move_department'">
|
|
118
|
+
<div>
|
|
119
|
+
<label :class="labelClass">Department</label>
|
|
120
|
+
<input
|
|
121
|
+
:class="inputClass"
|
|
122
|
+
:value="config.department || ''"
|
|
123
|
+
placeholder="Department name or ID"
|
|
124
|
+
@input="update('department', $event.target.value)"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
|
|
129
|
+
<!-- Add Internal Note -->
|
|
130
|
+
<template v-else-if="actionType === 'add_note'">
|
|
131
|
+
<div>
|
|
132
|
+
<label :class="labelClass">Note content</label>
|
|
133
|
+
<VariableTokenInput
|
|
134
|
+
:model-value="config.body || ''"
|
|
135
|
+
:multiline="true"
|
|
136
|
+
placeholder="Write the note..."
|
|
137
|
+
@update:model-value="update('body', $event)"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</template>
|
|
141
|
+
|
|
142
|
+
<!-- Send Email -->
|
|
143
|
+
<template v-else-if="actionType === 'send_email'">
|
|
144
|
+
<div>
|
|
145
|
+
<label :class="labelClass">To</label>
|
|
146
|
+
<VariableTokenInput
|
|
147
|
+
:model-value="config.to || ''"
|
|
148
|
+
placeholder="Recipient email or variable"
|
|
149
|
+
@update:model-value="update('to', $event)"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
<div>
|
|
153
|
+
<label :class="labelClass">Subject</label>
|
|
154
|
+
<VariableTokenInput
|
|
155
|
+
:model-value="config.subject || ''"
|
|
156
|
+
placeholder="Email subject"
|
|
157
|
+
@update:model-value="update('subject', $event)"
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
<div>
|
|
161
|
+
<label :class="labelClass">Body</label>
|
|
162
|
+
<VariableTokenInput
|
|
163
|
+
:model-value="config.body || ''"
|
|
164
|
+
:multiline="true"
|
|
165
|
+
placeholder="Email body..."
|
|
166
|
+
@update:model-value="update('body', $event)"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</template>
|
|
170
|
+
|
|
171
|
+
<!-- Send Webhook -->
|
|
172
|
+
<template v-else-if="actionType === 'send_webhook'">
|
|
173
|
+
<div>
|
|
174
|
+
<label :class="labelClass">URL</label>
|
|
175
|
+
<input
|
|
176
|
+
:class="inputClass"
|
|
177
|
+
:value="config.url || ''"
|
|
178
|
+
placeholder="https://..."
|
|
179
|
+
@input="update('url', $event.target.value)"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<label :class="labelClass">Method</label>
|
|
184
|
+
<select
|
|
185
|
+
:class="selectClass"
|
|
186
|
+
:value="config.method || 'POST'"
|
|
187
|
+
@change="update('method', $event.target.value)"
|
|
188
|
+
>
|
|
189
|
+
<option v-for="m in webhookMethods" :key="m" :value="m">{{ m }}</option>
|
|
190
|
+
</select>
|
|
191
|
+
</div>
|
|
192
|
+
<div>
|
|
193
|
+
<label :class="labelClass">Headers (JSON)</label>
|
|
194
|
+
<input
|
|
195
|
+
:class="inputClass"
|
|
196
|
+
:value="config.headers || ''"
|
|
197
|
+
:placeholder="headersPlaceholder"
|
|
198
|
+
@input="update('headers', $event.target.value)"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
<div>
|
|
202
|
+
<label :class="labelClass">Body template</label>
|
|
203
|
+
<VariableTokenInput
|
|
204
|
+
:model-value="config.body || ''"
|
|
205
|
+
:multiline="true"
|
|
206
|
+
placeholder="JSON body with variables..."
|
|
207
|
+
@update:model-value="update('body', $event)"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
</template>
|
|
211
|
+
|
|
212
|
+
<!-- Delay -->
|
|
213
|
+
<template v-else-if="actionType === 'delay'">
|
|
214
|
+
<div class="flex items-center gap-3">
|
|
215
|
+
<div class="flex-1">
|
|
216
|
+
<label :class="labelClass">Duration</label>
|
|
217
|
+
<input
|
|
218
|
+
type="number"
|
|
219
|
+
min="1"
|
|
220
|
+
:class="inputClass"
|
|
221
|
+
:value="config.duration || 1"
|
|
222
|
+
@input="update('duration', parseInt($event.target.value) || 1)"
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="flex-1">
|
|
226
|
+
<label :class="labelClass">Unit</label>
|
|
227
|
+
<select
|
|
228
|
+
:class="selectClass + ' w-full'"
|
|
229
|
+
:value="config.unit || 'hours'"
|
|
230
|
+
@change="update('unit', $event.target.value)"
|
|
231
|
+
>
|
|
232
|
+
<option v-for="u in delayUnits" :key="u.value" :value="u.value">{{ u.label }}</option>
|
|
233
|
+
</select>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="rounded-lg bg-orange-500/10 px-3 py-2 text-xs text-orange-400">
|
|
237
|
+
Wait {{ config.duration || 1 }} {{ config.unit || 'hours' }}, then continue...
|
|
238
|
+
</div>
|
|
239
|
+
</template>
|
|
240
|
+
|
|
241
|
+
<!-- Apply Macro -->
|
|
242
|
+
<template v-else-if="actionType === 'apply_macro'">
|
|
243
|
+
<div>
|
|
244
|
+
<label :class="labelClass">Macro</label>
|
|
245
|
+
<input
|
|
246
|
+
:class="inputClass"
|
|
247
|
+
:value="config.macro || ''"
|
|
248
|
+
placeholder="Macro name or ID"
|
|
249
|
+
@input="update('macro', $event.target.value)"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
</template>
|
|
253
|
+
|
|
254
|
+
<!-- Snooze -->
|
|
255
|
+
<template v-else-if="actionType === 'snooze'">
|
|
256
|
+
<div class="flex items-center gap-3">
|
|
257
|
+
<div class="flex-1">
|
|
258
|
+
<label :class="labelClass">Duration</label>
|
|
259
|
+
<input
|
|
260
|
+
type="number"
|
|
261
|
+
min="1"
|
|
262
|
+
:class="inputClass"
|
|
263
|
+
:value="config.duration || 1"
|
|
264
|
+
@input="update('duration', parseInt($event.target.value) || 1)"
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="flex-1">
|
|
268
|
+
<label :class="labelClass">Unit</label>
|
|
269
|
+
<select
|
|
270
|
+
:class="selectClass + ' w-full'"
|
|
271
|
+
:value="config.unit || 'hours'"
|
|
272
|
+
@change="update('unit', $event.target.value)"
|
|
273
|
+
>
|
|
274
|
+
<option v-for="u in delayUnits" :key="u.value" :value="u.value">{{ u.label }}</option>
|
|
275
|
+
</select>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</template>
|
|
279
|
+
|
|
280
|
+
<!-- Close Ticket (no config needed) -->
|
|
281
|
+
<template v-else-if="actionType === 'close_ticket'">
|
|
282
|
+
<p class="text-xs text-[var(--esc-panel-text-muted)]">
|
|
283
|
+
This action closes the ticket immediately. No configuration needed.
|
|
284
|
+
</p>
|
|
285
|
+
</template>
|
|
286
|
+
</div>
|
|
287
|
+
</template>
|