Skip to content

Commit 4843981

Browse files
committed
refactor(appstore): migrate apps manage pages to Vue 3 and Typescript
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent fc3a4cf commit 4843981

File tree

14 files changed

+450
-126
lines changed

14 files changed

+450
-126
lines changed

apps/appstore/lib/Controller/ApiController.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,16 @@ public function listApps(): DataResponse {
112112
}
113113
}
114114
$appData['groups'] = $groups;
115-
$appData['canUnInstall'] = !$appData['active'] && $appData['removable'];
115+
$appData['canUninstall'] = !$appData['active'] && $appData['removable'];
116116

117117
// analyze dependencies
118118
$ignoreMax = in_array($appData['id'], $ignoreMaxApps);
119119
$missing = $this->dependencyAnalyzer->analyze($appData, $ignoreMax);
120120
$appData['canInstall'] = empty($missing);
121121
$appData['missingDependencies'] = $missing;
122122

123-
$appData['missingMinOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
124-
$appData['missingMaxOwnCloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
123+
$appData['missingMinNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
124+
$appData['missingMaxNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
125125
$appData['isCompatible'] = $this->dependencyAnalyzer->isMarkedCompatible($appData);
126126

127127
return $appData;
@@ -329,13 +329,13 @@ private function getAppsForCategory($requestedCategory = ''): array {
329329
if (!isset($app['releases'][0]['rawPlatformVersionSpec'])) {
330330
continue;
331331
}
332-
$nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
333-
$nextCloudVersionDependencies = [];
334-
if ($nextCloudVersion->getMinimumVersion() !== '') {
335-
$nextCloudVersionDependencies['nextcloud']['@attributes']['min-version'] = $nextCloudVersion->getMinimumVersion();
332+
$nextcloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']);
333+
$nextcloudVersionDependencies = [];
334+
if ($nextcloudVersion->getMinimumVersion() !== '') {
335+
$nextcloudVersionDependencies['nextcloud']['@attributes']['min-version'] = $nextcloudVersion->getMinimumVersion();
336336
}
337-
if ($nextCloudVersion->getMaximumVersion() !== '') {
338-
$nextCloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion();
337+
if ($nextcloudVersion->getMaximumVersion() !== '') {
338+
$nextcloudVersionDependencies['nextcloud']['@attributes']['max-version'] = $nextcloudVersion->getMaximumVersion();
339339
}
340340
$phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']);
341341

@@ -397,12 +397,12 @@ private function getAppsForCategory($requestedCategory = ''): array {
397397
'website' => $app['website'],
398398
'bugs' => $app['issueTracker'],
399399
'dependencies' => array_merge(
400-
$nextCloudVersionDependencies,
400+
$nextcloudVersionDependencies,
401401
$phpDependencies
402402
),
403403
'level' => ($app['isFeatured'] === true) ? 200 : 100,
404-
'missingMaxOwnCloudVersion' => false,
405-
'missingMinOwnCloudVersion' => false,
404+
'missingMaxNextcloudVersion' => false,
405+
'missingMinNextcloudVersion' => false,
406406
'canInstall' => true,
407407
'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($app['screenshots'][0]['url']) : '',
408408
'score' => $app['ratingOverall'],

apps/appstore/src/AppstoreApp.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import AppstoreSidebar from './views/AppstoreSidebar.vue'
1414
import { APPSTORE_CATEGORY_NAMES } from './constants.ts'
1515
1616
const route = useRoute()
17-
const heading = computed(() => route.params.category
18-
&& (APPSTORE_CATEGORY_NAMES[route.params.category as string]
19-
?? route.params.category))
17+
18+
const currentCategory = computed(() => [route.params.category].flat()[0] ?? 'discover')
19+
const heading = computed(() => APPSTORE_CATEGORY_NAMES[currentCategory.value] ?? currentCategory.value)
2020
const pageTitle = computed(() => `${heading.value} - ${t('appstore', 'App store')}`)
2121
2222
const showSidebar = computed(() => !!route.params.id)
@@ -26,6 +26,7 @@ const showSidebar = computed(() => !!route.params.id)
2626
<NcContent app-name="appstore">
2727
<AppstoreNavigation />
2828
<NcAppContent
29+
:class="$style.appstoreApp__content"
2930
:page-heading="t('appstore', 'App store')"
3031
:page-title>
3132
<h2 v-if="heading" :class="$style.appstoreApp__heading">
@@ -38,6 +39,10 @@ const showSidebar = computed(() => !!route.params.id)
3839
</template>
3940

4041
<style module>
42+
.appstoreApp__content {
43+
padding-inline-end: var(--body-container-margin);
44+
}
45+
4146
.appstoreApp__heading {
4247
margin-block-start: var(--app-navigation-padding);
4348
margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);

apps/appstore/src/apps.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface IAppstoreAppResponse {
4747
version: string
4848
category: string | string[]
4949

50+
icon?: string
5051
screenshot?: string
5152

5253
score: number
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)