mirror of
https://github.com/element-hq/element-web
synced 2024-11-21 16:55:34 +03:00
New user profile UI in User Settings (#12548)
* New user profile UI in User Settings Using new Edit In Place component. * Show avatar upload error * Fix avatar upload error * Wire up errors & feedback for display name setting * Implement avatar upload / remove progress toast * Add 768px breakpoint * Fix room profile display * Update to released compund-web with required components / fixes * Require compound-web 4.4.0 because we do need it * Update snapshots Because of course all the auto-generated IDs of unrelated things have changed. * Fix duplicate import * Fix CSS comment * Update snapshot * Run all the tests so the ids stay the same * Start of a test for ProfileSettings * More tests * Test that a toast appears * Test ToastRack * Update snapshots * Add the usernamee control * Fix playwright tests * New compound version for editinplace fixes * Fix useId to not just generate a constant ID * Use the label in the username component * Fix widths of test boxes * Update screenshots * Put ^ back on compound-web version * Split CSS for room & user profile settings and name the components correspondingly * Fix playwright test * Update room settings screenshot * Use original screenshot instead * Fix styling of unrelated buttons Needed to be added in other places otherwise the specificity changes. Also put the old screenshots back. * Add copyright year * Fix copyright year
This commit is contained in:
parent
c4c1faff97
commit
cfa322cd62
25 changed files with 919 additions and 307 deletions
|
@ -77,7 +77,7 @@
|
|||
"@sentry/browser": "^7.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@vector-im/compound-design-tokens": "^1.2.0",
|
||||
"@vector-im/compound-web": "^4.3.1",
|
||||
"@vector-im/compound-web": "^4.4.1",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
|
|
|
@ -21,8 +21,6 @@ const USER_NAME_NEW = "Alice";
|
|||
const IntegrationManager = "scalar.vector.im";
|
||||
|
||||
test.describe("General user settings tab", () => {
|
||||
let userId: string;
|
||||
|
||||
test.use({
|
||||
displayName: USER_NAME,
|
||||
config: {
|
||||
|
@ -34,18 +32,18 @@ test.describe("General user settings tab", () => {
|
|||
},
|
||||
});
|
||||
|
||||
test("should be rendered properly", async ({ uut }) => {
|
||||
test("should be rendered properly", async ({ uut, user }) => {
|
||||
await expect(uut).toMatchScreenshot("general.png");
|
||||
|
||||
// Assert that the top heading is rendered
|
||||
await expect(uut.getByRole("heading", { name: "General" })).toBeVisible();
|
||||
|
||||
const profile = uut.locator(".mx_ProfileSettings_profile");
|
||||
const profile = uut.locator(".mx_UserProfileSettings_profile");
|
||||
await profile.scrollIntoViewIfNeeded();
|
||||
await expect(profile.getByRole("textbox", { name: "Display Name" })).toHaveValue(USER_NAME);
|
||||
|
||||
// Assert that a userId is rendered
|
||||
await expect(profile.locator(".mx_ProfileSettings_profile_controls_userId", { hasText: userId })).toBeVisible();
|
||||
expect(uut.getByLabel("Username")).toHaveText(user.userId);
|
||||
|
||||
// Check avatar setting
|
||||
const avatar = profile.locator(".mx_AvatarSetting_avatar");
|
||||
|
@ -131,12 +129,15 @@ test.describe("General user settings tab", () => {
|
|||
});
|
||||
|
||||
test("should support adding and removing a profile picture", async ({ uut }) => {
|
||||
const profileSettings = uut.locator(".mx_ProfileSettings");
|
||||
const profileSettings = uut.locator(".mx_UserProfileSettings");
|
||||
// Upload a picture
|
||||
await profileSettings.getByAltText("Upload").setInputFiles("playwright/sample-files/riot.png");
|
||||
|
||||
// Find and click "Remove" link button
|
||||
await profileSettings.locator(".mx_ProfileSettings_profile").getByRole("button", { name: "Remove" }).click();
|
||||
await profileSettings
|
||||
.locator(".mx_UserProfileSettings_profile")
|
||||
.getByRole("button", { name: "Remove" })
|
||||
.click();
|
||||
|
||||
// Assert that the link button disappeared
|
||||
await expect(
|
||||
|
@ -175,7 +176,7 @@ test.describe("General user settings tab", () => {
|
|||
test("should support changing a display name", async ({ uut, page, app }) => {
|
||||
// Change the diaplay name to USER_NAME_NEW
|
||||
const displayNameInput = uut
|
||||
.locator(".mx_SettingsTab .mx_ProfileSettings")
|
||||
.locator(".mx_SettingsTab .mx_UserProfileSettings")
|
||||
.getByRole("textbox", { name: "Display Name" });
|
||||
await displayNameInput.fill(USER_NAME_NEW);
|
||||
await displayNameInput.press("Enter");
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 48 KiB |
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 28 KiB |
|
@ -597,7 +597,10 @@ legend {
|
|||
* Elements that should not be styled like a dialog button are mentioned in a :not() pseudo-class.
|
||||
* For the widest browser support, we use multiple :not pseudo-classes instead of :not(.a, .b).
|
||||
*/
|
||||
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton),
|
||||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_ProfileSettings button
|
||||
),
|
||||
.mx_Dialog input[type="submit"],
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
||||
.mx_Dialog_buttons input[type="submit"] {
|
||||
|
@ -614,11 +617,17 @@ legend {
|
|||
font-family: inherit;
|
||||
}
|
||||
|
||||
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):last-child {
|
||||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_ProfileSettings button
|
||||
):last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):focus,
|
||||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_ProfileSettings button
|
||||
):focus,
|
||||
.mx_Dialog input[type="submit"]:focus,
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
|
||||
.mx_Dialog_buttons input[type="submit"]:focus {
|
||||
|
@ -627,7 +636,8 @@ legend {
|
|||
|
||||
.mx_Dialog button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]),
|
||||
.mx_Dialog input[type="submit"].mx_Dialog_primary,
|
||||
.mx_Dialog_buttons button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
||||
.mx_Dialog_buttons
|
||||
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_ProfileSettings button),
|
||||
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
|
||||
color: var(--cpd-color-text-on-solid-primary);
|
||||
background-color: var(--cpd-color-bg-action-primary-rest);
|
||||
|
@ -637,7 +647,8 @@ legend {
|
|||
|
||||
.mx_Dialog button.danger:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]),
|
||||
.mx_Dialog input[type="submit"].danger,
|
||||
.mx_Dialog_buttons button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
|
||||
.mx_Dialog_buttons
|
||||
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_ProfileSettings button),
|
||||
.mx_Dialog_buttons input[type="submit"].danger {
|
||||
background-color: var(--cpd-color-bg-critical-primary);
|
||||
border: solid 1px var(--cpd-color-bg-critical-primary);
|
||||
|
@ -650,7 +661,10 @@ legend {
|
|||
color: var(--cpd-color-text-critical-primary);
|
||||
}
|
||||
|
||||
.mx_Dialog button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):disabled,
|
||||
.mx_Dialog
|
||||
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
|
||||
.mx_ProfileSettings button
|
||||
):disabled,
|
||||
.mx_Dialog input[type="submit"]:disabled,
|
||||
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
|
||||
.mx_Dialog_buttons input[type="submit"]:disabled {
|
||||
|
|
|
@ -337,7 +337,7 @@
|
|||
@import "./views/settings/_Notifications.pcss";
|
||||
@import "./views/settings/_PhoneNumbers.pcss";
|
||||
@import "./views/settings/_PowerLevelSelector.pcss";
|
||||
@import "./views/settings/_ProfileSettings.pcss";
|
||||
@import "./views/settings/_RoomProfileSettings.pcss";
|
||||
@import "./views/settings/_SecureBackupPanel.pcss";
|
||||
@import "./views/settings/_SetIdServer.pcss";
|
||||
@import "./views/settings/_SetIntegrationManager.pcss";
|
||||
|
@ -345,6 +345,7 @@
|
|||
@import "./views/settings/_SpellCheckLanguages.pcss";
|
||||
@import "./views/settings/_ThemeChoicePanel.pcss";
|
||||
@import "./views/settings/_UpdateCheckButton.pcss";
|
||||
@import "./views/settings/_UserProfileSettings.pcss";
|
||||
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
||||
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
||||
@import "./views/settings/tabs/_SettingsSection.pcss";
|
||||
|
|
|
@ -14,6 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SettingsDialog_toastContainer {
|
||||
position: absolute;
|
||||
bottom: var(--cpd-space-10x);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ICONS */
|
||||
/* ========================================================== */
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020, 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_ProfileSettings {
|
||||
.mx_RoomProfileSettings {
|
||||
border-bottom: 1px solid $quinary-content;
|
||||
|
||||
.mx_ProfileSettings_profile {
|
||||
.mx_RoomProfileSettings_profile {
|
||||
display: flex;
|
||||
|
||||
.mx_ProfileSettings_profile_controls {
|
||||
.mx_RoomProfileSettings_profile_controls {
|
||||
flex-grow: 1;
|
||||
margin-inline-end: 54px;
|
||||
|
||||
|
@ -28,7 +28,7 @@ limitations under the License.
|
|||
margin-top: $spacing-8;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_profile_controls_topic {
|
||||
.mx_RoomProfileSettings_profile_controls_topic {
|
||||
margin-top: $spacing-8;
|
||||
|
||||
& > textarea {
|
||||
|
@ -36,18 +36,18 @@ limitations under the License.
|
|||
resize: vertical;
|
||||
}
|
||||
|
||||
&.mx_ProfileSettings_profile_controls_topic--room textarea {
|
||||
&.mx_RoomProfileSettings_profile_controls_topic--room textarea {
|
||||
min-height: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_profile_controls_userId {
|
||||
.mx_RoomProfileSettings_profile_controls_userId {
|
||||
margin-inline-end: $spacing-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_buttons {
|
||||
.mx_RoomProfileSettings_buttons {
|
||||
display: flex;
|
||||
gap: var(--cpd-space-4x);
|
||||
margin-top: 10px; /* 18px is already accounted for by the <p> above the buttons */
|
58
res/css/views/settings/_UserProfileSettings.pcss
Normal file
58
res/css/views/settings/_UserProfileSettings.pcss
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Copyright 2019, 2020, 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_UserProfileSettings {
|
||||
border-bottom: 1px solid $quinary-content;
|
||||
|
||||
.mx_UserProfileSettings_profile {
|
||||
display: flex;
|
||||
margin-top: var(--cpd-space-6x);
|
||||
gap: 16px;
|
||||
/* This is temporary until the 'Remove' link is replaced by a context menu. */
|
||||
margin-bottom: 20px;
|
||||
|
||||
.mx_UserProfileSettings_profile_displayName {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserProfileSettings_profile_controls {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mx_UserProfileSettings_profile_controls_userId {
|
||||
width: 100%;
|
||||
.mx_CopyableText {
|
||||
margin-top: var(--cpd-space-1x);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserProfileSettings_profile_controls_userId_label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mx_UserProfileSettings_profile {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Toast } from "@vector-im/compound-web";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import TabbedView, { Tab, useActiveTabWithDefault } from "../../structures/TabbedView";
|
||||
|
@ -38,6 +39,7 @@ import { UserTab } from "./UserTab";
|
|||
import { NonEmptyArray } from "../../../@types/common";
|
||||
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { ToastContext, useActiveToast } from "../../../contexts/ToastContext";
|
||||
|
||||
interface IProps {
|
||||
initialTabId?: UserTab;
|
||||
|
@ -215,27 +217,34 @@ export default function UserSettingsDialog(props: IProps): JSX.Element {
|
|||
setShowMsc4108QrCode(false);
|
||||
};
|
||||
|
||||
const [activeToast, toastRack] = useActiveToast();
|
||||
|
||||
return (
|
||||
// XXX: SDKContext is provided within the LoggedInView subtree.
|
||||
// Modals function outside the MatrixChat React tree, so sdkContext is reprovided here to simulate that.
|
||||
// The longer term solution is to move our ModalManager into the React tree to inherit contexts properly.
|
||||
<SDKContext.Provider value={props.sdkContext}>
|
||||
<BaseDialog
|
||||
className="mx_UserSettingsDialog"
|
||||
hasCancel={true}
|
||||
onFinished={props.onFinished}
|
||||
title={titleForTabID(activeTabId)}
|
||||
>
|
||||
<div className="mx_SettingsDialog_content">
|
||||
<TabbedView
|
||||
tabs={getTabs()}
|
||||
activeTabId={activeTabId}
|
||||
screenName="UserSettings"
|
||||
onChange={setActiveTabId}
|
||||
responsive={true}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<BaseDialog
|
||||
className="mx_UserSettingsDialog"
|
||||
hasCancel={true}
|
||||
onFinished={props.onFinished}
|
||||
title={titleForTabID(activeTabId)}
|
||||
>
|
||||
<div className="mx_SettingsDialog_content">
|
||||
<TabbedView
|
||||
tabs={getTabs()}
|
||||
activeTabId={activeTabId}
|
||||
screenName="UserSettings"
|
||||
onChange={setActiveTabId}
|
||||
responsive={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_SettingsDialog_toastContainer">
|
||||
{activeToast && <Toast>{activeToast}</Toast>}
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</ToastContext.Provider>
|
||||
</SDKContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,14 +22,14 @@ import { _t } from "../../../languageHandler";
|
|||
import { copyPlaintext } from "../../../utils/strings";
|
||||
import AccessibleButton, { ButtonEvent } from "./AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
getTextToCopy: () => string | null;
|
||||
border?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true, className }) => {
|
||||
const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true, className, ...props }) => {
|
||||
const [tooltip, setTooltip] = useState<string | undefined>(undefined);
|
||||
|
||||
const onCopyClickInternal = async (e: ButtonEvent): Promise<void> => {
|
||||
|
@ -50,7 +50,7 @@ const CopyableText: React.FC<IProps> = ({ children, getTextToCopy, border = true
|
|||
});
|
||||
|
||||
return (
|
||||
<div className={combinedClassName}>
|
||||
<div className={combinedClassName} {...props}>
|
||||
{children}
|
||||
<AccessibleButton
|
||||
title={tooltip ?? _t("action|copy")}
|
||||
|
|
|
@ -202,7 +202,7 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
let profileSettingsButtons;
|
||||
if (this.state.canSetName || this.state.canSetTopic || this.state.canSetAvatar) {
|
||||
profileSettingsButtons = (
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<div className="mx_RoomProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this.cancelProfileChanges}
|
||||
kind="primary_outline"
|
||||
|
@ -218,9 +218,9 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_profile_controls">
|
||||
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_RoomProfileSettings">
|
||||
<div className="mx_RoomProfileSettings_profile">
|
||||
<div className="mx_RoomProfileSettings_profile_controls">
|
||||
<Field
|
||||
label={_t("room_settings|general|name_field_label")}
|
||||
type="text"
|
||||
|
@ -231,8 +231,8 @@ export default class RoomProfileSettings extends React.Component<IProps, IState>
|
|||
/>
|
||||
<Field
|
||||
className={classNames(
|
||||
"mx_ProfileSettings_profile_controls_topic",
|
||||
"mx_ProfileSettings_profile_controls_topic--room",
|
||||
"mx_RoomProfileSettings_profile_controls_topic",
|
||||
"mx_RoomProfileSettings_profile_controls_topic--room",
|
||||
)}
|
||||
id="profileTopic" // See: NewRoomIntro.tsx
|
||||
label={_t("room_settings|general|topic_field_label")}
|
||||
|
|
|
@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { createRef, useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import { useId } from "../../../utils/useId";
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
|
@ -75,9 +76,8 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
}
|
||||
}, [avatar]);
|
||||
|
||||
// TODO: Use useId() as soon as we're using React 18.
|
||||
// Prevents ID collisions when this component is used more than once on the same page.
|
||||
const a11yId = useRef(`hover-text-${Math.random()}`);
|
||||
const a11yId = useId();
|
||||
|
||||
const onFileChanged = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -95,7 +95,7 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
element="div"
|
||||
onClick={uploadAvatar}
|
||||
className="mx_AvatarSetting_avatarPlaceholder mx_AvatarSetting_avatarDisplay"
|
||||
aria-labelledby={disabled ? undefined : a11yId.current}
|
||||
aria-labelledby={disabled ? undefined : a11yId}
|
||||
// Inhibit tab stop as we have explicit upload/remove buttons
|
||||
tabIndex={-1}
|
||||
/>
|
||||
|
@ -122,7 +122,7 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
<AccessibleButton
|
||||
onClick={uploadAvatar}
|
||||
className="mx_AvatarSetting_uploadButton"
|
||||
aria-labelledby={a11yId.current}
|
||||
aria-labelledby={a11yId}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
|
@ -151,7 +151,7 @@ const AvatarSetting: React.FC<IProps> = ({ avatar, avatarAltText, onChange, remo
|
|||
{avatarElement}
|
||||
<div className="mx_AvatarSetting_hover" aria-hidden="true">
|
||||
<div className="mx_AvatarSetting_hoverBg" />
|
||||
{!disabled && <span id={a11yId.current}>{_t("action|upload")}</span>}
|
||||
{!disabled && <span id={a11yId}>{_t("action|upload")}</span>}
|
||||
</div>
|
||||
{uploadAvatarBtn}
|
||||
{removeAvatarBtn}
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import Field from "../elements/Field";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AvatarSetting from "./AvatarSetting";
|
||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { SettingsSubsectionHeading } from "./shared/SettingsSubsectionHeading";
|
||||
|
||||
interface IState {
|
||||
originalDisplayName: string;
|
||||
displayName: string;
|
||||
originalAvatarUrl: string | null;
|
||||
avatarFile?: File | null;
|
||||
// If true, the user has indicated that they wish to remove the avatar and this should happen on save.
|
||||
avatarRemovalPending: boolean;
|
||||
enableProfileSave?: boolean;
|
||||
}
|
||||
|
||||
export default class ProfileSettings extends React.Component<{}, IState> {
|
||||
private readonly userId: string;
|
||||
private avatarUpload: React.RefObject<HTMLInputElement> = createRef();
|
||||
|
||||
public constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.userId = MatrixClientPeg.safeGet().getSafeUserId();
|
||||
const avatarUrl = OwnProfileStore.instance.avatarMxc;
|
||||
this.state = {
|
||||
originalDisplayName: OwnProfileStore.instance.displayName ?? "",
|
||||
displayName: OwnProfileStore.instance.displayName ?? "",
|
||||
originalAvatarUrl: avatarUrl,
|
||||
avatarFile: null,
|
||||
avatarRemovalPending: false,
|
||||
enableProfileSave: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onChange = (file: File): void => {
|
||||
PosthogTrackers.trackInteraction("WebProfileSettingsAvatarUploadButton");
|
||||
this.setState({
|
||||
avatarFile: file,
|
||||
avatarRemovalPending: false,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
private removeAvatar = (): void => {
|
||||
// clear file upload field so same file can be selected
|
||||
if (this.avatarUpload.current) {
|
||||
this.avatarUpload.current.value = "";
|
||||
}
|
||||
this.setState({
|
||||
avatarFile: null,
|
||||
avatarRemovalPending: true,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
private cancelProfileChanges = async (e: ButtonEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this.setState({
|
||||
enableProfileSave: false,
|
||||
displayName: this.state.originalDisplayName,
|
||||
avatarFile: null,
|
||||
avatarRemovalPending: false,
|
||||
});
|
||||
};
|
||||
|
||||
private saveProfile = async (e: ButtonEvent): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.state.enableProfileSave) return;
|
||||
this.setState({ enableProfileSave: false });
|
||||
|
||||
const newState: Partial<IState> = {};
|
||||
|
||||
const displayName = this.state.displayName.trim();
|
||||
try {
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setDisplayName(displayName);
|
||||
newState.originalDisplayName = displayName;
|
||||
newState.displayName = displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
logger.log(
|
||||
`Uploading new avatar, ${this.state.avatarFile.name} of type ${this.state.avatarFile.type},` +
|
||||
` (${this.state.avatarFile.size}) bytes`,
|
||||
);
|
||||
const { content_uri: uri } = await client.uploadContent(this.state.avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
newState.originalAvatarUrl = uri;
|
||||
newState.avatarFile = null;
|
||||
} else if (this.state.avatarRemovalPending) {
|
||||
await client.setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
newState.originalAvatarUrl = null;
|
||||
newState.avatarRemovalPending = false;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log("Failed to save profile", err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("settings|general|error_saving_profile_title"),
|
||||
description: err instanceof Error ? err.message : _t("settings|general|error_saving_profile"),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState<any>(newState);
|
||||
};
|
||||
|
||||
private onDisplayNameChanged = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
displayName: e.target.value,
|
||||
enableProfileSave: true,
|
||||
});
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(this.userId, {
|
||||
withDisplayName: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
|
||||
<div className="mx_ProfileSettings_profile">
|
||||
<div className="mx_ProfileSettings_profile_controls">
|
||||
<SettingsSubsectionHeading heading={_t("common|profile")} />
|
||||
<Field
|
||||
label={_t("common|display_name")}
|
||||
type="text"
|
||||
value={this.state.displayName}
|
||||
autoComplete="off"
|
||||
onChange={this.onDisplayNameChanged}
|
||||
/>
|
||||
<p>
|
||||
{userIdentifier && (
|
||||
<span className="mx_ProfileSettings_profile_controls_userId">{userIdentifier}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<AvatarSetting
|
||||
avatar={
|
||||
this.state.avatarRemovalPending
|
||||
? undefined
|
||||
: this.state.avatarFile ?? this.state.originalAvatarUrl ?? undefined
|
||||
}
|
||||
avatarAltText={_t("common|user_avatar")}
|
||||
onChange={this.onChange}
|
||||
removeAvatar={this.removeAvatar}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_ProfileSettings_buttons">
|
||||
<AccessibleButton
|
||||
onClick={this.cancelProfileChanges}
|
||||
kind="primary_outline"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("action|cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
onClick={this.saveProfile}
|
||||
kind="primary"
|
||||
disabled={!this.state.enableProfileSave}
|
||||
>
|
||||
{_t("action|save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
181
src/components/views/settings/UserProfileSettings.tsx
Normal file
181
src/components/views/settings/UserProfileSettings.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
Copyright 2019 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EditInPlace, Alert } from "@vector-im/compound-web";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
|
||||
import AvatarSetting from "./AvatarSetting";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||
import { useToastContext } from "../../../contexts/ToastContext";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
||||
import { useId } from "../../../utils/useId";
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
|
||||
const SpinnerToast: React.FC = ({ children }) => (
|
||||
<>
|
||||
<InlineSpinner />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
interface UsernameBoxProps {
|
||||
username: string;
|
||||
}
|
||||
|
||||
const UsernameBox: React.FC<UsernameBoxProps> = ({ username }) => {
|
||||
const labelId = useId();
|
||||
return (
|
||||
<div className="mx_UserProfileSettings_profile_controls_userId">
|
||||
<div className="mx_UserProfileSettings_profile_controls_userId_label" id={labelId}>
|
||||
{_t("settings|general|username")}
|
||||
</div>
|
||||
<CopyableText getTextToCopy={() => username} aria-labelledby={labelId}>
|
||||
{username}
|
||||
</CopyableText>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A group of settings views to allow the user to set their profile information.
|
||||
*/
|
||||
const UserProfileSettings: React.FC = () => {
|
||||
const [avatarURL, setAvatarURL] = useState(OwnProfileStore.instance.avatarMxc);
|
||||
const [displayName, setDisplayName] = useState(OwnProfileStore.instance.displayName ?? "");
|
||||
const [initialDisplayName, setInitialDisplayName] = useState(OwnProfileStore.instance.displayName ?? "");
|
||||
const [avatarError, setAvatarError] = useState<boolean>(false);
|
||||
const [maxUploadSize, setMaxUploadSize] = useState<number | undefined>();
|
||||
const [displayNameError, setDisplayNameError] = useState<boolean>(false);
|
||||
|
||||
const toastRack = useToastContext();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const mediaConfig = await MatrixClientPeg.safeGet().getMediaConfig();
|
||||
setMaxUploadSize(mediaConfig["m.upload.size"]);
|
||||
} catch (e) {
|
||||
logger.warn("Failed to get media config", e);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onAvatarRemove = useCallback(async () => {
|
||||
const removeToast = toastRack.displayToast(
|
||||
<SpinnerToast>{_t("settings|general|avatar_remove_progress")}</SpinnerToast>,
|
||||
);
|
||||
try {
|
||||
await MatrixClientPeg.safeGet().setAvatarUrl(""); // use empty string as Synapse 500s on undefined
|
||||
setAvatarURL("");
|
||||
} finally {
|
||||
removeToast();
|
||||
}
|
||||
}, [toastRack]);
|
||||
|
||||
const onAvatarChange = useCallback(
|
||||
async (avatarFile: File) => {
|
||||
PosthogTrackers.trackInteraction("WebProfileSettingsAvatarUploadButton");
|
||||
logger.log(
|
||||
`Uploading new avatar, ${avatarFile.name} of type ${avatarFile.type}, (${avatarFile.size}) bytes`,
|
||||
);
|
||||
const removeToast = toastRack.displayToast(
|
||||
<SpinnerToast>{_t("settings|general|avatar_save_progress")}</SpinnerToast>,
|
||||
);
|
||||
try {
|
||||
setAvatarError(false);
|
||||
const client = MatrixClientPeg.safeGet();
|
||||
const { content_uri: uri } = await client.uploadContent(avatarFile);
|
||||
await client.setAvatarUrl(uri);
|
||||
setAvatarURL(uri);
|
||||
} catch (e) {
|
||||
setAvatarError(true);
|
||||
} finally {
|
||||
removeToast();
|
||||
}
|
||||
},
|
||||
[toastRack],
|
||||
);
|
||||
|
||||
const onDisplayNameChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setDisplayName(e.target.value);
|
||||
}, []);
|
||||
|
||||
const onDisplayNameCancel = useCallback(() => {
|
||||
setDisplayName(OwnProfileStore.instance.displayName ?? "");
|
||||
}, []);
|
||||
|
||||
const onDisplayNameSave = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
setDisplayNameError(false);
|
||||
await MatrixClientPeg.safeGet().setDisplayName(displayName);
|
||||
setInitialDisplayName(displayName);
|
||||
} catch (e) {
|
||||
setDisplayNameError(true);
|
||||
}
|
||||
}, [displayName]);
|
||||
|
||||
const userIdentifier = useMemo(
|
||||
() =>
|
||||
UserIdentifierCustomisations.getDisplayUserIdentifier(MatrixClientPeg.safeGet().getSafeUserId(), {
|
||||
withDisplayName: true,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_UserProfileSettings">
|
||||
<h2>{_t("common|profile")}</h2>
|
||||
<div>{_t("settings|general|profile_subtitle")}</div>
|
||||
<div className="mx_UserProfileSettings_profile">
|
||||
<AvatarSetting
|
||||
avatar={avatarURL ?? undefined}
|
||||
avatarAltText={_t("common|user_avatar")}
|
||||
onChange={onAvatarChange}
|
||||
removeAvatar={onAvatarRemove}
|
||||
/>
|
||||
<EditInPlace
|
||||
className="mx_UserProfileSettings_profile_displayName"
|
||||
label={_t("settings|general|display_name")}
|
||||
value={displayName}
|
||||
disableSaveButton={displayName === initialDisplayName}
|
||||
saveButtonLabel={_t("common|save")}
|
||||
cancelButtonLabel={_t("common|cancel")}
|
||||
savedLabel={_t("common|saved")}
|
||||
onChange={onDisplayNameChanged}
|
||||
onCancel={onDisplayNameCancel}
|
||||
onSave={onDisplayNameSave}
|
||||
error={displayNameError ? _t("settings|general|display_name_error") : undefined}
|
||||
/>
|
||||
</div>
|
||||
{avatarError && (
|
||||
<Alert title={_t("settings|general|avatar_upload_error_title")} type="critical">
|
||||
{maxUploadSize === undefined
|
||||
? _t("settings|general|avatar_upload_error_text_generic")
|
||||
: _t("settings|general|avatar_upload_error_text", { size: formatBytes(maxUploadSize) })}
|
||||
</Alert>
|
||||
)}
|
||||
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileSettings;
|
|
@ -22,7 +22,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import { Icon as WarningIcon } from "../../../../../../res/img/feather-customised/warning-triangle.svg";
|
||||
import { UserFriendlyError, _t } from "../../../../../languageHandler";
|
||||
import ProfileSettings from "../../ProfileSettings";
|
||||
import UserProfileSettings from "../../UserProfileSettings";
|
||||
import * as languageHandler from "../../../../../languageHandler";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||
|
@ -561,7 +561,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
return (
|
||||
<SettingsTab data-testid="mx_GeneralUserSettingsTab">
|
||||
<SettingsSection>
|
||||
<ProfileSettings />
|
||||
<UserProfileSettings />
|
||||
{this.renderAccountSection()}
|
||||
{this.renderLanguageSection()}
|
||||
{supportsMultiLanguageSpellCheck ? this.renderSpellCheckSection() : null}
|
||||
|
|
93
src/contexts/ToastContext.tsx
Normal file
93
src/contexts/ToastContext.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* A ToastContext helps components display any kind of toast message and can be provided
|
||||
* by a parent component such that their children can display toasts, eg. a settings dialog
|
||||
* can provide a ToastContext such that controls within it can display toasts at the bottom
|
||||
* of the dialog.
|
||||
*
|
||||
* It is not (at time of writing) used by the *other* toasts that appear in the top right
|
||||
* corner of the app, however the name 'toast' as used in this class refers to the component
|
||||
* of the same name in compound that it is written to manage.
|
||||
*/
|
||||
export const ToastContext = createContext(null as any);
|
||||
ToastContext.displayName = "ToastContext";
|
||||
|
||||
/**
|
||||
* Returns the ToastRack in context in order to display toasts
|
||||
*/
|
||||
export function useToastContext(): ToastRack {
|
||||
return useContext(ToastContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* For components that wish to display toasts, return the currently active toast and
|
||||
* the ToastRack object that should be provided to the context
|
||||
*/
|
||||
export function useActiveToast(): [ReactNode | undefined, ToastRack] {
|
||||
const toastRack = useRef(new ToastRack());
|
||||
|
||||
const [activeToast, setActiveToast] = useState<ReactNode | undefined>(toastRack.current.getActiveToast());
|
||||
|
||||
const updateCallback = useCallback(() => {
|
||||
setActiveToast(toastRack.current.getActiveToast());
|
||||
}, [setActiveToast, toastRack]);
|
||||
|
||||
useEffect(() => {
|
||||
toastRack.current.setCallback(updateCallback);
|
||||
}, [toastRack, updateCallback]);
|
||||
|
||||
return [activeToast, toastRack.current];
|
||||
}
|
||||
|
||||
interface DisplayedToast {
|
||||
id: number;
|
||||
contents: ReactNode;
|
||||
}
|
||||
|
||||
type RemoveCallback = () => void;
|
||||
|
||||
export class ToastRack {
|
||||
private currentToast: DisplayedToast | undefined;
|
||||
private updateCallback?: () => void;
|
||||
private idSeq = 0;
|
||||
|
||||
public setCallback(cb: () => void): void {
|
||||
this.updateCallback = cb;
|
||||
}
|
||||
|
||||
public displayToast(contents: ReactNode): RemoveCallback {
|
||||
const newToastId = ++this.idSeq;
|
||||
|
||||
this.currentToast = { id: newToastId, contents: contents };
|
||||
this.updateCallback?.();
|
||||
const removeFn = (): void => {
|
||||
if (this.currentToast?.id === newToastId) {
|
||||
this.currentToast = undefined;
|
||||
this.updateCallback?.();
|
||||
}
|
||||
};
|
||||
|
||||
return removeFn;
|
||||
}
|
||||
|
||||
public getActiveToast(): ReactNode | undefined {
|
||||
return this.currentToast?.contents;
|
||||
}
|
||||
}
|
|
@ -457,6 +457,7 @@
|
|||
"beta": "Beta",
|
||||
"camera": "Camera",
|
||||
"cameras": "Cameras",
|
||||
"cancel": "Cancel",
|
||||
"capabilities": "Capabilities",
|
||||
"copied": "Copied!",
|
||||
"credits": "Credits",
|
||||
|
@ -465,7 +466,6 @@
|
|||
"description": "Description",
|
||||
"deselect_all": "Deselect all",
|
||||
"device": "Device",
|
||||
"display_name": "Display Name",
|
||||
"edited": "edited",
|
||||
"email_address": "Email address",
|
||||
"emoji": "Emoji",
|
||||
|
@ -540,6 +540,8 @@
|
|||
"room": "Room",
|
||||
"room_name": "Room name",
|
||||
"rooms": "Rooms",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"saving": "Saving…",
|
||||
"secure_backup": "Secure Backup",
|
||||
"security": "Security",
|
||||
|
@ -2458,6 +2460,11 @@
|
|||
"add_msisdn_dialog_title": "Add Phone Number",
|
||||
"add_msisdn_instructions": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
|
||||
"add_msisdn_misconfigured": "The add / bind with MSISDN flow is misconfigured",
|
||||
"avatar_remove_progress": "Removing image...",
|
||||
"avatar_save_progress": "Uploading image...",
|
||||
"avatar_upload_error_text": "The file format is not supported or the image is larger than %(size)s.",
|
||||
"avatar_upload_error_text_generic": "The file format may not be supported.",
|
||||
"avatar_upload_error_title": "Avatar image could not be uploaded",
|
||||
"confirm_adding_email_body": "Click the button below to confirm adding this email address.",
|
||||
"confirm_adding_email_title": "Confirm adding email",
|
||||
"deactivate_confirm_body": "Are you sure you want to deactivate your account? This is irreversible.",
|
||||
|
@ -2480,6 +2487,8 @@
|
|||
"discovery_msisdn_empty": "Discovery options will appear once you have added a phone number above.",
|
||||
"discovery_needs_terms": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.",
|
||||
"discovery_section": "Discovery",
|
||||
"display_name": "Display Name",
|
||||
"display_name_error": "Unable to set display name",
|
||||
"email_address_in_use": "This email address is already in use",
|
||||
"email_address_label": "Email Address",
|
||||
"email_not_verified": "Your email address hasn't been verified yet",
|
||||
|
@ -2500,8 +2509,6 @@
|
|||
"error_remove_3pid": "Unable to remove contact information",
|
||||
"error_revoke_email_discovery": "Unable to revoke sharing for email address",
|
||||
"error_revoke_msisdn_discovery": "Unable to revoke sharing for phone number",
|
||||
"error_saving_profile": "The operation could not be completed",
|
||||
"error_saving_profile_title": "Failed to save your profile",
|
||||
"error_share_email_discovery": "Unable to share email address",
|
||||
"error_share_msisdn_discovery": "Unable to share phone number",
|
||||
"external_account_management": "Your account details are managed separately at <code>%(hostname)s</code>.",
|
||||
|
@ -2518,10 +2525,12 @@
|
|||
"oidc_manage_button": "Manage account",
|
||||
"password_change_section": "Set a new account password…",
|
||||
"password_change_success": "Your password was successfully changed.",
|
||||
"profile_subtitle": "This is how you appear to others on the app.",
|
||||
"remove_email_prompt": "Remove %(email)s?",
|
||||
"remove_msisdn_prompt": "Remove %(phone)s?",
|
||||
"spell_check_locale_placeholder": "Choose a locale",
|
||||
"spell_check_section": "Spell check"
|
||||
"spell_check_section": "Spell check",
|
||||
"username": "Username"
|
||||
},
|
||||
"image_thumbnails": "Show previews/thumbnails for images",
|
||||
"inline_url_previews_default": "Enable inline URL previews by default",
|
||||
|
|
24
src/utils/useId.ts
Normal file
24
src/utils/useId.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
const getUniqueId = (() => {
|
||||
return () => `:r${Math.random()}:`;
|
||||
})();
|
||||
|
||||
// Replace this with React's own useId once we switch to React 18
|
||||
export const useId = (): string => React.useMemo(getUniqueId, []);
|
201
test/components/views/settings/UserProfileSettings-test.tsx
Normal file
201
test/components/views/settings/UserProfileSettings-test.tsx
Normal file
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from "react";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { MatrixClient, UploadResponse } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import UserProfileSettings from "../../../../src/components/views/settings/UserProfileSettings";
|
||||
import { stubClient } from "../../../test-utils";
|
||||
import { ToastContext, ToastRack } from "../../../../src/contexts/ToastContext";
|
||||
import { OwnProfileStore } from "../../../../src/stores/OwnProfileStore";
|
||||
|
||||
interface MockedAvatarSettingProps {
|
||||
removeAvatar: () => void;
|
||||
onChange: (file: File) => void;
|
||||
}
|
||||
|
||||
let removeAvatarFn: () => void;
|
||||
let changeAvatarFn: (file: File) => void;
|
||||
|
||||
jest.mock(
|
||||
"../../../../src/components/views/settings/AvatarSetting",
|
||||
() =>
|
||||
(({ removeAvatar, onChange }) => {
|
||||
removeAvatarFn = removeAvatar;
|
||||
changeAvatarFn = onChange;
|
||||
return <div>Mocked AvatarSetting</div>;
|
||||
}) as React.FC<MockedAvatarSettingProps>,
|
||||
);
|
||||
|
||||
let editInPlaceOnChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
let editInPlaceOnSave: () => void;
|
||||
let editInPlaceOnCancel: () => void;
|
||||
|
||||
interface MockedEditInPlaceProps {
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onSave: () => void;
|
||||
onCancel: () => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
jest.mock("@vector-im/compound-web", () => ({
|
||||
EditInPlace: (({ onChange, onSave, onCancel, value }) => {
|
||||
editInPlaceOnChange = onChange;
|
||||
editInPlaceOnSave = onSave;
|
||||
editInPlaceOnCancel = onCancel;
|
||||
return <div>Mocked EditInPlace: {value}</div>;
|
||||
}) as React.FC<MockedEditInPlaceProps>,
|
||||
}));
|
||||
|
||||
describe("ProfileSettings", () => {
|
||||
let client: MatrixClient;
|
||||
let toastRack: Partial<ToastRack>;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient();
|
||||
toastRack = {
|
||||
displayToast: jest.fn().mockReturnValue(jest.fn()),
|
||||
};
|
||||
});
|
||||
|
||||
it("removes avatar", async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(removeAvatarFn).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
removeAvatarFn();
|
||||
});
|
||||
|
||||
expect(client.setAvatarUrl).toHaveBeenCalledWith("");
|
||||
});
|
||||
|
||||
it("changes avatar", async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(changeAvatarFn).toBeDefined();
|
||||
|
||||
const returnedMxcUri = "mxc://example.org/my-avatar";
|
||||
mocked(client).uploadContent.mockResolvedValue({ content_uri: returnedMxcUri });
|
||||
|
||||
const fileSentinel = {};
|
||||
await act(async () => {
|
||||
await changeAvatarFn(fileSentinel as File);
|
||||
});
|
||||
|
||||
expect(client.uploadContent).toHaveBeenCalledWith(fileSentinel);
|
||||
expect(client.setAvatarUrl).toHaveBeenCalledWith(returnedMxcUri);
|
||||
});
|
||||
|
||||
it("displays toast while uploading avatar", async () => {
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
const clearToastFn = jest.fn();
|
||||
mocked(toastRack.displayToast!).mockReturnValue(clearToastFn);
|
||||
|
||||
expect(await screen.findByText("Mocked AvatarSetting")).toBeInTheDocument();
|
||||
expect(changeAvatarFn).toBeDefined();
|
||||
|
||||
let resolveUploadPromise = (r: UploadResponse) => {};
|
||||
const uploadPromise = new Promise<UploadResponse>((r) => {
|
||||
resolveUploadPromise = r;
|
||||
});
|
||||
mocked(client).uploadContent.mockReturnValue(uploadPromise);
|
||||
|
||||
const fileSentinel = {};
|
||||
const changeAvatarActPromise = act(async () => {
|
||||
await changeAvatarFn(fileSentinel as File);
|
||||
});
|
||||
|
||||
expect(toastRack.displayToast).toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
resolveUploadPromise({ content_uri: "bloop" });
|
||||
});
|
||||
await changeAvatarActPromise;
|
||||
|
||||
expect(clearToastFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("changes display name", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "displayName", "get").mockReturnValue("Alice");
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
expect(editInPlaceOnSave).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnChange({
|
||||
target: { value: "The Value" } as HTMLInputElement,
|
||||
} as ChangeEvent<HTMLInputElement>);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await editInPlaceOnSave();
|
||||
});
|
||||
|
||||
expect(client.setDisplayName).toHaveBeenCalledWith("The Value");
|
||||
});
|
||||
|
||||
it("resets on cancel", async () => {
|
||||
jest.spyOn(OwnProfileStore.instance, "displayName", "get").mockReturnValue("Alice");
|
||||
|
||||
render(
|
||||
<ToastContext.Provider value={toastRack}>
|
||||
<UserProfileSettings />
|
||||
</ToastContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
expect(editInPlaceOnChange).toBeDefined();
|
||||
expect(editInPlaceOnCancel).toBeDefined();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnChange({
|
||||
target: { value: "Alicia Zattic" } as HTMLInputElement,
|
||||
} as ChangeEvent<HTMLInputElement>);
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alicia Zattic")).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
editInPlaceOnCancel();
|
||||
});
|
||||
|
||||
expect(await screen.findByText("Mocked EditInPlace: Alice")).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -42,14 +42,14 @@ exports[`<GeneralUserSettingsTab /> 3pids should display 3pid email addresses an
|
|||
>
|
||||
<input
|
||||
autocomplete="email"
|
||||
id="mx_Field_61"
|
||||
id="mx_Field_50"
|
||||
label="Email Address"
|
||||
placeholder="Email Address"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_61"
|
||||
for="mx_Field_50"
|
||||
>
|
||||
Email Address
|
||||
</label>
|
||||
|
@ -150,14 +150,14 @@ exports[`<GeneralUserSettingsTab /> 3pids should display 3pid email addresses an
|
|||
</span>
|
||||
<input
|
||||
autocomplete="tel-national"
|
||||
id="mx_Field_62"
|
||||
id="mx_Field_51"
|
||||
label="Phone Number"
|
||||
placeholder="Phone Number"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<label
|
||||
for="mx_Field_62"
|
||||
for="mx_Field_51"
|
||||
>
|
||||
Phone Number
|
||||
</label>
|
||||
|
|
|
@ -127,7 +127,7 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] =
|
|||
|
||||
exports[`ThreadsActivityCentre should close the release announcement when the TAC button is clicked 1`] = `
|
||||
<body
|
||||
data-scroll-locked=""
|
||||
data-scroll-locked="1"
|
||||
style="pointer-events: none;"
|
||||
>
|
||||
<span
|
||||
|
|
43
test/contexts/ToastContext-test.ts
Normal file
43
test/contexts/ToastContext-test.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ToastRack } from "../../src/contexts/ToastContext";
|
||||
|
||||
describe("ToastRack", () => {
|
||||
it("should return a toast once one is displayed", () => {
|
||||
const toastRack = new ToastRack();
|
||||
toastRack.displayToast("Hello, world!");
|
||||
|
||||
expect(toastRack.getActiveToast()).toBe("Hello, world!");
|
||||
});
|
||||
|
||||
it("calls update callback when a toast is added", () => {
|
||||
const toastRack = new ToastRack();
|
||||
const updateCallbackFn = jest.fn();
|
||||
toastRack.setCallback(updateCallbackFn);
|
||||
toastRack.displayToast("Hello, world!");
|
||||
|
||||
expect(updateCallbackFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes toast when remove function is called", () => {
|
||||
const toastRack = new ToastRack();
|
||||
const removeFn = toastRack.displayToast("Hello, world!");
|
||||
expect(toastRack.getActiveToast()).toBe("Hello, world!");
|
||||
removeFn();
|
||||
expect(toastRack.getActiveToast()).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -210,6 +210,8 @@ export function createTestClient(): MatrixClient {
|
|||
getPushers: jest.fn().mockResolvedValue({ pushers: [] }),
|
||||
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
bulkLookupThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
|
||||
setAvatarUrl: jest.fn().mockResolvedValue(undefined),
|
||||
setDisplayName: jest.fn().mockResolvedValue(undefined),
|
||||
setPusher: jest.fn().mockResolvedValue(undefined),
|
||||
setPushRuleEnabled: jest.fn().mockResolvedValue(undefined),
|
||||
setPushRuleActions: jest.fn().mockResolvedValue(undefined),
|
||||
|
|
252
yarn.lock
252
yarn.lock
|
@ -1339,13 +1339,20 @@
|
|||
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
|
||||
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.9", "@babel/runtime@^7.23.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.24.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c"
|
||||
integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.13.10":
|
||||
version "7.24.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e"
|
||||
integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.20.7":
|
||||
version "7.20.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
|
||||
|
@ -1487,6 +1494,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b"
|
||||
integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==
|
||||
|
||||
"@emotion/use-insertion-effect-with-fallbacks@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963"
|
||||
integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
|
@ -1540,40 +1552,40 @@
|
|||
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
|
||||
|
||||
"@floating-ui/core@^1.0.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1"
|
||||
integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.2.tgz#d37f3e0ac1f1c756c7de45db13303a266226851a"
|
||||
integrity sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.1"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
|
||||
"@floating-ui/dom@^1.6.1":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef"
|
||||
integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==
|
||||
"@floating-ui/dom@^1.0.0":
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9"
|
||||
integrity sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.0.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.0.8":
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d"
|
||||
integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==
|
||||
"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.0.8", "@floating-ui/react-dom@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff"
|
||||
integrity sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.6.1"
|
||||
"@floating-ui/dom" "^1.0.0"
|
||||
|
||||
"@floating-ui/react@^0.26.9":
|
||||
version "0.26.10"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.10.tgz#d4a4878bcfaed70963ec0eaa67a71bead5924ee5"
|
||||
integrity sha512-sh6f9gVvWQdEzLObrWbJ97c0clJObiALsFe0LiR/kb3tDRKwEhObASEH2QyfdoO/ZBPzwxa9j+nYFo+sqgbioA==
|
||||
version "0.26.16"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.16.tgz#3415a087f452165161c2d313d1d57e8142894679"
|
||||
integrity sha512-HEf43zxZNAI/E781QIVpYSF3K2VH4TTYZpqecjdsFkjsaU1EbaWcM++kw0HXFffj7gDUcBFevX8s0rQGQpxkow==
|
||||
dependencies:
|
||||
"@floating-ui/react-dom" "^2.0.0"
|
||||
"@floating-ui/react-dom" "^2.1.0"
|
||||
"@floating-ui/utils" "^0.2.0"
|
||||
tabbable "^6.0.0"
|
||||
|
||||
"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
|
||||
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
|
||||
"@floating-ui/utils@^0.2.0":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5"
|
||||
integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.14":
|
||||
version "0.11.14"
|
||||
|
@ -2457,6 +2469,98 @@
|
|||
dependencies:
|
||||
"@sinonjs/commons" "^3.0.0"
|
||||
|
||||
"@storybook/channels@8.1.5":
|
||||
version "8.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-8.1.5.tgz#d00d033d318cf202ece1de728e55e85f82242e74"
|
||||
integrity sha512-R+puP4tWYzQUbpIp8sX6U5oI+ZUevVOaFxXGaAN3PRXjIRC38oKTVWzj/G6GdziVFzN6rDn+JsYPmiRMYo1sYg==
|
||||
dependencies:
|
||||
"@storybook/client-logger" "8.1.5"
|
||||
"@storybook/core-events" "8.1.5"
|
||||
"@storybook/global" "^5.0.0"
|
||||
telejson "^7.2.0"
|
||||
tiny-invariant "^1.3.1"
|
||||
|
||||
"@storybook/client-logger@8.1.5":
|
||||
version "8.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-8.1.5.tgz#aa4a6ce4ca46fdfe12539e571f9059a479c8ae43"
|
||||
integrity sha512-zd+aENXnOHsxBATppELmhw/UywLzCxQjz/8i/xkUjeTRB4Ggp0hJlOUdJUEdIJz631ydyytfvM70ktBj9gMl1w==
|
||||
dependencies:
|
||||
"@storybook/global" "^5.0.0"
|
||||
|
||||
"@storybook/core-events@8.1.5":
|
||||
version "8.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-8.1.5.tgz#d921984e12b27aaaa623499a7ac0c3eea5e96264"
|
||||
integrity sha512-fgwbrHoLtSX6kfmamTGJqD+KfuEgun8cc4mWKZK094ByaqbSjhnOyeYO1sfVk8qst7QTFlOfhLAUe4cz1z149A==
|
||||
dependencies:
|
||||
"@storybook/csf" "^0.1.7"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/csf@^0.1.7":
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.7.tgz#dcc6c16a353bc09c8c619ba1a23ba93b2aab0b9d"
|
||||
integrity sha512-53JeLZBibjQxi0Ep+/AJTfxlofJlxy1jXcSKENlnKxHjWEYyHQCumMP5yTFjf7vhNnMjEpV3zx6t23ssFiGRyw==
|
||||
dependencies:
|
||||
type-fest "^2.19.0"
|
||||
|
||||
"@storybook/global@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed"
|
||||
integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==
|
||||
|
||||
"@storybook/icons@^1.2.5":
|
||||
version "1.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-1.2.9.tgz#bb4a51a79e186b62e2dd0e04928b8617ac573838"
|
||||
integrity sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg==
|
||||
|
||||
"@storybook/manager-api@^8.1.1":
|
||||
version "8.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-8.1.5.tgz#1f1a8875cbc19fad5435f670943207158dc76551"
|
||||
integrity sha512-iVP7FOKDf9L7zWCb8C2XeZjWSILS3hHeNwILvd9YSX9dg9du41kJYahsAHxDCR/jp/gv0ZM/V0vuHzi+naVPkQ==
|
||||
dependencies:
|
||||
"@storybook/channels" "8.1.5"
|
||||
"@storybook/client-logger" "8.1.5"
|
||||
"@storybook/core-events" "8.1.5"
|
||||
"@storybook/csf" "^0.1.7"
|
||||
"@storybook/global" "^5.0.0"
|
||||
"@storybook/icons" "^1.2.5"
|
||||
"@storybook/router" "8.1.5"
|
||||
"@storybook/theming" "8.1.5"
|
||||
"@storybook/types" "8.1.5"
|
||||
dequal "^2.0.2"
|
||||
lodash "^4.17.21"
|
||||
memoizerific "^1.11.3"
|
||||
store2 "^2.14.2"
|
||||
telejson "^7.2.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/router@8.1.5":
|
||||
version "8.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/router/-/router-8.1.5.tgz#e1dd831136e874df833286fd76554958af6132fa"
|
||||
integrity sha512-DCwvAswlbLhQu6REPV04XNRhtPvsrRqHjMHKzjlfs+qYJWY7Egkofy05qlegqjkMDve33czfnRGBm0C16IydkA==
|
||||
dependencies:
|
||||
"@storybook/client-logger" "8.1.5"
|
||||
memoizerific "^1.11.3"
|
||||
qs "^6.10.0"
|
||||
|
||||
"@storybook/theming@8.1.5":
|
||||
version "8.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.1.5.tgz#8eb0718907ec443cfca1b73491f5e99df65930af"
|
||||
integrity sha512-E4z1t49fMbVvd/t2MSL0Ecp5zbqsU/QfWBX/eorJ+m+Xc9skkwwG5qf/FnP9x4RZ9KaX8U8+862t0eafVvf4Tw==
|
||||
dependencies:
|
||||
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.1"
|
||||
"@storybook/client-logger" "8.1.5"
|
||||
"@storybook/global" "^5.0.0"
|
||||
memoizerific "^1.11.3"
|
||||
|
||||
"@storybook/types@8.1.5":
|
||||
version "8.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/types/-/types-8.1.5.tgz#627cac55e8034deed4b763327ff938c84c541a05"
|
||||
integrity sha512-/PfAZh1xtXN2MvAZZKpiL/nPkC3bZj8BQ7P7z5a/aQarP+y7qdXuoitYQ6oOH3rkaiYywmkWzA/y4iW70KXLKg==
|
||||
dependencies:
|
||||
"@storybook/channels" "8.1.5"
|
||||
"@types/express" "^4.7.0"
|
||||
file-system-cache "2.3.0"
|
||||
|
||||
"@testing-library/dom@^8.0.0":
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6"
|
||||
|
@ -2644,7 +2748,7 @@
|
|||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express@^4.17.21":
|
||||
"@types/express@^4.17.21", "@types/express@^4.7.0":
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
||||
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
|
||||
|
@ -3112,10 +3216,10 @@
|
|||
dependencies:
|
||||
svg2vectordrawable "^2.9.1"
|
||||
|
||||
"@vector-im/compound-web@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-4.3.1.tgz#4570adeccdfcac6919256c5e399d592510bdb15d"
|
||||
integrity sha512-/Sw27GI0jCg6A7E+93SWFyF3pEwLyLzExB3lIVPTY0mMTx50+rZloRuhuqftUlIscWSlmAUex8Lo4WK8WKPFPA==
|
||||
"@vector-im/compound-web@^4.4.1":
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-4.4.1.tgz#378c6874888becd4b6dd3541904f63300b9ba09a"
|
||||
integrity sha512-KLYSU8GxR8EBuz+gKSoLLs4+s5xV4stUDbqJu5GG52OmO3YQlvmz/e5/uHYvzfbqBBU5dMmZhz5bdJJ38qxHPQ==
|
||||
dependencies:
|
||||
"@floating-ui/react" "^0.26.9"
|
||||
"@floating-ui/react-dom" "^2.0.8"
|
||||
|
@ -3125,6 +3229,7 @@
|
|||
"@radix-ui/react-separator" "^1.0.3"
|
||||
"@radix-ui/react-slot" "^1.0.2"
|
||||
"@radix-ui/react-tooltip" "^1.0.6"
|
||||
"@storybook/manager-api" "^8.1.1"
|
||||
classnames "^2.3.2"
|
||||
graphemer "^1.4.0"
|
||||
vaul "^0.7.0"
|
||||
|
@ -3306,9 +3411,9 @@ argparse@^2.0.1:
|
|||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||
|
||||
aria-hidden@^1.1.1:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954"
|
||||
integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522"
|
||||
integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
|
@ -4336,7 +4441,7 @@ depd@2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
|
||||
dequal@^2.0.3:
|
||||
dequal@^2.0.2, dequal@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
|
@ -5363,6 +5468,14 @@ file-saver@^2.0.5:
|
|||
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
|
||||
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
|
||||
|
||||
file-system-cache@2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-2.3.0.tgz#201feaf4c8cd97b9d0d608e96861bb6005f46fe6"
|
||||
integrity sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==
|
||||
dependencies:
|
||||
fs-extra "11.1.1"
|
||||
ramda "0.29.0"
|
||||
|
||||
filesize@10.1.2:
|
||||
version "10.1.2"
|
||||
resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.2.tgz#33bb71c5c134102499f1bc36e6f2863137f6cb0c"
|
||||
|
@ -5492,6 +5605,15 @@ fresh@0.5.2:
|
|||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||
|
||||
fs-extra@11.1.1:
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
|
||||
integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs-extra@^11.0.0:
|
||||
version "11.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b"
|
||||
|
@ -7118,6 +7240,11 @@ makeerror@1.0.12:
|
|||
dependencies:
|
||||
tmpl "1.0.5"
|
||||
|
||||
map-or-similar@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
|
||||
integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==
|
||||
|
||||
maplibre-gl@^2.0.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-2.4.0.tgz#2b53dbf526626bf4ee92ad4f33f13ef09e5af182"
|
||||
|
@ -7240,6 +7367,13 @@ memoize-one@^6.0.0:
|
|||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
|
||||
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
|
||||
|
||||
memoizerific@^1.11.3:
|
||||
version "1.11.3"
|
||||
resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
|
||||
integrity sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==
|
||||
dependencies:
|
||||
map-or-similar "^1.5.0"
|
||||
|
||||
meow@^13.2.0:
|
||||
version "13.2.0"
|
||||
resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f"
|
||||
|
@ -8009,6 +8143,13 @@ qs@6.11.0:
|
|||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
qs@^6.10.0:
|
||||
version "6.12.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a"
|
||||
integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==
|
||||
dependencies:
|
||||
side-channel "^1.0.6"
|
||||
|
||||
querystring@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd"
|
||||
|
@ -8034,6 +8175,11 @@ raf-schd@^4.0.2:
|
|||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
||||
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
|
||||
|
||||
ramda@0.29.0:
|
||||
version "0.29.0"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.29.0.tgz#fbbb67a740a754c8a4cbb41e2a6e0eb8507f55fb"
|
||||
integrity sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==
|
||||
|
||||
range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
|
@ -8143,9 +8289,9 @@ react-redux@^7.2.0:
|
|||
react-is "^17.0.2"
|
||||
|
||||
react-remove-scroll-bar@^2.3.3:
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz#cd2543b3ed7716c7c5b446342d21b0e0b303f47c"
|
||||
integrity sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c"
|
||||
integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==
|
||||
dependencies:
|
||||
react-style-singleton "^2.2.1"
|
||||
tslib "^2.0.0"
|
||||
|
@ -8666,7 +8812,7 @@ shebang-regex@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
side-channel@^1.0.4:
|
||||
side-channel@^1.0.4, side-channel@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
|
||||
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
|
||||
|
@ -8801,6 +8947,11 @@ stop-iteration-iterator@^1.0.0:
|
|||
dependencies:
|
||||
internal-slot "^1.0.4"
|
||||
|
||||
store2@^2.14.2:
|
||||
version "2.14.3"
|
||||
resolved "https://registry.yarnpkg.com/store2/-/store2-2.14.3.tgz#24077d7ba110711864e4f691d2af941ec533deb5"
|
||||
integrity sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==
|
||||
|
||||
string-length@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
|
||||
|
@ -9144,6 +9295,13 @@ tar-js@^0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/tar-js/-/tar-js-0.3.0.tgz#6949aabfb0ba18bb1562ae51a439fd0f30183a17"
|
||||
integrity sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA==
|
||||
|
||||
telejson@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/telejson/-/telejson-7.2.0.tgz#3994f6c9a8f8d7f2dba9be2c7c5bbb447e876f32"
|
||||
integrity sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==
|
||||
dependencies:
|
||||
memoizerific "^1.11.3"
|
||||
|
||||
test-exclude@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
|
||||
|
@ -9163,6 +9321,11 @@ tiny-invariant@^1.0.6:
|
|||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
|
||||
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
|
||||
|
||||
tiny-invariant@^1.3.1:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
|
||||
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
|
||||
|
||||
tinyqueue@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
|
||||
|
@ -9231,6 +9394,11 @@ ts-api-utils@^1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
|
||||
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
|
||||
|
||||
ts-dedent@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
|
||||
integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
|
||||
|
||||
ts-node@^10.9.1:
|
||||
version "10.9.2"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f"
|
||||
|
@ -9297,6 +9465,11 @@ type-fest@^0.8.1:
|
|||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||
|
||||
type-fest@^2.19.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
|
||||
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
|
@ -9448,14 +9621,7 @@ url-parse@^1.5.3:
|
|||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
use-callback-ref@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0"
|
||||
integrity sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-callback-ref@^1.3.2:
|
||||
use-callback-ref@^1.3.0, use-callback-ref@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693"
|
||||
integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==
|
||||
|
|
Loading…
Reference in a new issue