@escalated-dev/escalated 0.7.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 +31 -0
- package/package.json +12 -6
- package/src/components/ActiveChatsPanel.vue +246 -0
- package/src/components/ActivityTimeline.stories.js +55 -55
- package/src/components/ActivityTimeline.vue +83 -83
- package/src/components/AdminDashboard.stories.js +1423 -1095
- package/src/components/AgentLoadIndicator.stories.js +37 -37
- package/src/components/AgentLoadIndicator.vue +49 -49
- package/src/components/AgentPerformanceCard.vue +44 -44
- package/src/components/ArticleEditor.vue +82 -82
- package/src/components/AssigneeSelect.vue +37 -37
- package/src/components/AttachmentList.vue +13 -1
- package/src/components/AuditLogEntry.vue +95 -95
- package/src/components/BulkActionBar.vue +166 -166
- package/src/components/CategoryTree.vue +89 -89
- package/src/components/ChartWidget.vue +46 -46
- 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 -30
- package/src/components/CollisionWarning.vue +68 -68
- package/src/components/ConditionalFieldRules.vue +135 -135
- package/src/components/ContextPanel.vue +66 -66
- package/src/components/ContextPanelSection.vue +65 -65
- package/src/components/CustomFieldRenderer.vue +159 -159
- package/src/components/EscalatedLayout.vue +29 -6
- package/src/components/FileDropzone.vue +57 -57
- package/src/components/FollowButton.stories.js +30 -30
- package/src/components/FollowButton.vue +88 -88
- package/src/components/KeyboardShortcutHelp.vue +213 -213
- package/src/components/KnowledgePanel.vue +122 -122
- package/src/components/KpiCard.stories.js +32 -32
- package/src/components/KpiCard.vue +88 -88
- package/src/components/MacroDropdown.vue +150 -150
- 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 -107
- package/src/components/PinnedNotes.vue +129 -129
- package/src/components/PluginSlot.vue +68 -62
- package/src/components/PresenceIndicator.vue +46 -2
- package/src/components/PriorityBadge.stories.js +33 -33
- package/src/components/PriorityBadge.vue +46 -46
- package/src/components/ReplyComposer.vue +81 -18
- package/src/components/ReplyThread.vue +40 -2
- package/src/components/ReportChart.vue +395 -0
- package/src/components/ReportExportButton.vue +108 -0
- package/src/components/SatisfactionRating.vue +180 -180
- package/src/components/SaveViewDialog.vue +172 -0
- package/src/components/SavedViewSidebar.vue +208 -0
- package/src/components/ScheduleEditor.vue +104 -104
- package/src/components/SideConversation.vue +165 -165
- package/src/components/SideConversationList.vue +147 -147
- package/src/components/SkillTagManager.vue +79 -79
- package/src/components/SlaComplianceChart.vue +46 -53
- package/src/components/SlaTimer.stories.js +35 -35
- package/src/components/SlaTimer.vue +62 -62
- package/src/components/SnoozeButton.vue +247 -0
- package/src/components/StatsCard.stories.js +24 -24
- package/src/components/StatsCard.vue +58 -58
- package/src/components/StatusBadge.stories.js +53 -53
- package/src/components/StatusBadge.vue +78 -78
- package/src/components/TagSelect.vue +84 -84
- package/src/components/TicketFilters.vue +270 -225
- package/src/components/TicketLinkPanel.vue +193 -193
- package/src/components/TicketList.vue +655 -594
- package/src/components/TicketMergeDialog.vue +157 -157
- package/src/components/TicketSplitDialog.vue +98 -0
- package/src/components/TotpInput.stories.js +9 -9
- package/src/components/TotpInput.vue +81 -81
- package/src/components/TwoFactorSetup.vue +113 -113
- 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/useI18n.js +80 -80
- package/src/composables/useKeyboardShortcuts.js +46 -46
- package/src/composables/useMentions.js +134 -0
- package/src/composables/usePluginExtensions.js +159 -159
- package/src/composables/usePluginHooks.js +65 -65
- package/src/composables/useRealtime.js +182 -0
- package/src/defineEscalatedPlugin.js +76 -76
- package/src/index.js +84 -72
- package/src/locales/ar.json +727 -0
- package/src/locales/de.json +678 -430
- package/src/locales/en.json +717 -455
- 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 -381
- package/src/pages/Admin/AuditLog/Index.vue +154 -154
- package/src/pages/Admin/BusinessHours/Form.vue +172 -172
- package/src/pages/Admin/BusinessHours/Index.vue +113 -113
- package/src/pages/Admin/CannedResponses/Index.vue +127 -127
- package/src/pages/Admin/Capacity/Index.vue +134 -134
- package/src/pages/Admin/CustomFields/Form.vue +195 -195
- package/src/pages/Admin/CustomFields/Index.vue +134 -134
- package/src/pages/Admin/CustomObjects/Form.vue +202 -202
- package/src/pages/Admin/CustomObjects/Index.vue +127 -127
- package/src/pages/Admin/CustomObjects/Records.vue +228 -228
- package/src/pages/Admin/Departments/Form.vue +75 -75
- package/src/pages/Admin/Departments/Index.vue +110 -109
- package/src/pages/Admin/EscalationRules/Form.vue +157 -157
- package/src/pages/Admin/EscalationRules/Index.vue +178 -178
- package/src/pages/Admin/Import/Connect.vue +0 -1
- package/src/pages/Admin/Import/Index.vue +1 -1
- package/src/pages/Admin/Import/Progress.vue +1 -1
- package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
- package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
- package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
- package/src/pages/Admin/Macros/Index.vue +448 -448
- package/src/pages/Admin/Plugins/Index.vue +327 -327
- package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
- package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
- 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 -127
- package/src/pages/Admin/Reports/Dashboard.vue +36 -21
- 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 -166
- package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
- package/src/pages/Admin/Reports.vue +206 -206
- package/src/pages/Admin/Roles/Form.vue +71 -71
- package/src/pages/Admin/Roles/Index.vue +122 -122
- package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
- package/src/pages/Admin/Settings/DataRetention.vue +182 -182
- package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
- package/src/pages/Admin/Settings/Sandbox.vue +135 -135
- package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
- package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
- package/src/pages/Admin/Settings.vue +1008 -468
- package/src/pages/Admin/Skills/Form.vue +53 -53
- package/src/pages/Admin/Skills/Index.vue +94 -94
- package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
- package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
- package/src/pages/Admin/Statuses/Form.vue +114 -114
- package/src/pages/Admin/Statuses/Index.vue +153 -153
- package/src/pages/Admin/Tags/Index.vue +191 -191
- package/src/pages/Admin/Tickets/Index.vue +126 -91
- package/src/pages/Admin/Tickets/Show.vue +29 -0
- package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
- package/src/pages/Admin/Webhooks/Form.vue +110 -110
- package/src/pages/Admin/Webhooks/Index.vue +136 -136
- 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/TicketIndex.vue +112 -88
- package/src/pages/Agent/TicketShow.vue +192 -60
- package/src/pages/Customer/Create.vue +1 -1
- package/src/pages/Customer/Index.vue +31 -31
- package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
- package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
- 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 +25 -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/Automations/Form.vue +0 -184
- package/src/pages/Admin/Automations/Index.vue +0 -129
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { inject, computed } from 'vue';
|
|
3
|
-
|
|
4
|
-
defineProps({
|
|
5
|
-
visible: { type: Boolean, default: false },
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
const emit = defineEmits(['update:visible']);
|
|
9
|
-
|
|
10
|
-
const escDark = inject(
|
|
11
|
-
'esc-dark',
|
|
12
|
-
computed(() => false),
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
function close() {
|
|
16
|
-
emit('update:visible', false);
|
|
17
|
-
}
|
|
18
|
-
</script>
|
|
19
|
-
|
|
20
|
-
<template>
|
|
21
|
-
<transition
|
|
22
|
-
enter-active-class="transition-all duration-300 ease-out"
|
|
23
|
-
leave-active-class="transition-all duration-200 ease-in"
|
|
24
|
-
enter-from-class="translate-x-full opacity-0"
|
|
25
|
-
enter-to-class="translate-x-0 opacity-100"
|
|
26
|
-
leave-from-class="translate-x-0 opacity-100"
|
|
27
|
-
leave-to-class="translate-x-full opacity-0"
|
|
28
|
-
>
|
|
29
|
-
<div
|
|
30
|
-
v-if="visible"
|
|
31
|
-
:class="[
|
|
32
|
-
'fixed inset-y-0 right-0 z-40 w-80 overflow-y-auto border-l shadow-2xl backdrop-blur-sm lg:static lg:block lg:overflow-visible lg:shadow-none',
|
|
33
|
-
escDark ? 'border-white/[0.06] bg-neutral-900/95' : 'border-gray-200 bg-white',
|
|
34
|
-
]"
|
|
35
|
-
>
|
|
36
|
-
<!-- Header -->
|
|
37
|
-
<div
|
|
38
|
-
:class="[
|
|
39
|
-
'flex items-center justify-between border-b px-4 py-3',
|
|
40
|
-
escDark ? 'border-white/[0.06]' : 'border-gray-200',
|
|
41
|
-
]"
|
|
42
|
-
>
|
|
43
|
-
<h3 :class="['text-sm font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">Context</h3>
|
|
44
|
-
<button
|
|
45
|
-
type="button"
|
|
46
|
-
:class="[
|
|
47
|
-
'rounded p-1 lg:hidden',
|
|
48
|
-
escDark
|
|
49
|
-
? 'text-neutral-500 hover:bg-white/[0.06] hover:text-neutral-300'
|
|
50
|
-
: 'text-gray-400 hover:bg-gray-100 hover:text-gray-600',
|
|
51
|
-
]"
|
|
52
|
-
@click="close"
|
|
53
|
-
>
|
|
54
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
55
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
56
|
-
</svg>
|
|
57
|
-
</button>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<!-- Sections -->
|
|
61
|
-
<div>
|
|
62
|
-
<slot />
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</transition>
|
|
66
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { inject, computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
defineProps({
|
|
5
|
+
visible: { type: Boolean, default: false },
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits(['update:visible']);
|
|
9
|
+
|
|
10
|
+
const escDark = inject(
|
|
11
|
+
'esc-dark',
|
|
12
|
+
computed(() => false),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
function close() {
|
|
16
|
+
emit('update:visible', false);
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<transition
|
|
22
|
+
enter-active-class="transition-all duration-300 ease-out"
|
|
23
|
+
leave-active-class="transition-all duration-200 ease-in"
|
|
24
|
+
enter-from-class="translate-x-full opacity-0"
|
|
25
|
+
enter-to-class="translate-x-0 opacity-100"
|
|
26
|
+
leave-from-class="translate-x-0 opacity-100"
|
|
27
|
+
leave-to-class="translate-x-full opacity-0"
|
|
28
|
+
>
|
|
29
|
+
<div
|
|
30
|
+
v-if="visible"
|
|
31
|
+
:class="[
|
|
32
|
+
'fixed inset-y-0 right-0 z-40 w-80 overflow-y-auto border-l shadow-2xl backdrop-blur-sm lg:static lg:block lg:overflow-visible lg:shadow-none',
|
|
33
|
+
escDark ? 'border-white/[0.06] bg-neutral-900/95' : 'border-gray-200 bg-white',
|
|
34
|
+
]"
|
|
35
|
+
>
|
|
36
|
+
<!-- Header -->
|
|
37
|
+
<div
|
|
38
|
+
:class="[
|
|
39
|
+
'flex items-center justify-between border-b px-4 py-3',
|
|
40
|
+
escDark ? 'border-white/[0.06]' : 'border-gray-200',
|
|
41
|
+
]"
|
|
42
|
+
>
|
|
43
|
+
<h3 :class="['text-sm font-semibold', escDark ? 'text-neutral-200' : 'text-gray-900']">Context</h3>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
:class="[
|
|
47
|
+
'rounded p-1 lg:hidden',
|
|
48
|
+
escDark
|
|
49
|
+
? 'text-neutral-500 hover:bg-white/[0.06] hover:text-neutral-300'
|
|
50
|
+
: 'text-gray-400 hover:bg-gray-100 hover:text-gray-600',
|
|
51
|
+
]"
|
|
52
|
+
@click="close"
|
|
53
|
+
>
|
|
54
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
55
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- Sections -->
|
|
61
|
+
<div>
|
|
62
|
+
<slot />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</transition>
|
|
66
|
+
</template>
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { ref, inject, computed } from 'vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
title: { type: String, required: true },
|
|
6
|
-
icon: { type: String, default: '' },
|
|
7
|
-
defaultOpen: { type: Boolean, default: true },
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const escDark = inject(
|
|
11
|
-
'esc-dark',
|
|
12
|
-
computed(() => false),
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
const isOpen = ref(props.defaultOpen);
|
|
16
|
-
</script>
|
|
17
|
-
|
|
18
|
-
<template>
|
|
19
|
-
<div :class="['border-b last:border-b-0', escDark ? 'border-white/[0.06]' : 'border-gray-200']">
|
|
20
|
-
<button
|
|
21
|
-
type="button"
|
|
22
|
-
:aria-expanded="isOpen"
|
|
23
|
-
:class="[
|
|
24
|
-
'flex w-full items-center gap-2 px-4 py-3 text-left transition-colors',
|
|
25
|
-
escDark ? 'hover:bg-white/[0.02]' : 'hover:bg-gray-50',
|
|
26
|
-
]"
|
|
27
|
-
@click="isOpen = !isOpen"
|
|
28
|
-
>
|
|
29
|
-
<svg
|
|
30
|
-
v-if="icon"
|
|
31
|
-
:class="['h-4 w-4 shrink-0', escDark ? 'text-neutral-500' : 'text-gray-400']"
|
|
32
|
-
fill="none"
|
|
33
|
-
stroke="currentColor"
|
|
34
|
-
stroke-width="1.5"
|
|
35
|
-
viewBox="0 0 24 24"
|
|
36
|
-
>
|
|
37
|
-
<path stroke-linecap="round" stroke-linejoin="round" :d="icon" />
|
|
38
|
-
</svg>
|
|
39
|
-
<span
|
|
40
|
-
:class="[
|
|
41
|
-
'flex-1 text-xs font-semibold uppercase tracking-wider',
|
|
42
|
-
escDark ? 'text-neutral-400' : 'text-gray-500',
|
|
43
|
-
]"
|
|
44
|
-
>
|
|
45
|
-
{{ title }}
|
|
46
|
-
</span>
|
|
47
|
-
<svg
|
|
48
|
-
:class="[
|
|
49
|
-
'h-4 w-4 transition-transform',
|
|
50
|
-
escDark ? 'text-neutral-600' : 'text-gray-400',
|
|
51
|
-
isOpen ? 'rotate-180' : '',
|
|
52
|
-
]"
|
|
53
|
-
fill="none"
|
|
54
|
-
stroke="currentColor"
|
|
55
|
-
stroke-width="1.5"
|
|
56
|
-
viewBox="0 0 24 24"
|
|
57
|
-
>
|
|
58
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
59
|
-
</svg>
|
|
60
|
-
</button>
|
|
61
|
-
<div v-show="isOpen" class="px-4 pb-4">
|
|
62
|
-
<slot />
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, inject, computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
title: { type: String, required: true },
|
|
6
|
+
icon: { type: String, default: '' },
|
|
7
|
+
defaultOpen: { type: Boolean, default: true },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const escDark = inject(
|
|
11
|
+
'esc-dark',
|
|
12
|
+
computed(() => false),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const isOpen = ref(props.defaultOpen);
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div :class="['border-b last:border-b-0', escDark ? 'border-white/[0.06]' : 'border-gray-200']">
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
:aria-expanded="isOpen"
|
|
23
|
+
:class="[
|
|
24
|
+
'flex w-full items-center gap-2 px-4 py-3 text-left transition-colors',
|
|
25
|
+
escDark ? 'hover:bg-white/[0.02]' : 'hover:bg-gray-50',
|
|
26
|
+
]"
|
|
27
|
+
@click="isOpen = !isOpen"
|
|
28
|
+
>
|
|
29
|
+
<svg
|
|
30
|
+
v-if="icon"
|
|
31
|
+
:class="['h-4 w-4 shrink-0', escDark ? 'text-neutral-500' : 'text-gray-400']"
|
|
32
|
+
fill="none"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
stroke-width="1.5"
|
|
35
|
+
viewBox="0 0 24 24"
|
|
36
|
+
>
|
|
37
|
+
<path stroke-linecap="round" stroke-linejoin="round" :d="icon" />
|
|
38
|
+
</svg>
|
|
39
|
+
<span
|
|
40
|
+
:class="[
|
|
41
|
+
'flex-1 text-xs font-semibold uppercase tracking-wider',
|
|
42
|
+
escDark ? 'text-neutral-400' : 'text-gray-500',
|
|
43
|
+
]"
|
|
44
|
+
>
|
|
45
|
+
{{ title }}
|
|
46
|
+
</span>
|
|
47
|
+
<svg
|
|
48
|
+
:class="[
|
|
49
|
+
'h-4 w-4 transition-transform',
|
|
50
|
+
escDark ? 'text-neutral-600' : 'text-gray-400',
|
|
51
|
+
isOpen ? 'rotate-180' : '',
|
|
52
|
+
]"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
stroke-width="1.5"
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
>
|
|
58
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
61
|
+
<div v-show="isOpen" class="px-4 pb-4">
|
|
62
|
+
<slot />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { computed } from 'vue';
|
|
3
|
-
|
|
4
|
-
const props = defineProps({
|
|
5
|
-
fields: { type: Array, required: true },
|
|
6
|
-
modelValue: { type: Object, default: () => ({}) },
|
|
7
|
-
errors: { type: Object, default: () => ({}) },
|
|
8
|
-
conditions: { type: Array, default: () => [] },
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const emit = defineEmits(['update:modelValue']);
|
|
12
|
-
|
|
13
|
-
function updateField(name, value) {
|
|
14
|
-
emit('update:modelValue', { ...props.modelValue, [name]: value });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function toggleMultiSelect(name, option) {
|
|
18
|
-
const current = props.modelValue[name] || [];
|
|
19
|
-
const idx = current.indexOf(option);
|
|
20
|
-
const updated = idx >= 0 ? current.filter((_, i) => i !== idx) : [...current, option];
|
|
21
|
-
updateField(name, updated);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const sortedFields = computed(() => [...props.fields].sort((a, b) => (a.position || 0) - (b.position || 0)));
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Evaluate whether a field should be visible based on conditions.
|
|
28
|
-
* Conditions can come from the `conditions` prop or from the field's own `conditions` property.
|
|
29
|
-
*/
|
|
30
|
-
function isFieldVisible(field) {
|
|
31
|
-
// Merge global conditions and field-level conditions
|
|
32
|
-
const allConditions = [
|
|
33
|
-
...props.conditions.filter((c) => c.target_field === field.name || c.target_field === field.slug),
|
|
34
|
-
...(field.conditions || []).filter((c) => c.target_field === field.name || c.target_field === field.slug),
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
// If no conditions target this field, it's always visible
|
|
38
|
-
if (allConditions.length === 0) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// All conditions must be met (AND logic)
|
|
43
|
-
return allConditions.every((condition) => {
|
|
44
|
-
const currentValue = props.modelValue[condition.field];
|
|
45
|
-
|
|
46
|
-
switch (condition.operator) {
|
|
47
|
-
case 'equals':
|
|
48
|
-
return String(currentValue || '') === String(condition.value || '');
|
|
49
|
-
case 'not_equals':
|
|
50
|
-
return String(currentValue || '') !== String(condition.value || '');
|
|
51
|
-
case 'contains':
|
|
52
|
-
return String(currentValue || '').includes(String(condition.value || ''));
|
|
53
|
-
case 'is_empty':
|
|
54
|
-
return (
|
|
55
|
-
!currentValue || currentValue === '' || (Array.isArray(currentValue) && currentValue.length === 0)
|
|
56
|
-
);
|
|
57
|
-
case 'is_not_empty':
|
|
58
|
-
return (
|
|
59
|
-
currentValue && currentValue !== '' && !(Array.isArray(currentValue) && currentValue.length === 0)
|
|
60
|
-
);
|
|
61
|
-
default:
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
</script>
|
|
67
|
-
|
|
68
|
-
<template>
|
|
69
|
-
<div class="space-y-4">
|
|
70
|
-
<div v-for="field in sortedFields" v-show="isFieldVisible(field)" :key="field.id">
|
|
71
|
-
<label class="block text-sm font-medium text-neutral-300">
|
|
72
|
-
{{ field.label || field.name }}
|
|
73
|
-
<span v-if="field.required" class="text-rose-400">*</span>
|
|
74
|
-
</label>
|
|
75
|
-
<p v-if="field.description" class="mt-0.5 text-xs text-neutral-500">{{ field.description }}</p>
|
|
76
|
-
|
|
77
|
-
<!-- Text -->
|
|
78
|
-
<input
|
|
79
|
-
v-if="field.type === 'text'"
|
|
80
|
-
type="text"
|
|
81
|
-
:value="modelValue[field.name] || ''"
|
|
82
|
-
:placeholder="field.placeholder"
|
|
83
|
-
:required="field.required"
|
|
84
|
-
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
85
|
-
@input="updateField(field.name, $event.target.value)"
|
|
86
|
-
/>
|
|
87
|
-
|
|
88
|
-
<!-- Textarea -->
|
|
89
|
-
<textarea
|
|
90
|
-
v-else-if="field.type === 'textarea'"
|
|
91
|
-
:value="modelValue[field.name] || ''"
|
|
92
|
-
:placeholder="field.placeholder"
|
|
93
|
-
:required="field.required"
|
|
94
|
-
rows="3"
|
|
95
|
-
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
96
|
-
@input="updateField(field.name, $event.target.value)"
|
|
97
|
-
></textarea>
|
|
98
|
-
|
|
99
|
-
<!-- Number -->
|
|
100
|
-
<input
|
|
101
|
-
v-else-if="field.type === 'number'"
|
|
102
|
-
type="number"
|
|
103
|
-
:value="modelValue[field.name] || ''"
|
|
104
|
-
:placeholder="field.placeholder"
|
|
105
|
-
:required="field.required"
|
|
106
|
-
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
107
|
-
@input="updateField(field.name, $event.target.value)"
|
|
108
|
-
/>
|
|
109
|
-
|
|
110
|
-
<!-- Date -->
|
|
111
|
-
<input
|
|
112
|
-
v-else-if="field.type === 'date'"
|
|
113
|
-
type="date"
|
|
114
|
-
:value="modelValue[field.name] || ''"
|
|
115
|
-
:required="field.required"
|
|
116
|
-
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
117
|
-
@input="updateField(field.name, $event.target.value)"
|
|
118
|
-
/>
|
|
119
|
-
|
|
120
|
-
<!-- Checkbox -->
|
|
121
|
-
<label v-else-if="field.type === 'checkbox'" class="mt-1 flex items-center gap-2">
|
|
122
|
-
<input
|
|
123
|
-
type="checkbox"
|
|
124
|
-
:checked="!!modelValue[field.name]"
|
|
125
|
-
class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
|
|
126
|
-
@change="updateField(field.name, $event.target.checked)"
|
|
127
|
-
/>
|
|
128
|
-
<span class="text-sm text-neutral-300">{{ field.placeholder || 'Yes' }}</span>
|
|
129
|
-
</label>
|
|
130
|
-
|
|
131
|
-
<!-- Select -->
|
|
132
|
-
<select
|
|
133
|
-
v-else-if="field.type === 'select'"
|
|
134
|
-
:value="modelValue[field.name] || ''"
|
|
135
|
-
:required="field.required"
|
|
136
|
-
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
137
|
-
@change="updateField(field.name, $event.target.value)"
|
|
138
|
-
>
|
|
139
|
-
<option value="" disabled class="text-neutral-500">{{ field.placeholder || 'Select...' }}</option>
|
|
140
|
-
<option v-for="opt in field.options" :key="opt" :value="opt">{{ opt }}</option>
|
|
141
|
-
</select>
|
|
142
|
-
|
|
143
|
-
<!-- Multi-select (checkboxes) -->
|
|
144
|
-
<div v-else-if="field.type === 'multi_select'" class="mt-1 space-y-1">
|
|
145
|
-
<label v-for="opt in field.options" :key="opt" class="flex items-center gap-2">
|
|
146
|
-
<input
|
|
147
|
-
type="checkbox"
|
|
148
|
-
:checked="(modelValue[field.name] || []).includes(opt)"
|
|
149
|
-
class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
|
|
150
|
-
@change="toggleMultiSelect(field.name, opt)"
|
|
151
|
-
/>
|
|
152
|
-
<span class="text-sm text-neutral-300">{{ opt }}</span>
|
|
153
|
-
</label>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
<div v-if="errors[field.name]" class="mt-1 text-sm text-rose-400">{{ errors[field.name] }}</div>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
fields: { type: Array, required: true },
|
|
6
|
+
modelValue: { type: Object, default: () => ({}) },
|
|
7
|
+
errors: { type: Object, default: () => ({}) },
|
|
8
|
+
conditions: { type: Array, default: () => [] },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits(['update:modelValue']);
|
|
12
|
+
|
|
13
|
+
function updateField(name, value) {
|
|
14
|
+
emit('update:modelValue', { ...props.modelValue, [name]: value });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toggleMultiSelect(name, option) {
|
|
18
|
+
const current = props.modelValue[name] || [];
|
|
19
|
+
const idx = current.indexOf(option);
|
|
20
|
+
const updated = idx >= 0 ? current.filter((_, i) => i !== idx) : [...current, option];
|
|
21
|
+
updateField(name, updated);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const sortedFields = computed(() => [...props.fields].sort((a, b) => (a.position || 0) - (b.position || 0)));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Evaluate whether a field should be visible based on conditions.
|
|
28
|
+
* Conditions can come from the `conditions` prop or from the field's own `conditions` property.
|
|
29
|
+
*/
|
|
30
|
+
function isFieldVisible(field) {
|
|
31
|
+
// Merge global conditions and field-level conditions
|
|
32
|
+
const allConditions = [
|
|
33
|
+
...props.conditions.filter((c) => c.target_field === field.name || c.target_field === field.slug),
|
|
34
|
+
...(field.conditions || []).filter((c) => c.target_field === field.name || c.target_field === field.slug),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// If no conditions target this field, it's always visible
|
|
38
|
+
if (allConditions.length === 0) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// All conditions must be met (AND logic)
|
|
43
|
+
return allConditions.every((condition) => {
|
|
44
|
+
const currentValue = props.modelValue[condition.field];
|
|
45
|
+
|
|
46
|
+
switch (condition.operator) {
|
|
47
|
+
case 'equals':
|
|
48
|
+
return String(currentValue || '') === String(condition.value || '');
|
|
49
|
+
case 'not_equals':
|
|
50
|
+
return String(currentValue || '') !== String(condition.value || '');
|
|
51
|
+
case 'contains':
|
|
52
|
+
return String(currentValue || '').includes(String(condition.value || ''));
|
|
53
|
+
case 'is_empty':
|
|
54
|
+
return (
|
|
55
|
+
!currentValue || currentValue === '' || (Array.isArray(currentValue) && currentValue.length === 0)
|
|
56
|
+
);
|
|
57
|
+
case 'is_not_empty':
|
|
58
|
+
return (
|
|
59
|
+
currentValue && currentValue !== '' && !(Array.isArray(currentValue) && currentValue.length === 0)
|
|
60
|
+
);
|
|
61
|
+
default:
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<div class="space-y-4">
|
|
70
|
+
<div v-for="field in sortedFields" v-show="isFieldVisible(field)" :key="field.id">
|
|
71
|
+
<label class="block text-sm font-medium text-neutral-300">
|
|
72
|
+
{{ field.label || field.name }}
|
|
73
|
+
<span v-if="field.required" class="text-rose-400">*</span>
|
|
74
|
+
</label>
|
|
75
|
+
<p v-if="field.description" class="mt-0.5 text-xs text-neutral-500">{{ field.description }}</p>
|
|
76
|
+
|
|
77
|
+
<!-- Text -->
|
|
78
|
+
<input
|
|
79
|
+
v-if="field.type === 'text'"
|
|
80
|
+
type="text"
|
|
81
|
+
:value="modelValue[field.name] || ''"
|
|
82
|
+
:placeholder="field.placeholder"
|
|
83
|
+
:required="field.required"
|
|
84
|
+
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
85
|
+
@input="updateField(field.name, $event.target.value)"
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<!-- Textarea -->
|
|
89
|
+
<textarea
|
|
90
|
+
v-else-if="field.type === 'textarea'"
|
|
91
|
+
:value="modelValue[field.name] || ''"
|
|
92
|
+
:placeholder="field.placeholder"
|
|
93
|
+
:required="field.required"
|
|
94
|
+
rows="3"
|
|
95
|
+
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
96
|
+
@input="updateField(field.name, $event.target.value)"
|
|
97
|
+
></textarea>
|
|
98
|
+
|
|
99
|
+
<!-- Number -->
|
|
100
|
+
<input
|
|
101
|
+
v-else-if="field.type === 'number'"
|
|
102
|
+
type="number"
|
|
103
|
+
:value="modelValue[field.name] || ''"
|
|
104
|
+
:placeholder="field.placeholder"
|
|
105
|
+
:required="field.required"
|
|
106
|
+
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 placeholder-neutral-600 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
107
|
+
@input="updateField(field.name, $event.target.value)"
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
<!-- Date -->
|
|
111
|
+
<input
|
|
112
|
+
v-else-if="field.type === 'date'"
|
|
113
|
+
type="date"
|
|
114
|
+
:value="modelValue[field.name] || ''"
|
|
115
|
+
:required="field.required"
|
|
116
|
+
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
117
|
+
@input="updateField(field.name, $event.target.value)"
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<!-- Checkbox -->
|
|
121
|
+
<label v-else-if="field.type === 'checkbox'" class="mt-1 flex items-center gap-2">
|
|
122
|
+
<input
|
|
123
|
+
type="checkbox"
|
|
124
|
+
:checked="!!modelValue[field.name]"
|
|
125
|
+
class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
|
|
126
|
+
@change="updateField(field.name, $event.target.checked)"
|
|
127
|
+
/>
|
|
128
|
+
<span class="text-sm text-neutral-300">{{ field.placeholder || 'Yes' }}</span>
|
|
129
|
+
</label>
|
|
130
|
+
|
|
131
|
+
<!-- Select -->
|
|
132
|
+
<select
|
|
133
|
+
v-else-if="field.type === 'select'"
|
|
134
|
+
:value="modelValue[field.name] || ''"
|
|
135
|
+
:required="field.required"
|
|
136
|
+
class="mt-1 w-full rounded-lg border border-white/10 bg-neutral-950 px-3 py-2 text-sm text-neutral-200 focus:border-white/20 focus:outline-none focus:ring-1 focus:ring-white/10"
|
|
137
|
+
@change="updateField(field.name, $event.target.value)"
|
|
138
|
+
>
|
|
139
|
+
<option value="" disabled class="text-neutral-500">{{ field.placeholder || 'Select...' }}</option>
|
|
140
|
+
<option v-for="opt in field.options" :key="opt" :value="opt">{{ opt }}</option>
|
|
141
|
+
</select>
|
|
142
|
+
|
|
143
|
+
<!-- Multi-select (checkboxes) -->
|
|
144
|
+
<div v-else-if="field.type === 'multi_select'" class="mt-1 space-y-1">
|
|
145
|
+
<label v-for="opt in field.options" :key="opt" class="flex items-center gap-2">
|
|
146
|
+
<input
|
|
147
|
+
type="checkbox"
|
|
148
|
+
:checked="(modelValue[field.name] || []).includes(opt)"
|
|
149
|
+
class="rounded border-white/20 bg-neutral-900 text-cyan-500 focus:ring-white/10"
|
|
150
|
+
@change="toggleMultiSelect(field.name, opt)"
|
|
151
|
+
/>
|
|
152
|
+
<span class="text-sm text-neutral-300">{{ opt }}</span>
|
|
153
|
+
</label>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div v-if="errors[field.name]" class="mt-1 text-sm text-rose-400">{{ errors[field.name] }}</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|