Преглед на файлове

Remove users management settings cleaning leftovers

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
tags/v18.0.0beta1
John Molakvoæ (skjnldsv) преди 4 години
родител
ревизия
690330f308
No account linked to committer's email address

+ 1
- 1
apps/settings/src/components/UserList.vue Целия файл

@@ -227,7 +227,7 @@
</template>

<script>
import userRow from './userList/UserRow'
import userRow from './UserList/UserRow'
import { Multiselect } from 'nextcloud-vue'
import InfiniteLoading from 'vue-infinite-loading'
import Vue from 'vue'

apps/settings/src/components/userList/UserRow.vue → apps/settings/src/components/UserList/UserRow.vue Целия файл


+ 0
- 237
apps/settings/src/components/appDetails.vue Целия файл

@@ -1,237 +0,0 @@
<!--
- @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/>.
-
-->

<template>
<div id="app-details-view" style="padding: 20px;">
<h2>
<div v-if="!app.preview" class="icon-settings-dark"></div>
<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"></feColorMatrix></filter></defs>
<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" :filter="filterUrl" :xlink:href="app.preview" class="app-icon"></image>
</svg>
{{ app.name }}</h2>
<img v-if="app.screenshot" :src="app.screenshot" width="100%" />
<div class="app-level" v-if="app.level === 300 || app.level === 200 || hasRating">
<span class="supported icon-checkmark-color" v-if="app.level === 300"
v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')">
{{ t('settings', 'Supported') }}</span>
<span class="official icon-checkmark" v-if="app.level === 200"
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')">
{{ t('settings', 'Official') }}</span>
<app-score v-if="hasRating" :score="app.appstoreData.ratingOverall"></app-score>
</div>

<div class="app-author" v-if="author">
{{ t('settings', 'by') }}
<span v-for="(a, index) in author">
<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 class="app-licence" v-if="licence">{{ licence }}</div>
<div class="actions">
<div class="actions-buttons">
<input v-if="app.update" class="update primary" type="button" :value="t('settings', 'Update to {version}', {version: app.update})" v-on:click="update(app.id)" :disabled="installing || loading(app.id)"/>
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" v-on:click="remove(app.id)" :disabled="installing || loading(app.id)"/>
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="!app.active && (app.canInstall || app.isCompatible)" class="enable primary" type="button" :value="enableButtonText" v-on:click="enable(app.id)" v-tooltip.auto="enableButtonTooltip" :disabled="!app.canInstall || installing || loading(app.id)" />
<input v-else-if="!app.active" class="enable force" type="button" :value="forceEnableButtonText" v-on:click="forceEnable(app.id)" v-tooltip.auto="forceEnableButtonTooltip" :disabled="installing || loading(app.id)" />
</div>
<div class="app-groups">
<div class="groups-enable" v-if="app.active && canLimitToGroups(app)">
<input type="checkbox" :value="app.id" v-model="groupCheckedAppsData" v-on:change="setGroupLimit" class="groups-enable__checkbox checkbox" :id="prefix('groups_enable', app.id)">
<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" @select="addGroupLimitation" @remove="removeGroupLimitation" :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" @search-change="asyncFindGroup">
<span slot="noResult">{{t('settings', 'No results')}}</span>
</multiselect>
</div>
</div>
</div>

