diff --git a/cypress/e2e/item/copy/gridCopyItem.cy.ts b/cypress/e2e/item/copy/gridCopyItem.cy.ts index 967f6f478..59b4fc29d 100644 --- a/cypress/e2e/item/copy/gridCopyItem.cy.ts +++ b/cypress/e2e/item/copy/gridCopyItem.cy.ts @@ -1,7 +1,7 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { - HOME_MODAL_ITEM_ID, ITEM_MENU_COPY_BUTTON_CLASS, + MY_GRAASP_ITEM_PATH, buildItemCard, buildItemMenu, buildItemMenuButtonId, @@ -9,11 +9,19 @@ import { import { ITEM_LAYOUT_MODES } from '../../../../src/enums'; import { SAMPLE_ITEMS } from '../../../fixtures/items'; -const copyItem = ({ id, toItemPath }: { id: string; toItemPath: string }) => { +const copyItem = ({ + id, + toItemPath, + rootId, +}: { + id: string; + toItemPath: string; + rootId?: string; +}) => { const menuSelector = `#${buildItemMenuButtonId(id)}`; cy.get(menuSelector).click(); cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_COPY_BUTTON_CLASS}`).click(); - cy.fillTreeModal(toItemPath); + cy.handleTreeMenu(toItemPath, rootId); }; describe('Copy Item in Grid', () => { @@ -63,7 +71,7 @@ describe('Copy Item in Grid', () => { // copy const { id: copyItemId } = SAMPLE_ITEMS.items[2]; - const toItemPath = HOME_MODAL_ITEM_ID; + const toItemPath = MY_GRAASP_ITEM_PATH; copyItem({ id: copyItemId, toItemPath }); cy.wait('@copyItems').then(({ request: { url } }) => { diff --git a/cypress/e2e/item/copy/listCopyItem.cy.ts b/cypress/e2e/item/copy/listCopyItem.cy.ts index 023e4998f..aaefe8e10 100644 --- a/cypress/e2e/item/copy/listCopyItem.cy.ts +++ b/cypress/e2e/item/copy/listCopyItem.cy.ts @@ -1,19 +1,13 @@ +import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { - HOME_PATH, - SHARED_ITEMS_PATH, - buildItemPath, -} from '../../../../src/config/paths'; -import { - HOME_MODAL_ITEM_ID, ITEM_MENU_COPY_BUTTON_CLASS, - TREE_MODAL_SHARED_ITEMS_ID, + MY_GRAASP_ITEM_PATH, buildItemMenu, buildItemMenuButtonId, buildItemsTableRowIdAttribute, } from '../../../../src/config/selectors'; import ITEM_LAYOUT_MODES from '../../../../src/enums/itemLayoutModes'; import { SAMPLE_ITEMS } from '../../../fixtures/items'; -import { SHARED_ITEMS } from '../../../fixtures/sharedItems'; const copyItem = ({ id, @@ -26,7 +20,7 @@ const copyItem = ({ }) => { cy.get(`#${buildItemMenuButtonId(id)}`).click(); cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_COPY_BUTTON_CLASS}`).click(); - cy.fillTreeModal(toItemPath, rootId); + cy.handleTreeMenu(toItemPath, rootId); }; describe('Copy Item in List', () => { @@ -76,7 +70,7 @@ describe('Copy Item in List', () => { // copy const { id: copyItemId } = SAMPLE_ITEMS.items[2]; - copyItem({ id: copyItemId, toItemPath: HOME_MODAL_ITEM_ID }); + copyItem({ id: copyItemId, toItemPath: MY_GRAASP_ITEM_PATH }); cy.wait('@copyItems').then(({ request: { url } }) => { expect(url).to.contain(copyItemId); @@ -84,25 +78,4 @@ describe('Copy Item in List', () => { cy.get(buildItemsTableRowIdAttribute(copyItemId)).should('exist'); }); }); - - it('copy item in a shared item', () => { - cy.setUpApi(SHARED_ITEMS); - const { path } = SHARED_ITEMS.items[0]; - - // go to children item - cy.visit(SHARED_ITEMS_PATH); - cy.switchMode(ITEM_LAYOUT_MODES.LIST); - - // copy - const { id: copyItemId } = SHARED_ITEMS.items[1]; - copyItem({ - id: copyItemId, - toItemPath: path, - rootId: TREE_MODAL_SHARED_ITEMS_ID, - }); - - cy.wait('@copyItems').then(() => { - cy.get(buildItemsTableRowIdAttribute(copyItemId)).should('exist'); - }); - }); }); diff --git a/cypress/e2e/item/copy/listCopyMultiple.cy.ts b/cypress/e2e/item/copy/listCopyMultiple.cy.ts index ee6e14f8d..a8d7f742c 100644 --- a/cypress/e2e/item/copy/listCopyMultiple.cy.ts +++ b/cypress/e2e/item/copy/listCopyMultiple.cy.ts @@ -1,7 +1,7 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { - HOME_MODAL_ITEM_ID, ITEMS_TABLE_COPY_SELECTED_ITEMS_ID, + MY_GRAASP_ITEM_PATH, buildItemsTableRowIdAttribute, } from '../../../../src/config/selectors'; import ITEM_LAYOUT_MODES from '../../../../src/enums/itemLayoutModes'; @@ -10,9 +10,11 @@ import { SAMPLE_ITEMS } from '../../../fixtures/items'; const copyItems = ({ itemIds, toItemPath, + rootId, }: { itemIds: string[]; toItemPath: string; + rootId?: string; }) => { // check selected ids itemIds.forEach((id) => { @@ -20,7 +22,7 @@ const copyItems = ({ }); cy.get(`#${ITEMS_TABLE_COPY_SELECTED_ITEMS_ID}`).click(); - cy.fillTreeModal(toItemPath); + cy.handleTreeMenu(toItemPath, rootId); }; describe('Copy items in List', () => { @@ -74,7 +76,7 @@ describe('Copy items in List', () => { // copy const itemIds = [SAMPLE_ITEMS.items[2].id, SAMPLE_ITEMS.items[4].id]; - copyItems({ itemIds, toItemPath: HOME_MODAL_ITEM_ID }); + copyItems({ itemIds, toItemPath: MY_GRAASP_ITEM_PATH }); cy.wait('@copyItems').then(({ request: { url } }) => { itemIds.forEach((id) => { diff --git a/cypress/e2e/item/create/createShortcut.cy.ts b/cypress/e2e/item/create/createShortcut.cy.ts index e2212f1f0..82bc59152 100644 --- a/cypress/e2e/item/create/createShortcut.cy.ts +++ b/cypress/e2e/item/create/createShortcut.cy.ts @@ -5,6 +5,7 @@ import * as qs from 'qs'; import { HOME_PATH } from '../../../../src/config/paths'; import { ITEM_MENU_SHORTCUT_BUTTON_CLASS, + MY_GRAASP_ITEM_PATH, buildItemMenu, buildItemMenuButtonId, } from '../../../../src/config/selectors'; @@ -16,12 +17,14 @@ import { SAMPLE_ITEMS } from '../../../fixtures/items'; const createShortcut = ({ id, toItemPath, + rootId, }: { id: string; toItemPath: string; + rootId?: string; }) => { cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_SHORTCUT_BUTTON_CLASS}`).click(); - cy.fillTreeModal(toItemPath); + cy.handleTreeMenu(toItemPath, rootId); }; const createShortcutInGrid = ({ @@ -39,13 +42,15 @@ const createShortcutInGrid = ({ const createShortcutInList = ({ id, toItemPath, + rootId, }: { id: string; toItemPath?: string; + rootId?: string; }) => { const menuSelector = `#${buildItemMenuButtonId(id)}`; cy.get(menuSelector).click(); - createShortcut({ id, toItemPath }); + createShortcut({ id, toItemPath, rootId }); }; const checkCreateShortcutRequest = ({ @@ -77,8 +82,7 @@ describe('Create Shortcut', () => { cy.visit(HOME_PATH); const { id } = SAMPLE_ITEMS.items[0]; - // toItemId: TREE_MODAL_MY_ITEMS_ID - createShortcutInList({ id }); + createShortcutInList({ id, toItemPath: MY_GRAASP_ITEM_PATH }); checkCreateShortcutRequest({ id }); }); @@ -123,8 +127,7 @@ describe('Create Shortcut', () => { cy.switchMode(ITEM_LAYOUT_MODES.GRID); const { id } = SAMPLE_ITEMS.items[0]; - // toItemId: TREE_MODAL_MY_ITEMS_ID - createShortcutInGrid({ id }); + createShortcutInGrid({ id, toItemPath: MY_GRAASP_ITEM_PATH }); checkCreateShortcutRequest({ id }); }); diff --git a/cypress/e2e/item/move/gridMoveItem.cy.ts b/cypress/e2e/item/move/gridMoveItem.cy.ts index d8216818a..def6037d2 100644 --- a/cypress/e2e/item/move/gridMoveItem.cy.ts +++ b/cypress/e2e/item/move/gridMoveItem.cy.ts @@ -1,6 +1,7 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEM_MENU_MOVE_BUTTON_CLASS, + MY_GRAASP_ITEM_PATH, buildItemMenu, buildItemMenuButtonId, } from '../../../../src/config/selectors'; @@ -69,7 +70,7 @@ describe('Move Item in Grid', () => { // move const { id: movedItem } = SAMPLE_ITEMS.items[2]; - moveItem({ id: movedItem, toItemPath: 'selectionModalMyGraasp' }); + moveItem({ id: movedItem, toItemPath: MY_GRAASP_ITEM_PATH }); cy.wait('@moveItems').then(({ request: { body, url } }) => { expect(body.parentId).to.equal(undefined); diff --git a/cypress/e2e/item/move/listMoveItem.cy.ts b/cypress/e2e/item/move/listMoveItem.cy.ts index 7c9198613..3e793c059 100644 --- a/cypress/e2e/item/move/listMoveItem.cy.ts +++ b/cypress/e2e/item/move/listMoveItem.cy.ts @@ -1,6 +1,7 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEM_MENU_MOVE_BUTTON_CLASS, + MY_GRAASP_ITEM_PATH, buildItemMenu, buildItemMenuButtonId, buildItemRowArrowId, @@ -112,7 +113,7 @@ describe('Move Item in List', () => { // move const { id: movedItem } = SAMPLE_ITEMS.items[2]; - moveItem({ id: movedItem, toItemPath: 'selectionModalMyGraasp' }); + moveItem({ id: movedItem, toItemPath: MY_GRAASP_ITEM_PATH }); cy.wait('@moveItems').then(({ request: { body, url } }) => { expect(body.parentId).to.equal(undefined); diff --git a/cypress/e2e/item/move/listMoveMultiple.cy.ts b/cypress/e2e/item/move/listMoveMultiple.cy.ts index 1d96e6f21..f9bfa4654 100644 --- a/cypress/e2e/item/move/listMoveMultiple.cy.ts +++ b/cypress/e2e/item/move/listMoveMultiple.cy.ts @@ -1,6 +1,7 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEMS_TABLE_MOVE_SELECTED_ITEMS_ID, + MY_GRAASP_ITEM_PATH, buildItemsTableRowIdAttribute, } from '../../../../src/config/selectors'; import { ITEM_LAYOUT_MODES } from '../../../../src/enums'; @@ -71,7 +72,7 @@ describe('Move Items in List', () => { // move const itemIds = [SAMPLE_ITEMS.items[2].id, SAMPLE_ITEMS.items[4].id]; - moveItems({ itemIds, toItemPath: 'selectionModalMyGraasp' }); + moveItems({ itemIds, toItemPath: MY_GRAASP_ITEM_PATH }); cy.wait('@moveItems').then(({ request: { body, url } }) => { expect(body.parentId).to.equal(undefined); diff --git a/cypress/support/commands/item.ts b/cypress/support/commands/item.ts index 5ef6219a9..61c76010f 100644 --- a/cypress/support/commands/item.ts +++ b/cypress/support/commands/item.ts @@ -81,45 +81,6 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add( - 'fillTreeModal', - (toItemPath, treeRootId = HOME_MODAL_ITEM_ID) => { - const ids = getParentsIdsFromPath(toItemPath); - - cy.wait(TREE_VIEW_PAUSE); - - [treeRootId, ...ids].forEach((value, idx, array) => { - cy.get(`#${treeRootId}`).then(($tree) => { - // click on the element - if (idx === array.length - 1) { - cy.wrap($tree) - .get( - `#${buildTreeItemId(value, treeRootId)} .MuiTreeItem-label input`, - ) - .first() - .click(); - } - // if can't find children click on parent (current value) - if ( - idx !== array.length - 1 && - !$tree.find( - `#${buildTreeItemId( - array[idx + 1], - treeRootId, - )} .MuiTreeItem-label`, - ).length - ) { - cy.wrap($tree) - .get(`#${buildTreeItemId(value, treeRootId)} .MuiTreeItem-label`) - .first() - .click(); - } - }); - }); - - cy.get(`#${TREE_MODAL_CONFIRM_BUTTON_ID}`).click(); - }, -); Cypress.Commands.add( 'fillBaseItemModal', ({ name = '' }, { confirm = true } = {}) => { diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 078589a39..b5223cdd9 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -44,7 +44,6 @@ declare global { wsClientStub: any, ): void; - fillTreeModal(path: string, rootId?: string): void; handleTreeMenu(path: string, rootId?: string): void; switchMode(mode: string): void; goToItemInGrid(path: string): void; diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 04e9fd61f..fc6b7e00a 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -22,6 +22,7 @@ import { import App from './App'; import FallbackComponent from './Fallback'; import { CurrentUserContextProvider } from './context/CurrentUserContext'; +import { FilterItemsContextProvider } from './context/FilterItemsContext'; import ModalProviders from './context/ModalProviders'; const Root = (): JSX.Element => ( @@ -34,7 +35,9 @@ const Root = (): JSX.Element => ( - + + + diff --git a/src/components/common/MoveButton.tsx b/src/components/common/MoveButton.tsx index aed3493e2..e5ff53247 100644 --- a/src/components/common/MoveButton.tsx +++ b/src/components/common/MoveButton.tsx @@ -1,15 +1,17 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { IconButtonProps } from '@mui/material/IconButton'; +import { DiscriminatedItem } from '@graasp/sdk'; import { ActionButton, ActionButtonVariant, MoveButton as GraaspMoveButton, } from '@graasp/ui'; -import { hooks, mutations } from '@/config/queryClient'; -import { applyEllipsisOnLength, getDirectParentId } from '@/utils/item'; +import { mutations } from '@/config/queryClient'; +import { getDirectParentId } from '@/utils/item'; +import { computeButtonText } from '@/utils/itemSelection'; import { useBuilderTranslation } from '../../config/i18n'; import { @@ -22,8 +24,6 @@ import ItemSelectionModal, { ItemSelectionModalProps, } from '../main/itemSelectionModal/ItemSelectionModal'; -const TITLE_MAX_NAME_LENGTH = 15; - type MoveButtonProps = { itemIds: string[]; color?: IconButtonProps['color']; @@ -33,7 +33,7 @@ type MoveButtonProps = { }; const MoveButton = ({ - itemIds: defaultItemsIds, + itemIds, color = 'default', id, type = ActionButton.ICON_BUTTON, @@ -43,78 +43,59 @@ const MoveButton = ({ const { mutate: moveItems } = mutations.useMoveItems(); const [open, setOpen] = useState(false); - const [itemIds, setItemIds] = useState(defaultItemsIds || []); - - const { data: items } = hooks.useItems(itemIds); - const openMoveModal = (newItemIds: string[]) => { + const openMoveModal = () => { setOpen(true); - setItemIds(newItemIds); }; const onClose = () => { setOpen(false); }; - const onConfirm: ItemSelectionModalProps['onConfirm'] = (payload) => { - // change item's root id to null - const newPayload = { + const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { + moveItems({ ids: itemIds, - to: payload, - }; - moveItems(newPayload); + to: destination, + }); onClose(); }; - useEffect(() => { - // necessary to sync prop with a state because move-many-items' targets are updated dynamically with the table - setItemIds(defaultItemsIds); - }, [defaultItemsIds]); const handleMove = () => { - openMoveModal(itemIds); + openMoveModal(); onClick?.(); }; - const isDisabled = (item: NavigationElement, homeId: string) => { - if (items?.data) { + const isDisabled = ( + items: DiscriminatedItem[], + item: NavigationElement, + homeId: string, + ) => { + if (items) { // cannot move inside self and below - const moveInSelf = Object.values(items.data).some((i) => - item.path.includes(i.path), - ); + const moveInSelf = items.some((i) => item.path.includes(i.path)); // cannot move in same direct parent // todo: not opti because we only have the ids from the table - const directParentIds = Object.values(items.data).map((i) => - getDirectParentId(i.path), - ); + const directParentIds = items.map((i) => getDirectParentId(i.path)); const moveInDirectParent = directParentIds.includes(item.id); // cannot move to home if was already on home let moveToHome = false; - if (items?.data) { - moveToHome = - item.id === homeId && - !getDirectParentId(Object.values(items.data)[0].path); + if (items) { + moveToHome = item.id === homeId && !getDirectParentId(items[0].path); } return moveInSelf || moveInDirectParent || moveToHome; } return false; }; - const title = items - ? translateBuilder(BUILDER.MOVE_ITEM_MODAL_TITLE, { - name: applyEllipsisOnLength( - Object.values(items.data)[0].name, - TITLE_MAX_NAME_LENGTH, - ), - // -1 because we show one name - count: itemIds.length - 1, - }) - : translateBuilder(BUILDER.MOVE_ITEM_MODAL_TITLE); - const buttonText = (name?: string) => - translateBuilder(BUILDER.MOVE_BUTTON, { name, count: name ? 1 : 0 }); + computeButtonText({ + translateBuilder, + translateKey: BUILDER.MOVE_BUTTON, + name, + }); return ( <> @@ -127,16 +108,15 @@ const MoveButton = ({ menuItemClassName={ITEM_MENU_MOVE_BUTTON_CLASS} iconClassName={ITEM_MOVE_BUTTON_CLASS} /> - - {items?.data && open && ( + {itemIds && open && ( )} diff --git a/src/components/common/SelectTypes.tsx b/src/components/common/SelectTypes.tsx new file mode 100644 index 000000000..19c2ee9c3 --- /dev/null +++ b/src/components/common/SelectTypes.tsx @@ -0,0 +1,89 @@ +import { + Checkbox, + FormControl, + InputLabel, + ListItemText, + MenuItem, + OutlinedInput, + Select, + SelectChangeEvent, + Stack, +} from '@mui/material'; + +import { ItemType } from '@graasp/sdk'; +import { ItemIcon } from '@graasp/ui'; + +import { useBuilderTranslation, useEnumsTranslation } from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; + +import { useFilterItemsContext } from '../context/FilterItemsContext'; + +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + }, + }, +}; + +// Exclude LOCAL_FILE because it is also file like for S3_FILE but stored in another location. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { LOCAL_FILE, ...BUILDER_ITEM_TYPES } = ItemType; +const LABEL_ID = 'select-types-filter-label'; + +export const SelectTypes = (): JSX.Element => { + const { itemTypes, setItemTypes } = useFilterItemsContext(); + const { t: translateEnums } = useEnumsTranslation(); + const { t: translateBuilder } = useBuilderTranslation(); + + const types = Object.values(BUILDER_ITEM_TYPES).sort((t1, t2) => + translateEnums(t1).localeCompare(translateEnums(t2)), + ); + + const handleChange = (event: SelectChangeEvent) => { + const { + target: { value }, + } = event; + setItemTypes(value as typeof itemTypes); + }; + + const label = translateBuilder(BUILDER.FILTER_BY_TYPES_LABEL); + + const renderValues = (value: typeof itemTypes) => ( + + {value.map((type) => ( + + ))} + + ); + + return ( + + {label} + + + ); +}; + +export default SelectTypes; diff --git a/src/components/context/CopyItemModalContext.tsx b/src/components/context/CopyItemModalContext.tsx deleted file mode 100644 index 29cce7e84..000000000 --- a/src/components/context/CopyItemModalContext.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { createContext, useContext, useMemo, useState } from 'react'; - -import { validate } from 'uuid'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { mutations } from '../../config/queryClient'; -import { BUILDER } from '../../langs/constants'; -import TreeModal, { TreeModalProps } from '../main/TreeModal'; - -type CopyItemModalContextType = { - openModal: (newItemIds: string[]) => void; -}; - -const CopyItemModalContext = createContext({ - openModal: () => null, -}); - -type Props = { - children: JSX.Element; -}; - -export const CopyItemModalProvider = ({ children }: Props): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { mutate: copyItems } = mutations.useCopyItems(); - const [open, setOpen] = useState(false); - const [itemIds, setItemIds] = useState([]); - - const openModal = (newItemIds: string[]) => { - setOpen(true); - setItemIds(newItemIds); - }; - - const onClose = () => { - setOpen(false); - setItemIds([]); - }; - - const onConfirm: TreeModalProps['onConfirm'] = (payload) => { - // change item's root id to null - const newPayload = { - ...payload, - to: payload.to && validate(payload.to) ? payload.to : undefined, - }; - copyItems(newPayload); - onClose(); - }; - - const renderModal = () => { - if (!itemIds.length) { - return null; - } - - return ( - - ); - }; - - const value = useMemo(() => ({ openModal }), []); - - return ( - - {renderModal()} - {children} - - ); -}; - -export const useCopyItemModalContext = (): CopyItemModalContextType => - useContext(CopyItemModalContext); diff --git a/src/components/context/CreateShortcutModalContext.tsx b/src/components/context/CreateShortcutModalContext.tsx deleted file mode 100644 index 97e29a090..000000000 --- a/src/components/context/CreateShortcutModalContext.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { createContext, useMemo, useState } from 'react'; - -import { - DiscriminatedItem, - Item, - ItemType, - ShortcutItemType, -} from '@graasp/sdk'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { mutations } from '../../config/queryClient'; -import { HOME_MODAL_ITEM_ID } from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import { buildShortcutExtra } from '../../utils/itemExtra'; -import TreeModal, { TreeModalProps } from '../main/TreeModal'; - -const CreateShortcutModalContext = createContext({ - openModal: (_newItem: Item) => { - // do nothing - }, -}); - -type Props = { - children: JSX.Element | JSX.Element[]; -}; - -const CreateShortcutModalProvider = ({ children }: Props): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { mutate: createShortcut } = mutations.usePostItem(); - const [open, setOpen] = useState(false); - const [item, setItem] = useState(null); - - const openModal = (newItem: Item) => { - setOpen(true); - setItem(newItem); - }; - - const onClose = () => { - setOpen(false); - setItem(null); - }; - - const onConfirm: TreeModalProps['onConfirm'] = ({ ids: [target], to }) => { - const shortcut: Partial & - Pick & { - parentId?: string; - } = { - name: translateBuilder(BUILDER.CREATE_SHORTCUT_DEFAULT_NAME, { - name: item?.name, - }), - extra: buildShortcutExtra(target), - type: ItemType.SHORTCUT, - // set parent id if not root - parentId: to !== HOME_MODAL_ITEM_ID ? to : undefined, - }; - createShortcut(shortcut); - - onClose(); - }; - - const renderModal = () => { - if (!item) { - return null; - } - - return ( - - ); - }; - - const value = useMemo(() => ({ openModal }), []); - - return ( - - {renderModal()} - {children} - - ); -}; - -export { CreateShortcutModalProvider, CreateShortcutModalContext }; diff --git a/src/components/context/FilterItemsContext.tsx b/src/components/context/FilterItemsContext.tsx new file mode 100644 index 000000000..06b13c971 --- /dev/null +++ b/src/components/context/FilterItemsContext.tsx @@ -0,0 +1,62 @@ +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +type ItemTypeConst = DiscriminatedItem['type']; +type ItemTypes = ItemTypeConst[]; + +type FilterItemsContextType = { + itemTypes: ItemTypes; + setItemTypes: ( + newTypes: ItemTypes | ((types: ItemTypes) => ItemTypes), + ) => void; + shouldDisplayItem: (itemType: ItemTypeConst) => boolean; +}; + +const FilterItemsContext = createContext({ + itemTypes: [], + setItemTypes: () => { + console.error( + 'No Provider found for "FilterItemsContext". Check that this Provider is accessible.', + ); + }, + shouldDisplayItem: (_itemType): boolean => { + console.error( + 'No Provider found for "FilterItemsContext". Check that this Provider is accessible.', + ); + return true; + }, +}); + +type Props = { + children: JSX.Element | JSX.Element[]; +}; + +export const FilterItemsContextProvider = ({ + children, +}: Props): JSX.Element => { + const location = useLocation(); + const [itemTypes, setItemTypes] = useState([]); + + useEffect(() => setItemTypes([]), [location]); + + const value = useMemo( + () => ({ + itemTypes, + setItemTypes, + shouldDisplayItem: (itemType: ItemTypeConst) => + itemTypes.length === 0 || itemTypes.includes(itemType), + }), + [itemTypes], + ); + + return ( + + {children} + + ); +}; + +export const useFilterItemsContext = (): FilterItemsContextType => + useContext(FilterItemsContext); diff --git a/src/components/context/ModalProviders.tsx b/src/components/context/ModalProviders.tsx index 04960e852..b9590b900 100644 --- a/src/components/context/ModalProviders.tsx +++ b/src/components/context/ModalProviders.tsx @@ -1,5 +1,3 @@ -import { CopyItemModalProvider } from './CopyItemModalContext'; -import { CreateShortcutModalProvider } from './CreateShortcutModalContext'; import { FlagItemModalProvider } from './FlagItemModalContext'; import { LayoutContextProvider } from './LayoutContext'; @@ -7,11 +5,7 @@ type Props = { children: JSX.Element }; const ModalProviders = ({ children }: Props): JSX.Element => ( - - - {children} - - + {children} ); diff --git a/src/components/item/ItemContent.tsx b/src/components/item/ItemContent.tsx index 4ea7ff5b8..f1c5784e9 100644 --- a/src/components/item/ItemContent.tsx +++ b/src/components/item/ItemContent.tsx @@ -48,6 +48,7 @@ import { } from '../../config/selectors'; import ErrorAlert from '../common/ErrorAlert'; import { useCurrentUserContext } from '../context/CurrentUserContext'; +import { useFilterItemsContext } from '../context/FilterItemsContext'; import ItemActions from '../main/ItemActions'; import Items from '../main/Items'; import NewItemButton from '../main/NewItemButton'; @@ -177,6 +178,8 @@ const FolderContent = ({ item: FolderItemType; enableEditing: boolean; }): JSX.Element => { + const { shouldDisplayItem } = useFilterItemsContext(); + const { data: children, isLoading, @@ -185,6 +188,9 @@ const FolderContent = ({ ordered: true, }); + // TODO: use hook's filter when available + const folderChildren = children?.filter((f) => shouldDisplayItem(f.type)); + if (isLoading) { return ; } @@ -198,14 +204,14 @@ const FolderContent = ({ parentId={item.id} id={buildItemsTableId(item.id)} title={item.name} - items={children ?? []} + items={folderChildren ?? []} headerElements={ enableEditing ? [] : undefined } // todo: not exactly correct, since you could have write rights on some child, // but it's more tedious to check permissions over all selected items ToolbarActions={enableEditing ? ItemActions : undefined} - totalCount={children?.length} + totalCount={folderChildren?.length} /> ); }; diff --git a/src/components/main/CopyButton.tsx b/src/components/main/CopyButton.tsx index 77dc169f2..8484dc260 100644 --- a/src/components/main/CopyButton.tsx +++ b/src/components/main/CopyButton.tsx @@ -1,4 +1,4 @@ -import { MouseEventHandler } from 'react'; +import { useState } from 'react'; import { IconButtonProps } from '@mui/material'; @@ -7,18 +7,23 @@ import { CopyButton as GraaspCopyButton, } from '@graasp/ui'; +import { mutations } from '@/config/queryClient'; +import { computeButtonText } from '@/utils/itemSelection'; + import { useBuilderTranslation } from '../../config/i18n'; import { ITEM_COPY_BUTTON_CLASS, ITEM_MENU_COPY_BUTTON_CLASS, } from '../../config/selectors'; import { BUILDER } from '../../langs/constants'; -import { useCopyItemModalContext } from '../context/CopyItemModalContext'; +import ItemSelectionModal, { + ItemSelectionModalProps, +} from './itemSelectionModal/ItemSelectionModal'; export type Props = { color?: IconButtonProps['color']; id?: string; - onClick?: MouseEventHandler; + onClick?: () => void; type?: ActionButtonVariant; itemIds: string[]; }; @@ -31,26 +36,60 @@ const CopyButton = ({ onClick, }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); + const { mutate: copyItems } = mutations.useCopyItems(); + const [open, setOpen] = useState(false); + + const openCopyModal = () => { + setOpen(true); + }; + + const onClose = () => { + setOpen(false); + }; - const { openModal: openCopyModal } = useCopyItemModalContext(); + const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { + copyItems({ + ids: itemIds, + to: destination, + }); + onClose(); + }; - const handleCopy: MouseEventHandler = ( - e, - ) => { - openCopyModal(itemIds); - onClick?.(e); + const handleCopy = () => { + openCopyModal(); + onClick?.(); }; + const buttonText = (name?: string) => + computeButtonText({ + translateBuilder, + translateKey: BUILDER.COPY_BUTTON, + name, + }); + return ( - + <> + + + {itemIds && open && ( + + )} + ); }; diff --git a/src/components/main/CreateShortcutButton.tsx b/src/components/main/CreateShortcutButton.tsx new file mode 100644 index 000000000..106366f05 --- /dev/null +++ b/src/components/main/CreateShortcutButton.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; + +import LabelImportantIcon from '@mui/icons-material/LabelImportant'; +import { ListItemIcon, MenuItem } from '@mui/material'; + +import { + DiscriminatedItem, + ItemType, + ShortcutItemType, + buildShortcutExtra, +} from '@graasp/sdk'; + +import { mutations } from '@/config/queryClient'; +import { computeButtonText } from '@/utils/itemSelection'; + +import { useBuilderTranslation } from '../../config/i18n'; +import { ITEM_MENU_SHORTCUT_BUTTON_CLASS } from '../../config/selectors'; +import { BUILDER } from '../../langs/constants'; +import ItemSelectionModal, { + ItemSelectionModalProps, +} from './itemSelectionModal/ItemSelectionModal'; + +export type Props = { + item: DiscriminatedItem; + onClick?: () => void; +}; + +const CreateShortcutButton = ({ + item: defaultItem, + onClick, +}: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + const { mutate: createShortcut } = mutations.usePostItem(); + const [open, setOpen] = useState(false); + const [item, setItem] = useState(defaultItem); + + const openShortcutModal = (newItem: DiscriminatedItem) => { + setOpen(true); + setItem(newItem); + }; + + const onClose = () => { + setOpen(false); + }; + + const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { + const target = item.id; // id of the item where the shortcut is pointing + + const shortcut: Partial & + Pick & { + parentId?: string; + } = { + name: translateBuilder(BUILDER.CREATE_SHORTCUT_DEFAULT_NAME, { + name: item?.name, + }), + extra: buildShortcutExtra(target), + type: ItemType.SHORTCUT, + parentId: destination, + }; + + createShortcut(shortcut); + onClose(); + }; + + const handleShortcut = () => { + openShortcutModal(item); + onClick?.(); + }; + + const buttonText = (name?: string) => + computeButtonText({ + translateBuilder, + translateKey: BUILDER.CREATE_SHORTCUT_BUTTON, + name, + }); + + return ( + <> + + + + + {translateBuilder(BUILDER.ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM)} + + + {item && open && ( + + )} + + ); +}; + +export default CreateShortcutButton; diff --git a/src/components/main/ItemMenu.tsx b/src/components/main/ItemMenu.tsx index 7fac8a4ec..25d3ef2da 100644 --- a/src/components/main/ItemMenu.tsx +++ b/src/components/main/ItemMenu.tsx @@ -2,7 +2,6 @@ import { useContext, useState } from 'react'; import FileCopyIcon from '@mui/icons-material/FileCopy'; import FlagIcon from '@mui/icons-material/Flag'; -import LabelImportantIcon from '@mui/icons-material/LabelImportant'; import MoreVertIcon from '@mui/icons-material/MoreVert'; import IconButton, { IconButtonProps } from '@mui/material/IconButton'; import ListItemIcon from '@mui/material/ListItemIcon'; @@ -20,7 +19,6 @@ import { ITEM_MENU_BUTTON_CLASS, ITEM_MENU_DUPLICATE_BUTTON_CLASS, ITEM_MENU_FLAG_BUTTON_CLASS, - ITEM_MENU_SHORTCUT_BUTTON_CLASS, buildItemMenu, buildItemMenuButtonId, } from '../../config/selectors'; @@ -31,10 +29,10 @@ import HideButton from '../common/HideButton'; import MoveButton from '../common/MoveButton'; import PinButton from '../common/PinButton'; import RecycleButton from '../common/RecycleButton'; -import { CreateShortcutModalContext } from '../context/CreateShortcutModalContext'; import { useCurrentUserContext } from '../context/CurrentUserContext'; import { FlagItemModalContext } from '../context/FlagItemModalContext'; import CopyButton from './CopyButton'; +import CreateShortcutButton from './CreateShortcutButton'; type Props = { item: DiscriminatedItem; @@ -52,9 +50,6 @@ const ItemMenu = ({ const { data: member } = useCurrentUserContext(); const [anchorEl, setAnchorEl] = useState(null); const { t: translateBuilder } = useBuilderTranslation(); - const { openModal: openCreateShortcutModal } = useContext( - CreateShortcutModalContext, - ); const { openModal: openFlagModal } = useContext(FlagItemModalContext); const { mutate: copyItems } = mutations.useCopyItems(); const { data: memberships } = hooks.useItemMemberships(item.id); @@ -67,11 +62,6 @@ const ItemMenu = ({ setAnchorEl(null); }; - const handleCreateShortcut = () => { - openCreateShortcutModal(item); - handleClose(); - }; - const handleFlag = () => { openFlagModal?.(item.id); handleClose(); @@ -83,11 +73,10 @@ const ItemMenu = ({ const to = parentsIds.length > 1 ? parentsIds[parentsIds.length - 2] : undefined; - const newPayload = { + copyItems({ ids: [item.id], to, - }; - copyItems(newPayload); + }); }; const renderEditorActions = () => { if (canEdit) { @@ -154,15 +143,11 @@ const ItemMenu = ({ {translateBuilder(BUILDER.ITEM_MENU_DUPLICATE_MENU_ITEM)} - - - - - {translateBuilder(BUILDER.ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM)} - + void; diff --git a/src/components/main/ItemsToolbar.tsx b/src/components/main/ItemsToolbar.tsx index 7e05871c5..596483be7 100644 --- a/src/components/main/ItemsToolbar.tsx +++ b/src/components/main/ItemsToolbar.tsx @@ -1,14 +1,16 @@ -import { Stack, Typography } from '@mui/material'; -import Checkbox, { CheckboxProps } from '@mui/material/Checkbox'; +import { Stack, Switch, Typography } from '@mui/material'; import FormControlLabel from '@mui/material/FormControlLabel'; import { useBuilderTranslation } from '@/config/i18n'; import { ACCESSIBLE_ITEMS_ONLY_ME_ID } from '@/config/selectors'; +import { ShowOnlyMeChangeType } from '@/config/types'; + +import SelectTypes from '../common/SelectTypes'; type Props = { title: string; headerElements?: JSX.Element[]; - onShowOnlyMeChange?: CheckboxProps['onChange']; + onShowOnlyMeChange?: ShowOnlyMeChangeType; showOnlyMe?: boolean; }; @@ -29,18 +31,21 @@ const ItemsToolbar = ({ {headerElements} - {onShowOnlyMeChange && ( - - } - label={t('Show only created by me')} - /> - )} + + {onShowOnlyMeChange && ( + onShowOnlyMeChange(checked)} + /> + } + label={t('Show only created by me')} + /> + )} + + ); }; diff --git a/src/components/main/TreeModal.tsx b/src/components/main/TreeModal.tsx deleted file mode 100644 index 7873097bc..000000000 --- a/src/components/main/TreeModal.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { useState } from 'react'; - -import Dialog from '@mui/material/Dialog'; -import DialogActions from '@mui/material/DialogActions'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; - -import { DiscriminatedItem, ItemType } from '@graasp/sdk'; -import { Button, DynamicTreeView, Loader } from '@graasp/ui'; - -import { TREE_VIEW_MAX_WIDTH } from '../../config/constants'; -import { useBuilderTranslation } from '../../config/i18n'; -import { hooks } from '../../config/queryClient'; -import { - HOME_MODAL_ITEM_ID, - TREE_MODAL_CONFIRM_BUTTON_ID, - TREE_MODAL_SHARED_ITEMS_ID, - buildTreeItemId, -} from '../../config/selectors'; -import { TreePreventSelection } from '../../enums'; -import { BUILDER } from '../../langs/constants'; -import { getParentsIdsFromPath } from '../../utils/item'; -import CancelButton from '../common/CancelButton'; - -const dialogId = 'simple-dialog-title'; -const { useItem, useItems, useOwnItems, useChildren, useSharedItems } = hooks; - -export type TreeModalProps = { - onConfirm: (args: { ids: string[]; to?: string }) => void; - onClose: (args: { id: string | null; open: boolean }) => void; - title: string; - itemIds?: string[]; - open?: boolean; - prevent?: TreePreventSelection; -}; - -const TreeModal = ({ - title, - onClose, - onConfirm, - open = false, - itemIds = [], - prevent = TreePreventSelection.NONE, -}: TreeModalProps): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { data: ownItems, isLoading: isOwnItemsLoading } = useOwnItems(); - // todo: get only shared items with write/admin rights - // otherwise choosing an item without the write rights will result in an error - const { data: sharedItems, isLoading: isSharedItemsLoading } = - useSharedItems(); - const [selectedId, setSelectedId] = useState(); - const { data: items, isLoading: isItemLoading } = useItems(itemIds); - - // build the expanded item ids list for a given tree (with treeRootId as id) - // by default, we expand all parents of items - // all other tree roots should be closed - const buildExpandedItems = (treeRootId: string) => { - if (!items || !items.data) { - return []; - } - - // suppose all items are in the same parent - const parentIds = - getParentsIdsFromPath(Object.values(items.data)[0].path) || []; - if (!parentIds.length) { - return []; - } - - // return expanded list depending current root id - // define root id depending on whether is the root parent is in the owned items - const rootItemId = parentIds[0]; - const isRootItemOwned = Boolean( - ownItems?.find(({ id }) => id === rootItemId), - ); - const itemRootId = isRootItemOwned - ? HOME_MODAL_ITEM_ID - : TREE_MODAL_SHARED_ITEMS_ID; - - // trees root not being treeRootId should be closed - if (treeRootId !== itemRootId) { - return []; - } - - // return expanded ids - const newExpandedItems = [treeRootId, ...parentIds]; - return newExpandedItems; - }; - - if (isOwnItemsLoading || isSharedItemsLoading || isItemLoading) { - return ; - } - - // compute whether the given id tree item is disabled - // it depends on the prevent mode and the previous items - const isTreeItemDisabled = ({ - itemId: iId, - parentIsDisabled, - }: { - itemId: string; - parentIsDisabled: boolean; - }) => { - switch (prevent) { - case TreePreventSelection.SELF_AND_CHILDREN: - // if the previous item is disabled, its children will be disabled - // and prevent selection on self - return Boolean(parentIsDisabled || itemIds.find((x) => x === iId)); - case TreePreventSelection.NONE: - default: - return false; - } - }; - - const handleClose = () => { - onClose({ id: null, open: false }); - }; - - const onClickConfirm = () => { - onConfirm({ ids: itemIds, to: selectedId }); - handleClose(); - }; - - const onTreeItemSelect = (nodeId: string) => { - if (selectedId === nodeId) { - setSelectedId(undefined); - } else { - setSelectedId(nodeId); - } - }; - - const isFolder = (i: Pick) => - i.type === ItemType.FOLDER; - - // compute tree only when the modal is open - const tree = !open ? null : ( - <> - {ownItems && ( - - )} - {sharedItems && ( - - )} - - ); - - return ( - - {title} - {tree} - - - - - - ); -}; - -export default TreeModal; diff --git a/src/components/main/itemSelectionModal/ChildrenNavigationTree.tsx b/src/components/main/itemSelectionModal/ChildrenNavigationTree.tsx index 7a5806047..aa4e52a4c 100644 --- a/src/components/main/itemSelectionModal/ChildrenNavigationTree.tsx +++ b/src/components/main/itemSelectionModal/ChildrenNavigationTree.tsx @@ -25,11 +25,9 @@ const ChildrenNavigationTree = ({ isDisabled, }: ChildrenNavigationTreeProps): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); - // TODO: use filter in useChildren directly in another PR const { data: children } = hooks.useChildren(selectedNavigationItem.id); - - const folders = children?.filter((item) => item.type === ItemType.FOLDER); - + // TODO: use hook's filter when available + const folders = children?.filter((f) => f.type === ItemType.FOLDER); return ( <> {folders?.map((ele) => ( diff --git a/src/components/main/itemSelectionModal/ItemSelectionModal.tsx b/src/components/main/itemSelectionModal/ItemSelectionModal.tsx index cc6864bf2..1af289b3d 100644 --- a/src/components/main/itemSelectionModal/ItemSelectionModal.tsx +++ b/src/components/main/itemSelectionModal/ItemSelectionModal.tsx @@ -9,10 +9,13 @@ import DialogTitle from '@mui/material/DialogTitle'; import { DiscriminatedItem } from '@graasp/sdk'; +import { computeTitle } from '@/utils/itemSelection'; + import { useBuilderTranslation } from '../../../config/i18n'; import { hooks } from '../../../config/queryClient'; import { HOME_MODAL_ITEM_ID, + MY_GRAASP_ITEM_PATH, TREE_MODAL_CONFIRM_BUTTON_ID, } from '../../../config/selectors'; import { BUILDER } from '../../../langs/constants'; @@ -23,31 +26,42 @@ import ChildrenNavigationTree from './ChildrenNavigationTree'; import RootNavigationTree from './RootNavigationTree'; const dialogId = 'items-tree-modal'; -const MY_GRAASP_BREADCRUMB_ID = 'selectionModalMyGraasp'; export type ItemSelectionModalProps = { buttonText: (itemName?: string) => string; /** disabled rows * */ - isDisabled: (item: NavigationElement, homeId: string) => boolean; + isDisabled?: ( + items: DiscriminatedItem[], + item: NavigationElement, + homeId: string, + ) => boolean; // items can be undefined because "many" operations start empty - items?: DiscriminatedItem[]; + itemIds?: string[]; onClose: (args: { id: string | null; open: boolean }) => void; - onConfirm: (args: string | undefined) => void; + onConfirm: (destination: string | undefined) => void; open?: boolean; - title: string; + titleKey: string; }; const ItemSelectionModal = ({ buttonText = () => 'Submit', - isDisabled, - items = [], + isDisabled = () => false, + itemIds = [], onClose, onConfirm, open = false, - title, + titleKey, }: ItemSelectionModalProps): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); + const { data: items } = hooks.useItems(itemIds); + + const title = computeTitle({ + items, + count: itemIds.length - 1, + translateBuilder, + translateKey: titleKey, + }); // special elements for breadcrumbs // root displays specific paths @@ -60,8 +74,8 @@ const ItemSelectionModal = ({ // my graasp displays accessible items const MY_GRAASP_BREADCRUMB: NavigationElement = { name: translateBuilder(BUILDER.MY_ITEMS_TITLE), - id: MY_GRAASP_BREADCRUMB_ID, - path: MY_GRAASP_BREADCRUMB_ID, + id: MY_GRAASP_ITEM_PATH, + path: MY_GRAASP_ITEM_PATH, }; const SPECIAL_BREADCRUMB_IDS = [ROOT_BREADCRUMB.id, MY_GRAASP_BREADCRUMB.id]; @@ -124,6 +138,10 @@ const ItemSelectionModal = ({ return ; }; + const isDisabledLocal = (item: NavigationElement) => + !items?.data || + isDisabled(Object.values(items.data), item, MY_GRAASP_BREADCRUMB.id); + return ( {renderBreadcrumbs()} - {selectedNavigationItem.id === ROOT_BREADCRUMB.id && ( + {items?.data && selectedNavigationItem.id === ROOT_BREADCRUMB.id && ( isDisabled(item, MY_GRAASP_BREADCRUMB.id)} + isDisabled={(item) => + isDisabled( + Object.values(items.data), + item, + MY_GRAASP_BREADCRUMB.id, + ) + } onClick={setSelectedItem} selectedId={selectedItem?.id} onNavigate={onNavigate} - items={items} + items={Object.values(items.data)} rootMenuItems={[MY_GRAASP_BREADCRUMB]} /> )} {selectedNavigationItem.id === MY_GRAASP_BREADCRUMB.id && ( isDisabled(item, MY_GRAASP_BREADCRUMB.id)} + isDisabled={isDisabledLocal} onClick={setSelectedItem} onNavigate={onNavigate} selectedId={selectedItem?.id} @@ -164,7 +188,7 @@ const ItemSelectionModal = ({ )} {!SPECIAL_BREADCRUMB_IDS.includes(selectedNavigationItem.id) && ( isDisabled(item, MY_GRAASP_BREADCRUMB.id)} + isDisabled={isDisabledLocal} onClick={setSelectedItem} onNavigate={onNavigate} selectedId={selectedItem?.id} @@ -181,7 +205,7 @@ const ItemSelectionModal = ({ !selectedItem || // root is not a valid value selectedItem.id === ROOT_BREADCRUMB.id || - isDisabled(selectedItem, MY_GRAASP_BREADCRUMB.id) + isDisabledLocal(selectedItem) } id={TREE_MODAL_CONFIRM_BUTTON_ID} variant="contained" diff --git a/src/components/pages/FavoriteItemsScreen.tsx b/src/components/pages/FavoriteItemsScreen.tsx index 3730ad4c9..c2bc6ea5c 100644 --- a/src/components/pages/FavoriteItemsScreen.tsx +++ b/src/components/pages/FavoriteItemsScreen.tsx @@ -1,6 +1,5 @@ import Box from '@mui/material/Box'; -import { DiscriminatedItem } from '@graasp/sdk'; import { Loader } from '@graasp/ui'; import { useBuilderTranslation } from '../../config/i18n'; @@ -11,21 +10,25 @@ import { } from '../../config/selectors'; import { BUILDER } from '../../langs/constants'; import ErrorAlert from '../common/ErrorAlert'; +import { useFilterItemsContext } from '../context/FilterItemsContext'; import ItemHeader from '../item/header/ItemHeader'; import Items from '../main/Items'; const FavoriteItemsLoadableContent = (): JSX.Element | null => { const { t: translateBuilder } = useBuilderTranslation(); const { data, isLoading: isItemsLoading, isError } = hooks.useFavoriteItems(); + const { shouldDisplayItem } = useFilterItemsContext(); + // TODO: implement filter in the hooks directly ? + const filteredData = data?.filter((d) => shouldDisplayItem(d.item.type)); - if (data) { + if (filteredData) { return ( d.item as DiscriminatedItem)} + items={filteredData.map((d) => d.item)} /> ); diff --git a/src/components/pages/HomeScreen.tsx b/src/components/pages/HomeScreen.tsx index c3a98a3a8..e4240e647 100644 --- a/src/components/pages/HomeScreen.tsx +++ b/src/components/pages/HomeScreen.tsx @@ -1,11 +1,12 @@ import { useState } from 'react'; -import { CheckboxProps, LinearProgress } from '@mui/material'; +import { LinearProgress } from '@mui/material'; import Box from '@mui/material/Box'; import { Loader } from '@graasp/ui'; import { ITEM_PAGE_SIZE } from '@/config/constants'; +import { ShowOnlyMeChangeType } from '@/config/types'; import { useBuilderTranslation } from '../../config/i18n'; import { hooks } from '../../config/queryClient'; @@ -16,6 +17,7 @@ import { import { BUILDER } from '../../langs/constants'; import ErrorAlert from '../common/ErrorAlert'; import { useCurrentUserContext } from '../context/CurrentUserContext'; +import { useFilterItemsContext } from '../context/FilterItemsContext'; import FileUploader from '../file/FileUploader'; import { UppyContextProvider } from '../file/UppyContext'; import { useItemSearch } from '../item/ItemSearch'; @@ -34,6 +36,7 @@ type HomeItemSortableColumn = const HomeLoadableContent = (): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { data: currentMember } = useCurrentUserContext(); + const { itemTypes } = useFilterItemsContext(); const [showOnlyMe, setShowOnlyMe] = useState(false); const [page, setPage] = useState(1); @@ -53,13 +56,14 @@ const HomeLoadableContent = (): JSX.Element => { name: itemSearch.text, sortBy: sortColumn, ordering, + types: itemTypes, }, // todo: adapt page size given the user window height { page, pageSize: ITEM_PAGE_SIZE }, ); - const onShowOnlyMeChange: CheckboxProps['onChange'] = (e) => { - setShowOnlyMe(e.target.checked); + const onShowOnlyMeChange: ShowOnlyMeChangeType = (checked) => { + setShowOnlyMe(checked); setPage(1); }; diff --git a/src/components/pages/PublishedItemsScreen.tsx b/src/components/pages/PublishedItemsScreen.tsx index 1134bbd28..dbf2b4157 100644 --- a/src/components/pages/PublishedItemsScreen.tsx +++ b/src/components/pages/PublishedItemsScreen.tsx @@ -11,6 +11,7 @@ import { import { BUILDER } from '../../langs/constants'; import ErrorAlert from '../common/ErrorAlert'; import { useCurrentUserContext } from '../context/CurrentUserContext'; +import { useFilterItemsContext } from '../context/FilterItemsContext'; import ItemHeader from '../item/header/ItemHeader'; import Items from '../main/Items'; @@ -22,16 +23,18 @@ const PublishedItemsLoadableContent = (): JSX.Element | null => { isLoading, isError, } = hooks.usePublishedItemsForMember(member?.id); + const { shouldDisplayItem } = useFilterItemsContext(); + // TODO: implement filter in the hooks directly ? + const filteredData = publishedItems?.filter((d) => shouldDisplayItem(d.type)); - if (publishedItems) { + if (filteredData) { return ( ); diff --git a/src/components/pages/RecycledItemsScreen.tsx b/src/components/pages/RecycledItemsScreen.tsx index dcabfa5b1..cc9c19b68 100644 --- a/src/components/pages/RecycledItemsScreen.tsx +++ b/src/components/pages/RecycledItemsScreen.tsx @@ -15,6 +15,7 @@ import { BUILDER } from '../../langs/constants'; import DeleteButton from '../common/DeleteButton'; import ErrorAlert from '../common/ErrorAlert'; import RestoreButton from '../common/RestoreButton'; +import { useFilterItemsContext } from '../context/FilterItemsContext'; import ItemHeader from '../item/header/ItemHeader'; import Items from '../main/Items'; @@ -51,8 +52,11 @@ const ToolbarActions = ({ selectedIds }: ToolbarActionsProps): JSX.Element => ( const RecycleBinLoadableContent = (): JSX.Element | null => { const { t: translateBuilder } = useBuilderTranslation(); const { data: recycledItems, isLoading, isError } = hooks.useRecycledItems(); + const { shouldDisplayItem } = useFilterItemsContext(); + // TODO: implement filter in the hooks directly ? + const filteredData = recycledItems?.filter((d) => shouldDisplayItem(d.type)); - if (recycledItems) { + if (filteredData) { return ( @@ -60,7 +64,7 @@ const RecycleBinLoadableContent = (): JSX.Element | null => { id={RECYCLED_ITEMS_ID} clickable={false} title={translateBuilder(BUILDER.RECYCLE_BIN_TITLE)} - items={recycledItems} + items={filteredData} actions={RowActions} ToolbarActions={ToolbarActions} showThumbnails={false} diff --git a/src/components/pages/SharedItemsScreen.tsx b/src/components/pages/SharedItemsScreen.tsx index e043ba18e..3f8c85030 100644 --- a/src/components/pages/SharedItemsScreen.tsx +++ b/src/components/pages/SharedItemsScreen.tsx @@ -13,14 +13,18 @@ import { SHARED_ITEMS_ROOT_CONTAINER, } from '../../config/selectors'; import ErrorAlert from '../common/ErrorAlert'; +import { useFilterItemsContext } from '../context/FilterItemsContext'; import ItemHeader from '../item/header/ItemHeader'; import Items from '../main/Items'; const SharedItemsLoadableContent = (): JSX.Element | null => { const { t: translateBuilder } = useBuilderTranslation(); const { data: sharedItems, isLoading, isError } = hooks.useSharedItems(); + const { shouldDisplayItem } = useFilterItemsContext(); + // TODO: implement filter in the hooks directly ? + const filteredItems = sharedItems?.filter((i) => shouldDisplayItem(i.type)); - if (sharedItems) { + if (filteredItems) { return ( @@ -32,9 +36,9 @@ const SharedItemsLoadableContent = (): JSX.Element | null => { ); diff --git a/src/config/constants.ts b/src/config/constants.ts index 900103e8a..2f14efb50 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -10,6 +10,7 @@ export const DESCRIPTION_MAX_LENGTH = 30; export const DOUBLE_CLICK_DELAY_MS = 500; export const TREE_VIEW_MAX_WIDTH = 400; +export const ITEM_SELECT_MODAL_TITLE_MAX_NAME_LENGTH = 15; export const UUID_LENGTH = 36; export const DRAWER_WIDTH = 240; diff --git a/src/config/queryClient.ts b/src/config/queryClient.ts index c387daacc..d4c11ffb3 100644 --- a/src/config/queryClient.ts +++ b/src/config/queryClient.ts @@ -17,7 +17,7 @@ const { enableWebsocket: true, defaultQueryOptions: { keepPreviousData: true, - refetchOnMount: false, + refetchOnMount: true, }, DOMAIN, }); diff --git a/src/config/selectors.ts b/src/config/selectors.ts index b0b57d2b6..8c72f7e4c 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -28,7 +28,6 @@ export const buildNavigationModalItemId = (id: string): string => `${HOME_MODAL_ITEM_ID}-${id}`; export const ROOT_MODAL_ID = 'rootModal'; -export const TREE_MODAL_SHARED_ITEMS_ID = 'treeModalSharedItems'; export const buildTreeItemId = (id: string, treeRootId: string): string => `${treeRootId}-${id}`; export const buildItemRowArrowId = (id: string): string => @@ -348,4 +347,4 @@ export const buildShortLinkUrlTextId = (platform: ShortLinkPlatform): string => export const ACCESSIBLE_ITEMS_ONLY_ME_ID = 'accessibleItemsOnlyMe'; export const ACCESSIBLE_ITEMS_TABLE_ID = 'accessibleItemsTable'; export const ACCESSIBLE_ITEMS_NEXT_PAGE_BUTTON_SELECTOR = `#${ACCESSIBLE_ITEMS_TABLE_ID} [data-testid="KeyboardArrowRightIcon"]`; -export const NAVIGATION_MENU_ARROW = `#${NAVIGATION_ROOT_ID} [data-testid="NavigateNextIcon"]`; +export const MY_GRAASP_ITEM_PATH = 'myGraaspItemPath'; diff --git a/src/config/types.ts b/src/config/types.ts index cc958f5d7..630fb3637 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -6,3 +6,4 @@ export enum InternalItemType { } export type NewItemTabType = DiscriminatedItem['type'] | InternalItemType.ZIP; +export type ShowOnlyMeChangeType = (checked: boolean) => void; diff --git a/src/langs/ar.json b/src/langs/ar.json index aaadc4611..7e467f72d 100644 --- a/src/langs/ar.json +++ b/src/langs/ar.json @@ -9,6 +9,7 @@ "COLLAPSE_ITEM_UNCOLLAPSE_TEXT": "عدم الإنهيار", "CONFIRM_BUTTON": "إعتماد", "COPY_ITEM_MODAL_TITLE": "أين تريد نسخ هذا العنصر؟", + "COPY_BUTTON": "نسخ", "CREATE_ITEM_ADD_BUTTON": "أضِف", "CREATE_ITEM_LINK_INVALID_LINK_ERROR": "هذا الرابط غير ساري المفعول", "CREATE_ITEM_LINK_LABEL": "رابط", @@ -70,7 +71,6 @@ "INVITATIONS_TABLE_PERMISSION_HEADER": "إذن", "ITEM_CATEGORIES_SELECTION_TITLE": "فئة", "ITEM_CHATBOX_TITLE": "صندوق الدردشه", - "ITEM_COPY_BUTTON": "نسخ", "ITEM_MEMBERSHIP_PERMISSION_LABEL": "إذن", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "أجراءات", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "هذه العضوية محددة في العنصر الأصلي ولا يمكن حذفها هنا.", diff --git a/src/langs/constants.ts b/src/langs/constants.ts index 649e3a936..c915c8a19 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -11,6 +11,7 @@ export const BUILDER = { COLLAPSE_ITEM_UNCOLLAPSE_TEXT: 'COLLAPSE_ITEM_UNCOLLAPSE_TEXT', CONFIRM_BUTTON: 'CONFIRM_BUTTON', COPY_ITEM_MODAL_TITLE: 'COPY_ITEM_MODAL_TITLE', + COPY_BUTTON: 'COPY_BUTTON', CREATE_ITEM_ADD_BUTTON: 'CREATE_ITEM_ADD_BUTTON', CREATE_ITEM_APP_TITLE: 'CREATE_ITEM_APP_TITLE', CREATE_ITEM_DOCUMENT_TITLE: 'CREATE_ITEM_DOCUMENT_TITLE', @@ -27,6 +28,7 @@ export const BUILDER = { CREATE_NEW_ITEM_ETHERPAD_LABEL: 'CREATE_NEW_ITEM_ETHERPAD_LABEL', CREATE_NEW_ITEM_ETHERPAD_TITLE: 'CREATE_NEW_ITEM_ETHERPAD_TITLE', CREATE_NEW_ITEM_NAME_LABEL: 'CREATE_NEW_ITEM_NAME_LABEL', + CREATE_SHORTCUT_BUTTON: 'CREATE_SHORTCUT_BUTTON', CREATE_SHORTCUT_DEFAULT_NAME: 'CREATE_SHORTCUT_DEFAULT_NAME', CREATE_SHORTCUT_MODAL_TITLE: 'CREATE_SHORTCUT_MODAL_TITLE', CROP_IMAGE_MODAL_CONTENT_TEXT: 'CROP_IMAGE_MODAL_CONTENT_TEXT', @@ -127,7 +129,6 @@ export const BUILDER = { INVITATIONS_TABLE_PERMISSION_HEADER: 'INVITATIONS_TABLE_PERMISSION_HEADER', ITEM_CATEGORIES_SELECTION_TITLE: 'ITEM_CATEGORIES_SELECTION_TITLE', ITEM_CHATBOX_TITLE: 'ITEM_CHATBOX_TITLE', - ITEM_COPY_BUTTON: 'ITEM_COPY_BUTTON', ITEM_MEMBERSHIP_PERMISSION_LABEL: 'ITEM_MEMBERSHIP_PERMISSION_LABEL', ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER: 'ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER', @@ -369,4 +370,5 @@ export const BUILDER = { BACK: 'BACK', UPDATE_THUMBNAIL_AT_INFO_ALERT: 'UPDATE_THUMBNAIL_AT_INFO_ALERT', ITEM_ACTION_INFORMATION: 'ITEM_ACTION_INFORMATION', + FILTER_BY_TYPES_LABEL: 'FILTER_BY_TYPES_LABEL', }; diff --git a/src/langs/de.json b/src/langs/de.json index e65eaedf9..a6a7c54af 100644 --- a/src/langs/de.json +++ b/src/langs/de.json @@ -8,7 +8,13 @@ "COLLAPSE_ITEM_COLLAPSE_TEXT": "Zusammenklappen", "COLLAPSE_ITEM_UNCOLLAPSE_TEXT": "Aufklappen", "CONFIRM_BUTTON": "Bestätigen", + "COPY_BUTTON": "Kopieren", + "COPY_BUTTON_zero": "Kopieren", + "COPY_BUTTON_one": "Kopieren nach {{name}}", "COPY_ITEM_MODAL_TITLE": "Wohin möchten Sie dieses Element kopieren?", + "COPY_ITEM_MODAL_TITLE_zero": "Wohin möchten Sie {{name}} kopieren?", + "COPY_ITEM_MODAL_TITLE_one": "Wohin möchten Sie {{name}} und einen anderen kopieren?", + "COPY_ITEM_MODAL_TITLE_other": "Wohin möchten Sie {{name}} und {{count}} andere kopieren?", "CREATE_ITEM_ADD_BUTTON": "Hinzufügen", "CREATE_ITEM_LINK_INVALID_LINK_ERROR": "Dieser Link ist ungültig", "CREATE_ITEM_LINK_LABEL": "Link", @@ -21,8 +27,12 @@ "CREATE_NEW_ITEM_ETHERPAD_LABEL": "Name", "CREATE_NEW_ITEM_ETHERPAD_TITLE": "Erstellen Sie ein Etherpad", "CREATE_NEW_ITEM_NAME_LABEL": "Name", + "CREATE_SHORTCUT_BUTTON": "Erstelle die Verknüpfung", + "CREATE_SHORTCUT_BUTTON_zero": "Erstelle die Verknüpfung", + "CREATE_SHORTCUT_BUTTON_one": "Erstelle die Verknüpfung zu {{name}}", "CREATE_SHORTCUT_DEFAULT_NAME": "Verknüpfung zu {{name}}", "CREATE_SHORTCUT_MODAL_TITLE": "Wo möchten Sie die Verknüpfung erstellen?", + "CREATE_SHORTCUT_MODAL_TITLE_zero": "Wo möchten Sie die Verknüpfung für {{name}} erstellt werden?", "CROP_IMAGE_MODAL_CONTENT_TEXT": "Schneiden Sie das ausgewählte Bild zu, damit es den Anforderungen an die Bildgröße entspricht.", "CROP_IMAGE_MODAL_TITLE": "Bild zuschneiden", "DELETE_BUTTON": "Löschen", @@ -70,7 +80,6 @@ "INVITATIONS_TABLE_PERMISSION_HEADER": "Genehmigung", "ITEM_CATEGORIES_SELECTION_TITLE": "Kategorie", "ITEM_CHATBOX_TITLE": "Chat Box", - "ITEM_COPY_BUTTON": "Kopieren", "ITEM_MEMBERSHIP_PERMISSION_LABEL": "Genehmigung", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "Aktionen", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Diese Mitgliedschaft wird im übergeordneten Element definiert und kann hier nicht gelöscht werden.", diff --git a/src/langs/en.json b/src/langs/en.json index f6c9906a6..edbf4e167 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -8,7 +8,13 @@ "COLLAPSE_ITEM_COLLAPSE_TEXT": "Collapse", "COLLAPSE_ITEM_UNCOLLAPSE_TEXT": "Uncollapse", "CONFIRM_BUTTON": "Confirm", + "COPY_BUTTON": "Copy", + "COPY_BUTTON_zero": "Copy", + "COPY_BUTTON_one": "Copy to {{name}}", "COPY_ITEM_MODAL_TITLE": "Where do you want to copy this item?", + "COPY_ITEM_MODAL_TITLE_zero": "Where do you want to copy {{name}}?", + "COPY_ITEM_MODAL_TITLE_one": "Where do you want to copy {{name}} and one other?", + "COPY_ITEM_MODAL_TITLE_other": "Where do you want to copy {{name}} and {{count}} others?", "CREATE_ITEM_ADD_BUTTON": "Add", "CREATE_ITEM_LINK_INVALID_LINK_ERROR": "This link is not valid", "CREATE_ITEM_LINK_LABEL": "Link", @@ -21,8 +27,12 @@ "CREATE_NEW_ITEM_ETHERPAD_LABEL": "Name", "CREATE_NEW_ITEM_ETHERPAD_TITLE": "Create an Etherpad", "CREATE_NEW_ITEM_NAME_LABEL": "Name", + "CREATE_SHORTCUT_BUTTON": "Create shortcut", + "CREATE_SHORTCUT_BUTTON_zero": "Create shortcut", + "CREATE_SHORTCUT_BUTTON_one": "Create shortcut to {{name}}", "CREATE_SHORTCUT_DEFAULT_NAME": "Shortcut to {{name}}", "CREATE_SHORTCUT_MODAL_TITLE": "Where do you want to create the shortcut?", + "CREATE_SHORTCUT_MODAL_TITLE_zero": "Where do you want to create the shortcut for {{name}}?", "CROP_IMAGE_MODAL_CONTENT_TEXT": "Crop your chosen image to fit the image size requirements.", "CROP_IMAGE_MODAL_TITLE": "Crop Image", "CROP_IMAGE_MODAL_IMAGE_ALT_TEXT": "Crop me", @@ -72,7 +82,6 @@ "INVITATIONS_TABLE_PERMISSION_HEADER": "Permission", "ITEM_CATEGORIES_SELECTION_TITLE": "Category", "ITEM_CHATBOX_TITLE": "Chatbox", - "ITEM_COPY_BUTTON": "Copy", "ITEM_MEMBERSHIP_PERMISSION_LABEL": "Permission", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "Actions", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "This membership is defined in the parent item and cannot be deleted here.", @@ -308,5 +317,6 @@ "ITEM_SELECTION_NAVIGATION_PARENT": "Move above", "BACK": "Go Back", "UPDATE_THUMBNAIL_AT_INFO_ALERT": "You can change the item's thumbnail in its information page: Information", - "ITEM_ACTION_INFORMATION": "Information" + "ITEM_ACTION_INFORMATION": "Information", + "FILTER_BY_TYPES_LABEL": "Type" } diff --git a/src/langs/es.json b/src/langs/es.json index e1ed9cade..f3591bef3 100644 --- a/src/langs/es.json +++ b/src/langs/es.json @@ -8,7 +8,13 @@ "COLLAPSE_ITEM_COLLAPSE_TEXT": "Colapsar", "COLLAPSE_ITEM_UNCOLLAPSE_TEXT": "Destrabar", "CONFIRM_BUTTON": "Confirmar", + "COPY_BUTTON": "Copiar", + "COPY_BUTTON_zero": "Copiar", + "COPY_BUTTON_one": "Copiar a {{name}}", "COPY_ITEM_MODAL_TITLE": "¿Dónde desea copiar este elemento?", + "COPY_ITEM_MODAL_TITLE_zero": "¿Dónde desea copiar {{name}}?", + "COPY_ITEM_MODAL_TITLE_one": "¿Dónde desea copiar {{name}} y otro más?", + "COPY_ITEM_MODAL_TITLE_other": "¿Dónde desea copiar {{name}} y {{count}} otros?", "CREATE_ITEM_ADD_BUTTON": "Agregar", "CREATE_ITEM_LINK_INVALID_LINK_ERROR": "Este enlace no es válido", "CREATE_ITEM_LINK_LABEL": "Enlace", @@ -21,8 +27,12 @@ "CREATE_NEW_ITEM_ETHERPAD_LABEL": "Nombre", "CREATE_NEW_ITEM_ETHERPAD_TITLE": "Crear un Etherpad", "CREATE_NEW_ITEM_NAME_LABEL": "Nombre", - "CREATE_SHORTCUT_DEFAULT_NAME": "Acceso directo a {{nombre}}", + "CREATE_SHORTCUT_BUTTON": "Crear el acceso directo", + "CREATE_SHORTCUT_BUTTON_zero": "Crear el acceso directo", + "CREATE_SHORTCUT_BUTTON_one": "Crea el acceso directo a {{name}}", + "CREATE_SHORTCUT_DEFAULT_NAME": "Acceso directo a {{name}}", "CREATE_SHORTCUT_MODAL_TITLE": "¿Dónde quieres crear el acceso directo?", + "CREATE_SHORTCUT_MODAL_TITLE_zero": "¿Dónde quieres crear el acceso directo para {{name}}?", "CROP_IMAGE_MODAL_CONTENT_TEXT": "Recorte la imagen elegida para que se ajuste a los requisitos de tamaño de la imagen.", "CROP_IMAGE_MODAL_TITLE": "Delimitar imagen", "DELETE_BUTTON": "Borrar", @@ -70,7 +80,6 @@ "INVITATIONS_TABLE_PERMISSION_HEADER": "Permiso", "ITEM_CATEGORIES_SELECTION_TITLE": "Categoría", "ITEM_CHATBOX_TITLE": "Ventana de chat", - "ITEM_COPY_BUTTON": "Copiar", "ITEM_MEMBERSHIP_PERMISSION_LABEL": "Permiso", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "Comportamiento", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Esta membresía se define en el elemento principal y no se puede eliminar aquí.", @@ -291,5 +300,6 @@ "SHORT_LINK_MAX_CHARS_ERROR": "El enlace corto no debe exceder los {{data}} caracteres.", "SHORT_LINK_INVALID_CHARS_ERROR": "El enlace corto contiene caracteres no válidos ({{data}}).", "SHORT_LINK_UNKNOWN_ERROR": "Un error desconocido ocurrió.", - "You can also find the items of this page in ''My Graasp''. This page will be unavailable soon.": "También puede encontrar los elementos de esta página en ''My Graasp''. Esta página ''Elementos compartidos'' dejará de estar disponible pronto." + "You can also find the items of this page in ''My Graasp''. This page will be unavailable soon.": "También puede encontrar los elementos de esta página en ''My Graasp''. Esta página ''Elementos compartidos'' dejará de estar disponible pronto.", + "FILTER_BY_TYPES_LABEL": "Tipo" } diff --git a/src/langs/fr.json b/src/langs/fr.json index 795c3bbd2..22ee8edbe 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -8,7 +8,13 @@ "COLLAPSE_ITEM_COLLAPSE_TEXT": "Minifier", "COLLAPSE_ITEM_UNCOLLAPSE_TEXT": "Maximiser", "CONFIRM_BUTTON": "Confirmer", - "COPY_ITEM_MODAL_TITLE": "Où copier cet élément?", + "COPY_BUTTON": "Copier", + "COPY_BUTTON_zero": "Copier", + "COPY_BUTTON_one": "Copier vers {{name}}", + "COPY_ITEM_MODAL_TITLE": "Où voulez-vous copier cet élément?", + "COPY_ITEM_MODAL_TITLE_zero": "Où voulez-vous copier {{name}}?", + "COPY_ITEM_MODAL_TITLE_one": "Où voulez-vous copier {{name}} et un autre?", + "COPY_ITEM_MODAL_TITLE_other": "Où voulez-vous copier {{name}} et {{count}} autres?", "CREATE_ITEM_ADD_BUTTON": "Ajouter", "CREATE_ITEM_LINK_INVALID_LINK_ERROR": "Ce lien n'est pas valide", "CREATE_ITEM_LINK_LABEL": "Lien", @@ -21,8 +27,12 @@ "CREATE_NEW_ITEM_ETHERPAD_LABEL": "Nom", "CREATE_NEW_ITEM_ETHERPAD_TITLE": "Créer un Etherpad", "CREATE_NEW_ITEM_NAME_LABEL": "Nom", + "CREATE_SHORTCUT_BUTTON": "Créer le raccourci", + "CREATE_SHORTCUT_BUTTON_zero": "Créer le raccourci", + "CREATE_SHORTCUT_BUTTON_one": "Créer le raccourci dans {{name}}", "CREATE_SHORTCUT_DEFAULT_NAME": "Raccourci vers {{name}}", - "CREATE_SHORTCUT_MODAL_TITLE": "Où voulez-vous créer le raccourci ?", + "CREATE_SHORTCUT_MODAL_TITLE": "Où voulez-vous créer le raccourci?", + "CREATE_SHORTCUT_MODAL_TITLE_zero": "Où voulez-vous créer le raccourci pour {{name}}?", "CROP_IMAGE_MODAL_CONTENT_TEXT": "Couper l'image choisie pour correspondre aux critères de taille d'image.", "CROP_IMAGE_MODAL_TITLE": "Couper l'Image", "DELETE_BUTTON": "Supprimer", @@ -70,7 +80,6 @@ "INVITATIONS_TABLE_PERMISSION_HEADER": "Permission", "ITEM_CATEGORIES_SELECTION_TITLE": "Catégories", "ITEM_CHATBOX_TITLE": "Chatbox", - "ITEM_COPY_BUTTON": "Copier", "ITEM_MEMBERSHIP_PERMISSION_LABEL": "Permission", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "Actions", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Cette permission est définie dans l'élément parent, et ne peut pas être supprimée ici.", @@ -299,5 +308,6 @@ "You can also find the items of this page in ''My Graasp''. This page will be unavailable soon.": "Les éléments de cette page sont aussi disponibles dans ''Mon Graasp''. Cette page ''Eléments Partagés'' sera bientôt indisponible.", "ITEM_SELECTION_NAVIGATION_RECENT_ITEMS": "Éléments Récents", "ITEM_SELECTION_NAVIGATION_PARENT": "Déplacer au-dessus", - "ARIA_OPEN_DRAWER": "ouvrir la barre latérale" + "ARIA_OPEN_DRAWER": "ouvrir la barre latérale", + "FILTER_BY_TYPES_LABEL": "Type" } diff --git a/src/langs/it.json b/src/langs/it.json index a3e661319..39842d10c 100644 --- a/src/langs/it.json +++ b/src/langs/it.json @@ -8,7 +8,13 @@ "COLLAPSE_ITEM_COLLAPSE_TEXT": "Crollo", "COLLAPSE_ITEM_UNCOLLAPSE_TEXT": "Non collassare", "CONFIRM_BUTTON": "Confermare", + "COPY_BUTTON": "Copiare", + "COPY_BUTTON_zero": "Copiare", + "COPY_BUTTON_one": "Copiare in {{name}}", "COPY_ITEM_MODAL_TITLE": "Dove vuoi copiare questo elemento?", + "COPY_ITEM_MODAL_TITLE_zero": "Dove si vuole copiare {{name}}?", + "COPY_ITEM_MODAL_TITLE_one": "Dove vuoi copiare {{name}} e un altro?", + "COPY_ITEM_MODAL_TITLE_other": "Dove si desidera copiare {{name}} e {{count}} altri?", "CREATE_ITEM_ADD_BUTTON": "Aggiungere", "CREATE_ITEM_LINK_INVALID_LINK_ERROR": "Questo collegamento non è valido", "CREATE_ITEM_LINK_LABEL": "Collegamento", @@ -21,8 +27,12 @@ "CREATE_NEW_ITEM_ETHERPAD_LABEL": "Nome", "CREATE_NEW_ITEM_ETHERPAD_TITLE": "Crea un Etherpad", "CREATE_NEW_ITEM_NAME_LABEL": "Nome", - "CREATE_SHORTCUT_DEFAULT_NAME": "Scorciatoia per {{nome}}", + "CREATE_SHORTCUT_BUTTON": "Crea il collegamento", + "CREATE_SHORTCUT_BUTTON_zero": "Crea il collegamento", + "CREATE_SHORTCUT_BUTTON_one": "Crea il collegamento a {{name}}", + "CREATE_SHORTCUT_DEFAULT_NAME": "Scorciatoia per {{name}}", "CREATE_SHORTCUT_MODAL_TITLE": "Dove vuoi creare il collegamento?", + "CREATE_SHORTCUT_MODAL_TITLE_zero": "Dove vuoi creare il collegamento per {{name}}?", "CROP_IMAGE_MODAL_CONTENT_TEXT": "Ritaglia l'immagine scelta per adattarla ai requisiti di dimensione dell'immagine.", "CROP_IMAGE_MODAL_TITLE": "Ritaglia l'immagine", "DELETE_BUTTON": "Eliminare", @@ -70,7 +80,6 @@ "INVITATIONS_TABLE_PERMISSION_HEADER": "Autorizzazione", "ITEM_CATEGORIES_SELECTION_TITLE": "Categoria", "ITEM_CHATBOX_TITLE": "Chatbox", - "ITEM_COPY_BUTTON": "copia", "ITEM_MEMBERSHIP_PERMISSION_LABEL": "Autorizzazione", "ITEM_MEMBERSHIPS_TABLE_ACTIONS_HEADER": "Azioni", "ITEM_MEMBERSHIPS_TABLE_CANNOT_DELETE_PARENT_TOOLTIP": "Questa appartenenza è definita nell'elemento principale e non può essere eliminata qui.", @@ -291,5 +300,6 @@ "SHORT_LINK_MAX_CHARS_ERROR": "Il collegamento breve non deve superare i {{data}} caratteri.", "SHORT_LINK_INVALID_CHARS_ERROR": "Il collegamento breve contiene caratteri non validi ({{data}}).", "SHORT_LINK_UNKNOWN_ERROR": "Si è verificato un errore sconosciuto.", - "You can also find the items of this page in ''My Graasp''. This page will be unavailable soon.": "Puoi anche trovare gli articoli di questa pagina in ''My Graasp''. Questa pagina ''Elementi condivisi'' non sarà più disponibile a breve." + "You can also find the items of this page in ''My Graasp''. This page will be unavailable soon.": "Puoi anche trovare gli articoli di questa pagina in ''My Graasp''. Questa pagina ''Elementi condivisi'' non sarà più disponibile a breve.", + "FILTER_BY_TYPES_LABEL": "Tipo" } diff --git a/src/utils/itemSelection.ts b/src/utils/itemSelection.ts new file mode 100644 index 000000000..1fb282d73 --- /dev/null +++ b/src/utils/itemSelection.ts @@ -0,0 +1,43 @@ +import { DiscriminatedItem, ItemSettings, ResultOf } from '@graasp/sdk'; + +import { ITEM_SELECT_MODAL_TITLE_MAX_NAME_LENGTH } from '@/config/constants'; + +import { applyEllipsisOnLength } from './item'; + +type TFunction = (key: string, params?: { [key: string]: unknown }) => string; + +type TitleProps = { + items?: ResultOf>; + count: number; + translateBuilder: TFunction; + translateKey: string; +}; + +export const computeTitle = ({ + items, + count, + translateBuilder, + translateKey, +}: TitleProps): string => + items && Object.values(items.data ?? {}).length + ? translateBuilder(translateKey, { + name: applyEllipsisOnLength( + Object.values(items.data)[0].name, + ITEM_SELECT_MODAL_TITLE_MAX_NAME_LENGTH, + ), + count, + }) + : translateBuilder(translateKey); + +type ButtonProps = { + translateBuilder: TFunction; + translateKey: string; + name?: string; +}; + +export const computeButtonText = ({ + name, + translateBuilder, + translateKey, +}: ButtonProps): string => + translateBuilder(translateKey, { name, count: name ? 1 : 0 });