refactor(user_status): migrate to Vue 3

Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
pull/56544/head
Grigorii K. Shartsev 2025-11-20 01:36:00 +07:00 committed by Maksim Sukharev
parent b10d5d3ca0
commit 8ca4a7a036
13 changed files with 104 additions and 79 deletions

@ -1,4 +0,0 @@
/*!
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/.icon-user-status{background-image:url("../img/app.svg")}.icon-user-status-dark{background-image:url("../img/app-dark.svg");filter:var(--background-invert-if-dark)}/*# sourceMappingURL=user-status-menu.css.map */

@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["user-status-menu.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA,GAIA,kBACC,uCAGD,uBACC,4CACA","file":"user-status-menu.css"}

@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later

@ -70,6 +70,6 @@ class BeforeTemplateRenderedListener implements IEventListener {
}); });
Util::addScript('user_status', 'menu'); Util::addScript('user_status', 'menu');
Util::addStyle('user_status', 'user-status-menu'); Util::addStyle('user_status', 'menu');
} }
} }

@ -4,46 +4,44 @@
--> -->
<template> <template>
<Fragment> <NcListItem
<NcListItem v-if="!inline"
v-if="!inline" :class="$style.userStatusMenuItem"
class="user-status-menu-item" compact
compact :name="visibleMessage"
:name="visibleMessage" @click.stop="openModal">
@click.stop="openModal"> <template #icon>
<NcUserStatusIcon
:class="$style.userStatusIcon"
:status="statusType"
aria-hidden="true" />
</template>
</NcListItem>
<div v-else>
<!-- Dashboard Status -->
<NcButton @click.stop="openModal">
<template #icon> <template #icon>
<NcUserStatusIcon <NcUserStatusIcon
class="user-status-icon" :class="$style.userStatusIcon"
:status="statusType" :status="statusType"
aria-hidden="true" /> aria-hidden="true" />
</template> </template>
</NcListItem> {{ visibleMessage }}
</NcButton>
<div v-else> </div>
<!-- Dashboard Status --> <!-- Status management modal -->
<NcButton @click.stop="openModal"> <SetStatusModal
<template #icon> v-if="isModalOpen"
<NcUserStatusIcon :inline="inline"
class="user-status-icon" @close="closeModal" />
:status="statusType"
aria-hidden="true" />
</template>
{{ visibleMessage }}
</NcButton>
</div>
<!-- Status management modal -->
<SetStatusModal
v-if="isModalOpen"
:inline="inline"
@close="closeModal" />
</Fragment>
</template> </template>
<script> <script>
import { getCurrentUser } from '@nextcloud/auth' import { getCurrentUser } from '@nextcloud/auth'
import { subscribe, unsubscribe } from '@nextcloud/event-bus' import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import debounce from 'debounce' import debounce from 'debounce'
import { Fragment } from 'vue-frag' import { defineAsyncComponent } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton' import NcButton from '@nextcloud/vue/components/NcButton'
import NcListItem from '@nextcloud/vue/components/NcListItem' import NcListItem from '@nextcloud/vue/components/NcListItem'
import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon'
@ -55,11 +53,10 @@ export default {
name: 'UserStatus', name: 'UserStatus',
components: { components: {
Fragment,
NcButton, NcButton,
NcListItem, NcListItem,
NcUserStatusIcon, NcUserStatusIcon,
SetStatusModal: () => import(/* webpackChunkName: 'user-status-modal' */'./components/SetStatusModal.vue'), SetStatusModal: defineAsyncComponent(() => import('./components/SetStatusModal.vue')),
}, },
mixins: [OnlineStatusMixin], mixins: [OnlineStatusMixin],
@ -126,7 +123,7 @@ export default {
/** /**
* Some housekeeping before destroying the component * Some housekeeping before destroying the component
*/ */
beforeDestroy() { beforeUnmount() {
window.removeEventListener('mouseMove', this.mouseMoveListener) window.removeEventListener('mouseMove', this.mouseMoveListener)
clearInterval(this.heartbeatInterval) clearInterval(this.heartbeatInterval)
unsubscribe('user_status:status.updated', this.handleUserStatusUpdated) unsubscribe('user_status:status.updated', this.handleUserStatusUpdated)
@ -179,8 +176,15 @@ export default {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.user-status-icon { // Note: As for v9.3.0 NcListItem does not support <style scoped>
.userStatusMenuItem,
.userStatusMenuItem * {
// TODO: Vue 3 migration - add box-sizing to core menu component
box-sizing: border-box;
}
.userStatusIcon {
width: 20px; width: 20px;
height: 20px; height: 20px;
margin: calc((var(--default-clickable-area) - 20px) / 2); // 20px icon size margin: calc((var(--default-clickable-area) - 20px) / 2); // 20px icon size

@ -39,7 +39,7 @@ export default {
}, },
}, },
emits: ['select-clear-at'], emits: ['selectClearAt'],
data() { data() {
return { return {
@ -74,7 +74,7 @@ export default {
return return
} }
this.$emit('select-clear-at', option.clearAt) this.$emit('selectClearAt', option.clearAt)
}, },
}, },
} }

@ -61,7 +61,7 @@ export default {
emits: [ emits: [
'change', 'change',
'select-icon', 'selectIcon',
], ],
computed: { computed: {
@ -85,14 +85,14 @@ export default {
/** /**
* Notifies the parent component about a changed input * Notifies the parent component about a changed input
* *
* @param {Event} event The Change Event * @param {string} value The new input value
*/ */
onChange(event) { onChange(value) {
this.$emit('change', event.target.value) this.$emit('change', value)
}, },
setIcon(icon) { setIcon(icon) {
this.$emit('select-icon', icon) this.$emit('selectIcon', icon)
}, },
}, },
} }

@ -12,10 +12,12 @@
name="user-status-online" name="user-status-online"
@change="onChange"> @change="onChange">
<label :for="id" class="user-status-online-select__label"> <label :for="id" class="user-status-online-select__label">
<NcUserStatusIcon <span class="user-status-online-select__icon-wrapper">
:status="type" <NcUserStatusIcon
class="user-status-online-select__icon" :status="type"
aria-hidden="true" /> class="user-status-online-select__icon"
aria-hidden="true" />
</span>
{{ label }} {{ label }}
<em class="user-status-online-select__subline">{{ subline }}</em> <em class="user-status-online-select__subline">{{ subline }}</em>
</label> </label>
@ -92,10 +94,17 @@ export default {
} }
} }
&__icon-wrapper {
height: var(--default-clickable-area);
width: var(--default-clickable-area);
display: flex;
align-items: center;
justify-content: center;
}
&__icon { &__icon {
height: 20px; height: 20px;
width: 20px; width: 20px;
padding: calc((var(--default-clickable-area) - 20px) / 2);
} }
&__input:checked + &__label { &__input:checked + &__label {

@ -36,7 +36,7 @@ export default {
PredefinedStatus, PredefinedStatus,
}, },
emits: ['select-status'], emits: ['selectStatus'],
data() { data() {
return { return {
@ -80,7 +80,7 @@ export default {
*/ */
selectStatus(status) { selectStatus(status) {
this.lastSelected = status.id this.lastSelected = status.id
this.$emit('select-status', status) this.$emit('selectStatus', status)
}, },
}, },
} }

@ -3,13 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
import { getCSPNonce } from '@nextcloud/auth'
import { subscribe } from '@nextcloud/event-bus' import { subscribe } from '@nextcloud/event-bus'
import Vue from 'vue' import { createApp } from 'vue'
import UserStatus from './UserStatus.vue' import UserStatus from './UserStatus.vue'
import store from './store/index.js' import store from './store/index.js'
__webpack_nonce__ = getCSPNonce() import './user-status-icons.css'
const mountPoint = document.getElementById('user_status-menu-entry') const mountPoint = document.getElementById('user_status-menu-entry')
@ -18,12 +17,17 @@ const mountPoint = document.getElementById('user_status-menu-entry')
*/ */
function mountMenuEntry() { function mountMenuEntry() {
const mountPoint = document.getElementById('user_status-menu-entry') const mountPoint = document.getElementById('user_status-menu-entry')
// TODO: fix me after Core migration to Vue 3
new Vue({ // In Vue 2 menu items were mounted in place to the menu items
el: mountPoint, // In Vue 3 they are mounted inside the menu item
render: (h) => h(UserStatus), // A workaround - replace the menu item with "display: contents" div
store, const transparentMountPoint = document.createElement('div')
}) transparentMountPoint.style.display = 'contents'
mountPoint.replaceWith(transparentMountPoint)
createApp(UserStatus)
.use(store)
.mount(transparentMountPoint)
} }
if (mountPoint) { if (mountPoint) {
@ -39,12 +43,10 @@ document.addEventListener('DOMContentLoaded', function() {
} }
OCA.Dashboard.registerStatus('status', (el) => { OCA.Dashboard.registerStatus('status', (el) => {
const Dashboard = Vue.extend(UserStatus) createApp(UserStatus, {
return new Dashboard({ inline: true,
propsData: { })
inline: true, .use(store)
}, .mount(el)
store,
}).$mount(el)
}) })
}) })

