@escalated-dev/escalated 0.3.5 → 0.5.0

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.
Files changed (32) hide show
  1. package/README.md +35 -1
  2. package/package.json +9 -2
  3. package/src/components/BulkActionBar.vue +97 -0
  4. package/src/components/EscalatedLayout.vue +244 -219
  5. package/src/components/FollowButton.vue +63 -0
  6. package/src/components/KeyboardShortcutHelp.vue +123 -0
  7. package/src/components/MacroDropdown.vue +90 -0
  8. package/src/components/PinnedNotes.vue +87 -0
  9. package/src/components/PluginSlot.vue +62 -0
  10. package/src/components/PresenceIndicator.vue +103 -0
  11. package/src/components/QuickFilters.vue +49 -0
  12. package/src/components/ReplyThread.vue +60 -38
  13. package/src/components/SatisfactionRating.vue +128 -0
  14. package/src/components/TicketFilters.vue +69 -58
  15. package/src/components/TicketList.vue +193 -94
  16. package/src/components/TicketSidebar.vue +99 -77
  17. package/src/composables/useKeyboardShortcuts.js +46 -0
  18. package/src/composables/usePluginExtensions.js +67 -0
  19. package/src/defineEscalatedPlugin.js +50 -0
  20. package/src/index.js +16 -0
  21. package/src/pages/Admin/ApiTokens/Index.vue +217 -0
  22. package/src/pages/Admin/Macros/Index.vue +287 -0
  23. package/src/pages/Admin/Plugins/Index.vue +197 -0
  24. package/src/pages/Admin/Plugins.vue +129 -0
  25. package/src/pages/Admin/Reports.vue +101 -66
  26. package/src/pages/Admin/Tickets/Index.vue +39 -22
  27. package/src/pages/Admin/Tickets/Show.vue +156 -109
  28. package/src/pages/Agent/Dashboard.vue +186 -63
  29. package/src/pages/Agent/TicketIndex.vue +38 -21
  30. package/src/pages/Agent/TicketShow.vue +155 -108
  31. package/src/pages/Customer/Show.vue +63 -55
  32. package/src/pages/Guest/Show.vue +93 -86
