Skip to content

Commit a5cacf3

Browse files
committed
fix(appstore): bring back "update all" button
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 782f3f7 commit a5cacf3

3 files changed

Lines changed: 200 additions & 2 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<!--
2+
SPDX-License-Identifier: AGPL-3.0-or-later
3+
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
8+
9+
import { mdiCheck, mdiInformationOutline, mdiUpdate, mdiWeb } from '@mdi/js'
10+
import { showError } from '@nextcloud/dialogs'
11+
import { getLanguage, t } from '@nextcloud/l10n'
12+
import { NcButton, NcDialog, NcIconSvgWrapper, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
13+
import { computed, ref } from 'vue'
14+
import MarkdownPreview from './MarkdownPreview.vue'
15+
import { useUpdatesStore } from '../store/updates.ts'
16+
import logger from '../utils/logger.ts'
17+
18+
const props = defineProps<{
19+
apps: (IAppstoreApp | IAppstoreExApp)[]
20+
}>()
21+
22+
const emit = defineEmits<{
23+
close: []
24+
}>()
25+
26+
const store = useUpdatesStore()
27+
const showDetails = ref('')
28+
const isUpdating = ref(false)
29+
30+
const changelogText = computed(() => {
31+
if (!showDetails.value) {
32+
return ''
33+
}
34+
35+
const app = props.apps.find((app) => app.id === showDetails.value)
36+
if (!app || !app.releases || app.releases.length === 0) {
37+
return ''
38+
}
39+
40+
const [release] = app.releases
41+
const localizedEntry = release.translations[getLanguage()]
42+
return localizedEntry?.changelog ?? release.translations.en?.changelog ?? ''
43+
})
44+
45+
/**
46+
* Handle update all apps
47+
*/
48+
async function onUpdate() {
49+
isUpdating.value = true
50+
for (const app of props.apps) {
51+
try {
52+
await store.updateApp(app.id)
53+
} catch (error) {
54+
logger.error(`Failed to update app ${app.id}`, { error })
55+
showError(t('appstore', 'Failed to update app {appName}', { appName: app.name }))
56+
}
57+
}
58+
isUpdating.value = false
59+
emit('close')
60+
}
61+
</script>
62+
63+
<template>
64+
<NcDialog :contentClasses="$style.updateAllDialog" size="normal" :name="t('appstore', 'Update all apps')">
65+
<p>{{ t('appstore', 'Are you sure you want to update all apps?') }}</p>
66+
<ul>
67+
<li v-for="app in apps" :key="app.id" :class="$style.updateAllDialog__listEntry">
68+
<div :class="$style.updateAllDialog__listEntryContent">
69+
<div :class="$style.updateAllDialog__listEntryHeading">
70+
<NcIconSvgWrapper
71+
:path="app.update ? mdiUpdate : mdiCheck"
72+
:name="app.update ? undefined : t('appstore', 'Update done')" />
73+
<span :class="$style.updateAllDialog__listEntryName">{{ app.name }} ({{ app.version }} → {{ app.update }})</span>
74+
</div>
75+
<div :class="$style.updateAllDialog__listEntryActions">
76+
<NcButton
77+
v-if="app.website"
78+
:aria-label="t('appstore', 'View website')"
79+
:title="t('appstore', 'View website')"
80+
:href="app.website"
81+
target="_blank"
82+
variant="tertiary">
83+
<template #icon>
84+
<NcIconSvgWrapper :path="mdiWeb" />
85+
</template>
86+
</NcButton>
87+
<NcButton
88+
v-if="app.releases"
89+
:aria-label="t('appstore', 'Show details')"
90+
:title="t('appstore', 'Show details')"
91+
:pressed="showDetails === app.id"
92+
@update:pressed="showDetails = $event ? app.id : ''">
93+
<template #icon>
94+
<NcIconSvgWrapper :path="mdiInformationOutline" />
95+
</template>
96+
</NcButton>
97+
</div>
98+
</div>
99+
</li>
100+
</ul>
101+
102+
<NcNoteCard
103+
:class="$style.updateAllDialog__listEntryDetails"
104+
:heading="t('appstore', 'Details')"
105+
type="info">
106+
<MarkdownPreview
107+
:minHeadingLevel="3"
108+
:text="changelogText" />
109+
</NcNoteCard>
110+
111+
<template #actions>
112+
<NcButton variant="tertiary" @click="emit('close')">
113+
{{ t('appstore', 'Cancel') }}
114+
</NcButton>
115+
<NcButton variant="primary" @click="onUpdate">
116+
<template v-if="isUpdating" #icon>
117+
<NcLoadingIcon />
118+
</template>
119+
{{ t('appstore', 'Update all') }}
120+
</NcButton>
121+
</template>
122+
</NcDialog>
123+
</template>
124+
125+
<style module>
126+
.updateAllDialog {
127+
min-height: 50vh !important;
128+
}
129+
130+
.updateAllDialog__list {
131+
display: flex;
132+
flex-direction: row;
133+
gap: calc(3 * var(--default-grid-baseline));
134+
}
135+
136+
.updateAllDialog__listEntry {
137+
display: flex;
138+
flex-direction: column;
139+
gap: calc(2 * var(--default-grid-baseline));
140+
padding: calc(2 * var(--default-grid-baseline));
141+
}
142+
143+
.updateAllDialog__listEntryHeading {
144+
display: flex;
145+
}
146+
147+
.updateAllDialog__listEntryName {
148+
font-weight: 500;
149+
line-height: var(--default-clickable-area);
150+
}
151+
152+
.updateAllDialog__listEntryActions {
153+
display: flex;
154+
flex-direction: row;
155+
gap: var(--default-grid-baseline);
156+
}
157+
158+
.updateAllDialog__listEntryContent {
159+
display: flex;
160+
justify-content: space-between;
161+
}
162+
163+
.updateAllDialog__listEntryDetails {
164+
margin: 0;
165+
}
166+
</style>

apps/appstore/src/store/updates.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const useUpdatesStore = defineStore('updates', () => {
4949
internalUpdateCount.value = Math.max(internalUpdateCount.value - 1, 0)
5050
}
5151

52+
app.update = undefined
5253
rebuildNavigation()
5354
} catch (error) {
5455
logger.error('Failed to update app', { appId, error })

apps/appstore/src/views/AppstoreManage.vue

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
-->
55

66
<script setup lang="ts">
7+
import { mdiUpdate } from '@mdi/js'
78
import { t } from '@nextcloud/l10n'
8-
import { computed } from 'vue'
9+
import { NcIconSvgWrapper, spawnDialog } from '@nextcloud/vue'
10+
import { computed, defineAsyncComponent } from 'vue'
911
import { useRoute } from 'vue-router'
1012
import NcButton from '@nextcloud/vue/components/NcButton'
1113
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
@@ -15,10 +17,14 @@ import AppTable from '../components/AppTable/AppTable.vue'
1517
import AppToolbar from '../components/AppToolbar.vue'
1618
import { useFilteredApps } from '../composables/useFilteredApps.ts'
1719
import { useAppsStore } from '../store/apps.ts'
20+
import { useUpdatesStore } from '../store/updates.ts'
1821
import { useUserSettingsStore } from '../store/userSettings.ts'
1922
23+
const UpdateAllDialog = defineAsyncComponent(() => import('../components/UpdateAllDialog.vue'))
24+
2025
const route = useRoute()
2126
const store = useAppsStore()
27+
const updatesStore = useUpdatesStore()
2228
const userSettings = useUserSettingsStore()
2329
2430
const currentCategory = computed(() => route.params!.category as 'enabled' | 'installed' | 'disabled' | 'updates')
@@ -30,16 +36,36 @@ const apps = computed(() => {
3036
} else if (currentCategory.value === 'disabled') {
3137
return store.apps.filter((app) => app.installed && !app.active)
3238
} else if (currentCategory.value === 'updates') {
33-
return store.apps.filter((app) => app.update)
39+
return store.apps.filter((app) => app.active && app.update)
3440
}
3541
return []
3642
})
3743
const visibleApps = useFilteredApps(apps)
44+
45+
/**
46+
* Handle update all apps
47+
*/
48+
async function onUpdateAll() {
49+
await spawnDialog(UpdateAllDialog, {
50+
apps: visibleApps.value,
51+
})
52+
}
3853
</script>
3954

4055
<template>
4156
<AppToolbar />
4257

58+
<NcButton
59+
v-if="currentCategory === 'updates' && updatesStore.updateCount > 0"
60+
:class="$style.appstoreManage__updateAllButton"
61+
variant="primary"
62+
@click="onUpdateAll">
63+
<template #icon>
64+
<NcIconSvgWrapper :path="mdiUpdate" />
65+
</template>
66+
{{ t('appstore', 'Update all applications') }}
67+
</NcButton>
68+
4369
<!-- Apps list -->
4470
<NcEmptyContent
4571
v-if="store.isLoadingApps"
@@ -69,4 +95,9 @@ const visibleApps = useFilteredApps(apps)
6995
.appstoreManage {
7096
margin-bottom: var(--body-container-margin);
7197
}
98+
99+
.appstoreManage__updateAllButton {
100+
margin-inline: var(--app-navigation-padding);
101+
margin-block: calc(3 * var(--default-grid-baseline));
102+
}
72103
</style>

0 commit comments

Comments
 (0)