aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components/AppList.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components/AppList.vue')
-rw-r--r--apps/settings/src/components/AppList.vue328
1 files changed, 247 insertions, 81 deletions
diff --git a/apps/settings/src/components/AppList.vue b/apps/settings/src/components/AppList.vue
index 3a0c1fe51d0..3e40e08b257 100644
--- a/apps/settings/src/components/AppList.vue
+++ b/apps/settings/src/components/AppList.vue
@@ -1,96 +1,133 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div id="app-content-inner">
- <div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
+ <div id="apps-list"
+ class="apps-list"
+ :class="{
+ 'apps-list--list-view': (useBundleView || useListView),
+ 'apps-list--store-view': useAppStoreView,
+ }">
<template v-if="useListView">
- <div v-if="showUpdateAll" class="toolbar">
+ <div v-if="showUpdateAll" class="apps-list__toolbar">
{{ n('settings', '%n app has an update available', '%n apps have an update available', counter) }}
- <Button v-if="showUpdateAll"
+ <NcButton v-if="showUpdateAll"
id="app-list-update-all"
type="primary"
@click="updateAll">
{{ n('settings', 'Update', 'Update all', counter) }}
- </Button>
+ </NcButton>
</div>
- <div v-if="!showUpdateAll" class="toolbar">
+ <div v-if="!showUpdateAll" class="apps-list__toolbar">
{{ t('settings', 'All apps are up-to-date.') }}
</div>
- <transition-group name="app-list" tag="div" class="apps-list-container">
+ <TransitionGroup name="apps-list" tag="table" class="apps-list__list-container">
+ <tr key="app-list-view-header">
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Icon') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Name') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Version') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Level') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Actions') }}</span>
+ </th>
+ </tr>
<AppItem v-for="app in apps"
:key="app.id"
:app="app"
:category="category" />
- </transition-group>
+ </TransitionGroup>
</template>
- <transition-group v-if="useBundleView"
- name="app-list"
- tag="div"
- class="apps-list-container">
+ <table v-if="useBundleView"
+ class="apps-list__list-container">
+ <tr key="app-list-view-header">
+ <th id="app-table-col-icon">
+ <span class="hidden-visually">{{ t('settings', 'Icon') }}</span>
+ </th>
+ <th id="app-table-col-name">
+ <span class="hidden-visually">{{ t('settings', 'Name') }}</span>
+ </th>
+ <th id="app-table-col-version">
+ <span class="hidden-visually">{{ t('settings', 'Version') }}</span>
+ </th>
+ <th id="app-table-col-level">
+ <span class="hidden-visually">{{ t('settings', 'Level') }}</span>
+ </th>
+ <th id="app-table-col-actions">
+ <span class="hidden-visually">{{ t('settings', 'Actions') }}</span>
+ </th>
+ </tr>
<template v-for="bundle in bundles">
- <div :key="bundle.id" class="apps-header">
- <div class="app-image" />
- <h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" @click="toggleBundle(bundle.id)"></h2>
- <div class="app-version" />
- <div class="app-level" />
- <div class="app-groups" />
- <div class="actions">
- &nbsp;
- </div>
- </div>
+ <tr :key="bundle.id">
+ <th :id="`app-table-rowgroup-${bundle.id}`" colspan="5" scope="rowgroup">
+ <div class="apps-list__bundle-heading">
+ <span class="apps-list__bundle-header">
+ {{ bundle.name }}
+ </span>
+ <NcButton type="secondary" @click="toggleBundle(bundle.id)">
+ {{ t('settings', bundleToggleText(bundle.id)) }}
+ </NcButton>
+ </div>
+ </th>
+ </tr>
<AppItem v-for="app in bundleApps(bundle.id)"
:key="bundle.id + app.id"
+ :use-bundle-view="true"
+ :headers="`app-table-rowgroup-${bundle.id}`"
:app="app"
:category="category" />
</template>
- </transition-group>
- <template v-if="useAppStoreView">
+ </table>
+ <ul v-if="useAppStoreView" class="apps-list__store-container">
<AppItem v-for="app in apps"
:key="app.id"
:app="app"
:category="category"
:list-view="false" />
- </template>
+ </ul>
</div>
- <div id="apps-list-search" class="apps-list installed">
- <div class="apps-list-container">
- <template v-if="search !== '' && searchApps.length > 0">
- <div class="section">
- <div />
- <td colspan="5">
- <h2>{{ t('settings', 'Results from other categories') }}</h2>
- </td>
- </div>
+ <div id="apps-list-search" class="apps-list apps-list--list-view">
+ <div class="apps-list__list-container">
+ <table v-if="search !== '' && searchApps.length > 0" class="apps-list__list-container">
+ <caption class="apps-list__bundle-header">
+ {{ t('settings', 'Results from other categories') }}
+ </caption>
+ <tr key="app-list-view-header">
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Icon') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Name') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Version') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Level') }}</span>
+ </th>
+ <th>
+ <span class="hidden-visually">{{ t('settings', 'Actions') }}</span>
+ </th>
+ </tr>
<AppItem v-for="app in searchApps"
:key="app.id"
:app="app"
- :category="category"
- :list-view="true" />
- </template>
+ :category="category" />
+ </table>
</div>
</div>
@@ -98,31 +135,58 @@
<div id="app-list-empty-icon" class="icon-settings-dark" />
<h2>{{ t('settings', 'No apps found for your version') }}</h2>
</div>
-
- <div id="searchresults" />
</div>
</template>
<script>
-import AppItem from './AppList/AppItem'
-import PrefixMixin from './PrefixMixin'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import pLimit from 'p-limit'
-import Button from '@nextcloud/vue/dist/Components/Button'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import AppItem from './AppList/AppItem.vue'
+import AppManagement from '../mixins/AppManagement'
+import { useAppApiStore } from '../store/app-api-store'
+import { useAppsStore } from '../store/apps-store'
export default {
name: 'AppList',
components: {
AppItem,
- Button,
+ NcButton,
+ },
+
+ mixins: [AppManagement],
+
+ props: {
+ category: {
+ type: String,
+ required: true,
+ },
+ },
+
+ setup() {
+ const appApiStore = useAppApiStore()
+ const store = useAppsStore()
+
+ return {
+ appApiStore,
+ store,
+ }
+ },
+
+ data() {
+ return {
+ search: '',
+ }
},
- mixins: [PrefixMixin],
- props: ['category', 'app', 'search'],
computed: {
counter() {
return this.apps.filter(app => app.update).length
},
loading() {
- return this.$store.getters.loading('list')
+ if (!this.$store.getters['appApiApps/isAppApiEnabled']) {
+ return this.$store.getters.loading('list')
+ }
+ return this.$store.getters.loading('list') || this.appApiStore.getLoading('list')
},
hasPendingUpdate() {
return this.apps.filter(app => app.update).length > 0
@@ -131,12 +195,18 @@ export default {
return this.hasPendingUpdate && this.useListView
},
apps() {
- const apps = this.$store.getters.getAllApps
+ // Exclude ExApps from the list if AppAPI is disabled
+ const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
+ const apps = [...this.$store.getters.getAllApps, ...exApps]
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
.sort(function(a, b) {
- const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name
- const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name
- return OC.Util.naturalSortCompare(sortStringA, sortStringB)
+ const natSortDiff = OC.Util.naturalSortCompare(a, b)
+ if (natSortDiff === 0) {
+ const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1)
+ const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1)
+ return Number(sortStringA) - Number(sortStringB)
+ }
+ return natSortDiff
})
if (this.category === 'installed') {
@@ -154,9 +224,15 @@ export default {
if (this.category === 'updates') {
return apps.filter(app => app.update)
}
+ if (this.category === 'supported') {
+ // For customers of the Nextcloud GmbH the app level will be set to `300` for apps that are supported in their subscription
+ return apps.filter(app => app.level === 300)
+ }
if (this.category === 'featured') {
+ // An app level of `200` will be set for apps featured on the app store
return apps.filter(app => app.level === 200)
}
+
// filter app store categories
return apps.filter(app => {
return app.appstore && app.category !== undefined
@@ -164,7 +240,7 @@ export default {
})
},
bundles() {
- return this.$store.getters.getServerData.bundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
+ return this.$store.getters.getAppBundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
},
bundleApps() {
return function(bundle) {
@@ -178,7 +254,8 @@ export default {
if (this.search === '') {
return []
}
- return this.$store.getters.getAllApps
+ const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
+ return [...this.$store.getters.getAllApps, ...exApps]
.filter(app => {
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
return (!this.apps.find(_app => _app.id === app.id))
@@ -190,28 +267,43 @@ export default {
return !this.useListView && !this.useBundleView
},
useListView() {
- return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates' || this.category === 'featured')
+ return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates' || this.category === 'featured' || this.category === 'supported')
},
useBundleView() {
return (this.category === 'app-bundles')
},
allBundlesEnabled() {
- const self = this
- return function(id) {
- return self.bundleApps(id).filter(app => !app.active).length === 0
+ return (id) => {
+ return this.bundleApps(id).filter(app => !app.active).length === 0
}
},
bundleToggleText() {
- const self = this
- return function(id) {
- if (self.allBundlesEnabled(id)) {
+ return (id) => {
+ if (this.allBundlesEnabled(id)) {
return t('settings', 'Disable all')
}
- return t('settings', 'Enable all')
+ return t('settings', 'Download and enable all')
}
},
},
+
+ beforeDestroy() {
+ unsubscribe('nextcloud:unified-search.search', this.setSearch)
+ unsubscribe('nextcloud:unified-search.reset', this.resetSearch)
+ },
+
+ mounted() {
+ subscribe('nextcloud:unified-search.search', this.setSearch)
+ subscribe('nextcloud:unified-search.reset', this.resetSearch)
+ },
+
methods: {
+ setSearch({ query }) {
+ this.search = query
+ },
+ resetSearch() {
+ this.search = ''
+ },
toggleBundle(id) {
if (this.allBundlesEnabled(id)) {
return this.disableBundle(id)
@@ -237,9 +329,83 @@ export default {
const limit = pLimit(1)
this.apps
.filter(app => app.update)
- .map(app => limit(() => this.$store.dispatch('updateApp', { appId: app.id }))
- )
+ .map((app) => limit(() => {
+ this.update(app.id)
+ }))
},
},
}
</script>
+
+<style lang="scss" scoped>
+$toolbar-padding: 8px;
+$toolbar-height: 44px + $toolbar-padding * 2;
+
+.apps-list {
+ display: flex;
+ flex-wrap: wrap;
+ align-content: flex-start;
+
+ // For transition group
+ &--move {
+ transition: transform 1s;
+ }
+
+ #app-list-update-all {
+ margin-inline-start: 10px;
+ }
+
+ &__toolbar {
+ height: $toolbar-height;
+ padding: $toolbar-padding;
+ // Leave room for app-navigation-toggle
+ padding-inline-start: $toolbar-height;
+ width: 100%;
+ background-color: var(--color-main-background);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ }
+
+ &--list-view {
+ margin-bottom: 100px;
+ // For positioning link overlay on rows
+ position: relative;
+ }
+
+ &__list-container {
+ width: 100%;
+ }
+
+ &__store-container {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__bundle-heading {
+ display: flex;
+ align-items: center;
+ margin-block: 20px;
+ margin-inline: 0 10px;
+ }
+
+ &__bundle-header {
+ margin-block: 0;
+ margin-inline: 50px 10px;
+ font-weight: bold;
+ font-size: 20px;
+ line-height: 30px;
+ color: var(--color-text-light);
+ }
+}
+
+#apps-list-search {
+ .app-item {
+ h2 {
+ margin-bottom: 0;
+ }
+ }
+}
+</style>