diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2023-12-23 20:48:10 +0100 |
---|---|---|
committer | Eduardo Morales <emoral435@gmail.com> | 2024-01-10 11:22:27 -0600 |
commit | 949e09ccb734b23a09d70f79972aeb7e86bedda4 (patch) | |
tree | 1694194321bc9a8b905bfe41ffe9b90e1c2ef0e2 /core | |
parent | b2aa27f3e38ba156c21e345cc13dbcd21b1c5e1b (diff) | |
download | nextcloud-server-949e09ccb734b23a09d70f79972aeb7e86bedda4.tar.gz nextcloud-server-949e09ccb734b23a09d70f79972aeb7e86bedda4.zip |
enh(core): Refactor profile page to use vue components
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'core')
-rw-r--r-- | core/Controller/ProfilePageController.php | 4 | ||||
-rw-r--r-- | core/src/profile.ts (renamed from core/src/profile.js) | 22 | ||||
-rw-r--r-- | core/src/views/Profile.vue | 482 | ||||
-rw-r--r-- | core/templates/profile.php | 6 |
4 files changed, 189 insertions, 325 deletions
diff --git a/core/Controller/ProfilePageController.php b/core/Controller/ProfilePageController.php index 8ea2ad699c1..eb5b0aa4c0a 100644 --- a/core/Controller/ProfilePageController.php +++ b/core/Controller/ProfilePageController.php @@ -29,7 +29,7 @@ namespace OC\Core\Controller; use OC\Profile\ProfileManager; use OCP\AppFramework\Controller; -use OCP\AppFramework\Http\Attribute\IgnoreOpenAPI; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\EventDispatcher\IEventDispatcher; @@ -42,7 +42,7 @@ use OCP\Profile\BeforeTemplateRenderedEvent; use OCP\Share\IManager as IShareManager; use OCP\UserStatus\IManager as IUserStatusManager; -#[IgnoreOpenAPI] +#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class ProfilePageController extends Controller { public function __construct( string $appName, diff --git a/core/src/profile.js b/core/src/profile.ts index 79465c6a28d..ee1593e8705 100644 --- a/core/src/profile.js +++ b/core/src/profile.ts @@ -20,17 +20,14 @@ * */ -import Vue from 'vue' import { getRequestToken } from '@nextcloud/auth' -import { translate as t } from '@nextcloud/l10n' -import VTooltip from 'v-tooltip' - -import logger from './logger.js' +import Vue from 'vue' import Profile from './views/Profile.vue' import ProfileSections from './profile/ProfileSections.js' -__webpack_nonce__ = btoa(getRequestToken()) +// @ts-expect-error Script nonce required for webpack loading additional scripts +__webpack_nonce__ = btoa(getRequestToken() ?? '') if (!window.OCA) { window.OCA = {} @@ -41,19 +38,8 @@ if (!window.OCA.Core) { } Object.assign(window.OCA.Core, { ProfileSections: new ProfileSections() }) -Vue.use(VTooltip) - -Vue.mixin({ - props: { - logger, - }, - methods: { - t, - }, -}) - const View = Vue.extend(Profile) window.addEventListener('DOMContentLoaded', () => { - new View().$mount('#vue-profile') + new View().$mount('#content') }) diff --git a/core/src/views/Profile.vue b/core/src/views/Profile.vue index 84859f4a971..b7f593df8d2 100644 --- a/core/src/views/Profile.vue +++ b/core/src/views/Profile.vue @@ -22,197 +22,185 @@ --> <template> - <div class="profile"> - <div class="profile__header"> - <div class="profile__header__container"> - <div class="profile__header__container__placeholder" /> - <h2 class="profile__header__container__displayname"> - {{ displayname || userId }} - <a v-if="isCurrentUser" - class="primary profile__header__container__edit-button" - :href="settingsUrl"> - <PencilIcon class="pencil-icon" - :size="16" /> - {{ t('core', 'Edit Profile') }} - </a> - </h2> - <div v-if="status.icon || status.message" - class="profile__header__container__status-text" - :class="{ interactive: isCurrentUser }" - @click.prevent.stop="openStatusModal"> - {{ status.icon }} {{ status.message }} + <NcContent app-name="profile"> + <NcAppContent> + <div class="profile__header"> + <div class="profile__header__container"> + <div class="profile__header__container__placeholder" /> + <div class="profile__header__container__displayname"> + <h2>{{ displayname || userId }}</h2> + <NcButton v-if="isCurrentUser" + type="primary" + :href="settingsUrl"> + <template #icon> + <PencilIcon :size="20" /> + </template> + {{ t('core', 'Edit Profile') }} + </NcButton> + </div> + <NcButton v-if="status.icon || status.message" + :disabled="!isCurrentUser" + :type="isCurrentUser ? 'tertiary' : 'tertiary-no-background'" + @click="openStatusModal"> + {{ status.icon }} {{ status.message }} + </NcButton> </div> </div> - </div> - - <div class="profile__wrapper"> - <div class="profile__content"> - <div class="profile__sidebar"> - <NcAvatar class="avatar" - :class="{ interactive: isCurrentUser }" - :user="userId" - :size="180" - :show-user-status="true" - :show-user-status-compact="false" - :disable-menu="true" - :disable-tooltip="true" - :is-no-user="!isUserAvatarVisible" - @click.native.prevent.stop="openStatusModal" /> - - <div class="user-actions"> - <!-- When a tel: URL is opened with target="_blank", a blank new tab is opened which is inconsistent with the handling of other URLs so we set target="_self" for the phone action --> - <PrimaryActionButton v-if="primaryAction" - class="user-actions__primary" - :href="primaryAction.target" - :icon="primaryAction.icon" - :target="primaryAction.id === 'phone' ? '_self' :'_blank'"> - {{ primaryAction.title }} - </PrimaryActionButton> - <div class="user-actions__other"> - <!-- FIXME Remove inline styles after https://github.com/nextcloud/nextcloud-vue/issues/2315 is fixed --> - <NcActions v-for="action in middleActions" - :key="action.id" - :default-icon="action.icon" - style=" - background-position: 14px center; - background-size: 16px; - background-repeat: no-repeat;" - :style="{ - backgroundImage: `url(${action.icon})`, - ...(colorMainBackground === '#181818' && { filter: 'invert(1)' }) - }"> - <NcActionLink :close-after-click="true" - :icon="action.icon" + + <div class="profile__wrapper"> + <div class="profile__content"> + <div class="profile__sidebar"> + <NcAvatar class="avatar" + :class="{ interactive: isCurrentUser }" + :user="userId" + :size="180" + :show-user-status="true" + :show-user-status-compact="false" + :disable-menu="true" + :disable-tooltip="true" + :is-no-user="!isUserAvatarVisible" + @click.native.prevent.stop="openStatusModal" /> + + <div class="user-actions"> + <!-- When a tel: URL is opened with target="_blank", a blank new tab is opened which is inconsistent with the handling of other URLs so we set target="_self" for the phone action --> + <NcButton v-if="primaryAction" + type="primary" + class="user-actions__primary" + :href="primaryAction.target" + :icon="primaryAction.icon" + :target="primaryAction.id === 'phone' ? '_self' :'_blank'"> + <template #icon> + <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 --> + <img :src="primaryAction.icon" alt="" class="user-actions__primary__icon"> + </template> + {{ primaryAction.title }} + </NcButton> + <NcActions class="user-actions__other" :inline="4"> + <NcActionLink v-for="action in otherActions" + :key="action.id" + :close-after-click="true" :href="action.target" :target="action.id === 'phone' ? '_self' :'_blank'"> + <template #icon> + <!-- Fix for https://github.com/nextcloud-libraries/nextcloud-vue/issues/2315 --> + <img :src="action.icon" alt="" class="user-actions__other__icon"> + </template> {{ action.title }} </NcActionLink> </NcActions> - <template v-if="otherActions"> - <NcActions :force-menu="true"> - <NcActionLink v-for="action in otherActions" - :key="action.id" - :class="{ 'icon-invert': colorMainBackground === '#181818' }" - :close-after-click="true" - :icon="action.icon" - :href="action.target" - :target="action.id === 'phone' ? '_self' :'_blank'"> - {{ action.title }} - </NcActionLink> - </NcActions> - </template> </div> </div> - </div> - <div class="profile__blocks"> - <div v-if="organisation || role || address" class="profile__blocks-details"> - <div v-if="organisation || role" class="detail"> - <p>{{ organisation }} <span v-if="organisation && role">•</span> {{ role }}</p> + <div class="profile__blocks"> + <div v-if="organisation || role || address" class="profile__blocks-details"> + <div v-if="organisation || role" class="detail"> + <p>{{ organisation }} <span v-if="organisation && role">•</span> {{ role }}</p> + </div> + <div v-if="address" class="detail"> + <p> + <MapMarkerIcon class="map-icon" + :size="16" /> + {{ address }} + </p> + </div> </div> - <div v-if="address" class="detail"> - <p> - <MapMarkerIcon class="map-icon" - :size="16" /> - {{ address }} + <template v-if="headline || biography || sections.length > 0"> + <h3 v-if="headline" class="profile__blocks-headline"> + {{ headline }} + </h3> + <p v-if="biography" class="profile__blocks-biography"> + {{ biography }} </p> - </div> - </div> - <template v-if="headline || biography || sections.length > 0"> - <div v-if="headline" class="profile__blocks-headline"> - <h3>{{ headline }}</h3> - </div> - <div v-if="biography" class="profile__blocks-biography"> - <p>{{ biography }}</p> - </div> - <!-- additional entries, use it with cautious --> - <div v-for="(section, index) in sections" - :ref="'section-' + index" - :key="index" - class="profile__additionalContent"> - <component :is="section($refs['section-'+index], userId)" :userId="userId" /> - </div> - </template> - <template v-else> - <div class="profile__blocks-empty-info"> - <AccountIcon :size="60" - fill-color="var(--color-text-maxcontrast)" /> - <h3>{{ emptyProfileMessage }}</h3> - <p>{{ t('core', 'The headline and about sections will show up here') }}</p> - </div> - </template> + <!-- additional entries, use it with cautious --> + <div v-for="(section, index) in sections" + :ref="'section-' + index" + :key="index" + class="profile__additionalContent"> + <component :is="section($refs['section-'+index], userId)" :user-id="userId" /> + </div> + </template> + <NcEmptyContent v-else + class="profile__blocks-empty-info" + :name="emptyProfileMessage" + :description="t('core', 'The headline and about sections will show up here')"> + <template #icon> + <AccountIcon :size="60" /> + </template> + </NcEmptyContent> + </div> </div> </div> - </div> - </div> + </NcAppContent> + </NcContent> </template> -<script> +<script lang="ts"> import { getCurrentUser } from '@nextcloud/auth' +import { showError } from '@nextcloud/dialogs' import { subscribe, unsubscribe } from '@nextcloud/event-bus' import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' import { generateUrl } from '@nextcloud/router' -import { showError } from '@nextcloud/dialogs' +import { defineComponent } from 'vue' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' +import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js' +import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcContent from '@nextcloud/vue/dist/Components/NcContent.js' +import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' +import AccountIcon from 'vue-material-design-icons/Account.vue' import MapMarkerIcon from 'vue-material-design-icons/MapMarker.vue' import PencilIcon from 'vue-material-design-icons/Pencil.vue' -import AccountIcon from 'vue-material-design-icons/Account.vue' -import PrimaryActionButton from '../components/Profile/PrimaryActionButton.vue' - -const status = loadState('core', 'status', {}) -const { - userId, - displayname, - address, - organisation, - role, - headline, - biography, - actions, - isUserAvatarVisible, -} = loadState('core', 'profileParameters', { - userId: null, - displayname: null, - address: null, - organisation: null, - role: null, - headline: null, - biography: null, - actions: [], - isUserAvatarVisible: false, -}) +interface IProfileAction { + target: string + icon: string + id: string + title: string +} + +interface IStatus { + icon: string, + message: string, + userId: string, +} -export default { +export default defineComponent({ name: 'Profile', components: { AccountIcon, + MapMarkerIcon, NcActionLink, NcActions, + NcAppContent, NcAvatar, - MapMarkerIcon, + NcButton, + NcContent, + NcEmptyContent, PencilIcon, - PrimaryActionButton, }, data() { + const profileParameters = loadState('core', 'profileParameters', { + userId: null as string|null, + displayname: null as string|null, + address: null as string|null, + organisation: null as string|null, + role: null as string|null, + headline: null as string|null, + biography: null as string|null, + actions: [] as IProfileAction[], + isUserAvatarVisible: false, + }) + return { - status, - userId, - displayname, - address, - organisation, - role, - headline, - biography, - actions, - isUserAvatarVisible, - sections: OCA.Core.ProfileSections.getSections(), + ...profileParameters, + status: loadState<Partial<IStatus>>('core', 'status', {}), + sections: window.OCA.Core.ProfileSections.getSections(), } }, @@ -232,33 +220,22 @@ export default { return null }, - middleActions() { - if (this.allActions.slice(1, 4).length) { - return this.allActions.slice(1, 4) - } - return null - }, - otherActions() { - if (this.allActions.slice(4).length) { - return this.allActions.slice(4) + console.warn(this.allActions) + if (this.allActions.length > 1) { + return this.allActions.slice(1) } - return null + return [] }, settingsUrl() { return generateUrl('/settings/user') }, - colorMainBackground() { - // For some reason the returned string has prepended whitespace - return getComputedStyle(document.body).getPropertyValue('--color-main-background').trim() - }, - emptyProfileMessage() { return this.isCurrentUser ? t('core', 'You have not added any info yet') - : t('core', '{user} has not added any info yet', { user: (this.displayname || this.userId) }) + : t('core', '{user} has not added any info yet', { user: (this.displayname || this.userId!) }) }, }, @@ -273,14 +250,16 @@ export default { }, methods: { - handleStatusUpdate(status) { + t, + + handleStatusUpdate(status: IStatus) { if (this.isCurrentUser && status.userId === this.userId) { this.status = status } }, openStatusModal() { - const statusMenuItem = document.querySelector('.user-status-menu-item__toggle') + const statusMenuItem = document.querySelector<HTMLButtonElement>('.user-status-menu-item') // Changing the user status is only enabled if you are the current user if (this.isCurrentUser) { if (statusMenuItem) { @@ -291,25 +270,17 @@ export default { } }, }, -} +}) </script> -<style lang="scss"> -// Override header styles -#header { - background-color: transparent !important; - background-image: none !important; -} - -#content { - padding-top: 0px; -} -</style> - <style lang="scss" scoped> $profile-max-width: 1024px; $content-max-width: 640px; +:deep(#app-content-vue) { + background-color: unset; +} + .profile { width: 100%; overflow-y: auto; @@ -336,74 +307,17 @@ $content-max-width: 640px; grid-row: 1 / 3; } - &__displayname, &__status-text { - color: var(--color-main-text); - } - &__displayname { + padding-inline: 16px; // same as the status text button, see NcButton width: $content-max-width; height: 45px; - margin-top: 128px; - // Override the global style declaration - margin-bottom: 0; - font-size: 30px; + margin-block: 100px 0; display: flex; align-items: center; - cursor: text; - - &:not(:last-child) { - margin-top: 100px; - margin-bottom: 4px; - } - } - - &__edit-button { - border: none; - margin-left: 18px; - margin-top: 2px; - color: var(--color-primary-element-text); - background-color: var(--color-primary-element); - box-shadow: 0 0 0 2px var(--color-primary-element); - border-radius: var(--border-radius-pill); - padding: 0 18px; - font-size: var(--default-font-size); - height: 44px; - line-height: 44px; - font-weight: bold; - - &:hover, - &:focus, - &:active { - color: var(--color-primary-element-light-text); - background-color: var(--color-primary-element-light); - } + gap: 18px; - .pencil-icon { - display: inline-block; - vertical-align: middle; - margin-top: 2px; - } - } - - &__status-text { - width: max-content; - max-width: $content-max-width; - padding: 5px 10px; - margin-left: -12px; - margin-top: 2px; - - &.interactive { - cursor: pointer; - - &:hover, - &:focus, - &:active { - background-color: var(--color-main-background); - color: var(--color-main-text); - border-radius: var(--border-radius-pill); - font-weight: bold; - box-shadow: 0 3px 6px var(--color-box-shadow); - } + h2 { + font-size: 30px; } } } @@ -411,26 +325,26 @@ $content-max-width: 640px; &__sidebar { position: sticky; - top: var(--header-height); + top: 0; align-self: flex-start; padding-top: 20px; min-width: 220px; margin: -150px 20px 0 0; // Specificity hack is needed to override Avatar component styles - &::v-deep .avatar.avatardiv, h2 { + :deep(.avatar.avatardiv) { text-align: center; margin: auto; display: block; padding: 8px; - } - &::v-deep .avatar.avatardiv:not(.avatardiv--unknown) { - background-color: var(--color-main-background) !important; - box-shadow: none; - } + &.interactive { + .avatardiv__user-status { + // Show that the status is interactive + cursor: pointer; + } + } - &::v-deep .avatar.avatardiv { .avatardiv__user-status { right: 14px; bottom: 14px; @@ -444,18 +358,6 @@ $content-max-width: 640px; font-size: 20px; } } - - &::v-deep .avatar.interactive.avatardiv { - .avatardiv__user-status { - cursor: pointer; - - &:hover, - &:focus, - &:active { - box-shadow: 0 3px 6px var(--color-box-shadow); - } - } - } } &__wrapper { @@ -477,6 +379,7 @@ $content-max-width: 640px; width: $content-max-width; p, h3 { + cursor: text; overflow-wrap: anywhere; } @@ -497,36 +400,15 @@ $content-max-width: 640px; } &-headline { - margin-top: 10px; - - h3 { - font-weight: bold; - font-size: 20px; - margin: 0; - } + margin-inline: 0; + margin-block: 10px 0; + font-weight: bold; + font-size: 20px; } &-biography { white-space: pre-line; } - - h3, p { - cursor: text; - } - - &-empty-info { - margin-top: 80px; - margin-right: 100px; - display: flex; - flex-direction: column; - text-align: center; - - h3 { - font-weight: bold; - font-size: 18px; - margin: 8px 0; - } - } } } @@ -568,10 +450,6 @@ $content-max-width: 640px; max-width: 600px; margin: 0 auto; padding: 20px 50px 50px 50px; - - &-empty-info { - margin: 0; - } } &__sidebar { @@ -589,21 +467,25 @@ $content-max-width: 640px; &__primary { margin: 0 auto; + + &__icon { + filter: var(--primary-invert-if-dark); + } } &__other { display: flex; justify-content: center; gap: 0 4px; - a { + + &__icon { + height: 20px; + width: 20px; + object-fit: contain; filter: var(--background-invert-if-dark); + align-self: center; + margin: 12px; // so we get 44px x 44px } } } - -.icon-invert { - &::v-deep .action-link__icon { - filter: invert(1); - } -} </style> diff --git a/core/templates/profile.php b/core/templates/profile.php index 84d6eb0e38f..33cfaeb52c6 100644 --- a/core/templates/profile.php +++ b/core/templates/profile.php @@ -1,5 +1 @@ -<div - id="vue-profile" - class="icon-loading" - style="width: 100%;"> -</div> +<div id="content"></div> |