Skip to content

Commit

Permalink
[MS] User details modal
Browse files Browse the repository at this point in the history
  • Loading branch information
Ironicbay committed Dec 18, 2023
1 parent 4e309b4 commit 5fd1a1d
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 6 deletions.
12 changes: 12 additions & 0 deletions client/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,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 @@ -780,6 +780,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;
}

.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 @@ -145,11 +145,23 @@

<script setup lang="ts">
import { ref, Ref, computed, onMounted, inject, watch, onUnmounted } from 'vue';
import { IonContent, IonItem, IonList, IonPage, IonLabel, IonListHeader, IonCheckbox, IonText, popoverController } from '@ionic/vue';
import {
IonContent,
IonItem,
IonList,
IonPage,
IonLabel,
IonListHeader,
IonCheckbox,
IonText,
popoverController,
modalController,
} from '@ionic/vue';
import { personRemove, personAdd, eye } from 'ionicons/icons';
import UserListItem from '@/components/users/UserListItem.vue';
import UserCard from '@/components/users/UserCard.vue';
import UserContextMenu, { UserAction } from '@/views/users/UserContextMenu.vue';
import UserDetailsModal from '@/views/users/UserDetailsModal.vue';
import { DisplayState, Answer, askQuestion, MsActionBarButton, MsActionBar, MsGridListToggle } from '@/components/core';
import { routerNavigateTo } from '@/router';
import {
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 @@ -112,10 +112,22 @@

<script setup lang="ts">
import { ref, Ref, computed, onMounted, inject } from 'vue';
import { IonContent, IonItem, IonList, IonPage, IonLabel, IonListHeader, IonCheckbox, IonText, popoverController } from '@ionic/vue';
import {
IonContent,
IonItem,
IonList,
IonPage,
IonLabel,
IonListHeader,
IonCheckbox,
IonText,
popoverController,
modalController,
} from '@ionic/vue';
import { eye } from 'ionicons/icons';
import UserListItem from '@/components/users/UserListItem.vue';
import UserCard from '@/components/users/UserCard.vue';
import UserDetailsModal from '@/views/users/UserDetailsModal.vue';
import { DisplayState, MsActionBarButton, MsGridListToggle, MsActionBar } from '@/components/core';
import UserContextMenu, { UserAction } from '@/views/users/UserContextMenu.vue';
import { UserInfo, listRevokedUsers as parsecListRevokedUsers } from '@/parsec';
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
4 changes: 2 additions & 2 deletions client/src/views/users/UserContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export enum UserAction {
</script>

<script setup lang="ts">
import { IonContent, IonItem, IonLabel, IonList, popoverController, IonIcon, IonItemGroup } from '@ionic/vue';
import { personRemove, informationCircle } from 'ionicons/icons';
import { IonContent, IonIcon, IonItem, IonItemGroup, IonLabel, IonList, popoverController } from '@ionic/vue';
import { informationCircle, personRemove } from 'ionicons/icons';
defineProps<{
isRevoked: boolean;
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 { MsModal } from '@/components/core';
import { MsModalResult } from '@/components/core/ms-modal/types';
import {
IonChip,
IonAvatar,
IonButton,
IonList,
IonPage,
IonText,
modalController,
IonCard,
IonCardContent,
IonIcon,
IonLabel,
} from '@ionic/vue';
import { business, cloudDone, cloudOffline, ellipse } from 'ionicons/icons';
import { UserInfo, WorkspaceRole } from '@/parsec';
import { FormattersKey, Formatters } from '@/common/injectionKeys';
import { defineProps, inject } from 'vue';
import WorkspaceTagRole from '@/components/workspaces/WorkspaceTagRole.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;
flex-grow: 1;
gap: 0.5rem;
}
.details-joined {
display: flex;
flex-direction: column;
flex-grow: 1;
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();
cy.get('@modal').should('not.exist');
});
});

0 comments on commit 5fd1a1d

Please sign in to comment.