@kennofizet/apphub-frontend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +84 -0
  2. package/package.json +31 -0
  3. package/src/api/coreApi.js +25 -0
  4. package/src/api/index.js +80 -0
  5. package/src/composables/createZoneContext.js +156 -0
  6. package/src/composables/useAppHubHostApi.js +24 -0
  7. package/src/composables/useAppHubZoneContext.js +11 -0
  8. package/src/composables/useDevOriginToggle.js +40 -0
  9. package/src/i18n/index.js +16 -0
  10. package/src/i18n/resolveLang.js +6 -0
  11. package/src/i18n/resolveTheme.js +30 -0
  12. package/src/i18n/translations/en.js +303 -0
  13. package/src/i18n/translations/vi.js +302 -0
  14. package/src/index.js +427 -0
  15. package/src/moduleStore.js +10 -0
  16. package/src/modules/app-store/components/AppHubAppStoreApp.vue +210 -0
  17. package/src/modules/app-store/components/AppHubAppStoreCard.vue +88 -0
  18. package/src/modules/app-store/components/AppHubAppStoreSettingsPanel.vue +266 -0
  19. package/src/modules/app-store/components/AppHubAppVersionHistory.vue +77 -0
  20. package/src/modules/app-store/components/AppHubDevReviewPanel.vue +206 -0
  21. package/src/modules/app-store/components/AppHubDraftStoreApp.vue +184 -0
  22. package/src/modules/app-store/components/AppHubDraftStoreCard.vue +116 -0
  23. package/src/modules/app-store/composables/useAppStore.js +206 -0
  24. package/src/modules/app-store/composables/useCatalogInfiniteScroll.js +47 -0
  25. package/src/modules/app-store/constants/catalogModes.js +2 -0
  26. package/src/modules/app-store/data/defaultCatalog.js +19 -0
  27. package/src/modules/app-store/index.js +9 -0
  28. package/src/modules/app-store/utils/normalizeCatalogApp.js +37 -0
  29. package/src/modules/desktop/components/AppHubDesktop.vue +1510 -0
  30. package/src/modules/desktop/components/AppHubDesktopDevOriginBar.vue +57 -0
  31. package/src/modules/desktop/components/AppHubDesktopDropLayer.vue +15 -0
  32. package/src/modules/desktop/components/AppHubDesktopDropTarget.vue +32 -0
  33. package/src/modules/desktop/components/AppHubDesktopIconContextMenu.vue +74 -0
  34. package/src/modules/desktop/components/AppHubDesktopIconFolder.vue +60 -0
  35. package/src/modules/desktop/components/AppHubDesktopIconGroup.vue +58 -0
  36. package/src/modules/desktop/components/AppHubDesktopIconInfoDialog.vue +33 -0
  37. package/src/modules/desktop/components/AppHubDesktopIconRenameDialog.vue +62 -0
  38. package/src/modules/desktop/components/AppHubDesktopSettings.vue +28 -0
  39. package/src/modules/desktop/components/AppHubDropInstallBadge.vue +65 -0
  40. package/src/modules/desktop/components/AppHubDuplicateAppDialog.vue +38 -0
  41. package/src/modules/desktop/components/AppHubGuideApp.vue +278 -0
  42. package/src/modules/desktop/components/AppHubOriginBlockScreen.vue +105 -0
  43. package/src/modules/desktop/components/AppHubOriginLoadingScreen.vue +23 -0
  44. package/src/modules/desktop/components/AppHubPlaceholderApp.vue +14 -0
  45. package/src/modules/desktop/components/AppHubSettingsApp.vue +319 -0
  46. package/src/modules/desktop/components/AppHubStartButton.vue +24 -0
  47. package/src/modules/desktop/components/AppHubStartMenu.vue +182 -0
  48. package/src/modules/desktop/components/AppHubTaskbarPins.vue +23 -0
  49. package/src/modules/desktop/components/settings/AppHubSettingsKeyboardPanel.vue +82 -0
  50. package/src/modules/desktop/components/settings/AppHubSettingsScreenPanel.vue +41 -0
  51. package/src/modules/desktop/components/settings/AppHubSettingsStartMenuPanel.vue +95 -0
  52. package/src/modules/desktop/composables/simulateInstallProgress.js +15 -0
  53. package/src/modules/desktop/composables/useDesktopDropInstall.js +272 -0
  54. package/src/modules/desktop/composables/useDesktopHubSettings.js +51 -0
  55. package/src/modules/desktop/composables/useDesktopIconDrag.js +207 -0
  56. package/src/modules/desktop/composables/useDesktopShell.js +335 -0
  57. package/src/modules/desktop/data/builtinApps.js +77 -0
  58. package/src/modules/desktop/index.js +12 -0
  59. package/src/modules/desktop/styles/desktop.css +3104 -0
  60. package/src/modules/desktop/styles/theme.css +616 -0
  61. package/src/modules/desktop/utils/desktopGrid.js +43 -0
  62. package/src/modules/desktop/utils/desktopIconGroups.js +103 -0
  63. package/src/modules/desktop/utils/desktopSession.js +40 -0
  64. package/src/modules/desktop/utils/desktopSettings.js +37 -0
  65. package/src/modules/desktop/utils/dropPackageParser.js +140 -0
  66. package/src/modules/desktop/utils/duplicateAppUtils.js +28 -0
  67. package/src/modules/desktop/utils/hubKeyboardSettings.js +63 -0
  68. package/src/modules/desktop/utils/recentApps.js +148 -0
  69. package/src/modules/desktop/utils/startMenuFavorites.js +100 -0
  70. package/src/modules/desktop/utils/startMenuPins.js +90 -0
  71. package/src/modules/notifications/components/AppHubDesktopNotifications.vue +54 -0
  72. package/src/modules/notifications/composables/createDesktopNotifications.js +86 -0
  73. package/src/modules/notifications/index.js +9 -0
  74. package/src/modules/notifications/styles/notifications.css +118 -0
  75. package/src/modules/notifications/utils/parseApiError.js +29 -0
  76. package/src/modules/runner/components/AppHubRunner.vue +292 -0
  77. package/src/modules/runner/index.js +1 -0
  78. package/src/modules/window-manager/components/AppHubWindowFrame.vue +224 -0
  79. package/src/modules/window-manager/composables/useWindowManager.js +652 -0
  80. package/src/modules/window-manager/index.js +7 -0
  81. package/src/modules/window-manager/utils/sessionLayout.js +28 -0
  82. package/src/modules/window-manager/utils/windowLayout.js +236 -0
  83. package/src/modules/window-manager/utils/windowSnap.js +146 -0
  84. package/src/utils/bootstrapCache.js +47 -0
  85. package/src/utils/devOriginSettings.js +22 -0
  86. package/src/utils/launchUrl.js +111 -0
  87. package/src/utils/originSafety.js +267 -0
  88. package/src/utils/safeStorage.js +191 -0
  89. package/src/utils/semver.js +30 -0
  90. package/src/utils/zoneContext.js +38 -0
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <span class="apphub-store__icon">{{ app.icon }}</span>
3
+ <div class="apphub-store__meta">
4
+ <div class="apphub-store__title-row">
5
+ <strong>{{ app.name }}</strong>
6
+ <span
7
+ v-if="statusLabel"
8
+ class="apphub-store__badge"
9
+ :class="statusBadgeClass"
10
+ >
11
+ {{ statusLabel }}
12
+ </span>
13
+ </div>
14
+ <p v-if="app.version" class="apphub-store__version">
15
+ v{{ app.version }}
16
+ <span v-if="installedVersion" class="apphub-store__version-installed">
17
+ · {{ labels.app_store_installed_version }} v{{ installedVersion }}
18
+ </span>
19
+ </p>
20
+ <p>{{ app.description }}</p>
21
+ </div>
22
+ <button
23
+ v-if="!installed && canInstall"
24
+ type="button"
25
+ class="apphub-store__btn"
26
+ @click="emit('install', app)"
27
+ >
28
+ {{ labels.app_store_install }}
29
+ </button>
30
+ <span
31
+ v-else-if="!canInstall"
32
+ class="apphub-store__unavailable"
33
+ :title="labels.app_store_unavailable"
34
+ >
35
+ {{ labels.app_store_unavailable }}
36
+ </span>
37
+ <div v-else class="apphub-store__installed-row">
38
+ <span class="apphub-store__installed" :title="labels.app_store_installed">✓</span>
39
+ <button
40
+ v-if="updateAvailable"
41
+ type="button"
42
+ class="apphub-store__btn apphub-store__btn--primary"
43
+ @click="emit('update', app)"
44
+ >
45
+ {{ labels.app_store_update }}
46
+ </button>
47
+ <button
48
+ type="button"
49
+ class="apphub-store__btn apphub-store__btn--secondary"
50
+ @click="emit('uninstall', app)"
51
+ >
52
+ {{ labels.app_store_uninstall }}
53
+ </button>
54
+ </div>
55
+ </template>
56
+
57
+ <script setup>
58
+ import { computed } from 'vue'
59
+ import { isSemverGreaterThan } from '../../../utils/semver.js'
60
+
61
+ const props = defineProps({
62
+ app: { type: Object, required: true },
63
+ labels: { type: Object, required: true },
64
+ installed: { type: Boolean, default: false },
65
+ installedVersion: { type: String, default: null },
66
+ canInstall: { type: Boolean, default: true },
67
+ })
68
+
69
+ const emit = defineEmits(['install', 'uninstall', 'update'])
70
+
71
+ const updateAvailable = computed(() => {
72
+ if (!props.installed || !props.app?.version) return false
73
+ if (!props.installedVersion) return false
74
+ return isSemverGreaterThan(props.app.version, props.installedVersion)
75
+ })
76
+
77
+ const statusLabel = computed(() => {
78
+ if (props.app.status === 'draft') return props.labels.app_store_status_draft
79
+ if (props.app.status === 'disabled') return props.labels.app_store_status_offline
80
+ return ''
81
+ })
82
+
83
+ const statusBadgeClass = computed(() => {
84
+ if (props.app.status === 'draft') return 'apphub-store__badge--draft'
85
+ if (props.app.status === 'disabled') return 'apphub-store__badge--offline'
86
+ return ''
87
+ })
88
+ </script>
@@ -0,0 +1,266 @@
1
+ <template>
2
+ <div class="apphub-store-settings">
3
+ <section class="apphub-store-settings__section">
4
+ <h3 class="apphub-store-settings__heading">{{ labels.user_title }}</h3>
5
+ <dl class="apphub-store-settings__dl">
6
+ <div class="apphub-store-settings__row">
7
+ <dt>{{ labels.user_id }}</dt>
8
+ <dd>{{ userDisplay.id }}</dd>
9
+ </div>
10
+ <div class="apphub-store-settings__row">
11
+ <dt>{{ labels.user_name }}</dt>
12
+ <dd>{{ userDisplay.name }}</dd>
13
+ </div>
14
+ <div class="apphub-store-settings__row">
15
+ <dt>{{ labels.auth_status }}</dt>
16
+ <dd>
17
+ <span class="apphub-store-settings__badge" :class="authBadgeClass">{{ authBadgeLabel }}</span>
18
+ </dd>
19
+ </div>
20
+ <div v-if="zone.state.isManager" class="apphub-store-settings__row">
21
+ <dt>{{ labels.manager }}</dt>
22
+ <dd>{{ labels.manager_yes }}</dd>
23
+ </div>
24
+ </dl>
25
+ </section>
26
+
27
+ <section class="apphub-store-settings__section">
28
+ <h3 class="apphub-store-settings__heading">{{ labels.zone_title }}</h3>
29
+ <p class="apphub-store-settings__hint">{{ labels.zone_hint }}</p>
30
+
31
+ <p v-if="zone.state.loading" class="apphub-store-settings__msg">{{ labels.zone_loading }}</p>
32
+ <p v-else-if="zone.state.error === 'no_core_api'" class="apphub-store-settings__msg">
33
+ {{ labels.zone_no_core }}
34
+ </p>
35
+ <p v-else-if="zone.state.error" class="apphub-store-settings__msg">{{ labels.zone_error }}</p>
36
+ <p v-else-if="!zone.state.zones.length" class="apphub-store-settings__msg">{{ labels.zone_empty }}</p>
37
+
38
+ <template v-else>
39
+ <label v-if="zone.state.zones.length > 1" class="apphub-store-settings__check">
40
+ <input
41
+ type="checkbox"
42
+ :checked="zone.state.viewAllZones"
43
+ @change="onViewAllChange"
44
+ />
45
+ {{ labels.zone_all }}
46
+ </label>
47
+
48
+ <div
49
+ v-if="zone.state.zones.length > 1 && !zone.state.viewAllZones"
50
+ class="apphub-store-settings__field"
51
+ >
52
+ <label class="apphub-store-settings__label" for="apphub-store-zone-select">{{ labels.zone_select }}</label>
53
+ <select
54
+ id="apphub-store-zone-select"
55
+ class="apphub-store-settings__select"
56
+ :value="zone.state.selectedZoneId ?? ''"
57
+ @change="onZoneSelect"
58
+ >
59
+ <option v-for="z in zone.state.zones" :key="z.id" :value="z.id">
60
+ {{ z.name || labels.zone_name(z.id) }}
61
+ </option>
62
+ </select>
63
+ </div>
64
+
65
+ <div v-else-if="zone.state.zones.length === 1" class="apphub-store-settings__single">
66
+ <span class="apphub-store-settings__zone-chip">
67
+ {{ zone.state.zones[0].name || labels.zone_name(zone.state.zones[0].id) }}
68
+ </span>
69
+ </div>
70
+
71
+ <ul class="apphub-store-settings__zone-list">
72
+ <li
73
+ v-for="z in zone.state.zones"
74
+ :key="z.id"
75
+ class="apphub-store-settings__zone-item"
76
+ :class="{ 'apphub-store-settings__zone-item--active': isZoneActive(z.id) }"
77
+ >
78
+ <span class="apphub-store-settings__zone-id">#{{ z.id }}</span>
79
+ <span>{{ z.name || labels.zone_name(z.id) }}</span>
80
+ </li>
81
+ </ul>
82
+
83
+ <p v-if="zone.state.timezone" class="apphub-store-settings__meta">
84
+ {{ labels.timezone }}: {{ zone.state.timezone }}
85
+ </p>
86
+ </template>
87
+
88
+ <button type="button" class="apphub-store-settings__refresh" @click="onRefresh">
89
+ {{ labels.refresh }}
90
+ </button>
91
+ </section>
92
+
93
+ <AppHubDevReviewPanel
94
+ :root-app="rootApp"
95
+ :dev-apps="devApps"
96
+ @refreshed="onDevRefreshed"
97
+ />
98
+
99
+ <section v-if="devApps.length" class="apphub-store-settings__section">
100
+ <h3 class="apphub-store-settings__heading">{{ labels.dev_title }}</h3>
101
+ <p class="apphub-store-settings__hint">{{ labels.dev_hint }}</p>
102
+ <ul class="apphub-store-settings__dev-list">
103
+ <li v-for="app in devApps" :key="app.slug" class="apphub-store-settings__dev-item">
104
+ <span>{{ app.name }} ({{ app.status }})</span>
105
+ <button
106
+ v-if="app.status !== 'disabled'"
107
+ type="button"
108
+ class="apphub-store-settings__dev-btn"
109
+ @click="disableApp(app.slug)"
110
+ >
111
+ {{ labels.dev_disable }}
112
+ </button>
113
+ <button
114
+ v-else
115
+ type="button"
116
+ class="apphub-store-settings__dev-btn"
117
+ @click="setAppStatus(app.slug, 'active')"
118
+ >
119
+ {{ labels.dev_enable }}
120
+ </button>
121
+ </li>
122
+ </ul>
123
+ </section>
124
+ </div>
125
+ </template>
126
+
127
+ <script setup>
128
+ import { computed, inject, onMounted, ref } from 'vue'
129
+ import { getHostApiForApp } from '../../../composables/useAppHubHostApi.js'
130
+ import { useAppHubZoneContext } from '../../../composables/useAppHubZoneContext.js'
131
+ import { t } from '../../../i18n/index.js'
132
+ import { resolveLang } from '../../../i18n/resolveLang.js'
133
+ import AppHubDevReviewPanel from './AppHubDevReviewPanel.vue'
134
+
135
+ const props = defineProps({
136
+ rootApp: { type: Object, default: null },
137
+ })
138
+
139
+ const emit = defineEmits(['refreshed'])
140
+
141
+ const devApps = ref([])
142
+ const zone = useAppHubZoneContext()
143
+ const moduleOptions = inject('apphubOptions', {})
144
+ const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
145
+
146
+ const labels = computed(() => ({
147
+ user_title: t('app_store_settings_user_title', lang.value),
148
+ user_id: t('app_store_settings_user_id', lang.value),
149
+ user_name: t('app_store_settings_user_name', lang.value),
150
+ auth_status: t('app_store_settings_auth_status', lang.value),
151
+ auth_ok: t('app_store_settings_auth_ok', lang.value),
152
+ auth_missing: t('app_store_settings_auth_missing', lang.value),
153
+ auth_fail: t('app_store_settings_auth_fail', lang.value),
154
+ manager: t('app_store_settings_manager', lang.value),
155
+ manager_yes: t('app_store_settings_manager_yes', lang.value),
156
+ zone_title: t('app_store_settings_zone_title', lang.value),
157
+ zone_hint: t('app_store_settings_zone_hint', lang.value),
158
+ zone_loading: t('app_store_settings_zone_loading', lang.value),
159
+ zone_no_core: t('app_store_settings_zone_no_core', lang.value),
160
+ zone_error: t('app_store_settings_zone_error', lang.value),
161
+ zone_empty: t('app_store_settings_zone_empty', lang.value),
162
+ zone_select: t('app_store_settings_zone_select', lang.value),
163
+ zone_all: t('app_store_settings_zone_all', lang.value),
164
+ zone_name: (id) => t('app_store_settings_zone_name', lang.value, { id }),
165
+ timezone: t('app_store_settings_timezone', lang.value),
166
+ refresh: t('app_store_settings_refresh', lang.value),
167
+ dev_title: t('app_store_settings_dev_title', lang.value),
168
+ dev_hint: t('app_store_settings_dev_hint', lang.value),
169
+ dev_disable: t('app_store_settings_dev_disable', lang.value),
170
+ dev_enable: t('app_store_settings_dev_enable', lang.value),
171
+ }))
172
+
173
+ const userDisplay = computed(() => ({
174
+ id: zone.state.user?.id ?? '—',
175
+ name: zone.state.user?.name ?? '—',
176
+ }))
177
+
178
+ const authBadgeClass = computed(() => {
179
+ if (!moduleOptions?.hasToken) return 'apphub-store-settings__badge--warn'
180
+ if (zone.state.authOk) return 'apphub-store-settings__badge--ok'
181
+ return 'apphub-store-settings__badge--warn'
182
+ })
183
+
184
+ const authBadgeLabel = computed(() => {
185
+ if (!moduleOptions?.hasToken) return labels.value.auth_missing
186
+ if (zone.state.authOk) return labels.value.auth_ok
187
+ return labels.value.auth_fail
188
+ })
189
+
190
+ function isZoneActive(id) {
191
+ if (zone.state.viewAllZones) return true
192
+ return zone.state.selectedZoneId === id
193
+ }
194
+
195
+ function onZoneSelect(event) {
196
+ const id = Number(event.target.value)
197
+ if (Number.isFinite(id)) zone.selectZone(id)
198
+ }
199
+
200
+ function onViewAllChange(event) {
201
+ zone.setViewAllZones(event.target.checked)
202
+ }
203
+
204
+ async function loadDevApps() {
205
+ const api = getHostApiForApp(props.rootApp)
206
+ if (!api?.bootstrap || !api?.devApps) {
207
+ devApps.value = []
208
+ return
209
+ }
210
+ try {
211
+ const boot = await api.bootstrap()
212
+ const isDev = boot?.data?.data?.is_dev_user === true
213
+ if (!isDev) {
214
+ devApps.value = []
215
+ return
216
+ }
217
+ const res = await api.devApps()
218
+ devApps.value = res?.data?.data ?? []
219
+ } catch {
220
+ devApps.value = []
221
+ }
222
+ }
223
+
224
+ async function disableApp(slug) {
225
+ const api = getHostApiForApp(props.rootApp)
226
+ if (!api?.devDisableApp) return
227
+ try {
228
+ await api.devDisableApp(slug)
229
+ await loadDevApps()
230
+ emit('refreshed')
231
+ } catch {
232
+ /* ignore */
233
+ }
234
+ }
235
+
236
+ async function setAppStatus(slug, status) {
237
+ const api = getHostApiForApp(props.rootApp)
238
+ if (!api?.devSetAppStatus) return
239
+ try {
240
+ await api.devSetAppStatus(slug, status)
241
+ await loadDevApps()
242
+ emit('refreshed')
243
+ } catch {
244
+ /* ignore */
245
+ }
246
+ }
247
+
248
+ async function onDevRefreshed() {
249
+ await loadDevApps()
250
+ emit('refreshed')
251
+ }
252
+
253
+ async function onRefresh() {
254
+ await zone.refresh()
255
+ await loadDevApps()
256
+ emit('refreshed')
257
+ }
258
+
259
+ onMounted(() => {
260
+ if (!zone.state.zones.length && !zone.state.loading) {
261
+ onRefresh()
262
+ } else {
263
+ loadDevApps()
264
+ }
265
+ })
266
+ </script>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div v-if="open" class="apphub-version-history">
3
+ <p class="apphub-version-history__head">{{ labels.title }}</p>
4
+ <p v-if="loading" class="apphub-version-history__msg">{{ labels.loading }}</p>
5
+ <p v-else-if="error" class="apphub-version-history__msg apphub-version-history__msg--error">{{ error }}</p>
6
+ <p v-else-if="!rows.length" class="apphub-version-history__msg">{{ labels.empty }}</p>
7
+ <ul v-else class="apphub-version-history__list">
8
+ <li
9
+ v-for="row in rows"
10
+ :key="row.version"
11
+ class="apphub-version-history__item"
12
+ :class="{ 'apphub-version-history__item--current': row.is_current }"
13
+ >
14
+ <div class="apphub-version-history__row">
15
+ <strong>v{{ row.version }}</strong>
16
+ <span v-if="row.is_current" class="apphub-version-history__badge">{{ labels.current }}</span>
17
+ </div>
18
+ <p v-if="row.uploaded_at" class="apphub-version-history__meta">{{ formatDate(row.uploaded_at) }}</p>
19
+ <p v-if="row.bundle_hash" class="apphub-version-history__hash">{{ shortHash(row.bundle_hash) }}</p>
20
+ </li>
21
+ </ul>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup>
26
+ import { ref, watch } from 'vue'
27
+ import { getHostApiForApp } from '../../../composables/useAppHubHostApi.js'
28
+ import { parseApiError } from '../../notifications/utils/parseApiError.js'
29
+
30
+ const props = defineProps({
31
+ slug: { type: String, required: true },
32
+ rootApp: { type: Object, default: null },
33
+ open: { type: Boolean, default: false },
34
+ labels: { type: Object, required: true },
35
+ })
36
+
37
+ const loading = ref(false)
38
+ const error = ref('')
39
+ const rows = ref([])
40
+
41
+ function shortHash(hash) {
42
+ if (!hash || typeof hash !== 'string') return ''
43
+ return hash.length > 16 ? `${hash.slice(0, 12)}…` : hash
44
+ }
45
+
46
+ function formatDate(value) {
47
+ try {
48
+ return new Date(value).toLocaleString()
49
+ } catch {
50
+ return value
51
+ }
52
+ }
53
+
54
+ async function load() {
55
+ if (!props.open || !props.slug) return
56
+ const api = getHostApiForApp(props.rootApp)
57
+ if (!api?.appVersions) {
58
+ error.value = props.labels.no_api
59
+ return
60
+ }
61
+
62
+ loading.value = true
63
+ error.value = ''
64
+ rows.value = []
65
+
66
+ try {
67
+ const res = await api.appVersions(props.slug)
68
+ rows.value = res?.data?.data?.versions ?? []
69
+ } catch (err) {
70
+ error.value = parseApiError(err, props.labels.load_error)
71
+ } finally {
72
+ loading.value = false
73
+ }
74
+ }
75
+
76
+ watch(() => [props.open, props.slug], () => load(), { immediate: true })
77
+ </script>
@@ -0,0 +1,206 @@
1
+ <template>
2
+ <section v-if="visible" class="apphub-dev-review">
3
+ <h3 class="apphub-dev-review__title">{{ labels.title }}</h3>
4
+ <p class="apphub-dev-review__hint">{{ labels.hint }}</p>
5
+
6
+ <p v-if="loading" class="apphub-dev-review__msg">{{ labels.loading }}</p>
7
+ <p v-else-if="!draftApps.length" class="apphub-dev-review__msg">{{ labels.empty }}</p>
8
+
9
+ <ul v-else class="apphub-dev-review__list">
10
+ <li v-for="app in draftApps" :key="app.slug" class="apphub-dev-review__item">
11
+ <div class="apphub-dev-review__item-head">
12
+ <span class="apphub-dev-review__icon" aria-hidden="true">{{ app.icon || '📦' }}</span>
13
+ <div class="apphub-dev-review__meta">
14
+ <strong>{{ app.name }}</strong>
15
+ <span class="apphub-dev-review__slug">{{ app.slug }}</span>
16
+ <span v-if="app.version" class="apphub-dev-review__version">v{{ app.version }}</span>
17
+ <span class="apphub-dev-review__badge">{{ runtimeLabel(app) }}</span>
18
+ </div>
19
+ <div class="apphub-dev-review__actions">
20
+ <button
21
+ type="button"
22
+ class="apphub-dev-review__btn"
23
+ :disabled="historySlug === app.slug"
24
+ @click="toggleHistory(app.slug)"
25
+ >
26
+ {{ historySlug === app.slug ? labels.history_loading : labels.history_btn }}
27
+ </button>
28
+ <button
29
+ v-if="app.runtime_type === 'hosted'"
30
+ type="button"
31
+ class="apphub-dev-review__btn"
32
+ :disabled="inspectingSlug === app.slug"
33
+ @click="toggleInspect(app.slug)"
34
+ >
35
+ {{ inspectingSlug === app.slug ? labels.files_loading : labels.files_btn }}
36
+ </button>
37
+ <button
38
+ type="button"
39
+ class="apphub-dev-review__btn apphub-dev-review__btn--primary"
40
+ :disabled="actingSlug === app.slug"
41
+ @click="approve(app.slug)"
42
+ >
43
+ {{ actingSlug === app.slug ? labels.approving : labels.approve }}
44
+ </button>
45
+ </div>
46
+ </div>
47
+
48
+ <dl v-if="app.bundle_hash" class="apphub-dev-review__dl">
49
+ <div class="apphub-dev-review__row">
50
+ <dt>{{ labels.hash }}</dt>
51
+ <dd class="apphub-dev-review__mono">{{ shortHash(app.bundle_hash) }}</dd>
52
+ </div>
53
+ <div v-if="app.bundle_file_count != null" class="apphub-dev-review__row">
54
+ <dt>{{ labels.file_count }}</dt>
55
+ <dd>{{ app.bundle_file_count }}</dd>
56
+ </div>
57
+ </dl>
58
+
59
+ <AppHubAppVersionHistory
60
+ :slug="app.slug"
61
+ :root-app="rootApp"
62
+ :open="historySlug === app.slug"
63
+ :labels="historyLabels"
64
+ />
65
+
66
+ <div v-if="inspect[app.slug]" class="apphub-dev-review__files">
67
+ <p class="apphub-dev-review__files-head">
68
+ {{ labels.files_title }}
69
+ <span v-if="inspect[app.slug].files_truncated"> ({{ labels.files_truncated }})</span>
70
+ </p>
71
+ <ul class="apphub-dev-review__file-list">
72
+ <li v-for="file in inspect[app.slug].files" :key="file" class="apphub-dev-review__file">
73
+ {{ file }}
74
+ </li>
75
+ </ul>
76
+ </div>
77
+ </li>
78
+ </ul>
79
+ </section>
80
+ </template>
81
+
82
+ <script setup>
83
+ import { computed, inject, onMounted, reactive, ref } from 'vue'
84
+ import { getHostApiForApp } from '../../../composables/useAppHubHostApi.js'
85
+ import { t } from '../../../i18n/index.js'
86
+ import { resolveLang } from '../../../i18n/resolveLang.js'
87
+ import AppHubAppVersionHistory from './AppHubAppVersionHistory.vue'
88
+
89
+ const props = defineProps({
90
+ rootApp: { type: Object, default: null },
91
+ devApps: { type: Array, default: () => [] },
92
+ })
93
+
94
+ const emit = defineEmits(['refreshed'])
95
+
96
+ const moduleOptions = inject('apphubOptions', {})
97
+ const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
98
+ const visible = ref(false)
99
+ const loading = ref(false)
100
+ const actingSlug = ref('')
101
+ const inspectingSlug = ref('')
102
+ const historySlug = ref('')
103
+ const inspect = reactive({})
104
+
105
+ const labels = computed(() => ({
106
+ title: t('dev_review_title', lang.value),
107
+ hint: t('dev_review_hint', lang.value),
108
+ loading: t('dev_review_loading', lang.value),
109
+ empty: t('dev_review_empty', lang.value),
110
+ hash: t('dev_review_hash', lang.value),
111
+ file_count: t('dev_review_file_count', lang.value),
112
+ files_btn: t('dev_review_files_btn', lang.value),
113
+ files_loading: t('dev_review_files_loading', lang.value),
114
+ files_title: t('dev_review_files_title', lang.value),
115
+ files_truncated: t('dev_review_files_truncated', lang.value),
116
+ approve: t('dev_review_approve', lang.value),
117
+ approving: t('dev_review_approving', lang.value),
118
+ hosted: t('runner_hosted_badge', lang.value),
119
+ iframe: t('dev_review_iframe', lang.value),
120
+ history_btn: t('dev_review_history_btn', lang.value),
121
+ history_loading: t('dev_review_history_loading', lang.value),
122
+ }))
123
+
124
+ const historyLabels = computed(() => ({
125
+ title: t('dev_review_history_title', lang.value),
126
+ loading: t('dev_review_history_loading', lang.value),
127
+ empty: t('dev_review_history_empty', lang.value),
128
+ current: t('dev_review_history_current', lang.value),
129
+ no_api: t('app_store_no_api', lang.value),
130
+ load_error: t('dev_review_history_error', lang.value),
131
+ }))
132
+
133
+ const draftApps = computed(() =>
134
+ (props.devApps ?? []).filter((app) => app?.status === 'draft'),
135
+ )
136
+
137
+ function runtimeLabel(app) {
138
+ return app?.runtime_type === 'hosted' ? labels.value.hosted : labels.value.iframe
139
+ }
140
+
141
+ function shortHash(hash) {
142
+ if (!hash || typeof hash !== 'string') return '—'
143
+ return hash.length > 16 ? `${hash.slice(0, 12)}…` : hash
144
+ }
145
+
146
+ async function checkDevAccess() {
147
+ const api = getHostApiForApp(props.rootApp)
148
+ if (!api?.bootstrap) {
149
+ visible.value = false
150
+ return
151
+ }
152
+
153
+ loading.value = true
154
+ try {
155
+ const boot = await api.bootstrap()
156
+ visible.value = boot?.data?.data?.is_dev_user === true
157
+ } catch {
158
+ visible.value = false
159
+ } finally {
160
+ loading.value = false
161
+ }
162
+ }
163
+
164
+ function toggleHistory(slug) {
165
+ historySlug.value = historySlug.value === slug ? '' : slug
166
+ }
167
+
168
+ async function toggleInspect(slug) {
169
+ if (inspect[slug]) {
170
+ delete inspect[slug]
171
+ inspectingSlug.value = ''
172
+ return
173
+ }
174
+
175
+ const api = getHostApiForApp(props.rootApp)
176
+ if (!api?.devInspectBundle) return
177
+
178
+ inspectingSlug.value = slug
179
+ try {
180
+ const res = await api.devInspectBundle(slug)
181
+ inspect[slug] = res?.data?.data ?? null
182
+ } catch {
183
+ inspect[slug] = { files: [], file_count: 0 }
184
+ } finally {
185
+ inspectingSlug.value = ''
186
+ }
187
+ }
188
+
189
+ async function approve(slug) {
190
+ const api = getHostApiForApp(props.rootApp)
191
+ if (!api?.devSetAppStatus) return
192
+
193
+ actingSlug.value = slug
194
+ try {
195
+ await api.devSetAppStatus(slug, 'active')
196
+ delete inspect[slug]
197
+ emit('refreshed')
198
+ } catch {
199
+ /* ignore */
200
+ } finally {
201
+ actingSlug.value = ''
202
+ }
203
+ }
204
+
205
+ onMounted(checkDevAccess)
206
+ </script>