diff --git a/packages/client/src/components/FilesSelector/index.tsx b/packages/client/src/components/FilesSelector/index.tsx index 9e21878dd2..b7ba7cc87e 100644 --- a/packages/client/src/components/FilesSelector/index.tsx +++ b/packages/client/src/components/FilesSelector/index.tsx @@ -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); } }); diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/RoomType.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/RoomType.js index 5701347ad6..c701a6c281 100644 --- a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/RoomType.js +++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/RoomType.js @@ -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" ? ( - + {content} ) : type === "dropdownButton" ? ( @@ -229,6 +261,7 @@ const RoomType = ({ onClick={onClick} isOpen={isOpen} data-selected-id={selectedId} + accentColor={accentColor} > {content} @@ -239,6 +272,7 @@ const RoomType = ({ onClick={onClick} isOpen={isOpen} data-selected-id={selectedId} + currentColorScheme={currentColorScheme} > {content} @@ -247,6 +281,7 @@ const RoomType = ({ id={id} title={t(room.title)} data-selected-id={selectedId} + currentColorScheme={currentColorScheme} > {content} @@ -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)); diff --git a/packages/client/src/pages/Profile/Section/Body/sub-components/active-session/SessionsTable/RowView/SessionsRowContent.js b/packages/client/src/pages/Profile/Section/Body/sub-components/active-session/SessionsTable/RowView/SessionsRowContent.js index dc39f35d65..03652a24e1 100644 --- a/packages/client/src/pages/Profile/Section/Body/sub-components/active-session/SessionsTable/RowView/SessionsRowContent.js +++ b/packages/client/src/pages/Profile/Section/Body/sub-components/active-session/SessionsTable/RowView/SessionsRowContent.js @@ -70,10 +70,9 @@ const SessionsRowContent = ({ )} {convertTime(date)} {(country || city) && ( - + {country} - {country && city && ", "} - {city} + {country && city && ` ${city}`} )} diff --git a/packages/client/src/store/AccountsHotkeysStore.ts b/packages/client/src/store/AccountsHotkeysStore.ts index 3dd09f6551..33cf0b9299 100644 --- a/packages/client/src/store/AccountsHotkeysStore.ts +++ b/packages/client/src/store/AccountsHotkeysStore.ts @@ -255,7 +255,11 @@ class AccountsHotkeysStore { const scroll = document.getElementsByClassName( "section-scroll", ) as HTMLCollectionOf; - 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) { diff --git a/packages/client/src/store/ContextOptionsStore.js b/packages/client/src/store/ContextOptionsStore.js index 17c031f489..fd3bf6a73b 100644 --- a/packages/client/src/store/ContextOptionsStore.js +++ b/packages/client/src/store/ContextOptionsStore.js @@ -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) { diff --git a/packages/client/src/store/HotkeyStore.js b/packages/client/src/store/HotkeyStore.js index 705746e7cd..083f66bc35 100644 --- a/packages/client/src/store/HotkeyStore.js +++ b/packages/client/src/store/HotkeyStore.js @@ -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) { diff --git a/packages/doceditor/src/components/SelectFileDialog.tsx b/packages/doceditor/src/components/SelectFileDialog.tsx index 51005e1893..62ae2d6295 100644 --- a/packages/doceditor/src/components/SelectFileDialog.tsx +++ b/packages/doceditor/src/components/SelectFileDialog.tsx @@ -110,6 +110,7 @@ const SelectFileDialog = ({ submitButtonId="select-file-modal-submit" cancelButtonId="select-file-modal-cancel" {...fileTypeDetection} + withCreateFolder={false} /> ); }; diff --git a/packages/doceditor/src/components/SelectFolderDialog.tsx b/packages/doceditor/src/components/SelectFolderDialog.tsx index 9153e19565..5eca552be6 100644 --- a/packages/doceditor/src/components/SelectFolderDialog.tsx +++ b/packages/doceditor/src/components/SelectFolderDialog.tsx @@ -92,6 +92,7 @@ const SelectFolderDialog = ({ getFilesArchiveError={() => ""} parentId={0} getIsDisabled={getIsDisabled} + withCreateFolder /> ); }; diff --git a/packages/shared/.storybook/globals/theme-wrapper.js b/packages/shared/.storybook/globals/theme-wrapper.js index 168b4e00d4..099b86f2a1 100644 --- a/packages/shared/.storybook/globals/theme-wrapper.js +++ b/packages/shared/.storybook/globals/theme-wrapper.js @@ -2,7 +2,16 @@ import PropTypes from "prop-types"; import { ThemeProvider } from "../../components/theme-provider"; const ThemeWrapper = ({ theme, children }) => { - return {children}; + return ( + + {children} + + ); }; ThemeWrapper.propTypes = { diff --git a/packages/shared/.storybook/main.js b/packages/shared/.storybook/main.js index af58575c23..b7eb5c0482 100644 --- a/packages/shared/.storybook/main.js +++ b/packages/shared/.storybook/main.js @@ -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) { diff --git a/packages/shared/api/files/index.ts b/packages/shared/api/files/index.ts index 8cf2c8efa8..d8374b6ab9 100644 --- a/packages/shared/api/files/index.ts +++ b/packages/shared/api/files/index.ts @@ -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", diff --git a/packages/shared/components/article-item/ArticleItem.types.ts b/packages/shared/components/article-item/ArticleItem.types.ts index 20ec210f17..77f0b848e2 100644 --- a/packages/shared/components/article-item/ArticleItem.types.ts +++ b/packages/shared/components/article-item/ArticleItem.types.ts @@ -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 */ diff --git a/packages/shared/components/campaigns-banner/CampaignsBanner.test.tsx b/packages/shared/components/campaigns-banner/CampaignsBanner.test.tsx index 59cb0ef958..8ecacd0870 100644 --- a/packages/shared/components/campaigns-banner/CampaignsBanner.test.tsx +++ b/packages/shared/components/campaigns-banner/CampaignsBanner.test.tsx @@ -37,6 +37,7 @@ describe("", () => { render( null} diff --git a/packages/shared/components/docspace-logo/DocspaceLogo.tsx b/packages/shared/components/docspace-logo/DocspaceLogo.tsx index f95acf4449..522d56a4c1 100644 --- a/packages/shared/components/docspace-logo/DocspaceLogo.tsx +++ b/packages/shared/components/docspace-logo/DocspaceLogo.tsx @@ -63,7 +63,7 @@ const DocspaceLogo = ({ const logo = getLogoUrl(logoSize, !theme.isBase); return ( - + {logo && ( ` +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 }; diff --git a/packages/shared/components/selector-add-button/SelectorAddButton.tsx b/packages/shared/components/selector-add-button/SelectorAddButton.tsx index 1914c20938..a6a90c4370 100644 --- a/packages/shared/components/selector-add-button/SelectorAddButton.tsx +++ b/packages/shared/components/selector-add-button/SelectorAddButton.tsx @@ -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) => { { 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, }); } diff --git a/packages/shared/components/selector/Selector.styled.ts b/packages/shared/components/selector/Selector.styled.ts index 2b3fa130c2..3b324c17d8 100644 --- a/packages/shared/components/selector/Selector.styled.ts +++ b/packages/shared/components/selector/Selector.styled.ts @@ -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, }; diff --git a/packages/shared/components/selector/Selector.tsx b/packages/shared/components/selector/Selector.tsx index d433386e66..26f1dd1054 100644 --- a/packages/shared/components/selector/Selector.tsx +++ b/packages/shared/components/selector/Selector.tsx @@ -158,6 +158,8 @@ const Selector = ({ return null; }); + const [inputItemVisible, setInputItemVisible] = React.useState(false); + const [requestRunning, setRequestRunning] = React.useState(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 diff --git a/packages/shared/components/selector/Selector.types.ts b/packages/shared/components/selector/Selector.types.ts index 06de7293b6..e7e06269b1 100644 --- a/packages/shared/components/selector/Selector.types.ts +++ b/packages/shared/components/selector/Selector.types.ts @@ -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[]; -}; diff --git a/packages/shared/components/selector/sub-components/Body.tsx b/packages/shared/components/selector/sub-components/Body.tsx index f79d4762b0..273fbcdefa 100644 --- a/packages/shared/components/selector/sub-components/Body.tsx +++ b/packages/shared/components/selector/sub-components/Body.tsx @@ -102,13 +102,23 @@ const Body = ({ withInfo, infoText, + setInputItemVisible, }: BodyProps) => { const [bodyHeight, setBodyHeight] = React.useState(0); const bodyRef = React.useRef(null); const listOptionsRef = React.useRef(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 ) : ( ) ) : null} @@ -227,23 +239,27 @@ const Body = ({ searchImage={searchEmptyScreenImage} searchHeader={searchEmptyScreenHeader} searchDescription={searchEmptyScreenDescription} + items={items} /> ) : ( <> {!!descriptionText && ( {descriptionText} )} - {isMultiSelect && withSelectAll && !isSearch && ( - - )} + {isMultiSelect && withSelectAll && !isSearch ? ( + isLoading ? ( + rowLoader + ) : ( + + ) + ) : null} {bodyHeight && ( { +}: TSelectorBreadCrumbs) => { const [displayedItems, setDisplayedItems] = React.useState( [], ); 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, diff --git a/packages/shared/components/selector/sub-components/EmptyScreen.tsx b/packages/shared/components/selector/sub-components/EmptyScreen.tsx index 116cb579dd..0c73163a7e 100644 --- a/packages/shared/components/selector/sub-components/EmptyScreen.tsx +++ b/packages/shared/components/selector/sub-components/EmptyScreen.tsx @@ -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 ( empty-screen @@ -56,6 +75,34 @@ const EmptyScreen = ({ {currentDescription} + {createItem && ( +
+
+ + + {items[0].label} + +
+
+ + + {t("Common:Back")} + +
+
+ )}
); }; diff --git a/packages/shared/components/selector/sub-components/InputItem.tsx b/packages/shared/components/selector/sub-components/InputItem.tsx new file mode 100644 index 0000000000..5d9f90a1be --- /dev/null +++ b/packages/shared/components/selector/sub-components/InputItem.tsx @@ -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(false); + const inputRef = React.useRef(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) => { + const newVal = e.target.value; + + setValue(newVal); + }; + + React.useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, []); + + return ( + + {color ? ( + + ) : icon ? ( + + ) : null} + + + + + + + + + ); +}; + +export default InputItem; diff --git a/packages/shared/components/selector/sub-components/Item.tsx b/packages/shared/components/selector/sub-components/Item.tsx index bc67babf1f..30a38fba1c 100644 --- a/packages/shared/components/selector/sub-components/Item.tsx +++ b/packages/shared/components/selector/sub-components/Item.tsx @@ -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 ( + + ); + } + + if ( + isCreateNewItem && + (items.length > 2 || (items.length === 2 && !items[1].isInputItem)) + ) { + return ( + + ); + } + 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); diff --git a/packages/shared/components/selector/sub-components/NewItem.tsx b/packages/shared/components/selector/sub-components/NewItem.tsx new file mode 100644 index 0000000000..f4ff2d8f85 --- /dev/null +++ b/packages/shared/components/selector/sub-components/NewItem.tsx @@ -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 ( + + + + {label} + + + ); +}; + +export default NewItem; diff --git a/packages/shared/components/selector/sub-components/SelectAll.tsx b/packages/shared/components/selector/sub-components/SelectAll.tsx index 4d11bf4625..aec954ff31 100644 --- a/packages/shared/components/selector/sub-components/SelectAll.tsx +++ b/packages/shared/components/selector/sub-components/SelectAll.tsx @@ -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) => { if (e.target instanceof HTMLElement && e.target.closest(".checkbox")) return; @@ -52,35 +56,28 @@ const SelectAll = React.memo( return ( - {isLoading ? ( - rowLoader - ) : ( - <> - + - - {label} - + + {selectAllLabel} + - - - )} + ); }, diff --git a/packages/shared/components/share/sub-components/LinkRow.tsx b/packages/shared/components/share/sub-components/LinkRow.tsx index 46931f87c5..6b36ac4b63 100644 --- a/packages/shared/components/share/sub-components/LinkRow.tsx +++ b/packages/shared/components/share/sub-components/LinkRow.tsx @@ -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} diff --git a/packages/shared/components/social-button/SocialButton.types.ts b/packages/shared/components/social-button/SocialButton.types.ts index 43ed035e89..49748c75c3 100644 --- a/packages/shared/components/social-button/SocialButton.types.ts +++ b/packages/shared/components/social-button/SocialButton.types.ts @@ -58,5 +58,8 @@ export interface SocialButtonProps extends Partial { /** Button icon */ iconName?: string; + "data-url"?: string; + "data-providername"?: string; + IconComponent?: JSX.ElementType; } diff --git a/packages/shared/components/textarea/Textarea.styled.tsx b/packages/shared/components/textarea/Textarea.styled.tsx index 2e8896eaff..b20c0b858b 100644 --- a/packages/shared/components/textarea/Textarea.styled.tsx +++ b/packages/shared/components/textarea/Textarea.styled.tsx @@ -50,6 +50,8 @@ const ClearScrollbar = ({ hasError?: boolean; heightTextAreaProp?: string; ref?: React.Ref; + isFullHeight?: boolean; + fullHeight?: number; // @ts-expect-error error from custom scrollbar } & ScrollbarProps) => ; diff --git a/packages/shared/selectors/Files/FilesSelector.types.ts b/packages/shared/selectors/Files/FilesSelector.types.ts index cf5165b351..5b18d9d531 100644 --- a/packages/shared/selectors/Files/FilesSelector.types.ts +++ b/packages/shared/selectors/Files/FilesSelector.types.ts @@ -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>; 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; - 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; }; diff --git a/packages/shared/selectors/Files/hooks/useFilesHelper.ts b/packages/shared/selectors/Files/hooks/useFilesHelper.ts index 4b58368c66..a4c4c4f3e9 100644 --- a/packages/shared/selectors/Files/hooks/useFilesHelper.ts +++ b/packages/shared/selectors/Files/hooks/useFilesHelper.ts @@ -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, ], ); diff --git a/packages/shared/selectors/Files/hooks/useSocketHelper.ts b/packages/shared/selectors/Files/hooks/useSocketHelper.ts index 9439f064b0..c214eba473 100644 --- a/packages/shared/selectors/Files/hooks/useSocketHelper.ts +++ b/packages/shared/selectors/Files/hooks/useSocketHelper.ts @@ -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( diff --git a/packages/shared/selectors/Files/index.tsx b/packages/shared/selectors/Files/index.tsx index 713334d467..c301cf5fd3 100644 --- a/packages/shared/selectors/Files/index.tsx +++ b/packages/shared/selectors/Files/index.tsx @@ -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); diff --git a/packages/shared/selectors/Groups/index.tsx b/packages/shared/selectors/Groups/index.tsx index 8e29ec8bac..d0795434e9 100644 --- a/packages/shared/selectors/Groups/index.tsx +++ b/packages/shared/selectors/Groups/index.tsx @@ -121,7 +121,6 @@ const GroupsSelector = (props: GroupsSelectorProps) => { label: group.name, name: group.name, isGroup: true, - avatar: "", })); if (isFirstLoad.current) { diff --git a/packages/shared/selectors/People/index.tsx b/packages/shared/selectors/People/index.tsx index be6b82f925..5dd7eda4f5 100644 --- a/packages/shared/selectors/People/index.tsx +++ b/packages/shared/selectors/People/index.tsx @@ -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; }; diff --git a/packages/shared/selectors/Room/RoomSelector.types.ts b/packages/shared/selectors/Room/RoomSelector.types.ts index f37de4d20c..bfa38caf8f 100644 --- a/packages/shared/selectors/Room/RoomSelector.types.ts +++ b/packages/shared/selectors/Room/RoomSelector.types.ts @@ -42,7 +42,7 @@ export type RoomSelectorProps = TSelectorHeader & onSubmit: (items: TSelectorItem[]) => void | Promise; roomType?: RoomsType; - excludeItems?: number[]; + excludeItems?: (number | string | undefined)[]; setIsDataReady?: (value: boolean) => void; submitButtonLabel?: string; withSearch?: boolean; diff --git a/packages/shared/selectors/Room/RoomSelector.utils.ts b/packages/shared/selectors/Room/RoomSelector.utils.ts index 2e039dbd86..8cf861784a 100644 --- a/packages/shared/selectors/Room/RoomSelector.utils.ts +++ b/packages/shared/selectors/Room/RoomSelector.utils.ts @@ -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; diff --git a/packages/shared/selectors/Room/index.tsx b/packages/shared/selectors/Room/index.tsx index 7b4f95cd97..aa73fc615c 100644 --- a/packages/shared/selectors/Room/index.tsx +++ b/packages/shared/selectors/Room/index.tsx @@ -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]; }); diff --git a/packages/shared/themes/base.ts b/packages/shared/themes/base.ts index c999e45c33..b49e6a09f4 100644 --- a/packages/shared/themes/base.ts +++ b/packages/shared/themes/base.ts @@ -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: { diff --git a/packages/shared/themes/dark.ts b/packages/shared/themes/dark.ts index 363891c81f..516b78f09e 100644 --- a/packages/shared/themes/dark.ts +++ b/packages/shared/themes/dark.ts @@ -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", }, }, diff --git a/packages/shared/types/index.ts b/packages/shared/types/index.ts index 2f91402d9c..1d1c62f894 100644 --- a/packages/shared/types/index.ts +++ b/packages/shared/types/index.ts @@ -55,6 +55,8 @@ export type NonFunctionProperties = Pick< NonFunctionPropertyNames >; +export type MergeTypes = Omit & MergedType; + export type TPathParts = { id: number; title: string; diff --git a/public/images/icons/32/room/custom.svg b/public/images/icons/32/room/custom.svg index 570fc932d0..3033654a3d 100644 --- a/public/images/icons/32/room/custom.svg +++ b/public/images/icons/32/room/custom.svg @@ -1,10 +1,10 @@ - + + - - + diff --git a/public/images/icons/32/room/editing.svg b/public/images/icons/32/room/editing.svg index ca6b8d9045..a01fae0d38 100644 --- a/public/images/icons/32/room/editing.svg +++ b/public/images/icons/32/room/editing.svg @@ -1,10 +1,10 @@ - + + - - + diff --git a/public/images/icons/32/room/form.svg b/public/images/icons/32/room/form.svg index 05dda31953..d036241507 100644 --- a/public/images/icons/32/room/form.svg +++ b/public/images/icons/32/room/form.svg @@ -1,5 +1,5 @@ + - diff --git a/public/images/icons/32/room/public.svg b/public/images/icons/32/room/public.svg index c69a5473f2..d61c035682 100644 --- a/public/images/icons/32/room/public.svg +++ b/public/images/icons/32/room/public.svg @@ -1,11 +1,4 @@ - - + - - - - - - diff --git a/public/images/selector.input.accept.svg b/public/images/selector.input.accept.svg new file mode 100644 index 0000000000..3d5add8ba3 --- /dev/null +++ b/public/images/selector.input.accept.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/selector.input.cancel.svg b/public/images/selector.input.cancel.svg new file mode 100644 index 0000000000..1c0db52fe6 --- /dev/null +++ b/public/images/selector.input.cancel.svg @@ -0,0 +1,10 @@ + + + + + + + + + +