Web: Client: InvitePanel: Added user search, added email validation, added editing for manual added items, fixed invite panel state

This commit is contained in:
Ilya Oleshko 2022-08-30 08:55:19 +03:00
parent bbca780d4c
commit 38b217d65e
13 changed files with 315 additions and 85 deletions

View File

@ -139,7 +139,7 @@ export default inject(
const { uploadPanelVisible } = uploadDataStore;
const { isVisible: versionHistoryPanelVisible } = versionHistoryStore;
const { hotkeyPanelVisible, invitePanelVisible } = auth.settingsStore;
const { hotkeyPanelVisible, invitePanelOptions } = auth.settingsStore;
return {
sharingPanelVisible,
@ -163,7 +163,7 @@ export default inject(
createMasterForm,
setSelectFileDialogVisible,
hotkeyPanelVisible,
invitePanelVisible,
invitePanelVisible: invitePanelOptions.visible,
};
}
)(observer(Panels));

View File

@ -1,9 +1,10 @@
import React, { useState, useCallback, useEffect, useRef } from "react";
import React, { useState, useCallback } from "react";
import debounce from "lodash.debounce";
import Avatar from "@docspace/components/avatar";
import { parseAddresses } from "@docspace/components/utils/email";
import {
StyledInviteInput,
StyledInviteInputContainer,
@ -12,12 +13,14 @@ import {
SearchItemText,
} from "./StyledInvitePanel";
const InviteInput = ({ onAddUser, getUsersList }) => {
const InviteInput = ({ onAddUser, getUsersByQuery }) => {
const [inputValue, setInputValue] = useState("");
const [usersList, setUsersList] = useState([]);
const [panelVisible, setPanelVisible] = useState(false);
const toUserItem = (email) => {
const emails = parseAddresses(email);
const uid = () =>
String(Date.now().toString(32) + Math.random().toString(16)).replace(
/\./g,
@ -27,19 +30,21 @@ const InviteInput = ({ onAddUser, getUsersList }) => {
email,
id: uid(),
displayName: email,
errors: emails[0].parseErrors,
};
};
const searchByTerm = async (value) => {
const users = await getUsersList();
setUsersList(users);
value = value.trim();
//const user = toUserItem(value);
//onAddUser && onAddUser(user);
if (value.length > 0) {
const users = await getUsersByQuery(value);
setUsersList(users);
}
};
const debouncedSearch = useCallback(
debounce((value) => searchByTerm(value), 1000),
debounce((value) => searchByTerm(value), 500),
[]
);
@ -54,11 +59,16 @@ const InviteInput = ({ onAddUser, getUsersList }) => {
};
const getItemContent = (item) => {
const { avatarSmall, displayName, email, id } = item;
const { avatar, displayName, email, id } = item;
const addUser = () => {
onAddUser && onAddUser(item);
setPanelVisible(false);
};
return (
<StyledDropDownItem key={id}>
<Avatar size="min" role="user" source={avatarSmall} />
<StyledDropDownItem key={id} onClick={addUser}>
<Avatar size="min" role="user" source={avatar} />
<div>
<SearchItemText primary>{displayName}</SearchItemText>
<SearchItemText>{email}</SearchItemText>
@ -68,6 +78,14 @@ const InviteInput = ({ onAddUser, getUsersList }) => {
);
};
const addItem = () => {
const item = toUserItem(inputValue);
onAddUser && onAddUser(item);
setPanelVisible(false);
};
return (
<StyledInviteInputContainer>
<StyledInviteInput
@ -76,7 +94,13 @@ const InviteInput = ({ onAddUser, getUsersList }) => {
value={inputValue}
/>
<StyledDropDown isDefaultMode={false} open={panelVisible} manualX="16px">
{usersList?.map((user) => getItemContent(user))}
{usersList.length ? (
usersList?.map((user) => getItemContent(user))
) : (
<StyledDropDownItem onClick={addItem}>
Add «{inputValue}»
</StyledDropDownItem>
)}
</StyledDropDown>
</StyledInviteInputContainer>
);

View File

@ -6,6 +6,14 @@ import Box from "@docspace/components/box";
import DropDown from "@docspace/components/drop-down";
import DropDownItem from "@docspace/components/drop-down-item";
import Text from "@docspace/components/text";
import Button from "@docspace/components/button";
import HelpButton from "@docspace/components/help-button";
import CheckIcon from "PUBLIC_DIR/images/check.edit.react.svg";
import CrossIcon from "PUBLIC_DIR/images/cross.edit.react.svg";
import DeleteIcon from "PUBLIC_DIR/images/mobile.actions.remove.react.svg";
import commonIconsStyles from "@docspace/components/utils/common-icons-style";
import { Base } from "@docspace/components/themes";
@ -63,6 +71,10 @@ const StyledInviteInput = styled(TextInput)`
margin-bottom: 20px;
`;
const StyledEditInput = styled(TextInput)`
${fillAvailableWidth}
`;
const StyledComboBox = styled(ComboBox)`
margin-left: auto;
@ -102,6 +114,47 @@ const SearchItemText = styled(Text)`
StyledBlock.defaultProps = { theme: Base };
const StyledEditButton = styled(Button)`
width: 32px;
height: 32px;
padding: 0px;
`;
const StyledCheckIcon = styled(CheckIcon)`
${commonIconsStyles}
path {
fill: ${(props) => props.theme.filesEditingWrapper.fill} !important;
}
:hover {
fill: ${(props) => props.theme.filesEditingWrapper.hoverFill} !important;
}
`;
StyledCheckIcon.defaultProps = { theme: Base };
const StyledCrossIcon = styled(CrossIcon)`
${commonIconsStyles}
path {
fill: ${(props) => props.theme.filesEditingWrapper.fill} !important;
}
:hover {
fill: ${(props) => props.theme.filesEditingWrapper.hoverFill} !important;
}
`;
const StyledDeleteIcon = styled(DeleteIcon)`
margin-left: auto;
${commonIconsStyles}
path {
fill: ${(props) => props.theme.filesEditingWrapper.fill} !important;
}
:hover {
fill: ${(props) => props.theme.filesEditingWrapper.hoverFill} !important;
}
`;
const StyledHelpButton = styled(HelpButton)``;
export {
StyledBlock,
StyledHeading,
@ -114,4 +167,10 @@ export {
StyledDropDown,
StyledDropDownItem,
SearchItemText,
StyledEditInput,
StyledEditButton,
StyledCheckIcon,
StyledCrossIcon,
StyledHelpButton,
StyledDeleteIcon,
};

View File

@ -19,43 +19,32 @@ import Items from "./items.js";
import InviteInput from "./InviteInput";
const InvitePanel = ({
invitePanelVisible,
setInvitePanelVisible,
invitePanelOptions,
setInvitePanelOptions,
visible,
t,
theme,
tReady,
getUsersList,
getUsersByQuery,
getFolderInfo,
folders,
}) => {
const testItems = [
{
email: "test1@gmail.com",
id: "1",
displayName: "Administrator Test1",
avatarSmall:
"/storage/userPhotos/root/66faa6e4-f133-11ea-b126-00ffeec8b4ef_orig_46-46.jpeg?_=1380485370",
},
{
email: "test2@gmail.com",
id: "2",
displayName: "Administrator Test2",
avatarSmall:
"/storage/userPhotos/root/66faa6e4-f133-11ea-b126-00ffeec8b4ef_orig_46-46.jpeg?_=1380485370",
},
{
email: "test3@gmail.com",
id: "3",
displayName: "Administrator Test3",
},
{
email: "test4@gmail.com",
id: "4",
displayName: "Administrator Test4",
},
];
const [items, setItems] = useState([]);
const [selectedRoom, setSelectedRoom] = useState(null);
const [items, setItems] = useState(testItems);
useEffect(() => {
const { id } = invitePanelOptions;
const room = folders.find((folder) => folder.id === id);
if (room) {
setSelectedRoom(room);
} else {
getFolderInfo(id).then((info) => {
setSelectedRoom(info);
});
}
}, [invitePanelOptions]);
const onClose = () => setInvitePanelVisible(false);
const onClose = () => setInvitePanelOptions({ visible: false });
const onAddUser = (user) => {
setItems([...items, user]);
@ -78,17 +67,8 @@ const InvitePanel = ({
return (
<StyledInvitePanel>
<Backdrop
onClick={onClose}
visible={invitePanelVisible}
isAside={true}
zIndex={210}
/>
<Aside
className="invite_panel"
visible={invitePanelVisible}
onClose={onClose}
>
<Backdrop onClick={onClose} visible isAside={true} zIndex={210} />
<Aside className="invite_panel" visible onClose={onClose}>
<StyledBlock>
<StyledHeading>Invite users to the room</StyledHeading>
</StyledBlock>
@ -98,8 +78,10 @@ const InvitePanel = ({
</StyledBlock>
<StyledSubHeader>Individual invitation</StyledSubHeader>
<InviteInput getUsersList={getUsersList} onAddUser={onAddUser} />
<Items items={items} onSelectItemAccess={onSelectItemAccess} />
<InviteInput getUsersByQuery={getUsersByQuery} onAddUser={onAddUser} />
{!!items.length && (
<Items items={items} onSelectItemAccess={onSelectItemAccess} />
)}
</Aside>
</StyledInvitePanel>
);
@ -107,20 +89,25 @@ const InvitePanel = ({
InvitePanel.defaultProps = { theme: Base };
export default inject(({ auth, peopleStore }) => {
export default inject(({ auth, peopleStore, filesStore }) => {
const {
invitePanelVisible,
setInvitePanelVisible,
invitePanelOptions,
setInvitePanelOptions,
theme,
} = auth.settingsStore;
const { getUsersList } = peopleStore.usersStore;
const { getUsersByQuery } = peopleStore.usersStore;
const { getFolderInfo, folders } = filesStore;
return {
invitePanelVisible,
setInvitePanelVisible,
invitePanelOptions,
setInvitePanelOptions,
visible: invitePanelOptions.visible,
theme,
getUsersList,
getUsersByQuery,
getFolderInfo,
folders,
};
})(
withTranslation(["HotkeysPanel", "Article", "Common"])(observer(InvitePanel))

View File

@ -1,10 +1,33 @@
import React from "react";
import React, { useState, useCallback } from "react";
import debounce from "lodash.debounce";
import Text from "@docspace/components/text";
import Avatar from "@docspace/components/avatar";
import { StyledRow, StyledComboBox } from "./StyledInvitePanel";
import { parseAddresses } from "@docspace/components/utils/email";
import {
StyledRow,
StyledComboBox,
StyledEditInput,
StyledEditButton,
StyledCheckIcon,
StyledCrossIcon,
StyledHelpButton,
StyledDeleteIcon,
} from "./StyledInvitePanel";
const Item = ({ item, onSelectItemAccess }) => {
const { avatar, avatarSmall, displayName, email, id, errors } = item;
const userAvatar = avatar || avatarSmall;
const name = !!userAvatar ? displayName : email;
const source = !!userAvatar ? avatarSmall : "/static/images/@.react.svg";
const [edit, setEdit] = useState(false);
const [inputValue, setInputValue] = useState(name);
const [parseErrors, setParseErrors] = useState(errors);
const Items = ({ t, items, onSelectItemAccess }) => {
const getAccesses = (id) => {
return [
{
@ -50,17 +73,85 @@ const Items = ({ t, items, onSelectItemAccess }) => {
];
};
return items.map((item) => {
const { avatarSmall, displayName, email, id } = item;
const onEdit = (e) => {
if (e.detail === 2) {
setEdit(true);
}
};
const name = !!avatarSmall ? displayName : email;
const source = !!avatarSmall ? avatarSmall : "/static/images/@.react.svg";
const options = getAccesses(id);
const onCancelEdit = (e) => {
setInputValue(name);
setEdit(false);
};
return (
<StyledRow key={id}>
<Avatar size="min" role="user" source={source} />
<Text>{name}</Text>
const onSaveEdit = (e) => {
if (inputValue === "") {
setInputValue(name);
}
setEdit(false);
debouncedValidate(inputValue);
console.log(parseErrors);
};
const validateValue = (value) => {
const email = parseAddresses(value);
const errors = email[0].parseErrors;
if (!!errors.length) {
setParseErrors(errors);
} else {
setParseErrors([]);
}
};
const debouncedValidate = useCallback(
debounce((value) => validateValue(value), 500),
[]
);
const onChangeValue = (e) => {
const value = e.target.value.trim();
setInputValue(value);
debouncedValidate(value);
};
const options = getAccesses(id);
const hasError = !!parseErrors.length;
const tooltipBody = parseErrors.map((error) => (
<div key={error.key}>{error.message}</div>
));
const removeItem = (e) => {
const id = e.target.dataset.id;
onSelectItemAccess({
key: "delete",
id,
});
};
const displayBody = (
<>
<Text onClick={onEdit}>{inputValue}</Text>
{hasError ? (
<>
<StyledHelpButton
iconName="/static/images/info.edit.react.svg"
displayType="auto"
offsetRight={0}
tooltipContent={tooltipBody}
size={16}
color="#F21C0E"
/>
<StyledDeleteIcon size="medium" onClick={removeItem} data-id={id} />
</>
) : (
<StyledComboBox
onSelect={onSelectItemAccess}
noBorder
@ -71,8 +162,34 @@ const Items = ({ t, items, onSelectItemAccess }) => {
selectedOption={options[5]}
showDisabledItems
/>
</StyledRow>
);
});
)}
</>
);
const okIcon = <StyledCheckIcon size="scale" />;
const cancelIcon = <StyledCrossIcon size="scale" />;
const editBody = (
<>
<StyledEditInput hasError value={inputValue} onChange={onChangeValue} />
<StyledEditButton icon={okIcon} onClick={onSaveEdit} />
<StyledEditButton icon={cancelIcon} onClick={onCancelEdit} />
</>
);
return (
<>
<Avatar size="min" role="user" source={source} />
{edit ? editBody : displayBody}
</>
);
};
const Items = ({ t, items, onSelectItemAccess }) => {
return items.map((item) => (
<StyledRow key={item.id}>
<Item item={item} onSelectItemAccess={onSelectItemAccess} />
</StyledRow>
));
};
export default Items;

View File

@ -347,7 +347,13 @@ class ContextOptionsStore {
};
onClickInviteUsers = (e) => {
this.authStore.settingsStore.setInvitePanelVisible(true);
const data = (e.currentTarget && e.currentTarget.dataset) || e;
const { action } = data;
this.authStore.settingsStore.setInvitePanelOptions({
visible: true,
id: action,
});
};
onClickPin = (e, id, t) => {
@ -544,6 +550,7 @@ class ContextOptionsStore {
icon: "/static/images/person.react.svg",
onClick: (e) => this.onClickInviteUsers(e),
disabled: false,
action: item.id,
},
{
key: "room-info",

View File

@ -248,7 +248,7 @@ class DialogsStore {
this.convertDialogVisible ||
this.selectFileDialogVisible ||
this.authStore.settingsStore.hotkeyPanelVisible ||
this.authStore.settingsStore.invitePanelVisible ||
this.authStore.settingsStore.invitePanelOptions.visible ||
this.versionHistoryStore.isVisible
);
}

View File

@ -219,6 +219,12 @@ class UsersStore {
return this.peopleStore.selectionStore.selection.some((el) => el.id === id);
};
getUsersByQuery = async (query) => {
const users = await api.people.getUsersByQuery(query);
return users;
};
get peopleList() {
const list = this.users.map((user) => {
const {

View File

@ -313,3 +313,10 @@ export function changeTheme(key) {
data,
});
}
export function getUsersByQuery(query) {
return request({
method: "get",
url: `/people/search?query=${query}`,
});
}

View File

@ -129,7 +129,7 @@ class SettingsStore {
helpLink = null;
hotkeyPanelVisible = false;
frameConfig = null;
invitePanelVisible = false;
invitePanelOptions = { visible: false };
enablePlugins = false;
pluginOptions = [];
@ -561,8 +561,8 @@ class SettingsStore {
return this.frameConfig?.name === window.name;
}
setInvitePanelVisible = (invitePanelVisible) => {
this.invitePanelVisible = invitePanelVisible;
setInvitePanelOptions = (invitePanelOptions) => {
this.invitePanelOptions = invitePanelOptions;
};
}

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.20718 14.2072L15.7072 4.70718L14.293 3.29297L5.50007 12.0859L1.70718 8.29297L0.292969 9.70718L4.79296 14.2072C5.18349 14.5977 5.81665 14.5977 6.20718 14.2072Z" fill="#A3A9AE"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_28520_90146)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.41451 8.00041L13.707 12.293L12.2928 13.7072L8.00041 9.41474L3.70971 13.7061L2.29538 12.292L6.58619 8.00052L2.29284 3.70716L3.70705 2.29295L8.00029 6.58619L12.2928 2.29301L13.7071 3.70711L9.41451 8.00041Z" fill="#A3A9AE"/>
</g>
<defs>
<clipPath id="clip0_28520_90146">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 527 B

View File

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_28541_126172)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8ZM8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0ZM7 4V9H9V4H7ZM7 10V12H9V10H7Z" fill="#F21C0E"/>
</g>
<defs>
<clipPath id="clip0_28541_126172">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 566 B