Skip to content

Commit 0adabc5

Browse files
committed
refactor: migrate app bundles to Vue 3
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 9097822 commit 0adabc5

10 files changed

Lines changed: 236 additions & 31 deletions

File tree

apps/appstore/src/AppstoreApp.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@ import { APPSTORE_CATEGORY_NAMES } from './constants.ts'
1515
1616
const route = useRoute()
1717
18-
const currentCategory = computed(() => [route.params.category].flat()[0] ?? 'discover')
18+
const currentCategory = computed(() => {
19+
if (route.params.category) {
20+
return [route.params.category].flat()[0]!
21+
}
22+
if (route.name === 'apps-bundles') {
23+
return 'bundles'
24+
}
25+
return 'discover'
26+
})
1927
const heading = computed(() => APPSTORE_CATEGORY_NAMES[currentCategory.value] ?? currentCategory.value)
2028
const pageTitle = computed(() => `${heading.value} - ${t('appstore', 'App store')}`)
2129

apps/appstore/src/components/AppTable/AppTableRow.vue

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ const { app, isNarrow } = defineProps<{
2727
}>()
2828
2929
const actions = useActions(() => app)
30-
const inlineActions = computed(() => !isNarrow || actions.value.length === 1
31-
? actions.value.slice(0, 1)
32-
: [])
30+
const inlineActions = computed(() => {
31+
if (actions.value.length === 1) {
32+
return [...actions.value]
33+
}
34+
if (isNarrow) {
35+
return []
36+
}
37+
return actions.value.slice(0, 1)
38+
.filter((action) => action.inline !== false)
39+
})
3340
const menuActions = computed(() => actions.value.slice(inlineActions.value.length))
3441
3542
const route = useRoute()
@@ -74,15 +81,23 @@ const detailsRoute = computed(() => ({
7481
<NcButton
7582
v-for="action in inlineActions"
7683
:key="action.id"
84+
:ariaLabel="isNarrow ? action.label(app) : undefined"
85+
:title="isNarrow ? action.label(app) : undefined"
7786
:variant="action.variant"
7887
@click="action.callback(app)">
79-
{{ action.label(app) }}
88+
<template #icon>
89+
<NcIconSvgWrapper :path="action.icon" />
90+
</template>
91+
<template v-if="!isNarrow" #default>
92+
{{ action.label(app) }}
93+
</template>
8094
</NcButton>
8195
<NcActions forceMenu>
8296
<NcActionButton
8397
v-for="action in menuActions"
8498
:key="action.id"
8599
closeAfterClick
100+
:variant="action.variant"
86101
@click="action.callback(app)">
87102
<template #icon>
88103
<NcIconSvgWrapper :path="action.icon" />

apps/appstore/src/composables/useActions.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,40 @@
66
import type { MaybeRefOrGetter } from 'vue'
77
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
88

9-
import { mdiCheck, mdiClose, mdiDownload, mdiTrashCanOutline, mdiUpdate } from '@mdi/js'
9+
import { mdiAlertCircleCheckOutline, mdiCheck, mdiClose, mdiDownload, mdiTrashCanOutline, mdiUpdate } from '@mdi/js'
1010
import { t } from '@nextcloud/l10n'
1111
import { computed, toValue } from 'vue'
1212
import { useAppsStore } from '../store/apps.ts'
1313
import { useUpdatesStore } from '../store/updates.ts'
14-
import { canDisable, canEnable, canForceEnable, canInstall, canUninstall, canUpdate } from '../utils/appStatus.ts'
14+
import { canDisable, canEnable, canInstall, canUninstall, canUpdate, needForceEnable } from '../utils/appStatus.ts'
15+
16+
type AppAction = {
17+
id: string
18+
icon: string
19+
label: (app: IAppstoreApp | IAppstoreExApp) => string
20+
callback: (app: IAppstoreApp | IAppstoreExApp) => Promise<void>
21+
variant?: 'primary' | 'error' | 'warning'
22+
inline?: boolean
23+
}
1524

1625
const AppAction = Object.freeze({
1726
INSTALL: {
1827
id: 'install',
1928
icon: mdiDownload,
20-
variant: 'primary',
2129
label: (app: IAppstoreApp | IAppstoreExApp) => {
2230
if (app.app_api) {
2331
return t('appstore', 'Deploy and enable')
2432
}
25-
return t('appstore', 'Download and enable')
33+
if (app.needsDownload) {
34+
return t('appstore', 'Download and enable')
35+
}
36+
return t('appstore', 'Install and enable')
2637
},
2738
async callback(app: IAppstoreApp | IAppstoreExApp) {
2839
const store = useAppsStore()
2940
await store.enableApp(app.id)
3041
},
31-
} as const,
42+
} as AppAction,
3243
ENABLE: {
3344
id: 'enable',
3445
icon: mdiCheck,
@@ -38,37 +49,38 @@ const AppAction = Object.freeze({
3849
const store = useAppsStore()
3950
await store.enableApp(app.id)
4051
},
41-
} as const,
52+
} as AppAction,
4253
FORCE_ENABLE: {
4354
id: 'force-enable',
44-
icon: mdiCheck,
45-
variant: 'primary',
55+
icon: mdiAlertCircleCheckOutline,
56+
inline: false,
4657
label: () => t('appstore', 'Force enable'),
58+
variant: 'warning',
4759
async callback(app: IAppstoreApp | IAppstoreExApp) {
4860
const store = useAppsStore()
4961
await store.forceEnableApp(app.id)
5062
},
51-
} as const,
63+
} as AppAction,
5264
DISABLE: {
5365
id: 'disable',
5466
icon: mdiClose,
55-
variant: 'tertiary',
5667
label: () => t('appstore', 'Disable'),
5768
async callback(app: IAppstoreApp | IAppstoreExApp) {
5869
const store = useAppsStore()
5970
await store.disableApp(app.id)
6071
},
61-
} as const,
72+
} as AppAction,
6273
REMOVE: {
6374
id: 'remove',
6475
icon: mdiTrashCanOutline,
6576
variant: 'error',
77+
inline: false,
6678
label: () => t('appstore', 'Remove'),
6779
async callback(app: IAppstoreApp | IAppstoreExApp) {
6880
const store = useAppsStore()
6981
await store.uninstallApp(app.id)
7082
},
71-
} as const,
83+
} as AppAction,
7284
UPDATE: {
7385
id: 'update',
7486
icon: mdiUpdate,
@@ -78,7 +90,7 @@ const AppAction = Object.freeze({
7890
const store = useUpdatesStore()
7991
await store.updateApp(app.id)
8092
},
81-
} as const,
93+
} as AppAction,
8294
})
8395

