Skip to content

Commit

Permalink
feat(web): users adjustments (#1316)
Browse files Browse the repository at this point in the history
Among other changes (see commits) this PR restores the text for the
"Edit password too" in order to avoid introducing a new translation just
because. Of course, we can change texts as much as needed in the future,
but let's preserve them when possible now.
  • Loading branch information
dgdavid authored Jun 12, 2024
2 parents e311083 + 75f58ee commit 0e3802b
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 644 deletions.
270 changes: 23 additions & 247 deletions web/src/components/users/FirstUser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,30 @@
* find current contact information at www.suse.com.
*/

import React, { useState, useEffect, useRef } from "react";
import {
Alert,
Checkbox,
Form, FormGroup, TextInput,
Menu, MenuContent, MenuList, MenuItem,
Skeleton,
Stack
} from "@patternfly/react-core";
import React, { useState, useEffect } from "react";
import { Skeleton, Split, Stack } from "@patternfly/react-core";
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { useNavigate } from "react-router-dom";
import { RowActions, PasswordAndConfirmationInput, Popup, ButtonLink } from '~/components/core';
import { RowActions, ButtonLink } from '~/components/core';
import { _ } from "~/i18n";
import { useCancellablePromise } from "~/utils";
import { useInstallerClient } from "~/context/installer";
import { suggestUsernames } from '~/components/users/utils';

const UserNotDefined = ({ actionCb }) => {
return (
<Stack hasGutter>
<div>{_("No user defined yet.")}</div>
<div>
<strong>
{_("Please, be aware that a user must be defined before installing the system to be able to log into it.")}
</strong>
</div>
<ButtonLink to="first" isPrimary>{_("Define a user now")}</ButtonLink>
</Stack>
<>
<Stack hasGutter>
<div>{_("No user defined yet.")}</div>
<div>
<strong>
{_("Please, be aware that a user must be defined before installing the system to be able to log into it.")}
</strong>
</div>
<Split hasGutter>
<ButtonLink to="first" isPrimary>{_("Define a user now")}</ButtonLink>
</Split>
</Stack>
</>
);
};

Expand All @@ -73,38 +69,6 @@ const UserData = ({ user, actions }) => {
);
};

const UsernameSuggestions = ({ isOpen = false, entries, onSelect, setInsideDropDown, focusedIndex = -1 }) => {
if (!isOpen) return;

return (
<Menu
aria-label={_("Username suggestion dropdown")}
className="first-username-dropdown"
onMouseEnter={() => setInsideDropDown(true)}
onMouseLeave={() => setInsideDropDown(false)}
>
<MenuContent>
<MenuList>
{entries.map((suggestion, index) => (
<MenuItem
key={index}
itemId={index}
isFocused={focusedIndex === index}
onClick={() => onSelect(suggestion)}
>
{ /* TRANSLATORS: dropdown username suggestions */}
{_("Use suggested username")} <b>{suggestion}</b>
</MenuItem>
))}
</MenuList>
</MenuContent>
</Menu>
);
};

const CREATE_MODE = 'create';
const EDIT_MODE = 'edit';

const initialUser = {
userName: "",
fullName: "",
Expand All @@ -116,23 +80,11 @@ export default function FirstUser() {
const client = useInstallerClient();
const { cancellablePromise } = useCancellablePromise();
const [user, setUser] = useState({});
const [errors, setErrors] = useState([]);
const [formValues, setFormValues] = useState(initialUser);
const [isLoading, setIsLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [isFormOpen, setIsFormOpen] = useState(false);
const [isValidPassword, setIsValidPassword] = useState(true);
const [isSettingPassword, setIsSettingPassword] = useState(false);
const [showSuggestions, setShowSuggestions] = useState(false);
const [insideDropDown, setInsideDropDown] = useState(false);
const [focusedIndex, setFocusedIndex] = useState(-1);
const [suggestions, setSuggestions] = useState([]);
const usernameInputRef = useRef();

useEffect(() => {
cancellablePromise(client.users.getUser()).then(userValues => {
setUser(userValues);
setFormValues({ ...initialUser, ...userValues });
setIsLoading(false);
});
}, [client.users, cancellablePromise]);
Expand All @@ -145,60 +97,18 @@ export default function FirstUser() {
});
}, [client.users]);

const openForm = (e, mode = CREATE_MODE) => {
setIsEditing(mode === EDIT_MODE);
// Password will be always set when creating the user. In the edit mode it
// depends on the user choice
setIsSettingPassword(mode === CREATE_MODE);
// To avoid confusion, do not expose the current password
setFormValues({ ...initialUser, ...user, password: "" });
setIsFormOpen(true);
};

const closeForm = () => {
setErrors([]);
setIsEditing(false);
setIsFormOpen(false);
};

const accept = async (formName, e) => {
e.preventDefault();
setErrors([]);
setIsLoading(true);

// Preserve current password value if the user was not editing it.
const newUser = { ...formValues };
if (!isSettingPassword) newUser.password = user.password;

const { result, issues = [] } = await client.users.setUser(newUser);
setErrors(issues);
setIsLoading(false);
if (result) {
setUser(newUser);

closeForm();
}
};

