Skip to content

Commit 387b40b

Browse files
authored
Merge pull request #57290 from nextcloud/refactor/split-appstore
refactor(appstore): migrate to Typescript and Vue 3
2 parents e622901 + 7d8e640 commit 387b40b

629 files changed

Lines changed: 6060 additions & 6349 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/appstore/lib/Controller/ApiController.php

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,13 @@ public function listCategories(): DataResponse {
8282
/**
8383
* Get all available apps
8484
*
85+
* @param bool $details - Whether to include detailed appstore information about the app
8586
* @return DataResponse<Http::STATUS_OK, list<array{id: string, name: string, groups: list<string>, internal: bool, isCompatible: bool, missingDependencies?: list<string>, missingMaxNextcloudVersion: bool, missingMinNextcloudVersion: bool, ...<array-key, mixed>}>, array{}>
8687
*
8788
* 200: The apps were found successfully
8889
*/
8990
#[ApiRoute(verb: 'GET', url: '/api/v1/apps')]
90-
public function listApps(): DataResponse {
91+
public function listApps(bool $details = false): DataResponse {
9192
$apps = $this->getAllApps();
9293

9394
/** @var array<string>|mixed $ignoreMaxApps */
@@ -98,12 +99,16 @@ public function listApps(): DataResponse {
9899
}
99100

100101
// Extend existing app details
101-
$apps = array_map(function (array $appData) use ($ignoreMaxApps): array {
102+
$apps = array_map(function (array $appData) use ($ignoreMaxApps, $details): array {
102103
if (isset($appData['appstoreData'])) {
103104
$appstoreData = $appData['appstoreData'];
104105
$appData['screenshot'] = $this->createProxyPreviewUrl($appstoreData['screenshots'][0]['url'] ?? '');
105106
$appData['category'] = $appstoreData['categories'];
106107
$appData['releases'] = $appstoreData['releases'];
108+
109+
if (!$details) {
110+
unset($appData['appstoreData']);
111+
}
107112
}
108113

109114
$newVersion = $this->installer->isUpdateAvailable($appData['id']);
@@ -123,17 +128,15 @@ public function listApps(): DataResponse {
123128
}
124129

125130
$appData['groups'] = $groups;
126-
$appData['canUninstall'] = !$appData['active'] && $appData['removable'];
127-
128131
// analyze dependencies
129132
$ignoreMax = in_array($appData['id'], $ignoreMaxApps);
130133
$missing = $this->dependencyAnalyzer->analyze($appData, $ignoreMax);
131-
$appData['canInstall'] = empty($missing);
132134
$appData['missingDependencies'] = $missing;
133135

134136
$appData['missingMinNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['min-version']);
135137
$appData['missingMaxNextcloudVersion'] = !isset($appData['dependencies']['nextcloud']['@attributes']['max-version']);
136138
$appData['isCompatible'] = $this->dependencyAnalyzer->isMarkedCompatible($appData);
139+
$appData['internal'] = in_array($appData['id'], $this->appManager->getAlwaysEnabledApps());
137140

138141
return $appData;
139142
}, $apps);
@@ -204,6 +207,7 @@ public function enableApp(string $appId, array $groups = [], bool $force = false
204207
public function disableApp(string $appId): DataResponse {
205208
try {
206209
$appId = $this->appManager->cleanAppId($appId);
210+
$this->appManager->removeOverwriteNextcloudRequirement($appId);
207211
$this->appManager->disableApp($appId);
208212
return new DataResponse([]);
209213
} catch (\Exception $exception) {
@@ -214,7 +218,6 @@ public function disableApp(string $appId): DataResponse {
214218

215219
/**
216220
* Uninstall an app.
217-
* This will disable the app - if needed - and then remove the app from the system
218221
*
219222
* @param string $appId - The app to uninstall
220223
* @return DataResponse<Http::STATUS_OK, array{}, array{}>
@@ -226,6 +229,10 @@ public function disableApp(string $appId): DataResponse {
226229
#[ApiRoute(verb: 'POST', url: '/api/v1/apps/uninstall')]
227230
public function uninstallApp(string $appId): DataResponse {
228231
$appId = $this->appManager->cleanAppId($appId);
232+
if ($this->appManager->isEnabledForAnyone($appId)) {
233+
$this->disableApp($appId);
234+
}
235+
229236
$result = $this->installer->removeApp($appId);
230237
if ($result !== false) {
231238
// If this app was force enabled, remove the force-enabled-state
@@ -452,6 +459,7 @@ private function getAppsForCategory(string $requestedCategory = ''): array {
452459
'license' => $app['releases'][0]['licenses'],
453460
'author' => $authors,
454461
'shipped' => $this->appManager->isShipped($app['id']),
462+
'internal' => in_array($app['id'], $this->appManager->getAlwaysEnabledApps()),
455463
'version' => $currentVersion,
456464
'types' => [],
457465
'documentation' => [
@@ -468,11 +476,9 @@ private function getAppsForCategory(string $requestedCategory = ''): array {
468476
'level' => ($app['isFeatured'] === true) ? 200 : 100,
469477
'missingMaxNextcloudVersion' => false,
470478
'missingMinNextcloudVersion' => false,
471-
'canInstall' => true,
472479
'screenshot' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/' . base64_encode($app['screenshots'][0]['url']) : '',
473-
'score' => $app['ratingOverall'],
480+
'ratingOverall' => $app['ratingOverall'],
474481
'ratingNumOverall' => $app['ratingNumOverall'],
475-
'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5,
476482
'removable' => $existsLocally,
477483
'active' => $this->appManager->isEnabledForUser($app['id']),
478484
'needsDownload' => !$existsLocally,

apps/appstore/openapi.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,15 @@
197197
}
198198
],
199199
"parameters": [
200+
{
201+
"name": "details",
202+
"in": "query",
203+
"description": "- Whether to include detailed appstore information about the app",
204+
"schema": {
205+
"type": "boolean",
206+
"default": false
207+
}
208+
},
200209
{
201210
"name": "OCS-APIRequest",
202211
"in": "header",
@@ -640,7 +649,7 @@
640649
"/ocs/v2.php/apps/appstore/api/v1/apps/uninstall": {
641650
"post": {
642651
"operationId": "api-uninstall-app",
643-
"summary": "Uninstall an app. This will disable the app - if needed - and then remove the app from the system",
652+
"summary": "Uninstall an app.",
644653
"description": "This endpoint requires admin access\nThis endpoint requires password confirmation",
645654
"tags": [
646655
"api"

apps/appstore/src/AppstoreApp.vue

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { t } from '@nextcloud/l10n'
8+
import { computed } from 'vue'
9+
import { useRoute } from 'vue-router'
10+
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
11+
import NcContent from '@nextcloud/vue/components/NcContent'
12+
import AppstoreNavigation from './views/AppstoreNavigation.vue'
13+
import AppstoreSidebar from './views/AppstoreSidebar.vue'
14+
import { APPSTORE_CATEGORY_NAMES } from './constants.ts'
15+
import { useAppsStore } from './store/apps.ts'
16+
17+
const route = useRoute()
18+
const store = useAppsStore()
19+
20+
const currentCategory = computed(() => {
21+
if (route.params.category) {
22+
return [route.params.category].flat()[0]!
23+
}
24+
if (route.name === 'apps-bundles') {
25+
return 'bundles'
26+
} else if (route.name === 'apps-search') {
27+
return 'search'
28+
}
29+
return 'discover'
30+
})
31+
32+
const heading = computed(() => {
33+
if (currentCategory.value in APPSTORE_CATEGORY_NAMES) {
34+
return APPSTORE_CATEGORY_NAMES[currentCategory.value]
35+
}
36+
return store.getCategoryById(currentCategory.value)?.displayName ?? currentCategory.value
37+
})
38+
const pageTitle = computed(() => `${heading.value} - ${t('appstore', 'App store')}`)
39+
40+
const showSidebar = computed(() => !!route.params.id)
41+
</script>
42+
43+
<template>
44+
<NcContent appName="appstore">
45+
<AppstoreNavigation />
46+
<NcAppContent
47+
:class="$style.appstoreApp__content"
48+
:pageHeading="t('appstore', 'App store')"
49+
:pageTitle>
50+
<h2 v-if="heading" :class="$style.appstoreApp__heading">
51+
{{ heading }}
52+
</h2>
53+
<router-view />
54+
</NcAppContent>
55+
<AppstoreSidebar v-if="showSidebar" />
56+
</NcContent>
57+
</template>
58+
59+
<style module>
60+
.appstoreApp__content {
61+
padding-inline-end: var(--body-container-margin);
62+
position: relative;
63+
}
64+
65+
.appstoreApp__heading {
66+
margin-block-start: var(--app-navigation-padding);
67+
margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);
68+
min-height: var(--default-clickable-area);
69+
line-height: var(--default-clickable-area);
70+
vertical-align: center;
71+
}
72+
</style>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
7+
import type { AppAction } from './index.ts'
8+
9+
import { mdiClose } from '@mdi/js'
10+
import { t } from '@nextcloud/l10n'
11+
import { useAppsStore } from '../store/apps.ts'
12+
import { canDisable } from '../utils/appStatus.ts'
13+
14+
export const actionDisable: AppAction = {
15+
id: 'disable',
16+
icon: mdiClose,
17+
order: 10,
18+
enabled: canDisable,
19+
label: () => t('appstore', 'Disable'),
20+
async callback(app: IAppstoreApp | IAppstoreExApp) {
21+
const store = useAppsStore()
22+
await store.disableApp(app.id)
23+
},
24+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
7+
import type { AppAction } from './index.ts'
8+
9+
import { mdiCheck } from '@mdi/js'
10+
import { t } from '@nextcloud/l10n'
11+
import { useAppsStore } from '../store/apps.ts'
12+
import { canEnable, canInstall } from '../utils/appStatus.ts'
13+
14+
export const actionEnable: AppAction = {
15+
id: 'enable',
16+
icon: mdiCheck,
17+
order: 1,
18+
variant: 'primary',
19+
enabled(app: IAppstoreApp | IAppstoreExApp) {
20+
return !canInstall(app) && canEnable(app)
21+
},
22+
label: () => t('appstore', 'Enable'),
23+
async callback(app: IAppstoreApp | IAppstoreExApp) {
24+
const store = useAppsStore()
25+
await store.enableApp(app.id)
26+
},
27+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
7+
import type { AppAction } from './index.ts'
8+
9+
import { mdiAlertCircleCheckOutline } from '@mdi/js'
10+
import { t } from '@nextcloud/l10n'
11+
import { useAppsStore } from '../store/apps.ts'
12+
import { canForceEnable, canInstall, needForceEnable } from '../utils/appStatus.ts'
13+
14+
export const actionForceEnable: AppAction = {
15+
id: 'force-enable',
16+
icon: mdiAlertCircleCheckOutline,
17+
order: 3,
18+
inline: false,
19+
variant: 'warning',
20+
label: () => t('appstore', 'Force enable'),
21+
enabled(app: IAppstoreApp | IAppstoreExApp) {
22+
return !canInstall(app) && canForceEnable(app) && needForceEnable(app)
23+
},
24+
async callback(app: IAppstoreApp | IAppstoreExApp) {
25+
const store = useAppsStore()
26+
await store.forceEnableApp(app.id)
27+
},
28+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
7+
import type { AppAction } from './index.ts'
8+
9+
import { mdiDownload } from '@mdi/js'
10+
import { t } from '@nextcloud/l10n'
11+
import { useAppsStore } from '../store/apps.ts'
12+
import { canInstall, needForceEnable } from '../utils/appStatus.ts'
13+
14+
export const actionInstall: AppAction = {
15+
id: 'install',
16+
icon: mdiDownload,
17+
order: 5,
18+
enabled(app) {
19+
return canInstall(app) && !needForceEnable(app)
20+
},
21+
label: (app: IAppstoreApp | IAppstoreExApp) => {
22+
if (app.app_api) {
23+
return t('appstore', 'Deploy and enable')
24+
}
25+
if (app.needsDownload) {
26+
return t('appstore', 'Download and enable')
27+
}
28+
return t('appstore', 'Install and enable')
29+
},
30+
async callback(app: IAppstoreApp | IAppstoreExApp) {
31+
const store = useAppsStore()
32+
await store.enableApp(app.id)
33+
},
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*!
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
7+
import type { AppAction } from './index.ts'
8+
9+
import { mdiDownload } from '@mdi/js'
10+
import { t } from '@nextcloud/l10n'
11+
import { useAppsStore } from '../store/apps.ts'
12+
import { canInstall, needForceEnable } from '../utils/appStatus.ts'
13+
14+
export const actionInstallForced: AppAction = {
15+
id: 'install-forced',
16+
icon: mdiDownload,
17+
order: 5,
18+
inline: false,
19+
enabled(app) {
20+
return canInstall(app) && needForceEnable(app)
21+
},
22+
label: (app: IAppstoreApp | IAppstoreExApp) => {
23+
if (app.app_api) {
24+
return t('appstore', 'Deploy and force enable')
25+
}
26+
if (app.needsDownload) {
27+
return t('appstore', 'Download and force enable')
28+
}
29+
return t('appstore', 'Install and force enable')
30+
},
31+
async callback(app: IAppstoreApp | IAppstoreExApp) {
32+
const store = useAppsStore()
33+
await store.enableApp(app.id, true)
34+
},
35+
}

0 commit comments

Comments
 (0)