<ul class="app-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>
<li v-if="app.missingMaxOwnCloudVersion">{{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}</li>
<li v-if="!app.canInstall">
{{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
<ul class="missing-dependencies">
<li v-for="dep in app.missingDependencies">{{ dep }}</li>
</ul>
</li>
</ul>

<p class="documentation">
<a class="appslink" :href="appstoreUrl" v-if="!app.internal" target="_blank" rel="noreferrer noopener">{{ t('settings', 'View in store')}} ↗</a>

<a class="appslink" v-if="app.website" :href="app.website" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Visit website') }} ↗</a>
<a class="appslink" v-if="app.bugs" :href="app.bugs" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Report a bug') }} ↗</a>

<a class="appslink" v-if="app.documentation && app.documentation.user" :href="app.documentation.user" target="_blank" rel="noreferrer noopener">{{ t('settings', 'User documentation') }} ↗</a>
<a class="appslink" v-if="app.documentation && app.documentation.admin" :href="app.documentation.admin" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} ↗</a>
<a class="appslink" v-if="app.documentation && app.documentation.developer" :href="app.documentation.developer" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a>
</p>

<div class="app-description" v-html="renderMarkdown"></div>
</div>
</template>

<script>
import { Multiselect } from 'nextcloud-vue';
import marked from 'marked';
import dompurify from 'dompurify'

import AppScore from './appList/appScore';
import AppManagement from './appManagement';
import prefix from './prefixMixin';
import SvgFilterMixin from './svgFilterMixin';

export default {
mixins: [AppManagement, prefix, SvgFilterMixin],
name: 'appDetails',
props: ['category', 'app'],
components: {
Multiselect,
AppScore
},
data() {
return {
groupCheckedAppsData: false,
}
},
mounted() {
if (this.app.groups.length > 0) {
this.groupCheckedAppsData = true;
}
},
computed: {
appstoreUrl() {
return `https://apps.nextcloud.com/apps/${this.app.id}`;
},
licence() {
if (this.app.licence) {
return t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() } );
}
return null;
},
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5;
},
author() {
if (typeof this.app.author === 'string') {
return [
{
'@value': this.app.author
}
]
}
if (this.app.author['@value']) {
return [this.app.author];
}
return this.app.author;
},
appGroups() {
return this.app.groups.map(group => {return {id: group, name: group}});
},
groups() {
return this.$store.getters.getGroups
.filter(group => group.id !== 'disabled')
.sort((a, b) => a.name.localeCompare(b.name));
},
renderMarkdown() {
var renderer = new marked.Renderer();
renderer.link = function(href, title, text) {
try {
var prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return '';
}

if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
return '';
}

var out = '<a href="' + href + '" rel="noreferrer noopener"';
if (title) {
out += ' title="' + title + '"';
}
out += '>' + text + '</a>';
return out;
};
renderer.image = function(href, title, text) {
if (text) {
return text;
}
return title;
};
renderer.blockquote = function(quote) {
return quote;
};
return dompurify.sanitize(
marked(this.app.description.trim(), {
renderer: renderer,
gfm: false,
highlight: false,
tables: false,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
}),
{
SAFE_FOR_JQUERY: true,
ALLOWED_TAGS: [
'strong',
'p',
'a',
'ul',
'ol',
'li',
'em',
'del',
'blockquote'
]
}
);
}
}
}
</script>

<style scoped>
.force {
background: var(--color-main-background);
border-color: var(--color-error);
color: var(--color-error);
}
.force:hover,
.force:active {
background: var(--color-error);
border-color: var(--color-error) !important;
color: var(--color-main-background);
}
</style>

+ 0
- 201
apps/settings/src/components/appList.vue Целия файл

@@ -1,201 +0,0 @@
<!--
- @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/>.
-
-->

<template>
<div id="app-content-inner">
<div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
<template v-if="useListView">
<transition-group name="app-list" tag="div" class="apps-list-container">
<AppItem v-for="app in apps"
:key="app.id"
:app="app"
:category="category" />
</transition-group>
</template>
<transition-group v-if="useBundleView"
name="app-list"
tag="div"
class="apps-list-container">
<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>
<AppItem v-for="app in bundleApps(bundle.id)"
:key="bundle.id + app.id"
:app="app"
:category="category" />
</template>
</transition-group>
<template v-if="useAppStoreView">
<AppItem v-for="app in apps"
:key="app.id"
:app="app"
:category="category"
:list-view="false" />
</template>
</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>
<AppItem v-for="app in searchApps"
:key="app.id"
:app="app"
:category="category"
:list-view="true" />
</template>
</div>
</div>

