Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MS] User details modal #6000

Merged
merged 1 commit into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions client/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,18 @@
"cancel": "Cancel",
"create": "Invite"
},
"UserDetailsModal": {
"title": "User details",
"subtitles": {
"name": "Name",
"joined": "Joined",
"commonWorkspaces": "Shared workspaces",
"revoked": "Revoked"
},
"actions": {
"close": "Close"
}
},
"revocation": {
"revokeTitle": "Revoke this user? | Revoke these users?",
"revokeQuestion": "This will revoke {user}, preventing them from accessing this organization. Are you sure you want to proceed? | This will revoke these {count} users, preventing them from accessing this organization. Are you sure you want to proceed?",
Expand Down
12 changes: 12 additions & 0 deletions client/src/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,18 @@
"cancel": "Annuler",
"create": "Inviter"
},
"UserDetailsModal": {
"title": "Détails de l'utilisateur",
"subtitles": {
"name": "Nom",
"joined": "Rejoint",
"commonWorkspaces": "Espaces de travail communs",
"revoked": "Révoqué"
},
"actions": {
"close": "Fermer"
}
},
"revocation": {
"revokeTitle": "Révoquer cet utilisateur ? | Révoquer ces utilisateurs ?",
"revokeQuestion": "L'utilisateur {user} sera révoqué. Il ne pourra plus accéder à l'organisation. Voulez-vous continuer ? | Ces {count} utilisateurs seront révoqués. Ils ne pourront plus accéder à l'organisation. Voulez-vous continuer ?",
Expand Down
4 changes: 4 additions & 0 deletions client/src/theme/components/modals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ ion-modal.join-by-link-modal::part(content) {
}
}

