mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
Merge pull request #486 from YunoHost/enh-appv2modal
[enh] app v2 changes for AppCatalog, AppInstall, AppInfo and SystemUpdate (apps)
This commit is contained in:
commit
335d69168c
15 changed files with 1219 additions and 403 deletions
82
app/src/components/CardCollapse.vue
Normal file
82
app/src/components/CardCollapse.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<b-card
|
||||
v-bind="$attrs"
|
||||
no-body :class="_class"
|
||||
>
|
||||
<slot name="header" slot="header">
|
||||
<h2>
|
||||
<b-button v-b-toggle="id" :variant="variant" class="card-collapse-button">
|
||||
{{ title }}
|
||||
<icon class="ml-auto" iname="chevron-right" />
|
||||
</b-button>
|
||||
</h2>
|
||||
</slot>
|
||||
|
||||
<b-collapse :id="id" :visible="visible" role="region">
|
||||
<slot name="default" />
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CollapseCard',
|
||||
|
||||
props: {
|
||||
id: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
variant: { type: String, default: 'white' },
|
||||
visible: { type: Boolean, default: false },
|
||||
flush: { type: Boolean, default: false }
|
||||
},
|
||||
|
||||
computed: {
|
||||
_class () {
|
||||
const baseClass = 'card-collapse'
|
||||
return [
|
||||
baseClass,
|
||||
{
|
||||
[`${baseClass}-flush`]: this.flush,
|
||||
[`${baseClass}-${this.variant}`]: this.variant
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-collapse {
|
||||
.card-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding-top: $spacer * .5;
|
||||
padding-bottom: $spacer * .5;
|
||||
border-radius: 0;
|
||||
font: inherit
|
||||
}
|
||||
|
||||
&-flush {
|
||||
border-radius: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
@each $color, $value in $theme-colors {
|
||||
&-#{$color} {
|
||||
background-color: $value;
|
||||
color: color-yiq($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
81
app/src/components/CardDeckFeed.vue
Normal file
81
app/src/components/CardDeckFeed.vue
Normal file
|
@ -0,0 +1,81 @@
|
|||
<script>
|
||||
// Implementation of the feed pattern
|
||||
// https://www.w3.org/WAI/ARIA/apg/patterns/feed/
|
||||
|
||||
export default {
|
||||
name: 'CardDeckFeed',
|
||||
|
||||
props: {
|
||||
stacks: { type: Number, default: 21 }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
busy: false,
|
||||
range: this.stacks,
|
||||
childrenCount: this.$slots.default.length
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getTopParent (prev) {
|
||||
return prev.parentElement === this.$refs.feed ? prev : this.getTopParent(prev.parentElement)
|
||||
},
|
||||
|
||||
onScroll () {
|
||||
const elem = this.$refs.feed
|
||||
if (window.innerHeight > elem.clientHeight + elem.getBoundingClientRect().top - 200) {
|
||||
this.busy = true
|
||||
this.range = Math.min(this.range + this.stacks, this.childrenCount)
|
||||
this.$nextTick().then(() => {
|
||||
this.busy = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onKeydown (e) {
|
||||
if (['PageUp', 'PageDown'].includes(e.code)) {
|
||||
e.preventDefault()
|
||||
const key = e.code === 'PageUp' ? 'previous' : 'next'
|
||||
const sibling = this.getTopParent(e.target)[`${key}ElementSibling`]
|
||||
if (sibling) {
|
||||
sibling.focus()
|
||||
sibling.scrollIntoView({ block: 'center' })
|
||||
}
|
||||
}
|
||||
// FIXME Add `Home` and `End` shorcuts
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
window.addEventListener('scroll', this.onScroll)
|
||||
this.$refs.feed.addEventListener('keydown', this.onKeydown)
|
||||
this.onScroll()
|
||||
},
|
||||
|
||||
beforeUpdate () {
|
||||
const slots = this.$slots.default
|
||||
if (this.childrenCount !== slots.length) {
|
||||
this.range = this.stacks
|
||||
this.childrenCount = slots.length
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
return h(
|
||||
'b-card-group',
|
||||
{
|
||||
attrs: { role: 'feed', 'aria-busy': this.busy.toString() },
|
||||
props: { deck: true },
|
||||
ref: 'feed'
|
||||
},
|
||||
this.$slots.default.slice(0, this.range)
|
||||
)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('scroll', this.onScroll)
|
||||
this.$refs.feed.removeEventListener('keydown', this.onKeydown)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -24,6 +24,12 @@ export default {
|
|||
min-width: 3rem;
|
||||
}
|
||||
|
||||
&.md {
|
||||
width: 1.25rem;
|
||||
font-size: 1.25rem;
|
||||
min-width: 1.25rem;
|
||||
}
|
||||
|
||||
&.fs-sm {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
|
35
app/src/components/globals/YunoAlert.vue
Normal file
35
app/src/components/globals/YunoAlert.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<component
|
||||
v-bind="$attrs"
|
||||
:is="alert ? 'b-alert' : 'div'"
|
||||
:variant="alert ? variant : null"
|
||||
:class="{ ['alert alert-' + variant]: !alert }"
|
||||
class="yuno-alert d-flex flex-column flex-md-row align-items-center"
|
||||
>
|
||||
<icon :iname="_icon" class="mr-md-3 mb-md-0 mb-2 md" />
|
||||
|
||||
<div class="w-100">
|
||||
<slot name="default" />
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DEFAULT_STATUS_ICON } from '@/helpers/yunohostArguments'
|
||||
|
||||
export default {
|
||||
name: 'YunoAlert',
|
||||
|
||||
props: {
|
||||
alert: { type: Boolean, default: false },
|
||||
variant: { type: String, default: 'info' },
|
||||
icon: { type: String, default: null }
|
||||
},
|
||||
|
||||
computed: {
|
||||
_icon () {
|
||||
return DEFAULT_STATUS_ICON[this.variant]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -262,8 +262,11 @@ export function formatYunoHostArgument (arg) {
|
|||
]
|
||||
|
||||
// Default type management if no one is filled
|
||||
if (arg.choices && arg.choices.length) {
|
||||
arg.type = 'select'
|
||||
}
|
||||
if (arg.type === undefined) {
|
||||
arg.type = arg.choices && arg.choices.length ? 'select' : 'string'
|
||||
arg.type = 'string'
|
||||
}
|
||||
|
||||
// Search the component bind to the type
|
||||
|
|
|
@ -54,10 +54,106 @@
|
|||
"api_not_found": "Seems like the web-admin tried to query something that doesn't exist.",
|
||||
"api_not_responding": "The YunoHost API is not responding. Maybe 'yunohost-api' is down or got restarted?",
|
||||
"api_waiting": "Waiting for the server's response...",
|
||||
"app": {
|
||||
"installed_version": "Installed version: {version}",
|
||||
"open_this_app": "Open this app",
|
||||
"antifeatures": "This app has features you may not like:",
|
||||
"doc": {
|
||||
"about": {
|
||||
"title": "About",
|
||||
"description": "Description"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin doc"
|
||||
},
|
||||
"notifications": {
|
||||
"dismiss": "Dismiss",
|
||||
"title": "Notifications",
|
||||
"post_upgrade": "Post-upgrade notes",
|
||||
"post_install": "Post-install notes"
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"forum": "Search or ask the forum!",
|
||||
"problem": "A problem with this app?"
|
||||
},
|
||||
"install": {
|
||||
"license": "License: {license}",
|
||||
"notifs": {
|
||||
"post": {
|
||||
"title": "Post-install notifications for '{name}'",
|
||||
"alert": "It seems that the installation went well!\n Here is some notifications that the packager considers important to know.\nYou can read it again in the app info page."
|
||||
},
|
||||
"pre": {
|
||||
"warning": "Things to know before installation",
|
||||
"danger": "The installation of the application will most likely lead to issues",
|
||||
"critical": "The application cannot be installed"
|
||||
}
|
||||
},
|
||||
"problems": {
|
||||
"arch": "This app can only be installed on specific architectures ({required}) but your server architecture is {current}.",
|
||||
"broken": "This application is broken according to YunoHost's automatic tests and it is likely to break your system! You should probably NOT install it unless you know what you are doing.",
|
||||
"thirdparty": "This application is not part of the official YunoHost catalog, installing 3rd party applications may compromise the integrity and security of your system. You should probably NOT install it unless you know what you are doing.",
|
||||
"ignore": "I understand that this installation may break my system but i still want to try.",
|
||||
"inprogress": "This application is still experimental (if not explicitly not working) and it is likely to break your system! You should probably NOT install it unless you know what you are doing.",
|
||||
"install": "It is already installed and can't be installed more than once.",
|
||||
"lowquality": "This application may work but is not well-integrated in YunoHost. Some features such as single sign-on and backup/restore might not be available, or it does not respect the good practices.",
|
||||
"ram": "This application requires {required} of RAM to install/upgrade but only {current} is available right now. Even if this app could run, its installation process requires a large amount of RAM so your server may freeze and fail miserably.",
|
||||
"version": "This application requires YunoHost >= {required} but your current installed version is {current}, consider first upgrading YunoHost."
|
||||
},
|
||||
"try_demo": "Try the demo",
|
||||
"version": "Current version: {version}"
|
||||
},
|
||||
"integration": {
|
||||
"archs": "Supported architectures:",
|
||||
"ldap": {
|
||||
"false": "Does not use YunoHost accounts to login (LDAP)",
|
||||
"true": "Use YunoHost accounts to login (LDAP)",
|
||||
"?": "No information about LDAP integration"
|
||||
},
|
||||
"multi_instance": {
|
||||
"false": "Can be installed only once",
|
||||
"true": "Can be installed several times"
|
||||
},
|
||||
"resources": "Typical resource usage: {ram} RAM, {disk} disk",
|
||||
"sso": {
|
||||
"false": "Single sign-on is not available (SSO)",
|
||||
"true": "Single sign-on is available (SSO)",
|
||||
"?": "No information about SSO integration"
|
||||
},
|
||||
"title": "YunoHost integration"
|
||||
},
|
||||
"links": {
|
||||
"admindoc": "Official Admin documentation",
|
||||
"code": "Official code repository",
|
||||
"forum": "Topics about this app on YunoHost's forum",
|
||||
"package": "YunoHost package repository",
|
||||
"title": "Links",
|
||||
"userdoc": "Official User documentation",
|
||||
"website": "Official Website",
|
||||
"license": "License"
|
||||
},
|
||||
"potential_alternative_to": "Potential alternative to:",
|
||||
"upgrade": {
|
||||
"confirm": {
|
||||
"apps": "Apps that will be upgraded",
|
||||
"title": "Confirm app upgrades"
|
||||
},
|
||||
"continue": "Continue to next app",
|
||||
"notifs": {
|
||||
"pre": {
|
||||
"alert": "You should check those notifications before upgrading, there might be important stuff to know.",
|
||||
"title": "Be warned!"
|
||||
},
|
||||
"post": {
|
||||
"alert": "It seems that the upgrade went well!\n Here is some notifications that the packager considers important to know about this upgrade.\nYou can read it again in the app info page.",
|
||||
"title": "Post-upgrade notifications for '{name}'"
|
||||
}
|
||||
},
|
||||
"stop": "Cancel next app upgrades"
|
||||
}
|
||||
},
|
||||
"app_choose_category": "Choose a category",
|
||||
"app_config_panel": "Config panel",
|
||||
"app_config_panel_label": "Configure this app",
|
||||
"app_config_panel_no_panel": "This application doesn't have any configuration available",
|
||||
"app_info_access_desc": "Groups / users currently allowed to access this app:",
|
||||
"app_info_change_url_disabled_tooltip": "This feature hasn't been implemented in this app yet",
|
||||
"app_info_changeurl_desc": "Change the access URL of this application (domain and/or path).",
|
||||
|
@ -118,6 +214,8 @@
|
|||
"confirm_service_start": "Are you sure you want to start {name}?",
|
||||
"confirm_service_stop": "Are you sure you want to stop {name}?",
|
||||
"confirm_uninstall": "Are you sure you want to uninstall {name}?",
|
||||
"confirm_update_system": "Are you sure you want to update all system packages?",
|
||||
"confirm_upnp_enable": "Are you sure you want to enable UPnP?",
|
||||
"confirm_update_apps": "Are you sure you want to update all applications?",
|
||||
"confirm_update_specific_app": "Are you sure you want to update {app}?",
|
||||
"confirm_update_system": "Are you sure you want to update all system packages?",
|
||||
|
@ -292,6 +390,7 @@
|
|||
"change_url": "Change access URL of '{name}'",
|
||||
"install": "Install app '{name}'",
|
||||
"set_default": "Redirect '{domain}' domain root to '{name}'",
|
||||
"dismiss_notification": "Dismiss notification for '{name}'",
|
||||
"uninstall": "Uninstall app '{name}'",
|
||||
"update_config": "Update panel '{id}' of app '{name}' configuration"
|
||||
},
|
||||
|
|
|
@ -39,6 +39,19 @@ Vue.prototype.$askConfirmation = function (message, props) {
|
|||
})
|
||||
}
|
||||
|
||||
Vue.prototype.$askMdConfirmation = function (markdown, props, ok = false) {
|
||||
const content = this.$createElement('vue-showdown', {
|
||||
props: { markdown, flavor: 'github', options: { headerLevelStart: 4 } }
|
||||
})
|
||||
return this.$bvModal['msgBox' + (ok ? 'Ok' : 'Confirm')](content, {
|
||||
okTitle: this.$i18n.t('yes'),
|
||||
cancelTitle: this.$i18n.t('cancel'),
|
||||
headerBgVariant: 'warning',
|
||||
headerClass: store.state.theme ? 'text-white' : 'text-black',
|
||||
centered: true,
|
||||
...props
|
||||
})
|
||||
}
|
||||
|
||||
// Register global components
|
||||
const requireComponent = require.context('@/components/globals', true, /\.(js|vue)$/i)
|
||||
|
|
|
@ -175,6 +175,7 @@ const routes = [
|
|||
name: 'app-catalog',
|
||||
path: '/apps/catalog',
|
||||
component: () => import(/* webpackChunkName: "views/apps/catalog" */ '@/views/app/AppCatalog'),
|
||||
props: route => route.query,
|
||||
meta: {
|
||||
args: { trad: 'catalog' },
|
||||
breadcrumb: ['app-list', 'app-catalog']
|
||||
|
@ -201,30 +202,19 @@ const routes = [
|
|||
}
|
||||
},
|
||||
{
|
||||
name: 'app-info',
|
||||
path: '/apps/:id',
|
||||
component: () => import(/* webpackChunkName: "views/apps/info" */ '@/views/app/AppInfo'),
|
||||
props: true,
|
||||
meta: {
|
||||
args: { param: 'id' },
|
||||
breadcrumb: ['app-list', 'app-info']
|
||||
}
|
||||
},
|
||||
{
|
||||
// no need for name here, only children are visited
|
||||
path: '/apps/:id/config-panel',
|
||||
component: () => import(/* webpackChunkName: "views/apps/config" */ '@/views/app/AppConfigPanel'),
|
||||
props: true,
|
||||
children: [
|
||||
{
|
||||
name: 'app-config-panel',
|
||||
name: 'app-info',
|
||||
path: ':tabId?',
|
||||
component: () => import(/* webpackChunkName: "components/configPanel" */ '@/components/ConfigPanel'),
|
||||
props: true,
|
||||
meta: {
|
||||
routerParams: ['id'],
|
||||
args: { trad: 'app_config_panel' },
|
||||
breadcrumb: ['app-list', 'app-info', 'app-config-panel']
|
||||
routerParams: ['id'], // Override router key params to avoid view recreation at tab change.
|
||||
args: { param: 'id' },
|
||||
breadcrumb: ['app-list', 'app-info']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -216,7 +216,7 @@ body {
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.card, .list-group-item {
|
||||
.card-header, .list-group-item {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
id="search-input" :placeholder="$t('search.for', { items: $tc('items.apps', 2) })"
|
||||
v-model="search" @input="setCategory"
|
||||
:value="search" @input="updateQuery('search', $event)"
|
||||
/>
|
||||
<b-input-group-append>
|
||||
<b-select v-model="quality" :options="qualityOptions" @change="setCategory" />
|
||||
<b-select :value="quality" :options="qualityOptions" @change="updateQuery('quality', $event)" />
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
|
@ -24,9 +24,9 @@
|
|||
<b-input-group-prepend is-text>
|
||||
<icon iname="filter" />
|
||||
</b-input-group-prepend>
|
||||
<b-select v-model="category" :options="categories" />
|
||||
<b-select :value="category" :options="categories" @change="updateQuery('category', $event)" />
|
||||
<b-input-group-append>
|
||||
<b-button variant="primary" :disabled="category === null" @click="category = null">
|
||||
<b-button variant="primary" :disabled="category === null" @click="updateQuery('category', null)">
|
||||
{{ $t('app_show_categories') }}
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
|
@ -39,83 +39,86 @@
|
|||
</b-input-group-prepend>
|
||||
<b-form-radio-group
|
||||
id="subtags-radio" name="subtags"
|
||||
v-model="subtag" :options="subtags"
|
||||
:checked="subtag" :options="subtags" @change="updateQuery('subtag', $event)"
|
||||
buttons button-variant="outline-secondary"
|
||||
/>
|
||||
<b-select id="subtags-select" v-model="subtag" :options="subtags" />
|
||||
<b-select
|
||||
id="subtags-select" :value="subtag" :options="subtags"
|
||||
@change="updateQuery('subtag', $event)"
|
||||
/>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- CATEGORIES CARDS -->
|
||||
<b-card-group v-if="category === null" deck>
|
||||
<b-card-group v-if="category === null" deck tag="ul">
|
||||
<b-card
|
||||
v-for="cat in categories.slice(1)" :key="cat.value"
|
||||
class="category-card" no-body
|
||||
tag="li" class="category-card"
|
||||
>
|
||||
<b-button variant="outline-dark" @click="category = cat.value">
|
||||
<b-card-title>
|
||||
<b-card-title>
|
||||
<b-link @click="updateQuery('category', cat.value)" class="card-link">
|
||||
<icon :iname="cat.icon" /> {{ cat.text }}
|
||||
</b-card-title>
|
||||
<b-card-text>{{ cat.description }}</b-card-text>
|
||||
</b-button>
|
||||
</b-link>
|
||||
</b-card-title>
|
||||
<b-card-text>{{ cat.description }}</b-card-text>
|
||||
</b-card>
|
||||
</b-card-group>
|
||||
|
||||
<!-- APPS CARDS -->
|
||||
<b-card-group v-else deck>
|
||||
<lazy-renderer v-for="app in filteredApps" :key="app.id" :min-height="120">
|
||||
<b-card no-body>
|
||||
<b-card-body class="d-flex flex-column">
|
||||
<b-card-title class="d-flex mb-2">
|
||||
<card-deck-feed v-else>
|
||||
<b-card
|
||||
v-for="(app, i) in filteredApps" :key="app.id"
|
||||
tag="article" :aria-labelledby="`${app.id}-title`" :aria-describedby="`${app.id}-desc`"
|
||||
tabindex="0" :aria-posinset="i + 1" :aria-setsize="filteredApps.length"
|
||||
no-body class="app-card"
|
||||
>
|
||||
<b-card-body>
|
||||
<b-img v-if="app.logo_hash" class="app-logo rounded" :src="`./applogos/${app.logo_hash}.png`" />
|
||||
|
||||
<b-card-title :id="`${app.id}-title`" class="d-flex mb-2">
|
||||
<b-link :to="{ name: 'app-install', params: { id: app.id }}" class="card-link">
|
||||
{{ app.manifest.name }}
|
||||
</b-link>
|
||||
|
||||
<small v-if="app.state !== 'working' || app.high_quality" class="d-flex align-items-center ml-2">
|
||||
<b-badge
|
||||
v-if="app.state !== 'working'"
|
||||
:variant="app.color"
|
||||
v-b-popover.hover.bottom="$t(`app_state_${app.state}_explanation`)"
|
||||
>
|
||||
<!-- app.state can be 'lowquality' or 'inprogress' -->
|
||||
{{ $t('app_state_' + app.state) }}
|
||||
</b-badge>
|
||||
<small v-if="app.state !== 'working' || app.high_quality" class="d-flex align-items-center ml-2 position-relative">
|
||||
<b-badge
|
||||
v-if="app.state !== 'working'"
|
||||
:variant="app.color"
|
||||
v-b-popover.hover.bottom="$t(`app_state_${app.state}_explanation`)"
|
||||
>
|
||||
<!-- app.state can be 'lowquality' or 'inprogress' -->
|
||||
{{ $t('app_state_' + app.state) }}
|
||||
</b-badge>
|
||||
|
||||
<icon
|
||||
v-if="app.high_quality" iname="star" class="star"
|
||||
v-b-popover.hover.bottom="$t(`app_state_highquality_explanation`)"
|
||||
/>
|
||||
</small>
|
||||
</b-card-title>
|
||||
<icon
|
||||
v-if="app.high_quality" iname="star" class="star"
|
||||
v-b-popover.hover.bottom="$t(`app_state_highquality_explanation`)"
|
||||
/>
|
||||
</small>
|
||||
</b-card-title>
|
||||
|
||||
<b-card-text>{{ app.manifest.description }}</b-card-text>
|
||||
<b-card-text :id="`${app.id}-desc`">
|
||||
{{ app.manifest.description }}
|
||||
</b-card-text>
|
||||
|
||||
<b-card-text v-if="!app.maintained" class="align-self-end mt-auto">
|
||||
<span class="alert-warning p-1" v-b-popover.hover.top="$t('orphaned_details')">
|
||||
<icon iname="warning" /> {{ $t('orphaned') }}
|
||||
</span>
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
<b-card-text v-if="!app.maintained" class="align-self-end position-relative mt-auto">
|
||||
<span class="alert-warning p-1" v-b-popover.hover.top="$t('orphaned_details')">
|
||||
<icon iname="warning" /> {{ $t('orphaned') }}
|
||||
</span>
|
||||
</b-card-text>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</card-deck-feed>
|
||||
|
||||
<!-- APP BUTTONS -->
|
||||
<b-button-group>
|
||||
<b-button :href="app.git.url" variant="outline-dark" target="_blank">
|
||||
<icon iname="code" /> {{ $t('code') }}
|
||||
</b-button>
|
||||
|
||||
<b-button :href="app.git.url + '/blob/master/README.md'" variant="outline-dark" target="_blank">
|
||||
<icon iname="book" /> {{ $t('readme') }}
|
||||
</b-button>
|
||||
|
||||
<b-button v-if="app.isInstallable" :variant="app.color" @click="onInstallClick(app)">
|
||||
<icon iname="plus" /> {{ $t('install') }} <icon v-if="app.color === 'danger'" class="ml-1" iname="warning" />
|
||||
</b-button>
|
||||
<b-button v-else :variant="app.color" disabled>
|
||||
{{ $t('installed') }}
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
</b-card>
|
||||
</lazy-renderer>
|
||||
</b-card-group>
|
||||
<app-catalog-details
|
||||
v-if="selectedApp"
|
||||
id="modal-app-info"
|
||||
:app-id="selectedApp"
|
||||
:antifeatures="antifeatures"
|
||||
@ok="onInstallClick(selectedApp)"
|
||||
@hide="selectedApp = undefined"
|
||||
/>
|
||||
|
||||
<template #bot>
|
||||
<!-- INSTALL CUSTOM APP -->
|
||||
|
@ -160,26 +163,34 @@
|
|||
<script>
|
||||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
import LazyRenderer from '@/components/LazyRenderer'
|
||||
import CardDeckFeed from '@/components/CardDeckFeed'
|
||||
import { required, appRepoUrl } from '@/helpers/validators'
|
||||
|
||||
import { randint } from '@/helpers/commons'
|
||||
|
||||
export default {
|
||||
name: 'AppCatalog',
|
||||
|
||||
components: {
|
||||
LazyRenderer
|
||||
CardDeckFeed
|
||||
},
|
||||
|
||||
props: {
|
||||
search: { type: String, default: '' },
|
||||
quality: { type: String, default: 'decent_quality' },
|
||||
category: { type: String, default: null },
|
||||
subtag: { type: String, default: 'all' }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
['GET', 'apps/catalog?full&with_categories']
|
||||
['GET', 'apps/catalog?full&with_categories&with_antifeatures']
|
||||
],
|
||||
|
||||
// Data
|
||||
apps: undefined,
|
||||
selectedApp: undefined,
|
||||
antifeatures: undefined,
|
||||
|
||||
// Filtering options
|
||||
qualityOptions: [
|
||||
|
@ -194,12 +205,6 @@ export default {
|
|||
// The rest is filled from api data
|
||||
],
|
||||
|
||||
// Set by user inputs
|
||||
search: '',
|
||||
category: null,
|
||||
subtag: 'all',
|
||||
quality: 'decent_quality',
|
||||
|
||||
// Custom install form
|
||||
customInstall: {
|
||||
field: {
|
||||
|
@ -243,7 +248,7 @@ export default {
|
|||
|
||||
subtags () {
|
||||
// build an options array for subtags v-model/options
|
||||
if (this.category) {
|
||||
if (this.category && this.categories.length > 2) {
|
||||
const category = this.categories.find(cat => cat.value === this.category)
|
||||
if (category.subtags) {
|
||||
const subtags = [{ text: this.$i18n.t('all'), value: 'all' }]
|
||||
|
@ -298,17 +303,24 @@ export default {
|
|||
data.categories.forEach(({ title, id, icon, subtags, description }) => {
|
||||
this.categories.push({ text: title, value: id, icon, subtags, description })
|
||||
})
|
||||
this.antifeatures = Object.fromEntries(data.antifeatures.map((af) => ([af.id, af])))
|
||||
},
|
||||
|
||||
setCategory () {
|
||||
// allow search without selecting a category
|
||||
if (this.category === null) {
|
||||
this.category = 'all'
|
||||
}
|
||||
updateQuery (key, value) {
|
||||
// Update the query string without reloading the page
|
||||
this.$router.replace({
|
||||
query: {
|
||||
...this.$route.query,
|
||||
// allow search without selecting a category
|
||||
category: this.$route.query.category || 'all',
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// INSTALL APP
|
||||
async onInstallClick (app) {
|
||||
async onInstallClick (appId) {
|
||||
const app = this.apps.find((app) => app.id === appId)
|
||||
if (!app.decent_quality) {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_install_app_' + app.state))
|
||||
if (!confirmed) return
|
||||
|
@ -364,28 +376,43 @@ export default {
|
|||
}
|
||||
|
||||
.card-deck {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
> * {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 2rem;
|
||||
flex-basis: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-basis: 50%;
|
||||
max-width: calc(50% - 30px);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-basis: 33%;
|
||||
max-width: calc(33.3% - 30px);
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
border-color: $gray-400;
|
||||
@include hover() {
|
||||
color: color-yiq($dark);
|
||||
background-color: $dark;
|
||||
border-color: $dark;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
color: inherit;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// not maintained info
|
||||
.alert-warning {
|
||||
|
@ -403,37 +430,39 @@ export default {
|
|||
}
|
||||
|
||||
flex-basis: 90%;
|
||||
border: 0;
|
||||
|
||||
.btn {
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
outline: none;
|
||||
|
||||
&::after {
|
||||
border: $btn-border-width solid transparent;
|
||||
@include transition($btn-transition);
|
||||
@include border-radius($btn-border-radius, 0);
|
||||
}
|
||||
|
||||
&:focus::after {
|
||||
box-shadow: 0 0 0 $btn-focus-width rgba($dark, .5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
.btn {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom: 0;
|
||||
flex-basis: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.btn:first-of-type {
|
||||
border-left: 0;
|
||||
}
|
||||
.btn:last-of-type {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
.app-card {
|
||||
min-height: 125px;
|
||||
text-align: start;
|
||||
background-color: $gray-200;
|
||||
|
||||
.btn-outline-dark {
|
||||
border-color: $gray-400;
|
||||
|
||||
&:hover {
|
||||
border-color: $dark;
|
||||
.app-logo {
|
||||
float: left;
|
||||
background-color: white;
|
||||
max-width: 91px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
<template>
|
||||
<view-base
|
||||
:queries="queries" @queries-response="onQueriesResponse"
|
||||
ref="view" skeleton="card-form-skeleton"
|
||||
>
|
||||
<config-panels
|
||||
v-if="config.panels" v-bind="config"
|
||||
@submit="onConfigSubmit"
|
||||
/>
|
||||
|
||||
<b-alert v-else-if="config.panels === null" variant="warning">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('app_config_panel_no_panel') }}
|
||||
</b-alert>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api, { objectToParams } from '@/api'
|
||||
import {
|
||||
formatFormData,
|
||||
formatYunoHostConfigPanels
|
||||
} from '@/helpers/yunohostArguments'
|
||||
import ConfigPanels from '@/components/ConfigPanels'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'AppConfigPanel',
|
||||
|
||||
components: {
|
||||
ConfigPanels
|
||||
},
|
||||
|
||||
props: {
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
['GET', `apps/${this.id}/config?full`]
|
||||
],
|
||||
config: {}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onQueriesResponse (config) {
|
||||
if (!config.panels || config.panels.length === 0) {
|
||||
this.config = null
|
||||
} else {
|
||||
this.config = formatYunoHostConfigPanels(config)
|
||||
}
|
||||
},
|
||||
|
||||
async onConfigSubmit ({ id, form, action, name }) {
|
||||
const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
|
||||
|
||||
api.put(
|
||||
action
|
||||
? `apps/${this.id}/actions/${action}`
|
||||
: `apps/${this.id}/config/${id}`,
|
||||
{ args: objectToParams(args) },
|
||||
{ key: `apps.${action ? 'action' : 'update'}_config`, id, name: this.id }
|
||||
).then(() => {
|
||||
this.$refs.view.fetchQueries({ triggerLoading: true })
|
||||
}).catch(err => {
|
||||
if (err.name !== 'APIBadRequestError') throw err
|
||||
const panel = this.config.panels.find(panel => panel.id === id)
|
||||
if (err.data.name) {
|
||||
this.config.errors[id][err.data.name].message = err.message
|
||||
} else this.$set(panel, 'serverError', err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,137 +1,253 @@
|
|||
<template>
|
||||
<view-base :queries="queries" @queries-response="onQueriesResponse" ref="view">
|
||||
<!-- BASIC INFOS -->
|
||||
<card v-if="infos" :title="infos.label" icon="cube">
|
||||
<description-row
|
||||
v-for="(value, key) in infos" :key="key"
|
||||
:term="$t(key)"
|
||||
>
|
||||
<a v-if="key === 'url'" :href="value" target="_blank">{{ value }}</a>
|
||||
<template v-else>{{ value }}</template>
|
||||
</description-row>
|
||||
<description-row :term="$t('app_info_access_desc')">
|
||||
{{ allowedGroups.length > 0 ? allowedGroups.join(', ') + '.' : $t('nobody') }}
|
||||
<yuno-alert v-if="app && app.doc && app.doc.notifications && app.doc.notifications.postInstall.length" variant="info" class="my-4">
|
||||
<div class="d-md-flex align-items-center mb-3">
|
||||
<h2 v-t="'app.doc.notifications.post_install'" class="md-m-0" />
|
||||
<b-button
|
||||
size="sm" :to="{ name: 'group-list'}" variant="info"
|
||||
class="ml-2"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="ml-auto mr-2"
|
||||
@click="dismissNotification('post_install')"
|
||||
>
|
||||
<icon iname="key-modern" /> {{ $t('groups_and_permissions_manage') }}
|
||||
<icon iname="check" />
|
||||
{{ $t('app.doc.notifications.dismiss') }}
|
||||
</b-button>
|
||||
</description-row>
|
||||
</card>
|
||||
</div>
|
||||
|
||||
<!-- OPERATIONS -->
|
||||
<card v-if="app" :title="$t('operations')" icon="wrench">
|
||||
<!-- CHANGE PERMISSIONS LABEL -->
|
||||
<b-form-group :label="$t('app_manage_label_and_tiles')" label-class="font-weight-bold">
|
||||
<form-field
|
||||
v-for="(perm, i) in app.permissions" :key="i"
|
||||
:label="perm.title" :label-for="'perm-' + i"
|
||||
label-cols="0" label-class="" class="m-0"
|
||||
:validation="$v.form.labels.$each[i] "
|
||||
<vue-showdown
|
||||
v-for="[name, notif] in app.doc.notifications.postUpgrade" :key="name"
|
||||
:markdown="notif" flavor="github" :options="{ headerLevelStart: 4 }"
|
||||
/>
|
||||
</yuno-alert>
|
||||
|
||||
<yuno-alert v-if="app && app.doc && app.doc.notifications && app.doc.notifications.postUpgrade.length" variant="info" class="my-4">
|
||||
<div class="d-md-flex align-items-center mb-3">
|
||||
<h2 v-t="'app.doc.notifications.post_upgrade'" class="md-m-0" />
|
||||
<b-button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="ml-auto mr-2"
|
||||
@click="dismissNotification('post_upgrade')"
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<input-item
|
||||
:state="self.state" v-model="form.labels[i].label"
|
||||
:id="'perm' + i" :aria-describedby="'perm-' + i + '_group__BV_description_'"
|
||||
/>
|
||||
<b-input-group-append v-if="perm.tileAvailable" is-text>
|
||||
<checkbox-item v-model="form.labels[i].show_tile" :label="$t('permission_show_tile_enabled')" />
|
||||
</b-input-group-append>
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
variant="info" v-t="'save'"
|
||||
@click="changeLabel(perm.name, form.labels[i])"
|
||||
<icon iname="check" />
|
||||
{{ $t('app.doc.notifications.dismiss') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<vue-showdown
|
||||
v-for="[name, notif] in app.doc.notifications.postUpgrade" :key="name"
|
||||
:markdown="notif" flavor="github" :options="{ headerLevelStart: 4 }"
|
||||
/>
|
||||
</yuno-alert>
|
||||
|
||||
<section v-if="app" class="border rounded p-3 mb-4">
|
||||
<div class="d-md-flex align-items-center mb-4">
|
||||
<h1 class="mb-3 mb-md-0">
|
||||
<icon iname="cube" />
|
||||
{{ app.label }}
|
||||
|
||||
<span class="text-secondary tiny">
|
||||
{{ app.id }}
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<b-button
|
||||
v-if="app.url"
|
||||
:href="app.url" target="_blank"
|
||||
variant="success" class="ml-auto mr-2"
|
||||
>
|
||||
<icon iname="external-link" />
|
||||
{{ $t('app.open_this_app') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
@click="uninstall"
|
||||
id="uninstall"
|
||||
variant="danger"
|
||||
:class="{ 'ml-auto': !app.url }"
|
||||
>
|
||||
<icon iname="trash-o" />
|
||||
{{ $t('uninstall') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<p class="text-secondary">
|
||||
{{ $t('app.installed_version', { version: app.version }) }}<br>
|
||||
|
||||
<template v-if="app.alternativeTo">
|
||||
{{ $t('app.potential_alternative_to') }} {{ app.alternativeTo }}
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<icon iname="comments" /> {{ $t('app.info.problem') }}
|
||||
<a :href="`https://forum.yunohost.org/tag/${id}`" target="_blank">
|
||||
{{ $t('app.info.forum') }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<vue-showdown :markdown="app.description" flavor="github" />
|
||||
</section>
|
||||
|
||||
<!-- BASIC INFOS -->
|
||||
<config-panels v-bind="config" @submit="onConfigSubmit">
|
||||
<!-- OPERATIONS TAB -->
|
||||
<template v-if="currentTab === 'operations'" #tab-top>
|
||||
<!-- CHANGE PERMISSIONS LABEL -->
|
||||
<b-form-group :label="$t('app_manage_label_and_tiles')" label-class="font-weight-bold">
|
||||
<form-field
|
||||
v-for="(perm, i) in app.permissions" :key="i"
|
||||
:label="perm.title" :label-for="'perm-' + i"
|
||||
label-cols="0" label-class="" class="m-0"
|
||||
:validation="$v.form.labels.$each[i] "
|
||||
>
|
||||
<template #default="{ self }">
|
||||
<b-input-group>
|
||||
<input-item
|
||||
:state="self.state" v-model="form.labels[i].label"
|
||||
:id="'perm' + i" :aria-describedby="'perm-' + i + '_group__BV_description_'"
|
||||
/>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
<b-input-group-append v-if="perm.tileAvailable" is-text>
|
||||
<checkbox-item v-model="form.labels[i].show_tile" :label="$t('permission_show_tile_enabled')" />
|
||||
</b-input-group-append>
|
||||
<b-input-group-append>
|
||||
<b-button
|
||||
variant="info" v-t="'save'"
|
||||
@click="changeLabel(perm.name, form.labels[i])"
|
||||
/>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
|
||||
<template v-if="perm.url" #description>
|
||||
{{ $t('permission_corresponding_url') }}:
|
||||
<b-link :href="'https://' + perm.url">
|
||||
https://{{ perm.url }}
|
||||
</b-link>
|
||||
</template>
|
||||
</form-field>
|
||||
</b-form-group>
|
||||
<hr>
|
||||
<template v-if="perm.url" #description>
|
||||
{{ $t('permission_corresponding_url') }}:
|
||||
<b-link :href="'https://' + perm.url">
|
||||
https://{{ perm.url }}
|
||||
</b-link>
|
||||
</template>
|
||||
</form-field>
|
||||
</b-form-group>
|
||||
<hr>
|
||||
|
||||
<!-- CHANGE URL -->
|
||||
<b-form-group
|
||||
:label="$t('app_info_changeurl_desc')" label-for="input-url"
|
||||
:label-cols-lg="app.supports_change_url ? 0 : 0" label-class="font-weight-bold"
|
||||
v-if="app.is_webapp"
|
||||
>
|
||||
<b-input-group v-if="app.supports_change_url">
|
||||
<b-input-group-prepend is-text>
|
||||
https://
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-input-group-prepend class="flex-grow-1">
|
||||
<b-select v-model="form.url.domain" :options="domains" />
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-input-group-prepend is-text>
|
||||
/
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-input id="input-url" v-model="form.url.path" class="flex-grow-3" />
|
||||
|
||||
<b-input-group-append>
|
||||
<b-button @click="changeUrl" variant="info" v-t="'save'" />
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<div v-else class="alert alert-warning">
|
||||
<icon iname="exclamation" /> {{ $t('app_info_change_url_disabled_tooltip') }}
|
||||
</div>
|
||||
</b-form-group>
|
||||
<hr v-if="app.is_webapp">
|
||||
|
||||
<!-- MAKE DEFAULT -->
|
||||
<b-form-group
|
||||
:label="$t('app_info_default_desc', { domain: app.domain })" label-for="main-domain"
|
||||
label-class="font-weight-bold" label-cols-md="4"
|
||||
v-if="app.is_webapp"
|
||||
>
|
||||
<template v-if="!app.is_default">
|
||||
<b-button @click="setAsDefaultDomain($event, false)" id="main-domain" variant="success">
|
||||
<icon iname="star" /> {{ $t('app_make_default') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<b-button @click="setAsDefaultDomain($event, true)" id="main-domain" variant="warning">
|
||||
<icon iname="star" /> {{ $t('app_make_not_default') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-form-group>
|
||||
<hr v-if="app.is_webapp">
|
||||
|
||||
<!-- APP CONFIG PANEL -->
|
||||
<template v-if="app.supports_config_panel">
|
||||
<!-- PERMISSIONS -->
|
||||
<b-form-group
|
||||
:label="$t('app_config_panel_label')" label-for="config"
|
||||
label-cols-md="4" label-class="font-weight-bold"
|
||||
:label="$t('app_info_access_desc')" label-for="permissions"
|
||||
label-class="font-weight-bold" label-cols-lg="0"
|
||||
>
|
||||
<b-button id="config" variant="warning" :to="{ name: 'app-config-panel', params: { id } }">
|
||||
<icon iname="cog" /> {{ $t('app_config_panel') }}
|
||||
{{ allowedGroups.length > 0 ? allowedGroups.join(', ') : $t('nobody') }}
|
||||
<b-button
|
||||
size="sm" :to="{ name: 'group-list'}" variant="info"
|
||||
class="ml-2"
|
||||
>
|
||||
<icon iname="key-modern" /> {{ $t('groups_and_permissions_manage') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
<hr>
|
||||
</template>
|
||||
|
||||
<!-- UNINSTALL -->
|
||||
<b-form-group
|
||||
:label="$t('app_info_uninstall_desc')" label-for="uninstall"
|
||||
label-class="font-weight-bold" label-cols-md="4"
|
||||
>
|
||||
<b-button @click="uninstall" id="uninstall" variant="danger">
|
||||
<icon iname="trash-o" /> {{ $t('uninstall') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
<!-- CHANGE URL -->
|
||||
<b-form-group
|
||||
:label="$t('app_info_changeurl_desc')" label-for="input-url"
|
||||
:label-cols-lg="app.supports_change_url ? 0 : 0" label-class="font-weight-bold"
|
||||
v-if="app.is_webapp"
|
||||
>
|
||||
<b-input-group v-if="app.supports_change_url">
|
||||
<b-input-group-prepend is-text>
|
||||
https://
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-input-group-prepend class="flex-grow-1">
|
||||
<b-select v-model="form.url.domain" :options="domains" />
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-input-group-prepend is-text>
|
||||
/
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-input id="input-url" v-model="form.url.path" class="flex-grow-3" />
|
||||
|
||||
<b-input-group-append>
|
||||
<b-button @click="changeUrl" variant="info" v-t="'save'" />
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<div v-else class="alert alert-warning">
|
||||
<icon iname="exclamation" /> {{ $t('app_info_change_url_disabled_tooltip') }}
|
||||
</div>
|
||||
</b-form-group>
|
||||
<hr v-if="app.is_webapp">
|
||||
|
||||
<!-- MAKE DEFAULT -->
|
||||
<b-form-group
|
||||
:label="$t('app_info_default_desc', { domain: app.domain })" label-for="main-domain"
|
||||
label-class="font-weight-bold" label-cols-md="4"
|
||||
v-if="app.is_webapp"
|
||||
>
|
||||
<template v-if="!app.is_default">
|
||||
<b-button @click="setAsDefaultDomain(false)" id="main-domain" variant="success">
|
||||
<icon iname="star" /> {{ $t('app_make_default') }}
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<b-button @click="setAsDefaultDomain(true)" id="main-domain" variant="warning">
|
||||
<icon iname="star" /> {{ $t('app_make_not_default') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-form-group>
|
||||
</template>
|
||||
</config-panels>
|
||||
|
||||
<b-card v-if="app && app.doc.admin.length" no-body>
|
||||
<b-tabs card fill pills>
|
||||
<b-tab
|
||||
v-for="[name, content] in app.doc.admin" :key="name"
|
||||
>
|
||||
<template #title>
|
||||
<icon iname="book" class="mr-2" />
|
||||
{{ name === "admin" ? $t('app.doc.admin.title') : name }}
|
||||
</template>
|
||||
<vue-showdown :markdown="content" flavor="github" />
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</b-card>
|
||||
|
||||
<card
|
||||
v-if="app && app.integration"
|
||||
id="app-integration" :title="$t('app.integration.title')"
|
||||
collapsable collapsed no-body
|
||||
>
|
||||
<b-list-group flush>
|
||||
<yuno-list-group-item variant="info">
|
||||
{{ $t('app.integration.archs') }} {{ app.integration.archs }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item v-if="app.integration.ldap" :variant="app.integration.ldap === true ? 'success' : 'warning'">
|
||||
{{ $t(`app.integration.ldap.${app.integration.ldap}`) }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item v-if="app.integration.sso" :variant="app.integration.sso === true ? 'success' : 'warning'">
|
||||
{{ $t(`app.integration.sso.${app.integration.sso}`) }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item variant="info">
|
||||
{{ $t(`app.integration.multi_instance.${app.integration.multi_instance}`) }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item variant="info">
|
||||
{{ $t('app.integration.resources', app.integration.resources) }}
|
||||
</yuno-list-group-item>
|
||||
</b-list-group>
|
||||
</card>
|
||||
|
||||
<card
|
||||
v-if="app"
|
||||
id="app-links" icon="link" :title="$t('app.links.title')"
|
||||
collapsable collapsed no-body
|
||||
>
|
||||
<b-list-group flush>
|
||||
<yuno-list-group-item v-for="[key, link] in app.links" :key="key" no-status>
|
||||
<b-link :href="link" target="_blank">
|
||||
<icon :iname="appLinksIcons(key)" />
|
||||
{{ $t('app.links.' + key) }}
|
||||
</b-link>
|
||||
</yuno-list-group-item>
|
||||
</b-list-group>
|
||||
</card>
|
||||
|
||||
<template #skeleton>
|
||||
|
@ -145,14 +261,24 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
import { validationMixin } from 'vuelidate'
|
||||
|
||||
import api from '@/api'
|
||||
import api, { objectToParams } from '@/api'
|
||||
import { readableDate } from '@/helpers/filters/date'
|
||||
import { humanPermissionName } from '@/helpers/filters/human'
|
||||
import { required } from '@/helpers/validators'
|
||||
import {
|
||||
formatFormData,
|
||||
formatI18nField,
|
||||
formatYunoHostConfigPanels
|
||||
} from '@/helpers/yunohostArguments'
|
||||
import ConfigPanels from '@/components/ConfigPanels'
|
||||
|
||||
export default {
|
||||
name: 'AppInfo',
|
||||
|
||||
components: {
|
||||
ConfigPanels
|
||||
},
|
||||
|
||||
props: {
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
@ -162,17 +288,33 @@ export default {
|
|||
queries: [
|
||||
['GET', `apps/${this.id}?full`],
|
||||
['GET', { uri: 'users/permissions?full', storeKey: 'permissions' }],
|
||||
['GET', { uri: 'domains' }]
|
||||
['GET', { uri: 'domains' }],
|
||||
['GET', `apps/${this.id}/config?full`]
|
||||
],
|
||||
infos: undefined,
|
||||
app: undefined,
|
||||
form: undefined
|
||||
form: undefined,
|
||||
config: {
|
||||
panels: [
|
||||
// Fake integration of operations in config panels
|
||||
{
|
||||
hasApplyButton: false,
|
||||
id: 'operations',
|
||||
name: this.$i18n.t('operations')
|
||||
}
|
||||
],
|
||||
validations: {}
|
||||
},
|
||||
doc: undefined
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['domains']),
|
||||
|
||||
currentTab () {
|
||||
return this.$route.params.tabId
|
||||
},
|
||||
|
||||
allowedGroups () {
|
||||
if (!this.app) return
|
||||
return this.app.permissions[0].allowed
|
||||
|
@ -191,7 +333,26 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onQueriesResponse (app) {
|
||||
appLinksIcons (linkType) {
|
||||
const linksIcons = {
|
||||
license: 'institution',
|
||||
website: 'globe',
|
||||
admindoc: 'book',
|
||||
userdoc: 'book',
|
||||
code: 'code',
|
||||
package: 'code',
|
||||
forum: 'comments'
|
||||
}
|
||||
return linksIcons[linkType]
|
||||
},
|
||||
onQueriesResponse (app, _, __, config) {
|
||||
if (app.supports_config_panel) {
|
||||
const config_ = formatYunoHostConfigPanels(config)
|
||||
// reinject 'operations' fake config tab
|
||||
config_.panels.unshift(this.config.panels[0])
|
||||
this.config = config_
|
||||
}
|
||||
|
||||
const form = { labels: [] }
|
||||
|
||||
const mainPermission = app.permissions[this.id + '.main']
|
||||
|
@ -213,35 +374,81 @@ export default {
|
|||
form.labels.push({ label: perm.sublabel, show_tile: perm.show_tile })
|
||||
}
|
||||
}
|
||||
|
||||
this.infos = {
|
||||
id: this.id,
|
||||
label: mainPermission.label,
|
||||
description: app.description,
|
||||
version: app.version,
|
||||
multi_instance: this.$i18n.t(app.manifest.integration.multi_instance ? 'yes' : 'no'),
|
||||
install_time: readableDate(app.settings.install_time, true, true)
|
||||
}
|
||||
if (app.settings.domain && app.settings.path) {
|
||||
this.infos.url = 'https://' + app.settings.domain + app.settings.path
|
||||
form.url = {
|
||||
domain: app.settings.domain,
|
||||
path: app.settings.path.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
this.form = form
|
||||
|
||||
const { DESCRIPTION, ADMIN, ...doc } = app.manifest.doc
|
||||
const notifs = app.manifest.notifications
|
||||
const { ldap, sso, multi_instance, ram, disk, architectures: archs } = app.manifest.integration
|
||||
this.app = {
|
||||
id: this.id,
|
||||
version: app.version,
|
||||
label: mainPermission.label,
|
||||
domain: app.settings.domain,
|
||||
alternativeTo: app.from_catalog.potential_alternative_to?.length
|
||||
? app.from_catalog.potential_alternative_to.join(this.$i18n.t('words.separator'))
|
||||
: null,
|
||||
description: DESCRIPTION ? formatI18nField(DESCRIPTION) : app.description,
|
||||
integration: app.manifest.packaging_format >= 2 ? {
|
||||
archs: Array.isArray(archs) ? archs.join(this.$i18n.t('words.separator')) : archs,
|
||||
ldap: ldap === 'not_relevant' ? null : ldap,
|
||||
sso: sso === 'not_relevant' ? null : sso,
|
||||
multi_instance,
|
||||
resources: { ram: ram.runtime, disk }
|
||||
} : null,
|
||||
links: [
|
||||
['license', `https://spdx.org/licenses/${app.manifest.upstream.license}`],
|
||||
...['website', 'admindoc', 'userdoc', 'code'].map((key) => ([key, app.manifest.upstream[key]])),
|
||||
['forum', `https://forum.yunohost.org/tag/${this.id}`]
|
||||
].filter(([key, val]) => !!val),
|
||||
doc: {
|
||||
notifications: {
|
||||
postInstall: notifs.POST_INSTALL && notifs.POST_INSTALL.main ? [['main', formatI18nField(notifs.POST_INSTALL.main)]] : [],
|
||||
postUpgrade: notifs.POST_UPGRADE ? Object.entries(notifs.POST_UPGRADE).map(([key, content]) => {
|
||||
return [key, formatI18nField(content)]
|
||||
}) : []
|
||||
},
|
||||
admin: [
|
||||
['admin', formatI18nField(ADMIN)],
|
||||
...Object.keys(doc).sort().map((key) => [key.charAt(0) + key.slice(1).toLowerCase(), formatI18nField(doc[key])])
|
||||
].filter((doc) => doc[1])
|
||||
},
|
||||
is_webapp: app.is_webapp,
|
||||
is_default: app.is_default,
|
||||
supports_change_url: app.supports_change_url,
|
||||
supports_config_panel: app.supports_config_panel,
|
||||
permissions
|
||||
}
|
||||
if (this.app.is_webapp) {
|
||||
this.app.is_default = app.is_default
|
||||
if (app.settings.domain && app.settings.path) {
|
||||
this.app.url = 'https://' + app.settings.domain + app.settings.path
|
||||
form.url = {
|
||||
domain: app.settings.domain,
|
||||
path: app.settings.path.slice(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.values(this.app.doc.notifications).some((notif) => notif.length)) {
|
||||
this.app.doc.notifications = null
|
||||
}
|
||||
},
|
||||
|
||||
async onConfigSubmit ({ id, form, action, name }) {
|
||||
const args = await formatFormData(form, { removeEmpty: false, removeNull: true })
|
||||
|
||||
api.put(
|
||||
action
|
||||
? `apps/${this.id}/actions/${action}`
|
||||
: `apps/${this.id}/config/${id}`,
|
||||
{ args: objectToParams(args) },
|
||||
{ key: `apps.${action ? 'action' : 'update'}_config`, id, name: this.id }
|
||||
).then(() => {
|
||||
this.$refs.view.fetchQueries({ triggerLoading: true })
|
||||
}).catch(err => {
|
||||
if (err.name !== 'APIBadRequestError') throw err
|
||||
const panel = this.config.panels.find(panel => panel.id === id)
|
||||
if (err.data.name) {
|
||||
this.config.errors[id][err.data.name].message = err.message
|
||||
} else this.$set(panel, 'serverError', err.message)
|
||||
})
|
||||
},
|
||||
|
||||
changeLabel (permName, data) {
|
||||
|
@ -249,7 +456,7 @@ export default {
|
|||
api.put(
|
||||
'users/permissions/' + permName,
|
||||
data,
|
||||
{ key: 'apps.change_label', prevName: this.infos.label, nextName: data.label }
|
||||
{ key: 'apps.change_label', prevName: this.app.label, nextName: data.label }
|
||||
).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
|
@ -261,18 +468,26 @@ export default {
|
|||
api.put(
|
||||
`apps/${this.id}/changeurl`,
|
||||
{ domain, path: '/' + path },
|
||||
{ key: 'apps.change_url', name: this.infos.label }
|
||||
{ key: 'apps.change_url', name: this.app.label }
|
||||
).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
async setAsDefaultDomain (event, undo = false) {
|
||||
async setAsDefaultDomain (undo = false) {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_app_default'))
|
||||
if (!confirmed) return
|
||||
|
||||
api.put(
|
||||
`apps/${this.id}/default${undo ? '?undo' : ''}`,
|
||||
{},
|
||||
{ key: 'apps.set_default', name: this.infos.label, domain: this.app.domain }
|
||||
{ key: 'apps.set_default', name: this.app.label, domain: this.app.domain }
|
||||
).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
async dismissNotification (name) {
|
||||
api.put(
|
||||
`apps/${this.id}/dismiss_notification/${name}`,
|
||||
{},
|
||||
{ key: 'apps.dismiss_notification', name: this.app.label }
|
||||
).then(this.$refs.view.fetchQueries)
|
||||
},
|
||||
|
||||
|
@ -282,7 +497,7 @@ export default {
|
|||
)
|
||||
if (!confirmed) return
|
||||
|
||||
api.delete('apps/' + this.id, {}, { key: 'apps.uninstall', name: this.infos.label }).then(() => {
|
||||
api.delete('apps/' + this.id, {}, { key: 'apps.uninstall', name: this.app.label }).then(() => {
|
||||
this.$router.push({ name: 'app-list' })
|
||||
})
|
||||
}
|
||||
|
@ -302,4 +517,13 @@ select {
|
|||
.input-group input {
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.tiny {
|
||||
font-size: 50%;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.yuno-alert div div:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,16 +1,135 @@
|
|||
<template>
|
||||
<view-base :queries="queries" @queries-response="onQueriesResponse">
|
||||
<template v-if="infos">
|
||||
<!-- BASIC INFOS -->
|
||||
<card :title="name" icon="download">
|
||||
<description-row
|
||||
v-for="(info, key) in infos" :key="key"
|
||||
:term="$t(key)" :details="info"
|
||||
<template v-if="app">
|
||||
<section class="border rounded p-3 mb-4">
|
||||
<div class="d-md-flex align-items-center mb-4">
|
||||
<h1 class="mb-3 mb-md-0">
|
||||
{{ app.name }}
|
||||
</h1>
|
||||
|
||||
<b-button
|
||||
v-if="app.demo"
|
||||
:href="app.demo" target="_blank"
|
||||
variant="primary" class="ml-auto"
|
||||
>
|
||||
<icon iname="external-link" />
|
||||
{{ $t('app.install.try_demo') }}
|
||||
</b-button>
|
||||
</div>
|
||||
|
||||
<p class="text-secondary">
|
||||
{{ $t('app.install.version', { version: app.version }) }}<br>
|
||||
|
||||
<template v-if="app.alternativeTo">
|
||||
{{ $t('app.potential_alternative_to') }} {{ app.alternativeTo }}
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<vue-showdown :markdown="app.description" flavor="github" />
|
||||
|
||||
<b-img
|
||||
v-if="app.screenshot"
|
||||
:src="app.screenshot"
|
||||
aria-hidden="true" class="d-block" fluid
|
||||
/>
|
||||
</section>
|
||||
|
||||
<card
|
||||
v-if="app.integration"
|
||||
id="app-integration" :title="$t('app.integration.title')"
|
||||
collapsable collapsed no-body
|
||||
>
|
||||
<b-list-group flush>
|
||||
<yuno-list-group-item variant="info">
|
||||
{{ $t('app.integration.archs') }} {{ app.integration.archs }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item v-if="app.integration.ldap" :variant="app.integration.ldap === true ? 'success' : 'warning'">
|
||||
{{ $t(`app.integration.ldap.${app.integration.ldap}`) }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item v-if="app.integration.sso" :variant="app.integration.sso === true ? 'success' : 'warning'">
|
||||
{{ $t(`app.integration.sso.${app.integration.sso}`) }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item variant="info">
|
||||
{{ $t(`app.integration.multi_instance.${app.integration.multi_instance}`) }}
|
||||
</yuno-list-group-item>
|
||||
<yuno-list-group-item variant="info">
|
||||
{{ $t('app.integration.resources', app.integration.resources) }}
|
||||
</yuno-list-group-item>
|
||||
</b-list-group>
|
||||
</card>
|
||||
|
||||
<card
|
||||
id="app-links" icon="link" :title="$t('app.links.title')"
|
||||
collapsable collapsed no-body
|
||||
>
|
||||
<template #header>
|
||||
<h2><icon iname="link" /> {{ $t('app.links.title') }}</h2>
|
||||
</template>
|
||||
|
||||
<b-list-group flush>
|
||||
<yuno-list-group-item v-for="[key, link] in app.links" :key="key" no-status>
|
||||
<b-link :href="link" target="_blank">
|
||||
<icon :iname="appLinksIcons(key)" />
|
||||
{{ $t('app.links.' + key) }}
|
||||
</b-link>
|
||||
</yuno-list-group-item>
|
||||
</b-list-group>
|
||||
</card>
|
||||
|
||||
<yuno-alert v-if="app.hasWarning" variant="warning" class="my-4">
|
||||
<h2>{{ $t('app.install.notifs.pre.warning') }}</h2>
|
||||
|
||||
<template v-if="app.antifeatures">
|
||||
<strong v-t="'app.antifeatures'" class="d-block mb-2" />
|
||||
<dl class="antifeatures">
|
||||
<div v-for="antifeature in app.antifeatures" :key="antifeature.id">
|
||||
<dt class="d-inline">
|
||||
<icon :iname="antifeature.icon" class="md mr-1" />
|
||||
{{ antifeature.title }}:
|
||||
</dt>
|
||||
<dd class="d-inline">
|
||||
{{ antifeature.description }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
<p v-if="app.quality.state === 'lowquality'" v-t="'app.install.problems.lowquality'" />
|
||||
|
||||
<vue-showdown v-if="app.preInstall" :markdown="app.preInstall" flavor="github" />
|
||||
</yuno-alert>
|
||||
|
||||
<yuno-alert
|
||||
v-if="!app.hasSupport"
|
||||
variant="danger" icon="warning" class="my-4"
|
||||
>
|
||||
<h2>{{ $t('app.install.notifs.pre.critical') }}</h2>
|
||||
|
||||
<p v-if="!app.requirements.arch.pass">
|
||||
{{ $t('app.install.problems.arch', app.requirements.arch.values) }}
|
||||
</p>
|
||||
<p v-if="!app.requirements.install.pass">
|
||||
{{ $t('app.install.problems.install', app.requirements.install.values) }}
|
||||
</p>
|
||||
<p v-if="!app.requirements.required_yunohost_version.pass">
|
||||
{{ $t('app.install.problems.version', app.requirements.required_yunohost_version.values) }}
|
||||
</p>
|
||||
</yuno-alert>
|
||||
|
||||
<yuno-alert v-else-if="app.hasDanger" variant="danger" class="my-4">
|
||||
<h2>{{ $t('app.install.notifs.pre.danger') }}</h2>
|
||||
|
||||
<p v-if="['inprogress', 'broken', 'thirdparty'].includes(app.quality.state)" v-t="'app.install.problems.' + app.quality.state" />
|
||||
<p v-if="!app.requirements.ram.pass">
|
||||
{{ $t('app.install.problems.ram', app.requirements.ram.values) }}
|
||||
</p>
|
||||
|
||||
<checkbox-item v-model="force" id="force-install" :label="$t('app.install.problems.ignore')" />
|
||||
</yuno-alert>
|
||||
|
||||
<!-- INSTALL FORM -->
|
||||
<card-form
|
||||
v-if="app.canInstall || force"
|
||||
:title="$t('app_install_parameters')" icon="cog" :submit-text="$t('install')"
|
||||
:validation="$v" :server-error="serverError"
|
||||
@submit.prevent="performInstall"
|
||||
|
@ -25,7 +144,7 @@
|
|||
</template>
|
||||
|
||||
<!-- In case of a custom url with no manifest found -->
|
||||
<b-alert v-else-if="infos === null" variant="warning">
|
||||
<b-alert v-else-if="app === null" variant="warning">
|
||||
<icon iname="exclamation-triangle" /> {{ $t('app_install_custom_no_manifest') }}
|
||||
</b-alert>
|
||||
|
||||
|
@ -45,12 +164,17 @@ import {
|
|||
formatI18nField,
|
||||
formatFormData
|
||||
} from '@/helpers/yunohostArguments'
|
||||
import CardCollapse from '@/components/CardCollapse'
|
||||
|
||||
export default {
|
||||
name: 'AppInstall',
|
||||
|
||||
mixins: [validationMixin],
|
||||
|
||||
components: {
|
||||
CardCollapse
|
||||
},
|
||||
|
||||
props: {
|
||||
id: { type: String, required: true }
|
||||
},
|
||||
|
@ -58,15 +182,17 @@ export default {
|
|||
data () {
|
||||
return {
|
||||
queries: [
|
||||
['GET', 'apps/manifest?app=' + this.id]
|
||||
['GET', 'apps/catalog?full&with_categories&with_antifeatures'],
|
||||
['GET', `apps/manifest?app=${this.id}&with_screenshot`]
|
||||
],
|
||||
app: undefined,
|
||||
name: undefined,
|
||||
infos: undefined,
|
||||
form: undefined,
|
||||
fields: undefined,
|
||||
validations: null,
|
||||
errors: undefined,
|
||||
serverError: ''
|
||||
serverError: '',
|
||||
force: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -75,21 +201,86 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
onQueriesResponse (manifest) {
|
||||
this.name = manifest.name
|
||||
const infosKeys = ['id', 'description', 'license', 'version', 'multi_instance']
|
||||
manifest.license = manifest.upstream.license
|
||||
if (manifest.license === undefined || manifest.license === 'free') {
|
||||
infosKeys.splice(2, 1)
|
||||
appLinksIcons (linkType) {
|
||||
const linksIcons = {
|
||||
license: 'institution',
|
||||
website: 'globe',
|
||||
admindoc: 'book',
|
||||
userdoc: 'book',
|
||||
code: 'code',
|
||||
package: 'code',
|
||||
forum: 'comments'
|
||||
}
|
||||
return linksIcons[linkType]
|
||||
},
|
||||
|
||||
onQueriesResponse (catalog, _app) {
|
||||
const antifeaturesList = Object.fromEntries(catalog.antifeatures.map((af) => ([af.id, af])))
|
||||
|
||||
const { id, name, version, requirements } = _app
|
||||
const { ldap, sso, multi_instance, ram, disk, architectures: archs } = _app.integration
|
||||
|
||||
const quality = { state: _app.quality.state, variant: 'danger' }
|
||||
if (quality.state === 'working') {
|
||||
if (_app.quality.level <= 0) {
|
||||
quality.state = 'broken'
|
||||
} else if (_app.quality.level <= 4) {
|
||||
quality.state = 'lowquality'
|
||||
quality.variant = 'warning'
|
||||
} else {
|
||||
quality.variant = 'success'
|
||||
quality.state = _app.quality.level >= 8 ? 'highquality' : 'goodquality'
|
||||
}
|
||||
}
|
||||
const preInstall = formatI18nField(_app.notifications.PRE_INSTALL.main)
|
||||
const antifeatures = _app.antifeatures?.length
|
||||
? _app.antifeatures.map((af) => antifeaturesList[af])
|
||||
: null
|
||||
|
||||
const hasDanger = quality.variant === 'danger' || !requirements.ram.pass
|
||||
const hasSupport = Object.keys(requirements).every((key) => {
|
||||
// ram support is non-blocking requirement and handled on its own.
|
||||
return key === 'ram' || requirements[key].pass
|
||||
})
|
||||
|
||||
const app = {
|
||||
id,
|
||||
name,
|
||||
alternativeTo: _app.potential_alternative_to && _app.potential_alternative_to.length
|
||||
? _app.potential_alternative_to.join(this.$i18n.t('words.separator'))
|
||||
: null,
|
||||
description: formatI18nField(_app.doc.DESCRIPTION || _app.description),
|
||||
screenshot: _app.screenshot,
|
||||
demo: _app.upstream.demo,
|
||||
version,
|
||||
license: _app.upstream.license,
|
||||
integration: _app.packaging_format >= 2 ? {
|
||||
archs: Array.isArray(archs) ? archs.join(this.$i18n.t('words.separator')) : archs,
|
||||
ldap: ldap === 'not_relevant' ? null : ldap,
|
||||
sso: sso === 'not_relevant' ? null : sso,
|
||||
multi_instance,
|
||||
resources: { ram: ram.runtime, disk }
|
||||
} : null,
|
||||
links: [
|
||||
['license', `https://spdx.org/licenses/${_app.upstream.license}`],
|
||||
...['website', 'admindoc', 'userdoc', 'code'].map((key) => ([key, _app.upstream[key]])),
|
||||
['package', _app.remote.url],
|
||||
['forum', `https://forum.yunohost.org/tag/${id}`]
|
||||
].filter(([key, val]) => !!val),
|
||||
preInstall,
|
||||
antifeatures,
|
||||
quality,
|
||||
requirements,
|
||||
hasWarning: !!preInstall || antifeatures || quality.variant === 'warning',
|
||||
hasDanger,
|
||||
hasSupport,
|
||||
canInstall: hasSupport && !hasDanger
|
||||
}
|
||||
manifest.description = formatI18nField(manifest.description)
|
||||
manifest.multi_instance = this.$i18n.t(manifest.integration.multi_instance ? 'yes' : 'no')
|
||||
this.infos = Object.fromEntries(infosKeys.map(key => [key, manifest[key]]))
|
||||
|
||||
// FIXME yunohost should add the label field by default
|
||||
manifest.install.unshift({
|
||||
ask: this.$t('label_for_manifestname', { name: manifest.name }),
|
||||
default: manifest.name,
|
||||
_app.install.unshift({
|
||||
ask: this.$t('label_for_manifestname', { name }),
|
||||
default: name,
|
||||
name: 'label',
|
||||
help: this.$t('label_for_manifestname_help')
|
||||
})
|
||||
|
@ -99,14 +290,21 @@ export default {
|
|||
fields,
|
||||
validations,
|
||||
errors
|
||||
} = formatYunoHostArguments(manifest.install)
|
||||
} = formatYunoHostArguments(_app.install)
|
||||
|
||||
this.app = app
|
||||
this.fields = fields
|
||||
this.form = form
|
||||
this.validations = { form: validations }
|
||||
this.errors = errors
|
||||
},
|
||||
|
||||
formatAppNotifs (notifs) {
|
||||
return Object.keys(notifs).reduce((acc, key) => {
|
||||
return acc + '\n\n' + notifs[key]
|
||||
}, '')
|
||||
},
|
||||
|
||||
async performInstall () {
|
||||
if ('path' in this.form && this.form.path === '/') {
|
||||
const confirmed = await this.$askConfirmation(
|
||||
|
@ -121,7 +319,15 @@ export default {
|
|||
)
|
||||
const data = { app: this.id, label, args: Object.entries(args).length ? objectToParams(args) : undefined }
|
||||
|
||||
api.post('apps', data, { key: 'apps.install', name: this.name }).then(() => {
|
||||
api.post('apps', data, { key: 'apps.install', name: this.app.name }).then(async ({ notifications }) => {
|
||||
const postInstall = this.formatAppNotifs(notifications)
|
||||
if (postInstall) {
|
||||
const message = this.$i18n.t('app.install.notifs.post.alert') + '\n\n' + postInstall
|
||||
await this.$askMdConfirmation(message, {
|
||||
title: this.$i18n.t('app.install.notifs.post.title', { name: this.app.name }),
|
||||
okTitle: this.$i18n.t('ok')
|
||||
}, true)
|
||||
}
|
||||
this.$router.push({ name: 'app-list' })
|
||||
}).catch(err => {
|
||||
if (err.name !== 'APIBadRequestError') throw err
|
||||
|
@ -133,3 +339,11 @@ export default {
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.antifeatures {
|
||||
dt::before {
|
||||
content: "• ";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
<div>
|
||||
<h5 class="font-weight-bold">
|
||||
{{ label }}
|
||||
<small v-if="name" class="text-secondary">{{ name }}</small>
|
||||
<small class="text-secondary">{{ id }}</small>
|
||||
</h5>
|
||||
<p class="m-0">
|
||||
{{ description }}
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
skeleton="card-list-skeleton"
|
||||
>
|
||||
<!-- MIGRATIONS WARN -->
|
||||
<b-alert variant="warning" :show="pendingMigrations">
|
||||
<icon iname="exclamation-triangle" /> <span v-html="$t('pending_migrations')" />
|
||||
</b-alert>
|
||||
<yuno-alert v-if="pendingMigrations" variant="warning" alert>
|
||||
<span v-html="$t('pending_migrations')" />
|
||||
</yuno-alert>
|
||||
|
||||
<!-- MAJOR YUNOHOST UPGRADE WARN -->
|
||||
<b-alert variant="warning" :show="importantYunohostUpgrade">
|
||||
<icon iname="exclamation-triangle" /> <span v-html="$t('important_yunohost_upgrade')" />
|
||||
</b-alert>
|
||||
<yuno-alert v-if="importantYunohostUpgrade" variant="warning" alert>
|
||||
<span v-html="$t('important_yunohost_upgrade')" />
|
||||
</yuno-alert>
|
||||
|
||||
<!-- SYSTEM UPGRADE -->
|
||||
<card :title="$t('system')" icon="server" no-body>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<template #buttons v-if="system">
|
||||
<b-button
|
||||
variant="success" v-t="'system_upgrade_all_packages_btn'"
|
||||
@click="performUpgrade({ type: 'system' })"
|
||||
@click="performSystemUpgrade()"
|
||||
/>
|
||||
</template>
|
||||
</card>
|
||||
|
@ -50,7 +50,7 @@
|
|||
|
||||
<b-button
|
||||
variant="success" size="sm" v-t="'system_upgrade_btn'"
|
||||
@click="performUpgrade({ type: 'specific_app', id })"
|
||||
@click="confirmAppsUpgrade(id)"
|
||||
/>
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
|
@ -62,19 +62,67 @@
|
|||
<template #buttons v-if="apps">
|
||||
<b-button
|
||||
variant="success" v-t="'system_upgrade_all_applications_btn'"
|
||||
@click="performUpgrade({ type: 'apps' })"
|
||||
@click="confirmAppsUpgrade()"
|
||||
/>
|
||||
</template>
|
||||
</card>
|
||||
|
||||
<b-modal
|
||||
id="apps-pre-upgrade"
|
||||
:title="$t('app.upgrade.confirm.title')"
|
||||
header-bg-variant="warning"
|
||||
:header-class="theme ? 'text-white' : 'text-black'"
|
||||
:ok-title="$t('system_upgrade_btn')" ok-variant="success"
|
||||
:cancel-title="$t('cancel')"
|
||||
@ok="performAppsUpgrade(preUpgrade.apps.map((app) => app.id))"
|
||||
>
|
||||
<h3>
|
||||
{{ $t('app.upgrade.confirm.apps') }}
|
||||
</h3>
|
||||
<ul>
|
||||
<li v-for="{ name, id } in preUpgrade.apps" :key="id">
|
||||
{{ name }} ({{ id }})
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="preUpgrade.hasNotifs" class="mt-4">
|
||||
<h3>
|
||||
{{ $t('app.upgrade.notifs.pre.title') }}
|
||||
</h3>
|
||||
|
||||
<yuno-alert variant="warning">
|
||||
{{ $t('app.upgrade.notifs.pre.alert' ) }}
|
||||
</yuno-alert>
|
||||
|
||||
<div class="card-collapse-wrapper">
|
||||
<card-collapse
|
||||
v-for="{ id, name, notif } in preUpgrade.apps" :key="`${id}-notifs`"
|
||||
:title="name" :id="`${id}-notifs`"
|
||||
visible flush
|
||||
>
|
||||
<b-card-body>
|
||||
<vue-showdown :markdown="notif" flavor="github" :options="{ headerLevelStart: 6 }" />
|
||||
</b-card-body>
|
||||
</card-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</view-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/api'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import CardCollapse from '@/components/CardCollapse'
|
||||
|
||||
export default {
|
||||
name: 'SystemUpdate',
|
||||
|
||||
components: {
|
||||
CardCollapse
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
queries: [
|
||||
|
@ -84,10 +132,18 @@ export default {
|
|||
system: undefined,
|
||||
apps: undefined,
|
||||
importantYunohostUpgrade: undefined,
|
||||
pendingMigrations: undefined
|
||||
pendingMigrations: undefined,
|
||||
preUpgrade: {
|
||||
apps: [],
|
||||
notifs: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['theme'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
// eslint-disable-next-line camelcase
|
||||
onQueriesResponse ({ apps, system, important_yunohost_upgrade, pending_migrations }) {
|
||||
|
@ -98,25 +154,85 @@ export default {
|
|||
this.pendingMigrations = pending_migrations.length !== 0
|
||||
},
|
||||
|
||||
async performUpgrade ({ type, id = null }) {
|
||||
const confirmMsg = this.$i18n.t('confirm_update_' + type, id ? { app: id } : {})
|
||||
const confirmed = await this.$askConfirmation(confirmMsg)
|
||||
formatAppNotifs (notifs) {
|
||||
return Object.keys(notifs).reduce((acc, key) => {
|
||||
return acc + '\n\n' + notifs[key]
|
||||
}, '')
|
||||
},
|
||||
|
||||
async confirmAppsUpgrade (id = null) {
|
||||
const appList = id ? [this.apps.find((app) => app.id === id)] : this.apps
|
||||
const apps = appList.map((app) => ({
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
notif: app.notifications.PRE_UPGRADE
|
||||
? this.formatAppNotifs(app.notifications.PRE_UPGRADE)
|
||||
: ''
|
||||
}))
|
||||
this.preUpgrade = { apps, hasNotifs: apps.some((app) => app.notif) }
|
||||
this.$bvModal.show('apps-pre-upgrade')
|
||||
},
|
||||
|
||||
async performAppsUpgrade (ids) {
|
||||
const apps = ids.map((id) => this.apps.find((app) => app.id === id))
|
||||
const lastAppId = apps[apps.length - 1].id
|
||||
|
||||
for (const app of apps) {
|
||||
const continue_ = await api.put(
|
||||
`apps/${app.id}/upgrade`, {}, { key: 'upgrade.app', app: app.name }
|
||||
).then((response) => {
|
||||
const postMessage = this.formatAppNotifs(response.notifications.POST_UPGRADE)
|
||||
const isLast = app.id === lastAppId
|
||||
this.apps = this.apps.filter((a) => app.id !== a.id)
|
||||
|
||||
if (postMessage) {
|
||||
const message = this.$i18n.t('app.upgrade.notifs.post.alert') + '\n\n' + postMessage
|
||||
return this.$askMdConfirmation(message, {
|
||||
title: this.$i18n.t('app.upgrade.notifs.post.title', { name: app.name }),
|
||||
okTitle: this.$i18n.t(isLast ? 'ok' : 'app.upgrade.continue'),
|
||||
cancelTitle: this.$i18n.t('app.upgrade.stop')
|
||||
}, isLast)
|
||||
} else {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
})
|
||||
if (!continue_) break
|
||||
}
|
||||
|
||||
if (!this.apps.length) {
|
||||
this.apps = null
|
||||
}
|
||||
},
|
||||
|
||||
async performSystemUpgrade () {
|
||||
const confirmed = await this.$askConfirmation(this.$i18n.t('confirm_update_system'))
|
||||
if (!confirmed) return
|
||||
|
||||
const uri = id !== null ? `apps/${id}/upgrade` : 'upgrade/' + type
|
||||
api.put(uri, {}, { key: 'upgrade.' + (id ? 'app' : type), app: id }).then(() => {
|
||||
if (id !== null) {
|
||||
this.apps = this.apps.filter(app => id !== app.id)
|
||||
} else if (type === 'apps') {
|
||||
this.apps = null
|
||||
} else {
|
||||
if (this.system.some(({ name }) => name.includes('yunohost'))) {
|
||||
this.$store.dispatch('TRY_TO_RECONNECT', { attemps: 1, origin: 'upgrade_system', initialDelay: 2000 })
|
||||
}
|
||||
this.system = null
|
||||
api.put('upgrade/system', {}, { key: 'upgrade.system' }).then(() => {
|
||||
if (this.system.some(({ name }) => name.includes('yunohost'))) {
|
||||
this.$store.dispatch('TRY_TO_RECONNECT', { attemps: 1, origin: 'upgrade_system', initialDelay: 2000 })
|
||||
}
|
||||
this.system = null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-collapse-wrapper {
|
||||
border: $card-border-width solid $card-border-color;
|
||||
border-radius: $card-border-radius;
|
||||
|
||||
.card {
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
border-top-right-radius: $card-border-radius;
|
||||
border-top-left-radius: $card-border-radius;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Add table
Reference in a new issue