<div v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0" id="apps-list-empty" class="emptycontent emptycontent-search">
<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'

export default {
name: 'AppList',
components: {
AppItem
},
mixins: [PrefixMixin],
props: ['category', 'app', 'search'],
computed: {
loading() {
return this.$store.getters.loading('list')
},
apps() {
let apps = this.$store.getters.getAllApps
.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)
})

if (this.category === 'installed') {
return apps.filter(app => app.installed)
}
if (this.category === 'enabled') {
return apps.filter(app => app.active && app.installed)
}
if (this.category === 'disabled') {
return apps.filter(app => !app.active && app.installed)
}
if (this.category === 'app-bundles') {
return apps.filter(app => app.bundles)
}
if (this.category === 'updates') {
return apps.filter(app => app.update)
}
// filter app store categories
return apps.filter(app => {
return app.appstore && app.category !== undefined
&& (app.category === this.category || app.category.indexOf(this.category) > -1)
})
},
bundles() {
return this.$store.getters.getServerData.bundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
},
bundleApps() {
return function(bundle) {
return this.$store.getters.getAllApps
.filter(app => app.bundleId === bundle)
}
},
searchApps() {
if (this.search === '') {
return []
}
return this.$store.getters.getAllApps
.filter(app => {
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
return (!this.apps.find(_app => _app.id === app.id))
}
return false
})
},
useAppStoreView() {
return !this.useListView && !this.useBundleView
},
useListView() {
return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates')
},
useBundleView() {
return (this.category === 'app-bundles')
},
allBundlesEnabled() {
let self = this
return function(id) {
return self.bundleApps(id).filter(app => !app.active).length === 0
}
},
bundleToggleText() {
let self = this
return function(id) {
if (self.allBundlesEnabled(id)) {
return t('settings', 'Disable all')
}
return t('settings', 'Enable all')
}
}
},
methods: {
toggleBundle(id) {
if (this.allBundlesEnabled(id)) {
return this.disableBundle(id)
}
return this.enableBundle(id)
},
enableBundle(id) {
let apps = this.bundleApps(id).map(app => app.id)
this.$store.dispatch('enableApp', { appId: apps, groups: [] })
.catch((error) => { console.error(error); OC.Notification.show(error) })
},
disableBundle(id) {
let apps = this.bundleApps(id).map(app => app.id)
this.$store.dispatch('disableApp', { appId: apps, groups: [] })
.catch((error) => { OC.Notification.show(error) })
}
}
}
</script>

+ 0
- 136
apps/settings/src/components/appList/appItem.vue Целия файл

@@ -1,136 +0,0 @@
<!--
- @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/>.
-
-->

<template>
<div class="section" v-bind:class="{ selected: isSelected }" v-on:click="showAppDetails">
<div class="app-image app-image-icon" v-on:click="showAppDetails">
<div v-if="(listView && !app.preview) || (!listView && !app.screenshot)" class="icon-settings-dark"></div>

<svg v-if="listView && 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"></feColorMatrix></filter></defs>
<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" :filter="filterUrl" :xlink:href="app.preview" class="app-icon"></image>
</svg>

<img v-if="!listView && app.screenshot" :src="app.screenshot" width="100%" />
</div>
<div class="app-name" v-on:click="showAppDetails">
{{ app.name }}
</div>
<div class="app-summary" v-if="!listView">{{ app.summary }}</div>
<div class="app-version" v-if="listView">
<span v-if="app.version">{{ app.version }}</span>
<span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
</div>

<div class="app-level">
<span class="supported icon-checkmark-color" v-if="app.level === 300"
v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')">
{{ t('settings', 'Supported') }}</span>
<span class="official icon-checkmark" v-if="app.level === 200"
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')">
{{ t('settings', 'Official') }}</span>
<app-score v-if="hasRating && !listView" :score="app.score"></app-score>
</div>

