Cleanup updatenotification

- Port away from jquery inside vue
- Use modern vue components when possible
- Fix some readability isssues particularly on dark theme
- Use IInitialState
- Use php7.4

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
Signed-off-by: nextcloud-command <nextcloud-command@users.noreply.github.com>
This commit is contained in:
Carl Schwan 2022-09-07 18:04:23 +02:00 committed by nextcloud-command
parent 204a700f85
commit 7bd83d9dcf
8 changed files with 114 additions and 138 deletions

View File

@ -794,10 +794,6 @@ span.version {
display: none !important;
}
}
#version.section {
border-bottom: none;
}
.section {
margin-bottom: 0;
/* section divider lines, none needed for last one */
@ -818,7 +814,6 @@ span.version {
.followupsection {
display: block;
padding: 0 30px 30px 30px;
color: #555;
}
.app-image {

File diff suppressed because one or more lines are too long

View File

@ -901,10 +901,6 @@ span.version {
}
}
#version.section {
border-bottom: none;
}
.section {
margin-bottom: 0;
/* section divider lines, none needed for last one */
@ -927,7 +923,6 @@ span.version {
.followupsection {
display: block;
padding: 0 30px 30px 30px;
color: #555;
}
.app-image {

View File

@ -33,6 +33,7 @@ use OC\User\Backend;
use OCP\User\Backend\ICountUsersBackend;
use OCA\UpdateNotification\UpdateChecker;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IGroupManager;
@ -44,22 +45,15 @@ use OCP\IUserManager;
use Psr\Log\LoggerInterface;
class Admin implements ISettings {
/** @var IConfig */
private $config;
/** @var UpdateChecker */
private $updateChecker;
/** @var IGroupManager */
private $groupManager;
/** @var IDateTimeFormatter */
private $dateTimeFormatter;
/** @var IFactory */
private $l10nFactory;
/** @var IRegistry */
private $subscriptionRegistry;
/** @var IUserManager */
private $userManager;
/** @var LoggerInterface */
private $logger;
private IConfig $config;
private UpdateChecker $updateChecker;
private IGroupManager $groupManager;
private IDateTimeFormatter $dateTimeFormatter;
private IFactory $l10nFactory;
private IRegistry $subscriptionRegistry;
private IUserManager $userManager;
private LoggerInterface $logger;
private IInitialState $initialState;
public function __construct(
IConfig $config,
@ -69,7 +63,8 @@ class Admin implements ISettings {
IFactory $l10nFactory,
IRegistry $subscriptionRegistry,
IUserManager $userManager,
LoggerInterface $logger
LoggerInterface $logger,
IInitialState $initialState
) {
$this->config = $config;
$this->updateChecker = $updateChecker;
@ -79,11 +74,9 @@ class Admin implements ISettings {
$this->subscriptionRegistry = $subscriptionRegistry;
$this->userManager = $userManager;
$this->logger = $logger;
$this->initialState = $initialState;
}
/**
* @return TemplateResponse
*/
public function getForm(): TemplateResponse {
$lastUpdateCheckTimestamp = $this->config->getAppValue('core', 'lastupdatedat');
$lastUpdateCheck = $this->dateTimeFormatter->formatDateTime($lastUpdateCheckTimestamp);
@ -131,12 +124,9 @@ class Admin implements ISettings {
'notifyGroups' => $this->getSelectedGroups($notifyGroups),
'hasValidSubscription' => $hasValidSubscription,
];
$this->initialState->provideInitialState('data', $params);
$params = [
'json' => json_encode($params),
];
return new TemplateResponse('updatenotification', 'admin', $params, '');
return new TemplateResponse('updatenotification', 'admin', [], '');
}
protected function filterChanges(array $changes): array {
@ -162,8 +152,8 @@ class Admin implements ISettings {
}
/**
* @param array $groupIds
* @return array
* @param list<string> $groupIds
* @return list<array{id: string, displayname: string}>
*/
protected function getSelectedGroups(array $groupIds): array {
$result = [];
@ -174,26 +164,16 @@ class Admin implements ISettings {
continue;
}
$result[] = ['value' => $group->getGID(), 'label' => $group->getDisplayName()];
$result[] = ['id' => $group->getGID(), 'displayname' => $group->getDisplayName()];
}
return $result;
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection(): string {
return 'overview';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority(): int {
return 11;
}

View File

@ -1,13 +1,10 @@
<template>
<div id="updatenotification" class="followupsection">
<NcSettingsSection id="updatenotification" :title="t('updatenotification', 'Update')">
<div class="update">
<template v-if="isNewVersionAvailable">
<p v-if="versionIsEol">
<span class="warning">
<span class="icon icon-error-white" />
{{ t('updatenotification', 'The version you are running is not maintained anymore. Please make sure to update to a supported version as soon as possible.') }}
</span>
</p>
<NcNoteCard v-if="versionIsEol" type="warning">
{{ t('updatenotification', 'The version you are running is not maintained anymore. Please make sure to update to a supported version as soon as possible.') }}
</NcNoteCard>
<p>
<span v-html="newVersionAvailableString" /><br>
@ -109,24 +106,41 @@
<p id="oca_updatenotification_groups">
{{ t('updatenotification', 'Notify members of the following groups about available updates:') }}
<NcMultiselect v-model="notifyGroups"
:options="availableGroups"
:options="groups"
:multiple="true"
label="label"
track-by="value"
:tag-width="75" /><br>
:searchable="true"
label="displayname"
:loading="loadingGroups"
:show-no-options="false"
:close-on-select="false"
track-by="id"
:tag-width="75"
@search-change="searchGroup" /><br>
<em v-if="currentChannel === 'daily' || currentChannel === 'git'">{{ t('updatenotification', 'Only notifications for app updates are available.') }}</em>
<em v-if="currentChannel === 'daily'">{{ t('updatenotification', 'The selected update channel makes dedicated notifications for the server obsolete.') }}</em>
<em v-if="currentChannel === 'git'">{{ t('updatenotification', 'The selected update channel does not support updates of the server.') }}</em>
</p>
</div>
</NcSettingsSection>
</template>
<script>
import { generateUrl, getRootUrl, generateOcsUrl } from '@nextcloud/router'
import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu'
import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect'
import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu.js'
import NcMultiselect from '@nextcloud/vue/dist/Components/NcMultiselect.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import { VTooltip } from 'v-tooltip'
import ClickOutside from 'vue-click-outside'
import axios from '@nextcloud/axios'
import { loadState } from '@nextcloud/initial-state'
import { showSuccess } from '@nextcloud/dialogs'
import debounce from 'debounce'
import { getLoggerBuilder } from '@nextcloud/logger'
const logger = getLoggerBuilder()
.setApp('updatenotification')
.detectUser()
.build()
VTooltip.options.defaultHtml = false
@ -135,6 +149,8 @@ export default {
components: {
NcMultiselect,
NcPopoverMenu,
NcSettingsSection,
NcNoteCard,
},
directives: {
ClickOutside,
@ -142,6 +158,7 @@ export default {
},
data() {
return {
loadingGroups: false,
newVersionString: '',
lastCheckedDate: '',
isUpdateChecked: false,
@ -158,7 +175,7 @@ export default {
currentChannel: '',
channels: [],
notifyGroups: '',
availableGroups: [],
groups: [],
isDefaultUpdateServerURL: true,
enableChangeWatcher: false,
@ -174,9 +191,6 @@ export default {
}
},
_$el: null,
_$notifyGroups: null,
computed: {
newVersionAvailableString() {
return t('updatenotification', 'A new version is available: <strong>{newVersionString}</strong>', {
@ -293,46 +307,41 @@ export default {
watch: {
notifyGroups(selectedOptions) {
if (!this.enableChangeWatcher) {
// The first time is when loading the app
this.enableChangeWatcher = true
return
}
const selectedGroups = []
_.each(selectedOptions, function(group) {
selectedGroups.push(group.value)
const groups = this.notifyGroups.map(group => {
return group.id
})
OCP.AppConfig.setValue('updatenotification', 'notify_groups', JSON.stringify(selectedGroups))
OCP.AppConfig.setValue('updatenotification', 'notify_groups', JSON.stringify(groups))
},
isNewVersionAvailable() {
if (!this.isNewVersionAvailable) {
return
}
$.ajax({
url: generateOcsUrl('apps/updatenotification/api/v1/applist/{newVersion}', { newVersion: this.newVersion }),
type: 'GET',
beforeSend(request) {
request.setRequestHeader('Accept', 'application/json')
},
success: function(response) {
this.availableAppUpdates = response.ocs.data.available
this.missingAppUpdates = response.ocs.data.missing
this.isListFetched = true
this.appStoreFailed = false
}.bind(this),
error: function(xhr) {
this.availableAppUpdates = []
this.missingAppUpdates = []
this.appStoreDisabled = xhr.responseJSON.ocs.data.appstore_disabled
this.isListFetched = true
this.appStoreFailed = true
}.bind(this),
axios.get(generateOcsUrl('apps/updatenotification/api/v1/applist/{newVersion}', {
newVersion: this.newVersion,
})).then(({ data }) => {
this.availableAppUpdates = data.ocs.data.available
this.missingAppUpdates = data.ocs.data.missing
this.isListFetched = true
this.appStoreFailed = false
}).catch(({ data }) => {
this.availableAppUpdates = []
this.missingAppUpdates = []
this.appStoreDisabled = data.ocs.data.appstore_disabled
this.isListFetched = true
this.appStoreFailed = true
})
},
},
beforeMount() {
// Parse server data
const data = JSON.parse($('#updatenotification').attr('data-json'))
const data = loadState('updatenotification', 'data')
this.newVersion = data.newVersion
this.newVersionString = data.newVersionString
@ -360,51 +369,50 @@ export default {
this.whatsNewData = this.whatsNewData.concat(data.changes.whatsNew.regular)
}
},
mounted() {
this._$el = $(this.$el)
this._$notifyGroups = this._$el.find('#oca_updatenotification_groups_list')
this._$notifyGroups.on('change', function() {
this.$emit('input')
}.bind(this))
$.ajax({
url: generateOcsUrl('cloud/groups'),
dataType: 'json',
success: function(data) {
const results = []
$.each(data.ocs.data.groups, function(i, group) {
results.push({ value: group, label: group })
})
this.availableGroups = results
this.enableChangeWatcher = true
}.bind(this),
})
this.searchGroup()
},
methods: {
searchGroup: debounce(async function(query) {
this.loadingGroups = true
try {
const response = await axios.get(generateOcsUrl('cloud/groups/details'), {
search: query,
limit: 20,
offset: 0,
})
this.groups = response.data.ocs.data.groups.sort(function(a, b) {
return a.displayname.localeCompare(b.displayname)
})
} catch (err) {
logger.error('Could not fetch groups', err)
} finally {
this.loadingGroups = false
}
}, 500),
/**
* Creates a new authentication token and loads the updater URL
*/
clickUpdaterButton() {
$.ajax({
url: generateUrl('/apps/updatenotification/credentials'),
}).success(function(token) {
axios.get(generateUrl('/apps/updatenotification/credentials'))
.then(({ data }) => {
// create a form to send a proper post request to the updater
const form = document.createElement('form')
form.setAttribute('method', 'post')
form.setAttribute('action', getRootUrl() + '/updater/')
const form = document.createElement('form')
form.setAttribute('method', 'post')
form.setAttribute('action', getRootUrl() + '/updater/')
const hiddenField = document.createElement('input')
hiddenField.setAttribute('type', 'hidden')
hiddenField.setAttribute('name', 'updater-secret-input')
hiddenField.setAttribute('value', token)
const hiddenField = document.createElement('input')
hiddenField.setAttribute('type', 'hidden')
hiddenField.setAttribute('name', 'updater-secret-input')
hiddenField.setAttribute('value', data.token)
form.appendChild(hiddenField)
form.appendChild(hiddenField)
document.body.appendChild(form)
form.submit()
})
document.body.appendChild(form)
form.submit()
})
},
changeReleaseChannelToEnterprise() {
this.changeReleaseChannel('enterprise')
@ -418,15 +426,10 @@ export default {
changeReleaseChannel(channel) {
this.currentChannel = channel
$.ajax({
url: generateUrl('/apps/updatenotification/channel'),
type: 'POST',
data: {
channel: this.currentChannel,
},
success(data) {
OC.msg.finishedAction('#channel_save_msg', data)
},
axios.post(generateUrl('/apps/updatenotification/channel'), {
channel: this.currentChannel,
}).then(({ data }) => {
showSuccess(data.data.message)
})
this.openedUpdateChannelMenu = false
@ -455,8 +458,10 @@ export default {
<style lang="scss" scoped>
#updatenotification {
margin-top: -25px;
margin-bottom: 200px;
& > * {
max-width: 900px;
}
div.update,
p:not(.inlineblock) {
margin-bottom: 25px;

View File

@ -8,6 +8,7 @@ declare(strict_types=1);
* This file is licensed under the Affero General Public License version 3 or
* later. See the COPYING file.
*/
script('updatenotification', 'updatenotification');
/** @var array $_ */ ?>
<div id="updatenotification" data-json="<?php p($_['json']); ?>"></div>
\OCP\Util::addScript('updatenotification', 'updatenotification');
?>
<div id="updatenotification"></div>

Binary file not shown.

Binary file not shown.