Skip to content

Commit

Permalink
Exporter les membres d'un salon dans un fichier csv (#625)
Browse files Browse the repository at this point in the history
* Add tab to download csv file of the list of users in a room

* Put download file in tchap utils, and change csv to txt donwload

* Add tooltip

* Add tab to download csv file of the list of users in a room (#613)

* Add tab to download csv file of the list of users in a room

* Put download file in tchap utils, and change csv to txt donwload

* Add tooltip

* Add tests for button download txt file

* Undo cypress modifs

* Fix eslint

* Add cypress test

* Fix eslint

* Cleanup

* Move button to Members menu

* Add back tooltip, using AccessibleTooltipButton

* Move button to the top, with other button

* Use FileDownloader from react-sdk rather than implement our own

* Remove previous implementation

* add room name in exported filename

* Rename patch, shorter name

* Update translations

* Add icon (better icon coming)

* Button styling

* Final icon

* Better names for things

* Separate ids with commas in text file

* Cypress test WIP

* Cypress : use normal room name

* Cypress tooltip test

* Cypress : leave room when done

* Rename file

* Fit linter

* Make separate component for minimal footprint in patch

---------

Co-authored-by: Estelle Comment <[email protected]>
  • Loading branch information
aulamber and estellecomment authored Aug 29, 2023
1 parent 1db4922 commit 11961f2
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 5 deletions.
46 changes: 46 additions & 0 deletions cypress/e2e/export-room-members/export-room-members.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/// <reference types="cypress" />

import RoomUtils from "../../utils/room-utils";
import RandomUtils from "../../utils/random-utils";
import { normalize } from "../../../yarn-linked-dependencies/matrix-js-sdk/src/utils";

describe("Export room members feature", () => {
const homeserverUrl = Cypress.env("E2E_TEST_USER_HOMESERVER_URL");
const homeserverShort = Cypress.env("E2E_TEST_USER_HOMESERVER_SHORT");
const email = Cypress.env("E2E_TEST_USER_EMAIL");
const password = Cypress.env("E2E_TEST_USER_PASSWORD");
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
// This will fail if email has special characters.
const userId = "@" + email.replace("@", "-") + ":" + homeserverShort;

beforeEach(() => {
cy.loginUser(homeserverUrl, email, password);
});

afterEach(() => {});

it("should display the tooltip on button hover", () => {
const roomName = "test/" + today + "/export_room_members/" + RandomUtils.generateRandom(4);
RoomUtils.createPublicRoom(roomName).then((roomId) => {
RoomUtils.openPeopleMenu(roomName);
cy.get('[data-testid="tc_exportRoomMembersButton"]')
.trigger("mouseover")
.get(".tc_exportRoomMembersTooltip"); // tooltip should show
cy.leaveRoom(roomId);
});
});

it("downloads the file when button is clicked", () => {
const roomName = "test/" + today + "/export_room_members/" + RandomUtils.generateRandom(4);
const normalizedRoomName = normalize(roomName);
RoomUtils.createPublicRoom(roomName).then((roomId) => {
RoomUtils.openPeopleMenu(roomName);
cy.get('[data-testid="tc_exportRoomMembersButton"]')
.click()
.then(() => {
cy.readFile("cypress/downloads/membres_de_" + normalizedRoomName + ".txt").should("eq", userId);
});
cy.leaveRoom(roomId);
});
});
});
21 changes: 16 additions & 5 deletions cypress/utils/room-utils.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import Chainable = Cypress.Chainable;
import TchapCreateRoom from "../../src/tchap/lib/createTchapRoom";
import { TchapRoomType } from "../../src/tchap/@types/tchap";

export default class RoomUtils {
public static createPublicRoom(roomName: string): Chainable<string> {
return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Forum).createOpts);
return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Forum, false).createOpts);
}
public static createPrivateRoom(roomName: string): Chainable<string> {
return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Private).createOpts);
return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.Private, false).createOpts);
}
public static createPrivateWithExternalRoom(roomName: string): Chainable<string> {
return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.External).createOpts);
return cy.createRoom(TchapCreateRoom.roomCreateOptions(roomName, TchapRoomType.External, false).createOpts);
}
public static openRoom(roomName: string): Chainable<JQuery<HTMLElement>> {
return cy.get('[aria-label="' + roomName + '"]').click();
}
public static openRoomAccessSettings(roomName: string): Chainable<JQuery<HTMLElement>> {
//open room
cy.get('[aria-label="' + roomName + '"]').click();
cy.get('[aria-label="' + roomName + '"]').click(); //open room
cy.get(".mx_RoomHeader_chevron").click();
cy.get('[aria-label="Paramètres"] > .mx_IconizedContextMenu_label').click();
return cy.get('[data-testid="settings-tab-ROOM_SECURITY_TAB"] > .mx_TabbedView_tabLabel_text').click();
}
public static openRoomInformation(roomName: string): Chainable<JQuery<HTMLElement>> {
cy.get('[aria-label="' + roomName + '"]').click(); //open room
return cy.get('[aria-label="Information du salon"]').click();
}
public static openPeopleMenu(roomName: string): Chainable<JQuery<HTMLElement>> {
this.openRoomInformation(roomName);
return cy.get(".mx_RoomSummaryCard_icon_people").click();
}
}
25 changes: 25 additions & 0 deletions patches/export-room-members/matrix-react-sdk+3.71.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
diff --git a/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx b/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx
index 44dba2e..69ffbed 100644
--- a/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx
+++ b/node_modules/matrix-react-sdk/src/components/views/rooms/MemberList.tsx
@@ -46,6 +46,7 @@ import { shouldShowComponent } from "../../../customisations/helpers/UIComponent
import { UIComponent } from "../../../settings/UIFeature";
import PosthogTrackers from "../../../PosthogTrackers";
import { SDKContext } from "../../../contexts/SDKContext";
+import TchapExportMembersButton from "../../../../../../src/tchap/components/views/rooms/TchapExportMembersButton"; // TCHAP

