Merge branch 'develop' into feature/oauth2-client

This commit is contained in:
Timofey Boyko 2024-05-28 14:00:07 +03:00
commit d78e24f4bf
49 changed files with 1044 additions and 365 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

@ -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",
},
},

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",
},
},

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

@ -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