8496
/**
@@ -97,12 +109,12 @@ export function useActions(app: MaybeRefOrGetter<IAppstoreApp | IAppstoreExApp>)
97109
actions.push(AppAction.DISABLE)
98110
}
99111

100-
if (canInstall(toValue(app))) {
112+
if (needForceEnable(toValue(app))) {
113+
actions.push(AppAction.FORCE_ENABLE)
114+
} else if (canInstall(toValue(app))) {
101115
actions.push(AppAction.INSTALL)
102116
} else if (canEnable(toValue(app))) {
103117
actions.push(AppAction.ENABLE)
104-
} else if (canForceEnable(toValue(app))) {
105-
actions.push(AppAction.FORCE_ENABLE)
106118
}
107119

108120
if (canUninstall(toValue(app))) {

apps/appstore/src/router/routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const appstoreEnabled = loadState<boolean>('appstore', 'appstoreEnabled', true)
1313
// Dynamic loading
1414
const AppstoreDiscover = defineAsyncComponent(() => import('../views/AppstoreDiscover.vue'))
1515
const AppstoreManage = defineAsyncComponent(() => import('../views/AppstoreManage.vue'))
16+
const AppstoreBundles = defineAsyncComponent(() => import('../views/AppstoreBundles.vue'))
1617

1718
const routes: RouteRecordRaw[] = [
1819
{
@@ -32,6 +33,11 @@ const routes: RouteRecordRaw[] = [
3233
name: 'apps-discover',
3334
component: AppstoreDiscover,
3435
},
36+
{
37+
path: 'bundles/:id?',
38+
name: 'apps-bundles',
39+
component: AppstoreBundles,
40+
},
3541
{
3642
path: ':category(installed|enabled|disabled|updates)/:id?',
3743
name: 'apps-manage',

apps/appstore/src/service/api.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { IAppstoreApp, IAppstoreCategory } from '../apps.d.ts'
99
import axios from '@nextcloud/axios'
1010
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
1111
import { generateOcsUrl } from '@nextcloud/router'
12+
import PQueue from 'p-queue'
1213
import { APPSTORE_CATEGORY_ICONS } from '../constants.ts'
1314

1415
addPasswordConfirmationInterceptors(axios)
@@ -24,13 +25,17 @@ const Url = Object.freeze({
2425
update: `${BASE_URL}/apps/update`,
2526
})
2627

28+
const queue = new PQueue({ concurrency: 1 })
29+
2730
/**
2831
* Force enable app by ignoring its dependencies
2932
*
3033
* @param appId - The app to force enable
3134
*/
3235
export async function forceEnableApp(appId: string) {
33-
await axios.post(Url.forceEnable, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
36+
return queue.add(async () => {
37+
await axios.post(Url.forceEnable, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
38+
})
3439
}
3540

3641
/**
@@ -39,7 +44,9 @@ export async function forceEnableApp(appId: string) {
3944
* @param appId - The app to enable
4045
*/
4146
export async function enableApp(appId: string) {
42-
await axios.post(Url.enable, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
47+
return queue.add(async () => {
48+
await axios.post(Url.enable, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
49+
})
4350
}
4451

4552
/**
@@ -48,7 +55,9 @@ export async function enableApp(appId: string) {
4855
* @param appId - The app to disable
4956
*/
5057
export async function disableApp(appId: string) {
51-
await axios.post(Url.disable, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
58+
return queue.add(async () => {
59+
await axios.post(Url.disable, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
60+
})
5261
}
5362

5463
/**
@@ -57,7 +66,9 @@ export async function disableApp(appId: string) {
5766
* @param appId - The app id to update
5867
*/
5968
export async function updateApp(appId: string) {
60-
await axios.post(Url.update, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
69+
return queue.add(async () => {
70+
await axios.post(Url.update, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
71+
})
6172
}
6273

6374
/**
@@ -66,7 +77,9 @@ export async function updateApp(appId: string) {
6677
* @param appId - The app to uninstall
6778
*/
6879
export async function uninstallApp(appId: string) {
69-
await axios.post(Url.uninstall, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
80+
return queue.add(async () => {
81+
await axios.post(Url.uninstall, { appId }, { confirmPassword: PwdConfirmationMode.Strict })
82+
})
7083
}
7184

7285
/**

apps/appstore/src/utils/appStatus.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,20 @@ import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
1111
* @param app - The app to check if installable
1212
*/
1313
export function canInstall(app: IAppstoreApp | IAppstoreExApp) {
14-
return !app.installed && (!app.missingDependencies || app.missingDependencies.length === 0)
14+
if (app.installed || app.internal) {
15+
return false
16+
}
17+
18+
if (app.missingDependencies === undefined || app.missingDependencies.length === 0) {
19+
return true
20+
}
21+
22+
if (!app.isCompatible && app.missingDependencies.length === 1) {
23+
// incompatible so can be installed but has to be force-enabled
24+
return true
25+
}
26+
27+
return false
1528
}
1629

1730
/**
@@ -41,6 +54,15 @@ export function canForceEnable(app: IAppstoreApp | IAppstoreExApp) {
4154
return !app.active && (app.installed || canInstall(app))
4255
}
4356

57+
/**
58+
* Check if an app needs to be force-enabled
59+
*
60+
* @param app - The app to check
61+
*/
62+
export function needForceEnable(app: IAppstoreApp | IAppstoreExApp) {
63+
return !app.active && !app.isCompatible
64+
}
65+
4466
/**
4567
* Check if an app can be disabled.
4668
*

0 commit comments

Comments
 (0)