Skip to content

Commit 0596319

Browse files
committed
refactor(appstore): migrate app management views to Vue 3 and Typescript
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 071c20a commit 0596319

7 files changed

Lines changed: 434 additions & 16 deletions

File tree

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@ export interface IAppstoreAppRelease {
2727
}
2828
}
2929

30-
export interface IAppstoreApp {
30+
export interface IAppstoreAppData extends Record<string, unknown> {
31+
ratingOverall: number
32+
ratingNumOverall: number
33+
ratingRecent: number
34+
ratingNumRecent: number
35+
36+
releases: IAppstoreAppRelease[]
37+
}
38+
39+
export interface IAppstoreAppResponse {
3140
id: string
3241
name: string
3342
summary: string
@@ -38,10 +47,13 @@ export interface IAppstoreApp {
3847
version: string
3948
category: string | string[]
4049

41-
preview?: string
50+
icon?: string
4251
screenshot?: string
4352

44-
app_api: boolean
53+
score: number
54+
ratingNumThresholdReached: boolean
55+
56+
app_api: false
4557
active: boolean
4658
internal: boolean
4759
removable: boolean
@@ -52,10 +64,14 @@ export interface IAppstoreApp {
5264
needsDownload: boolean
5365
update?: string
5466

55-
appstoreData: Record<string, never>
67+
appstoreData?: IAppstoreAppData
5668
releases?: IAppstoreAppRelease[]
5769
}
5870

71+
export interface IAppstoreApp extends IAppstoreAppResponse {
72+
loading?: boolean
73+
}
74+
5975
export interface IComputeDevice {
6076
id: string
6177
label: string
@@ -81,10 +97,10 @@ export interface IDeployDaemon {
8197
export interface IExAppStatus {
8298
action: string
8399
deploy: number
84-
deploy_start_time: number
85-
error: string
100+
deploy_start_time?: number
101+
error?: string
86102
init: number
87-
init_start_time: number
103+
init_start_time?: number
88104
type: string
89105
}
90106

@@ -111,8 +127,9 @@ export interface IAppstoreExAppRelease extends IAppstoreAppRelease {
111127
}
112128

113129
export interface IAppstoreExApp extends IAppstoreApp {
130+
app_api: true
114131
daemon: IDeployDaemon | null | undefined
115132
status: IExAppStatus | Record<string, never>
116-
error: string
133+
error?: string
117134
releases: IAppstoreExAppRelease[]
118135
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
8+
9+
import { mdiCogOutline } from '@mdi/js'
10+
import { computed, ref, watch } from 'vue'
11+
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
12+
13+
const { app, noFallback, size = 20 } = defineProps<{
14+
app: IAppstoreApp | IAppstoreExApp
15+
noFallback?: boolean
16+
size?: number
17+
}>()
18+
19+
const isSvg = computed(() => app.icon?.endsWith('.svg'))
20+
const svgIcon = ref<string>('')
21+
watch(() => app.icon, async () => {
22+
svgIcon.value = ''
23+
if (app.icon?.endsWith('.svg')) {
24+
const response = await fetch(app.icon)
25+
if (response.ok) {
26+
svgIcon.value = await response.text()
27+
}
28+
}
29+
}, { immediate: true })
30+
</script>
31+
32+
<template>
33+
<span :class="$style.appIcon">
34+
<NcIconSvgWrapper
35+
v-if="svgIcon"
36+
:size
37+
:svg="svgIcon" />
38+
<img
39+
v-else-if="app.icon && !isSvg"
40+
:class="$style.appIcon__image"
41+
alt=""
42+
:src="app.icon"
43+
:height="size"
44+
:width="size">
45+
<NcIconSvgWrapper
46+
v-else-if="!noFallback"
47+
:path="mdiCogOutline"
48+
:size />
49+
</span>
50+
</template>
51+
52+
<style module>
53+
.appIcon {
54+
display: inline-flex;
55+
justify-content: center;
56+
}
57+
58+
.appImage__image {
59+
filter: var(--invert-if-dark);
60+
object-fit: cover;
61+
height: 100%;
62+
width: 100%;
63+
}
64+
</style>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.ts';
8+
9+
import { t } from '@nextcloud/l10n'
10+
import AppTableRow from './AppTableRow.vue';
11+
import { computed, useTemplateRef } from 'vue';
12+
import { useElementSize } from '@vueuse/core';
13+
14+
defineProps<{
15+
apps: (IAppstoreApp | IAppstoreExApp)[]
16+
}>()
17+
18+
const tableElement = useTemplateRef('table')
19+
const { width: tableWidth } = useElementSize(tableElement)
20+
21+
const isNarrow = computed(() => tableWidth.value < 768)
22+
</script>
23+
24+
<template>
25+
<table ref="table" :class="$style.appTable">
26+
<thead hidden>
27+
<tr>
28+
<th>{{ t('appstore', 'App name') }}</th>
29+
<th>{{ t('appstore', 'Version') }}</th>
30+
<th v-if="!isNarrow">{{ t('appstore', 'Support level') }}</th>
31+
<th>{{ t('appstore', 'Actions') }}</th>
32+
</tr>
33+
</thead>
34+
<tbody>
35+
<AppTableRow
36+
v-for="app in apps"
37+
:key="app.id"
38+
:app
39+
:isNarrow />
40+
</tbody>
41+
</table>
42+
</template>
43+
44+
<style module>
45+
.appTable {
46+
width: 100%;
47+
}
48+
</style>
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.ts'
8+
9+
import { mdiInformationOutline } from '@mdi/js'
10+
import { t } from '@nextcloud/l10n'
11+
import { useRoute } from 'vue-router'
12+
import { computed } from 'vue'
13+
import NcActionButton from '@nextcloud/vue/components/NcActionButton'
14+
import NcActionRouter from '@nextcloud/vue/components/NcActionRouter'
15+
import NcActions from '@nextcloud/vue/components/NcActions'
16+
import NcButton from '@nextcloud/vue/components/NcButton'
17+
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
18+
import AppIcon from './AppIcon.vue'
19+
import AppLevelBadge from './AppLevelBadge.vue'
20+
import AppDaemonBadge from './AppDaemonBadge.vue'
21+
import { useActions } from '../composables/useActions.ts'
22+
23+
const { app, isNarrow } = defineProps<{
24+
app: IAppstoreApp | IAppstoreExApp,
25+
isNarrow?: boolean
26+
}>()
27+
28+
const actions = useActions(() => app)
29+
const inlineActions = computed(() => !isNarrow || actions.value.length === 1
30+
? actions.value.slice(0, 1)
31+
: [])
32+
const menuActions = computed(() => actions.value.slice(inlineActions.value.length))
33+
34+
const route = useRoute()
35+
const detailsRoute = computed(() => ({
36+
name: route.name!,
37+
params: {
38+
...route.params,
39+
id: app.id,
40+
},
41+
}))
42+
</script>
43+
44+
<template>
45+
<tr :class="$style.appTableRow">
46+
<td>
47+
<NcButton
48+
alignment="start"
49+
:title="t('appstore', 'Show details')"
50+
:to="detailsRoute"
51+
variant="tertiary-no-background"
52+
wide>
53+
<template #icon>
54+
<AppIcon :app :size="24" />
55+
</template>
56+
{{ app.name }}
57+
<span class="hidden-visually">({{ t('appstore', 'Show details') }})</span>
58+
</NcButton>
59+
</td>
60+
<td>
61+
<span :class="$style.appTableRow__versionCell">{{ app.version }}</span>
62+
</td>
63+
<td v-if="!isNarrow">
64+
<div :class="$style.appTableRow__levelCell">
65+
<AppLevelBadge v-if="app.level" :level="app.level" />
66+
<AppDaemonBadge v-if="'daemon' in app && app.daemon" :daemon="app.daemon" />
67+
</div>
68+
</td>
69+
<td>
70+
<div :class="$style.appTableRow__actionsCell">
71+
<NcButton v-for="action in inlineActions"
72+
:key="action.id"
73+
:variant="action.variant"
74+
@click="action.callback(app)">
75+
{{ action.label(app) }}
76+
</NcButton>
77+
<NcActions force-menu>
78+
<NcActionButton
79+
v-for="action in menuActions"
80+
:key="action.id"
81+
closeAfterClick
82+
@click="action.callback(app)">
83+
<template #icon>
84+
<NcIconSvgWrapper :path="action.icon" />
85+
</template>
86+
{{ action.label(app) }}
87+
</NcActionButton>
88+
<NcActionRouter closeAfterClick :to="detailsRoute">
89+
<template #icon>
90+
<NcIconSvgWrapper :path="mdiInformationOutline" />
91+
</template>
92+
{{ t('appstore', 'Show details') }}
93+
</NcActionRouter>
94+
</NcActions>
95+
</div>
96+
</td>
97+
</tr>
98+
</template>
99+
100+
<style module>
101+
.appTableRow {
102+
height: calc(var(--default-clickable-area) + var(--default-grid-baseline));
103+
}
104+
105+
.appTableRow td {
106+
padding-block: calc(var(--default-grid-baseline) / 2);
107+
vertical-align: middle;
108+
}
109+
110+
.appTableRow__nameCell {
111+
display: flex;
112+
align-items: center;
113+
gap: var(--default-grid-baseline)
114+
}
115+
116+
.appTableRow__levelCell {
117+
display: flex;
118+
align-items: center;
119+
gap: var(--default-grid-baseline)
120+
}
121+
122+
.appTableRow__versionCell {
123+
color: var(--color-text-maxcontrast);
124+
}
125+
126+
.appTableRow__actionsCell {
127+
display: flex;
128+
gap: var(--default-grid-baseline);
129+
justify-content: end;
130+
}
131+
</style>

0 commit comments

Comments
 (0)