Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 4fec436

Browse files
author
Kerry
authored
Device manager - rename session (PSG-528) (#9282)
* split heading into component * switch between editing and view * style file * basic tests * style device rename component * add loading state * kind of handle missing current device in drilled props * use local loading state, add basic error message * integration-ish test rename * tidy * fussy import ordering * strict errors
1 parent b8bb8f1 commit 4fec436

19 files changed

+720
-43
lines changed

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
@import "./components/views/location/_ZoomButtons.pcss";
2929
@import "./components/views/messages/_MBeaconBody.pcss";
3030
@import "./components/views/messages/shared/_MediaProcessingError.pcss";
31+
@import "./components/views/settings/devices/_DeviceDetailHeading.pcss";
3132
@import "./components/views/settings/devices/_DeviceDetails.pcss";
3233
@import "./components/views/settings/devices/_DeviceExpandDetailsButton.pcss";
3334
@import "./components/views/settings/devices/_DeviceSecurityCard.pcss";
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_DeviceDetailHeading {
18+
display: flex;
19+
flex-direction: row;
20+
justify-content: space-between;
21+
align-items: center;
22+
}
23+
24+
.mx_DeviceDetailHeading_renameCta {
25+
flex-shrink: 0;
26+
}
27+
28+
.mx_DeviceDetailHeading_renameForm {
29+
display: grid;
30+
grid-gap: $spacing-16;
31+
justify-content: left;
32+
grid-template-columns: 100%;
33+
}
34+
35+
.mx_DeviceDetailHeading_renameFormButtons {
36+
display: flex;
37+
flex-direction: row;
38+
gap: $spacing-8;
39+
40+
.mx_Spinner {
41+
width: auto;
42+
flex-grow: 0;
43+
}
44+
}
45+
46+
.mx_DeviceDetailHeading_renameFormInput {
47+
// override field styles
48+
margin: 0 0 $spacing-4 0 !important;
49+
}
50+
51+
.mx_DeviceDetailHeading_renameFormHeading {
52+
margin: 0;
53+
}
54+
55+
.mx_DeviceDetailHeading_renameFormError {
56+
color: $alert;
57+
padding-right: $spacing-4;
58+
display: block;
59+
}

res/css/components/views/settings/devices/_DeviceDetails.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ limitations under the License.
3535
display: grid;
3636
grid-gap: $spacing-16;
3737
justify-content: left;
38+
grid-template-columns: 100%;
3839

3940
&:last-child {
4041
padding-bottom: 0;

src/components/views/settings/devices/CurrentDeviceSection.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface Props {
3131
isSigningOut: boolean;
3232
onVerifyCurrentDevice: () => void;
3333
onSignOutCurrentDevice: () => void;
34+
saveDeviceName: (deviceName: string) => Promise<void>;
3435
}
3536

3637
const CurrentDeviceSection: React.FC<Props> = ({
@@ -39,14 +40,16 @@ const CurrentDeviceSection: React.FC<Props> = ({
3940
isSigningOut,
4041
onVerifyCurrentDevice,
4142
onSignOutCurrentDevice,
43+
saveDeviceName,
4244
}) => {
4345
const [isExpanded, setIsExpanded] = useState(false);
4446

4547
return <SettingsSubsection
4648
heading={_t('Current session')}
4749
data-testid='current-session-section'
4850
>
49-
{ isLoading && <Spinner /> }
51+
{ /* only show big spinner on first load */ }
52+
{ isLoading && !device && <Spinner /> }
5053
{ !!device && <>
5154
<DeviceTile
5255
device={device}
@@ -61,7 +64,9 @@ const CurrentDeviceSection: React.FC<Props> = ({
6164
<DeviceDetails
6265
device={device}
6366
isSigningOut={isSigningOut}
67+
onVerifyDevice={onVerifyCurrentDevice}
6468
onSignOutDevice={onSignOutCurrentDevice}
69+
saveDeviceName={saveDeviceName}
6570
/>
6671
}
6772
<br />
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, { FormEvent, useEffect, useState } from 'react';
18+
19+
import { _t } from '../../../../languageHandler';
20+
import AccessibleButton from '../../elements/AccessibleButton';
21+
import Field from '../../elements/Field';
22+
import Spinner from '../../elements/Spinner';
23+
import { Caption } from '../../typography/Caption';
24+
import Heading from '../../typography/Heading';
25+
import { DeviceWithVerification } from './types';
26+
27+
interface Props {
28+
device: DeviceWithVerification;
29+
saveDeviceName: (deviceName: string) => Promise<void>;
30+
}
31+
32+
const DeviceNameEditor: React.FC<Props & { stopEditing: () => void }> = ({
33+
device, saveDeviceName, stopEditing,
34+
}) => {
35+
const [deviceName, setDeviceName] = useState(device.display_name || '');
36+
const [isLoading, setIsLoading] = useState(false);
37+
const [error, setError] = useState<string | null>(null);
38+
39+
useEffect(() => {
40+
setDeviceName(device.display_name || '');
41+
}, [device.display_name]);
42+
43+
const onInputChange = (event: React.ChangeEvent<HTMLInputElement>): void =>
44+
setDeviceName(event.target.value);
45+
46+
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
47+
setIsLoading(true);
48+
setError(null);
49+
event.preventDefault();
50+
try {
51+
await saveDeviceName(deviceName);
52+
stopEditing();
53+
} catch (error) {
54+
setError(_t('Failed to set display name'));
55+
setIsLoading(false);
56+
}
57+
};
58+
59+
const headingId = `device-rename-${device.device_id}`;
60+
const descriptionId = `device-rename-description-${device.device_id}`;
61+
62+
return <form
63+
aria-disabled={isLoading}
64+
className="mx_DeviceDetailHeading_renameForm"
65+
onSubmit={onSubmit}
66+
method="post"
67+
>
68+
<p
69+
id={headingId}
70+
className="mx_DeviceDetailHeading_renameFormHeading"
71+
>
72+
{ _t('Rename session') }
73+
</p>
74+
<div>
75+
<Field
76+
data-testid='device-rename-input'
77+
type="text"
78+
value={deviceName}
79+
autoComplete="off"
80+
onChange={onInputChange}
81+
autoFocus
82+
disabled={isLoading}
83+
aria-labelledby={headingId}
84+
aria-describedby={descriptionId}
85+
className="mx_DeviceDetailHeading_renameFormInput"
86+
maxLength={100}
87+
/>
88+
<Caption
89+
id={descriptionId}
90+
>
91+
{ _t('Please be aware that session names are also visible to people you communicate with') }
92+
{ !!error &&
93+
<span
94+
data-testid="device-rename-error"
95+
className='mx_DeviceDetailHeading_renameFormError'>
96+
{ error }
97+
</span>
98+
}
99+
</Caption>
100+
</div>
101+
<div className="mx_DeviceDetailHeading_renameFormButtons">
102+
<AccessibleButton
103+
onClick={onSubmit}
104+
kind="primary"
105+
data-testid='device-rename-submit-cta'
106+
disabled={isLoading}
107+
>
108+
{ _t('Save') }
109+
</AccessibleButton>
110+
<AccessibleButton
111+
onClick={stopEditing}
112+
kind="secondary"
113+
data-testid='device-rename-cancel-cta'
114+
disabled={isLoading}
115+
>
116+
{ _t('Cancel') }
117+
</AccessibleButton>
118+
{ isLoading && <Spinner w={16} h={16} /> }
119+
</div>
120+
</form>;
121+
};
122+
123+
export const DeviceDetailHeading: React.FC<Props> = ({
124+
device, saveDeviceName,
125+
}) => {
126+
const [isEditing, setIsEditing] = useState(false);
127+
128+
return isEditing
129+
? <DeviceNameEditor
130+
device={device}
131+
saveDeviceName={saveDeviceName}
132+
stopEditing={() => setIsEditing(false)}
133+
/>
134+
: <div className='mx_DeviceDetailHeading' data-testid='device-detail-heading'>
135+
<Heading size='h3'>{ device.display_name || device.device_id }</Heading>
136+
<AccessibleButton
137+
kind='link_inline'
138+
onClick={() => setIsEditing(true)}
139+
className='mx_DeviceDetailHeading_renameCta'
140+
data-testid='device-heading-rename-cta'
141+
>
142+
{ _t('Rename') }
143+
</AccessibleButton>
144+
</div>;
145+
};

src/components/views/settings/devices/DeviceDetails.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { formatDate } from '../../../../DateUtils';
2020
import { _t } from '../../../../languageHandler';
2121
import AccessibleButton from '../../elements/AccessibleButton';
2222
import Spinner from '../../elements/Spinner';
23-
import Heading from '../../typography/Heading';
23+
import { DeviceDetailHeading } from './DeviceDetailHeading';
2424
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
2525
import { DeviceWithVerification } from './types';
2626

@@ -29,6 +29,7 @@ interface Props {
2929
isSigningOut: boolean;
3030
onVerifyDevice?: () => void;
3131
onSignOutDevice: () => void;
32+
saveDeviceName: (deviceName: string) => Promise<void>;
3233
}
3334

3435
interface MetadataTable {
@@ -41,6 +42,7 @@ const DeviceDetails: React.FC<Props> = ({
4142
isSigningOut,
4243
onVerifyDevice,
4344
onSignOutDevice,
45+
saveDeviceName,
4446
}) => {
4547
const metadata: MetadataTable[] = [
4648
{
@@ -61,7 +63,10 @@ const DeviceDetails: React.FC<Props> = ({
6163
];
6264
return <div className='mx_DeviceDetails' data-testid={`device-detail-${device.device_id}`}>
6365
<section className='mx_DeviceDetails_section'>
64-
<Heading size='h3'>{ device.display_name ?? device.device_id }</Heading>
66+
<DeviceDetailHeading
67+
device={device}
68+
saveDeviceName={saveDeviceName}
69+
/>
6570
<DeviceVerificationStatusCard
6671
device={device}
6772
onVerifyDevice={onVerifyDevice}

src/components/views/settings/devices/FilteredDeviceList.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
DeviceSecurityVariation,
3333
DeviceWithVerification,
3434
} from './types';
35+
import { DevicesState } from './useOwnDevices';
3536

3637
interface Props {
3738
devices: DevicesDictionary;
@@ -41,6 +42,7 @@ interface Props {
4142
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
4243
onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void;
4344
onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
45+
saveDeviceName: DevicesState['saveDeviceName'];
4446
onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
4547
}
4648

@@ -137,13 +139,15 @@ const DeviceListItem: React.FC<{
137139
isSigningOut: boolean;
138140
onDeviceExpandToggle: () => void;
139141
onSignOutDevice: () => void;
142+
saveDeviceName: (deviceName: string) => Promise<void>;
140143
onRequestDeviceVerification?: () => void;
141144
}> = ({
142145
device,
143146
isExpanded,
144147
isSigningOut,
145148
onDeviceExpandToggle,
146149
onSignOutDevice,
150+
saveDeviceName,
147151
onRequestDeviceVerification,
148152
}) => <li className='mx_FilteredDeviceList_listItem'>
149153
<DeviceTile
@@ -161,6 +165,7 @@ const DeviceListItem: React.FC<{
161165
isSigningOut={isSigningOut}
162166
onVerifyDevice={onRequestDeviceVerification}
163167
onSignOutDevice={onSignOutDevice}
168+
saveDeviceName={saveDeviceName}
164169
/>
165170
}
166171
</li>;
@@ -177,6 +182,7 @@ export const FilteredDeviceList =
177182
signingOutDeviceIds,
178183
onFilterChange,
179184
onDeviceExpandToggle,
185+
saveDeviceName,
180186
onSignOutDevices,
181187
onRequestDeviceVerification,
182188
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
@@ -234,6 +240,7 @@ export const FilteredDeviceList =
234240
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
235241
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
236242
onSignOutDevice={() => onSignOutDevices([device.device_id])}
243+
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
237244
onRequestDeviceVerification={
238245
onRequestDeviceVerification
239246
? () => onRequestDeviceVerification(device.device_id)

0 commit comments

Comments
 (0)