@feedmepos/mf-order-setting 0.0.92 → 0.0.93
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/dist/DisplaySystemView-BMRCs7o8.js +8891 -0
- package/dist/{KioskDevicesView-d2WLB0-w.js → KioskDevicesView-BCZOqou3.js} +1 -1
- package/dist/{KioskDevicesView.vue_vue_type_script_setup_true_lang-rs-hNWUW.js → KioskDevicesView.vue_vue_type_script_setup_true_lang-4wFO9co5.js} +3 -3
- package/dist/{KioskSettingView-DFE3_u5c.js → KioskSettingView-DIvpRwFV.js} +3 -3
- package/dist/{KioskView-C3Pi3lWW.js → KioskView-w4txC9m2.js} +5 -5
- package/dist/{OrderSettingsView-Dxg9RjSd.js → OrderSettingsView-BPuBA2QC.js} +23285 -24117
- package/dist/{app-BFlQtcsd.js → app-xZAaURng.js} +2635 -1965
- package/dist/app.js +1 -1
- package/dist/featureFlag-Cbj0uMaM.js +846 -0
- package/dist/frontend/mf-order/src/api/brandkit/index.d.ts +3 -0
- package/dist/frontend/mf-order/src/app.d.ts +668 -0
- package/dist/frontend/mf-order/src/composables/featureFlag.d.ts +7 -0
- package/dist/frontend/mf-order/src/composables/useBusinessFeatureFlag.d.ts +4 -0
- package/dist/frontend/mf-order/src/composables/useLegacyOdsAccess.d.ts +4 -0
- package/dist/frontend/mf-order/src/composables/useMiniProgramPermission.d.ts +5 -0
- package/dist/frontend/mf-order/src/main.d.ts +668 -0
- package/dist/frontend/mf-order/src/modules/display-system/legacy-ods/interface.d.ts +80 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorOverrideDialog.vue.d.ts +26 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/ColorSelection.vue.d.ts +15 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue.d.ts +24 -0
- package/dist/frontend/mf-order/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue.d.ts +4 -0
- package/dist/frontend/mf-order/src/views/display-system/view-selection.d.ts +18 -0
- package/dist/frontend/mf-order/tsconfig.app.tsbuildinfo +1 -1
- package/dist/{index-ByiTrmdq.js → index-BAFwfvqv.js} +2 -2
- package/dist/{index-CaO7aaaT.js → index-BxQKdXyN.js} +1 -1
- package/dist/{index-s2kWGMtz.js → index-Cipbdi6i.js} +1 -1
- package/dist/{order.dto-C829lA69.js → order.dto-yLICIGDQ.js} +1 -1
- package/dist/style.css +1 -1
- package/env.d.ts +2 -0
- package/package.json +2 -1
- package/src/api/brandkit/index.ts +24 -0
- package/src/composables/featureFlag.ts +22 -0
- package/src/composables/useBusinessFeatureFlag.ts +44 -0
- package/src/composables/useLegacyOdsAccess.ts +35 -0
- package/src/composables/useMiniProgramPermission.ts +19 -0
- package/src/composables/useQrOrderingEnabled.ts +1 -21
- package/src/locales/en-US.json +167 -0
- package/src/locales/ja-JP.json +167 -0
- package/src/locales/th-TH.json +167 -0
- package/src/locales/zh-CN.json +167 -0
- package/src/modules/display-system/legacy-ods/interface.ts +108 -0
- package/src/router/routes.ts +1 -1
- package/src/views/display-system/DisplaySystemView.vue +82 -11
- package/src/views/display-system/legacy-ods/ColorOverrideDialog.vue +223 -0
- package/src/views/display-system/legacy-ods/ColorSelection.vue +67 -0
- package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.spec.ts +185 -0
- package/src/views/display-system/legacy-ods/LegacyOdsEditorDialog.vue +1677 -0
- package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.spec.ts +124 -0
- package/src/views/display-system/legacy-ods/LegacyOdsTemplateList.vue +297 -0
- package/src/views/display-system/legacy-ods/brandkit.spec.ts +67 -0
- package/src/views/display-system/view-selection.spec.ts +93 -0
- package/src/views/display-system/view-selection.ts +54 -0
- package/dist/DisplaySystemView-DZS7EVfk.js +0 -6564
- package/dist/_plugin-vue_export-helper-CHgC5LLL.js +0 -9
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
3
4
|
import { useCoreStore, useI18n } from '@feedmepos/mf-common'
|
|
4
5
|
import type { FmTabProps, ISelectItem } from '@feedmepos/ui-library'
|
|
5
6
|
|
|
@@ -10,21 +11,83 @@ import { useDisplaySystemStore } from '@/stores/display-system'
|
|
|
10
11
|
import DisplayScreenEditor from './DisplayScreenEditor.vue'
|
|
11
12
|
import DisplayTemplateEditor from './DisplayTemplateEditor.vue'
|
|
12
13
|
import DisplayLayoutMap from './DisplayLayoutMap.vue'
|
|
14
|
+
import { useOrderSettingPermission } from '@/composables/useOrderSettingPermission'
|
|
15
|
+
import { useMiniProgramPermission } from '@/composables/useMiniProgramPermission'
|
|
16
|
+
import { getBusinessBrandkit } from '@/api/brandkit'
|
|
17
|
+
import { availableDisplaySystemViews, defaultDisplaySystemView, isPosV8, requestedDisplaySystemView, type DisplaySystemView } from './view-selection'
|
|
18
|
+
import LegacyOdsTemplateList from './legacy-ods/LegacyOdsTemplateList.vue'
|
|
13
19
|
|
|
14
|
-
type Tab = 'screens' | 'templates'
|
|
15
20
|
const { t } = useI18n()
|
|
16
21
|
const { showError, showSuccess } = useSnackbarFunctions()
|
|
22
|
+
const route = useRoute()
|
|
23
|
+
const router = useRouter()
|
|
17
24
|
const core = useCoreStore()
|
|
18
25
|
const store = useDisplaySystemStore()
|
|
19
|
-
const
|
|
26
|
+
const { canManageOrderSetting } = useOrderSettingPermission()
|
|
27
|
+
const { canManageBrandkit } = useMiniProgramPermission()
|
|
28
|
+
const hasLegacyOdsConfigs = ref(false)
|
|
29
|
+
const availableViews = computed(() =>
|
|
30
|
+
availableDisplaySystemViews({
|
|
31
|
+
canManageOrderSetting: canManageOrderSetting.value,
|
|
32
|
+
canManageBrandkit: canManageBrandkit.value,
|
|
33
|
+
outlets: core.restaurants.value,
|
|
34
|
+
hasLegacyOdsConfigs: hasLegacyOdsConfigs.value
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
const selectedView = ref<DisplaySystemView>()
|
|
38
|
+
// The default is settled once the outlets and the current outlet have loaded;
|
|
39
|
+
// after that, picking another outlet on the Screens tab must not swap the view.
|
|
40
|
+
let viewSettled = false
|
|
41
|
+
watch(
|
|
42
|
+
[availableViews, () => core.restaurants.value.length, () => core.currentRestaurant.value?._id],
|
|
43
|
+
([available, outletCount, currentOutletId]) => {
|
|
44
|
+
const requested = requestedDisplaySystemView(route.query)
|
|
45
|
+
if (requested && available.includes(requested)) {
|
|
46
|
+
selectedView.value = requested
|
|
47
|
+
viewSettled = true
|
|
48
|
+
} else if (!viewSettled || !selectedView.value || !available.includes(selectedView.value)) {
|
|
49
|
+
selectedView.value = defaultDisplaySystemView(available, core.restaurants.value, currentOutletId)
|
|
50
|
+
viewSettled = Boolean(outletCount && currentOutletId)
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{ immediate: true }
|
|
54
|
+
)
|
|
55
|
+
const otherView = computed(() => availableViews.value.find((view) => view !== selectedView.value))
|
|
56
|
+
function switchView(view: DisplaySystemView) {
|
|
57
|
+
selectedView.value = view
|
|
58
|
+
viewSettled = true
|
|
59
|
+
const { tab: _tab, ...query } = route.query
|
|
60
|
+
router.replace({ query: { ...query, view } })
|
|
61
|
+
}
|
|
62
|
+
// Only an all-v8 business needs this: one with a v7 outlet already gets the v7 view.
|
|
63
|
+
watch(
|
|
64
|
+
[() => core.currentBusiness.value?._id, canManageBrandkit, () => core.restaurants.value.some((outlet) => outlet.posVersion && !isPosV8(outlet.posVersion))],
|
|
65
|
+
async ([businessId, canManage, hasV7Outlet]) => {
|
|
66
|
+
hasLegacyOdsConfigs.value = false
|
|
67
|
+
if (!businessId || !canManage || hasV7Outlet) return
|
|
68
|
+
try {
|
|
69
|
+
const brandkit = await getBusinessBrandkit(businessId, core.namespace.value)
|
|
70
|
+
hasLegacyOdsConfigs.value = Boolean(Array.isArray(brandkit.ods) ? brandkit.ods.length : brandkit.ods)
|
|
71
|
+
} catch {
|
|
72
|
+
hasLegacyOdsConfigs.value = false
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{ immediate: true }
|
|
76
|
+
)
|
|
77
|
+
const selectedTab = ref<'screens' | 'templates'>('screens')
|
|
20
78
|
const selectedRestaurantId = ref(core.currentRestaurant.value?._id ?? '')
|
|
21
79
|
const templateEditorOpen = ref(false)
|
|
22
80
|
const screenEditorOpen = ref(false)
|
|
23
81
|
const editingTemplate = ref<DisplayTemplate>()
|
|
24
82
|
const editingScreen = ref<DisplayScreen>()
|
|
25
83
|
const saving = ref(false)
|
|
84
|
+
const legacyOdsList = ref<InstanceType<typeof LegacyOdsTemplateList>>()
|
|
26
85
|
|
|
27
86
|
const tabs = computed<FmTabProps[]>(() => [{ label: t('order.displaySystem.screens'), value: 'screens' }, { label: t('order.displaySystem.templates'), value: 'templates' }])
|
|
87
|
+
const selectedRestaurant = computed(() =>
|
|
88
|
+
core.restaurants.value.find((item) => item._id === selectedRestaurantId.value)
|
|
89
|
+
)
|
|
90
|
+
const selectedRestaurantIsV8 = computed(() => isPosV8(selectedRestaurant.value?.posVersion))
|
|
28
91
|
const restaurantItems = computed<ISelectItem[]>(() => core.restaurants.value.map((restaurant) => ({ label: restaurant.profile.name, value: restaurant._id })))
|
|
29
92
|
const entitlement = computed(() => store.screenCapabilities)
|
|
30
93
|
// Oldest screens keep the licence, so the newest lose it first. Merchants can
|
|
@@ -33,7 +96,7 @@ const unlicensedScreens = computed(() =>
|
|
|
33
96
|
unlicensedScreenIds(store.screens, entitlement.value?.screenLimit ?? 0))
|
|
34
97
|
const remainingScreens = computed(() => Math.max(0, (entitlement.value?.screenLimit ?? 0) - (entitlement.value?.screenUsed ?? 0)))
|
|
35
98
|
const applicableTemplates = computed(() => store.templates.filter((template) => !template.restaurantIds?.length || template.restaurantIds.includes(selectedRestaurantId.value)))
|
|
36
|
-
const canAddScreen = computed(() => Boolean(selectedRestaurantId.value && entitlement.value?.posV8 && remainingScreens.value > 0 && applicableTemplates.value.length))
|
|
99
|
+
const canAddScreen = computed(() => Boolean(selectedRestaurantId.value && selectedRestaurantIsV8.value && entitlement.value?.posV8 && remainingScreens.value > 0 && applicableTemplates.value.length))
|
|
37
100
|
|
|
38
101
|
function templateName(id: string) { return store.templates.find((template) => template._id === id)?.label ?? 'Unknown template' }
|
|
39
102
|
function orientationMismatch(screen: DisplayScreen) {
|
|
@@ -49,7 +112,7 @@ function layoutSummary(template: DisplayTemplate) {
|
|
|
49
112
|
function isOnline(screen: DisplayScreen) { return Boolean(screen.lastSeenAt && Date.now() - new Date(screen.lastSeenAt).getTime() < 120_000) }
|
|
50
113
|
|
|
51
114
|
async function loadContext() {
|
|
52
|
-
if (!selectedRestaurantId.value) return
|
|
115
|
+
if (!canManageOrderSetting.value || !selectedRestaurantId.value) return
|
|
53
116
|
try { await Promise.all([store.loadScreens(selectedRestaurantId.value), store.loadScreenCapabilities(selectedRestaurantId.value)]) }
|
|
54
117
|
catch (error) { showError(error instanceof Error ? error.message : t('order.displaySystem.loadFailed')) }
|
|
55
118
|
}
|
|
@@ -109,6 +172,7 @@ async function toggleStatus(screen: DisplayScreen) {
|
|
|
109
172
|
|
|
110
173
|
onMounted(async () => {
|
|
111
174
|
if (!selectedRestaurantId.value && core.restaurants.value.length) selectedRestaurantId.value = core.restaurants.value[0]._id
|
|
175
|
+
if (!canManageOrderSetting.value) return
|
|
112
176
|
try { await store.loadTemplates() } catch (error) { showError(error instanceof Error ? error.message : t('order.displaySystem.loadFailed')) }
|
|
113
177
|
await loadContext()
|
|
114
178
|
})
|
|
@@ -116,14 +180,19 @@ watch(() => core.restaurants.value.length, async (length) => { if (!selectedRest
|
|
|
116
180
|
</script>
|
|
117
181
|
|
|
118
182
|
<template>
|
|
119
|
-
<div class="py-[1.5rem]"><FmPageHead :title="t('order.displaySystem.title')" />
|
|
183
|
+
<div class="py-[1.5rem]"><FmPageHead :title="selectedView === 'v7' ? t('order.displaySystem.legacyOds.title') : t('order.displaySystem.title')" />
|
|
120
184
|
<div class="px-[1.5rem]">
|
|
121
|
-
<div class="display-toolbar"><p class="text-sm text-gray-500">{{ t('order.displaySystem.description') }}</p><FmButton icon="add" size="md" :label="selectedTab === 'screens' ? t('order.displaySystem.addScreen') : t('order.displaySystem.addTemplateAction')" :disabled="selectedTab === 'screens' && !canAddScreen" @click="selectedTab === 'screens' ? addScreen() : addTemplate()" /></div>
|
|
185
|
+
<div class="display-toolbar"><p class="text-sm text-gray-500">{{ selectedView === 'v7' ? t('order.displaySystem.legacyOds.description') : t('order.displaySystem.description') }}</p><div class="display-actions"><FmButton v-if="otherView" variant="plain" size="md" :label="otherView === 'v7' ? t('order.displaySystem.legacyOds.switchToLegacy') : t('order.displaySystem.legacyOds.switchToDisplaySystem')" @click="switchView(otherView)" /><FmButton v-if="selectedView === 'v7'" icon="add" size="md" :label="t('order.displaySystem.legacyOds.addConfiguration')" @click="legacyOdsList?.addOdsEntry()" /><FmButton v-else-if="selectedView === 'v8'" icon="add" size="md" :label="selectedTab === 'screens' ? t('order.displaySystem.addScreen') : t('order.displaySystem.addTemplateAction')" :disabled="selectedTab === 'screens' && !canAddScreen" @click="selectedTab === 'screens' ? addScreen() : addTemplate()" /></div></div>
|
|
186
|
+
<template v-if="selectedView === 'v8'">
|
|
122
187
|
<!-- The outlet picker and its allowance belong to the Screens tab only:
|
|
123
188
|
screens are bought and paired per outlet, while templates are
|
|
124
189
|
business-wide and would read as outlet-scoped next to a picker. -->
|
|
125
190
|
<div class="display-controls"><FmTabs v-model:model-value="selectedTab" :items="tabs" /><div v-if="selectedTab === 'screens'" class="display-outlet"><p v-if="entitlement" class="display-entitlement">{{ t('order.displaySystem.screensUsed', { used: entitlement.screenUsed, limit: entitlement.screenLimit }) }} · {{ entitlement.capabilities.join(', ') || t('order.displaySystem.noModulesLicensed') }}</p><FmSelect class="display-restaurant-select" :model-value="selectedRestaurantId" :items="restaurantItems" :placeholder="t('order.displaySystem.selectRestaurant')" searchable @update:model-value="selectRestaurant" /></div></div>
|
|
126
|
-
<
|
|
191
|
+
<div v-if="selectedTab === 'screens' && selectedRestaurant && !selectedRestaurantIsV8" class="restriction-note">
|
|
192
|
+
<p>{{ selectedRestaurant.posVersion ? t('order.displaySystem.posV8RequiredVersion', { version: selectedRestaurant.posVersion }) : t('order.displaySystem.posV8Required') }}</p>
|
|
193
|
+
<FmButton v-if="availableViews.includes('v7')" variant="plain" :label="t('order.displaySystem.legacyOds.openLegacyTab')" @click="switchView('v7')" />
|
|
194
|
+
</div>
|
|
195
|
+
<p v-else-if="selectedTab === 'screens' && !canAddScreen && !store.loadingScreenCapabilities" class="restriction-note">{{ applicableTemplates.length ? t('order.displaySystem.screenLimitReached') : t('order.displaySystem.noTemplatesDescription') }}</p>
|
|
127
196
|
|
|
128
197
|
<section v-if="selectedTab === 'screens'" class="mt-12">
|
|
129
198
|
<div v-if="store.loadingScreens" class="flex justify-center py-40"><FmCircularProgress size="lg" /></div>
|
|
@@ -132,17 +201,19 @@ watch(() => core.restaurants.value.length, async (length) => { if (!selectedRest
|
|
|
132
201
|
<article v-for="screen in store.screens" :key="screen._id" class="resource-card"><div class="flex justify-between gap-12"><div class="min-w-0"><div class="flex flex-wrap items-center gap-8"><h3 class="truncate text-lg font-semibold">{{ screen.name }}</h3><span class="status-badge" :class="screen.status.toLowerCase()">{{ screen.status }}</span><span class="online-dot" :class="{ online: isOnline(screen) }">{{ isOnline(screen) ? t('order.displaySystem.online') : t('order.displaySystem.offline') }}</span><span v-if="unlicensedScreens.has(screen._id)" class="status-badge unlicensed">{{ t('order.displaySystem.unlicensed') }}</span></div><p class="mt-4 text-sm text-gray-500">{{ templateName(screen.templateId) }}</p><p v-if="orientationMismatch(screen)" class="mt-2 text-xs text-orange-700">Reported viewport orientation does not match the template.</p><p v-if="unlicensedScreens.has(screen._id)" class="mt-2 text-xs text-red-700">{{ t('order.displaySystem.unlicensedNote') }}</p></div><div class="flex gap-4"><FmButton :icon="screen.status === 'DISABLED' ? 'play_arrow' : 'pause'" variant="plain" @click="toggleStatus(screen)" /><FmButton icon="edit" variant="plain" @click="editScreen(screen)" /><FmButton icon="delete" variant="plain" @click="removeScreen(screen)" /></div></div><dl class="meta-grid"><div><dt>{{ t('order.displaySystem.deviceId') }}</dt><dd>{{ screen.deviceId || '—' }}</dd></div><div><dt>{{ t('order.displaySystem.lastSeen') }}</dt><dd>{{ screen.lastSeenAt ? new Date(screen.lastSeenAt).toLocaleString() : '—' }}</dd></div></dl></article>
|
|
133
202
|
</div>
|
|
134
203
|
</section>
|
|
135
|
-
<section v-else class="mt-12">
|
|
204
|
+
<section v-else-if="selectedTab === 'templates'" class="mt-12">
|
|
136
205
|
<div v-if="store.loadingTemplates" class="flex justify-center py-40"><FmCircularProgress size="lg" /></div>
|
|
137
206
|
<div v-else-if="!store.templates.length" class="empty-state"><strong>{{ t('order.displaySystem.noTemplates') }}</strong><span>{{ t('order.displaySystem.noTemplatesDescription') }}</span></div>
|
|
138
207
|
<div v-else class="grid grid-cols-1 gap-16 xl:grid-cols-2"><article v-for="template in store.templates" :key="template._id" class="resource-card"><div class="flex justify-between gap-12"><div class="min-w-0"><div class="flex items-center gap-8"><h3 class="truncate text-lg font-semibold">{{ template.label }}</h3><span class="mode-badge">{{ template.orientation }}</span></div><p class="mt-2 text-xs text-gray-500">{{ templateCapabilities(template).join(', ') || 'INFO' }}</p></div><div class="flex gap-4"><FmButton icon="edit" variant="plain" @click="editTemplate(template)" /><FmButton icon="content_copy" variant="plain" @click="duplicateTemplate(template)" /><FmButton icon="delete" variant="plain" @click="removeTemplate(template)" /></div></div><div class="layout-thumb"><DisplayLayoutMap :node="template.root as never" selected-zone-id="" :start-index="1" /></div><p class="thumb-caption">{{ t('order.displaySystem.areasCount', { count: layoutSummary(template).areas }) }} · {{ t('order.displaySystem.modulesCount', { count: layoutSummary(template).modules }) }}</p></article></div>
|
|
139
208
|
</section>
|
|
209
|
+
</template>
|
|
210
|
+
<LegacyOdsTemplateList v-else-if="selectedView === 'v7'" ref="legacyOdsList" />
|
|
140
211
|
</div>
|
|
141
212
|
</div>
|
|
142
|
-
<DisplayTemplateEditor v-model="templateEditorOpen" :template="editingTemplate" :preview-restaurant-id="selectedRestaurantId" :outlet-capabilities="entitlement?.capabilities ?? []" :saving="saving" @save="saveTemplate" />
|
|
143
|
-
<DisplayScreenEditor v-if="selectedRestaurantId" v-model="screenEditorOpen" :screen="editingScreen" :templates="applicableTemplates" :restaurant-id="selectedRestaurantId" :saving="saving" @save="saveScreen" />
|
|
213
|
+
<DisplayTemplateEditor v-if="canManageOrderSetting" v-model="templateEditorOpen" :template="editingTemplate" :preview-restaurant-id="selectedRestaurantId" :outlet-capabilities="entitlement?.capabilities ?? []" :saving="saving" @save="saveTemplate" />
|
|
214
|
+
<DisplayScreenEditor v-if="canManageOrderSetting && selectedRestaurantId" v-model="screenEditorOpen" :screen="editingScreen" :templates="applicableTemplates" :restaurant-id="selectedRestaurantId" :saving="saving" @save="saveScreen" />
|
|
144
215
|
</template>
|
|
145
216
|
|
|
146
217
|
<style scoped>
|
|
147
|
-
.display-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid #e5e7eb;padding-bottom:14px}.display-controls{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-top:12px}.display-restaurant-select{width:min(360px,100%)}.display-outlet{display:flex;align-items:center;gap:16px}.display-entitlement{color:#6b7280;font-size:12px;white-space:nowrap}.restriction-note{margin-top:12px;border:1px solid #fed7aa;border-radius:10px;padding:10px 12px;color:#9a3412;background:#fff7ed;font-size:13px}.empty-state{display:grid;justify-items:center;gap:6px;border:1px dashed #d1d5db;border-radius:12px;padding:56px 20px;color:#6b7280;text-align:center}.empty-state strong{color:#111827}.layout-thumb{display:flex;height:auto;min-height:96px;margin-top:16px;border-radius:10px;background:#f3f4f6;padding:5px;pointer-events:none}.thumb-caption{margin-top:8px;color:#6b7280;font-size:11px}.resource-card{border:1px solid #e5e7eb;border-radius:12px;padding:20px;background:white;box-shadow:0 2px 8px #0000000a}.mode-badge,.status-badge{border-radius:999px;padding:3px 8px;background:#f3f4f6;font-size:11px;font-weight:700}.status-badge.active{color:#166534;background:#dcfce7}.status-badge.disabled{color:#991b1b;background:#fee2e2}.status-badge.unlicensed{color:#b42318;background:#fef3f2;border:1px solid #fecdca}.online-dot{color:#6b7280;font-size:11px}.online-dot::before{display:inline-block;width:7px;height:7px;margin-right:5px;border-radius:50%;background:#9ca3af;content:''}.online-dot.online{color:#15803d}.online-dot.online::before{background:#22c55e}.meta-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:18px;border-top:1px solid #f0f0f0;padding-top:16px}.meta-grid dt{color:#6b7280;font-size:11px}.meta-grid dd{overflow:hidden;margin-top:2px;font-size:12px;text-overflow:ellipsis;white-space:nowrap}@media(max-width:640px){.display-toolbar{align-items:flex-start}.display-controls{align-items:stretch;flex-direction:column}.display-outlet{align-items:stretch;flex-direction:column;gap:8px}.display-restaurant-select{width:100%}}
|
|
218
|
+
.display-toolbar{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid #e5e7eb;padding-bottom:14px}.display-actions{display:flex;flex-shrink:0;align-items:center;gap:8px}.display-controls{display:flex;align-items:center;justify-content:space-between;gap:16px;padding-top:12px}.display-restaurant-select{width:min(360px,100%)}.display-outlet{display:flex;align-items:center;gap:16px}.display-entitlement{color:#6b7280;font-size:12px;white-space:nowrap}.restriction-note{margin-top:12px;border:1px solid #fed7aa;border-radius:10px;padding:10px 12px;color:#9a3412;background:#fff7ed;font-size:13px}.empty-state{display:grid;justify-items:center;gap:6px;border:1px dashed #d1d5db;border-radius:12px;padding:56px 20px;color:#6b7280;text-align:center}.empty-state strong{color:#111827}.layout-thumb{display:flex;height:auto;min-height:96px;margin-top:16px;border-radius:10px;background:#f3f4f6;padding:5px;pointer-events:none}.thumb-caption{margin-top:8px;color:#6b7280;font-size:11px}.resource-card{border:1px solid #e5e7eb;border-radius:12px;padding:20px;background:white;box-shadow:0 2px 8px #0000000a}.mode-badge,.status-badge{border-radius:999px;padding:3px 8px;background:#f3f4f6;font-size:11px;font-weight:700}.status-badge.active{color:#166534;background:#dcfce7}.status-badge.disabled{color:#991b1b;background:#fee2e2}.status-badge.unlicensed{color:#b42318;background:#fef3f2;border:1px solid #fecdca}.online-dot{color:#6b7280;font-size:11px}.online-dot::before{display:inline-block;width:7px;height:7px;margin-right:5px;border-radius:50%;background:#9ca3af;content:''}.online-dot.online{color:#15803d}.online-dot.online::before{background:#22c55e}.meta-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:18px;border-top:1px solid #f0f0f0;padding-top:16px}.meta-grid dt{color:#6b7280;font-size:11px}.meta-grid dd{overflow:hidden;margin-top:2px;font-size:12px;text-overflow:ellipsis;white-space:nowrap}@media(max-width:640px){.display-toolbar{align-items:flex-start}.display-controls{align-items:stretch;flex-direction:column}.display-outlet{align-items:stretch;flex-direction:column;gap:8px}.display-restaurant-select{width:100%}}
|
|
148
219
|
</style>
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import ColorSelection from './ColorSelection.vue'
|
|
4
|
+
import {
|
|
5
|
+
type BaseBrandKit,
|
|
6
|
+
type RecursivePartial
|
|
7
|
+
} from '@/modules/display-system/legacy-ods/interface'
|
|
8
|
+
import { useI18n } from '@feedmepos/mf-common'
|
|
9
|
+
|
|
10
|
+
type ColorField = { key: string; title: string; description: string }
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(
|
|
13
|
+
defineProps<{
|
|
14
|
+
modelValue: boolean
|
|
15
|
+
title: string
|
|
16
|
+
description?: string
|
|
17
|
+
baseBrandkit: BaseBrandKit
|
|
18
|
+
value?: RecursivePartial<BaseBrandKit>
|
|
19
|
+
colorFields: ColorField[]
|
|
20
|
+
}>(),
|
|
21
|
+
{
|
|
22
|
+
colorFields: () => []
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
(e: 'update:modelValue', v: boolean): void
|
|
28
|
+
(e: 'save', v: RecursivePartial<BaseBrandKit> | undefined): void
|
|
29
|
+
(e: 'cancel'): void
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const { t } = useI18n()
|
|
33
|
+
|
|
34
|
+
const opened = computed({
|
|
35
|
+
get: () => props.modelValue,
|
|
36
|
+
set: (v) => emit('update:modelValue', v)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const draft = ref<RecursivePartial<BaseBrandKit>>({})
|
|
40
|
+
|
|
41
|
+
const cloneValue = () => JSON.parse(JSON.stringify(props.value ?? {}))
|
|
42
|
+
|
|
43
|
+
const syncDraft = () => {
|
|
44
|
+
draft.value = cloneValue()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
watch(
|
|
48
|
+
() => props.modelValue,
|
|
49
|
+
(v) => {
|
|
50
|
+
if (v) syncDraft()
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
watch(
|
|
55
|
+
() => props.value,
|
|
56
|
+
() => {
|
|
57
|
+
if (props.modelValue) syncDraft()
|
|
58
|
+
},
|
|
59
|
+
{ deep: true }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const getFromPath = (obj: any, path: string) =>
|
|
63
|
+
path.split('.').reduce((acc, key) => (acc && typeof acc === 'object' ? acc[key] : undefined), obj)
|
|
64
|
+
|
|
65
|
+
const pruneEmpty = (value: any): any => {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
const pruned = value.map(pruneEmpty).filter((v) => v !== undefined)
|
|
68
|
+
return pruned.length ? pruned : undefined
|
|
69
|
+
}
|
|
70
|
+
if (value && typeof value === 'object') {
|
|
71
|
+
const entries = Object.entries(value)
|
|
72
|
+
.map(([k, v]) => [k, pruneEmpty(v)] as const)
|
|
73
|
+
.filter(([, v]) => v !== undefined)
|
|
74
|
+
if (!entries.length) return undefined
|
|
75
|
+
return Object.fromEntries(entries)
|
|
76
|
+
}
|
|
77
|
+
return value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const setAtPath = (obj: any, path: string, value: any) => {
|
|
81
|
+
const keys = path.split('.')
|
|
82
|
+
let current = obj
|
|
83
|
+
keys.forEach((key, idx) => {
|
|
84
|
+
if (idx === keys.length - 1) {
|
|
85
|
+
current[key] = value
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
current[key] = current[key] ?? {}
|
|
89
|
+
current = current[key]
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const deleteAtPath = (obj: any, path: string) => {
|
|
94
|
+
const keys = path.split('.')
|
|
95
|
+
const stack: { parent: any; key: string }[] = []
|
|
96
|
+
let current = obj
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
if (!current || typeof current !== 'object') return
|
|
99
|
+
stack.push({ parent: current, key })
|
|
100
|
+
current = current[key]
|
|
101
|
+
}
|
|
102
|
+
const last = stack.pop()
|
|
103
|
+
if (last) {
|
|
104
|
+
delete last.parent[last.key]
|
|
105
|
+
}
|
|
106
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
107
|
+
const { parent, key } = stack[i]
|
|
108
|
+
if (parent[key] && typeof parent[key] === 'object' && !Object.keys(parent[key]).length) {
|
|
109
|
+
delete parent[key]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const baseColor = (path: string) => {
|
|
115
|
+
const value = getFromPath(props.baseBrandkit, path)
|
|
116
|
+
if (typeof value === 'string') return value
|
|
117
|
+
return '#000000'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const displayColor = (path: string) => {
|
|
121
|
+
const fromOverride = getFromPath(draft.value, path)
|
|
122
|
+
if (typeof fromOverride === 'string') return fromOverride
|
|
123
|
+
return baseColor(path)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const toggleOverride = (path: string, enabled: boolean) => {
|
|
127
|
+
if (!enabled) {
|
|
128
|
+
deleteAtPath(draft.value, path)
|
|
129
|
+
const pruned = pruneEmpty(draft.value)
|
|
130
|
+
draft.value = pruned ?? {}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
setAtPath(draft.value, path, baseColor(path))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const onReset = (path: string) => {
|
|
137
|
+
setAtPath(draft.value, path, baseColor(path))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const onUpdateColor = (path: string, color: string) => {
|
|
141
|
+
setAtPath(draft.value, path, color)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const isOverrideEnabled = (path: string) => getFromPath(draft.value, path) !== undefined
|
|
145
|
+
|
|
146
|
+
const onClose = () => {
|
|
147
|
+
syncDraft()
|
|
148
|
+
emit('cancel')
|
|
149
|
+
opened.value = false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const onSave = () => {
|
|
153
|
+
const allowedDraft: RecursivePartial<BaseBrandKit> = {}
|
|
154
|
+
for (const field of props.colorFields) {
|
|
155
|
+
const value = getFromPath(draft.value, field.key)
|
|
156
|
+
if (value !== undefined) {
|
|
157
|
+
setAtPath(allowedDraft, field.key, value)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const cleaned = pruneEmpty(allowedDraft)
|
|
161
|
+
emit('save', cleaned)
|
|
162
|
+
opened.value = false
|
|
163
|
+
}
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<template>
|
|
167
|
+
<FmDialog v-model="opened" :max-width="720">
|
|
168
|
+
<template #dialog-header>
|
|
169
|
+
<div class="fm-typo-en-body-lg-600 text-fm-color-typo-primary">
|
|
170
|
+
{{ title }}
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
<div class="space-y-16 max-h-[70vh] overflow-y-auto">
|
|
174
|
+
<div
|
|
175
|
+
v-for="field in colorFields"
|
|
176
|
+
:key="field.key"
|
|
177
|
+
class="border border-fm-color-neutral-gray-100 rounded-lg overflow-hidden"
|
|
178
|
+
>
|
|
179
|
+
<div
|
|
180
|
+
class="flex items-center justify-between px-16 py-12 border-b border-fm-color-neutral-gray-100"
|
|
181
|
+
>
|
|
182
|
+
<div class="flex flex-col gap-4">
|
|
183
|
+
<div class="fm-typo-en-body-md-600 text-fm-color-typo-primary">
|
|
184
|
+
{{ field.title }}
|
|
185
|
+
</div>
|
|
186
|
+
<div class="fm-typo-en-body-sm-400 text-fm-color-typo-secondary">
|
|
187
|
+
{{ field.description }}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<FmSwitch
|
|
191
|
+
:model-value="isOverrideEnabled(field.key)"
|
|
192
|
+
@update:model-value="(v: boolean) => toggleOverride(field.key, v)"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<div
|
|
196
|
+
class="px-16 py-12"
|
|
197
|
+
:class="{
|
|
198
|
+
'opacity-50 pointer-events-none': !isOverrideEnabled(field.key)
|
|
199
|
+
}"
|
|
200
|
+
>
|
|
201
|
+
<ColorSelection
|
|
202
|
+
:initial-color="baseColor(field.key)"
|
|
203
|
+
:color="displayColor(field.key)"
|
|
204
|
+
@update="(v) => onUpdateColor(field.key, v)"
|
|
205
|
+
@remove="onReset(field.key)"
|
|
206
|
+
:title="field.title"
|
|
207
|
+
:description="field.description"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
<template #dialog-footer>
|
|
213
|
+
<div class="w-full flex justify-end gap-8">
|
|
214
|
+
<FmButton
|
|
215
|
+
variant="secondary"
|
|
216
|
+
:label="t('order.displaySystem.legacyOds.close')"
|
|
217
|
+
@click="onClose"
|
|
218
|
+
/>
|
|
219
|
+
<FmButton :label="t('order.displaySystem.legacyOds.save')" @click="onSave" />
|
|
220
|
+
</div>
|
|
221
|
+
</template>
|
|
222
|
+
</FmDialog>
|
|
223
|
+
</template>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ColorPicker } from 'vue-accessible-color-picker'
|
|
3
|
+
import 'vue-accessible-color-picker/styles'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
initialColor: string
|
|
7
|
+
color: string
|
|
8
|
+
title: string
|
|
9
|
+
description: string
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const handleCopy = (v: string) => {
|
|
13
|
+
void v
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const emits = defineEmits<{
|
|
17
|
+
(e: 'remove'): void
|
|
18
|
+
(e: 'update', value: string): void
|
|
19
|
+
}>()
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div class="fm-typo-en-body-md-400">
|
|
24
|
+
<div class="flex gap-8 items-center">
|
|
25
|
+
<div class="p-4 relative">
|
|
26
|
+
<FmPopover>
|
|
27
|
+
<template #popover-button>
|
|
28
|
+
<div
|
|
29
|
+
class="aspect-square w-[60px] rounded-sm border border-fm-color-neutral-gray-200 cursor-pointer"
|
|
30
|
+
:style="{
|
|
31
|
+
backgroundColor: color
|
|
32
|
+
}"
|
|
33
|
+
></div>
|
|
34
|
+
</template>
|
|
35
|
+
<div class="shadow-lg rounded-lg p-8">
|
|
36
|
+
<ColorPicker
|
|
37
|
+
:copy="handleCopy"
|
|
38
|
+
@color-copy="(v) => handleCopy(v.cssColor)"
|
|
39
|
+
:default-format="'hex'"
|
|
40
|
+
:color="color"
|
|
41
|
+
@color-change="(v) => emits('update', v.cssColor)"
|
|
42
|
+
>
|
|
43
|
+
<template #copy-button>
|
|
44
|
+
<FmButton class="w-full h-32" variant="secondary" icon="content_copy" />
|
|
45
|
+
</template>
|
|
46
|
+
</ColorPicker>
|
|
47
|
+
</div>
|
|
48
|
+
</FmPopover>
|
|
49
|
+
<div class="absolute top-8 right-8" v-if="initialColor != color">
|
|
50
|
+
<div
|
|
51
|
+
@click="emits('remove')"
|
|
52
|
+
class="h-16 w-16 rounded-full bg-fm-color-neutral-white flex items-center justify-center cursor-pointer"
|
|
53
|
+
>
|
|
54
|
+
<FmIcon name="close_small" class="!text-[16px]" />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="space-y-4">
|
|
59
|
+
<div>{{ title }}</div>
|
|
60
|
+
<div class="text-fm-color-typo-secondary">{{ description }}</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="text-fm-color-typo-secondary px-4">
|
|
64
|
+
{{ color }}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
4
|
+
import { ref } from 'vue'
|
|
5
|
+
import type { OdsConfigEntry } from '@/modules/display-system/legacy-ods/interface'
|
|
6
|
+
|
|
7
|
+
const access = {
|
|
8
|
+
canAccessOdsTemplateSettings: ref(true),
|
|
9
|
+
isAdvancedCustomizationEnabled: ref(false)
|
|
10
|
+
}
|
|
11
|
+
vi.mock('@/composables/useLegacyOdsAccess', () => ({ useLegacyOdsAccess: () => access }))
|
|
12
|
+
vi.mock('@feedmepos/media-picker', () => ({
|
|
13
|
+
MediaPickerModal: {
|
|
14
|
+
name: 'MediaPickerModal',
|
|
15
|
+
props: ['open', 'businessId', 'kinds', 'zIndex'],
|
|
16
|
+
template: '<div />'
|
|
17
|
+
}
|
|
18
|
+
}))
|
|
19
|
+
vi.mock('@feedmepos/mf-common', () => ({
|
|
20
|
+
RestaurantSelector: { name: 'RestaurantSelector', template: '<div />' },
|
|
21
|
+
useCoreStore: () => ({ currentBusiness: ref({ _id: 'business' }), namespace: ref('dev') }),
|
|
22
|
+
useI18n: () => ({ t: (key: string) => key })
|
|
23
|
+
}))
|
|
24
|
+
vi.mock('./ColorOverrideDialog.vue', () => ({ default: { template: '<div />' } }))
|
|
25
|
+
import Editor from './LegacyOdsEditorDialog.vue'
|
|
26
|
+
|
|
27
|
+
const wrappers: VueWrapper[] = []
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
access.canAccessOdsTemplateSettings.value = true
|
|
30
|
+
access.isAdvancedCustomizationEnabled.value = false
|
|
31
|
+
vi.stubGlobal(
|
|
32
|
+
'ResizeObserver',
|
|
33
|
+
class {
|
|
34
|
+
observe() {}
|
|
35
|
+
disconnect() {}
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
wrappers.forEach((wrapper) => wrapper.unmount())
|
|
41
|
+
wrappers.length = 0
|
|
42
|
+
vi.unstubAllGlobals()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
async function editor(entry: OdsConfigEntry = { isDefault: true, label: '默认 🍵' }) {
|
|
46
|
+
const wrapper = mount(Editor, {
|
|
47
|
+
props: {
|
|
48
|
+
modelValue: false,
|
|
49
|
+
entry,
|
|
50
|
+
editingIndex: 0,
|
|
51
|
+
allEntries: [entry],
|
|
52
|
+
brandkit: { colors: { primary: '#123456' } } as never,
|
|
53
|
+
colorFields: []
|
|
54
|
+
},
|
|
55
|
+
global: {
|
|
56
|
+
stubs: {
|
|
57
|
+
Teleport: true,
|
|
58
|
+
FmButton: {
|
|
59
|
+
props: ['label', 'disabled'],
|
|
60
|
+
template: '<button :disabled="disabled">{{ label }}</button>'
|
|
61
|
+
},
|
|
62
|
+
FmIcon: true,
|
|
63
|
+
FmTextField: true,
|
|
64
|
+
FmSwitch: true,
|
|
65
|
+
FmButtonGroup: true,
|
|
66
|
+
FmStepperField: true,
|
|
67
|
+
FmRadio: true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
wrappers.push(wrapper)
|
|
72
|
+
await wrapper.setProps({ modelValue: true })
|
|
73
|
+
return wrapper
|
|
74
|
+
}
|
|
75
|
+
async function confirm(wrapper: VueWrapper) {
|
|
76
|
+
await wrapper
|
|
77
|
+
.findAll('button')
|
|
78
|
+
.find((button) => button.text().endsWith('.confirm'))!
|
|
79
|
+
.trigger('click')
|
|
80
|
+
// The list closes the editor only once the save has landed.
|
|
81
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
82
|
+
return wrapper.emitted('save')![0][0] as OdsConfigEntry
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function previewBrandKit(wrapper: VueWrapper) {
|
|
86
|
+
const url = new URL(wrapper.get('iframe').attributes('src'))
|
|
87
|
+
expect(url.origin).toBe('https://v2-dev.feedme-ods.pages.dev')
|
|
88
|
+
expect(url.searchParams.get('preview')).toBe('1')
|
|
89
|
+
return JSON.parse(Buffer.from(url.searchParams.get('brandKit')!, 'base64url').toString('utf8'))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('legacy ODS editor', () => {
|
|
93
|
+
it('keeps collected-order hiding across preset changes and previews the draft as default', async () => {
|
|
94
|
+
const wrapper = await editor({
|
|
95
|
+
isDefault: false,
|
|
96
|
+
label: '默认 🍵',
|
|
97
|
+
restaurantIds: ['outlet'],
|
|
98
|
+
display: { hideCompletedOrders: true } as never
|
|
99
|
+
})
|
|
100
|
+
const presets = wrapper.findAllComponents({ name: 'FmButtonGroup' })[0]
|
|
101
|
+
presets.vm.$emit('update:modelValue', { value: 'preset2' })
|
|
102
|
+
await wrapper.vm.$nextTick()
|
|
103
|
+
const preview = previewBrandKit(wrapper)
|
|
104
|
+
expect(preview.colors.primary).toBe('#123456')
|
|
105
|
+
expect(preview.ods[0]).toMatchObject({
|
|
106
|
+
isDefault: true,
|
|
107
|
+
layout: { background: null, sidePanel: { url: '/images/feedme-side.png' } },
|
|
108
|
+
display: { groupBy: 'status', hideCompletedOrders: true }
|
|
109
|
+
})
|
|
110
|
+
const saved = await confirm(wrapper)
|
|
111
|
+
expect(saved.label).toBe('默认 🍵')
|
|
112
|
+
expect(saved.restaurantIds).toEqual(['outlet'])
|
|
113
|
+
expect(saved.isDefault).toBe(false)
|
|
114
|
+
expect(saved.display?.hideCompletedOrders).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('leaves metadata, outlet selection and presets editable without the template flag', async () => {
|
|
118
|
+
access.canAccessOdsTemplateSettings.value = false
|
|
119
|
+
const wrapper = await editor({ isDefault: false, label: 'Existing', restaurantIds: ['outlet'] })
|
|
120
|
+
const label = wrapper.findComponent({ name: 'FmTextField' })
|
|
121
|
+
expect(label.attributes('disabled')).toBeUndefined()
|
|
122
|
+
label.vm.$emit('update:modelValue', 'Renamed')
|
|
123
|
+
const selector = wrapper.findComponent({ name: 'RestaurantSelector' })
|
|
124
|
+
expect(selector.attributes('disabled')).toBeUndefined()
|
|
125
|
+
expect(selector.attributes('managable-only')).toBe('false')
|
|
126
|
+
selector.vm.$emit('update:modelValue', ['other-outlet'])
|
|
127
|
+
const defaultSwitch = wrapper
|
|
128
|
+
.findAllComponents({ name: 'FmSwitch' })
|
|
129
|
+
.find((item) => item.attributes('label')?.endsWith('.defaultConfiguration'))!
|
|
130
|
+
expect(defaultSwitch.attributes('disabled')).toBeUndefined()
|
|
131
|
+
defaultSwitch.vm.$emit('update:modelValue', true)
|
|
132
|
+
const presets = wrapper.findComponent({ name: 'FmButtonGroup' })
|
|
133
|
+
expect(presets.attributes('disabled')).toBeUndefined()
|
|
134
|
+
presets.vm.$emit('update:modelValue', { value: 'preset2' })
|
|
135
|
+
const hide = wrapper
|
|
136
|
+
.findAllComponents({ name: 'FmSwitch' })
|
|
137
|
+
.find((item) => item.attributes('label')?.endsWith('.hideCollectedOrders'))!
|
|
138
|
+
hide.vm.$emit('update:modelValue', true)
|
|
139
|
+
const saved = await confirm(wrapper)
|
|
140
|
+
expect(saved).toMatchObject({
|
|
141
|
+
label: 'Renamed',
|
|
142
|
+
isDefault: true,
|
|
143
|
+
restaurantIds: ['other-outlet'],
|
|
144
|
+
layout: { background: null, sidePanel: { url: '/images/feedme-side.png' } },
|
|
145
|
+
display: { groupBy: 'status', hideCompletedOrders: true }
|
|
146
|
+
})
|
|
147
|
+
expect(wrapper.text()).not.toContain('order.displaySystem.legacyOds.displaySettingsDescription')
|
|
148
|
+
expect(wrapper.text()).not.toContain('order.displaySystem.legacyOds.layoutSettingsDescription')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('replaces a custom image with one picked from the media library', async () => {
|
|
152
|
+
const media = 'https://image.feedmedev.vip/media-image/business/asset/a1/main'
|
|
153
|
+
const wrapper = await editor({
|
|
154
|
+
isDefault: true,
|
|
155
|
+
layout: {
|
|
156
|
+
background: { url: 'https://image.feedmedev.vip/business/business/ods/background/old.png' },
|
|
157
|
+
sidePanel: null
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
const picker = () => wrapper.findComponent({ name: 'MediaPickerModal' })
|
|
161
|
+
expect(picker().props()).toMatchObject({ open: false, businessId: 'business', kinds: ['image'] })
|
|
162
|
+
await wrapper
|
|
163
|
+
.findAll('button')
|
|
164
|
+
.find((button) => button.text().endsWith('.changeImage'))!
|
|
165
|
+
.trigger('click')
|
|
166
|
+
expect(picker().props('open')).toBe(true)
|
|
167
|
+
picker().vm.$emit('select', { url: media, kind: 'image' })
|
|
168
|
+
await wrapper.vm.$nextTick()
|
|
169
|
+
expect(picker().props('open')).toBe(false)
|
|
170
|
+
const saved = await confirm(wrapper)
|
|
171
|
+
expect(saved.layout?.background).toMatchObject({
|
|
172
|
+
url: media,
|
|
173
|
+
size: 'cover',
|
|
174
|
+
position: 'center'
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('shows advanced controls only when the advanced flag is enabled', async () => {
|
|
179
|
+
const wrapper = await editor()
|
|
180
|
+
expect(wrapper.text()).not.toContain('order.displaySystem.legacyOds.footerModeDescription')
|
|
181
|
+
access.isAdvancedCustomizationEnabled.value = true
|
|
182
|
+
await wrapper.vm.$nextTick()
|
|
183
|
+
expect(wrapper.text()).toContain('order.displaySystem.legacyOds.footerModeDescription')
|
|
184
|
+
})
|
|
185
|
+
})
|