package/README.md CHANGED
@@ -18,6 +18,7 @@ Pick your framework:
18
18
  | **Laravel** | [escalated-dev/escalated-laravel](https://github.com/escalated-dev/escalated-laravel) | `composer require escalated-dev/escalated-laravel` |
19
19
  | **Rails** | [escalated-dev/escalated-rails](https://github.com/escalated-dev/escalated-rails) | `gem "escalated"` |
20
20
  | **Django** | [escalated-dev/escalated-django](https://github.com/escalated-dev/escalated-django) | `pip install escalated-django` |
21
+ | **AdonisJS** | [escalated-dev/escalated-adonis](https://github.com/escalated-dev/escalated-adonis) | `npm install @escalated-dev/escalated-adonis` |
21
22
 
22
23
  Each backend repo has full setup instructions — install command, migrations, config, and frontend integration.
23
24
 
@@ -112,9 +113,17 @@ import BaseLayout from '@/layouts/BaseLayout.vue'
112
113
  app.use(EscalatedPlugin, { layout: BaseLayout })
113
114
  ```
114
115
 
116
+ **AdonisJS** (Inertia + Vue 3):
117
+ ```js
118
+ import { EscalatedPlugin } from '@escalated-dev/escalated'
119
+ import AppLayout from '@/layouts/AppLayout.vue'
120
+
121
+ app.use(EscalatedPlugin, { layout: AppLayout })
122
+ ```
123
+
115
124
  ## What's in This Repo
116
125
 
117
- All the Vue 3 + Inertia.js components that power the Escalated UI. These are identical across Laravel, Rails, and Django — the backend framework renders them via Inertia.
126
+ All the Vue 3 + Inertia.js components that power the Escalated UI. These are identical across Laravel, Rails, Django, and AdonisJS — the backend framework renders them via Inertia.
118
127
 
119
128
  ### Pages
120
129
 
@@ -157,6 +166,20 @@ Reusable building blocks used across the pages above.
157
166
  | `AttachmentList` | File attachment display with download links |
158
167
  | `StatsCard` | Metric card with label, value, and trend |
159
168
  | `EscalatedLayout` | Top-level layout with navigation (supports host layout injection) |
169
+ | `BulkActionBar` | Toolbar for batch operations on selected tickets |
170
+ | `QuickFilters` | One-click filter chips (My Tickets, Unassigned, Urgent, SLA Breaching) |
171
+ | `MacroDropdown` | Dropdown to apply multi-step macros to a ticket |
172
+ | `FollowButton` | Toggle button to follow/unfollow a ticket |
173
+ | `SatisfactionRating` | 1-5 star CSAT rating input with optional comment |
174
+ | `KeyboardShortcutHelp` | Modal overlay showing all available keyboard shortcuts |
175
+ | `PinnedNotes` | Display pinned internal notes at the top of the thread |
176
+ | `PresenceIndicator` | Real-time indicator showing who is viewing a ticket |
177
+
178
+ ### Composables
179
+
180
+ | Composable | Description |
181
+ |------------|-------------|
182
+ | `useKeyboardShortcuts` | Registers and manages keyboard shortcuts for ticket actions |
160
183
 
161
184
  ### Plugin
162
185
 
@@ -185,6 +208,17 @@ import CustomerIndex from '@escalated-dev/escalated/pages/Customer/Index.vue'
185
208
 
186
209
  Peer dependencies: `vue` ^3.3.0, `@inertiajs/vue3` ^1.0.0 || ^2.0.0
187
210
 
211
+ ## Ecosystem
212
+
213
+ This is the shared frontend for the Escalated support ticket system. Backend packages available for every major framework:
214
+
215
+ - **[Escalated for Laravel](https://github.com/escalated-dev/escalated-laravel)** — Laravel Composer package
216
+ - **[Escalated for Rails](https://github.com/escalated-dev/escalated-rails)** — Ruby on Rails engine
217
+ - **[Escalated for Django](https://github.com/escalated-dev/escalated-django)** — Django reusable app
218
+ - **[Escalated for AdonisJS](https://github.com/escalated-dev/escalated-adonis)** — AdonisJS v6 package
219
+ - **[Escalated for Filament](https://github.com/escalated-dev/escalated-filament)** — Filament v3 admin panel plugin
220
+ - **[Shared Frontend](https://github.com/escalated-dev/escalated)** — Vue 3 + Inertia.js UI components (you are here)
221
+
188
222
  ## License
189
223
 
190
224
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escalated-dev/escalated",
3
- "version": "0.3.5",
3
+ "version": "0.5.0",
4
4
  "description": "Vue 3 + Inertia.js UI components for Escalated — the embeddable support ticket system",
5
5
  "author": "Escalated Dev <hello@escalated.dev>",
6
6
  "license": "MIT",
@@ -30,8 +30,15 @@
30
30
  "vue": "^3.3.0",
31
31
  "@inertiajs/vue3": "^1.0.0 || ^2.0.0"
32
32
  },
33
+ "scripts": {
34
+ "test": "vitest run"
35
+ },
33
36
  "devDependencies": {
34
37
  "vue": "^3.5.0",
35
- "@inertiajs/vue3": "^2.0.0"
38
+ "@inertiajs/vue3": "^2.0.0",
39
+ "@vitejs/plugin-vue": "^5.0.0",
40
+ "@vue/test-utils": "^2.4.0",
41
+ "happy-dom": "^15.0.0",
42
+ "vitest": "^2.0.0"
36
43
  }
37
44
  }
@@ -0,0 +1,97 @@
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ selectedIds: { type: Array, required: true },
7
+ bulkRoute: { type: String, required: true },
8
+ agents: { type: Array, default: () => [] },
9
+ departments: { type: Array, default: () => [] },
10
+ });
11
+
12
+ const emit = defineEmits(['clear']);
13
+ const escDark = inject('esc-dark', computed(() => false));
14
+ const processing = ref(false);
15
+
16
+ function performAction(action, value) {
17
+ if (!props.selectedIds.length || processing.value) return;
18
+ processing.value = true;
19
+ router.post(props.bulkRoute, {
20
+ ticket_ids: props.selectedIds,
21
+ action,
22
+ value,
23
+ }, {
24
+ preserveScroll: true,
25
+ onSuccess: () => emit('clear'),
26
+ onFinish: () => { processing.value = false; },
27
+ });
28
+ }
29
+
30
+ const statusOptions = [
31
+ { value: 'in_progress', label: 'In Progress' },
32
+ { value: 'waiting_on_customer', label: 'Waiting on Customer' },
33
+ { value: 'resolved', label: 'Resolved' },
34
+ { value: 'closed', label: 'Closed' },
35
+ ];
36
+
37
+ const priorityOptions = [
38
+ { value: 'low', label: 'Low' },
39
+ { value: 'medium', label: 'Medium' },
40
+ { value: 'high', label: 'High' },
41
+ { value: 'urgent', label: 'Urgent' },
42
+ { value: 'critical', label: 'Critical' },
43
+ ];
44
+
45
+ const selectClass = computed(() => escDark.value
46
+ ? 'rounded-lg border border-white/10 bg-neutral-950 px-2 py-1.5 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10'
47
+ : 'rounded-md border border-gray-300 bg-white px-2 py-1.5 text-sm focus:border-blue-500 focus:outline-none'
48
+ );
49
+ </script>
50
+
51
+ <template>
52
+ <Transition enter-active-class="transition-all duration-200 ease-out" enter-from-class="translate-y-full opacity-0" enter-to-class="translate-y-0 opacity-100"
53
+ leave-active-class="transition-all duration-150 ease-in" leave-from-class="translate-y-0 opacity-100" leave-to-class="translate-y-full opacity-0">
54
+ <div v-if="selectedIds.length > 0"
55
+ :class="['fixed bottom-0 left-0 right-0 z-50 border-t px-6 py-3',
56
+ escDark ? 'border-white/[0.06] bg-neutral-950/95 backdrop-blur-xl' : 'border-gray-200 bg-white/95 backdrop-blur-xl shadow-lg']">
57
+ <div class="mx-auto flex max-w-7xl items-center gap-4">
58
+ <span :class="['text-sm font-semibold', escDark ? 'text-white' : 'text-gray-900']">
59
+ {{ selectedIds.length }} ticket{{ selectedIds.length !== 1 ? 's' : '' }} selected
60
+ </span>
61
+
62
+ <select @change="performAction('status', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
63
+ <option value="">Status...</option>
64
+ <option v-for="o in statusOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
65
+ </select>
66
+
67
+ <select @change="performAction('priority', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
68
+ <option value="">Priority...</option>
69
+ <option v-for="o in priorityOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
70
+ </select>
71
+
72
+ <select v-if="agents.length" @change="performAction('assign', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
73
+ <option value="">Assign to...</option>
74
+ <option v-for="a in agents" :key="a.id" :value="a.id">{{ a.name }}</option>
75
+ </select>
76
+
77
+ <select v-if="departments.length" @change="performAction('department', $event.target.value); $event.target.value = ''" :class="selectClass" :disabled="processing">
78
+ <option value="">Department...</option>
79
+ <option v-for="d in departments" :key="d.id" :value="d.id">{{ d.name }}</option>
80
+ </select>
81
+
82
+ <button @click="performAction('delete', null)"
83
+ :class="['rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
84
+ escDark ? 'bg-rose-500/15 text-rose-400 ring-1 ring-rose-500/20 hover:bg-rose-500/25' : 'bg-red-50 text-red-600 ring-1 ring-red-200 hover:bg-red-100']"
85
+ :disabled="processing">
86
+ Delete
87
+ </button>
88
+
89
+ <button @click="emit('clear')"
90
+ :class="['ml-auto rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
91
+ escDark ? 'text-neutral-400 hover:bg-white/[0.04] hover:text-neutral-200' : 'text-gray-500 hover:bg-gray-100 hover:text-gray-700']">
92
+ Deselect All
93
+ </button>
94
+ </div>
95
+ </div>
96
+ </Transition>
97
+ </template>