Signed-off-by: Julius Härtl <jus@bitgrid.net>tags/v14.0.0beta1
@@ -756,6 +756,23 @@ span.version { | |||
opacity: .5; | |||
} | |||
.app-settings-content { | |||
#searchresults { | |||
display: none; | |||
} | |||
} | |||
#apps-list.store { | |||
.app-name { | |||
display: block; | |||
} | |||
} | |||
#app-sidebar #app-details-view { | |||
.app-description p { | |||
margin-bottom: 10px; | |||
} | |||
} | |||
@media (min-width: 1601px) { | |||
#apps-list .section { | |||
width: 22%; | |||
@@ -946,8 +963,6 @@ span.version { | |||
height: 64px; | |||
opacity: .1; | |||
} | |||
position: relative; | |||
height: 100%; | |||
display: flex; | |||
flex-wrap: wrap; | |||
align-content: flex-start; |
@@ -21,167 +21,123 @@ | |||
--> | |||
<template> | |||
<div id="app-sidebar"> | |||
<div id="app-details-view" style="padding: 20px;"> | |||
<h2>{{ app.name }}</h2> | |||
<img :src="app.preview" width="100%" /> | |||
<app-score v-if="app.ratingNumThresholdReached" :score="app.score"></app-score> | |||
<div class="app-author"> | |||
{{ author }} | |||
{{ licence }} | |||
</div> | |||
<div class="actions"> | |||
<div class="warning hidden"></div> | |||
<input v-if="app.update" class="update" type="button" :value="t('settings', 'Update to %s', app.update)" /> | |||
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" /> | |||
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" /> | |||
<input v-if="!app.active && !app.needsDownload" class="enable" type="button" :value="t('settings','Enable')" v-on:click="enable(app.id)" :disabled="!app.canInstall" /> | |||
<input v-if="!app.active && app.needsDownload" class="enable needs-download" type="button" :value="t('settings', 'Enable')" :disabled="!app.canInstall"/> | |||
</div> | |||
<p class="documentation"> | |||
<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.user" :href="app.documentation.user" target="_blank" rel="noreferrer noopener">{{ t('settings', 'User documentation') }} ↗</a> | |||
<a class="appslink" v-if="app.documentation.admin" :href="app.documentation.admin" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} ↗</a> | |||
<a class="appslink" v-if="app.documentation.developer" :href="app.documentation.developer" target="_blank" rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a> | |||
</p> | |||
<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> | |||
<div class="app-description" v-html="renderMarkdown"></div> | |||
</div> | |||
</template> | |||
<script> | |||
import userRow from './userList/userRow'; | |||
import Multiselect from 'vue-multiselect'; | |||
import InfiniteLoading from 'vue-infinite-loading'; | |||
import Vue from 'vue'; | |||
import AppScore from './appList/appScore'; | |||
export default { | |||
name: 'userList', | |||
props: ['users', 'showConfig', 'selectedGroup'], | |||
name: 'appDetails', | |||
props: ['app'], | |||
components: { | |||
userRow, | |||
Multiselect, | |||
InfiniteLoading | |||
}, | |||
data() { | |||
let unlimitedQuota = {id:'none', label:t('settings', 'Unlimited')}, | |||
defaultQuota = {id:'default', label:t('settings', 'Default quota')}; | |||
return { | |||
unlimitedQuota: unlimitedQuota, | |||
defaultQuota: defaultQuota, | |||
loading: false, | |||
scrolled: false, | |||
newUser: { | |||
id:'', | |||
displayName:'', | |||
password:'', | |||
mailAddress:'', | |||
groups: [], | |||
subAdminsGroups: [], | |||
quota: defaultQuota, | |||
language: {code: 'en', name: t('settings', 'Default language')} | |||
} | |||
}; | |||
}, | |||
mounted() { | |||
if (!this.settings.canChangePassword) { | |||
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled')); | |||
} | |||
/** | |||
* Init default language from server data. The use of this.settings | |||
* requires a computed variable,vwhich break the v-model binding of the form, | |||
* this is a much easier solution than getter and setter | |||
*/ | |||
Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage); | |||
AppScore | |||
}, | |||
computed: { | |||
settings() { | |||
return this.$store.getters.getServerData; | |||
}, | |||
filteredUsers() { | |||
if (this.selectedGroup === 'disabled') { | |||
let disabledUsers = this.users.filter(user => user.enabled !== true); | |||
if (disabledUsers.length===0 && this.$refs.infiniteLoading && this.$refs.infiniteLoading.isComplete) { | |||
// disabled group is empty, redirection to all users | |||
this.$router.push('users'); | |||
this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset'); | |||
} | |||
return disabledUsers; | |||
} | |||
return this.users.filter(user => user.enabled === true); | |||
}, | |||
groups() { | |||
// data provided php side + remove the disabled group | |||
return this.$store.getters.getGroups.filter(group => group.id !== 'disabled'); | |||
}, | |||
subAdminsGroups() { | |||
// data provided php side | |||
return this.$store.getters.getServerData.subadmingroups; | |||
}, | |||
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; | |||
licence() { | |||
return this.app.license + t('settings', '-licensed'); | |||
}, | |||
minPasswordLength() { | |||
return this.$store.getters.getPasswordPolicyMinLength; | |||
}, | |||
usersOffset() { | |||
return this.$store.getters.getUsersOffset; | |||
}, | |||
usersLimit() { | |||
return this.$store.getters.getUsersLimit; | |||
author() { | |||
return t('settings', 'by') + ' ' + this.app.author; | |||
}, | |||
renderMarkdown() { | |||
// TODO: bundle marked as well | |||
var renderer = new window.marked.Renderer(); | |||
renderer.link = function(href, title, text) { | |||
try { | |||
var prot = decodeURIComponent(unescape(href)) | |||
.replace(/[^\w:]/g, '') | |||
.toLowerCase(); | |||
} catch (e) { | |||
return ''; | |||
} | |||
/* LANGUAGES */ | |||
languages() { | |||
return Array( | |||
{ | |||
label: t('settings', 'Common languages'), | |||
languages: this.settings.languages.commonlanguages | |||
}, | |||
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( | |||
window.marked(this.app.description.trim(), { | |||
renderer: renderer, | |||
gfm: false, | |||
highlight: false, | |||
tables: false, | |||
breaks: false, | |||
pedantic: false, | |||
sanitize: true, | |||
smartLists: true, | |||
smartypants: false | |||
}), | |||
{ | |||
label: t('settings', 'All languages'), | |||
languages: this.settings.languages.languages | |||
SAFE_FOR_JQUERY: true, | |||
ALLOWED_TAGS: [ | |||
'strong', | |||
'p', | |||
'a', | |||
'ul', | |||
'ol', | |||
'li', | |||
'em', | |||
'del', | |||
'blockquote' | |||
] | |||
} | |||
); | |||
} | |||
}, | |||
watch: { | |||
// watch url change and group select | |||
selectedGroup: function (val, old) { | |||
this.$store.commit('resetUsers'); | |||
this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset'); | |||
} | |||
}, | |||
methods: { | |||
onScroll(event) { | |||
this.scrolled = event.target.scrollTop>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 : ''}) | |||
.then((response) => {response?$state.loaded():$state.complete()}); | |||
}, | |||
resetForm() { | |||
// revert form to original state | |||
Object.assign(this.newUser, this.$options.data.call(this).newUser); | |||
this.loading = false; | |||
}, | |||
createUser() { | |||
this.loading = true; | |||
this.$store.dispatch('addUser', { | |||
userid: this.newUser.id, | |||
password: this.newUser.password, | |||
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()); | |||
} | |||
} | |||
} | |||
</script> |
@@ -21,19 +21,43 @@ | |||
--> | |||
<template> | |||
<div id="app-content" :class="{ 'with-app-sidebar': app }"> | |||
<div id="apps-list" class="installed"> | |||
<div class="apps-header" v-if="category === 'app-bundles'"> | |||
<div class="app-image"></div> | |||
<h2>Firmen-Paket <input class="enable" type="submit" data-bundleid="EnterpriseBundle" data-active="true" value="Alle aktivieren"></h2> | |||
<div class="app-version"></div> | |||
<div class="app-level"></div> | |||
<div class="app-groups"></div> | |||
<div class="actions"> </div> | |||
<div id="app-content" class="app-settings-content" :class="{ 'with-app-sidebar': app }"> | |||
<div id="apps-list" class="installed" v-if="useListView"> | |||
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" /> | |||
</div> | |||
<div id="apps-list" class="installed" v-if="useBundleView"> | |||
<template v-for="app in apps"> | |||
<div class="apps-header" v-if="app.newCategory"> | |||
<div class="app-image"></div> | |||
<h2>{{ app.categoryName }} <input class="enable" type="button" value="Alle aktivieren"></h2> | |||
<div class="app-version"></div> | |||
<div class="app-level"></div> | |||
<div class="app-groups"></div> | |||
<div class="actions"> </div> | |||
</div> | |||
<app-item v-else :key="app.id" :app="app" :category="category"/> | |||
</template> | |||
</div> | |||
<div id="apps-list" class="store" v-if="useAppStoreView"> | |||
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" :list-view="false" /> | |||
</div> | |||
<div id="apps-list" class="installed" v-if="search !== ''"> | |||
<div> | |||
<div></div> | |||
<h2>{{ t('settings', 'Results from other categories') }}</h2> | |||
</div> | |||
<app-item v-for="app in searchApps" :key="app.id" :app="app" :category="category" :list-view="true" /> | |||
</div> | |||
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" /> | |||
<div id="apps-list-empty" class="emptycontent emptycontent-search" v-if="apps.length == 0 && loading"> | |||
<div id="app-list-empty-icon" class="icon-search"></div> | |||
<h2>{{ t('settings', 'No apps found for your versoin')}}</h2> | |||
</div> | |||
<div id="searchresults"></div> | |||
</div> | |||
</template> | |||
@@ -43,7 +67,7 @@ import Multiselect from 'vue-multiselect'; | |||
export default { | |||
name: 'appList', | |||
props: ['category', 'app'], | |||
props: ['category', 'app', 'search'], | |||
components: { | |||
Multiselect, | |||
appItem | |||
@@ -65,14 +89,28 @@ export default { | |||
}, | |||
computed: { | |||
apps() { | |||
return this.$store.getters.getApps; | |||
return this.$store.getters.getApps | |||
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) | |||
}, | |||
searchApps() { | |||
return this.$store.getters.getAllApps | |||
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) | |||
}, | |||
groups() { | |||
console.log(this.$store.getters.getGroups); | |||
return this.$store.getters.getGroups; | |||
/*.filter(group => group.id !== 'disabled') | |||
.sort((a, b) => a.name.localeCompare(b.name));*/ | |||
return this.$store.getters.getGroups | |||
.filter(group => group.id !== 'disabled') | |||
.sort((a, b) => a.name.localeCompare(b.name)); | |||
}, | |||
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'); | |||
} | |||
}, | |||
methods: { | |||
prefix(prefix, content) { |
@@ -21,23 +21,26 @@ | |||
--> | |||
<template> | |||
<div class="section" v-on:click="showAppDetails"> | |||
<div class="app-image app-image-icon"> | |||
<svg width="32" height="32" viewBox="0 0 32 32"> | |||
<div class="section"> | |||
<div class="app-image app-image-icon" v-on:click="showAppDetails"> | |||
<img :src="app.preview" v-if="!app.previewAsIcon" width="100%" /> | |||
<svg v-else width="32" height="32" viewBox="0 0 32 32" v-if="app.previewAsIcon"> | |||
<defs><filter id="invertIconApps-606"><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="url(#invertIconApps-606)" xlink:href="/core/img/places/default-app-icon.svg?v=13.0.2.1" class="app-icon"></image> | |||
<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invertIconApps-606)" :xlink:href="app.preview" class="app-icon"></image> | |||
</svg> | |||
</div> | |||
<div class="app-name"> | |||
<div class="app-name" v-on:click="showAppDetails"> | |||
{{ app.name }} | |||
</div> | |||
<div class="app-version">{{ app.version }}</div> | |||
<div class="app-score" v-if="!listView"><app-score :score="app.score"></app-score> </div> | |||
<div class="app-summary" v-if="!listView">{{ app.summary }}</div> | |||
<div class="app-level"> | |||
<span class="official icon-checkmark" v-if="app.level === 200">{{ t('settings', 'Official') }}</span> | |||
<a :href="app.appstoreUrl" v-if="!app.internal">Im Store anzeigen ↗</a> | |||
<a :href="appstoreUrl" v-if="!app.internal && listView">Im Store anzeigen ↗</a> | |||
</div> | |||
<div class="app-groups"> | |||
<div class="app-groups" v-if="listView"> | |||
<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)">Auf Gruppen beschränken</label> | |||
@@ -53,26 +56,31 @@ | |||
<div class="actions"> | |||
<div class="warning hidden"></div> | |||
<input v-if="app.update" class="update" type="button" :value="t('settings', 'Update to %s', app.update)" :data-appid="app.id" /> | |||
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" :data-appid="app.id" /> | |||
<input v-if="app.active" class="enable" type="button" :data-appid="app.id" data-active="true" :value="t('settings','Disable')" v-on:click="disable(app.id)" /> | |||
<input v-if="!app.active && !app.needsDownload" class="enable" type="button" :data-appid="app.id" :value="t('settings','Enable')" v-on:click="enable(app.id)" /> | |||
<!--if !canInstall -> disable the enable button {{#unless canInstall}}disabled="disabled"{{/unless}} //--> | |||
<input v-if="!app.active && app.needsDownload" class="enable needs-download" type="button" :data-appid="app.id" :value="t('settings', 'Enable')"/> | |||
<!-- <php /* data-active="false" {{#unless canInstall}}disabled="disabled"{{/unless}} */ ?>//--> | |||
<input v-if="app.update" class="update" type="button" :value="t('settings', 'Update to %s', app.update)" v-on:click="update(app.id)" /> | |||
<input v-if="app.canUnInstall" class="uninstall" type="button" :value="t('settings', 'Remove')" v-on:click="remove(app.id)" /> | |||
<input v-if="app.active" class="enable" type="button" :value="t('settings','Disable')" v-on:click="disable(app.id)" /> | |||
<input v-if="!app.active" class="enable" type="button" :value="enableButtonText" v-on:click="enable(app.id)" :disabled="!app.canInstall" /> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import Multiselect from 'vue-multiselect'; | |||
import AppScore from './appScore'; | |||
export default { | |||
name: 'appItem', | |||
props: ['app', 'category'], | |||
props: { | |||
app: {}, | |||
category: {}, | |||
listView: { | |||
type: Boolean, | |||
default: true, | |||
} | |||
}, | |||
components: { | |||
Multiselect, | |||
AppScore, | |||
}, | |||
data() { | |||
return { | |||
@@ -87,15 +95,23 @@ | |||
} | |||
}, | |||
computed: { | |||
appstoreUrl() { | |||
return `https://apps.nextcloud.com/apps/${this.app.id}`; | |||
}, | |||
appGroups() { | |||
return this.app.groups.map(group => {return {id: group, name: group}}); | |||
}, | |||
groups() { | |||
console.log(this.$store.getters.getGroups); | |||
return this.$store.getters.getGroups | |||
.filter(group => group.id !== 'disabled') | |||
.sort((a, b) => a.name.localeCompare(b.name)); | |||
}, | |||
enableButtonText() { | |||
if (this.app.needsDownload) { | |||
return t('settings','Download and enable'); | |||
} | |||
return t('settings','Enable'); | |||
} | |||
}, | |||
watchers: { | |||
@@ -145,15 +161,25 @@ | |||
this.$store.dispatch('enableApp', { appId: this.app.id, groups: currentGroups}); | |||
}, | |||
enable(appId) { | |||
this.$store.dispatch('enableApp', { appId: appId }) | |||
this.$store.dispatch('enableApp', { appId: appId, groups: [] }) | |||
.catch((error) => { OC.Notification.show(error)}); | |||
}, | |||
disable(appId) { | |||
this.$store.dispatch('disableApp', { appId: appId }) | |||
.catch((error) => { OC.Notification.show(error)}); | |||
}, | |||
remove() {}, | |||
install() {}, | |||
remove(appId) { | |||
this.$store.dispatch('uninstallApp', { appId: appId }) | |||
.catch((error) => { OC.Notification.show(error)}); | |||
}, | |||
install(appId) { | |||
this.$store.dispatch('installApp', { appId: appId }) | |||
.catch((error) => { OC.Notification.show(error)}); | |||
}, | |||
update(appId) { | |||
this.$store.dispatch('updateApp', { appId: appId }) | |||
.catch((error) => { OC.Notification.show(error)}); | |||
}, | |||
createUser() { | |||
this.loading = true; | |||
this.$store.dispatch('addUser', { |
@@ -0,0 +1,38 @@ | |||
<!-- | |||
- @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" /> | |||
</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> |
@@ -71,7 +71,7 @@ export default { | |||
waitForpassword(); | |||
}); | |||
}, | |||
get(url, data) { | |||
get(url) { | |||
return axios.get(sanitize(url), tokenHeaders) | |||
.then((response) => Promise.resolve(response)) | |||
.catch((error) => Promise.reject(error)); |
@@ -25,11 +25,18 @@ import axios from 'axios/index'; | |||
const state = { | |||
apps: [], | |||
allApps: [], | |||
categories: [], | |||
updateCount: 0 | |||
}; | |||
const mutations = { | |||
APPS_API_FAILURE(state, error) { | |||
OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+error.error.response.data.data.message, {timeout: 7}); | |||
console.log(state, error); | |||
}, | |||
initCategories(state, {categories, updateCount}) { | |||
state.categories = categories; | |||
state.updateCount = updateCount; | |||
@@ -52,16 +59,36 @@ const mutations = { | |||
state.apps = apps; | |||
}, | |||
setAllApps(state, apps) { | |||
state.allApps = apps; | |||
}, | |||
enableApp(state, {appId, groups}) { | |||
state.apps.find(app => app.id === appId).active = true; | |||
state.apps.find(app => app.id === appId).groups = groups; | |||
console.log(state.apps.find(app => app.id === appId).groups); | |||
let app = state.apps.find(app => app.id === appId); | |||
app.active = true; | |||
app.groups = groups; | |||
}, | |||
disableApp(state, appId) { | |||
let app = state.apps.find(app => app.id === appId); | |||
app.active = false; | |||
app.groups = []; | |||
if (app.removable) { | |||
app.canUnInstall = true; | |||
} | |||
}, | |||
uninstallApp(state, appId) { | |||
state.apps.find(app => app.id === appId).active = false; | |||
state.apps.find(app => app.id === appId).groups = []; | |||
state.apps.find(app => app.id === appId).needsDownload = true; | |||
state.apps.find(app => app.id === appId).canUnInstall = false; | |||
state.apps.find(app => app.id === appId).canInstall = true; | |||
}, | |||
resetApps(state) { | |||
state.apps = []; | |||
}, | |||
reset(state) { | |||
state.apps = []; | |||
state.categories = []; | |||
@@ -74,7 +101,18 @@ const getters = { | |||
return state.categories; | |||
}, | |||
getApps(state) { | |||
return state.apps; | |||
return state.apps.concat([]).sort(function (a, b) { | |||
if (a.active !== b.active) { | |||
return (a.active ? -1 : 1) | |||
} | |||
if (a.update !== b.update) { | |||
return (a.update ? -1 : 1) | |||
} | |||
return OC.Util.naturalSortCompare(a.name, b.name); | |||
}); | |||
}, | |||
getAllApps(state) { | |||
return state.allApps; | |||
}, | |||
getUpdateCount(state) { | |||
return state.updateCount; | |||
@@ -92,7 +130,7 @@ const actions = { | |||
context.commit('enableApp', {appId: appId, groups: groups}); | |||
return true; | |||
}) | |||
.catch((error) => {throw error;}) | |||
.catch((error) => context.commit('APPS_API_FAILURE', { appId, error })) | |||
}).catch((error) => context.commit('API_FAILURE', { appId, error })); | |||
}, | |||
@@ -103,14 +141,28 @@ const actions = { | |||
context.commit('disableApp', appId); | |||
return true; | |||
}) | |||
.catch((error) => {throw error;}) | |||
.catch((error) => context.commit('APPS_API_FAILURE', { appId, error })) | |||
}).catch((error) => context.commit('API_FAILURE', { appId, error })); | |||
}, | |||
installApp(appId) { | |||
installApp(context, { appId }) { | |||
return api.requireAdmin().then((response) => { | |||
return api.get(OC.generateUrl(`settings/apps/enable/${appId}`)) | |||
.then((response) => { | |||
context.commit('enableApp', appId); | |||
return true; | |||
}) | |||
.catch((error) => context.commit('APPS_API_FAILURE', { appId, error })) | |||
}).catch((error) => context.commit('API_FAILURE', { appId, error })); | |||
}, | |||
uninstallApp(appId) { | |||
uninstallApp(context, { appId }) { | |||
return api.requireAdmin().then((response) => { | |||
return api.get(OC.generateUrl(`settings/apps/uninstall/${appId}`)) | |||
.then((response) => { | |||
context.commit('uninstallApp', appId); | |||
return true; | |||
}) | |||
.catch((error) => context.commit('APPS_API_FAILURE', { appId, error })) | |||
}).catch((error) => context.commit('API_FAILURE', { appId, error })); | |||
}, | |||
getApps(context, { category }) { | |||
@@ -122,6 +174,15 @@ const actions = { | |||
.catch((error) => context.commit('API_FAILURE', error)) | |||
}, | |||
getAllApps(context) { | |||
return api.get(OC.generateUrl(`settings/apps/list`)) | |||
.then((response) => { | |||
context.commit('setAllApps', response.data.apps); | |||
return true; | |||
}) | |||
.catch((error) => context.commit('API_FAILURE', error)) | |||
}, | |||
getCategories(context) { | |||
return api.get(OC.generateUrl('settings/apps/categories')) | |||
.then((response) => { |
@@ -23,12 +23,10 @@ | |||
<template> | |||
<div id="app"> | |||
<app-navigation :menu="menu" /> | |||
<app-list :category="category" :app="currentApp"></app-list> | |||
<app-list :category="category" :app="currentApp" :search="search"></app-list> | |||
<div id="app-sidebar" v-if="currentApp"> | |||
{{ currentApp.name }} | |||
<div class="app-description"> | |||
{{ currentApp.description }} | |||
</div> | |||
{{ search }} | |||
<app-details :app="currentApp"></app-details> | |||
</div> | |||
</div> | |||
</template> | |||
@@ -41,6 +39,7 @@ import Vue from 'vue'; | |||
import VueLocalStorage from 'vue-localstorage' | |||
import Multiselect from 'vue-multiselect'; | |||
import api from '../store/api'; | |||
import AppDetails from '../components/appDetails'; | |||
Vue.use(VueLocalStorage) | |||
Vue.use(VueLocalStorage) | |||
@@ -58,17 +57,33 @@ export default { | |||
} | |||
}, | |||
components: { | |||
AppDetails, | |||
appNavigation, | |||
appList, | |||
}, | |||
methods: { | |||
setSearch(search) { | |||
this.search = search; | |||
} | |||
}, | |||
beforeMount() { | |||
this.$store.dispatch('getCategories'); | |||
this.$store.dispatch('getApps', { category: this.category }); | |||
this.$store.dispatch('getApps', {category: this.category}); | |||
this.$store.dispatch('getAllApps'); | |||
this.$store.commit('setUpdateCount', this.$store.getters.getServerData.updateCount) | |||
console.log(this.$store.getters.getServerData.updateCount); | |||
}, | |||
mounted() { | |||
// TODO: remove jQuery once we have a proper standardisation of the search | |||
$('#searchbox').show(); | |||
let self = this; | |||
$('#searchbox').change(function(e) { | |||
self.setSearch($('#searchbox').val()); | |||
}); | |||
}, | |||
data() { | |||
return { | |||
search: '' | |||
} | |||
}, | |||
watch: { |