@ -3,15 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
import Vue from 'vue' import { createStore } from 'vuex'
import Vuex, { Store } from 'vuex'
import predefinedStatuses from './predefinedStatuses.js' import predefinedStatuses from './predefinedStatuses.js'
import userBackupStatus from './userBackupStatus.js' import userBackupStatus from './userBackupStatus.js'
import userStatus from './userStatus.js' import userStatus from './userStatus.js'
Vue.use(Vuex) export default createStore({
export default new Store({
modules: { modules: {
predefinedStatuses, predefinedStatuses,
userStatus, userStatus,

@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
.icon-user-status { .icon-user-status {
background-image: url("../img/app.svg"); background-image: url("../img/app.svg");
} }

@ -51,7 +51,6 @@ import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu' import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue' import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
import AccountMenuProfileEntry from '../components/AccountMenu/AccountMenuProfileEntry.vue' import AccountMenuProfileEntry from '../components/AccountMenu/AccountMenuProfileEntry.vue'
import { getAllStatusOptions } from '../../../apps/user_status/src/services/statusOptionsService.js'
import logger from '../logger.js' import logger from '../logger.js'
interface ISettingsNavigationEntry { interface ISettingsNavigationEntry {
@ -93,7 +92,27 @@ interface ISettingsNavigationEntry {
classes: string classes: string
} }
const USER_DEFINABLE_STATUSES = getAllStatusOptions() // See: apps/user_status/src/services/statusOptionsService.js
// TODO: either import this again from the user_status app when core is migrated to Vue 3
// Or get rid of the forbidden import
const USER_DEFINABLE_STATUSES = [{
type: 'online',
label: t('user_status', 'Online'),
}, {
type: 'away',
label: t('user_status', 'Away'),
}, {
type: 'busy',
label: t('user_status', 'Busy'),
}, {
type: 'dnd',
label: t('user_status', 'Do not disturb'),
subline: t('user_status', 'Mute all notifications'),
}, {
type: 'invisible',
label: t('user_status', 'Invisible'),
subline: t('user_status', 'Appear offline'),
}]
export default defineComponent({ export default defineComponent({
name: 'AccountMenu', name: 'AccountMenu',