mirror of
https://github.com/Novaloop-AG/healthchat-web.git
synced 2026-11-06 06:28:27 +00:00
refactor(profile): drop extended-profile editing, keep right-panel display
Extended-profile fields (io.element.profile.*) are no longer edited in the client, so remove the edit form and the dead code that came with it, keeping only the read-only right-panel display (UserExtendedProfileInfo). - Remove ExtendedProfileSettings (edit form) + its Settings integration and CSS - Remove ExtendedProfileView (never-rendered, dead) + its CSS - Inline the EXT_PROFILE_KEY_* constants into UserExtendedProfileInfo so it no longer depends on the deleted settings component - Drop now-unused set/deleteExtendedProfileProperty from UserProfilesStore - Prune dead settings|extended_profile_settings|* i18n keys (and the unused user_info.extended_profile header/not_provided keys) across all locales - Regenerate _components.pcss - Revert UserProfileSettings/AccountUserSettingsTab tests (+snapshot) to master, since their adaptations existed only for the removed edit form
This commit is contained in:
@@ -235,7 +235,6 @@
|
|||||||
@import "./views/right_panel/_BaseCard.pcss";
|
@import "./views/right_panel/_BaseCard.pcss";
|
||||||
@import "./views/right_panel/_EmptyState.pcss";
|
@import "./views/right_panel/_EmptyState.pcss";
|
||||||
@import "./views/right_panel/_EncryptionInfo.pcss";
|
@import "./views/right_panel/_EncryptionInfo.pcss";
|
||||||
@import "./views/right_panel/_ExtendedProfileView.pcss";
|
|
||||||
@import "./views/right_panel/_ExtensionsCard.pcss";
|
@import "./views/right_panel/_ExtensionsCard.pcss";
|
||||||
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
@import "./views/right_panel/_PinnedMessagesCard.pcss";
|
||||||
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
@import "./views/right_panel/_RoomSummaryCard.pcss";
|
||||||
@@ -304,7 +303,6 @@
|
|||||||
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
|
||||||
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
|
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
|
||||||
@import "./views/settings/_AvatarSetting.pcss";
|
@import "./views/settings/_AvatarSetting.pcss";
|
||||||
@import "./views/settings/_ExtendedProfileSettings.pcss";
|
|
||||||
@import "./views/settings/_FontScalingPanel.pcss";
|
@import "./views/settings/_FontScalingPanel.pcss";
|
||||||
@import "./views/settings/_ImageSizePanel.pcss";
|
@import "./views/settings/_ImageSizePanel.pcss";
|
||||||
@import "./views/settings/_IntegrationManager.pcss";
|
@import "./views/settings/_IntegrationManager.pcss";
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_ExtendedProfileView {
|
|
||||||
/* Anpassung an das bestehende UserInfo Panel */
|
|
||||||
font-size: var(--cpd-font-size-body-sm);
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
/* Styling für das Accordion */
|
|
||||||
.mx_ExtendedProfileView_accordion {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Abstand zwischen den Feldern */
|
|
||||||
.mx_ExtendedProfileView_content {
|
|
||||||
padding: var(--cpd-space-2x) 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--cpd-space-4x);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styling für die Felder */
|
|
||||||
.mx_ExtendedProfileView_field {
|
|
||||||
margin-bottom: var(--cpd-space-2x);
|
|
||||||
|
|
||||||
.mx_ExtendedProfileView_icon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
color: var(--cpd-color-icon-secondary);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Labels und Werte */
|
|
||||||
.mx_ExtendedProfileView_label {
|
|
||||||
color: var(--cpd-color-text-secondary);
|
|
||||||
font-size: var(--cpd-font-size-body-xs);
|
|
||||||
margin-bottom: var(--cpd-space-1x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileView_value {
|
|
||||||
color: var(--cpd-color-text-primary);
|
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
&.mx_ExtendedProfileView_multiline {
|
|
||||||
white-space: pre-line;
|
|
||||||
margin-top: var(--cpd-space-1x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Spezielle Styles für Website-Links */
|
|
||||||
.mx_ExtendedProfileView_link {
|
|
||||||
color: var(--cpd-color-text-action-accent);
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Für Bereich "Weitere Informationen" */
|
|
||||||
.mx_ExtendedProfileView_further_info {
|
|
||||||
padding: var(--cpd-space-1x) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading State */
|
|
||||||
.mx_ExtendedProfileView_loading {
|
|
||||||
padding: var(--cpd-space-4x) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Error State */
|
|
||||||
.mx_ExtendedProfileView_error {
|
|
||||||
margin: var(--cpd-space-2x) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
.mx_ExtendedProfileSettings {
|
|
||||||
margin-top: 24px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings h3 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_fields {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_field {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_fieldsRow {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_field {
|
|
||||||
flex: 1;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Business contact field specific layouts */
|
|
||||||
.mx_ExtendedProfileSettings_address_row {
|
|
||||||
.mx_ExtendedProfileSettings_field_business_street {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_field_business_street_nr {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_postal_row {
|
|
||||||
.mx_ExtendedProfileSettings_field_business_plz {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_field_business_city {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_contact_row {
|
|
||||||
.mx_ExtendedProfileSettings_field_business_email {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_field_business_tel {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_ExtendedProfileSettings_multiline textarea {
|
|
||||||
min-height: 200px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { Alert } from "@vector-im/compound-web";
|
|
||||||
import GlobeIcon from "@vector-im/compound-design-tokens/assets/web/icons/web-browser";
|
|
||||||
import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin";
|
|
||||||
import BadgeIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-profile";
|
|
||||||
import StarIcon from "@vector-im/compound-design-tokens/assets/web/icons/favourite";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
|
||||||
import { Container } from "../right_panel/UserInfo";
|
|
||||||
import InlineSpinner from "../elements/InlineSpinner";
|
|
||||||
import {
|
|
||||||
EXT_PROFILE_KEY_FURTHER_INFO,
|
|
||||||
EXT_PROFILE_KEY_SPECIALIZATION,
|
|
||||||
EXT_PROFILE_KEY_TITLE,
|
|
||||||
EXT_PROFILE_KEY_WEBSITE,
|
|
||||||
} from "../settings/ExtendedProfileSettings";
|
|
||||||
import { Flex } from "@element-hq/web-shared-components";
|
|
||||||
|
|
||||||
interface ExtendedProfileViewProps {
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtendedProfileData = {
|
|
||||||
title: string | null;
|
|
||||||
specialization: string | null;
|
|
||||||
address: string | null;
|
|
||||||
website: string | null;
|
|
||||||
furtherInfo: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Komponente zur Anzeige erweiterter Profilfelder eines anderen Benutzers
|
|
||||||
*/
|
|
||||||
const ExtendedProfileView: React.FC<ExtendedProfileViewProps> = ({ userId }) => {
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
const [error, setError] = useState<boolean>(false);
|
|
||||||
const [profileData, setProfileData] = useState<ExtendedProfileData | null>(null);
|
|
||||||
const [hasData, setHasData] = useState<boolean>(false);
|
|
||||||
|
|
||||||
//const client = useMatrixClientContext();
|
|
||||||
const userProfilesStore = SdkContextClass.instance.userProfilesStore;
|
|
||||||
|
|
||||||
// Laden der erweiterten Profilfelder für den angegebenen Benutzer
|
|
||||||
const fetchExtendedProfileData = useCallback(async () => {
|
|
||||||
if (!userId) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [
|
|
||||||
titleValue,
|
|
||||||
specializationValue,
|
|
||||||
websiteValue,
|
|
||||||
furtherInfoValue,
|
|
||||||
] = await Promise.all([
|
|
||||||
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE),
|
|
||||||
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION),
|
|
||||||
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE),
|
|
||||||
userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const data: ExtendedProfileData = {
|
|
||||||
title: titleValue as string | null,
|
|
||||||
specialization: specializationValue as string | null,
|
|
||||||
address: null, // Address not yet implemented
|
|
||||||
website: websiteValue as string | null,
|
|
||||||
furtherInfo: furtherInfoValue as string | null,
|
|
||||||
};
|
|
||||||
|
|
||||||
setProfileData(data);
|
|
||||||
|
|
||||||
// Prüfen, ob mindestens ein Feld ausgefüllt ist
|
|
||||||
const hasAnyData = Object.values(data).some((value) => value !== null && value !== "");
|
|
||||||
setHasData(hasAnyData);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Fehler beim Laden der erweiterten Profildaten", e);
|
|
||||||
setError(true);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [userId, userProfilesStore]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchExtendedProfileData();
|
|
||||||
}, [fetchExtendedProfileData]);
|
|
||||||
|
|
||||||
// Wenn keine Daten vorhanden sind, zeigen wir nichts an
|
|
||||||
if (!loading && !hasData && !error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container className="mx_ExtendedProfileView">
|
|
||||||
{loading ? (
|
|
||||||
<Flex justify="center" align="center" className="mx_ExtendedProfileView_loading">
|
|
||||||
<InlineSpinner />
|
|
||||||
</Flex>
|
|
||||||
) : error ? (
|
|
||||||
<Alert
|
|
||||||
type="critical"
|
|
||||||
title={_t("common|error")}
|
|
||||||
className="mx_ExtendedProfileView_error"
|
|
||||||
>
|
|
||||||
{_t("settings|extended_profile_settings|failed_to_load")}
|
|
||||||
</Alert>
|
|
||||||
) : hasData ? (
|
|
||||||
<div className="mx_ExtendedProfileView_content">
|
|
||||||
{(profileData?.title || profileData?.specialization) && (
|
|
||||||
<div className="mx_ExtendedProfileView_section mx_ExtendedProfileView_professional">
|
|
||||||
{profileData.title && (
|
|
||||||
<div className="mx_ExtendedProfileView_field">
|
|
||||||
<Flex align="center" gap="var(--cpd-space-2x)">
|
|
||||||
<BadgeIcon className="mx_ExtendedProfileView_icon" />
|
|
||||||
<div>
|
|
||||||
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|professional_title")}</div>
|
|
||||||
<div className="mx_ExtendedProfileView_value">{profileData.title}</div>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{profileData.specialization && (
|
|
||||||
<div className="mx_ExtendedProfileView_field">
|
|
||||||
<Flex align="center" gap="var(--cpd-space-2x)">
|
|
||||||
<StarIcon className="mx_ExtendedProfileView_icon" />
|
|
||||||
<div>
|
|
||||||
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|specialization")}</div>
|
|
||||||
<div className="mx_ExtendedProfileView_value">{profileData.specialization}</div>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{profileData?.address && (
|
|
||||||
<div className="mx_ExtendedProfileView_field">
|
|
||||||
<Flex align="center" gap="var(--cpd-space-2x)">
|
|
||||||
<LocationIcon className="mx_ExtendedProfileView_icon" />
|
|
||||||
<div>
|
|
||||||
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|business_city")}</div>
|
|
||||||
<div className="mx_ExtendedProfileView_value">{profileData.address}</div>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{profileData?.website && (
|
|
||||||
<div className="mx_ExtendedProfileView_field">
|
|
||||||
<Flex align="center" gap="var(--cpd-space-2x)">
|
|
||||||
<GlobeIcon className="mx_ExtendedProfileView_icon" />
|
|
||||||
<div>
|
|
||||||
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|website")}</div>
|
|
||||||
<div className="mx_ExtendedProfileView_value">
|
|
||||||
<a
|
|
||||||
href={profileData.website.startsWith("http") ? profileData.website : `https://${profileData.website}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
className="mx_ExtendedProfileView_link"
|
|
||||||
>
|
|
||||||
{profileData.website}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{profileData?.furtherInfo && (
|
|
||||||
<div className="mx_ExtendedProfileView_field">
|
|
||||||
<div className="mx_ExtendedProfileView_further_info">
|
|
||||||
<div className="mx_ExtendedProfileView_label">{_t("settings|extended_profile_settings|further_information")}</div>
|
|
||||||
<div className="mx_ExtendedProfileView_value mx_ExtendedProfileView_multiline">{profileData.furtherInfo}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExtendedProfileView;
|
|
||||||
@@ -11,20 +11,19 @@ import { Text } from "@vector-im/compound-web";
|
|||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
// Extended profile property keys
|
|
||||||
import {
|
// Extended profile property keys (read-only display in the right panel).
|
||||||
EXT_PROFILE_KEY_TITLE,
|
const EXT_PROFILE_KEY_TITLE = "io.element.profile.title";
|
||||||
EXT_PROFILE_KEY_SPECIALIZATION,
|
const EXT_PROFILE_KEY_SPECIALIZATION = "io.element.profile.specialization";
|
||||||
EXT_PROFILE_KEY_PRACTICE_NAME,
|
const EXT_PROFILE_KEY_WEBSITE = "io.element.profile.website";
|
||||||
EXT_PROFILE_KEY_BUSINESS_STREET,
|
const EXT_PROFILE_KEY_FURTHER_INFO = "io.element.profile.further_info";
|
||||||
EXT_PROFILE_KEY_BUSINESS_STREET_NR,
|
const EXT_PROFILE_KEY_PRACTICE_NAME = "io.element.profile.practice_name";
|
||||||
EXT_PROFILE_KEY_BUSINESS_CITY,
|
const EXT_PROFILE_KEY_BUSINESS_STREET = "io.element.profile.business_street";
|
||||||
EXT_PROFILE_KEY_BUSINESS_PLZ,
|
const EXT_PROFILE_KEY_BUSINESS_STREET_NR = "io.element.profile.business_street_nr";
|
||||||
EXT_PROFILE_KEY_BUSINESS_EMAIL,
|
const EXT_PROFILE_KEY_BUSINESS_CITY = "io.element.profile.business_city";
|
||||||
EXT_PROFILE_KEY_BUSINESS_TEL,
|
const EXT_PROFILE_KEY_BUSINESS_PLZ = "io.element.profile.business_plz";
|
||||||
EXT_PROFILE_KEY_WEBSITE,
|
const EXT_PROFILE_KEY_BUSINESS_EMAIL = "io.element.profile.business_email";
|
||||||
EXT_PROFILE_KEY_FURTHER_INFO,
|
const EXT_PROFILE_KEY_BUSINESS_TEL = "io.element.profile.business_tel";
|
||||||
} from "../settings/ExtendedProfileSettings";
|
|
||||||
|
|
||||||
// Helper function to preserve line breaks
|
// Helper function to preserve line breaks
|
||||||
const renderMultilineText = (text: string): React.ReactNode[] => {
|
const renderMultilineText = (text: string): React.ReactNode[] => {
|
||||||
@@ -60,7 +59,7 @@ const UserExtendedProfileInfo: React.FC<UserExtendedProfileInfoProps> = ({ userI
|
|||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProfileData = async () => {
|
const fetchProfileData = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
setError(false);
|
setError(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -1,536 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { type ChangeEvent, useCallback, useEffect, useState } from "react";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { Alert } from "@vector-im/compound-web";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { useToastContext } from "../../../contexts/ToastContext";
|
|
||||||
import InlineSpinner from "../elements/InlineSpinner";
|
|
||||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
|
||||||
import Field from "../elements/Field";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
|
|
||||||
const SpinnerToast: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
|
|
||||||
<>
|
|
||||||
<InlineSpinner />
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Extended profile property keys
|
|
||||||
export const EXT_PROFILE_KEY_TITLE = "io.element.profile.title";
|
|
||||||
export const EXT_PROFILE_KEY_SPECIALIZATION = "io.element.profile.specialization";
|
|
||||||
export const EXT_PROFILE_KEY_WEBSITE = "io.element.profile.website";
|
|
||||||
export const EXT_PROFILE_KEY_FURTHER_INFO = "io.element.profile.further_info";
|
|
||||||
export const EXT_PROFILE_KEY_PRACTICE_NAME = "io.element.profile.practice_name";
|
|
||||||
export const EXT_PROFILE_KEY_BUSINESS_STREET = "io.element.profile.business_street";
|
|
||||||
export const EXT_PROFILE_KEY_BUSINESS_STREET_NR = "io.element.profile.business_street_nr";
|
|
||||||
export const EXT_PROFILE_KEY_BUSINESS_CITY = "io.element.profile.business_city";
|
|
||||||
export const EXT_PROFILE_KEY_BUSINESS_PLZ = "io.element.profile.business_plz";
|
|
||||||
export const EXT_PROFILE_KEY_BUSINESS_EMAIL = "io.element.profile.business_email";
|
|
||||||
export const EXT_PROFILE_KEY_BUSINESS_TEL = "io.element.profile.business_tel";
|
|
||||||
|
|
||||||
// Unstable feature key for extended profiles
|
|
||||||
export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "org.matrix.msc4133.extended_profiles";
|
|
||||||
|
|
||||||
interface ExtendedProfileSettingsProps {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component for managing extended profile fields for medical professionals.
|
|
||||||
* Allows editing title, specialization, website, and further information.
|
|
||||||
*/
|
|
||||||
const ExtendedProfileSettings: React.FC<ExtendedProfileSettingsProps> = ({ className }) => {
|
|
||||||
const [title, setTitle] = useState<string>("");
|
|
||||||
const [specialization, setSpecialization] = useState<string>("");
|
|
||||||
const [website, setWebsite] = useState<string>("");
|
|
||||||
const [furtherInfo, setFurtherInfo] = useState<string>("");
|
|
||||||
const [practiceName, setPracticeName] = useState<string>("");
|
|
||||||
|
|
||||||
// Business contact field states
|
|
||||||
const [businessStreet, setBusinessStreet] = useState<string>("");
|
|
||||||
const [businessStreetNr, setBusinessStreetNr] = useState<string>("");
|
|
||||||
const [businessCity, setBusinessCity] = useState<string>("");
|
|
||||||
const [businessPlz, setBusinessPlz] = useState<string>("");
|
|
||||||
const [businessEmail, setBusinessEmail] = useState<string>("");
|
|
||||||
const [businessTel, setBusinessTel] = useState<string>("");
|
|
||||||
|
|
||||||
// Validation states for email and phone
|
|
||||||
const [businessEmailValid, setBusinessEmailValid] = useState<boolean>(true);
|
|
||||||
const [businessTelValid, setBusinessTelValid] = useState<boolean>(true);
|
|
||||||
|
|
||||||
// Store original values for cancel functionality
|
|
||||||
const [originalTitle, setOriginalTitle] = useState<string>("");
|
|
||||||
const [originalSpecialization, setOriginalSpecialization] = useState<string>("");
|
|
||||||
const [originalWebsite, setOriginalWebsite] = useState<string>("");
|
|
||||||
const [originalFurtherInfo, setOriginalFurtherInfo] = useState<string>("");
|
|
||||||
const [originalPracticeName, setOriginalPracticeName] = useState<string>("");
|
|
||||||
|
|
||||||
// Original business contact field values
|
|
||||||
const [originalBusinessStreet, setOriginalBusinessStreet] = useState<string>("");
|
|
||||||
const [originalBusinessStreetNr, setOriginalBusinessStreetNr] = useState<string>("");
|
|
||||||
const [originalBusinessCity, setOriginalBusinessCity] = useState<string>("");
|
|
||||||
const [originalBusinessPlz, setOriginalBusinessPlz] = useState<string>("");
|
|
||||||
const [originalBusinessEmail, setOriginalBusinessEmail] = useState<string>("");
|
|
||||||
const [originalBusinessTel, setOriginalBusinessTel] = useState<string>("");
|
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState<boolean>(false);
|
|
||||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
|
||||||
const [saveError, setSaveError] = useState<boolean>(false);
|
|
||||||
const [loadError, setLoadError] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const toastRack = useToastContext();
|
|
||||||
const client = useMatrixClientContext();
|
|
||||||
const userProfilesStore = SdkContextClass.instance.userProfilesStore;
|
|
||||||
const userId = client.getSafeUserId();
|
|
||||||
|
|
||||||
// Load extended profile data without checking server support
|
|
||||||
useEffect(() => {
|
|
||||||
const loadExtendedProfileData = async () => {
|
|
||||||
try {
|
|
||||||
setLoadError(false);
|
|
||||||
// Try to get existing data
|
|
||||||
try {
|
|
||||||
const titleValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_TITLE);
|
|
||||||
const specializationValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_SPECIALIZATION);
|
|
||||||
const websiteValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_WEBSITE);
|
|
||||||
const furtherInfoValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_FURTHER_INFO);
|
|
||||||
const practiceNameValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_PRACTICE_NAME);
|
|
||||||
|
|
||||||
// Fetch business contact fields
|
|
||||||
const businessStreetValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET);
|
|
||||||
const businessStreetNrValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_STREET_NR);
|
|
||||||
const businessCityValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_CITY);
|
|
||||||
const businessPlzValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_PLZ);
|
|
||||||
const businessEmailValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_EMAIL);
|
|
||||||
const businessTelValue = await userProfilesStore.getOrFetchExtendedProfileProperty(userId, EXT_PROFILE_KEY_BUSINESS_TEL);
|
|
||||||
|
|
||||||
const titleStr = titleValue as string || "";
|
|
||||||
const specializationStr = specializationValue as string || "";
|
|
||||||
const websiteStr = websiteValue as string || "";
|
|
||||||
const furtherInfoStr = furtherInfoValue as string || "";
|
|
||||||
const practiceNameStr = practiceNameValue as string || "";
|
|
||||||
|
|
||||||
// Convert business contact field values to strings
|
|
||||||
const businessStreetStr = businessStreetValue as string || "";
|
|
||||||
const businessStreetNrStr = businessStreetNrValue as string || "";
|
|
||||||
const businessCityStr = businessCityValue as string || "";
|
|
||||||
const businessPlzStr = businessPlzValue as string || "";
|
|
||||||
const businessEmailStr = businessEmailValue as string || "";
|
|
||||||
const businessTelStr = businessTelValue as string || "";
|
|
||||||
|
|
||||||
setTitle(titleStr);
|
|
||||||
setSpecialization(specializationStr);
|
|
||||||
setWebsite(websiteStr);
|
|
||||||
setFurtherInfo(furtherInfoStr);
|
|
||||||
setPracticeName(practiceNameStr);
|
|
||||||
|
|
||||||
// Set business contact fields
|
|
||||||
setBusinessStreet(businessStreetStr);
|
|
||||||
setBusinessStreetNr(businessStreetNrStr);
|
|
||||||
setBusinessCity(businessCityStr);
|
|
||||||
setBusinessPlz(businessPlzStr);
|
|
||||||
setBusinessEmail(businessEmailStr);
|
|
||||||
setBusinessTel(businessTelStr);
|
|
||||||
|
|
||||||
// Store original values for cancel functionality
|
|
||||||
setOriginalTitle(titleStr);
|
|
||||||
setOriginalSpecialization(specializationStr);
|
|
||||||
setOriginalWebsite(websiteStr);
|
|
||||||
setOriginalFurtherInfo(furtherInfoStr);
|
|
||||||
setOriginalPracticeName(practiceNameStr);
|
|
||||||
|
|
||||||
// Store original business contact field values
|
|
||||||
setOriginalBusinessStreet(businessStreetStr);
|
|
||||||
setOriginalBusinessStreetNr(businessStreetNrStr);
|
|
||||||
setOriginalBusinessCity(businessCityStr);
|
|
||||||
setOriginalBusinessPlz(businessPlzStr);
|
|
||||||
setOriginalBusinessEmail(businessEmailStr);
|
|
||||||
setOriginalBusinessTel(businessTelStr);
|
|
||||||
} catch (e) {
|
|
||||||
// Just log the error but don't set loadError to true
|
|
||||||
// as we want the fields to show up even when empty
|
|
||||||
logger.warn("Failed to fetch extended profile properties", e);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to load extended profile data", e);
|
|
||||||
setLoadError(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadExtendedProfileData();
|
|
||||||
}, [userId, userProfilesStore, client]);
|
|
||||||
|
|
||||||
// Check if any field has been modified
|
|
||||||
useEffect(() => {
|
|
||||||
const hasChanges =
|
|
||||||
title !== originalTitle ||
|
|
||||||
specialization !== originalSpecialization ||
|
|
||||||
website !== originalWebsite ||
|
|
||||||
furtherInfo !== originalFurtherInfo ||
|
|
||||||
practiceName !== originalPracticeName ||
|
|
||||||
businessStreet !== originalBusinessStreet ||
|
|
||||||
businessStreetNr !== originalBusinessStreetNr ||
|
|
||||||
businessCity !== originalBusinessCity ||
|
|
||||||
businessPlz !== originalBusinessPlz ||
|
|
||||||
businessEmail !== originalBusinessEmail ||
|
|
||||||
businessTel !== originalBusinessTel;
|
|
||||||
|
|
||||||
setIsModified(hasChanges);
|
|
||||||
}, [
|
|
||||||
title, specialization, website, furtherInfo, practiceName,
|
|
||||||
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
|
|
||||||
businessStreet, businessStreetNr, businessCity, businessPlz, businessEmail, businessTel,
|
|
||||||
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
|
|
||||||
originalBusinessEmail, originalBusinessTel
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Field change handlers
|
|
||||||
const onTitleChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setTitle(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSpecializationChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSpecialization(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onWebsiteChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setWebsite(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onFurtherInfoChanged = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
setFurtherInfo(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onPracticeNameChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setPracticeName(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Business contact field change handlers
|
|
||||||
const onBusinessStreetChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setBusinessStreet(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onBusinessStreetNrChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setBusinessStreetNr(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onBusinessCityChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setBusinessCity(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onBusinessPlzChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setBusinessPlz(e.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Email validation with simple regex
|
|
||||||
const onBusinessEmailChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setBusinessEmail(value);
|
|
||||||
// Email format validation - basic check for something@something.something
|
|
||||||
const emailValid = value === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
||||||
setBusinessEmailValid(emailValid);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Phone validation
|
|
||||||
const onBusinessTelChanged = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
setBusinessTel(value);
|
|
||||||
// Phone format validation - allow digits, spaces, plus, minus, parentheses
|
|
||||||
const phoneValid = value === "" || /^[0-9+\-() ]+$/.test(value);
|
|
||||||
setBusinessTelValid(phoneValid);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Cancel handler - restore all original values
|
|
||||||
const onCancel = useCallback(() => {
|
|
||||||
setTitle(originalTitle);
|
|
||||||
setSpecialization(originalSpecialization);
|
|
||||||
setWebsite(originalWebsite);
|
|
||||||
setFurtherInfo(originalFurtherInfo);
|
|
||||||
setPracticeName(originalPracticeName);
|
|
||||||
|
|
||||||
// Reset business contact fields to original values
|
|
||||||
setBusinessStreet(originalBusinessStreet);
|
|
||||||
setBusinessStreetNr(originalBusinessStreetNr);
|
|
||||||
setBusinessCity(originalBusinessCity);
|
|
||||||
setBusinessPlz(originalBusinessPlz);
|
|
||||||
setBusinessEmail(originalBusinessEmail);
|
|
||||||
setBusinessTel(originalBusinessTel);
|
|
||||||
|
|
||||||
// Reset validation states
|
|
||||||
setBusinessEmailValid(true);
|
|
||||||
setBusinessTelValid(true);
|
|
||||||
}, [
|
|
||||||
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
|
|
||||||
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
|
|
||||||
originalBusinessEmail, originalBusinessTel
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Save handler - save all fields at once
|
|
||||||
const onSave = useCallback(async (): Promise<void> => {
|
|
||||||
if (!isModified) return;
|
|
||||||
|
|
||||||
// Validate email and phone fields before saving
|
|
||||||
if (!businessEmailValid || !businessTelValid) {
|
|
||||||
setSaveError(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSaving(true);
|
|
||||||
setSaveError(false);
|
|
||||||
const removeToast = toastRack.displayToast(
|
|
||||||
<SpinnerToast>{_t("settings|extended_profile_settings|saving_profile")}</SpinnerToast>
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Save all modified fields
|
|
||||||
const savePromises = [];
|
|
||||||
|
|
||||||
if (title !== originalTitle) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_TITLE, title));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (specialization !== originalSpecialization) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_SPECIALIZATION, specialization));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (website !== originalWebsite) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_WEBSITE, website));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (furtherInfo !== originalFurtherInfo) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_FURTHER_INFO, furtherInfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (practiceName !== originalPracticeName) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_PRACTICE_NAME, practiceName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save business contact fields if modified
|
|
||||||
if (businessStreet !== originalBusinessStreet) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_STREET, businessStreet));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (businessStreetNr !== originalBusinessStreetNr) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_STREET_NR, businessStreetNr));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (businessCity !== originalBusinessCity) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_CITY, businessCity));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (businessPlz !== originalBusinessPlz) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_PLZ, businessPlz));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (businessEmail !== originalBusinessEmail) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_EMAIL, businessEmail));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (businessTel !== originalBusinessTel) {
|
|
||||||
savePromises.push(userProfilesStore.setExtendedProfileProperty(EXT_PROFILE_KEY_BUSINESS_TEL, businessTel));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(savePromises);
|
|
||||||
|
|
||||||
// Update original values after successful save
|
|
||||||
setOriginalTitle(title);
|
|
||||||
setOriginalSpecialization(specialization);
|
|
||||||
setOriginalWebsite(website);
|
|
||||||
setOriginalFurtherInfo(furtherInfo);
|
|
||||||
setOriginalPracticeName(practiceName);
|
|
||||||
|
|
||||||
// Update original business contact field values
|
|
||||||
setOriginalBusinessStreet(businessStreet);
|
|
||||||
setOriginalBusinessStreetNr(businessStreetNr);
|
|
||||||
setOriginalBusinessCity(businessCity);
|
|
||||||
setOriginalBusinessPlz(businessPlz);
|
|
||||||
setOriginalBusinessEmail(businessEmail);
|
|
||||||
setOriginalBusinessTel(businessTel);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to save profile information", e);
|
|
||||||
setSaveError(true);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
removeToast();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
isModified, title, specialization, website, furtherInfo, practiceName,
|
|
||||||
originalTitle, originalSpecialization, originalWebsite, originalFurtherInfo, originalPracticeName,
|
|
||||||
businessStreet, businessStreetNr, businessCity, businessPlz, businessEmail, businessTel,
|
|
||||||
originalBusinessStreet, originalBusinessStreetNr, originalBusinessCity, originalBusinessPlz,
|
|
||||||
originalBusinessEmail, originalBusinessTel, businessEmailValid, businessTelValid,
|
|
||||||
toastRack, userProfilesStore
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames("mx_ExtendedProfileSettings", className)}>
|
|
||||||
<div className="mx_ExtendedProfileSettings_section">
|
|
||||||
<h3 className="mx_ExtendedProfileSettings_heading">{_t("settings|extended_profile_settings|additional_information")}</h3>
|
|
||||||
|
|
||||||
{loadError && (
|
|
||||||
<Alert title={_t("common|error")} type="critical">
|
|
||||||
{_t("settings|extended_profile_settings|failed_to_load")}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mx_ExtendedProfileSettings_fields">
|
|
||||||
<div className="mx_ExtendedProfileSettings_fieldsRow">
|
|
||||||
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_title">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|professional_title")}
|
|
||||||
value={title}
|
|
||||||
onChange={onTitleChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_specialization">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|specialization")}
|
|
||||||
value={specialization}
|
|
||||||
onChange={onSpecializationChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx_ExtendedProfileSettings_field">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|practice_name")}
|
|
||||||
value={practiceName}
|
|
||||||
onChange={onPracticeNameChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Street and number in one row, street takes 2/3 width */}
|
|
||||||
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_address_row">
|
|
||||||
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_street">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|business_street")}
|
|
||||||
value={businessStreet}
|
|
||||||
onChange={onBusinessStreetChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_street_nr">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|business_street_nr")}
|
|
||||||
value={businessStreetNr}
|
|
||||||
onChange={onBusinessStreetNrChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PLZ and City in one row, PLZ takes 1/3 width */}
|
|
||||||
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_postal_row">
|
|
||||||
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_plz">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|business_plz")}
|
|
||||||
value={businessPlz}
|
|
||||||
onChange={onBusinessPlzChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mx_ExtendedProfileSettings_field mx_ExtendedProfileSettings_field_business_city">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|business_city")}
|
|
||||||
value={businessCity}
|
|
||||||
onChange={onBusinessCityChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Email field with validation */}
|
|
||||||
<div className="mx_ExtendedProfileSettings_fieldsRow mx_ExtendedProfileSettings_contact_row">
|
|
||||||
<div className="mx_ExtendedProfileSettings_field">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|business_email")}
|
|
||||||
value={businessEmail}
|
|
||||||
onChange={onBusinessEmailChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
type="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Telephone field with validation */}
|
|
||||||
<div className="mx_ExtendedProfileSettings_field">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|business_tel")}
|
|
||||||
value={businessTel}
|
|
||||||
onChange={onBusinessTelChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
type="tel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="mx_ExtendedProfileSettings_field">
|
|
||||||
<Field
|
|
||||||
label={_t("settings|extended_profile_settings|website")}
|
|
||||||
value={website}
|
|
||||||
onChange={onWebsiteChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx_ExtendedProfileSettings_field">
|
|
||||||
<Field
|
|
||||||
className="mx_ExtendedProfileSettings_multiline"
|
|
||||||
label={_t("settings|extended_profile_settings|further_information")}
|
|
||||||
value={furtherInfo}
|
|
||||||
onChange={onFurtherInfoChanged}
|
|
||||||
disabled={isSaving}
|
|
||||||
element="textarea"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Single set of buttons for all fields */}
|
|
||||||
<div className="mx_ExtendedProfileSettings_buttons">
|
|
||||||
<AccessibleButton onClick={onCancel} kind="primary_outline" disabled={isSaving || !isModified}>
|
|
||||||
{_t("common|cancel")}
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton onClick={onSave} kind="primary" disabled={isSaving || !isModified}>
|
|
||||||
{_t("common|save")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{saveError && (
|
|
||||||
<Alert title={_t("settings|extended_profile_settings|failed_to_save")} type="critical">
|
|
||||||
{businessEmailValid ? null : _t("settings|extended_profile_settings|invalid_email")}
|
|
||||||
<span> </span>
|
|
||||||
{businessTelValid ? null : _t("settings|extended_profile_settings|invalid_phone")}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExtendedProfileSettings;
|
|
||||||
@@ -28,7 +28,6 @@ import LogoutDialog, { shouldShowLogoutDialog } from "../dialogs/LogoutDialog";
|
|||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import { SettingsSection } from "./shared/SettingsSection.tsx";
|
import { SettingsSection } from "./shared/SettingsSection.tsx";
|
||||||
import ExtendedProfileSettings from "./ExtendedProfileSettings";
|
|
||||||
|
|
||||||
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
const SpinnerToast: React.FC<{ children?: ReactNode }> = ({ children }) => (
|
||||||
<>
|
<>
|
||||||
@@ -239,9 +238,6 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({
|
|||||||
)}
|
)}
|
||||||
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
{userIdentifier && <UsernameBox username={userIdentifier} />}
|
||||||
|
|
||||||
{/* Always display the Extended Profile Settings component */}
|
|
||||||
<ExtendedProfileSettings className="mx_UserProfileSettings_extendedProfile" />
|
|
||||||
|
|
||||||
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
|
<Flex gap="var(--cpd-space-4x)" className="mx_UserProfileSettings_profile_buttons">
|
||||||
{externalAccountManagementUrl && (
|
{externalAccountManagementUrl && (
|
||||||
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
<ManageAccountButton externalAccountManagementUrl={externalAccountManagementUrl} />
|
||||||
|
|||||||
@@ -2441,26 +2441,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"extended_profile_settings": {
|
|
||||||
"additional_information": "Zusätzliche Informationen",
|
|
||||||
"professional_title": "Titel",
|
|
||||||
"specialization": "Fachgebiet",
|
|
||||||
"practice_name": "Praxisname / Firmenname",
|
|
||||||
"business_street": "Strasse",
|
|
||||||
"business_street_nr": "Hausnummer",
|
|
||||||
"business_plz": "PLZ",
|
|
||||||
"business_city": "Ort",
|
|
||||||
"business_email": "Email (geschäftlich)",
|
|
||||||
"business_tel": "Telefon (geschäftlich)",
|
|
||||||
"website": "Webseite",
|
|
||||||
"further_information": "Weitere Informationen / Qualifikationen",
|
|
||||||
"saving_profile": "Profil wird gespeichert...",
|
|
||||||
"failed_to_load": "Fehler beim Laden der erweiterten Profileinstellungen.",
|
|
||||||
"failed_to_save": "Fehler beim Speichern der erweiterten Profileinstellungen.",
|
|
||||||
"server_not_supported": "Dieser Server unterstützt keine erweiterten Profileinstellungen.",
|
|
||||||
"invalid_email": "Ungültige E-Mail-Adresse.",
|
|
||||||
"invalid_phone": "Ungültige Telefonnummer."
|
|
||||||
},
|
|
||||||
"account": {
|
"account": {
|
||||||
"dialog_title": "<strong>Einstellungen:</strong> Konto",
|
"dialog_title": "<strong>Einstellungen:</strong> Konto",
|
||||||
"title": "Konto"
|
"title": "Konto"
|
||||||
@@ -3760,7 +3740,6 @@
|
|||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"extended_profile": {
|
"extended_profile": {
|
||||||
"header": "Weitere Informationen",
|
|
||||||
"title": "Titel",
|
"title": "Titel",
|
||||||
"specialization": "Fachgebiet",
|
"specialization": "Fachgebiet",
|
||||||
"practice_name": "Praxisname / Firmenname",
|
"practice_name": "Praxisname / Firmenname",
|
||||||
@@ -3769,7 +3748,6 @@
|
|||||||
"business_tel": "Telefon (geschäftlich)",
|
"business_tel": "Telefon (geschäftlich)",
|
||||||
"website": "Webseite",
|
"website": "Webseite",
|
||||||
"further_info": "Weitere Infos / Qualifikationen",
|
"further_info": "Weitere Infos / Qualifikationen",
|
||||||
"not_provided": "Nicht angegeben",
|
|
||||||
"error_loading_profile": "Fehler beim Laden des Profils"
|
"error_loading_profile": "Fehler beim Laden des Profils"
|
||||||
},
|
},
|
||||||
"admin_tools_section": "Administrationswerkzeuge",
|
"admin_tools_section": "Administrationswerkzeuge",
|
||||||
|
|||||||
@@ -2484,26 +2484,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"extended_profile_settings": {
|
|
||||||
"additional_information": "Additional Information",
|
|
||||||
"professional_title": "Title",
|
|
||||||
"specialization": "Specialization",
|
|
||||||
"practice_name": "Practice Name",
|
|
||||||
"business_street": "Strasse",
|
|
||||||
"business_street_nr": "Street Number",
|
|
||||||
"business_plz": "ZIP Code",
|
|
||||||
"business_city": "City",
|
|
||||||
"business_email": "Business Email",
|
|
||||||
"business_tel": "Business Phone",
|
|
||||||
"website": "Website",
|
|
||||||
"further_information": "Further Information / Qualifications",
|
|
||||||
"saving_profile": "Saving profile...",
|
|
||||||
"failed_to_load": "Failed to load extended profile settings.",
|
|
||||||
"failed_to_save": "Failed to save extended profile settings.",
|
|
||||||
"server_not_supported": "Your server does not support extended profile settings.",
|
|
||||||
"invalid_email": "Invalid email.",
|
|
||||||
"invalid_phone": "Invalid phone number."
|
|
||||||
},
|
|
||||||
"account": {
|
"account": {
|
||||||
"dialog_title": "<strong>Settings:</strong> Account",
|
"dialog_title": "<strong>Settings:</strong> Account",
|
||||||
"title": "Account"
|
"title": "Account"
|
||||||
@@ -3816,7 +3796,6 @@
|
|||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"extended_profile": {
|
"extended_profile": {
|
||||||
"header": "Additional infos",
|
|
||||||
"title": "Title",
|
"title": "Title",
|
||||||
"specialization": "Speciality",
|
"specialization": "Speciality",
|
||||||
"practice_name": "Practice name",
|
"practice_name": "Practice name",
|
||||||
@@ -3825,7 +3804,6 @@
|
|||||||
"business_tel": "Business phone",
|
"business_tel": "Business phone",
|
||||||
"website": "Website",
|
"website": "Website",
|
||||||
"further_info": "Further infos / qualifications",
|
"further_info": "Further infos / qualifications",
|
||||||
"not_provided": "Not provided",
|
|
||||||
"error_loading_profile": "Error loading profile"
|
"error_loading_profile": "Error loading profile"
|
||||||
},
|
},
|
||||||
"admin_tools_section": "Admin Tools",
|
"admin_tools_section": "Admin Tools",
|
||||||
|
|||||||
@@ -2488,26 +2488,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"extended_profile_settings": {
|
|
||||||
"additional_information": "Informations supplémentaires",
|
|
||||||
"professional_title": "Titre",
|
|
||||||
"specialization": "Spécialisation",
|
|
||||||
"practice_name": "Nom de l’entreprise",
|
|
||||||
"business_street": "Rue",
|
|
||||||
"business_street_nr": "Numéro de rue",
|
|
||||||
"business_plz": "Code postal",
|
|
||||||
"business_city": "Ville",
|
|
||||||
"business_email": "E-mail professionnel",
|
|
||||||
"business_tel": "Téléphone professionnel",
|
|
||||||
"website": "Site web",
|
|
||||||
"further_information": "Informations supplémentaires / qualifications",
|
|
||||||
"saving_profile": "Enregistrement des paramètres de profil étendus…",
|
|
||||||
"failed_to_load": "Erreur lors du chargement des paramètres de profil étendus.",
|
|
||||||
"failed_to_save": "Erreur lors de l'enregistrement des paramètres de profil étendus.",
|
|
||||||
"server_not_supported": "Le serveur d’accueil ne prend pas en charge les paramètres de profil étendus.",
|
|
||||||
"invalid_email": "Adresse e-mail invalide.",
|
|
||||||
"invalid_phone": "Numéro de téléphone invalide."
|
|
||||||
},
|
|
||||||
"account": {
|
"account": {
|
||||||
"dialog_title": "<strong>Paramètres : </strong> Compte",
|
"dialog_title": "<strong>Paramètres : </strong> Compte",
|
||||||
"title": "Compte"
|
"title": "Compte"
|
||||||
@@ -3820,7 +3800,6 @@
|
|||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"extended_profile": {
|
"extended_profile": {
|
||||||
"header": "Informations supplémentaires",
|
|
||||||
"title": "Titre",
|
"title": "Titre",
|
||||||
"specialization": "Spécialisation",
|
"specialization": "Spécialisation",
|
||||||
"practice_name": "Nom de l’entreprise",
|
"practice_name": "Nom de l’entreprise",
|
||||||
@@ -3829,7 +3808,6 @@
|
|||||||
"business_tel": "Téléphone (professionnel)",
|
"business_tel": "Téléphone (professionnel)",
|
||||||
"website": "Site web",
|
"website": "Site web",
|
||||||
"further_info": "Informations supplémentaires / qualifications",
|
"further_info": "Informations supplémentaires / qualifications",
|
||||||
"not_provided": "Non fourni",
|
|
||||||
"error_loading_profile": "Erreur lors du chargement du profil"
|
"error_loading_profile": "Erreur lors du chargement du profil"
|
||||||
},
|
},
|
||||||
"admin_tools_section": "Outils d’administration",
|
"admin_tools_section": "Outils d’administration",
|
||||||
|
|||||||
@@ -2119,18 +2119,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"extended_profile_settings": {
|
|
||||||
"additional_information": "Informazioni aggiuntive",
|
|
||||||
"professional_title": "Titolo",
|
|
||||||
"specialization": "Specializzazione",
|
|
||||||
"address": "Indirizzo",
|
|
||||||
"website": "Sito web",
|
|
||||||
"further_information": "Ulteriori informazioni / dettagli",
|
|
||||||
"saving_profile": "Salvataggio del profilo esteso...",
|
|
||||||
"failed_to_load": "Impossibile caricare le impostazioni del profilo esteso.",
|
|
||||||
"failed_to_save": "Impossibile salvare le impostazioni del profilo esteso.",
|
|
||||||
"server_not_supported": "Il tuo server non supporta le impostazioni del profilo esteso."
|
|
||||||
},
|
|
||||||
"all_rooms_home": "Mostra tutte le stanze nella pagina principale",
|
"all_rooms_home": "Mostra tutte le stanze nella pagina principale",
|
||||||
"all_rooms_home_description": "Tutte le stanze in cui sei appariranno nella pagina principale.",
|
"all_rooms_home_description": "Tutte le stanze in cui sei appariranno nella pagina principale.",
|
||||||
"always_show_message_timestamps": "Mostra sempre l'orario dei messaggi",
|
"always_show_message_timestamps": "Mostra sempre l'orario dei messaggi",
|
||||||
|
|||||||
@@ -2195,13 +2195,11 @@
|
|||||||
},
|
},
|
||||||
"user_info": {
|
"user_info": {
|
||||||
"extended_profile": {
|
"extended_profile": {
|
||||||
"header": "Informazioni aggiuntive",
|
|
||||||
"title": "Titolo",
|
"title": "Titolo",
|
||||||
"specialization": "Specializzazione",
|
"specialization": "Specializzazione",
|
||||||
"address": "Indirizzo",
|
"address": "Indirizzo",
|
||||||
"website": "Sito web",
|
"website": "Sito web",
|
||||||
"further_info": "Ulteriori informazioni / qualifiche",
|
"further_info": "Ulteriori informazioni / qualifiche",
|
||||||
"not_provided": "Non fornito",
|
|
||||||
"error_loading_profile": "Errore nel caricamento del profilo"
|
"error_loading_profile": "Errore nel caricamento del profilo"
|
||||||
},
|
},
|
||||||
"admin_tools_section": "Administratoriaus įrankiai",
|
"admin_tools_section": "Administratoriaus įrankiai",
|
||||||
|
|||||||
@@ -216,52 +216,6 @@ export class UserProfilesStore {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an extended profile property for the current user.
|
|
||||||
*
|
|
||||||
* @param property - The property key to set
|
|
||||||
* @param value - The value to set
|
|
||||||
* @returns true if successful, false otherwise
|
|
||||||
*/
|
|
||||||
public async setExtendedProfileProperty(property: string, value: ExtendedProfileValue): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await this.client.setExtendedProfileProperty(property, value);
|
|
||||||
|
|
||||||
const userId = this.client.getSafeUserId();
|
|
||||||
if (!this.extendedProfiles.has(userId)) {
|
|
||||||
this.extendedProfiles.set(userId, new Map());
|
|
||||||
}
|
|
||||||
this.extendedProfiles.get(userId)?.set(property, value);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(`Error setting extended profile property ${property}`, e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an extended profile property for the current user.
|
|
||||||
*
|
|
||||||
* @param property - The property key to delete
|
|
||||||
* @returns true if successful, false otherwise
|
|
||||||
*/
|
|
||||||
public async deleteExtendedProfileProperty(property: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await this.client.deleteExtendedProfileProperty(property);
|
|
||||||
|
|
||||||
const userId = this.client.getSafeUserId();
|
|
||||||
if (this.extendedProfiles.has(userId)) {
|
|
||||||
this.extendedProfiles.get(userId)?.delete(property);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(`Error deleting extended profile property ${property}`, e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public flush(): void {
|
public flush(): void {
|
||||||
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
this.profiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
this.knownProfiles = new LruCache<string, IMatrixProfile | null>(cacheSize);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { mkStubRoom, stubClient } from "../../../../test-utils";
|
|||||||
import { ToastContext, type ToastRack } from "../../../../../src/contexts/ToastContext";
|
import { ToastContext, type ToastRack } from "../../../../../src/contexts/ToastContext";
|
||||||
import { OwnProfileStore } from "../../../../../src/stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../../../../src/stores/OwnProfileStore";
|
||||||
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
|
||||||
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
|
|
||||||
import Modal from "../../../../../src/Modal";
|
import Modal from "../../../../../src/Modal";
|
||||||
|
|
||||||
interface MockedAvatarSettingProps {
|
interface MockedAvatarSettingProps {
|
||||||
@@ -87,9 +86,6 @@ describe("ProfileSettings", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
// HealthChat's ExtendedProfileSettings reads the global SdkContextClass.instance.userProfilesStore,
|
|
||||||
// which requires the singleton client to be set (it is, on login, in the real app).
|
|
||||||
SdkContextClass.instance.client = client;
|
|
||||||
toastRack = {
|
toastRack = {
|
||||||
displayToast: jest.fn().mockReturnValue(jest.fn()),
|
displayToast: jest.fn().mockReturnValue(jest.fn()),
|
||||||
};
|
};
|
||||||
|
|||||||
-3
@@ -85,9 +85,6 @@ describe("<AccountUserSettingsTab />", () => {
|
|||||||
|
|
||||||
stores = new SdkContextClass();
|
stores = new SdkContextClass();
|
||||||
stores.client = mockClient;
|
stores.client = mockClient;
|
||||||
// HealthChat's ExtendedProfileSettings (mounted via UserProfileSettings) reads the global
|
|
||||||
// SdkContextClass.instance.userProfilesStore, which requires the singleton client to be set.
|
|
||||||
SdkContextClass.instance.client = mockClient;
|
|
||||||
// stub out this store completely to avoid mocking initialisation
|
// stub out this store completely to avoid mocking initialisation
|
||||||
const mockOidcClientStore = {} as unknown as OidcClientStore;
|
const mockOidcClientStore = {} as unknown as OidcClientStore;
|
||||||
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
jest.spyOn(stores, "oidcClientStore", "get").mockReturnValue(mockOidcClientStore);
|
||||||
|
|||||||
+4
-4
@@ -42,14 +42,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
id="mx_Field_113"
|
id="mx_Field_3"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
placeholder="Email Address"
|
placeholder="Email Address"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
for="mx_Field_113"
|
for="mx_Field_3"
|
||||||
>
|
>
|
||||||
Email Address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
@@ -154,14 +154,14 @@ exports[`<AccountUserSettingsTab /> 3pids should display 3pid email addresses an
|
|||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
autocomplete="tel-national"
|
autocomplete="tel-national"
|
||||||
id="mx_Field_114"
|
id="mx_Field_4"
|
||||||
label="Phone Number"
|
label="Phone Number"
|
||||||
placeholder="Phone Number"
|
placeholder="Phone Number"
|
||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
for="mx_Field_114"
|
for="mx_Field_4"
|
||||||
>
|
>
|
||||||
Phone Number
|
Phone Number
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user