diff --git a/apps/meteor/client/views/account/tokens/AccountTokensPage.js b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx
similarity index 50%
rename from apps/meteor/client/views/account/tokens/AccountTokensPage.js
rename to apps/meteor/client/views/account/tokens/AccountTokensPage.tsx
index da4f878977ec..ae594b7f6f48 100644
--- a/apps/meteor/client/views/account/tokens/AccountTokensPage.js
+++ b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx
@@ -1,21 +1,17 @@
import { useTranslation } from '@rocket.chat/ui-contexts';
-import React from 'react';
+import React, { ReactElement } from 'react';
import Page from '../../../components/Page';
-import { useEndpointData } from '../../../hooks/useEndpointData';
import AccountTokensTable from './AccountTokensTable';
-import AddToken from './AddToken';
-const AccountTokensPage = () => {
+const AccountTokensPage = (): ReactElement => {
const t = useTranslation();
- const { value: data, reload } = useEndpointData('/v1/users.getPersonalAccessTokens');
return (
-
-
+
);
diff --git a/apps/meteor/client/views/account/tokens/AccountTokensRow.tsx b/apps/meteor/client/views/account/tokens/AccountTokensRow.tsx
deleted file mode 100644
index 188ca546ed7e..000000000000
--- a/apps/meteor/client/views/account/tokens/AccountTokensRow.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Button, ButtonGroup, Icon, Table } from '@rocket.chat/fuselage';
-import { useTranslation } from '@rocket.chat/ui-contexts';
-import type { MomentInput } from 'moment';
-import React, { useCallback, FC } from 'react';
-
-import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
-
-type AccountTokensRowProps = {
- bypassTwoFactor: unknown;
- createdAt: MomentInput;
- isMedium: boolean;
- lastTokenPart: string;
- name: string;
- onRegenerate: (name: string) => void;
- onRemove: (name: string) => void;
-};
-
-const AccountTokensRow: FC = ({
- bypassTwoFactor,
- createdAt,
- isMedium,
- lastTokenPart,
- name,
- onRegenerate,
- onRemove,
-}) => {
- const t = useTranslation();
- const formatDateAndTime = useFormatDateAndTime();
- const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]);
- const handleRemove = useCallback(() => onRemove(name), [name, onRemove]);
-
- return (
-
-
- {name}
-
- {isMedium && {formatDateAndTime(createdAt)}}
- ...{lastTokenPart}
- {bypassTwoFactor ? t('Ignore') : t('Require')}
-
-
-
-
-
-
-
- );
-};
-
-export default AccountTokensRow;
diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable.js b/apps/meteor/client/views/account/tokens/AccountTokensTable.js
deleted file mode 100644
index 70fb7f80aad6..000000000000
--- a/apps/meteor/client/views/account/tokens/AccountTokensTable.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import { Box } from '@rocket.chat/fuselage';
-import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useMemo, useCallback, useState } from 'react';
-
-import GenericTable from '../../../components/GenericTable';
-import { useResizeInlineBreakpoint } from '../../../hooks/useResizeInlineBreakpoint';
-import AccountTokensRow from './AccountTokensRow';
-import InfoModal from './InfoModal';
-
-const AccountTokensTable = ({ data, reload }) => {
- const t = useTranslation();
- const dispatchToastMessage = useToastMessageDispatch();
- const setModal = useSetModal();
-
- const userId = useUserId();
-
- const regenerateToken = useMethod('personalAccessTokens:regenerateToken');
- const removeToken = useMethod('personalAccessTokens:removeToken');
-
- const [ref, isMedium] = useResizeInlineBreakpoint([600], 200);
-
- const [params, setParams] = useState({ current: 0, itemsPerPage: 25 });
-
- const tokensTotal = data && data.success ? data.tokens.length : 0;
-
- const { current, itemsPerPage } = params;
-
- const tokens = useMemo(() => {
- if (!data) {
- return null;
- }
- if (!data.success) {
- return [];
- }
- const sliceStart = current > tokensTotal ? tokensTotal - itemsPerPage : current;
- return data.tokens.slice(sliceStart, sliceStart + itemsPerPage);
- }, [current, data, itemsPerPage, tokensTotal]);
-
- const closeModal = useCallback(() => setModal(null), [setModal]);
-
- const header = useMemo(
- () =>
- [
- {t('API_Personal_Access_Token_Name')},
- isMedium && {t('Created_at')},
- {t('Last_token_part')},
- {t('Two Factor Authentication')},
- ,
- ].filter(Boolean),
- [isMedium, t],
- );
-
- const onRegenerate = useCallback(
- (name) => {
- const onConfirm = async () => {
- try {
- setModal(null);
-
- const token = await regenerateToken({ tokenName: name });
-
- setModal(
-
- }
- confirmText={t('ok')}
- onConfirm={closeModal}
- />,
- );
-
- reload();
- } catch (e) {
- setModal(null);
- dispatchToastMessage({ type: 'error', message: e });
- }
- };
-
- setModal(
- ,
- );
- },
- [closeModal, dispatchToastMessage, regenerateToken, reload, setModal, t, userId],
- );
-
- const onRemove = useCallback(
- (name) => {
- const onConfirm = async () => {
- try {
- await removeToken({ tokenName: name });
-
- dispatchToastMessage({ type: 'success', message: t('Removed') });
- reload();
- closeModal();
- } catch (e) {
- dispatchToastMessage({ type: 'error', message: e });
- }
- };
-
- setModal(
- ,
- );
- },
- [closeModal, dispatchToastMessage, reload, removeToken, setModal, t],
- );
-
- return (
-
- {useCallback(
- (props) => (
-
- ),
- [isMedium, onRegenerate, onRemove],
- )}
-
- );
-};
-
-export default AccountTokensTable;
diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensRow.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensRow.tsx
new file mode 100644
index 000000000000..31477fa347a2
--- /dev/null
+++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensRow.tsx
@@ -0,0 +1,46 @@
+import { IPersonalAccessToken, Serialized } from '@rocket.chat/core-typings';
+import { ButtonGroup, IconButton, TableRow, TableCell } from '@rocket.chat/fuselage';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import React, { useCallback, FC } from 'react';
+
+import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime';
+
+type AccountTokensRowProps = {
+ isMedium: boolean;
+ onRegenerate: (name: string) => void;
+ onRemove: (name: string) => void;
+} & Serialized>;
+
+const AccountTokensRow: FC = ({
+ bypassTwoFactor,
+ createdAt,
+ isMedium,
+ lastTokenPart,
+ name,
+ onRegenerate,
+ onRemove,
+}) => {
+ const t = useTranslation();
+ const formatDateAndTime = useFormatDateAndTime();
+ const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]);
+ const handleRemove = useCallback(() => onRemove(name), [name, onRemove]);
+
+ return (
+
+
+ {name}
+
+ {isMedium && {formatDateAndTime(createdAt)}}
+ ...{lastTokenPart}
+ {bypassTwoFactor ? t('Ignore') : t('Require')}
+
+
+
+
+
+
+
+ );
+};
+
+export default AccountTokensRow;
diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx
new file mode 100644
index 000000000000..a08422285e20
--- /dev/null
+++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx
@@ -0,0 +1,185 @@
+import { Box, Pagination, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
+import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
+import React, { ReactElement, RefObject, useMemo, useCallback } from 'react';
+
+import GenericModal from '../../../../components/GenericModal';
+import {
+ GenericTable,
+ GenericTableHeader,
+ GenericTableBody,
+ GenericTableLoadingTable,
+ GenericTableHeaderCell,
+} from '../../../../components/GenericTable';
+import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
+import { useEndpointData } from '../../../../hooks/useEndpointData';
+import { useResizeInlineBreakpoint } from '../../../../hooks/useResizeInlineBreakpoint';
+import { AsyncStatePhase } from '../../../../lib/asyncState';
+import AccountTokensRow from './AccountTokensRow';
+import AddToken from './AddToken';
+
+const AccountTokensTable = (): ReactElement => {
+ const t = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+ const setModal = useSetModal();
+ const userId = useUserId();
+
+ const regenerateToken = useMethod('personalAccessTokens:regenerateToken');
+ const removeToken = useMethod('personalAccessTokens:removeToken');
+ const { value: data, phase, error, reload } = useEndpointData('/v1/users.getPersonalAccessTokens');
+
+ const [ref, isMedium] = useResizeInlineBreakpoint([600], 200) as [RefObject, boolean];
+
+ const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
+
+ const filteredTokens = useMemo(() => {
+ if (!data?.tokens) {
+ return null;
+ }
+
+ const sliceStart = current > data?.tokens.length ? data?.tokens.length - itemsPerPage : current;
+ return data?.tokens.slice(sliceStart, sliceStart + itemsPerPage);
+ }, [current, data?.tokens, itemsPerPage]);
+
+ const closeModal = useCallback(() => setModal(null), [setModal]);
+
+ const headers = useMemo(
+ () =>
+ [
+ {t('API_Personal_Access_Token_Name')},
+ isMedium && {t('Created_at')},
+ {t('Last_token_part')},
+ {t('Two Factor Authentication')},
+ ,
+ ].filter(Boolean),
+ [isMedium, t],
+ );
+
+ const handleRegenerate = useCallback(
+ (name) => {
+ const onConfirm: () => Promise = async () => {
+ try {
+ setModal(null);
+ const token = await regenerateToken({ tokenName: name });
+
+ setModal(
+
+
+ ,
+ );
+
+ reload();
+ } catch (error) {
+ setModal(null);
+ dispatchToastMessage({ type: 'error', message: error as Error });
+ }
+ };
+
+ setModal(
+
+ {t('API_Personal_Access_Tokens_Regenerate_Modal')}
+ ,
+ );
+ },
+ [closeModal, dispatchToastMessage, regenerateToken, reload, setModal, t, userId],
+ );
+
+ const handleRemove = useCallback(
+ (name) => {
+ const onConfirm: () => Promise = async () => {
+ try {
+ await removeToken({ tokenName: name });
+ dispatchToastMessage({ type: 'success', message: t('Token_has_been_removed') });
+ reload();
+ closeModal();
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error as Error });
+ }
+ };
+
+ setModal(
+
+ {t('API_Personal_Access_Tokens_Remove_Modal')}
+ ,
+ );
+ },
+ [closeModal, dispatchToastMessage, reload, removeToken, setModal, t],
+ );
+
+ if (phase === AsyncStatePhase.REJECTED) {
+ return (
+
+
+
+ {t('Something_Went_Wrong')}
+ {t('We_Could_not_retrive_any_data')}
+ {error?.message}
+
+ {t('Retry')}
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {phase === AsyncStatePhase.LOADING && (
+
+ {headers}
+ {phase === AsyncStatePhase.LOADING && }
+
+ )}
+ {filteredTokens && filteredTokens?.length > 0 && phase === AsyncStatePhase.RESOLVED && (
+ <>
+
+ {headers}
+
+ {phase === AsyncStatePhase.RESOLVED &&
+ filteredTokens &&
+ filteredTokens.map((filteredToken) => (
+
+ ))}
+
+
+
+ >
+ )}
+ {phase === AsyncStatePhase.RESOLVED && filteredTokens?.length === 0 && (
+
+
+ {t('No_results_found')}
+
+ )}
+ >
+ );
+};
+
+export default AccountTokensTable;
diff --git a/apps/meteor/client/views/account/tokens/AddToken.js b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx
similarity index 54%
rename from apps/meteor/client/views/account/tokens/AddToken.js
rename to apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx
index dbcdbcae0a0f..dc695fc8eb06 100644
--- a/apps/meteor/client/views/account/tokens/AddToken.js
+++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx
@@ -1,77 +1,65 @@
import { Box, TextInput, Button, Field, FieldGroup, Margins, CheckBox } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useCallback } from 'react';
+import React, { ReactElement, useCallback } from 'react';
-import { useForm } from '../../../hooks/useForm';
-import InfoModal from './InfoModal';
+import GenericModal from '../../../../components/GenericModal';
+import { useForm } from '../../../../hooks/useForm';
const initialValues = {
name: '',
bypassTwoFactor: false,
};
-const AddToken = ({ onDidAddToken, ...props }) => {
+const AddToken = ({ reload, ...props }: { reload: () => void }): ReactElement => {
const t = useTranslation();
+ const userId = useUserId();
const createTokenFn = useMethod('personalAccessTokens:generateToken');
const dispatchToastMessage = useToastMessageDispatch();
+ const bypassTwoFactorCheckboxId = useUniqueId();
const setModal = useSetModal();
- const userId = useUserId();
-
const { values, handlers, reset } = useForm(initialValues);
-
- const { name, bypassTwoFactor } = values;
+ const { name, bypassTwoFactor } = values as typeof initialValues;
const { handleName, handleBypassTwoFactor } = handlers;
- const closeModal = useCallback(() => setModal(null), [setModal]);
-
- const handleAdd = useCallback(async () => {
+ const handleAddToken = useCallback(async () => {
try {
const token = await createTokenFn({ tokenName: name, bypassTwoFactor });
setModal(
-
- }
- confirmText={t('ok')}
- onConfirm={closeModal}
- />,
+ setModal(null)}>
+
+ ,
);
reset();
- onDidAddToken();
+ reload();
} catch (error) {
- dispatchToastMessage({ type: 'error', message: error });
+ dispatchToastMessage({ type: 'error', message: error as Error });
}
- }, [bypassTwoFactor, closeModal, createTokenFn, dispatchToastMessage, name, onDidAddToken, reset, setModal, t, userId]);
-
- const bypassTwoFactorCheckboxId = useUniqueId();
+ }, [bypassTwoFactor, createTokenFn, dispatchToastMessage, name, reload, reset, setModal, t, userId]);
return (
-
+
-
-
- {t('Ignore')} {t('Two Factor Authentication')}
-
+ {t('Ignore_Two_Factor_Authentication')}
diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/index.ts b/apps/meteor/client/views/account/tokens/AccountTokensTable/index.ts
new file mode 100644
index 000000000000..0643bb37626d
--- /dev/null
+++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/index.ts
@@ -0,0 +1 @@
+export { default } from './AccountTokensTable';
diff --git a/apps/meteor/client/views/account/tokens/InfoModal.tsx b/apps/meteor/client/views/account/tokens/InfoModal.tsx
deleted file mode 100644
index 25e473ad49e2..000000000000
--- a/apps/meteor/client/views/account/tokens/InfoModal.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Button, ButtonGroup, Modal } from '@rocket.chat/fuselage';
-import React, { FC, ReactNode } from 'react';
-
-type InfoModalProps = {
- title: string;
- content: ReactNode;
- icon: ReactNode;
- confirmText: string;
- cancelText: string;
- onConfirm: () => void;
- onClose: () => void;
-};
-
-const InfoModal: FC = ({ title, content, icon, confirmText, cancelText, onConfirm, onClose, ...props }) => (
-
-
- {icon}
- {title}
-
-
- {content}
-
-
- {cancelText && {cancelText}}
- {confirmText && onConfirm && (
-
- {confirmText}
-
- )}
-
-
-
-);
-
-export default InfoModal;
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index 74df500ad0fd..94a55cb88d6b 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -2313,6 +2313,7 @@
"Iframe_X_Frame_Options_Description": "Options to X-Frame-Options. [You can see all the options here.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Syntax)",
"Ignore": "Ignore",
"Ignored": "Ignored",
+ "Ignore_Two_Factor_Authentication": "Ignore Two Factor Authentication",
"Images": "Images",
"IMAP_intercepter_already_running": "IMAP intercepter already running",
"IMAP_intercepter_Not_running": "IMAP intercepter Not running",
@@ -4565,6 +4566,7 @@
"Token": "Token",
"Token_Access": "Token Access",
"Token_Controlled_Access": "Token Controlled Access",
+ "Token_has_been_removed": "Token has been removed",
"Token_required": "Token required",
"Tokens_Minimum_Needed_Balance": "Minimum needed token balance",
"Tokens_Minimum_Needed_Balance_Description": "Set minimum needed balance on each token. Blank or \"0\" for not limit.",
diff --git a/apps/meteor/tests/e2e/10-user-preferences.spec.ts b/apps/meteor/tests/e2e/10-user-preferences.spec.ts
index ee2678025663..bf41ad363be3 100644
--- a/apps/meteor/tests/e2e/10-user-preferences.spec.ts
+++ b/apps/meteor/tests/e2e/10-user-preferences.spec.ts
@@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker';
import { test, expect } from './utils/test';
import { Auth, HomeChannel, AccountProfile } from './page-objects';
-test.describe('User preferences', () => {
+test.describe('My Account', () => {
let pageAuth: Auth;
let pageHomeChannel: HomeChannel;
let pageAccountProfile: AccountProfile;
@@ -11,39 +11,80 @@ test.describe('User preferences', () => {
const newName = faker.name.findName();
const newUsername = faker.internet.userName(newName);
+ const token = faker.random.alpha(10);
+
test.beforeEach(async ({ page }) => {
pageAuth = new Auth(page);
pageHomeChannel = new HomeChannel(page);
pageAccountProfile = new AccountProfile(page);
});
- test.beforeEach(async () => {
- await pageAuth.doLogin();
- });
+ test.describe('User Profile', () => {
+ test.beforeEach(async () => {
+ await pageAuth.doLogin();
+ });
- test('expect update profile with new name and username', async ({ page }) => {
- await pageHomeChannel.sidenav.doOpenProfile();
+ test('expect update profile with new name and username', async ({ page }) => {
+ await pageHomeChannel.sidenav.doOpenProfile();
- await pageAccountProfile.inputName.fill(newName);
- await pageAccountProfile.inputUsername.fill(newUsername);
- await pageAccountProfile.btnSubmit.click();
- await page.goto('/');
- });
+ await pageAccountProfile.inputName.fill(newName);
+ await pageAccountProfile.inputUsername.fill(newUsername);
+ await pageAccountProfile.btnSubmit.click();
+ await page.goto('/');
+ });
+
+ test('expect show new username in the last message', async () => {
+ await pageHomeChannel.sidenav.doOpenChat('general');
+ await pageHomeChannel.content.doSendMessage('any_message');
+
+ await expect(pageHomeChannel.content.lastUserMessageNotSequential).toContainText(newUsername);
+ });
- test('expect show new username in the last message', async () => {
- await pageHomeChannel.sidenav.doOpenChat('general');
- await pageHomeChannel.content.doSendMessage('any_message');
+ test('expect show new username in card and profile', async () => {
+ await pageHomeChannel.sidenav.doOpenChat('general');
+ await pageHomeChannel.content.doSendMessage('any_message');
- await expect(pageHomeChannel.content.lastUserMessageNotSequential).toContainText(newUsername);
+ await pageHomeChannel.content.lastUserMessageNotSequential.locator('figure').click();
+ await pageHomeChannel.content.userCardLinkProfile.click();
+
+ await expect(pageHomeChannel.tabs.userInfoUsername).toHaveText(newUsername);
+ });
});
- test('expect show new username in card and profile', async () => {
- await pageHomeChannel.sidenav.doOpenChat('general');
- await pageHomeChannel.content.doSendMessage('any_message');
+ test.describe('Personal Access Tokens', () => {
+ test.beforeEach(async () => {
+ await pageAuth.doLogin();
+ await pageHomeChannel.sidenav.doOpenProfile();
+ await pageAccountProfile.sidenav.linkTokens.click();
+ });
+
+ test('expect show empty personal access tokens table', async () => {
+ await expect(pageAccountProfile.tokensTableEmpty).toBeVisible();
+ await expect(pageAccountProfile.inputToken).toBeVisible();
+ });
+
+ test('expect show new personal token', async () => {
+ await pageAccountProfile.inputToken.type(token);
+ await pageAccountProfile.btnTokensAdd.click();
+ await expect(pageAccountProfile.tokenAddedModal).toBeVisible();
+ });
+
+ test('expect not allow add new personal token with same name', async ({ page }) => {
+ await pageAccountProfile.inputToken.type(token);
+ await pageAccountProfile.btnTokensAdd.click();
+ await expect(page.locator('.rcx-toastbar.rcx-toastbar--error')).toBeVisible();
+ });
- await pageHomeChannel.content.lastUserMessageNotSequential.locator('figure').click();
- await pageHomeChannel.content.userCardLinkProfile.click();
+ test('expect regenerate personal token', async () => {
+ await pageAccountProfile.tokenInTable(token).locator('button >> nth=0').click();
+ await pageAccountProfile.btnRegenerateTokenModal.click();
+ await expect(pageAccountProfile.tokenAddedModal).toBeVisible();
+ });
- await expect(pageHomeChannel.tabs.userInfoUsername).toHaveText(newUsername);
+ test('expect delete personal token', async ({ page }) => {
+ await pageAccountProfile.tokenInTable(token).locator('button >> nth=1').click();
+ await pageAccountProfile.btnRemoveTokenModal.click();
+ await expect(page.locator('.rcx-toastbar.rcx-toastbar--success')).toBeVisible();
+ });
});
});
diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts
index 5e951a1d64ba..ecf167e266bb 100644
--- a/apps/meteor/tests/e2e/page-objects/account-profile.ts
+++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts
@@ -1,10 +1,15 @@
import { Locator, Page } from '@playwright/test';
+import { AccountSidenav } from './fragments/account-sidenav';
+
export class AccountProfile {
private readonly page: Page;
+ readonly sidenav: AccountSidenav;
+
constructor(page: Page) {
this.page = page;
+ this.sidenav = new AccountSidenav(page);
}
get inputName(): Locator {
@@ -26,4 +31,32 @@ export class AccountProfile {
get emailTextInput(): Locator {
return this.page.locator('//label[contains(text(), "Email")]/..//input');
}
+
+ get inputToken(): Locator {
+ return this.page.locator('[data-qa="PersonalTokenField"]');
+ }
+
+ get tokensTableEmpty(): Locator {
+ return this.page.locator('//div[contains(text(), "No results found")]');
+ }
+
+ get btnTokensAdd(): Locator {
+ return this.page.locator('//button[contains(text(), "Add")]');
+ }
+
+ get tokenAddedModal(): Locator {
+ return this.page.locator("//div[text()='Personal Access Token successfully generated']");
+ }
+
+ tokenInTable(name: string): Locator {
+ return this.page.locator(`tr[qa-token-name="${name}"]`);
+ }
+
+ get btnRegenerateTokenModal(): Locator {
+ return this.page.locator('//button[contains(text(), "Regenerate token")]');
+ }
+
+ get btnRemoveTokenModal(): Locator {
+ return this.page.locator('//button[contains(text(), "Remove")]');
+ }
}
diff --git a/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts
new file mode 100644
index 000000000000..714adab52a1a
--- /dev/null
+++ b/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts
@@ -0,0 +1,13 @@
+import { Locator, Page } from '@playwright/test';
+
+export class AccountSidenav {
+ private readonly page: Page;
+
+ constructor(page: Page) {
+ this.page = page;
+ }
+
+ get linkTokens(): Locator {
+ return this.page.locator('.flex-nav [href="/account/tokens"]');
+ }
+}
diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts
index b4a5c5e7ed2f..89bcd0f2b7bf 100644
--- a/packages/core-typings/src/IUser.ts
+++ b/packages/core-typings/src/IUser.ts
@@ -16,7 +16,7 @@ export interface IPersonalAccessToken extends ILoginToken {
type: 'personalAccessToken';
createdAt: Date;
lastTokenPart: string;
- name?: string;
+ name: string;
bypassTwoFactor?: boolean;
}
diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts
index 1b07ad614ff7..ca42e250f5d8 100644
--- a/packages/rest-typings/src/v1/users.ts
+++ b/packages/rest-typings/src/v1/users.ts
@@ -1,4 +1,4 @@
-import type { IExportOperation, ISubscription, ITeam, IUser } from '@rocket.chat/core-typings';
+import type { IExportOperation, ISubscription, ITeam, IUser, IPersonalAccessToken } from '@rocket.chat/core-typings';
import Ajv from 'ajv';
import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST';
@@ -104,6 +104,8 @@ export type UserPresence = Readonly<
Partial> & Required>
>;
+export type UserPersonalTokens = Pick & { createdAt: string };
+
export type UsersEndpoints = {
'/v1/users.2fa.enableEmail': {
POST: () => void;
@@ -193,12 +195,7 @@ export type UsersEndpoints = {
'/v1/users.getPersonalAccessTokens': {
GET: () => {
- tokens: {
- name?: string;
- createdAt: string;
- lastTokenPart: string;
- bypassTwoFactor: boolean;
- }[];
+ tokens: UserPersonalTokens[];
};
};
'/v1/users.regeneratePersonalAccessToken': {