aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2023-12-23 20:48:10 +0100
committerEduardo Morales <emoral435@gmail.com>2024-01-10 11:22:27 -0600
commit949e09ccb734b23a09d70f79972aeb7e86bedda4 (patch)
tree1694194321bc9a8b905bfe41ffe9b90e1c2ef0e2 /core
parentb2aa27f3e38ba156c21e345cc13dbcd21b1c5e1b (diff)
downloadnextcloud-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.php4
-rw-r--r--core/src/profile.ts (renamed from core/src/profile.js)22
-rw-r--r--core/src/views/Profile.vue482
-rw-r--r--core/templates/profile.php6
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>