@escalated-dev/escalated 0.4.0 → 0.6.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.
- package/README.md +45 -1
- package/package.json +14 -5
- package/src/components/ActivityTimeline.vue +66 -66
- package/src/components/AssigneeSelect.vue +23 -23
- package/src/components/EscalatedLayout.vue +35 -11
- package/src/components/FileDropzone.vue +41 -41
- package/src/components/PinnedNotes.vue +6 -3
- package/src/components/PluginSlot.vue +62 -0
- package/src/components/PriorityBadge.vue +39 -39
- package/src/components/ReplyComposer.vue +159 -159
- package/src/components/ReplyThread.vue +9 -6
- package/src/components/SlaTimer.vue +49 -49
- package/src/components/StatusBadge.vue +46 -46
- package/src/components/TagSelect.vue +53 -53
- package/src/composables/useI18n.js +80 -0
- package/src/composables/usePluginExtensions.js +67 -0
- package/src/defineEscalatedPlugin.js +50 -0
- package/src/index.js +5 -0
- package/src/locales/de.json +442 -0
- package/src/locales/en.json +442 -0
- package/src/locales/es.json +442 -0
- package/src/locales/fr.json +442 -0
- package/src/locales/index.js +6 -0
- package/src/pages/Admin/ApiTokens/Index.vue +217 -0
- package/src/pages/Admin/Plugins/Index.vue +197 -0
- package/src/pages/Admin/Plugins.vue +129 -0
- package/src/pages/Admin/Tickets/Show.vue +12 -1
- package/src/pages/Agent/Dashboard.vue +31 -1
- package/src/pages/Agent/TicketShow.vue +12 -1
- package/src/utils/sanitizeHtml.js +64 -0
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { inject, computed } from 'vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
status: { type: String, required: true },
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
const escDark = inject('esc-dark', computed(() => false));
|
|
9
|
-
|
|
10
|
-
const darkConfig = {
|
|
11
|
-
open: { label: 'Open', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
|
|
12
|
-
in_progress: { label: 'In Progress', color: 'bg-violet-500/10 text-violet-400 ring-1 ring-violet-500/20' },
|
|
13
|
-
waiting_on_customer: { label: 'Waiting on Customer', color: 'bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20' },
|
|
14
|
-
waiting_on_agent: { label: 'Waiting on Agent', color: 'bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20' },
|
|
15
|
-
escalated: { label: 'Escalated', color: 'bg-rose-500/10 text-rose-400 ring-1 ring-rose-500/20' },
|
|
16
|
-
resolved: { label: 'Resolved', color: 'bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20' },
|
|
17
|
-
closed: { label: 'Closed', color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' },
|
|
18
|
-
reopened: { label: 'Reopened', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const lightConfig = {
|
|
22
|
-
open: { label: 'Open', color: 'bg-blue-100 text-blue-800' },
|
|
23
|
-
in_progress: { label: 'In Progress', color: 'bg-purple-100 text-purple-800' },
|
|
24
|
-
waiting_on_customer: { label: 'Waiting on Customer', color: 'bg-yellow-100 text-yellow-800' },
|
|
25
|
-
waiting_on_agent: { label: 'Waiting on Agent', color: 'bg-orange-100 text-orange-800' },
|
|
26
|
-
escalated: { label: 'Escalated', color: 'bg-red-100 text-red-800' },
|
|
27
|
-
resolved: { label: 'Resolved', color: 'bg-green-100 text-green-800' },
|
|
28
|
-
closed: { label: 'Closed', color: 'bg-gray-100 text-gray-800' },
|
|
29
|
-
reopened: { label: 'Reopened', color: 'bg-blue-100 text-blue-800' },
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const fallback = { label: props.status, color: 'bg-gray-100 text-gray-800' };
|
|
33
|
-
const darkFallback = { label: props.status, color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' };
|
|
34
|
-
|
|
35
|
-
const config = computed(() => {
|
|
36
|
-
const map = escDark.value ? darkConfig : lightConfig;
|
|
37
|
-
const fb = escDark.value ? darkFallback : fallback;
|
|
38
|
-
return map[props.status] || fb;
|
|
39
|
-
});
|
|
40
|
-
</script>
|
|
41
|
-
|
|
42
|
-
<template>
|
|
43
|
-
<span :class="['inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', config.color]">
|
|
44
|
-
{{ config.label }}
|
|
45
|
-
</span>
|
|
46
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { inject, computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
status: { type: String, required: true },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const escDark = inject('esc-dark', computed(() => false));
|
|
9
|
+
|
|
10
|
+
const darkConfig = {
|
|
11
|
+
open: { label: 'Open', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
|
|
12
|
+
in_progress: { label: 'In Progress', color: 'bg-violet-500/10 text-violet-400 ring-1 ring-violet-500/20' },
|
|
13
|
+
waiting_on_customer: { label: 'Waiting on Customer', color: 'bg-amber-500/10 text-amber-400 ring-1 ring-amber-500/20' },
|
|
14
|
+
waiting_on_agent: { label: 'Waiting on Agent', color: 'bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20' },
|
|
15
|
+
escalated: { label: 'Escalated', color: 'bg-rose-500/10 text-rose-400 ring-1 ring-rose-500/20' },
|
|
16
|
+
resolved: { label: 'Resolved', color: 'bg-emerald-500/10 text-emerald-400 ring-1 ring-emerald-500/20' },
|
|
17
|
+
closed: { label: 'Closed', color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' },
|
|
18
|
+
reopened: { label: 'Reopened', color: 'bg-cyan-500/10 text-white ring-1 ring-cyan-500/20' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const lightConfig = {
|
|
22
|
+
open: { label: 'Open', color: 'bg-blue-100 text-blue-800' },
|
|
23
|
+
in_progress: { label: 'In Progress', color: 'bg-purple-100 text-purple-800' },
|
|
24
|
+
waiting_on_customer: { label: 'Waiting on Customer', color: 'bg-yellow-100 text-yellow-800' },
|
|
25
|
+
waiting_on_agent: { label: 'Waiting on Agent', color: 'bg-orange-100 text-orange-800' },
|
|
26
|
+
escalated: { label: 'Escalated', color: 'bg-red-100 text-red-800' },
|
|
27
|
+
resolved: { label: 'Resolved', color: 'bg-green-100 text-green-800' },
|
|
28
|
+
closed: { label: 'Closed', color: 'bg-gray-100 text-gray-800' },
|
|
29
|
+
reopened: { label: 'Reopened', color: 'bg-blue-100 text-blue-800' },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const fallback = { label: props.status, color: 'bg-gray-100 text-gray-800' };
|
|
33
|
+
const darkFallback = { label: props.status, color: 'bg-gray-500/10 text-gray-400 ring-1 ring-gray-500/20' };
|
|
34
|
+
|
|
35
|
+
const config = computed(() => {
|
|
36
|
+
const map = escDark.value ? darkConfig : lightConfig;
|
|
37
|
+
const fb = escDark.value ? darkFallback : fallback;
|
|
38
|
+
return map[props.status] || fb;
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<span :class="['inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium', config.color]">
|
|
44
|
+
{{ config.label }}
|
|
45
|
+
</span>
|
|
46
|
+
</template>
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, computed, inject } from 'vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
tags: { type: Array, required: true },
|
|
6
|
-
modelValue: { type: Array, default: () => [] },
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
const emit = defineEmits(['update:modelValue']);
|
|
10
|
-
const dark = inject('esc-dark', computed(() => false));
|
|
11
|
-
|
|
12
|
-
const search = ref('');
|
|
13
|
-
|
|
14
|
-
const filteredTags = computed(() => {
|
|
15
|
-
if (!search.value) return props.tags;
|
|
16
|
-
const q = search.value.toLowerCase();
|
|
17
|
-
return props.tags.filter(t => t.name.toLowerCase().includes(q));
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function toggle(tagId) {
|
|
21
|
-
const current = [...props.modelValue];
|
|
22
|
-
const index = current.indexOf(tagId);
|
|
23
|
-
if (index >= 0) {
|
|
24
|
-
current.splice(index, 1);
|
|
25
|
-
} else {
|
|
26
|
-
current.push(tagId);
|
|
27
|
-
}
|
|
28
|
-
emit('update:modelValue', current);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isSelected(tagId) {
|
|
32
|
-
return props.modelValue.includes(tagId);
|
|
33
|
-
}
|
|
34
|
-
</script>
|
|
35
|
-
|
|
36
|
-
<template>
|
|
37
|
-
<div>
|
|
38
|
-
<label :class="['mb-1 block text-xs font-medium', dark ? 'text-neutral-500' : 'text-gray-600']">Tags</label>
|
|
39
|
-
<input v-model="search" type="text" placeholder="Filter tags..."
|
|
40
|
-
:class="['mb-2 w-full rounded-md border px-2 py-1 text-xs focus:outline-none',
|
|
41
|
-
dark ? 'border-white/10 bg-neutral-950 text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:ring-1 focus:ring-white/10' : 'border-gray-300 focus:border-blue-500']" />
|
|
42
|
-
<div class="flex flex-wrap gap-1">
|
|
43
|
-
<button v-for="tag in filteredTags" :key="tag.id" @click="toggle(tag.id)"
|
|
44
|
-
:class="['rounded-full px-2 py-0.5 text-xs font-medium transition-colors',
|
|
45
|
-
dark
|
|
46
|
-
? (isSelected(tag.id) ? 'bg-white/[0.12] text-white ring-1 ring-white/20' : 'bg-white/[0.04] text-neutral-500 hover:bg-white/[0.08]')
|
|
47
|
-
: (isSelected(tag.id) ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300' : 'bg-gray-100 text-gray-600 hover:bg-gray-200')]"
|
|
48
|
-
:style="tag.color ? { backgroundColor: isSelected(tag.id) ? tag.color + '33' : undefined, color: isSelected(tag.id) ? tag.color : undefined } : {}">
|
|
49
|
-
{{ tag.name }}
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, inject } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
tags: { type: Array, required: true },
|
|
6
|
+
modelValue: { type: Array, default: () => [] },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits(['update:modelValue']);
|
|
10
|
+
const dark = inject('esc-dark', computed(() => false));
|
|
11
|
+
|
|
12
|
+
const search = ref('');
|
|
13
|
+
|
|
14
|
+
const filteredTags = computed(() => {
|
|
15
|
+
if (!search.value) return props.tags;
|
|
16
|
+
const q = search.value.toLowerCase();
|
|
17
|
+
return props.tags.filter(t => t.name.toLowerCase().includes(q));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function toggle(tagId) {
|
|
21
|
+
const current = [...props.modelValue];
|
|
22
|
+
const index = current.indexOf(tagId);
|
|
23
|
+
if (index >= 0) {
|
|
24
|
+
current.splice(index, 1);
|
|
25
|
+
} else {
|
|
26
|
+
current.push(tagId);
|
|
27
|
+
}
|
|
28
|
+
emit('update:modelValue', current);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isSelected(tagId) {
|
|
32
|
+
return props.modelValue.includes(tagId);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div>
|
|
38
|
+
<label :class="['mb-1 block text-xs font-medium', dark ? 'text-neutral-500' : 'text-gray-600']">Tags</label>
|
|
39
|
+
<input v-model="search" type="text" placeholder="Filter tags..."
|
|
40
|
+
:class="['mb-2 w-full rounded-md border px-2 py-1 text-xs focus:outline-none',
|
|
41
|
+
dark ? 'border-white/10 bg-neutral-950 text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:ring-1 focus:ring-white/10' : 'border-gray-300 focus:border-blue-500']" />
|
|
42
|
+
<div class="flex flex-wrap gap-1">
|
|
43
|
+
<button v-for="tag in filteredTags" :key="tag.id" @click="toggle(tag.id)"
|
|
44
|
+
:class="['rounded-full px-2 py-0.5 text-xs font-medium transition-colors',
|
|
45
|
+
dark
|
|
46
|
+
? (isSelected(tag.id) ? 'bg-white/[0.12] text-white ring-1 ring-white/20' : 'bg-white/[0.04] text-neutral-500 hover:bg-white/[0.08]')
|
|
47
|
+
: (isSelected(tag.id) ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300' : 'bg-gray-100 text-gray-600 hover:bg-gray-200')]"
|
|
48
|
+
:style="tag.color ? { backgroundColor: isSelected(tag.id) ? tag.color + '33' : undefined, color: isSelected(tag.id) ? tag.color : undefined } : {}">
|
|
49
|
+
{{ tag.name }}
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
import locales from '../locales/index.js';
|
|
3
|
+
|
|
4
|
+
const currentLocale = ref('en');
|
|
5
|
+
const customMessages = ref({});
|
|
6
|
+
|
|
7
|
+
function resolve(obj, path) {
|
|
8
|
+
return path.split('.').reduce((acc, key) => acc?.[key], obj);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function applyReplacements(str, replacements) {
|
|
12
|
+
if (!replacements) return str;
|
|
13
|
+
return Object.entries(replacements).reduce(
|
|
14
|
+
(result, [key, value]) => result.replace(new RegExp(`:${key}`, 'g'), value),
|
|
15
|
+
str,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function setLocale(locale) {
|
|
20
|
+
currentLocale.value = locale;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getLocale() {
|
|
24
|
+
return currentLocale.value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function mergeMessages(locale, messages) {
|
|
28
|
+
customMessages.value = {
|
|
29
|
+
...customMessages.value,
|
|
30
|
+
[locale]: deepMerge(customMessages.value[locale] || {}, messages),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deepMerge(target, source) {
|
|
35
|
+
const result = { ...target };
|
|
36
|
+
for (const key of Object.keys(source)) {
|
|
37
|
+
if (
|
|
38
|
+
source[key] &&
|
|
39
|
+
typeof source[key] === 'object' &&
|
|
40
|
+
!Array.isArray(source[key]) &&
|
|
41
|
+
target[key] &&
|
|
42
|
+
typeof target[key] === 'object'
|
|
43
|
+
) {
|
|
44
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
45
|
+
} else {
|
|
46
|
+
result[key] = source[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useI18n() {
|
|
53
|
+
const locale = computed(() => currentLocale.value);
|
|
54
|
+
|
|
55
|
+
function t(key, replacements) {
|
|
56
|
+
const lang = currentLocale.value;
|
|
57
|
+
|
|
58
|
+
// Try custom messages for current locale
|
|
59
|
+
const custom = resolve(customMessages.value[lang], key);
|
|
60
|
+
if (typeof custom === 'string') return applyReplacements(custom, replacements);
|
|
61
|
+
|
|
62
|
+
// Try built-in messages for current locale
|
|
63
|
+
const msg = resolve(locales[lang], key);
|
|
64
|
+
if (typeof msg === 'string') return applyReplacements(msg, replacements);
|
|
65
|
+
|
|
66
|
+
// Fallback to English
|
|
67
|
+
if (lang !== 'en') {
|
|
68
|
+
const customEn = resolve(customMessages.value.en, key);
|
|
69
|
+
if (typeof customEn === 'string') return applyReplacements(customEn, replacements);
|
|
70
|
+
|
|
71
|
+
const fallback = resolve(locales.en, key);
|
|
72
|
+
if (typeof fallback === 'string') return applyReplacements(fallback, replacements);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Return raw key
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { t, locale };
|
|
80
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { computed } from 'vue';
|
|
2
|
+
import { usePage } from '@inertiajs/vue3';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composable for reading plugin extensions from Inertia shared props.
|
|
6
|
+
*
|
|
7
|
+
* The Laravel backend (escalated-laravel) shares plugin UI extensions via
|
|
8
|
+
* `$page.props.escalated.plugins`, which contains:
|
|
9
|
+
* - menuItems: Array of navigation items injected by plugins
|
|
10
|
+
* - dashboardWidgets: Array of widget definitions for the agent dashboard
|
|
11
|
+
* - pageComponents: Object mapping slot names to arrays of component definitions
|
|
12
|
+
*
|
|
13
|
+
* @returns {Object} Plugin extension data and helper functions
|
|
14
|
+
*/
|
|
15
|
+
export function usePluginExtensions() {
|
|
16
|
+
const page = usePage();
|
|
17
|
+
|
|
18
|
+
const pluginData = computed(() => page.props?.escalated?.plugins || {});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Menu items injected by plugins, sorted by position.
|
|
22
|
+
* Each item: { label, route, icon, position, permission, badge, submenu }
|
|
23
|
+
*/
|
|
24
|
+
const menuItems = computed(() => {
|
|
25
|
+
const items = pluginData.value.menuItems || [];
|
|
26
|
+
return [...items].sort((a, b) => (a.position || 100) - (b.position || 100));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Dashboard widgets injected by plugins, sorted by position.
|
|
31
|
+
* Each widget: { id, title, component, plugin, data, position, width }
|
|
32
|
+
*/
|
|
33
|
+
const dashboardWidgets = computed(() => {
|
|
34
|
+
const widgets = pluginData.value.dashboardWidgets || [];
|
|
35
|
+
return [...widgets].sort((a, b) => (a.position || 100) - (b.position || 100));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get plugin components for a specific page slot, sorted by position.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} pageName - The page name (e.g. 'dashboard', 'ticket.show')
|
|
42
|
+
* @param {string} slotName - The slot name (e.g. 'header', 'sidebar', 'footer')
|
|
43
|
+
* @returns {Array} Sorted array of component definitions for the given slot
|
|
44
|
+
*/
|
|
45
|
+
function getPageComponents(pageName, slotName) {
|
|
46
|
+
const key = `${pageName}.${slotName}`;
|
|
47
|
+
const components = pluginData.value.pageComponents || {};
|
|
48
|
+
return [...(components[key] || [])].sort((a, b) => (a.position || 100) - (b.position || 100));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check whether any plugins have registered extensions.
|
|
53
|
+
*/
|
|
54
|
+
const hasPlugins = computed(() => {
|
|
55
|
+
return menuItems.value.length > 0
|
|
56
|
+
|| dashboardWidgets.value.length > 0
|
|
57
|
+
|| Object.keys(pluginData.value.pageComponents || {}).length > 0;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
pluginData,
|
|
62
|
+
menuItems,
|
|
63
|
+
dashboardWidgets,
|
|
64
|
+
getPageComponents,
|
|
65
|
+
hasPlugins,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for plugin authors to define an Escalated plugin.
|
|
3
|
+
*
|
|
4
|
+
* Returns a Vue plugin object that registers components globally when installed.
|
|
5
|
+
*
|
|
6
|
+
* Usage in a plugin's main entry file:
|
|
7
|
+
*
|
|
8
|
+
* import { defineEscalatedPlugin } from '@escalated-dev/escalated'
|
|
9
|
+
* import SalesWidget from './Components/SalesWidget.vue'
|
|
10
|
+
* import CustomSidebar from './Components/CustomSidebar.vue'
|
|
11
|
+
*
|
|
12
|
+
* export default defineEscalatedPlugin({
|
|
13
|
+
* name: 'my-plugin',
|
|
14
|
+
* components: {
|
|
15
|
+
* SalesWidget,
|
|
16
|
+
* CustomSidebar,
|
|
17
|
+
* },
|
|
18
|
+
* setup(app) {
|
|
19
|
+
* // Optional: run custom setup logic when the plugin is installed
|
|
20
|
+
* },
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
* @param {string} options.name - Plugin identifier
|
|
25
|
+
* @param {Object} [options.components] - Map of component name to component definition
|
|
26
|
+
* @param {Function} [options.setup] - Optional setup callback receiving the Vue app instance
|
|
27
|
+
* @returns {Object} A Vue plugin object with an install() method
|
|
28
|
+
*/
|
|
29
|
+
export function defineEscalatedPlugin(options = {}) {
|
|
30
|
+
return {
|
|
31
|
+
install(app) {
|
|
32
|
+
// Register all provided components globally
|
|
33
|
+
if (options.components) {
|
|
34
|
+
Object.entries(options.components).forEach(([name, component]) => {
|
|
35
|
+
app.component(name, component);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Run optional setup callback
|
|
40
|
+
if (typeof options.setup === 'function') {
|
|
41
|
+
options.setup(app);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Provide the plugin name for debugging
|
|
45
|
+
if (options.name) {
|
|
46
|
+
app.provide(`escalated-plugin:${options.name}`, true);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { default as FollowButton } from './components/FollowButton.vue'
|
|
|
12
12
|
export { default as KeyboardShortcutHelp } from './components/KeyboardShortcutHelp.vue'
|
|
13
13
|
export { default as MacroDropdown } from './components/MacroDropdown.vue'
|
|
14
14
|
export { default as PinnedNotes } from './components/PinnedNotes.vue'
|
|
15
|
+
export { default as PluginSlot } from './components/PluginSlot.vue'
|
|
15
16
|
export { default as PresenceIndicator } from './components/PresenceIndicator.vue'
|
|
16
17
|
export { default as PriorityBadge } from './components/PriorityBadge.vue'
|
|
17
18
|
export { default as QuickFilters } from './components/QuickFilters.vue'
|
|
@@ -28,3 +29,7 @@ export { default as TicketSidebar } from './components/TicketSidebar.vue'
|
|
|
28
29
|
|
|
29
30
|
// Composables
|
|
30
31
|
export { useKeyboardShortcuts } from './composables/useKeyboardShortcuts'
|
|
32
|
+
export { usePluginExtensions } from './composables/usePluginExtensions'
|
|
33
|
+
|
|
34
|
+
// Plugin authoring helper
|
|
35
|
+
export { defineEscalatedPlugin } from './defineEscalatedPlugin'
|