diff options
-rw-r--r-- | settings/css/settings.scss | 19 | ||||
-rw-r--r-- | settings/src/components/appDetails.vue | 242 | ||||
-rw-r--r-- | settings/src/components/appList.vue | 68 | ||||
-rw-r--r-- | settings/src/components/appList/appItem.vue | 66 | ||||
-rw-r--r-- | settings/src/components/appList/appScore.vue | 38 | ||||
-rw-r--r-- | settings/src/store/api.js | 2 | ||||
-rw-r--r-- | settings/src/store/apps.js | 81 | ||||
-rw-r--r-- | settings/src/views/Apps.vue | 29 |
8 files changed, 347 insertions, 198 deletions
diff --git a/settings/css/settings.scss b/settings/css/settings.scss index b200a4932cb..14e502551f7 100644 --- a/settings/css/settings.scss +++ b/settings/css/settings.scss @@ -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; diff --git a/settings/src/components/appDetails.vue b/settings/src/components/appDetails.vue index d007555dd5b..9192c68a868 100644 --- a/settings/src/components/appDetails.vue +++ b/settings/src/components/appDetails.vue @@ -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> diff --git a/settings/src/components/appList.vue b/settings/src/components/appList.vue index b446b9c8a61..08809cf9b2b 100644 --- a/settings/src/components/appList.vue +++ b/settings/src/components/appList.vue @@ -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) { diff --git a/settings/src/components/appList/appItem.vue b/settings/src/components/appList/appItem.vue index d72f69ff267..4b65aed0ca8 100644 --- a/settings/src/components/appList/appItem.vue +++ b/settings/src/components/appList/appItem.vue @@ -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', { diff --git a/settings/src/components/appList/appScore.vue b/settings/src/components/appList/appScore.vue index e69de29bb2d..8b099f5e3d9 100644 --- a/settings/src/components/appList/appScore.vue +++ b/settings/src/components/appList/appScore.vue @@ -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>
\ No newline at end of file diff --git a/settings/src/store/api.js b/settings/src/store/api.js index 059c9a11ef8..7501a7bb4cc 100644 --- a/settings/src/store/api.js +++ b/settings/src/store/api.js @@ -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)); diff --git a/settings/src/store/apps.js b/settings/src/store/apps.js index d79540ad525..cc859c96845 100644 --- a/settings/src/store/apps.js +++ b/settings/src/store/apps.js @@ -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) => { diff --git a/settings/src/views/Apps.vue b/settings/src/views/Apps.vue index 17b54415979..7dbf9e887d8 100644 --- a/settings/src/views/Apps.vue +++ b/settings/src/views/Apps.vue @@ -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: { |