const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@@ -409,6 +410,12 @@ export default class MemberList extends React.Component<IProps, IState> {
<React.Fragment>
{scopeHeader}
{inviteButton}
+ {/** TCHAP */}
+ <TchapExportMembersButton
+ room={room}
+ roomMembersIds={this.state.filteredJoinedMembers.map(roomMember => roomMember.userId)}>
+ </TchapExportMembersButton>
+ {/** end TCHAP */}
</React.Fragment>
}
footer={footer}
7 changes: 7 additions & 0 deletions patches/patches.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,5 +210,12 @@
"src/components/structures/RoomSearchView.tsx",
"src/components/structures/RoomStatusBar.tsx"
]
},
"export-room-members": {
"github-issue": "https://github.com/tchapgouv/tchap-web-v4/issues/593",
"package": "matrix-react-sdk",
"files": [
"src/components/views/rooms/MemberList.tsx"
]
}
}
31 changes: 31 additions & 0 deletions res/css/views/rooms/TchapExportMembersButton.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright DINUM 2023
*/

.mx_AccessibleButton.mx_AccessibleButton_hasKind.tc_MemberList_export {
/* Copy the style from mx_MemberList_invite button just above. For some reason it doesn't follow the standard style. */
display: flex;
justify-content: center;
margin: 5px 9px 9px;
padding: 0px;
border-radius: 4px;
font-weight: 600;
}

.tc_MemberList_export span {
padding: 8px 0;
display: inline-flex;

&::before {
content: "";
display: inline-block;
background-color: var(--accent);
mask-image: url("../../../img/tchap/user-export.svg");
mask-position: center;
mask-repeat: no-repeat;
mask-size: 20px;
width: 20px;
height: 20px;
margin-right: 5px;
}
}
4 changes: 4 additions & 0 deletions res/img/tchap/user-export.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions src/tchap/components/views/rooms/TchapExportMembersButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright DINUM 2023
*/

import React from "react";

import { FileDownloader } from "matrix-react-sdk/src/utils/FileDownloader";
import AccessibleTooltipButton from "matrix-react-sdk/src/components/views/elements/AccessibleTooltipButton";
import { ButtonEvent } from "matrix-react-sdk/src/components/views/elements/AccessibleButton";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from "matrix-react-sdk/src/languageHandler";

import "../../../../../res/css/views/rooms/TchapExportMembersButton.pcss";

interface IProps {
room: Room;
roomMembersIds: Array<string>;
}

interface IState {
}

export default class MemberList extends React.Component<IProps, IState> {
private downloader = new FileDownloader();

public constructor(props: IProps) {
super(props);
}

private onExportButtonClick = (ev: ButtonEvent): void => {
const blob = new Blob([this.props.roomMembersIds.join()], { type : 'plain/text' })

const filename = _t('members_of_%(roomName)s.txt', {
roomName: this.props.room.normalizedName,
});

this.downloader.download({
blob: blob,
name: filename,
});

return;
};

public render(): React.ReactNode {
if (this.props.room?.getMyMembership() === "join" && !this.props.room.isSpaceRoom() && this.props.roomMembersIds.length > 0) {
return (
<AccessibleTooltipButton
data-testid="tc_exportRoomMembersButton"
className="tc_MemberList_export mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
onClick={this.onExportButtonClick}
tooltip={_t("Download the list of all this room's members, in a text file. Useful for adding them all to another room.")}
tooltipClassName="tc_exportRoomMembersTooltip"
>
<span>{_t("Export room members")}</span>
</AccessibleTooltipButton>
);
}
return null;
}

}
12 changes: 12 additions & 0 deletions src/tchap/i18n/strings/tchap_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,18 @@
"fr": "Veuillez ne continuer que si vous êtes certain d’avoir perdu tous vos autres appareils et votre Code de Récupération.",
"en": "Please only proceed if you're sure you've lost all of your other devices and your Recovery Code."
},
"Export room members": {
"fr": "Exporter les membres du salon",
"en": "Export room members"
},
"Download the list of all this room's members, in a text file. Useful for adding them all to another room.": {
"fr": "Récupérer la liste des membres de ce salon, dans un fichier texte. Utile pour inviter toutes ces personnes à un autre salon.",
"en": "Download the list of all this room's members, in a text file. Useful for adding them all to another room."
},
"members_of_%(roomName)s.txt": {
"fr": "membres_de_%(roomName)s.txt",
"en": "members_of_%(roomName)s.txt"
},
"Destroy cross-signing keys?": {
"fr": "Réinitialiser les clés de signature croisée ?",
"en": "Reset cross-signing keys?"
Expand Down

0 comments on commit 11961f2

Please sign in to comment.