<div class="actions">
<div class="warning" v-if="app.error">{{ app.error }}</div>
<div class="icon icon-loading-small" v-if="loading(app.id)"></div>
<input v-if="app.update" class="update primary" type="button" :value="t('settings', 'Update to {update}', {update:app.update})" v-on:click.stop="update(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" v-on:click.stop="remove(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click.stop="disable(app.id)" :disabled="installing || loading(app.id)" />
<input v-if="!app.active && (app.canInstall || app.isCompatible)" class="enable" type="button" :value="enableButtonText" v-on:click.stop="enable(app.id)" v-tooltip.auto="enableButtonTooltip" :disabled="!app.canInstall || installing || loading(app.id)" />
<input v-else-if="!app.active" class="enable force" type="button" :value="forceEnableButtonText" v-on:click.stop="forceEnable(app.id)" v-tooltip.auto="forceEnableButtonTooltip" :disabled="installing || loading(app.id)" />
</div>
</div>
</template>

<script>
import AppScore from './appScore';
import AppManagement from '../appManagement';
import SvgFilterMixin from '../svgFilterMixin';

export default {
name: 'appItem',
mixins: [AppManagement, SvgFilterMixin],
props: {
app: {},
category: {},
listView: {
type: Boolean,
default: true,
}
},
watch: {
'$route.params.id': function (id) {
this.isSelected = (this.app.id === id);
}
},
components: {
AppScore,
},
data() {
return {
isSelected: false,
scrolled: false,
};
},
mounted() {
this.isSelected = (this.app.id === this.$route.params.id);
},
computed: {
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5;
},
},
watchers: {

},
methods: {
showAppDetails(event) {
if (event.currentTarget.tagName === 'INPUT' || event.currentTarget.tagName === 'A') {
return;
}
this.$router.push({
name: 'apps-details',
params: {category: this.category, id: this.app.id}
});
},
prefix(prefix, content) {
return prefix + '_' + content;
},
}
}
</script>

<style scoped>
.force {
background: var(--color-main-background);
border-color: var(--color-error);
color: var(--color-error);
}
.force:hover,
.force:active {
background: var(--color-error);
border-color: var(--color-error) !important;
color: var(--color-main-background);
}
</style>

+ 0
- 38
apps/settings/src/components/appList/appScore.vue Целия файл

@@ -1,38 +0,0 @@
<!--
- @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/>.
-
-->

<template>
<img :src="scoreImage" class="app-score-image">
</template>
<script>
export default {
name: 'AppScore',
props: ['score'],
computed: {
scoreImage() {
let score = Math.round(this.score * 10)
let imageName = 'rating/s' + score + '.svg'
return OC.imagePath('core', imageName)
}
}
}
</script>

+ 0
- 138
apps/settings/src/components/appManagement.vue Целия файл

@@ -1,138 +0,0 @@
<!--
- @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/>.
-
-->

