Merge pull request #18969 from nextcloud/enh/apps/use-sidebar

Migrate app management settings to proper sidebar standards
This commit is contained in:
Morris Jobke 2020-08-14 15:44:04 +02:00 committed by GitHub
commit 7d550e3679
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 289 additions and 173 deletions

View File

@ -663,8 +663,6 @@ span.version {
}
.app-level {
margin-top: 8px;
span {
color: var(--color-text-maxcontrast);
background-color: transparent;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -21,116 +21,74 @@
-->
<template>
<div id="app-details-view" style="padding: 20px;">
<h2>
<div v-if="!app.preview" class="icon-settings-dark" />
<svg v-if="app.previewAsIcon && app.preview"
width="32"
height="32"
viewBox="0 0 32 32">
<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs>
<image x="0"
y="0"
width="32"
height="32"
preserveAspectRatio="xMinYMin meet"
:filter="filterUrl"
:xlink:href="app.preview"
class="app-icon" />
</svg>
{{ app.name }}
</h2>
<img v-if="screenshotLoaded" :src="app.screenshot" width="100%">
<div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level">
<span v-if="app.level === 300"
v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')"
class="supported icon-checkmark-color">
{{ t('settings', 'Supported') }}</span>
<span v-if="app.level === 200"
v-tooltip.auto="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')"
class="official icon-checkmark">
{{ t('settings', 'Featured') }}</span>
<AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" />
</div>
<div v-if="author" class="app-author">
{{ t('settings', 'by') }}
<span v-for="(a, index) in author" :key="index">
<a v-if="a['@attributes'] && a['@attributes']['homepage']" :href="a['@attributes']['homepage']">{{ a['@value'] }}</a><span v-else-if="a['@value']">{{ a['@value'] }}</span><span v-else>{{ a }}</span><span v-if="index+1 < author.length">, </span>
</span>
</div>
<div v-if="licence" class="app-licence">
{{ licence }}
</div>
<div class="actions">
<div class="actions-buttons">
<div class="app-details">
<div class="app-details__actions">
<div v-if="app.active && canLimitToGroups(app)" class="app-details__actions-groups">
<input :id="prefix('groups_enable', app.id)"
v-model="groupCheckedAppsData"
type="checkbox"
:value="app.id"
class="groups-enable__checkbox checkbox"
@change="setGroupLimit">
<label :for="prefix('groups_enable', app.id)">{{ t('settings', 'Limit to groups') }}</label>
<input type="hidden"
class="group_select"
:title="t('settings', 'All')"
value="">
<Multiselect v-if="isLimitedToGroups(app)"
:options="groups"
:value="appGroups"
:options-limit="5"
:placeholder="t('settings', 'Limit app usage to groups')"
label="name"
track-by="id"
class="multiselect-vue"
:multiple="true"
:close-on-select="false"
:tag-width="60"
@select="addGroupLimitation"
@remove="removeGroupLimitation"
@search-change="asyncFindGroup">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</div>
<div class="app-details__actions-manage">
<input v-if="app.update"
class="update primary"
type="button"
:value="t('settings', 'Update to {version}', {version: app.update})"
:disabled="installing || loading(app.id)"
:value="t('settings', 'Update to {version}', { version: app.update })"
:disabled="installing || isLoading"
@click="update(app.id)">
<input v-if="app.canUnInstall"
class="uninstall"
type="button"
:value="t('settings', 'Remove')"
:disabled="installing || loading(app.id)"
:disabled="installing || isLoading"
@click="remove(app.id)">
<input v-if="app.active"
class="enable"
type="button"
:value="t('settings','Disable')"
:disabled="installing || loading(app.id)"
:disabled="installing || isLoading"
@click="disable(app.id)">
<input v-if="!app.active && (app.canInstall || app.isCompatible)"
v-tooltip.auto="enableButtonTooltip"
class="enable primary"
type="button"
:value="enableButtonText"
:disabled="!app.canInstall || installing || loading(app.id)"
:disabled="!app.canInstall || installing || isLoading"
@click="enable(app.id)">
<input v-else-if="!app.active"
<input v-else-if="!app.active && !app.canInstall"
v-tooltip.auto="forceEnableButtonTooltip"
class="enable force"
type="button"
:value="forceEnableButtonText"
:disabled="installing || loading(app.id)"
:disabled="installing || isLoading"
@click="forceEnable(app.id)">
</div>
<div class="app-groups">
<div v-if="app.active && canLimitToGroups(app)" class="groups-enable">
<input :id="prefix('groups_enable', app.id)"
v-model="groupCheckedAppsData"
type="checkbox"
:value="app.id"
class="groups-enable__checkbox checkbox"
@change="setGroupLimit">
<label :for="prefix('groups_enable', app.id)">{{ t('settings', 'Limit to groups') }}</label>
<input type="hidden"
class="group_select"
:title="t('settings', 'All')"
value="">
<Multiselect v-if="isLimitedToGroups(app)"
:options="groups"
:value="appGroups"
:options-limit="5"
:placeholder="t('settings', 'Limit app usage to groups')"
label="name"
track-by="id"
class="multiselect-vue"
:multiple="true"
:close-on-select="false"
:tag-width="60"
@select="addGroupLimitation"
@remove="removeGroupLimitation"
@search-change="asyncFindGroup">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</Multiselect>
</div>
</div>
</div>
<ul class="app-dependencies">
<ul class="app-details__dependencies">
<li v-if="app.missingMinOwnCloudVersion">
{{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}
</li>
@ -147,7 +105,7 @@
</li>
</ul>
<p class="documentation">
<p class="app-details__documentation">
<a v-if="!app.internal"
class="appslink"
:href="appstoreUrl"
@ -182,7 +140,7 @@
rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} </a>
</p>
<div class="app-description" v-html="renderMarkdown" />
<div class="app-details__description" v-html="renderMarkdown" />
</div>
</template>
@ -191,25 +149,30 @@ import { Multiselect } from '@nextcloud/vue'
import marked from 'marked'
import dompurify from 'dompurify'
import AppScore from './AppList/AppScore'
import AppManagement from './AppManagement'
import AppManagement from '../mixins/AppManagement'
import PrefixMixin from './PrefixMixin'
import SvgFilterMixin from './SvgFilterMixin'
export default {
name: 'AppDetails',
components: {
Multiselect,
AppScore,
},
mixins: [AppManagement, PrefixMixin, SvgFilterMixin],
props: ['category', 'app'],
mixins: [AppManagement, PrefixMixin],
props: {
app: {
type: Object,
required: true,
},
},
data() {
return {
groupCheckedAppsData: false,
screenshotLoaded: false,
}
},
computed: {
appstoreUrl() {
return `https://apps.nextcloud.com/apps/${this.app.id}`
@ -220,9 +183,6 @@ export default {
}
return null
},
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
},
author() {
if (typeof this.app.author === 'string') {
return [
@ -309,27 +269,49 @@ export default {
if (this.app.groups.length > 0) {
this.groupCheckedAppsData = true
}
if (this.app.screenshot) {
const image = new Image()
image.onload = (e) => {
this.screenshotLoaded = true
}
image.src = this.app.screenshot
}
},
}
</script>
<style scoped>
.force {
background: var(--color-main-background);
border-color: var(--color-error);
color: var(--color-error);
<style scoped lang="scss">
.app-details {
padding: 20px;
&__actions {
// app management
&-manage {
// if too many, shrink them and ellipsis
display: flex;
input {
flex: 0 1 auto;
min-width: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
}
.force:hover,
.force:active {
background: var(--color-error);
border-color: var(--color-error) !important;
color: var(--color-main-background);
&__dependencies {
opacity: .7;
}
&__documentation {
padding-top: 20px;
}
&__description {
padding-top: 20px;
}
}
.force {
color: var(--color-error);
border-color: var(--color-error);
background: var(--color-main-background);
}
.force:hover,
.force:active {
color: var(--color-main-background);
border-color: var(--color-error) !important;
background: var(--color-error);
}
</style>

View File

@ -69,38 +69,38 @@
<div v-if="app.error" class="warning">
{{ app.error }}
</div>
<div v-if="loading(app.id)" class="icon icon-loading-small" />
<div v-if="isLoading" class="icon icon-loading-small" />
<input v-if="app.update"
class="update primary"
type="button"
:value="t('settings', 'Update to {update}', {update:app.update})"
:disabled="installing || loading(app.id)"
:disabled="installing || isLoading"
@click.stop="update(app.id)">
<input v-if="app.canUnInstall"
class="uninstall"
type="button"
:value="t('settings', 'Remove')"
:disabled="installing || loading(app.id)"
:disabled="installing || isLoading"
@click.stop="remove(app.id)">
<input v-if="app.active"
class="enable"
type="button"
:value="t('settings','Disable')"
:disabled="installing || loading(app.id)"
:disabled="installing || isLoading"
@click.stop="disable(app.id)">
<input v-if="!app.active && (app.canInstall || app.isCompatible)"
v-tooltip.auto="enableButtonTooltip"
class="enable"
type="button"
:value="enableButtonText"
:disabled="!app.canInstall || installing || loading(app.id)"
:disabled="!app.canInstall || installing || isLoading"
@click.stop="enable(app.id)">
<input v-else-if="!app.active"
v-tooltip.auto="forceEnableButtonTooltip"
class="enable force"
type="button"
:value="forceEnableButtonText"
:disabled="installing || loading(app.id)"
:disabled="installing || isLoading"
@click.stop="forceEnable(app.id)">
</div>
</div>
@ -108,7 +108,7 @@
<script>
import AppScore from './AppScore'
import AppManagement from '../AppManagement'
import AppManagement from '../../mixins/AppManagement'
import SvgFilterMixin from '../SvgFilterMixin'
export default {

View File

@ -1,40 +1,37 @@
<!--
- @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/>.
-
-->
/**
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
<script>
export default {
computed: {
appGroups() {
return this.app.groups.map(group => { return { id: group, name: group } })
},
loading() {
const self = this
return function(id) {
return self.$store.getters.loading(id)
}
},
installing() {
return this.$store.getters.loading('install')
},
isLoading() {
return this.app && this.$store.getters.loading(this.app.id)
},
enableButtonText() {
if (this.app.needsDownload) {
return t('settings', 'Download and enable')
@ -61,11 +58,19 @@ export default {
return base
},
},
data() {
return {
groupCheckedAppsData: false,
}
},
mounted() {
if (this.app.groups.length > 0) {
if (this.app && this.app.groups && this.app.groups.length > 0) {
this.groupCheckedAppsData = true
}
},
methods: {
asyncFindGroup(query) {
return this.$store.dispatch('getGroups', { search: query, limit: 5, offset: 0 })
@ -135,4 +140,3 @@ export default {
},
},
}
</script>

View File

@ -22,9 +22,10 @@
<template>
<Content app-name="settings"
:class="{ 'with-app-sidebar': currentApp}"
:class="{ 'with-app-sidebar': app}"
:content-class="{ 'icon-loading': loadingList }"
:navigation-class="{ 'icon-loading': loading }">
<!-- Categories & filters -->
<AppNavigation>
<template #list>
<AppNavigationItem
@ -86,11 +87,39 @@
:title="t('settings', 'Developer documentation') + ' ↗'" />
</template>
</AppNavigation>
<!-- Apps list -->
<AppContent class="app-settings-content" :class="{ 'icon-loading': loadingList }">
<AppList :category="category" :app="currentApp" :search="searchQuery" />
<AppList :category="category" :app="app" :search="searchQuery" />
</AppContent>
<AppSidebar v-if="id && currentApp" @close="hideAppDetails">
<AppDetails :category="category" :app="currentApp" />
<!-- Selected app details -->
<AppSidebar
v-if="id && app"
v-bind="appSidebar"
:class="{'app-sidebar--without-background': !appSidebar.background}"
@close="hideAppDetails">
<template v-if="!appSidebar.background" #header>
<div class="app-sidebar-header__figure--default-app-icon icon-settings-dark" />
</template>
<template #primary-actions>
<!-- Featured/Supported badges -->
<div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level">
<span v-if="app.level === 300"
v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')"
class="supported icon-checkmark-color">
{{ t('settings', 'Supported') }}</span>
<span v-if="app.level === 200"
v-tooltip.auto="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')"
class="official icon-checkmark">
{{ t('settings', 'Featured') }}</span>
<AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" />
</div>
</template>
<!-- Tab content -->
<AppDetails :app="app" />
</AppSidebar>
</Content>
</template>
@ -108,11 +137,14 @@ import VueLocalStorage from 'vue-localstorage'
import AppList from '../components/AppList'
import AppDetails from '../components/AppDetails'
import AppManagement from '../mixins/AppManagement'
import AppScore from '../components/AppList/AppScore'
Vue.use(VueLocalStorage)
export default {
name: 'Apps',
components: {
AppContent,
AppDetails,
@ -121,9 +153,13 @@ export default {
AppNavigationCounter,
AppNavigationItem,
AppNavigationSpacer,
AppScore,
AppSidebar,
Content,
},
mixins: [AppManagement],
props: {
category: {
type: String,
@ -134,11 +170,14 @@ export default {
default: '',
},
},
data() {
return {
searchQuery: '',
screenshotLoaded: false,
}
},
computed: {
loading() {
return this.$store.getters.loading('categories')
@ -146,7 +185,7 @@ export default {
loadingList() {
return this.$store.getters.loading('list')
},
currentApp() {
app() {
return this.apps.find(app => app.id === this.id)
},
categories() {
@ -161,12 +200,53 @@ export default {
settings() {
return this.$store.getters.getServerData
},
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
},
// sidebar app binding
appSidebar() {
const author = Array.isArray(this.app.author)
? this.app.author[0]['@value']
? this.app.author.map(author => author['@value']).join(', ')
: this.app.author.join(', ')
: this.app.author['@value']
? this.app.author['@value']
: this.app.author
const license = t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() })
const subtitle = t('settings', 'by {author}\n{license}', { author, license })
return {
subtitle,
background: this.app.screenshot && this.screenshotLoaded
? this.app.screenshot
: this.app.preview,
compact: !(this.app.screenshot && this.screenshotLoaded),
title: this.app.name,
}
},
},
watch: {
category(val, old) {
this.setSearch('')
},
app() {
this.screenshotLoaded = false
if (this.app && this.app.screenshot) {
const image = new Image()
image.onload = (e) => {
this.screenshotLoaded = true
}
image.src = this.app.screenshot
}
},
},
beforeMount() {
this.$store.dispatch('getCategories')
this.$store.dispatch('getAllApps')
@ -179,6 +259,7 @@ export default {
*/
this.appSearch = new OCA.Search(this.setSearch, this.resetSearch)
},
methods: {
setSearch(query) {
this.searchQuery = query
@ -195,3 +276,54 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.app-sidebar::v-deep {
&:not(.app-sidebar--without-background) {
// with full screenshot, let's fill the figure
:not(.app-sidebar-header--compact) .app-sidebar-header__figure {
background-size: cover;
}
// revert sidebar app icon so it is black
.app-sidebar-header--compact .app-sidebar-header__figure {
background-size: 32px;
filter: invert(1);
}
}
// default icon slot styling
&.app-sidebar--without-background {
.app-sidebar-header__figure {
display: flex;
align-items: center;
justify-content: center;
&--default-app-icon {
width: 32px;
height: 32px;
background-size: 32px;
}
}
}
// TODO: migrate to components
.app-sidebar-header__desc {
// allow multi line subtitle for the license
.app-sidebar-header__subtitle {
overflow: visible !important;
height: auto;
white-space: normal !important;
line-height: 16px;
}
}
.app-sidebar-header__action {
// align with tab content
margin: 0 20px;
input {
margin: 3px;
}
}
}
</style>