Merge branch 'develop' into bugfix/info-panel-optimization

This commit is contained in:
namushka 2024-05-28 15:03:24 +03:00
commit 45f2e43c60
No known key found for this signature in database
54 changed files with 1106 additions and 392 deletions

View File

@ -34,6 +34,7 @@ import FilesSelector from "@docspace/shared/selectors/Files";
import { toastr } from "@docspace/shared/components/toast";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import {
TFile,
TFileSecurity,
TFolder,
TFolderSecurity,
@ -41,7 +42,7 @@ import {
import { TBreadCrumb } from "@docspace/shared/components/selector/Selector.types";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { TSelectedFileInfo } from "@docspace/shared/selectors/Files/FilesSelector.types";
import { TRoomSecurity } from "@docspace/shared/api/rooms/types";
import { TRoom, TRoomSecurity } from "@docspace/shared/api/rooms/types";
import { TTranslation } from "@docspace/shared/types";
import SelectedFolderStore from "SRC_DIR/store/SelectedFolderStore";
@ -55,6 +56,8 @@ import InfoPanelStore from "SRC_DIR/store/InfoPanelStore";
import { FilesSelectorProps } from "./FilesSelector.types";
import { getAcceptButtonLabel, getHeaderLabel, getIsDisabled } from "./utils";
let disabledItems: (string | number)[] = [];
const FilesSelectorWrapper = ({
isPanelVisible = false,
// withoutImmediatelyClose = false,
@ -87,7 +90,7 @@ const FilesSelectorWrapper = ({
treeFolders,
selection,
disabledItems,
// disabledItems,
setConflictDialogData,
checkFileConflicts,
itemOperationToFolder,
@ -169,8 +172,16 @@ const FilesSelectorWrapper = ({
onCloseAction();
};
const getFilesArchiveError = (name: string) =>
t("Common:ArchivedRoomAction", { name });
const getFilesArchiveError = React.useCallback(
(name: string) => t("Common:ArchivedRoomAction", { name }),
[t],
);
React.useEffect(() => {
return () => {
disabledItems = [];
};
}, []);
const onAccept = async (
selectedItemId: string | number | undefined,
@ -375,6 +386,9 @@ const FilesSelectorWrapper = ({
isMove || isCopy || isRestore ? "select-file-modal-cancel" : ""
}
getFilesArchiveError={getFilesArchiveError}
withCreateFolder={
(isMove || isCopy || isRestore || isRestoreAll) ?? false
}
/>
);
};
@ -471,10 +485,13 @@ export default inject(
? selections
: selections.filter((f) => f && !f?.isEditing);
const disabledItems: (string | number)[] = [];
selectionsWithoutEditing.forEach((item) => {
if ((item?.isFolder || item?.parentId) && item?.id) {
selectionsWithoutEditing.forEach((item: TFile | TFolder | TRoom) => {
if (
(("isFolder" in item && item?.isFolder) ||
("parentId" in item && item?.parentId)) &&
item?.id &&
!disabledItems.includes(item.id)
) {
disabledItems.push(item.id);
}
});

View File

@ -24,6 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { inject, observer } from "mobx-react";
import ArrowReactSvgUrl from "PUBLIC_DIR/images/arrow.react.svg?url";
import React from "react";
import PropTypes from "prop-types";
@ -102,6 +103,15 @@ const StyledListItem = styled(StyledRoomType)`
${(props) => props.theme.createEditRoomDialog.roomType.listItem.borderColor};
border-radius: 6px;
&:hover:not(:active) {
background-color: ${(props) =>
props.theme.createEditRoomDialog.roomType.listItem.hoverBackground};
}
&:active {
border-color: ${({ accentColor }) => accentColor};
}
.choose_room-description {
color: ${(props) =>
props.theme.createEditRoomDialog.roomType.listItem.descriptionText};
@ -112,12 +122,26 @@ const StyledDropdownButton = styled(StyledRoomType)`
border-radius: 6px;
background-color: ${(props) =>
props.theme.createEditRoomDialog.roomType.dropdownButton.background};
${({ isOpen }) =>
!isOpen &&
css`
&:hover:not(:active) {
background-color: ${(props) =>
props.theme.createEditRoomDialog.roomType.dropdownButton
.hoverBackground};
}
`}
border: 1px solid
${(props) =>
props.isOpen
? props.theme.createEditRoomDialog.roomType.dropdownButton
.isOpenBorderColor
: props.theme.createEditRoomDialog.roomType.dropdownButton.borderColor};
${({ isOpen, accentColor, theme }) =>
isOpen
? accentColor
: theme.createEditRoomDialog.roomType.dropdownButton.borderColor};
&:active {
border-color: ${({ accentColor }) => accentColor};
}
.choose_room-description {
color: ${(props) =>
@ -178,6 +202,7 @@ const RoomType = ({
isOpen,
id,
selectedId,
currentColorScheme,
}) => {
const room = {
type: roomType,
@ -185,6 +210,8 @@ const RoomType = ({
description: getRoomTypeDescriptionTranslation(roomType, t),
};
const accentColor = currentColorScheme?.main?.accent;
const arrowClassName =
type === "dropdownButton"
? "choose_room-forward_btn dropdown-button"
@ -219,7 +246,12 @@ const RoomType = ({
);
return type === "listItem" ? (
<StyledListItem id={id} title={t(room.title)} onClick={onClick}>
<StyledListItem
accentColor={accentColor}
id={id}
title={t(room.title)}
onClick={onClick}
>
{content}
</StyledListItem>
) : type === "dropdownButton" ? (
@ -229,6 +261,7 @@ const RoomType = ({
onClick={onClick}
isOpen={isOpen}
data-selected-id={selectedId}
accentColor={accentColor}
>
{content}
</StyledDropdownButton>
@ -239,6 +272,7 @@ const RoomType = ({
onClick={onClick}
isOpen={isOpen}
data-selected-id={selectedId}
currentColorScheme={currentColorScheme}
>
{content}
</StyledDropdownItem>
@ -247,6 +281,7 @@ const RoomType = ({
id={id}
title={t(room.title)}
data-selected-id={selectedId}
currentColorScheme={currentColorScheme}
>
{content}
</StyledDisplayItem>
@ -268,6 +303,9 @@ RoomType.propTypes = {
"dropdownItem",
]),
isOpen: PropTypes.bool,
currentColorScheme: PropTypes.object,
};
export default RoomType;
export default inject(({ settingsStore }) => ({
currentColorScheme: settingsStore.currentColorScheme,
}))(observer(RoomType));

View File

@ -70,10 +70,9 @@ const SessionsRowContent = ({
)}
<Text truncate>{convertTime(date)}</Text>
{(country || city) && (
<Text truncate>
<Text fontSize="12px" fontWeight="600">
{country}
{country && city && ", "}
{city}
{country && city && ` ${city}`}
</Text>
)}
<Text truncate containerWidth="160px">

View File

@ -255,7 +255,11 @@ class AccountsHotkeysStore {
const scroll = document.getElementsByClassName(
"section-scroll",
) as HTMLCollectionOf<HTMLElement>;
if (scroll && scroll[0]) scroll[0].focus();
if (scroll && scroll[0]) {
const scrollElem = scroll[0]?.firstChild as HTMLElement;
scrollElem?.focus();
}
}
if (!this.hotkeyCaret && selection.length) {

View File

@ -2051,6 +2051,7 @@ class ContextOptionsStore {
],
};
const showUploadFolder = !(isMobile || isTablet);
const moreActions = {
id: "personal_more-form",
className: "main-button_drop-down",
@ -2072,7 +2073,7 @@ class ContextOptionsStore {
key: "personal_more-form__separator-2",
},
uploadFiles,
uploadFolder,
showUploadFolder ? uploadFolder : null,
],
};
@ -2321,6 +2322,7 @@ class ContextOptionsStore {
]
: [createTemplateForm, createTemplateNewFormFile, templateOformsGallery];
const showUploadFolder = !(isMobile || isTablet);
const options = isRoomsFolder
? [
{
@ -2338,7 +2340,7 @@ class ContextOptionsStore {
createNewFolder,
{ key: "separator", isSeparator: true },
uploadFiles,
uploadFolder,
showUploadFolder ? uploadFolder : null,
];
if (mainButtonItemsList && enablePlugins) {

View File

@ -135,7 +135,7 @@ class HotkeyStore {
if (!hotkeyCaret) {
const scroll = document.getElementsByClassName("section-scroll");
scroll && scroll[0] && scroll[0].focus();
scroll && scroll[0] && scroll[0]?.firstChild.focus();
}
if (!hotkeyCaret && selection.length) {

View File

@ -110,6 +110,7 @@ const SelectFileDialog = ({
submitButtonId="select-file-modal-submit"
cancelButtonId="select-file-modal-cancel"
{...fileTypeDetection}
withCreateFolder={false}
/>
);
};

View File

@ -92,6 +92,7 @@ const SelectFolderDialog = ({
getFilesArchiveError={() => ""}
parentId={0}
getIsDisabled={getIsDisabled}
withCreateFolder
/>
);
};

View File

@ -2,7 +2,16 @@ import PropTypes from "prop-types";
import { ThemeProvider } from "../../components/theme-provider";
const ThemeWrapper = ({ theme, children }) => {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
return (
<ThemeProvider
theme={theme}
currentColorScheme={{
main: { accent: "#4781D1", buttons: "#5299E0" },
}}
>
{children}
</ThemeProvider>
);
};
ThemeWrapper.propTypes = {

View File

@ -44,6 +44,16 @@ module.exports = {
docs: {
autodocs: true,
},
typescript: {
check: false,
checkOptions: {},
reactDocgen: false,
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
};
function getAbsolutePath(value) {

View File

@ -337,7 +337,10 @@ export async function getTrashFolderList() {
// return request(options);
// }
export async function createFolder(parentFolderId: number, title: string) {
export async function createFolder(
parentFolderId: number | string,
title: string,
) {
const data = { title };
const options: AxiosRequestConfig = {
method: "post",

View File

@ -40,7 +40,7 @@ export interface ArticleItemProps {
/** Sets the catalog item to display text */
showText?: boolean;
/** Invokes a function upon clicking on a catalog item */
onClick?: (id?: string) => void;
onClick?: (e: React.MouseEvent, id?: string) => void;
/** Invokes a function upon dragging and dropping a catalog item */
onDrop?: (id?: string, text?: string) => void;
/** Tells when the catalog item should display initial on icon, text should be hidden */

View File

@ -37,6 +37,7 @@ describe("<CampaignsBanner />", () => {
render(
<CampaignsBanner
campaignBackground="" // TODO: add url on image
campaignIcon=""
campaignTranslate={translates}
campaignConfig={config}
onClose={() => null}

View File

@ -63,7 +63,7 @@ const DocspaceLogo = ({
const logo = getLogoUrl(logoSize, !theme.isBase);
return (
<StyledWrapper isMobile={isMobile} isResizable={isResizable} logo={!!logo}>
<StyledWrapper isMobile={isMobile} isResizable={isResizable}>
{logo && (
<img
src={logo}

View File

@ -25,13 +25,13 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { isIOS, isMobile } from "react-device-detect";
import { EmailSettings, parseAddress } from "../../utils";
import { InputSize, InputType, TextInputProps } from "../text-input";
import StyledEmailInput from "./EmailInput.styled";
import { EmailInputProps, TValidate } from "./EmailInput.types";
import { isIOS, isMobile } from "react-device-detect";
const TextInputWrapper = ({
onValidateInput,

View File

@ -25,7 +25,6 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled, { css } from "styled-components";
import { isIOS, isMobile } from "react-device-detect";
import CrossIcon from "PUBLIC_DIR/images/cross.react.svg";

View File

@ -25,7 +25,6 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { isTablet, isIOS } from "react-device-detect";
import { InputSize, TextInput } from "../text-input";
import { IconButton } from "../icon-button";

View File

@ -28,5 +28,5 @@ export type DesktopDetailsProps = {
title: string;
onMaskClick: VoidFunction;
className?: string;
showCloseButton: boolean;
showCloseButton?: boolean;
};

View File

@ -24,8 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect, useRef, useCallback } from "react";
import { isMobileOnly, isIOS } from "react-device-detect";
import React, { useEffect, useRef } from "react";
import { DeviceType } from "../../../enums";
import { Portal } from "../../portal";

View File

@ -27,7 +27,7 @@
import styled, { css } from "styled-components";
import { Base } from "../../themes";
const StyledButton = styled.div<{ isDisabled?: boolean }>`
const StyledButton = styled.div<{ isDisabled?: boolean; isAction?: boolean }>`
display: inline-block;
background: ${(props) => props.theme.selectorAddButton.background};
border: ${(props) => props.theme.selectorAddButton.border};
@ -79,6 +79,56 @@ const StyledButton = styled.div<{ isDisabled?: boolean }>`
}
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
${(props) =>
props.isAction &&
css`
// convert into 0.1 opacity
background-color: ${props.theme.currentColorScheme?.main.accent}1A;
svg {
path {
${!props.isDisabled &&
css`
fill: ${props.theme.currentColorScheme?.main.accent};
`}
}
}
:hover {
background-color: ${props.theme.currentColorScheme?.main.accent}1A;
svg {
path {
${!props.isDisabled &&
css`
fill: ${props.theme.currentColorScheme?.main.accent};
opacity: 0.85;
`}
}
}
}
:active {
background-color: ${props.theme.currentColorScheme?.main.accent}1A;
svg {
path {
${!props.isDisabled &&
css`
fill: ${props.theme.currentColorScheme?.main.accent};
opacity: 1;
filter: ${props.theme.isBase
? "brightness(90%)"
: "brightness(82%)"};
`}
}
}
}
div {
opacity: 1;
}
`}
`;
StyledButton.defaultProps = { theme: Base };

View File

@ -36,6 +36,7 @@ import { SelectorAddButtonProps } from "./SelectorAddButton.types";
const SelectorAddButton = (props: SelectorAddButtonProps) => {
const {
isDisabled = false,
isAction,
title,
className,
id,
@ -52,6 +53,7 @@ const SelectorAddButton = (props: SelectorAddButtonProps) => {
<StyledButton
{...props}
isDisabled={isDisabled}
isAction={isAction}
title={title}
onClick={onClickAction}
className={className}

View File

@ -39,4 +39,6 @@ export interface SelectorAddButtonProps {
style?: React.CSSProperties;
/** Specifies the icon name */
iconName?: string;
/** Change colors to accent */
isAction?: boolean;
}

View File

@ -28,10 +28,12 @@ import React from "react";
import styled from "styled-components";
import { Meta, StoryObj } from "@storybook/react";
import CustomSvgUrl from "PUBLIC_DIR/images/icons/32/room/custom.svg?url";
import ArchiveSvgUrl from "PUBLIC_DIR/images/room.archive.svg?url";
import FolderSvgUrl from "PUBLIC_DIR/images/icons/32/folder.svg?url";
import EmptyScreenFilter from "PUBLIC_DIR/images/empty_screen_filter.png";
import { RoomsType } from "../../enums";
import { AvatarRole } from "../avatar";
import { Selector } from "./Selector";
import { SelectorProps, TSelectorItem } from "./Selector.types";
@ -89,7 +91,26 @@ function makeName() {
const getItems = (count: number) => {
const items: TSelectorItem[] = [];
for (let i = 0; i < count / 2; i += 1) {
items.push({
key: "create_new",
id: "create_new_item",
label: "New folder",
isCreateNewItem: true,
onCreateClick: () => {},
});
items.push({
key: "input_item",
id: "input_item",
label: "",
isInputItem: true,
icon: FolderSvgUrl,
defaultInputValue: "New folder",
onAcceptInput: () => {},
onCancelInput: () => {},
});
for (let i = 0; i < count; i += 1) {
const label = makeName();
items.push({
key: `${label} ${i}`,
@ -100,21 +121,10 @@ const getItems = (count: number) => {
isAdmin: false,
isVisitor: false,
isCollaborator: false,
isRoomAdmin: false,
avatar: "",
});
}
for (let i = 0; i < count / 2; i += 1) {
const label = makeName();
items.push({
key: `room_${i}`,
id: `room_${i}`,
label: `${label} ${i}`,
icon: CustomSvgUrl,
shared: false,
isFolder: true,
roomType: RoomsType.CustomRoom,
role: AvatarRole.owner,
hasAvatar: false,
});
}

View File

@ -186,6 +186,7 @@ const StyledItem = styled.div<{
isSelected: boolean | undefined;
isDisabled?: boolean;
isMultiSelect: boolean;
noHover?: boolean;
}>`
display: flex;
align-items: center;
@ -221,6 +222,15 @@ const StyledItem = styled.div<{
`}
}
.clicked-label {
width: fit-content;
cursor: pointer;
}
.input-component {
margin-inline-start: 8px;
}
.checkbox {
svg {
margin-inline-end: 0px;
@ -245,12 +255,13 @@ const StyledItem = styled.div<{
`
: css`
${props.isSelected && !props.isMultiSelect && selectedCss}
@media (hover: hover) {
${!props.noHover &&
` @media (hover: hover) {
&:hover {
cursor: pointer;
background: ${props.theme.selector.item.hoverBackground};
}
}
}`}
`}
`;
@ -266,6 +277,36 @@ const StyledEmptyScreen = styled.div<{ withSearch: boolean }>`
box-sizing: border-box;
.buttons {
margin-top: 32px;
display: flex;
gap: 16px;
align-items: center;
justify-content: center;
.empty-folder_container-links {
display: flex;
align-items: center;
gap: 8px;
.empty-folder_link {
color: ${(props) => props.theme.selector.emptyScreen.buttonColor};
}
&:hover {
.empty-folder_link {
color: ${(props) =>
props.theme.selector.emptyScreen.hoverButtonColor};
}
svg path {
fill: ${(props) => props.theme.selector.emptyScreen.hoverButtonColor};
}
}
}
}
.empty-image {
max-width: 72px;
max-height: 72px;
@ -325,7 +366,10 @@ const StyledBreadCrumbs = styled.div<{
StyledBreadCrumbs.defaultProps = { theme: Base };
const StyledItemText = styled(Text)<{ isCurrent: boolean; isLoading: boolean }>`
const StyledItemText = styled(Text)<{
isCurrent: boolean;
isLoading?: boolean;
}>`
${(props) =>
!props.isCurrent &&
css`
@ -440,6 +484,36 @@ const StyledInfo = styled.div`
}
`;
const StyledInputWrapper = styled.div`
width: 32px;
height: 32px;
margin-inline-start: 8px;
border: 1px solid ${(props) => props.theme.selector.item.inputButtonBorder};
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
:hover {
div {
cursor: pointer;
}
cursor: pointer;
border-color: ${(props) =>
props.theme.selector.item.inputButtonBorderHover};
path {
fill: ${(props) => props.theme.selector.item.inputButtonBorderHover};
}
}
`;
StyledSelector.defaultProps = { theme: Base };
StyledHeader.defaultProps = { theme: Base };
StyledBody.defaultProps = { theme: Base };
@ -449,6 +523,7 @@ StyledEmptyScreen.defaultProps = { theme: Base };
StyledArrowRightSvg.defaultProps = { theme: Base };
StyledComboBox.defaultProps = { theme: Base };
StyledInfo.defaultProps = { theme: Base };
StyledInputWrapper.defaultProps = { theme: Base };
export {
StyledSelector,
@ -468,4 +543,5 @@ export {
StyledTabs,
StyledInfo,
StyledAccessSelector,
StyledInputWrapper,
};

View File

@ -158,6 +158,8 @@ const Selector = ({
return null;
});
const [inputItemVisible, setInputItemVisible] = React.useState(false);
const [requestRunning, setRequestRunning] = React.useState<boolean>(false);
const onSubmitAction = async (
@ -386,6 +388,7 @@ const Selector = ({
React.useEffect(() => {
const onKeyboardAction = (e: KeyboardEvent) => {
if (inputItemVisible) return;
if (e.key === ButtonKeys.esc) {
onCancel?.();
}
@ -395,7 +398,7 @@ const Selector = ({
return () => {
window.removeEventListener("keydown", onKeyboardAction);
};
}, [onCancel]);
}, [inputItemVisible, onCancel]);
React.useLayoutEffect(() => {
if (items) {
@ -596,6 +599,7 @@ const Selector = ({
withFooterInput={withFooterInput}
withFooterCheckbox={withFooterCheckbox}
descriptionText={descriptionText}
setInputItemVisible={setInputItemVisible}
// bread crumbs
{...breadCrumbsProps}
// select all

View File

@ -26,10 +26,14 @@
import React from "react";
import { RoomsType, ShareAccessRights } from "../../enums";
import { AvatarRole } from "../avatar";
import { MergeTypes } from "../../types";
import { TFileSecurity, TFolderSecurity } from "../../api/files/types";
import { TRoomSecurity } from "../../api/rooms/types";
import { AvatarRole } from "../avatar";
import { TSubmenuItem } from "../submenu";
import { SelectorAccessRightsMode } from "./Selector.enums";
// header
@ -56,12 +60,12 @@ export type TSelectorHeader =
| { withHeader?: undefined; headerProps?: undefined };
// bread crumbs
export type TBreadCrumb = {
id: string | number;
label: string;
isRoom?: boolean;
minWidth?: string;
roomType?: RoomsType;
onClick?: ({
e,
open,
@ -71,49 +75,41 @@ export type TBreadCrumb = {
open: boolean;
item: TBreadCrumb;
}) => void;
roomType?: RoomsType;
};
export interface BreadCrumbsProps {
breadCrumbs: TBreadCrumb[];
onSelectBreadCrumb: (item: TBreadCrumb) => void;
isLoading: boolean;
}
export type TDisplayedItem = {
id: string | number;
label: string;
isArrow: boolean;
isList: boolean;
isRoom?: boolean;
listItems?: TBreadCrumb[];
};
export type TSelectorBreadCrumbs =
| {
withBreadCrumbs: true;
breadCrumbs: TBreadCrumb[];
onSelectBreadCrumb: (item: TBreadCrumb) => void;
breadCrumbsLoader: React.ReactNode;
isBreadCrumbsLoading: boolean;
breadCrumbs: TBreadCrumb[];
breadCrumbsLoader: React.ReactNode;
onSelectBreadCrumb: (item: TBreadCrumb) => void;
}
| {
withBreadCrumbs?: undefined;
breadCrumbs?: undefined;
onSelectBreadCrumb?: undefined;
breadCrumbsLoader?: undefined;
isBreadCrumbsLoading?: undefined;
breadCrumbs?: undefined;
breadCrumbsLoader?: undefined;
onSelectBreadCrumb?: undefined;
};
// tabs
export type TWithTabs =
| { withTabs: true; tabsData: TSubmenuItem[]; activeTabId: string }
| { withTabs?: undefined; tabsData?: undefined; activeTabId?: undefined };
// select all
export interface SelectAllProps {
label: string;
icon: string;
onSelectAll: () => void;
isChecked: boolean;
isIndeterminate: boolean;
isLoading: boolean;
rowLoader: React.ReactNode;
}
export type TSelectorSelectAll = {
isAllIndeterminate?: boolean;
isAllChecked?: boolean;
@ -175,6 +171,8 @@ export interface EmptyScreenProps {
searchHeader: string;
searchDescription: string;
withSearch: boolean;
items: TSelectorItem[];
}
type TSelectorEmptyScreen = {
@ -358,6 +356,8 @@ export type BodyProps = TSelectorBreadCrumbs &
isMultiSelect: boolean;
setInputItemVisible: (value: boolean) => void;
items: TSelectorItem[];
renderCustomItem?: (
label: string,
@ -389,146 +389,148 @@ export type FooterProps = TSelectorFooterSubmitButton &
requestRunning?: boolean;
};
type TSelectorItemLogo =
| {
avatar: string;
color?: undefined;
hasAvatar?: boolean;
icon?: undefined;
iconOriginal?: string;
role?: AvatarRole;
}
| {
avatar?: undefined;
color: string;
hasAvatar?: undefined;
icon?: undefined;
iconOriginal?: string;
role?: undefined;
}
| {
avatar?: undefined;
color?: undefined;
hasAvatar?: undefined;
icon: string;
iconOriginal: string;
role?: undefined;
};
type TSelectorItemEmpty = {
avatar?: undefined;
color?: undefined;
hasAvatar?: undefined;
icon?: undefined;
iconOriginal?: undefined;
role?: undefined;
email?: undefined;
isOwner?: undefined;
isAdmin?: undefined;
isVisitor?: undefined;
isCollaborator?: undefined;
isRoomAdmin?: undefined;
access?: undefined;
fileExst?: undefined;
shared?: undefined;
parentId?: undefined;
rootFolderType?: undefined;
security?: undefined;
isFolder?: undefined;
filesCount?: undefined;
foldersCount?: undefined;
roomType?: undefined;
isGroup?: undefined;
name?: undefined;
isCreateNewItem?: undefined;
onCreateClick?: undefined;
onBackClick?: undefined;
isInputItem?: undefined;
defaultInputValue?: undefined;
onAcceptInput?: undefined;
onCancelInput?: undefined;
};
export type TSelectorItemUser = MergeTypes<
TSelectorItemEmpty,
{
email: string;
isOwner: boolean;
isAdmin: boolean;
isVisitor: boolean;
isCollaborator: boolean;
isRoomAdmin: boolean;
avatar: string;
hasAvatar: boolean;
role: AvatarRole;
access?: ShareAccessRights | string | number;
}
>;
export type TSelectorItemFile = MergeTypes<
TSelectorItemEmpty,
{
fileExst: string;
parentId: string | number;
rootFolderType: string | number;
security: TFileSecurity;
icon: string;
}
>;
export type TSelectorItemFolder = MergeTypes<
TSelectorItemEmpty,
{
isFolder: boolean;
parentId: string | number;
rootFolderType: string | number;
filesCount: number;
foldersCount: number;
security: TFolderSecurity;
icon?: string;
avatar?: string;
}
>;
export type TSelectorItemRoom = MergeTypes<
TSelectorItemEmpty,
{
isFolder: boolean;
roomType: RoomsType;
shared: boolean;
parentId: string | number;
rootFolderType: string | number;
filesCount: number;
foldersCount: number;
security: TRoomSecurity;
icon?: string;
color?: string;
iconOriginal?: string;
}
>;
export type TSelectorItemGroup = MergeTypes<
TSelectorItemEmpty,
{
isGroup: boolean;
name: string;
}
>;
export type TSelectorItemNew = MergeTypes<
TSelectorItemEmpty,
{
isCreateNewItem: boolean;
onCreateClick: VoidFunction;
onBackClick: VoidFunction;
}
>;
export type TSelectorItemInput = MergeTypes<
TSelectorItemEmpty,
{
isInputItem: boolean;
defaultInputValue: string;
icon?: string;
color?: string;
onAcceptInput: (value: string) => void;
onCancelInput: VoidFunction;
}
>;
type TSelectorItemType =
| {
email: string;
fileExst?: undefined;
roomType?: undefined;
shared?: undefined;
isOwner: boolean;
isAdmin: boolean;
isVisitor: boolean;
isCollaborator: boolean;
isRoomAdmin: boolean;
access?: ShareAccessRights | string | number;
isFolder?: undefined;
parentId?: undefined;
rootFolderType?: undefined;
filesCount?: undefined;
foldersCount?: undefined;
security?: undefined;
isGroup?: undefined;
name?: undefined;
}
| {
email?: undefined;
fileExst: string;
roomType?: undefined;
shared?: boolean;
isOwner?: undefined;
isAdmin?: undefined;
isVisitor?: undefined;
isCollaborator?: undefined;
isRoomAdmin?: undefined;
access?: undefined;
isFolder?: undefined;
parentId?: string | number;
rootFolderType?: string | number;
filesCount?: undefined;
foldersCount?: undefined;
security?: TFileSecurity;
isGroup?: undefined;
name?: undefined;
}
| {
email?: undefined;
fileExst?: undefined;
roomType: RoomsType;
shared?: boolean;
isOwner?: undefined;
isAdmin?: undefined;
isVisitor?: undefined;
isCollaborator?: undefined;
isRoomAdmin?: undefined;
access?: undefined;
isFolder: boolean;
parentId?: string | number;
rootFolderType?: string | number;
filesCount?: number;
foldersCount?: number;
security?: TRoomSecurity;
isGroup?: undefined;
name?: undefined;
}
| {
email?: undefined;
fileExst?: undefined;
roomType?: undefined;
shared?: boolean;
isOwner?: undefined;
isAdmin?: undefined;
isVisitor?: undefined;
isCollaborator?: undefined;
isRoomAdmin?: undefined;
access?: undefined;
isFolder: boolean;
parentId?: string | number;
rootFolderType?: string | number;
filesCount?: number;
foldersCount?: number;
security?: TFolderSecurity;
isGroup?: undefined;
name?: undefined;
}
| {
email?: undefined;
fileExst?: undefined;
roomType?: undefined;
shared?: boolean;
isOwner?: undefined;
isAdmin?: undefined;
isVisitor?: undefined;
isCollaborator?: undefined;
isRoomAdmin?: undefined;
access?: undefined;
isFolder?: undefined;
parentId?: string | number;
rootFolderType?: string | number;
filesCount?: number;
foldersCount?: number;
security?: TFolderSecurity;
isGroup: true;
name: string;
};
| TSelectorItemUser
| TSelectorItemFile
| TSelectorItemFolder
| TSelectorItemRoom
| TSelectorItemGroup
| TSelectorItemNew
| TSelectorItemInput;
export type TSelectorItem = TSelectorItemLogo &
TSelectorItemType & {
key?: string;
id?: string | number;
label: string;
displayName?: string;
export type TSelectorItem = TSelectorItemType & {
label: string;
isSelected?: boolean;
isDisabled?: boolean;
disabledText?: string;
};
key?: string;
id?: string | number;
displayName?: string;
isSelected?: boolean;
isDisabled?: boolean;
disabledText?: string;
};
export type Data = {
items: TSelectorItem[];
@ -542,6 +544,7 @@ export type Data = {
email?: string,
isGroup?: boolean,
) => React.ReactNode | null;
setInputItemVisible: (value: boolean) => void;
};
export interface ItemProps {
@ -549,12 +552,3 @@ export interface ItemProps {
style: React.CSSProperties;
data: Data;
}
export type TDisplayedItem = {
id: string | number;
label: string;
isArrow: boolean;
isList: boolean;
isRoom?: boolean;
listItems?: TBreadCrumb[];
};

View File

@ -102,13 +102,23 @@ const Body = ({
withInfo,
infoText,
setInputItemVisible,
}: BodyProps) => {
const [bodyHeight, setBodyHeight] = React.useState(0);
const bodyRef = React.useRef<HTMLDivElement>(null);
const listOptionsRef = React.useRef<null | InfiniteLoader>(null);
const itemsCount = hasNextPage ? items.length + 1 : items.length;
const isEmptyInput =
items.length === 2 && items[1].isInputItem && items[0].isCreateNewItem;
const itemsCount = hasNextPage
? items.length + 1
: items.length === 1 && items[0].isCreateNewItem
? 0
: isEmptyInput
? 1
: items.length;
const resetCache = React.useCallback(() => {
if (listOptionsRef && listOptionsRef.current) {
@ -185,9 +195,11 @@ const Body = ({
breadCrumbsLoader
) : (
<BreadCrumbs
withBreadCrumbs
isBreadCrumbsLoading={isLoading}
breadCrumbs={breadCrumbs}
breadCrumbsLoader={breadCrumbsLoader}
onSelectBreadCrumb={onSelectBreadCrumb}
isLoading={isLoading}
/>
)
) : null}
@ -227,23 +239,27 @@ const Body = ({
searchImage={searchEmptyScreenImage}
searchHeader={searchEmptyScreenHeader}
searchDescription={searchEmptyScreenDescription}
items={items}
/>
) : (
<>
{!!descriptionText && (
<Text className="body-description-text">{descriptionText}</Text>
)}
{isMultiSelect && withSelectAll && !isSearch && (
<SelectAll
label={selectAllLabel}
icon={selectAllIcon}
isChecked={isAllChecked || false}
isIndeterminate={isAllIndeterminate || false}
onSelectAll={onSelectAll}
isLoading={isLoading}
rowLoader={rowLoader}
/>
)}
{isMultiSelect && withSelectAll && !isSearch ? (
isLoading ? (
rowLoader
) : (
<SelectAll
withSelectAll
selectAllIcon={selectAllIcon}
selectAllLabel={selectAllLabel}
isAllChecked={isAllChecked}
isAllIndeterminate={isAllIndeterminate}
onSelectAll={onSelectAll}
/>
)
) : null}
{bodyHeight && (
<InfiniteLoader
@ -259,12 +275,13 @@ const Body = ({
width="100%"
itemCount={itemsCount}
itemData={{
items,
items: isEmptyInput ? [items[1]] : items,
onSelect,
isMultiSelect: isMultiSelect || false,
rowLoader,
isItemLoaded,
renderCustomItem,
setInputItemVisible,
}}
itemSize={48}
onItemsRendered={onItemsRendered}

View File

@ -34,8 +34,8 @@ import { ContextMenuModel } from "../../context-menu";
import {
TBreadCrumb,
BreadCrumbsProps,
TDisplayedItem,
TSelectorBreadCrumbs,
} from "../Selector.types";
import {
StyledBreadCrumbs,
@ -45,20 +45,20 @@ import {
const BreadCrumbs = ({
breadCrumbs,
isBreadCrumbsLoading,
onSelectBreadCrumb,
isLoading,
}: BreadCrumbsProps) => {
}: TSelectorBreadCrumbs) => {
const [displayedItems, setDisplayedItems] = React.useState<TDisplayedItem[]>(
[],
);
const onClickItem = React.useCallback(
({ item }: { item: TBreadCrumb }) => {
if (isLoading) return;
if (isBreadCrumbsLoading) return;
onSelectBreadCrumb(item);
onSelectBreadCrumb?.(item);
},
[isLoading, onSelectBreadCrumb],
[isBreadCrumbsLoading, onSelectBreadCrumb],
);
const calculateDisplayedItems = React.useCallback(
@ -214,11 +214,12 @@ const BreadCrumbs = ({
noSelect
truncate
isCurrent={index === displayedItems.length - 1}
isLoading={isLoading || false}
isLoading={isBreadCrumbsLoading}
onClick={() => {
if (index === displayedItems.length - 1 || isLoading) return;
if (index === displayedItems.length - 1 || isBreadCrumbsLoading)
return;
onSelectBreadCrumb({
onSelectBreadCrumb?.({
id: item.id,
label: item.label,
isRoom: item.isRoom,

View File

@ -25,13 +25,27 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { useTranslation } from "react-i18next";
import PlusSvgUrl from "PUBLIC_DIR/images/plus.svg?url";
import UpSvgUrl from "PUBLIC_DIR/images/up.svg?url";
import { Heading } from "../../heading";
import { Text } from "../../text";
import { IconButton } from "../../icon-button";
import { Link, LinkType } from "../../link";
import { StyledEmptyScreen } from "../Selector.styled";
import { EmptyScreenProps } from "../Selector.types";
const linkStyles = {
isHovered: true,
type: LinkType.action,
fontWeight: "600",
className: "empty-folder_link",
display: "flex",
};
const EmptyScreen = ({
image,
header,
@ -40,11 +54,16 @@ const EmptyScreen = ({
searchHeader,
searchDescription,
withSearch,
items,
}: EmptyScreenProps) => {
const { t } = useTranslation(["Common"]);
const currentImage = withSearch ? searchImage : image;
const currentHeader = withSearch ? searchHeader : header;
const currentDescription = withSearch ? searchDescription : description;
const createItem = items.length > 0 ? items[0] : null;
return (
<StyledEmptyScreen withSearch={withSearch}>
<img className="empty-image" src={currentImage} alt="empty-screen" />
@ -56,6 +75,34 @@ const EmptyScreen = ({
<Text className="empty-description" noSelect>
{currentDescription}
</Text>
{createItem && (
<div className="buttons">
<div className="empty-folder_container-links">
<IconButton
className="empty-folder_container-icon"
size={12}
onClick={createItem.onCreateClick}
iconName={PlusSvgUrl}
isFill
/>
<Link {...linkStyles} onClick={createItem.onCreateClick}>
{items[0].label}
</Link>
</div>
<div className="empty-folder_container-links">
<IconButton
className="empty-folder_container-icon"
size={12}
onClick={createItem.onBackClick}
iconName={UpSvgUrl}
isFill
/>
<Link {...linkStyles} onClick={createItem.onBackClick}>
{t("Common:Back")}
</Link>
</div>
</div>
)}
</StyledEmptyScreen>
);
};

View File

@ -0,0 +1,148 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import AcceptIconSvgUrl from "PUBLIC_DIR/images/selector.input.accept.svg?url";
import CancelIconSvgUrl from "PUBLIC_DIR/images/selector.input.cancel.svg?url";
import { InputSize, InputType, TextInput } from "../../text-input";
import { IconButton } from "../../icon-button";
import { RoomIcon } from "../../room-icon";
import { StyledInputWrapper, StyledItem } from "../Selector.styled";
const InputItem = ({
defaultInputValue,
onAcceptInput,
onCancelInput,
style,
color,
icon,
setInputItemVisible,
}: {
defaultInputValue: string;
onAcceptInput: (value: string) => void;
onCancelInput: VoidFunction;
style: React.CSSProperties;
color?: string;
icon?: string;
setInputItemVisible: (value: boolean) => void;
}) => {
const [value, setValue] = React.useState(defaultInputValue);
const requestRunning = React.useRef<boolean>(false);
const inputRef = React.useRef<HTMLInputElement | null>(null);
const onAcceptInputAction = React.useCallback(async () => {
if (requestRunning.current) return;
requestRunning.current = true;
await onAcceptInput(value);
requestRunning.current = false;
}, [onAcceptInput, value]);
React.useEffect(() => {
setInputItemVisible(true);
return () => {
setInputItemVisible(false);
};
}, [setInputItemVisible]);
React.useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") onAcceptInputAction();
else if (e.key === "Escape") onCancelInput();
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [onAcceptInputAction, onCancelInput]);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVal = e.target.value;
setValue(newVal);
};
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, []);
return (
<StyledItem
key="input-item"
isSelected={false}
isMultiSelect={false}
isDisabled={false}
noHover
style={style}
>
{color ? (
<RoomIcon
color={color}
title={value}
showDefault
className="item-logo"
/>
) : icon ? (
<RoomIcon
title={value}
className="item-logo"
imgClassName="room-logo"
imgSrc={icon}
showDefault={false}
/>
) : null}
<TextInput
value={value}
size={InputSize.base}
type={InputType.text}
onChange={onChange}
forwardedRef={inputRef}
/>
<StyledInputWrapper onClick={onAcceptInputAction}>
<IconButton iconName={AcceptIconSvgUrl} size={16} />
</StyledInputWrapper>
<StyledInputWrapper onClick={onCancelInput}>
<IconButton iconName={CancelIconSvgUrl} size={16} />
</StyledInputWrapper>
</StyledItem>
);
};
export default InputItem;

View File

@ -36,6 +36,8 @@ import { RoomIcon } from "../../room-icon";
import { StyledItem } from "../Selector.styled";
import { ItemProps, Data, TSelectorItem } from "../Selector.types";
import { RoomsType } from "../../../enums";
import NewItem from "./NewItem";
import InputItem from "./InputItem";
const compareFunction = (prevProps: ItemProps, nextProps: ItemProps) => {
const prevData = prevProps.data;
@ -64,6 +66,7 @@ const Item = React.memo(({ index, style, data }: ItemProps) => {
isItemLoaded,
rowLoader,
renderCustomItem,
setInputItemVisible,
}: Data = data;
const { t } = useTranslation(["Common"]);
@ -81,6 +84,13 @@ const Item = React.memo(({ index, style, data }: ItemProps) => {
const {
label,
isCreateNewItem,
onCreateClick,
isInputItem,
defaultInputValue,
onAcceptInput,
onCancelInput,
avatar,
icon,
role,
@ -92,6 +102,32 @@ const Item = React.memo(({ index, style, data }: ItemProps) => {
disabledText,
} = item;
if (isInputItem) {
return (
<InputItem
defaultInputValue={defaultInputValue}
onAcceptInput={onAcceptInput}
onCancelInput={onCancelInput}
style={style}
color={color}
icon={icon}
setInputItemVisible={setInputItemVisible}
/>
);
}
if (
isCreateNewItem &&
(items.length > 2 || (items.length === 2 && !items[1].isInputItem))
) {
return (
<NewItem label={label} onCreateClick={onCreateClick} style={style} />
);
}
if (isCreateNewItem) {
return null;
}
const showPlanetIcon =
(item.roomType === RoomsType.PublicRoom ||
item.roomType === RoomsType.CustomRoom) &&
@ -101,7 +137,10 @@ const Item = React.memo(({ index, style, data }: ItemProps) => {
const currentRole = role || AvatarRole.user;
const typeLabel = getUserTypeLabel(role, t);
const typeLabel = getUserTypeLabel(
role as "owner" | "admin" | "user" | "collaborator" | "manager",
t,
);
const onChangeAction = () => {
onSelect?.(item, false);

View File

@ -0,0 +1,68 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { Text } from "../../text";
import { SelectorAddButton } from "../../selector-add-button";
import { StyledItem } from "../Selector.styled";
const NewItem = ({
label,
style,
onCreateClick,
}: {
label: string;
style: React.CSSProperties;
onCreateClick: VoidFunction;
}) => {
return (
<StyledItem
key="create-new-item"
style={style}
isSelected={false}
isMultiSelect={false}
noHover
>
<SelectorAddButton onClick={onCreateClick} isAction />
<Text
className="label label-disabled clicked-label"
fontWeight={600}
fontSize="14px"
noSelect
truncate
dir="auto"
onClick={onCreateClick}
title={label}
>
{label}
</Text>
</StyledItem>
);
};
export default NewItem;

View File

@ -31,18 +31,22 @@ import { Text } from "../../text";
import { Checkbox } from "../../checkbox";
import { StyledSelectAll } from "../Selector.styled";
import { SelectAllProps } from "../Selector.types";
import { TSelectorSelectAll } from "../Selector.types";
const SelectAll = React.memo(
({
label,
icon,
withSelectAll,
selectAllLabel,
selectAllIcon,
isAllChecked,
isAllIndeterminate,
onSelectAll,
isChecked,
isIndeterminate,
isLoading,
rowLoader,
}: SelectAllProps) => {
}: TSelectorSelectAll) => {
if (!withSelectAll) return;
const onClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target instanceof HTMLElement && e.target.closest(".checkbox"))
return;
@ -52,35 +56,28 @@ const SelectAll = React.memo(
return (
<StyledSelectAll onClick={onClick}>
{isLoading ? (
rowLoader
) : (
<>
<Avatar
className="select-all_avatar"
source={icon}
role={AvatarRole.user}
size={AvatarSize.min}
/>
<Avatar
className="select-all_avatar"
source={selectAllIcon}
role={AvatarRole.user}
size={AvatarSize.min}
/>
<Text
className="label"
fontWeight={600}
fontSize="14px"
noSelect
truncate
>
{label}
</Text>
<Text
className="label"
fontWeight={600}
fontSize="14px"
noSelect
truncate
>
{selectAllLabel}
</Text>
<Checkbox
className="checkbox"
isChecked={isChecked}
isIndeterminate={isIndeterminate}
// onChange={onSelectAll}
/>
</>
)}
<Checkbox
className="checkbox"
isChecked={isAllChecked}
isIndeterminate={isAllIndeterminate}
/>
</StyledSelectAll>
);
},

View File

@ -40,13 +40,13 @@ import { ComboBox, ComboBoxSize, TOption } from "../../combobox";
import { IconButton } from "../../icon-button";
import { toastr } from "../../toast";
import { Loader, LoaderTypes } from "../../loader";
import { Text } from "../../text";
import { StyledLinkRow, StyledSquare } from "../Share.styled";
import { getShareOptions, getAccessOptions } from "../Share.helpers";
import { LinkRowProps } from "../Share.types";
import ExpiredComboBox from "./ExpiredComboBox";
import { Text } from "@docspace/shared/components/text";
const LinkRow = ({
onAddClick,
@ -149,7 +149,7 @@ const LinkRow = ({
scaledOptions={false}
showDisabledItems
size={ComboBoxSize.content}
fillIcon={true}
fillIcon
modernView
type="onlyIcon"
isDisabled={isExpiredLink || isLoaded}

View File

@ -58,5 +58,8 @@ export interface SocialButtonProps extends Partial<StyledSocialButtonProps> {
/** Button icon */
iconName?: string;
"data-url"?: string;
"data-providername"?: string;
IconComponent?: JSX.ElementType;
}

View File

@ -50,6 +50,8 @@ const ClearScrollbar = ({
hasError?: boolean;
heightTextAreaProp?: string;
ref?: React.Ref<HTMLDivElement>;
isFullHeight?: boolean;
fullHeight?: number;
// @ts-expect-error error from custom scrollbar
} & ScrollbarProps) => <Scrollbar {...props} />;

View File

@ -62,6 +62,7 @@ export type UseSocketHelperProps = {
disabledItems: (string | number)[];
filterParam?: string;
getIcon: (fileExst: string) => string;
withCreateFolder: boolean;
};
export type UseRoomsHelperProps = {
@ -85,7 +86,7 @@ export type UseRoomsHelperProps = {
export type UseFilesHelpersProps = {
roomsFolderId?: number;
setBreadCrumbs: (items: TBreadCrumb[]) => void;
setBreadCrumbs: React.Dispatch<React.SetStateAction<TBreadCrumb[]>>;
setIsBreadCrumbsLoading: (value: boolean) => void;
setIsSelectedParentFolder: (value: boolean) => void;
setIsNextPageLoading: (value: boolean) => void;
@ -97,7 +98,7 @@ export type UseFilesHelpersProps = {
setIsRoot: (value: boolean) => void;
setIsInit: (value: boolean) => void;
searchValue?: string;
disabledItems: string[] | number[];
disabledItems: (string | number)[];
setSelectedItemSecurity: (value: TFileSecurity | TFolderSecurity) => void;
isThirdParty: boolean;
setSelectedTreeNode: (treeNode: TFolder) => void;
@ -119,6 +120,8 @@ export type UseFilesHelpersProps = {
getFilesArchiveError: (name: string) => string;
isInit: boolean;
setIsFirstLoad: (value: boolean) => void;
withCreateFolder: boolean;
setSelectedItemId: (value: number | string) => void;
};
export type TSelectedFileInfo = {
@ -138,7 +141,7 @@ export type FilesSelectorProps = (
) & {
socketHelper: SocketIOHelper;
socketSubscribers: Set<string>;
disabledItems: string[] | number[];
disabledItems: (string | number)[];
filterParam?: string;
withoutBackButton: boolean;
withBreadCrumbs: boolean;
@ -199,4 +202,6 @@ export type FilesSelectorProps = (
isPanelVisible: boolean;
currentDeviceType: DeviceType;
getFilesArchiveError: (name: string) => string;
withCreateFolder: boolean;
};

View File

@ -25,8 +25,16 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { useTranslation } from "react-i18next";
import { getFolder, getFolderInfo, getSettingsFiles } from "../../../api/files";
import FolderSvgUrl from "PUBLIC_DIR/images/icons/32/folder.svg?url";
import {
createFolder,
getFolder,
getFolderInfo,
getSettingsFiles,
} from "../../../api/files";
import FilesFilter from "../../../api/files/filter";
import {
ApplyFilterOption,
@ -75,7 +83,11 @@ const useFilesHelper = ({
isInit,
setIsInit,
setIsFirstLoad,
withCreateFolder,
setSelectedItemId,
}: UseFilesHelpersProps) => {
const { t } = useTranslation(["Common"]);
const requestRunning = React.useRef(false);
const initRef = React.useRef(isInit);
const firstLoadRef = React.useRef(isFirstLoad);
@ -93,6 +105,65 @@ const useFilesHelper = ({
initRef.current = isInit;
}, [isInit]);
const onCancelInput = React.useCallback(() => {
if (!withCreateFolder) return;
setItems((value) => {
if (!value[1]?.isInputItem && !value[0]?.isInputItem) return value;
let idx = 1;
if (value[0].isInputItem) idx = 0;
const newValue = [...value];
newValue.splice(idx, 1);
return newValue;
});
}, [setItems, withCreateFolder]);
const onAcceptInput = React.useCallback(
async (value: string) => {
if (!withCreateFolder || !selectedItemId) return;
await createFolder(selectedItemId, value);
onCancelInput();
// setBreadCrumbs((val) => {
// return [...val, { id: folder.id, label: folder.title }];
// });
// setSelectedItemId(folder.id);
},
[withCreateFolder, selectedItemId, onCancelInput],
);
const addInputItem = React.useCallback(() => {
if (!withCreateFolder) return;
const inputItem: TSelectorItem = {
label: "",
id: "new-folder-input",
isInputItem: true,
onAcceptInput,
onCancelInput,
defaultInputValue: t("NewFolder"),
icon: FolderSvgUrl,
};
setItems((value) => {
if (value[1]?.isInputItem || value[0]?.isInputItem) return value;
const newValue = [...value];
newValue.splice(1, 0, inputItem);
return newValue;
});
}, [onAcceptInput, onCancelInput, setItems, t, withCreateFolder]);
const getFileList = React.useCallback(
async (startIndex: number) => {
if (requestRunning.current) return;
@ -310,7 +381,28 @@ const useFilesHelper = ({
}
if (firstLoadRef.current || startIndex === 0) {
setTotal(total);
if (withCreateFolder) {
setTotal(total + 1);
itemList.unshift({
isCreateNewItem: true,
label: "New folder",
id: "create-folder-item",
key: "create-folder-item",
onCreateClick: addInputItem,
onBackClick: () => {
setSelectedItemId(current.parentId);
setBreadCrumbs((val) => {
const newVal = [...val];
newVal.pop();
return newVal;
});
},
});
} else {
setTotal(total);
}
setItems(itemList);
} else {
setItems((prevState) => {
@ -361,8 +453,8 @@ const useFilesHelper = ({
setIsNextPageLoading,
searchValue,
filterParam,
isUserOnly,
selectedItemId,
isUserOnly,
getRootData,
setSelectedItemSecurity,
getIcon,
@ -380,8 +472,11 @@ const useFilesHelper = ({
setIsBreadCrumbsLoading,
roomsFolderId,
setIsSelectedParentFolder,
setTotal,
withCreateFolder,
setItems,
setTotal,
addInputItem,
setSelectedItemId,
rootThirdPartyId,
],
);

View File

@ -43,6 +43,7 @@ const useSocketHelper = ({
socketSubscribers,
disabledItems,
filterParam,
withCreateFolder,
setItems,
setBreadCrumbs,
setTotal,
@ -109,9 +110,9 @@ const useSocketHelper = ({
if (opt?.type === "file" && "folderId" in data) {
item = convertFilesToItems([data], getIcon, filterParam)[0];
} else if (opt?.type === "folder" && "roomType" in data) {
} else if (opt?.type === "folder" && !("folderId" in data)) {
item =
data.roomType && "tags" in data
"roomType" in data && data.roomType && "tags" in data
? convertRoomsToItems([data])[0]
: convertFoldersToItems([data], disabledItems, filterParam)[0];
}
@ -122,6 +123,18 @@ const useSocketHelper = ({
if (opt.type === "folder") {
setTotal((v) => v + 1);
if (withCreateFolder) {
const newValue = [...value];
let idx = 1;
if (value[1]?.isInputItem) idx = 2;
newValue.splice(idx, 0, item);
return newValue;
}
return [item, ...value];
}
@ -129,7 +142,12 @@ const useSocketHelper = ({
let idx = 0;
for (let i = 0; i < value.length - 1; i += 1) {
if (!value[i].isFolder) break;
if (
!value[i]?.isFolder &&
!value[i]?.isCreateNewItem &&
!value[i]?.isInputItem
)
break;
idx = i + 1;
}
@ -146,7 +164,7 @@ const useSocketHelper = ({
return value;
});
},
[disabledItems, filterParam, getIcon, setItems, setTotal],
[disabledItems, filterParam, getIcon, setItems, setTotal, withCreateFolder],
);
const updateItem = React.useCallback(

View File

@ -107,6 +107,7 @@ const FilesSelector = ({
withBreadCrumbs: withBreadCrumbsProp,
filesSettings,
cancelButtonLabel,
withCreateFolder,
}: FilesSelectorProps) => {
const theme = useTheme();
const { t } = useTranslation(["Common"]);
@ -148,6 +149,7 @@ const FilesSelector = ({
socketSubscribers,
disabledItems,
filterParam,
withCreateFolder,
getIcon,
setItems,
setBreadCrumbs,
@ -195,6 +197,72 @@ const FilesSelector = ({
setIsFirstLoad,
});
const onClickBreadCrumb = React.useCallback(
(item: TBreadCrumb) => {
if (!isFirstLoad) {
afterSearch.current = false;
setSearchValue("");
setIsFirstLoad(true);
if (+item.id === 0) {
setSelectedItemSecurity(undefined);
setSelectedItemType(undefined);
getRootData();
} else {
setItems([]);
setBreadCrumbs((bc) => {
const idx = bc.findIndex(
(value) => value.id.toString() === item.id.toString(),
);
const maxLength = bc.length - 1;
let foundParentId = false;
let currentFolderIndex = -1;
const newBreadCrumbs = bc.map((i, index) => {
if (!foundParentId) {
currentFolderIndex = disabledItems.findIndex(
(id) => id === i?.id,
);
}
if (index !== maxLength && currentFolderIndex !== -1) {
foundParentId = true;
if (!isSelectedParentFolder) setIsSelectedParentFolder(true);
}
if (
index === maxLength &&
!foundParentId &&
isSelectedParentFolder
)
setIsSelectedParentFolder(false);
return { ...i };
});
newBreadCrumbs.splice(idx + 1, newBreadCrumbs.length - idx - 1);
return newBreadCrumbs;
});
setSelectedItemId(item.id);
if (item.isRoom) {
setSelectedItemType("rooms");
} else {
setSelectedItemType("files");
}
}
}
},
[
disabledItems,
getRootData,
isFirstLoad,
isSelectedParentFolder,
setIsFirstLoad,
],
);
const { getFileList } = useFilesHelper({
setIsBreadCrumbsLoading,
setBreadCrumbs,
@ -224,6 +292,8 @@ const FilesSelector = ({
getFilesArchiveError,
isInit,
setIsInit,
withCreateFolder,
setSelectedItemId,
});
const onSelectAction = React.useCallback(
@ -323,72 +393,6 @@ const FilesSelector = ({
setIsFirstLoad,
]);
const onClickBreadCrumb = React.useCallback(
(item: TBreadCrumb) => {
if (!isFirstLoad) {
afterSearch.current = false;
setSearchValue("");
setIsFirstLoad(true);
if (+item.id === 0) {
setSelectedItemSecurity(undefined);
setSelectedItemType(undefined);
getRootData();
} else {
setItems([]);
setBreadCrumbs((bc) => {
const idx = bc.findIndex(
(value) => value.id.toString() === item.id.toString(),
);
const maxLength = bc.length - 1;
let foundParentId = false;
let currentFolderIndex = -1;
const newBreadCrumbs = bc.map((i, index) => {
if (!foundParentId) {
currentFolderIndex = disabledItems.findIndex(
(id) => id === i?.id,
);
}
if (index !== maxLength && currentFolderIndex !== -1) {
foundParentId = true;
if (!isSelectedParentFolder) setIsSelectedParentFolder(true);
}
if (
index === maxLength &&
!foundParentId &&
isSelectedParentFolder
)
setIsSelectedParentFolder(false);
return { ...i };
});
newBreadCrumbs.splice(idx + 1, newBreadCrumbs.length - idx - 1);
return newBreadCrumbs;
});
setSelectedItemId(item.id);
if (item.isRoom) {
setSelectedItemType("rooms");
} else {
setSelectedItemType("files");
}
}
}
},
[
disabledItems,
getRootData,
isFirstLoad,
isSelectedParentFolder,
setIsFirstLoad,
],
);
const onSearchAction = React.useCallback(
(value: string, callback?: Function) => {
setIsFirstLoad(true);

View File

@ -121,7 +121,6 @@ const GroupsSelector = (props: GroupsSelectorProps) => {
label: group.name,
name: group.name,
isGroup: true,
avatar: "",
}));
if (isFirstLoad.current) {

View File

@ -86,7 +86,7 @@ const toListItem = (
? t("Common:Disabled")
: "";
const i = {
const i: TSelectorItem = {
id: userId,
email,
avatar: userAvatar,
@ -100,7 +100,7 @@ const toListItem = (
hasAvatar,
isDisabled: isInvited || isDisabled,
disabledText,
} as TSelectorItem;
};
return i;
};

View File

@ -42,7 +42,7 @@ export type RoomSelectorProps = TSelectorHeader &
onSubmit: (items: TSelectorItem[]) => void | Promise<void>;
roomType?: RoomsType;
excludeItems?: number[];
excludeItems?: (number | string | undefined)[];
setIsDataReady?: (value: boolean) => void;
submitButtonLabel?: string;
withSearch?: boolean;

View File

@ -25,16 +25,42 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { TRoom } from "../../api/rooms/types";
import { TSelectorItem } from "../../components/selector";
export const convertToItems = (folders: TRoom[]) => {
const items = folders.map((folder) => {
const { id, title, roomType, logo, shared } = folder;
const items: TSelectorItem[] = folders.map((folder) => {
const {
id,
title,
roomType,
logo,
shared,
parentId,
filesCount,
foldersCount,
rootFolderType,
security,
} = folder;
const icon = logo.medium;
const iconOriginal = logo.original;
const color = logo.color;
return { id, label: title, icon, iconOriginal, color, roomType, shared };
return {
id,
label: title,
icon,
iconOriginal,
color,
roomType,
shared,
isFolder: true,
parentId,
filesCount,
foldersCount,
rootFolderType,
security,
};
});
return items;

View File

@ -159,10 +159,10 @@ const RoomSelector = ({
if (isFirstLoad) {
setTotal(totalCount);
setItems([...rooms] as TSelectorItem[]);
setItems([...rooms]);
} else {
setItems((prevItems) => {
const newItems = [...rooms] as TSelectorItem[];
const newItems = [...rooms];
return [...prevItems, ...newItems];
});

View File

@ -2391,10 +2391,17 @@ export const getBaseTheme = () => {
item: {
hoverBackground: grayLight,
selectedBackground: lightHover,
inputButtonBorder: "#D0D5DA",
inputButtonBorderHover: grayMain,
},
emptyScreen: {
descriptionColor: cyanBlueDarkShade,
buttonColor: "#657077",
hoverButtonColor: "#333333",
pressedButtonColor: "#555F65",
},
},
@ -2467,18 +2474,20 @@ export const getBaseTheme = () => {
roomType: {
listItem: {
background: "none",
hoverBackground: "#F8F9F9",
borderColor: "#ECEEF1",
descriptionText: "#A3A9AE",
},
dropdownButton: {
background: "none",
hoverBackground: "#F8F9F9",
borderColor: "#ECEEF1",
isOpenBorderColor: "#2DA7DB",
descriptionText: "#A3A9AE",
},
dropdownItem: {
background: "#ffffff",
hoverBackground: "#f3f4f4",
hoverBackground: "#F8F9F9",
descriptionText: "#A3A9AE",
},
displayItem: {

View File

@ -2370,10 +2370,16 @@ const Dark: TTheme = {
item: {
hoverBackground: "#3d3d3d",
selectedBackground: "#3d3d3d",
inputButtonBorder: "#474747",
inputButtonBorderHover: grayMaxLight,
},
emptyScreen: {
descriptionColor: "#ADADAD",
buttonColor: "#ADADAD",
hoverButtonColor: "#FFFFFF",
pressedButtonColor: "#CCCCCC",
},
},
@ -2446,23 +2452,25 @@ const Dark: TTheme = {
roomType: {
listItem: {
background: "none",
hoverBackground: "#282828",
borderColor: "#474747",
descriptionText: "#A3A9AE",
},
dropdownButton: {
background: "none",
hoverBackground: "#282828",
borderColor: "#474747",
isOpenBorderColor: "#F97A0B",
descriptionText: "#A3A9AE",
},
dropdownItem: {
background: "#333333",
hoverBackground: "#474747",
hoverBackground: "#282828",
descriptionText: "#A3A9AE",
},
displayItem: {
background: "#474747",
borderColor: "#474747",
background: "#282828",
borderColor: "#282828",
descriptionText: "#a3a9ae",
},
},
@ -2492,7 +2500,7 @@ const Dark: TTheme = {
background: "#333333",
borderColor: "#474747",
item: {
hoverBackground: "#474747",
hoverBackground: "#282828",
},
},

View File

@ -55,6 +55,8 @@ export type NonFunctionProperties<T, ExcludeTypes> = Pick<
NonFunctionPropertyNames<T, ExcludeTypes>
>;
export type MergeTypes<T, MergedType> = Omit<T, keyof MergedType> & MergedType;
export type TPathParts = {
id: number;
title: string;

View File

@ -1,10 +1,10 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4501_40546)">
<rect width="32" height="32" rx="6" fill="#F2557C" fill-opacity="0.1"/>
<g clip-path="url(#clip0_15158_11954)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0002 8C16.3903 8 16.7449 8.22689 16.9083 8.58115L18.6488 12.3546L22.7753 12.8438C23.1628 12.8898 23.4881 13.1568 23.6087 13.5279C23.7292 13.8989 23.623 14.3062 23.3366 14.5711L20.2857 17.3924L21.0955 21.4682C21.1716 21.8509 21.0181 22.2428 20.7025 22.4721C20.3869 22.7015 19.9667 22.7263 19.6263 22.5357L16.0002 20.506L12.3741 22.5357C12.0337 22.7263 11.6135 22.7015 11.2979 22.4721C10.9823 22.2428 10.8288 21.8509 10.9049 21.4682L11.7147 17.3924L8.66386 14.5711C8.37743 14.3062 8.27119 13.8989 8.39175 13.5279C8.51231 13.1568 8.83764 12.8898 9.22507 12.8438L13.3516 12.3546L15.0921 8.58115C15.2556 8.22689 15.6101 8 16.0002 8ZM16.0002 11.3875L14.9333 13.7005C14.7876 14.0164 14.4883 14.2338 14.143 14.2747L11.6135 14.5747L13.4836 16.3041C13.7389 16.5402 13.8533 16.8921 13.7855 17.2332L13.2891 19.7316L15.5118 18.4874C15.8152 18.3175 16.1852 18.3175 16.4886 18.4874L18.7114 19.7316L18.2149 17.2332C18.1472 16.8921 18.2615 16.5402 18.5168 16.3041L20.3869 14.5747L17.8574 14.2747C17.5121 14.2338 17.2128 14.0164 17.0671 13.7005L16.0002 11.3875Z" fill="#F2557C"/>
</g>
<path d="M6 2H26V-2H6V2ZM30 6V26H34V6H30ZM26 30H6V34H26V30ZM2 26V6H-2V26H2ZM6 30C3.79086 30 2 28.2091 2 26H-2C-2 30.4183 1.58172 34 6 34V30ZM30 26C30 28.2091 28.2091 30 26 30V34C30.4183 34 34 30.4183 34 26H30ZM26 2C28.2091 2 30 3.79086 30 6H34C34 1.58172 30.4183 -2 26 -2V2ZM6 -2C1.58172 -2 -2 1.58172 -2 6H2C2 3.79086 3.79086 2 6 2V-2Z" fill="#F2557C"/>
<defs>
<clipPath id="clip0_4501_40546">
<clipPath id="clip0_15158_11954">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,10 +1,10 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4529_34510)">
<rect width="32" height="32" rx="6" fill="#EB7B0C" fill-opacity="0.1"/>
<g clip-path="url(#clip0_15158_11944)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 9.29312C19.5119 8.07417 21.4882 8.07417 22.7071 9.29312C23.9261 10.5121 23.9261 12.4884 22.7071 13.7073L13.7071 22.7073C13.579 22.8355 13.4184 22.9264 13.2426 22.9704L9.24256 23.9704C8.90178 24.0556 8.54129 23.9557 8.29291 23.7073C8.04453 23.459 7.94468 23.0985 8.02988 22.7577L9.02988 18.7577C9.07384 18.5819 9.16476 18.4213 9.29291 18.2931L18.2929 9.29312ZM21.2929 10.7073C20.855 10.2694 20.145 10.2694 19.7071 10.7073L18.9142 11.5002L20.5 13.086L21.2929 12.2931C21.7308 11.8552 21.7308 11.1452 21.2929 10.7073ZM19.0858 14.5002L17.5 12.9144L10.903 19.5115L10.3744 21.6259L12.4888 21.0973L19.0858 14.5002Z" fill="#EB7B0C"/>
</g>
<path d="M6 2H26V-2H6V2ZM30 6V26H34V6H30ZM26 30H6V34H26V30ZM2 26V6H-2V26H2ZM6 30C3.79086 30 2 28.2091 2 26H-2C-2 30.4183 1.58172 34 6 34V30ZM30 26C30 28.2091 28.2091 30 26 30V34C30.4183 34 34 30.4183 34 26H30ZM26 2C28.2091 2 30 3.79086 30 6H34C34 1.58172 30.4183 -2 26 -2V2ZM6 -2C1.58172 -2 -2 1.58172 -2 6H2C2 3.79086 3.79086 2 6 2V-2Z" fill="#EB7B0C"/>
<defs>
<clipPath id="clip0_4529_34510">
<clipPath id="clip0_15158_11944">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="#56A8F4" fill-opacity="0.1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 11C9 9.89543 9.89543 9 11 9H21C22.1046 9 23 9.89543 23 11V21C23 22.1046 22.1046 23 21 23H17V21H21V11L11 11V17H9V11ZM15 17H17H20V15H15V17ZM20 14H12V12H20V14ZM20 20H18V18H20V20Z" fill="#56A8F4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4142 19.2929L11.7071 20.5858V16H13.7071V20.5858L15 19.2929L16.4142 20.7071L13.4142 23.7071C13.2267 23.8946 12.9723 24 12.7071 24C12.4419 24 12.1875 23.8946 12 23.7071L9 20.7071L10.4142 19.2929Z" fill="#56A8F4"/>
<rect x="1" y="1" width="30" height="30" rx="5" stroke="#56A8F4" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 698 B

After

Width:  |  Height:  |  Size: 686 B

View File

@ -1,11 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="30" height="30" rx="5" stroke="#22C386" stroke-width="2"/>
<g clip-path="url(#clip0_4152_179192)">
<rect width="32" height="32" rx="6" fill="#22C386" fill-opacity="0.1"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3372 21.6372C9.61662 20.9134 9.04754 20.071 8.63 19.11C8.21 18.1433 8 17.1067 8 16C8 14.8933 8.21 13.8567 8.63 12.89C9.05 11.9233 9.62333 11.0767 10.35 10.35C11.0767 9.62333 11.9267 9.05 12.9 8.63C13.8733 8.21 14.9067 8 16 8C17.1067 8 18.1433 8.21 19.11 8.63C20.0767 9.05 20.9233 9.62333 21.65 10.35C22.3767 11.0767 22.95 11.9233 23.37 12.89C23.79 13.8567 24 14.8933 24 16C24 17.1067 23.79 18.1433 23.37 19.11C22.95 20.0767 22.3767 20.9233 21.65 21.65C20.9233 22.3767 20.0733 22.95 19.1 23.37C18.1267 23.79 17.0933 24 16 24C14.8933 24 13.8567 23.79 12.89 23.37C11.929 22.9525 11.0866 22.3834 10.3628 21.6628C10.3585 21.6585 10.3543 21.6543 10.35 21.65C10.3457 21.6457 10.3415 21.6415 10.3372 21.6372ZM13.29 20.67C13.47 21.1433 13.68 21.6 13.92 22.04C13.8733 22.025 13.827 22.0095 13.781 21.9935C13.0907 21.7537 12.4738 21.4025 11.93 20.94C11.8575 20.8783 11.7865 20.8153 11.717 20.7509C11.2307 20.3003 10.8183 19.7833 10.48 19.2H12.84C12.96 19.7067 13.11 20.1967 13.29 20.67ZM12.43 16.81C12.45 17.07 12.48 17.3333 12.52 17.6H9.8C9.73333 17.3333 9.68333 17.07 9.65 16.81C9.64583 16.7775 9.64193 16.7448 9.63828 16.712C9.61276 16.4823 9.6 16.245 9.6 16C9.6 15.7975 9.60872 15.6002 9.62616 15.4081C9.63466 15.3397 9.64594 15.2703 9.66 15.2H9.64873C9.64915 15.1967 9.64957 15.1933 9.65 15.19C9.68333 14.93 9.73333 14.6667 9.8 14.4H12.52C12.48 14.6667 12.45 14.93 12.43 15.19C12.4297 15.1933 12.4295 15.1967 12.4292 15.2H12.42C12.4067 15.3333 12.4 15.4633 12.4 15.59L12.4 16C12.4 16.28 12.41 16.55 12.43 16.81ZM16.8 21.0595C16.8341 20.9933 16.8674 20.9268 16.9 20.86C17.1533 20.34 17.36 19.7867 17.52 19.2H14.48C14.62 19.7133 14.7957 20.2011 15.0072 20.6634C15.0374 20.7295 15.0683 20.795 15.1 20.86C15.3217 21.315 15.5791 21.7547 15.8722 22.1791C15.9141 22.2397 15.9567 22.3 16 22.36C16 22.36 16 22.36 16 22.36C16.1467 22.1467 16.2867 21.93 16.42 21.71C16.5533 21.49 16.68 21.2667 16.8 21.04V21.0595ZM17.88 17.6C17.92 17.3333 17.95 17.07 17.97 16.81C17.99 16.55 18 16.28 18 16C18 15.72 17.99 15.45 17.97 15.19C17.95 14.93 17.92 14.6667 17.88 14.4H14.12C14.08 14.6667 14.05 14.93 14.03 15.19C14.01 15.45 14 15.72 14 16C14 16.245 14.0077 16.4823 14.023 16.712C14.0252 16.7448 14.0275 16.7775 14.03 16.81C14.05 17.07 14.08 17.3333 14.12 17.6H17.88ZM17.52 12.8C17.36 12.2133 17.1533 11.66 16.9 11.14C16.6467 10.62 16.3467 10.12 16 9.64C15.8533 9.85333 15.7133 10.07 15.58 10.29C15.4467 10.51 15.32 10.7333 15.2 10.96V10.9405C15.1659 11.0067 15.1326 11.0732 15.1 11.14C14.8467 11.66 14.64 12.2133 14.48 12.8H17.52ZM19.5708 16.8C19.5705 16.8033 19.5703 16.8067 19.57 16.81C19.55 17.07 19.52 17.3333 19.48 17.6H22.2C22.2667 17.3333 22.3167 17.07 22.35 16.81C22.3504 16.8067 22.3509 16.8033 22.3513 16.8H22.34C22.3541 16.7297 22.3653 16.6603 22.3738 16.5919C22.3913 16.3998 22.4 16.2025 22.4 16C22.4 15.72 22.3833 15.45 22.35 15.19C22.3167 14.93 22.2667 14.6667 22.2 14.4H19.48C19.515 14.6333 19.5423 14.8641 19.562 15.0923C19.5648 15.1249 19.5675 15.1575 19.57 15.19C19.59 15.45 19.6 15.72 19.6 16V16.41C19.6 16.5367 19.5933 16.6667 19.58 16.8H19.5708ZM12.84 12.8H10.48C10.8667 12.1333 11.35 11.5533 11.93 11.06C12.51 10.5667 13.1733 10.2 13.92 9.96C13.68 10.4 13.47 10.8567 13.29 11.33C13.11 11.8033 12.96 12.2933 12.84 12.8ZM20.07 11.06C20.65 11.5533 21.1333 12.1333 21.52 12.8H19.16C19.04 12.2933 18.89 11.8033 18.71 11.33C18.6987 11.3004 18.6874 11.2709 18.6759 11.2414C18.5036 10.7996 18.305 10.3725 18.08 9.96C18.8267 10.2 19.49 10.5667 20.07 11.06ZM19.16 19.2H21.52C21.1333 19.8667 20.65 20.4467 20.07 20.94C19.49 21.4333 18.8267 21.8 18.08 22.04C18.32 21.6 18.53 21.1433 18.71 20.67C18.89 20.1967 19.04 19.7067 19.16 19.2Z" fill="#22C386"/>
</g>
<defs>
<clipPath id="clip0_4152_179192">
<rect width="16" height="16" fill="white" transform="translate(8 8)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

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.20712 14.2069L15.7071 4.70694L14.2929 3.29272L5.50001 12.0856L1.70712 8.29272L0.292908 9.70694L4.7929 14.2069C5.18343 14.5975 5.81659 14.5975 6.20712 14.2069Z" 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_630_1430)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.41499 8.00016L13.7075 12.2927L12.2933 13.7069L8.00089 9.41449L3.7102 13.7059L2.29587 12.2918L6.58668 8.00028L2.29332 3.70692L3.70754 2.29271L8.00078 6.58595L12.2933 2.29276L13.7076 3.70686L9.41499 8.00016Z" fill="#A3A9AE"/>
</g>
<defs>
<clipPath id="clip0_630_1430">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 523 B