<script>
export default {
mounted() {
if (this.app.groups.length > 0) {
this.groupCheckedAppsData = true;
}
},
computed: {
appGroups() {
return this.app.groups.map(group => {return {id: group, name: group}});
},
loading() {
let self = this;
return function(id) {
return self.$store.getters.loading(id);
}
},
installing() {
return this.$store.getters.loading('install');
},
enableButtonText() {
if (this.app.needsDownload) {
return t('settings', 'Download and enable');
}
return t('settings', 'Enable');
},
forceEnableButtonText() {
if (this.app.needsDownload) {
return t('settings', 'Enable untested app');
}
return t('settings', 'Enable untested app');
},
enableButtonTooltip() {
if (this.app.needsDownload) {
return t('settings','The app will be downloaded from the app store');
}
return false;
},
forceEnableButtonTooltip() {
const base = t('settings', 'This app is not marked as compatible with your Nextcloud version. If you continue you will still be able to install the app. Note that the app might not work as expected.');
if (this.app.needsDownload) {
return base + ' ' + t('settings','The app will be downloaded from the app store');
}
return base;
}
},
methods: {
asyncFindGroup(query) {
return this.$store.dispatch('getGroups', {search: query, limit: 5, offset: 0});
},
isLimitedToGroups(app) {
if (this.app.groups.length || this.groupCheckedAppsData) {
return true;
}
return false;
},
setGroupLimit: function() {
if (!this.groupCheckedAppsData) {
this.$store.dispatch('enableApp', {appId: this.app.id, groups: []});
}
},
canLimitToGroups(app) {
if (app.types && app.types.includes('filesystem')
|| app.types.includes('prelogin')
|| app.types.includes('authentication')
|| app.types.includes('logging')
|| app.types.includes('prevent_group_restriction')) {
return false;
}
return true;
},
addGroupLimitation(group) {
let groups = this.app.groups.concat([]).concat([group.id]);
this.$store.dispatch('enableApp', { appId: this.app.id, groups: groups});
},
removeGroupLimitation(group) {
let currentGroups = this.app.groups.concat([]);
let index = currentGroups.indexOf(group.id);
if (index > -1) {
currentGroups.splice(index, 1);
}
this.$store.dispatch('enableApp', { appId: this.app.id, groups: currentGroups});
},
forceEnable(appId) {
this.$store.dispatch('forceEnableApp', { appId: appId, groups: [] })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
enable(appId) {
this.$store.dispatch('enableApp', { appId: appId, groups: [] })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
disable(appId) {
this.$store.dispatch('disableApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
remove(appId) {
this.$store.dispatch('uninstallApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
install(appId) {
this.$store.dispatch('enableApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
},
update(appId) {
this.$store.dispatch('updateApp', { appId: appId })
.then((response) => { OC.Settings.Apps.rebuildNavigation(); })
.catch((error) => { OC.Notification.show(error)});
}
}
}
</script>

+ 0
- 32
apps/settings/src/components/prefixMixin.vue Целия файл

@@ -1,32 +0,0 @@
<!--
- @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/>.
-
-->

<script>
export default {
name: 'PrefixMixin',
methods: {
prefix(prefix, content) {
return prefix + '_' + content
}
}
}
</script>

+ 0
- 40
apps/settings/src/components/svgFilterMixin.vue Целия файл

@@ -1,40 +0,0 @@
<!--
- @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/>.
-
-->

<script>
export default {
name: 'SvgFilterMixin',
data() {
return {
filterId: ''
}
},
computed: {
filterUrl() {
return `url(#${this.filterId})`
}
},
mounted() {
this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100)) + new Date().getSeconds() + new Date().getMilliseconds()
}
}
</script>

+ 0
- 429
apps/settings/src/components/userList.vue Целия файл

@@ -1,429 +0,0 @@
<!--
- @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
-
- @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/>.
-
-->
<template>
<div id="app-content" class="user-list-grid" v-on:scroll.passive="onScroll">
<div class="row" id="grid-header" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
<div id="headerAvatar" class="avatar"></div>
<div id="headerName" class="name">{{ t('settings', 'Username') }}</div>
<div id="headerDisplayName" class="displayName">{{ t('settings', 'Display name') }}</div>
<div id="headerPassword" class="password">{{ t('settings', 'Password') }}</div>
<div id="headerAddress" class="mailAddress">{{ t('settings', 'Email') }}</div>
<div id="headerGroups" class="groups">{{ t('settings', 'Groups') }}</div>
<div id="headerSubAdmins" class="subadmins"
v-if="subAdminsGroups.length>0 && settings.isAdmin">{{ t('settings', 'Group admin for') }}</div>
<div id="headerQuota" class="quota">{{ t('settings', 'Quota') }}</div>
<div id="headerLanguages" class="languages"
v-if="showConfig.showLanguages">{{ t('settings', 'Language') }}</div>
<div class="headerStorageLocation storageLocation"
v-if="showConfig.showStoragePath">{{ t('settings', 'Storage location') }}</div>
<div class="headerUserBackend userBackend"
v-if="showConfig.showUserBackend">{{ t('settings', 'User backend') }}</div>
<div class="headerLastLogin lastLogin"
v-if="showConfig.showLastLogin">{{ t('settings', 'Last login') }}</div>
<div class="userActions"></div>
</div>

<form class="row" id="new-user" v-show="showConfig.showNewUserForm"
v-on:submit.prevent="createUser" :disabled="loading.all"
:class="{'sticky': scrolled && showConfig.showNewUserForm}">
<div :class="loading.all?'icon-loading-small':'icon-add'"></div>
<div class="name">
<input id="newusername" type="text" required v-model="newUser.id"
:placeholder="this.settings.newUserGenerateUserID
? t('settings', 'Will be autogenerated')
: t('settings', 'Username')"
name="username" autocomplete="off" autocapitalize="none"
autocorrect="off" ref="newusername" pattern="[a-zA-Z0-9 _\.@\-']+"
:disabled="this.settings.newUserGenerateUserID">
</div>
<div class="displayName">
<input id="newdisplayname" type="text" v-model="newUser.displayName"
:placeholder="t('settings', 'Display name')" name="displayname"
autocomplete="off" autocapitalize="none" autocorrect="off">
</div>
<div class="password">
<input id="newuserpassword" type="password" v-model="newUser.password"
:required="newUser.mailAddress===''" ref="newuserpassword"
:placeholder="t('settings', 'Password')" name="password"
autocomplete="new-password" autocapitalize="none" autocorrect="off"
:minlength="minPasswordLength">
</div>
<div class="mailAddress">
<input id="newemail" type="email" v-model="newUser.mailAddress"
:required="newUser.password==='' || this.settings.newUserRequireEmail"
:placeholder="t('settings', 'Email')" name="email"
autocomplete="off" autocapitalize="none" autocorrect="off">
</div>
<div class="groups">
<!-- hidden input trick for vanilla html5 form validation -->
<input type="text" :value="newUser.groups" v-if="!settings.isAdmin"
tabindex="-1" id="newgroups" :required="!settings.isAdmin"
:class="{'icon-loading-small': loading.groups}"/>
<multiselect v-model="newUser.groups" :options="canAddGroups" :disabled="loading.groups||loading.all"
tag-placeholder="create" :placeholder="t('settings', 'Add user in group')"
label="name" track-by="id" class="multiselect-vue"
:multiple="true" :taggable="true" :close-on-select="false"
:tag-width="60" @tag="createGroup">
<!-- If user is not admin, he is a subadmin.
Subadmins can't create users outside their groups
Therefore, empty select is forbidden -->
<span slot="noResult">{{t('settings', 'No results')}}</span>
</multiselect>
</div>
<div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin">
<multiselect :options="subAdminsGroups" v-model="newUser.subAdminsGroups"
:placeholder="t('settings', 'Set user as admin for')"
label="name" track-by="id" class="multiselect-vue"
:multiple="true" :close-on-select="false" :tag-width="60">
<span slot="noResult">{{t('settings', 'No results')}}</span>
</multiselect>
</div>
<div class="quota">
<multiselect :options="quotaOptions" v-model="newUser.quota"
:placeholder="t('settings', 'Select user quota')"
label="label" track-by="id" class="multiselect-vue"
:allowEmpty="false" :taggable="true"
@tag="validateQuota" >
</multiselect>
</div>
<div class="languages" v-if="showConfig.showLanguages">
<multiselect :options="languages" v-model="newUser.language"
:placeholder="t('settings', 'Default language')"
label="name" track-by="code" class="multiselect-vue"
:allowEmpty="false" group-values="languages" group-label="label">
</multiselect>
</div>
<div class="storageLocation" v-if="showConfig.showStoragePath"></div>
<div class="userBackend" v-if="showConfig.showUserBackend"></div>
<div class="lastLogin" v-if="showConfig.showLastLogin"></div>
<div class="userActions">
<input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip"
value="" :title="t('settings', 'Add a new user')">
</div>
</form>

<user-row v-for="(user, key) in filteredUsers" :user="user" :key="key" :settings="settings" :showConfig="showConfig"
:groups="groups" :subAdminsGroups="subAdminsGroups" :quotaOptions="quotaOptions" :languages="languages"
:externalActions="externalActions" />
<infinite-loading @infinite="infiniteHandler" ref="infiniteLoading">
<div slot="spinner"><div class="users-icon-loading icon-loading"></div></div>
<div slot="no-more"><div class="users-list-end"></div></div>
<div slot="no-results">
<div id="emptycontent">
<div class="icon-contacts-dark"></div>
<h2>{{t('settings', 'No users in here')}}</h2>
</div>
</div>
</infinite-loading>
</div>
</template>

<script>
import userRow from './userList/userRow';
import { Multiselect } from 'nextcloud-vue'
import InfiniteLoading from 'vue-infinite-loading';
import Vue from 'vue';

const unlimitedQuota = {
id: 'none',
label: t('settings', 'Unlimited')
}
const defaultQuota = {
id: 'default',
label: t('settings', 'Default quota')
}
const newUser = {
id: '',
displayName: '',
password: '',
mailAddress: '',
groups: [],
subAdminsGroups: [],
quota: defaultQuota,
language: {
code: 'en',
name: t('settings', 'Default language')
}
}

export default {
name: 'userList',
props: ['users', 'showConfig', 'selectedGroup', 'externalActions'],
components: {
userRow,
Multiselect,
InfiniteLoading
},
data() {
return {
unlimitedQuota,
defaultQuota,
loading: {
all: false,
groups: false
},
scrolled: false,
searchQuery: '',
newUser: Object.assign({}, newUser)
};
},
mounted() {
if (!this.settings.canChangePassword) {
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'));
}

/**
* Reset and init new user form
*/
this.resetForm()

/**
* Register search
*/
this.userSearch = new OCA.Search(this.search, this.resetSearch);
},
computed: {
settings() {
return this.$store.getters.getServerData;
},
filteredUsers() {
if (this.selectedGroup === 'disabled') {
let disabledUsers = this.users.filter(user => user.enabled === false);
if (disabledUsers.length === 0 && this.$refs.infiniteLoading && this.$refs.infiniteLoading.isComplete) {
// disabled group is empty, redirection to all users
this.$router.push({ name: 'users' });
this.$refs.infiniteLoading.stateChanger.reset()
}
return disabledUsers;
}
if (!this.settings.isAdmin) {
// we don't want subadmins to edit themselves
return this.users.filter(user => user.enabled !== false && user.id !== OC.getCurrentUser().uid);
}
return this.users.filter(user => user.enabled !== false);
},
groups() {
// data provided php side + remove the disabled group
return this.$store.getters.getGroups
.filter(group => group.id !== 'disabled')
.sort((a, b) => a.name.localeCompare(b.name));
},
canAddGroups() {
// disabled if no permission to add new users to group
return this.groups.map(group => {
// clone object because we don't want
// to edit the original groups
group = Object.assign({}, group);
group.$isDisabled = group.canAdd === false;
return group;
});
},
subAdminsGroups() {
// data provided php side
return this.$store.getters.getSubadminGroups;
},
quotaOptions() {
// convert the preset array into objects
let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({id: cur, label: cur}), []);
// add default presets
quotaPreset.unshift(this.unlimitedQuota);
quotaPreset.unshift(this.defaultQuota);
return quotaPreset;
},
minPasswordLength() {
return this.$store.getters.getPasswordPolicyMinLength;
},
usersOffset() {
return this.$store.getters.getUsersOffset;
},
usersLimit() {
return this.$store.getters.getUsersLimit;
},
usersCount() {
return this.users.length
},

/* LANGUAGES */
languages() {
return Array(
{
label: t('settings', 'Common languages'),
languages: this.settings.languages.commonlanguages
},
{
label: t('settings', 'All languages'),
languages: this.settings.languages.languages
}
);
}
},
watch: {
// watch url change and group select
selectedGroup: function (val, old) {
this.$store.commit('resetUsers');
this.$refs.infiniteLoading.stateChanger.reset()
this.setNewUserDefaultGroup(val);
},

// make sure the infiniteLoading state is changed if we manually
// add/remove data from the store
usersCount: function(val, old) {
// deleting the last user, reset the list
if (val === 0 && old === 1) {
this.$refs.infiniteLoading.stateChanger.reset()
// adding the first user, warn the infiniteLoader that
// the list is not empty anymore (we don't fetch the newly
// added user as we already have all the info we need)
} else if (val === 1 && old === 0) {
this.$refs.infiniteLoading.stateChanger.loaded()
}
}
},
methods: {
onScroll(event) {
this.scrolled = event.target.scrollTo > 0;
},

/**
* Validate quota string to make sure it's a valid human file size
*
* @param {string} quota Quota in readable format '5 GB'
* @returns {Object}
*/
validateQuota(quota) {
// only used for new presets sent through @Tag
let validQuota = OC.Util.computerFileSize(quota);
if (validQuota !== null && validQuota >= 0) {
// unify format output
quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota));
return this.newUser.quota = {id: quota, label: quota};
}
// Default is unlimited
return this.newUser.quota = this.quotaOptions[0];
},

infiniteHandler($state) {
this.$store.dispatch('getUsers', {
offset: this.usersOffset,
limit: this.usersLimit,
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
search: this.searchQuery
})
.then((response) => { response ? $state.loaded() : $state.complete() });
},

/* SEARCH */
search(query) {
this.searchQuery = query;
this.$store.commit('resetUsers');
this.$refs.infiniteLoading.stateChanger.reset()
},
resetSearch() {
this.search('');
},

resetForm() {
// revert form to original state
this.newUser = Object.assign({}, newUser);

/**
* Init default language from server data. The use of this.settings
* requires a computed variable, which break the v-model binding of the form,
* this is a much easier solution than getter and setter on a computed var
*/
if (this.settings.defaultLanguage) {
Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage);
}

/**
* In case the user directly loaded the user list within a group
* the watch won't be triggered. We need to initialize it.
*/
this.setNewUserDefaultGroup(this.selectedGroup);

this.loading.all = false;
},
createUser() {
this.loading.all = true;
this.$store.dispatch('addUser', {
userid: this.newUser.id,
password: this.newUser.password,
displayName: this.newUser.displayName,
email: this.newUser.mailAddress,
groups: this.newUser.groups.map(group => group.id),
subadmin: this.newUser.subAdminsGroups.map(group => group.id),
quota: this.newUser.quota.id,
language: this.newUser.language.code,
})
.then(() => {
this.resetForm()
this.$refs.newusername.focus();
})
.catch((error) => {
this.loading.all = false;
if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
const statuscode = error.response.data.ocs.meta.statuscode
if (statuscode === 102) {
// wrong username
this.$refs.newusername.focus();
} else if (statuscode === 107) {
// wrong password
this.$refs.newuserpassword.focus();
}
}
});
},
setNewUserDefaultGroup(value) {
if (value && value.length > 0) {
// setting new user default group to the current selected one
let currentGroup = this.groups.find(group => group.id === value);
if (currentGroup) {
this.newUser.groups = [currentGroup];
return;
}
}
// fallback, empty selected group
this.newUser.groups = [];
},

/**
* Create a new group
*
* @param {string} groups Group id
* @returns {Promise}
*/
createGroup(gid) {
this.loading.groups = true;
this.$store.dispatch('addGroup', gid)
.then((group) => {
this.newUser.groups.push(this.groups.find(group => group.id === gid))
this.loading.groups = false;
})
.catch(() => {
this.loading.groups = false;
});
return this.$store.getters.getGroups[this.groups.length];
}
}
}
</script>

Loading…
Отказ
Запис