.user-details-modal {
--width: 642px;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is it coming from? copy/paste? is there a specific reflexion behind these values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These values are taken from the mockups, I was unsure if setting them explicitly was necessary or not (and now that you mention it, the height value is different in the revoked mockup so I'm guessing it's not)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes in lots of cases the modal will fits its content and it will be good enough.
@fabienSvtr a thought on this?

Copy link
Contributor

@fabienSvtr fabienSvtr Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to open an issue to harmonise all modal width. For this PR you can let the specific value.
I recently updated the Figma with width variables for that.
image


.file-upload-modal {
--width: 842px;
}
Expand Down
25 changes: 23 additions & 2 deletions client/src/views/users/ActiveUsersPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,19 @@ import {
import { routerNavigateTo } from '@/router';
import { Notification, NotificationKey, NotificationLevel, NotificationManager } from '@/services/notificationManager';
import UserContextMenu, { UserAction } from '@/views/users/UserContextMenu.vue';
import { IonCheckbox, IonContent, IonItem, IonLabel, IonList, IonListHeader, IonPage, IonText, popoverController } from '@ionic/vue';
import UserDetailsModal from '@/views/users/UserDetailsModal.vue';
import {
IonCheckbox,
IonContent,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonPage,
IonText,
modalController,
popoverController,
} from '@ionic/vue';
import { eye, personAdd, personRemove } from 'ionicons/icons';
import { Ref, computed, inject, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
Expand Down Expand Up @@ -327,7 +339,16 @@ async function revokeSelectedUsers(): Promise<void> {
}

async function details(user: UserInfo): Promise<void> {
console.log(`Show details on user ${user.humanHandle.label}`);
const modal = await modalController.create({
component: UserDetailsModal,
cssClass: 'user-details-modal',
componentProps: {
user: user,
},
});
await modal.present();
await modal.onWillDismiss();
await modal.dismiss();
}

function isCurrentUser(userId: UserID): boolean {
Expand Down
25 changes: 23 additions & 2 deletions client/src/views/users/RevokedUsersPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,19 @@ import UserListItem from '@/components/users/UserListItem.vue';
import { UserInfo, listRevokedUsers as parsecListRevokedUsers } from '@/parsec';
import { Notification, NotificationKey, NotificationLevel, NotificationManager } from '@/services/notificationManager';
import UserContextMenu, { UserAction } from '@/views/users/UserContextMenu.vue';
import { IonCheckbox, IonContent, IonItem, IonLabel, IonList, IonListHeader, IonPage, IonText, popoverController } from '@ionic/vue';
import UserDetailsModal from '@/views/users/UserDetailsModal.vue';
import {
IonCheckbox,
IonContent,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonPage,
IonText,
modalController,
popoverController,
} from '@ionic/vue';
import { eye } from 'ionicons/icons';
import { Ref, computed, inject, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
Expand Down Expand Up @@ -181,7 +193,16 @@ function selectAllUsers(checked: boolean): void {
}

async function details(user: UserInfo): Promise<void> {
console.log(`Show details on user ${user.humanHandle.label}`);
const modal = await modalController.create({
component: UserDetailsModal,
cssClass: 'user-details-modal',
componentProps: {
user: user,
},
});
await modal.present();
await modal.onWillDismiss();
await modal.dismiss();
}

async function openUserContextMenu(event: Event, user: UserInfo, onFinished?: () => void): Promise<void> {
Expand Down
190 changes: 190 additions & 0 deletions client/src/views/users/UserDetailsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<!-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -->

<template>
<ion-page class="modal">
<ms-modal
:title="$t('UsersPage.UserDetailsModal.title')"
:close-button="{ visible: true }"
>
<div class="details">
<div class="details-name">
<ion-text class="button-small">
{{ $t('UsersPage.UserDetailsModal.subtitles.name') }}
</ion-text>
<ion-text class="details-name__fullname subtitles-normal">
{{ user.humanHandle.label }}
</ion-text>
</div>
<div class="details-joined">
<ion-text class="button-small">
{{ $t('UsersPage.UserDetailsModal.subtitles.joined') }}
</ion-text>
<ion-text class="details-joined__date body-lg">
{{ timeSince(user.createdOn, '--', 'short') }}
</ion-text>
</div>
</div>
<div>
<ion-chip
v-if="user.isRevoked()"
color="danger"
>
<ion-icon
:icon="ellipse"
class="revoked"
/>
<ion-label>
{{ $t('UsersPage.UserDetailsModal.subtitles.revoked') }}
</ion-label>
</ion-chip>
</div>
<ion-list>
<ion-text class="button-small">
{{ $t('UsersPage.UserDetailsModal.subtitles.commonWorkspaces') }}
</ion-text>
<ion-card
v-for="workspace in workspaces"
:key="workspace.id"
:disabled="user.isRevoked()"
class="workspace-card"
>
<ion-card-content>
<ion-avatar class="card-content-icons">
<ion-icon
class="card-content-icons__item"
:icon="business"
/>
<ion-icon
class="cloud-overlay"
:class="workspace.availableOffline ? 'cloud-overlay-ok' : 'cloud-overlay-ko'"
:icon="workspace.availableOffline ? cloudDone : cloudOffline"
/>
<ion-text class="workspace-name cell">
{{ workspace.name }}
</ion-text>
<workspace-tag-role :role="workspace.role" />
</ion-avatar>
</ion-card-content>
</ion-card>
</ion-list>
<ion-button
class="close-button"
@click="cancel"
>
{{ $t('UsersPage.UserDetailsModal.actions.close') }}
</ion-button>
</ms-modal>
</ion-page>
</template>

<script setup lang="ts">
import { Formatters, FormattersKey } from '@/common/injectionKeys';
import { MsModal } from '@/components/core';
import { MsModalResult } from '@/components/core/ms-modal/types';
import WorkspaceTagRole from '@/components/workspaces/WorkspaceTagRole.vue';
import { UserInfo, WorkspaceRole } from '@/parsec';
import {
IonAvatar,
IonButton,
IonCard,
IonCardContent,
IonChip,
IonIcon,
IonLabel,
IonList,
IonPage,
IonText,
modalController,
} from '@ionic/vue';
import { business, cloudDone, cloudOffline, ellipse } from 'ionicons/icons';
import { defineProps, inject } from 'vue';

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { timeSince } = inject(FormattersKey)! as Formatters;
const workspaces = [
{
id: 'fake1',
name: 'Workspace1',
role: WorkspaceRole.Owner,
availableOffline: true,
},
{
id: 'fake2',
name: 'Workspace2',
role: WorkspaceRole.Contributor,
availableOffline: false,
},
];

defineProps<{
user: UserInfo;
}>();

async function cancel(): Promise<boolean> {
return await modalController.dismiss(null, MsModalResult.Cancel);
}
</script>

<style lang="scss" scoped>
.details {
display: flex;
margin-bottom: 1rem;
.details-name {
display: flex;
flex-direction: column;
width: 50%;
gap: 0.5rem;
}
.details-joined {
display: flex;
flex-direction: column;
width: 50%;
gap: 0.5rem;
}
}
.workspace-name {
margin-left: 1rem;
margin-right: auto;
}
.close-button {
display: flex;
margin-left: auto;
width: fit-content;
}
.card-content-icons {
margin: 0 auto 0.5rem;
position: relative;
height: fit-content;
display: flex;
justify-content: left;
align-items: center;
color: var(--parsec-color-light-primary-900);
width: 100%;

&__item {
font-size: 2rem;
}

.cloud-overlay {
position: absolute;
font-size: 1rem;
bottom: -10px;
left: 4%;
padding: 2px;
background: white;
border-radius: 50%;
}

.cloud-overlay-ok {
color: var(--parsec-color-light-primary-500);
}

.cloud-overlay-ko {
color: var(--parsec-color-light-secondary-text);
}
}
.revoked {
font-size: 0.5rem;
margin-right: 0.5rem;
}
</style>
35 changes: 35 additions & 0 deletions client/tests/e2e/specs/test_user_details_modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

describe('Check user details modal', () => {
beforeEach(() => {
cy.visitApp();
cy.login('Boby', 'P@ssw0rd.');
cy.get('.organization-card__manageBtn').click();
cy.get('.users-container').find('.user-list-item').eq(2).find('.options-button').invoke('show').click();
cy.get('.user-context-menu').find('.menu-list').find('ion-item').as('menuItems');
cy.get('@menuItems').eq(3).contains('View details').click();
});

afterEach(() => {
cy.dropTestbed();
});

it('Tests user details modal', () => {
cy.get('.user-details-modal').as('modal').find('ion-header').contains('User details');
cy.get('@modal').find('.ms-modal-content').as('modal-content').find('ion-text').eq(0).contains('Name');
// cspell:disable-next-line
cy.get('@modal-content').find('ion-text').eq(1).contains('Jaheira');
cy.get('@modal-content').find('ion-text').eq(2).contains('Joined');
cy.get('@modal-content').find('ion-text').eq(3).contains('one second ago');
cy.get('@modal-content').find('ion-text').eq(4).contains('Shared workspaces');
cy.get('@modal-content').find('ion-list').find('ion-card').as('workspace-cards').should('have.length', 2);
cy.get('@workspace-cards').eq(0).find('ion-text').contains('Workspace1');
cy.get('@workspace-cards').eq(0).find('ion-icon').should('exist');
cy.get('@workspace-cards').eq(0).find('ion-label').contains('Owner');
cy.get('@workspace-cards').eq(1).find('ion-text').contains('Workspace2');
cy.get('@workspace-cards').eq(1).find('ion-icon').should('exist');
cy.get('@workspace-cards').eq(1).find('ion-label').contains('Contributor');
cy.get('@modal-content').find('ion-button').contains('Close').click();
Ironicbay marked this conversation as resolved.
Show resolved Hide resolved
cy.get('@modal').should('not.exist');
});
});
Loading