const remove = async () => {
setIsLoading(true);

const result = await client.users.removeUser();

if (result) {
setUser(initialUser);
setFormValues(initialUser);
setIsLoading(false);
}
};

const handleInputChange = ({ target }, value) => {
const { name } = target;
setFormValues({ ...formValues, [name]: value });
};

const isUserDefined = user?.userName && user?.userName !== "";
const showErrors = () => ((errors || []).length > 0);
const navigate = useNavigate();

const actions = [
Expand All @@ -213,145 +123,11 @@ export default function FirstUser() {
}
];

const toggleShowPasswordField = () => setIsSettingPassword(!isSettingPassword);
const usingValidPassword = formValues.password && formValues.password !== "" && isValidPassword;
const submitDisable = formValues.userName === "" || (isSettingPassword && !usingValidPassword);

const displaySuggestions = !formValues.userName && formValues.fullName && showSuggestions;
useEffect(() => {
if (displaySuggestions) {
setFocusedIndex(-1);
setSuggestions(suggestUsernames(formValues.fullName));
}
}, [displaySuggestions, formValues.fullName]);

const onSuggestionSelected = (suggestion) => {
setInsideDropDown(false);
setFormValues({ ...formValues, userName: suggestion });
usernameInputRef.current?.focus();
};

const handleKeyDown = (event) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault(); // Prevent page scrolling
if (suggestions.length > 0) setShowSuggestions(true);
setFocusedIndex((prevIndex) => (prevIndex + 1) % suggestions.length);
break;
case 'ArrowUp':
event.preventDefault(); // Prevent page scrolling
if (suggestions.length > 0) setShowSuggestions(true);
setFocusedIndex((prevIndex) => (prevIndex - (prevIndex === -1 ? 0 : 1) + suggestions.length) % suggestions.length);
break;
case 'Enter':
if (focusedIndex >= 0) {
onSuggestionSelected(suggestions[focusedIndex]);
}
break;
case 'Escape':
case 'Tab':
setShowSuggestions(false);
break;
default:
break;
}
};

if (isLoading) return <Skeleton />;

return (
<>
{isUserDefined ? <UserData user={user} actions={actions} /> : <UserNotDefined actionCb={openForm} />}
{ /* TODO: Extract this form to a component, if possible */}
{isFormOpen &&
<Popup
isOpen
title={isEditing ? _("Edit user account") : _("Create user account")}
inlineSize="small"
>
<Form id="createUser" onSubmit={(e) => accept("createUser", e)}>
{showErrors() &&
<Alert variant="warning" isInline title={_("Something went wrong")}>
{errors.map((e, i) => <p key={`error_${i}`}>{e}</p>)}
</Alert>}

<FormGroup fieldId="userFullName" label={_("Full name")}>
<TextInput
id="userFullName"
name="fullName"
aria-label={_("User full name")}
value={formValues.fullName}
label={_("User full name")}
onChange={handleInputChange}
/>
</FormGroup>

<FormGroup
className="first-username-wrapper"
fieldId="userName"
label={_("Username")}
isRequired
onFocus={() => setShowSuggestions(true)}
onBlur={() => !insideDropDown && setShowSuggestions(false)}
>
<TextInput
id="userName"
name="userName"
aria-label={_("Username")}
ref={usernameInputRef}
value={formValues.userName}
label={_("Username")}
isRequired
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<UsernameSuggestions
isOpen={displaySuggestions && suggestions.length > 0}
entries={suggestions}
onSelect={onSuggestionSelected}
setInsideDropDown={setInsideDropDown}
focusedIndex={focusedIndex}
/>
</FormGroup>

{isEditing &&
<Checkbox
aria-label={_("Edit password too")}
id="edit-password"
name="edit-password"
// TRANSLATORS: check box label
label={_("Edit password too")}
isChecked={isSettingPassword}
onChange={toggleShowPasswordField}
/>}

{isSettingPassword &&
<PasswordAndConfirmationInput
value={formValues.password}
onChange={handleInputChange}
onValidation={isValid => setIsValidPassword(isValid)}
/>}

<Checkbox
aria-label={_("user autologin")}
id="autologin"
name="autologin"
// TRANSLATORS: check box label
label={_("Auto-login")}
isChecked={formValues.autologin}
onChange={handleInputChange}
/>
</Form>

<Popup.Actions>
<Popup.Confirm
form="createUser"
type="submit"
isDisabled={submitDisable}
/>
<Popup.Cancel onClick={closeForm} />
</Popup.Actions>
</Popup>}
</>
);
if (isLoading) {
return <Skeleton />;
} else if (isUserDefined) {
return <UserData user={user} actions={actions} />;
} else {
return <UserNotDefined />;
}
}
Loading

0 comments on commit 0e3802b

Please sign in to comment.