Web: Client: InvitePanel: Added user search, added email validation, added editing for manual added items, fixed invite panel state
This commit is contained in:
parent
bbca780d4c
commit
38b217d65e
@ -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));
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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))
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -313,3 +313,10 @@ export function changeTheme(key) {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function getUsersByQuery(query) {
|
||||
return request({
|
||||
method: "get",
|
||||
url: `/people/search?query=${query}`,
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
3
public/images/check.edit.react.svg
Normal file
3
public/images/check.edit.react.svg
Normal 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 |
10
public/images/cross.edit.react.svg
Normal file
10
public/images/cross.edit.react.svg
Normal 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 |
10
public/images/info.edit.react.svg
Normal file
10
public/images/info.edit.react.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user