@escalated-dev/escalated 0.9.0 → 0.11.2
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/README.md +13 -5
- package/package.json +86 -83
- package/src/components/EscalatedLayout.vue +9 -0
- package/src/index.js +4 -0
- package/src/pages/Admin/Automations/Form.vue +280 -0
- package/src/pages/Admin/Automations/Index.vue +230 -0
- package/src/pages/Admin/Plugins/Index.vue +10 -2
- package/src/pages/Admin/Settings/DatabaseConnection.vue +228 -0
- package/src/pages/Admin/Settings/PublicTickets.vue +163 -0
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +1 -2
- package/src/pages/Admin/Workflows/Form.vue +24 -0
- package/src/pages/Plugin/Page.vue +61 -0
- package/src/resolvePage.js +160 -0
package/README.md
CHANGED
|
@@ -15,19 +15,23 @@
|
|
|
15
15
|
<a href="docs/translations/README.zh-CN.md">简体中文</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
<h1>
|
|
18
|
+
<h1 align="center">
|
|
19
19
|
<img src="https://escalated.dev/apple-touch-icon.png" width="28" style="vertical-align:middle;" />
|
|
20
20
|
Escalated
|
|
21
21
|
</h1>
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
[](https://www.npmjs.com/package/@escalated-dev/escalated)
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+
[](https://github.com/escalated-dev/escalated/actions/workflows/run-tests.yml)[](https://www.npmjs.com/package/@escalated-dev/escalated)
|
|
26
26
|
[](https://vuejs.org/)
|
|
27
27
|
[](https://opensource.org/licenses/MIT)
|
|
28
28
|
|
|
29
|
+
</div>
|
|
30
|
+
|
|
29
31
|
<p align="center">
|
|
30
|
-
<
|
|
32
|
+
<a href="https://escalated.dev">
|
|
33
|
+
<img src=".github/og.png" alt="Escalated — support tickets, built into your app" width="840" />
|
|
34
|
+
</a>
|
|
31
35
|
</p>
|
|
32
36
|
|
|
33
37
|
Escalated is an embeddable support ticket system with SLA tracking, escalation rules, agent workflows, and a customer portal. This repo contains all the shared frontend assets (Vue 3 + Inertia.js) used across every supported backend framework.
|
|
@@ -36,6 +40,10 @@ Escalated is an embeddable support ticket system with SLA tracking, escalation r
|
|
|
36
40
|
|
|
37
41
|
**You don't install this package directly.** Start with the backend package for your framework — it handles everything including pulling in these frontend assets.
|
|
38
42
|
|
|
43
|
+
<p align="center">
|
|
44
|
+
<img src=".github/profile/demo.gif" alt="Escalated demo: filing a ticket and replying from the agent inbox" width="800" />
|
|
45
|
+
</p>
|
|
46
|
+
|
|
39
47
|
## Features
|
|
40
48
|
|
|
41
49
|
- **Ticket splitting** — Split a reply into a new standalone ticket while preserving context
|
package/package.json
CHANGED
|
@@ -1,85 +1,88 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
2
|
+
"name": "@escalated-dev/escalated",
|
|
3
|
+
"version": "0.11.2",
|
|
4
|
+
"description": "Vue 3 + Inertia.js UI components for Escalated — the embeddable support ticket system",
|
|
5
|
+
"author": "Escalated Dev <hello@escalated.dev>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/escalated-dev/escalated",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/escalated-dev/escalated.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"vue",
|
|
14
|
+
"inertia",
|
|
15
|
+
"support",
|
|
16
|
+
"tickets",
|
|
17
|
+
"helpdesk",
|
|
18
|
+
"escalated"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
"./components/*": "./src/components/*",
|
|
23
|
+
"./pages/*": "./src/pages/*",
|
|
24
|
+
"./locales": "./src/locales/index.js",
|
|
25
|
+
"./locales/*": "./src/locales/*",
|
|
26
|
+
".": "./src/index.js",
|
|
27
|
+
"./pages.json": "./pages.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src"
|
|
31
|
+
],
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@inertiajs/vue3": "^1.0.0 || ^2.0.0 || ^3.0.0",
|
|
34
|
+
"vue": "^3.3.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@escalated-dev/locale": "^0.1.8"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"lint": "eslint src/ --max-warnings 0",
|
|
42
|
+
"lint:fix": "eslint src/ --fix",
|
|
43
|
+
"format": "prettier --write src/",
|
|
44
|
+
"format:check": "prettier --check src/",
|
|
45
|
+
"prepare": "husky",
|
|
46
|
+
"storybook": "storybook dev -p 6006",
|
|
47
|
+
"build-storybook": "storybook build",
|
|
48
|
+
"screenshot": "npx playwright test --config=playwright-screenshots.config.js",
|
|
49
|
+
"demo:serve": "node scripts/demo-server.js",
|
|
50
|
+
"demo:record": "node scripts/generate-demo.js",
|
|
51
|
+
"demo": "concurrently -k -s second \"npm run demo:serve\" \"wait-on http://localhost:3000 && npm run demo:record\"",
|
|
52
|
+
"pages:manifest": "node scripts/generate-page-manifest.js",
|
|
53
|
+
"pages:check": "node scripts/generate-page-manifest.js --check"
|
|
54
|
+
},
|
|
55
|
+
"lint-staged": {
|
|
56
|
+
"*.{js,vue}": [
|
|
57
|
+
"eslint --fix",
|
|
58
|
+
"prettier --write"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@inertiajs/vue3": "^3.7.0",
|
|
64
|
+
"@playwright/test": "^1.62.1",
|
|
65
|
+
"@storybook/addon-a11y": "^10.6.0",
|
|
66
|
+
"@storybook/addon-docs": "^10.6.0",
|
|
67
|
+
"@storybook/vue3-vite": "^10.6.0",
|
|
68
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
69
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
70
|
+
"@vue/test-utils": "^2.5.0",
|
|
71
|
+
"autoprefixer": "^10.5.4",
|
|
72
|
+
"concurrently": "^10.0.5",
|
|
73
|
+
"eslint": "^10.9.1",
|
|
74
|
+
"eslint-plugin-storybook": "^10.5.10",
|
|
75
|
+
"eslint-plugin-vue": "^10.10.0",
|
|
76
|
+
"happy-dom": "^20.14.0",
|
|
77
|
+
"husky": "^9.1.7",
|
|
78
|
+
"lint-staged": "^17.4.1",
|
|
79
|
+
"playwright": "^1.62.1",
|
|
80
|
+
"postcss": "^8.5.26",
|
|
81
|
+
"prettier": "^3.9.6",
|
|
82
|
+
"storybook": "^10.5.10",
|
|
83
|
+
"tailwindcss": "^4.3.3",
|
|
84
|
+
"vitest": "^5.0.0",
|
|
85
|
+
"vue": "^3.5.42",
|
|
86
|
+
"wait-on": "^9.1.0"
|
|
87
|
+
}
|
|
85
88
|
}
|
|
@@ -85,6 +85,15 @@ const adminLinks = computed(() => {
|
|
|
85
85
|
icon: 'M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
|
|
86
86
|
position: 53,
|
|
87
87
|
},
|
|
88
|
+
{
|
|
89
|
+
href: `${p}/admin/automations`,
|
|
90
|
+
label: 'Automations',
|
|
91
|
+
// Time-based rules, next to the event-driven ones. The two are
|
|
92
|
+
// separate surfaces and always have been; this entry was simply
|
|
93
|
+
// never added, so the screen the backends render had no way in.
|
|
94
|
+
icon: 'M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
95
|
+
position: 53.5,
|
|
96
|
+
},
|
|
88
97
|
{
|
|
89
98
|
href: `${p}/admin/webhooks`,
|
|
90
99
|
label: 'Webhooks',
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Plugin
|
|
2
2
|
export { EscalatedPlugin } from './plugin';
|
|
3
3
|
|
|
4
|
+
// Page resolution. A page name with no component behind it resolves to nothing
|
|
5
|
+
// in Inertia -- a 200 and a blank panel -- so this says so instead.
|
|
6
|
+
export { resolveEscalatedPage, isEscalatedPage, escalatedPages, suggestPages } from './resolvePage';
|
|
7
|
+
|
|
4
8
|
// Components
|
|
5
9
|
export { default as ActiveChatsPanel } from './components/ActiveChatsPanel.vue';
|
|
6
10
|
export { default as ActivityTimeline } from './components/ActivityTimeline.vue';
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import EscalatedLayout from '../../../components/EscalatedLayout.vue';
|
|
3
|
+
import { Link, useForm } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create and edit a time-based automation.
|
|
7
|
+
*
|
|
8
|
+
* The vocabulary here is the runner's: every field, operator and action type
|
|
9
|
+
* below is one the backend actually evaluates. Offering anything else would
|
|
10
|
+
* produce a rule that saves cleanly and then silently never matches.
|
|
11
|
+
*/
|
|
12
|
+
const props = defineProps({
|
|
13
|
+
automation: { type: Object, default: null },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/** Fields that count hours take an operator and a number; the rest take a value. */
|
|
17
|
+
const TIME_FIELDS = ['hours_since_created', 'hours_since_updated', 'hours_since_assigned'];
|
|
18
|
+
|
|
19
|
+
const FIELDS = [
|
|
20
|
+
{ value: 'hours_since_created', label: 'Hours since created' },
|
|
21
|
+
{ value: 'hours_since_updated', label: 'Hours since last update' },
|
|
22
|
+
{ value: 'hours_since_assigned', label: 'Hours since assigned' },
|
|
23
|
+
{ value: 'status', label: 'Status' },
|
|
24
|
+
{ value: 'priority', label: 'Priority' },
|
|
25
|
+
{ value: 'assigned', label: 'Assignment' },
|
|
26
|
+
{ value: 'ticket_type', label: 'Ticket type' },
|
|
27
|
+
{ value: 'subject_contains', label: 'Subject contains' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const OPERATORS = [
|
|
31
|
+
{ value: '>', label: 'more than' },
|
|
32
|
+
{ value: '>=', label: 'at least' },
|
|
33
|
+
{ value: '<', label: 'fewer than' },
|
|
34
|
+
{ value: '<=', label: 'at most' },
|
|
35
|
+
{ value: '=', label: 'exactly' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const ACTIONS = [
|
|
39
|
+
{ value: 'change_status', label: 'Change status' },
|
|
40
|
+
{ value: 'change_priority', label: 'Change priority' },
|
|
41
|
+
{ value: 'assign', label: 'Assign to agent' },
|
|
42
|
+
{ value: 'add_tag', label: 'Add tag' },
|
|
43
|
+
{ value: 'set_ticket_type', label: 'Set ticket type' },
|
|
44
|
+
{ value: 'add_note', label: 'Add internal note' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const ASSIGNMENT_VALUES = [
|
|
48
|
+
{ value: 'assigned', label: 'Assigned' },
|
|
49
|
+
{ value: 'unassigned', label: 'Unassigned' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const form = useForm({
|
|
53
|
+
name: props.automation?.name ?? '',
|
|
54
|
+
conditions: (props.automation?.conditions ?? []).map((c) => ({
|
|
55
|
+
field: c.field ?? 'hours_since_updated',
|
|
56
|
+
operator: c.operator ?? '>',
|
|
57
|
+
value: c.value ?? '',
|
|
58
|
+
})),
|
|
59
|
+
actions: (props.automation?.actions ?? []).map((a) => ({
|
|
60
|
+
type: a.type ?? 'change_status',
|
|
61
|
+
value: a.value ?? '',
|
|
62
|
+
})),
|
|
63
|
+
active: props.automation?.active ?? true,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// A new automation starts with one of each, because an empty one cannot be
|
|
67
|
+
// saved: the backend requires at least one condition and one action.
|
|
68
|
+
if (!form.conditions.length) {
|
|
69
|
+
form.conditions.push({ field: 'hours_since_updated', operator: '>', value: 24 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!form.actions.length) {
|
|
73
|
+
form.actions.push({ type: 'change_status', value: '' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isTimeField(field) {
|
|
77
|
+
return TIME_FIELDS.includes(field);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function addCondition() {
|
|
81
|
+
form.conditions.push({ field: 'hours_since_updated', operator: '>', value: 24 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeCondition(index) {
|
|
85
|
+
form.conditions.splice(index, 1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function addAction() {
|
|
89
|
+
form.actions.push({ type: 'change_status', value: '' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function removeAction(index) {
|
|
93
|
+
form.actions.splice(index, 1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function submit() {
|
|
97
|
+
if (props.automation?.id) {
|
|
98
|
+
form.put(route('escalated.admin.automations.update', props.automation.id));
|
|
99
|
+
} else {
|
|
100
|
+
form.post(route('escalated.admin.automations.store'));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<EscalatedLayout :title="automation ? 'Edit Automation' : 'Create Automation'">
|
|
107
|
+
<div class="mb-4">
|
|
108
|
+
<Link
|
|
109
|
+
:href="route('escalated.admin.automations.index')"
|
|
110
|
+
class="text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-accent)]"
|
|
111
|
+
>
|
|
112
|
+
← Back to automations
|
|
113
|
+
</Link>
|
|
114
|
+
<h2 class="mt-2 text-lg font-semibold text-[var(--esc-panel-text-secondary)]">
|
|
115
|
+
{{ automation ? 'Edit Automation' : 'Create Automation' }}
|
|
116
|
+
</h2>
|
|
117
|
+
<p class="text-xs text-[var(--esc-panel-text-muted)]">
|
|
118
|
+
Runs on a schedule against open tickets. Every condition must match for the actions to apply.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<form class="space-y-6" @submit.prevent="submit">
|
|
123
|
+
<div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4">
|
|
124
|
+
<label class="block text-xs font-medium text-[var(--esc-panel-text-muted)]" for="automation-name">
|
|
125
|
+
Name
|
|
126
|
+
</label>
|
|
127
|
+
<input
|
|
128
|
+
id="automation-name"
|
|
129
|
+
v-model="form.name"
|
|
130
|
+
type="text"
|
|
131
|
+
required
|
|
132
|
+
maxlength="255"
|
|
133
|
+
placeholder="Escalate tickets untouched for a day"
|
|
134
|
+
class="mt-1 w-full rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)] focus:border-[var(--esc-panel-accent)] focus:outline-none"
|
|
135
|
+
/>
|
|
136
|
+
<p v-if="form.errors.name" class="mt-1 text-xs text-red-400">{{ form.errors.name }}</p>
|
|
137
|
+
|
|
138
|
+
<label class="mt-4 flex items-center gap-2 text-sm text-[var(--esc-panel-text-secondary)]">
|
|
139
|
+
<input v-model="form.active" type="checkbox" class="rounded border-[var(--esc-panel-border)]" />
|
|
140
|
+
Active
|
|
141
|
+
</label>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4">
|
|
145
|
+
<div class="mb-3 flex items-center justify-between">
|
|
146
|
+
<div>
|
|
147
|
+
<h3 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">When</h3>
|
|
148
|
+
<p class="text-xs text-[var(--esc-panel-text-muted)]">All conditions must match</p>
|
|
149
|
+
</div>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
class="rounded-lg border border-[var(--esc-panel-border)] px-3 py-1.5 text-xs font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
|
|
153
|
+
@click="addCondition"
|
|
154
|
+
>
|
|
155
|
+
Add condition
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div v-for="(condition, i) in form.conditions" :key="`condition-${i}`" class="mb-2 flex gap-2">
|
|
160
|
+
<select
|
|
161
|
+
v-model="condition.field"
|
|
162
|
+
:aria-label="`Condition ${i + 1} field`"
|
|
163
|
+
class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
|
|
164
|
+
>
|
|
165
|
+
<option v-for="field in FIELDS" :key="field.value" :value="field.value">
|
|
166
|
+
{{ field.label }}
|
|
167
|
+
</option>
|
|
168
|
+
</select>
|
|
169
|
+
|
|
170
|
+
<select
|
|
171
|
+
v-if="isTimeField(condition.field)"
|
|
172
|
+
v-model="condition.operator"
|
|
173
|
+
:aria-label="`Condition ${i + 1} operator`"
|
|
174
|
+
class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
|
|
175
|
+
>
|
|
176
|
+
<option v-for="operator in OPERATORS" :key="operator.value" :value="operator.value">
|
|
177
|
+
{{ operator.label }}
|
|
178
|
+
</option>
|
|
179
|
+
</select>
|
|
180
|
+
|
|
181
|
+
<select
|
|
182
|
+
v-if="condition.field === 'assigned'"
|
|
183
|
+
v-model="condition.value"
|
|
184
|
+
:aria-label="`Condition ${i + 1} value`"
|
|
185
|
+
class="flex-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
|
|
186
|
+
>
|
|
187
|
+
<option v-for="option in ASSIGNMENT_VALUES" :key="option.value" :value="option.value">
|
|
188
|
+
{{ option.label }}
|
|
189
|
+
</option>
|
|
190
|
+
</select>
|
|
191
|
+
<input
|
|
192
|
+
v-else
|
|
193
|
+
v-model="condition.value"
|
|
194
|
+
:type="isTimeField(condition.field) ? 'number' : 'text'"
|
|
195
|
+
:min="isTimeField(condition.field) ? 0 : undefined"
|
|
196
|
+
:aria-label="`Condition ${i + 1} value`"
|
|
197
|
+
:placeholder="isTimeField(condition.field) ? 'hours' : 'value'"
|
|
198
|
+
class="flex-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
:aria-label="`Remove condition ${i + 1}`"
|
|
204
|
+
class="rounded-lg px-2 text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:text-red-400"
|
|
205
|
+
:disabled="form.conditions.length === 1"
|
|
206
|
+
@click="removeCondition(i)"
|
|
207
|
+
>
|
|
208
|
+
Remove
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<p v-if="form.errors.conditions" class="mt-1 text-xs text-red-400">{{ form.errors.conditions }}</p>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div class="rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4">
|
|
216
|
+
<div class="mb-3 flex items-center justify-between">
|
|
217
|
+
<div>
|
|
218
|
+
<h3 class="text-sm font-semibold text-[var(--esc-panel-text-secondary)]">Then</h3>
|
|
219
|
+
<p class="text-xs text-[var(--esc-panel-text-muted)]">Applied to every matching ticket</p>
|
|
220
|
+
</div>
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
class="rounded-lg border border-[var(--esc-panel-border)] px-3 py-1.5 text-xs font-medium text-[var(--esc-panel-text-secondary)] transition-colors hover:bg-[var(--esc-panel-hover)]"
|
|
224
|
+
@click="addAction"
|
|
225
|
+
>
|
|
226
|
+
Add action
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div v-for="(action, i) in form.actions" :key="`action-${i}`" class="mb-2 flex gap-2">
|
|
231
|
+
<select
|
|
232
|
+
v-model="action.type"
|
|
233
|
+
:aria-label="`Action ${i + 1} type`"
|
|
234
|
+
class="rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
|
|
235
|
+
>
|
|
236
|
+
<option v-for="option in ACTIONS" :key="option.value" :value="option.value">
|
|
237
|
+
{{ option.label }}
|
|
238
|
+
</option>
|
|
239
|
+
</select>
|
|
240
|
+
|
|
241
|
+
<input
|
|
242
|
+
v-model="action.value"
|
|
243
|
+
type="text"
|
|
244
|
+
:aria-label="`Action ${i + 1} value`"
|
|
245
|
+
placeholder="value"
|
|
246
|
+
class="flex-1 rounded-lg border border-[var(--esc-panel-border)] bg-[var(--esc-panel-bg)] px-3 py-2 text-sm text-[var(--esc-panel-text-secondary)]"
|
|
247
|
+
/>
|
|
248
|
+
|
|
249
|
+
<button
|
|
250
|
+
type="button"
|
|
251
|
+
:aria-label="`Remove action ${i + 1}`"
|
|
252
|
+
class="rounded-lg px-2 text-xs text-[var(--esc-panel-text-muted)] transition-colors hover:text-red-400"
|
|
253
|
+
:disabled="form.actions.length === 1"
|
|
254
|
+
@click="removeAction(i)"
|
|
255
|
+
>
|
|
256
|
+
Remove
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<p v-if="form.errors.actions" class="mt-1 text-xs text-red-400">{{ form.errors.actions }}</p>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div class="flex items-center gap-3">
|
|
264
|
+
<button
|
|
265
|
+
type="submit"
|
|
266
|
+
:disabled="form.processing"
|
|
267
|
+
class="rounded-lg bg-gradient-to-r from-[var(--esc-panel-accent)] to-[var(--esc-panel-accent-secondary)] px-4 py-2 text-sm font-medium text-white transition-all hover:from-[var(--esc-panel-accent-hover)] hover:to-[var(--esc-panel-accent-secondary-hover)] disabled:opacity-50"
|
|
268
|
+
>
|
|
269
|
+
{{ automation ? 'Save changes' : 'Create automation' }}
|
|
270
|
+
</button>
|
|
271
|
+
<Link
|
|
272
|
+
:href="route('escalated.admin.automations.index')"
|
|
273
|
+
class="text-sm text-[var(--esc-panel-text-muted)] transition-colors hover:text-[var(--esc-panel-text-secondary)]"
|
|
274
|
+
>
|
|
275
|
+
Cancel
|
|
276
|
+
</Link>
|
|
277
|
+
</div>
|
|
278
|
+
</form>
|
|
279
|
+
</EscalatedLayout>
|
|
280
|
+
</template>
|