diff options
Diffstat (limited to 'apps/settings/src/components/AppList.vue')
-rw-r--r-- | apps/settings/src/components/AppList.vue | 328 |
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"> - - </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> |