@escalated-dev/escalated 0.8.0 → 0.9.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/LICENSE +21 -21
- package/README.md +380 -361
- package/package.json +85 -84
- package/src/components/ActiveChatsPanel.vue +246 -246
- package/src/components/AdminDashboard.stories.js +1423 -1423
- package/src/components/AttachmentList.vue +65 -65
- package/src/components/ChatActionBar.vue +226 -226
- package/src/components/ChatBubble.vue +162 -162
- package/src/components/ChatComposer.vue +308 -308
- package/src/components/ChatQueue.vue +222 -222
- package/src/components/ChatThread.vue +197 -197
- package/src/components/CustomTicketActions.stories.js +71 -0
- package/src/components/CustomTicketActions.vue +101 -0
- package/src/components/EscalatedLayout.vue +619 -604
- package/src/components/FollowButton.vue +20 -1
- package/src/components/MentionDropdown.vue +154 -154
- package/src/components/MetricComparison.vue +89 -89
- package/src/components/PercentileChart.vue +67 -67
- package/src/components/PeriodSelector.vue +98 -98
- package/src/components/PresenceIndicator.vue +228 -213
- package/src/components/ReplyComposer.vue +336 -336
- package/src/components/ReplyThread.vue +153 -153
- package/src/components/ReportChart.vue +395 -395
- package/src/components/ReportExportButton.vue +108 -108
- package/src/components/SaveViewDialog.vue +172 -172
- package/src/components/SavedViewSidebar.vue +208 -208
- package/src/components/SkillTagManager.vue +3 -0
- package/src/components/SnoozeButton.vue +247 -247
- package/src/components/TicketFilters.vue +270 -270
- package/src/components/TicketList.vue +655 -655
- package/src/components/TicketSidebar.vue +4 -0
- package/src/components/TicketSplitDialog.vue +98 -98
- package/src/components/TicketSubjects.stories.js +63 -0
- package/src/components/TicketSubjects.vue +107 -0
- package/src/components/TwoFactorSetup.vue +1 -37
- package/src/components/VariableTokenInput.vue +111 -111
- package/src/components/WorkflowActionConfig.vue +287 -287
- package/src/components/WorkflowActionList.vue +350 -350
- package/src/components/WorkflowConditionBuilder.vue +265 -265
- package/src/components/WorkflowTriggerSelector.vue +125 -125
- package/src/components/admin/newsletters/AnalyticsTiles.stories.js +14 -0
- package/src/components/admin/newsletters/AnalyticsTiles.vue +55 -0
- package/src/components/admin/newsletters/DeliveriesTable.stories.js +28 -0
- package/src/components/admin/newsletters/DeliveriesTable.vue +104 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.stories.js +11 -0
- package/src/components/admin/newsletters/DynamicFilterBuilder.vue +53 -0
- package/src/components/admin/newsletters/ListMemberTable.stories.js +12 -0
- package/src/components/admin/newsletters/ListMemberTable.vue +53 -0
- package/src/components/admin/newsletters/MarkdownEditor.stories.js +6 -0
- package/src/components/admin/newsletters/MarkdownEditor.vue +53 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.stories.js +6 -0
- package/src/components/admin/newsletters/MergeFieldDropdown.vue +84 -0
- package/src/components/admin/newsletters/PreviewIframe.stories.js +7 -0
- package/src/components/admin/newsletters/PreviewIframe.vue +40 -0
- package/src/components/admin/newsletters/SummaryTiles.stories.js +8 -0
- package/src/components/admin/newsletters/SummaryTiles.vue +63 -0
- package/src/composables/useChat.js +187 -187
- package/src/composables/useI18n.js +102 -102
- package/src/composables/useMentions.js +134 -134
- package/src/composables/usePermissions.js +31 -0
- package/src/composables/useRealtime.js +182 -182
- package/src/index.js +85 -84
- package/src/locales/README.md +13 -13
- package/src/locales/ar.json +727 -727
- package/src/locales/de.json +690 -690
- package/src/locales/en.json +889 -729
- package/src/locales/es.json +691 -691
- package/src/locales/fr.json +691 -691
- package/src/locales/index.js +16 -16
- package/src/locales/it.json +727 -727
- package/src/locales/ja.json +727 -727
- package/src/locales/ko.json +727 -727
- package/src/locales/nl.json +727 -727
- package/src/locales/pl.json +727 -727
- package/src/locales/pt-BR.json +727 -727
- package/src/locales/ru.json +727 -727
- package/src/locales/tr.json +727 -727
- package/src/locales/zh-CN.json +727 -727
- package/src/pages/Admin/Newsletters/Compose.vue +192 -0
- package/src/pages/Admin/Newsletters/Edit.vue +24 -0
- package/src/pages/Admin/Newsletters/Index.vue +141 -0
- package/src/pages/Admin/Newsletters/Lists/Create.vue +64 -0
- package/src/pages/Admin/Newsletters/Lists/Index.vue +59 -0
- package/src/pages/Admin/Newsletters/Lists/Show.vue +53 -0
- package/src/pages/Admin/Newsletters/Settings.vue +85 -0
- package/src/pages/Admin/Newsletters/Show.vue +140 -0
- package/src/pages/Admin/Newsletters/Templates/Create.vue +11 -0
- package/src/pages/Admin/Newsletters/Templates/Index.vue +54 -0
- package/src/pages/Admin/Newsletters/Templates/Show.vue +65 -0
- package/src/pages/Admin/Newsletters/usePreview.js +44 -0
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -132
- package/src/pages/Admin/Reports/AgentRanking.vue +246 -246
- package/src/pages/Admin/Reports/Cohorts.vue +180 -180
- package/src/pages/Admin/Reports/Comparison.vue +141 -141
- package/src/pages/Admin/Reports/Dashboard.vue +226 -226
- package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -164
- package/src/pages/Admin/Reports/ResponseTimes.vue +164 -164
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -183
- package/src/pages/Admin/Settings/SsoSettings.vue +157 -4
- package/src/pages/Admin/Settings/TwoFactor.vue +32 -6
- package/src/pages/Admin/Settings.vue +1008 -1008
- package/src/pages/Admin/Skills/Form.vue +30 -10
- package/src/pages/Admin/Tickets/Index.vue +126 -126
- package/src/pages/Admin/Tickets/Show.vue +254 -249
- package/src/pages/Admin/Users/Index.vue +145 -145
- package/src/pages/Admin/Workflows/Builder.vue +207 -207
- package/src/pages/Admin/Workflows/Index.vue +235 -235
- package/src/pages/Admin/Workflows/Logs.vue +286 -286
- package/src/pages/Agent/TicketIndex.vue +112 -112
- package/src/pages/Agent/TicketShow.vue +547 -494
- package/src/pages/Customer/Index.vue +56 -56
- package/src/pages/Customer/KnowledgeBase/Article.vue +129 -129
- package/src/plugin.js +175 -175
- package/src/utils/mentionHighlight.js +24 -24
- package/src/widget/EscalatedWidget.vue +804 -804
- package/src/widget/index.js +254 -254
|
@@ -7,6 +7,7 @@ import AssigneeSelect from './AssigneeSelect.vue';
|
|
|
7
7
|
import TagSelect from './TagSelect.vue';
|
|
8
8
|
import TicketTypeSelector from './TicketTypeSelector.vue';
|
|
9
9
|
import ActivityTimeline from './ActivityTimeline.vue';
|
|
10
|
+
import TicketSubjects from './TicketSubjects.vue';
|
|
10
11
|
|
|
11
12
|
defineProps({
|
|
12
13
|
ticket: { type: Object, required: true },
|
|
@@ -85,6 +86,9 @@ function renderStars(rating) {
|
|
|
85
86
|
</dl>
|
|
86
87
|
</div>
|
|
87
88
|
|
|
89
|
+
<!-- Related host-app entities this ticket is about (Project, Customer, …) -->
|
|
90
|
+
<TicketSubjects :subjects="ticket.subjects || []" />
|
|
91
|
+
|
|
88
92
|
<!-- Satisfaction Rating (read-only) -->
|
|
89
93
|
<div v-if="ticket.satisfaction_rating" :class="cardClass">
|
|
90
94
|
<h3 :class="['mb-3 text-sm font-semibold', escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900']">
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref } from 'vue';
|
|
3
|
-
import { router } from '@inertiajs/vue3';
|
|
4
|
-
|
|
5
|
-
const props = defineProps({
|
|
6
|
-
ticketReference: { type: String, required: true },
|
|
7
|
-
replyId: { type: [Number, String], required: true },
|
|
8
|
-
replyAuthor: { type: String, default: '' },
|
|
9
|
-
replyExcerpt: { type: String, default: '' },
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const emit = defineEmits(['close']);
|
|
13
|
-
|
|
14
|
-
const subject = ref('');
|
|
15
|
-
const splitting = ref(false);
|
|
16
|
-
|
|
17
|
-
function confirmSplit() {
|
|
18
|
-
if (splitting.value) return;
|
|
19
|
-
splitting.value = true;
|
|
20
|
-
|
|
21
|
-
const payload = { reply_id: props.replyId };
|
|
22
|
-
if (subject.value.trim()) {
|
|
23
|
-
payload.subject = subject.value.trim();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
router.post(route('escalated.admin.tickets.split', props.ticketReference), payload, {
|
|
27
|
-
onFinish: () => {
|
|
28
|
-
splitting.value = false;
|
|
29
|
-
},
|
|
30
|
-
onError: () => {
|
|
31
|
-
splitting.value = false;
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
</script>
|
|
36
|
-
|
|
37
|
-
<template>
|
|
38
|
-
<Teleport to="body">
|
|
39
|
-
<div
|
|
40
|
-
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
41
|
-
@click.self="emit('close')"
|
|
42
|
-
>
|
|
43
|
-
<div class="w-full max-w-lg rounded-xl border border-white/[0.06] bg-neutral-900 p-6 shadow-2xl">
|
|
44
|
-
<div class="mb-4 flex items-center justify-between">
|
|
45
|
-
<h3 class="text-lg font-semibold text-neutral-200">Split to New Ticket</h3>
|
|
46
|
-
<button class="text-neutral-500 hover:text-neutral-300" @click="emit('close')">
|
|
47
|
-
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
48
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
49
|
-
</svg>
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<p class="mb-4 text-sm text-neutral-400">
|
|
54
|
-
This will create a new ticket from the selected reply
|
|
55
|
-
<span v-if="replyAuthor" class="text-neutral-300">by {{ replyAuthor }}</span
|
|
56
|
-
>. The new ticket will be linked to
|
|
57
|
-
<span class="font-mono text-neutral-300">{{ ticketReference }}</span
|
|
58
|
-
>.
|
|
59
|
-
</p>
|
|
60
|
-
|
|
61
|
-
<!-- Reply excerpt preview -->
|
|
62
|
-
<div v-if="replyExcerpt" class="mb-4 rounded-lg border border-white/[0.06] bg-white/[0.02] p-3">
|
|
63
|
-
<p class="text-xs font-semibold uppercase tracking-wider text-neutral-500">Reply content</p>
|
|
64
|
-
<p class="mt-1 line-clamp-3 text-sm text-neutral-300">{{ replyExcerpt }}</p>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<label class="mb-1 block text-sm font-medium text-neutral-400">
|
|
68
|
-
Subject <span class="text-neutral-600">(optional override)</span>
|
|
69
|
-
</label>
|
|
70
|
-
<input
|
|
71
|
-
v-model="subject"
|
|
72
|
-
type="text"
|
|
73
|
-
placeholder="Leave empty to auto-generate from source ticket"
|
|
74
|
-
class="w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
75
|
-
/>
|
|
76
|
-
|
|
77
|
-
<div class="mt-5 flex justify-end gap-3">
|
|
78
|
-
<button
|
|
79
|
-
class="rounded-lg border border-white/10 bg-white/[0.03] px-4 py-2 text-sm font-medium text-neutral-300 transition-colors hover:bg-white/[0.06]"
|
|
80
|
-
@click="emit('close')"
|
|
81
|
-
>
|
|
82
|
-
Cancel
|
|
83
|
-
</button>
|
|
84
|
-
<button
|
|
85
|
-
:disabled="splitting"
|
|
86
|
-
:class="[
|
|
87
|
-
'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-4 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400',
|
|
88
|
-
splitting && 'cursor-not-allowed opacity-40',
|
|
89
|
-
]"
|
|
90
|
-
@click="confirmSplit"
|
|
91
|
-
>
|
|
92
|
-
{{ splitting ? 'Splitting...' : 'Split to New Ticket' }}
|
|
93
|
-
</button>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
</Teleport>
|
|
98
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import { router } from '@inertiajs/vue3';
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
ticketReference: { type: String, required: true },
|
|
7
|
+
replyId: { type: [Number, String], required: true },
|
|
8
|
+
replyAuthor: { type: String, default: '' },
|
|
9
|
+
replyExcerpt: { type: String, default: '' },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits(['close']);
|
|
13
|
+
|
|
14
|
+
const subject = ref('');
|
|
15
|
+
const splitting = ref(false);
|
|
16
|
+
|
|
17
|
+
function confirmSplit() {
|
|
18
|
+
if (splitting.value) return;
|
|
19
|
+
splitting.value = true;
|
|
20
|
+
|
|
21
|
+
const payload = { reply_id: props.replyId };
|
|
22
|
+
if (subject.value.trim()) {
|
|
23
|
+
payload.subject = subject.value.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
router.post(route('escalated.admin.tickets.split', props.ticketReference), payload, {
|
|
27
|
+
onFinish: () => {
|
|
28
|
+
splitting.value = false;
|
|
29
|
+
},
|
|
30
|
+
onError: () => {
|
|
31
|
+
splitting.value = false;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<Teleport to="body">
|
|
39
|
+
<div
|
|
40
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
41
|
+
@click.self="emit('close')"
|
|
42
|
+
>
|
|
43
|
+
<div class="w-full max-w-lg rounded-xl border border-white/[0.06] bg-neutral-900 p-6 shadow-2xl">
|
|
44
|
+
<div class="mb-4 flex items-center justify-between">
|
|
45
|
+
<h3 class="text-lg font-semibold text-neutral-200">Split to New Ticket</h3>
|
|
46
|
+
<button class="text-neutral-500 hover:text-neutral-300" @click="emit('close')">
|
|
47
|
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
48
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
49
|
+
</svg>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<p class="mb-4 text-sm text-neutral-400">
|
|
54
|
+
This will create a new ticket from the selected reply
|
|
55
|
+
<span v-if="replyAuthor" class="text-neutral-300">by {{ replyAuthor }}</span
|
|
56
|
+
>. The new ticket will be linked to
|
|
57
|
+
<span class="font-mono text-neutral-300">{{ ticketReference }}</span
|
|
58
|
+
>.
|
|
59
|
+
</p>
|
|
60
|
+
|
|
61
|
+
<!-- Reply excerpt preview -->
|
|
62
|
+
<div v-if="replyExcerpt" class="mb-4 rounded-lg border border-white/[0.06] bg-white/[0.02] p-3">
|
|
63
|
+
<p class="text-xs font-semibold uppercase tracking-wider text-neutral-500">Reply content</p>
|
|
64
|
+
<p class="mt-1 line-clamp-3 text-sm text-neutral-300">{{ replyExcerpt }}</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<label class="mb-1 block text-sm font-medium text-neutral-400">
|
|
68
|
+
Subject <span class="text-neutral-600">(optional override)</span>
|
|
69
|
+
</label>
|
|
70
|
+
<input
|
|
71
|
+
v-model="subject"
|
|
72
|
+
type="text"
|
|
73
|
+
placeholder="Leave empty to auto-generate from source ticket"
|
|
74
|
+
class="w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-500 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<div class="mt-5 flex justify-end gap-3">
|
|
78
|
+
<button
|
|
79
|
+
class="rounded-lg border border-white/10 bg-white/[0.03] px-4 py-2 text-sm font-medium text-neutral-300 transition-colors hover:bg-white/[0.06]"
|
|
80
|
+
@click="emit('close')"
|
|
81
|
+
>
|
|
82
|
+
Cancel
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
:disabled="splitting"
|
|
86
|
+
:class="[
|
|
87
|
+
'rounded-lg bg-gradient-to-r from-cyan-500 to-violet-500 px-4 py-2 text-sm font-medium text-white shadow-lg shadow-black/20 transition-all hover:from-cyan-400 hover:to-violet-400',
|
|
88
|
+
splitting && 'cursor-not-allowed opacity-40',
|
|
89
|
+
]"
|
|
90
|
+
@click="confirmSplit"
|
|
91
|
+
>
|
|
92
|
+
{{ splitting ? 'Splitting...' : 'Split to New Ticket' }}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</Teleport>
|
|
98
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import TicketSubjects from './TicketSubjects.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Components/TicketSubjects',
|
|
5
|
+
component: TicketSubjects,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const Single = {
|
|
9
|
+
args: {
|
|
10
|
+
subjects: [
|
|
11
|
+
{
|
|
12
|
+
type: 'App\\Models\\Project',
|
|
13
|
+
id: '42',
|
|
14
|
+
role: 'project',
|
|
15
|
+
title: 'Acme Website Redesign',
|
|
16
|
+
subtitle: 'Project · Acme Corp',
|
|
17
|
+
url: 'https://app.example.com/projects/42',
|
|
18
|
+
color: '#2563eb',
|
|
19
|
+
icon: 'folder',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Multiple = {
|
|
26
|
+
args: {
|
|
27
|
+
subjects: [
|
|
28
|
+
{
|
|
29
|
+
type: 'App\\Models\\Project',
|
|
30
|
+
id: '42',
|
|
31
|
+
role: 'project',
|
|
32
|
+
title: 'Acme Website Redesign',
|
|
33
|
+
subtitle: 'Project · Acme Corp',
|
|
34
|
+
url: 'https://app.example.com/projects/42',
|
|
35
|
+
color: '#2563eb',
|
|
36
|
+
icon: 'folder',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'App\\Models\\Customer',
|
|
40
|
+
id: '7',
|
|
41
|
+
role: 'account',
|
|
42
|
+
title: 'Acme Corp',
|
|
43
|
+
subtitle: 'Enterprise · 320 seats',
|
|
44
|
+
url: 'https://app.example.com/customers/7',
|
|
45
|
+
color: '#059669',
|
|
46
|
+
icon: 'building',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'App\\Models\\Asset',
|
|
50
|
+
id: 'srv-19',
|
|
51
|
+
title: 'web-prod-19',
|
|
52
|
+
subtitle: 'Server · us-east-1',
|
|
53
|
+
url: null,
|
|
54
|
+
color: null,
|
|
55
|
+
icon: null,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Empty = {
|
|
62
|
+
args: { subjects: [] },
|
|
63
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { inject, computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
subjects: { type: Array, default: () => [] },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const escDark = inject(
|
|
9
|
+
'esc-dark',
|
|
10
|
+
computed(() => false),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const cardClass = computed(() =>
|
|
14
|
+
escDark.value
|
|
15
|
+
? 'rounded-xl border border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface)] p-4'
|
|
16
|
+
: 'rounded-lg border border-gray-200 bg-white p-4',
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const hasSubjects = computed(() => Array.isArray(props.subjects) && props.subjects.length > 0);
|
|
20
|
+
|
|
21
|
+
function initials(subject) {
|
|
22
|
+
const text = (subject.title || '').trim();
|
|
23
|
+
if (!text) return '?';
|
|
24
|
+
return text
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.slice(0, 2)
|
|
27
|
+
.map((w) => w.charAt(0).toUpperCase())
|
|
28
|
+
.join('');
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div v-if="hasSubjects" :class="cardClass">
|
|
34
|
+
<h3 :class="['mb-3 text-sm font-semibold', escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900']">
|
|
35
|
+
Related
|
|
36
|
+
</h3>
|
|
37
|
+
<ul class="space-y-2">
|
|
38
|
+
<li v-for="(subject, i) in subjects" :key="`${subject.type}:${subject.id}:${i}`">
|
|
39
|
+
<component
|
|
40
|
+
:is="subject.url ? 'a' : 'div'"
|
|
41
|
+
:href="subject.url || undefined"
|
|
42
|
+
:target="subject.url ? '_blank' : undefined"
|
|
43
|
+
:rel="subject.url ? 'noopener noreferrer' : undefined"
|
|
44
|
+
:class="[
|
|
45
|
+
'flex items-center gap-3 rounded-md px-2 py-1.5 text-sm transition',
|
|
46
|
+
subject.url ? 'cursor-pointer' : '',
|
|
47
|
+
subject.url
|
|
48
|
+
? escDark
|
|
49
|
+
? 'hover:bg-[var(--esc-panel-hover,rgba(255,255,255,0.05))]'
|
|
50
|
+
: 'hover:bg-gray-50'
|
|
51
|
+
: '',
|
|
52
|
+
]"
|
|
53
|
+
>
|
|
54
|
+
<span
|
|
55
|
+
class="flex h-8 w-8 flex-none items-center justify-center rounded-md text-xs font-semibold text-white"
|
|
56
|
+
:style="{ backgroundColor: subject.color || '#6b7280' }"
|
|
57
|
+
:title="subject.icon || undefined"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
{{ initials(subject) }}
|
|
61
|
+
</span>
|
|
62
|
+
<span class="min-w-0 flex-1">
|
|
63
|
+
<span
|
|
64
|
+
:class="[
|
|
65
|
+
'block truncate font-medium',
|
|
66
|
+
escDark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900',
|
|
67
|
+
]"
|
|
68
|
+
>
|
|
69
|
+
{{ subject.title }}
|
|
70
|
+
</span>
|
|
71
|
+
<span
|
|
72
|
+
v-if="subject.subtitle"
|
|
73
|
+
:class="[
|
|
74
|
+
'block truncate text-xs',
|
|
75
|
+
escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500',
|
|
76
|
+
]"
|
|
77
|
+
>
|
|
78
|
+
{{ subject.subtitle }}
|
|
79
|
+
</span>
|
|
80
|
+
</span>
|
|
81
|
+
<span
|
|
82
|
+
v-if="subject.role"
|
|
83
|
+
:class="['flex-none text-xs', escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400']"
|
|
84
|
+
>
|
|
85
|
+
{{ subject.role }}
|
|
86
|
+
</span>
|
|
87
|
+
<svg
|
|
88
|
+
v-if="subject.url"
|
|
89
|
+
class="h-3.5 w-3.5 flex-none"
|
|
90
|
+
:class="escDark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400'"
|
|
91
|
+
fill="none"
|
|
92
|
+
viewBox="0 0 24 24"
|
|
93
|
+
stroke="currentColor"
|
|
94
|
+
stroke-width="2"
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
>
|
|
97
|
+
<path
|
|
98
|
+
stroke-linecap="round"
|
|
99
|
+
stroke-linejoin="round"
|
|
100
|
+
d="M14 5h5v5m0-5L10 14m-1 5H6a2 2 0 01-2-2V8"
|
|
101
|
+
/>
|
|
102
|
+
</svg>
|
|
103
|
+
</component>
|
|
104
|
+
</li>
|
|
105
|
+
</ul>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { ref } from 'vue';
|
|
3
3
|
import TotpInput from './TotpInput.vue';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
defineProps({
|
|
6
6
|
qrUri: { type: String, default: '' },
|
|
7
|
-
recoveryCodes: { type: Array, default: () => [] },
|
|
8
7
|
});
|
|
9
8
|
|
|
10
9
|
const emit = defineEmits(['confirm', 'cancel']);
|
|
@@ -24,12 +23,6 @@ function confirmCode() {
|
|
|
24
23
|
}
|
|
25
24
|
error.value = '';
|
|
26
25
|
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
26
|
}
|
|
34
27
|
</script>
|
|
35
28
|
|
|
@@ -80,34 +73,5 @@ function copyRecoveryCodes() {
|
|
|
80
73
|
</button>
|
|
81
74
|
</div>
|
|
82
75
|
</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
76
|
</div>
|
|
113
77
|
</template>
|