Skip to content

Commit

Permalink
feat: let user being able to resize mails list view and change layout…
Browse files Browse the repository at this point in the history
… configuration
  • Loading branch information
nubsthead authored Jul 10, 2024
1 parent b777212 commit 55d294d
Show file tree
Hide file tree
Showing 13 changed files with 1,026 additions and 83 deletions.
2 changes: 2 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
defaultBeforeEachTest,
getFailOnConsoleDefaultConfig
} from './src/carbonio-ui-commons/test/jest-setup';
import { useLocalStorage } from './src/carbonio-ui-commons/test/mocks/carbonio-shell-ui';
import { registerRestHandler } from './src/carbonio-ui-commons/test/mocks/network/msw/handlers';
import { handleGetConvRequest } from './src/tests/mocks/network/msw/handle-get-conv';
import { handleGetMsgRequest } from './src/tests/mocks/network/msw/handle-get-msg';
Expand All @@ -31,6 +32,7 @@ beforeAll(() => {
registerRestHandler(h);
registerRestHandler(j);
defaultBeforeAllTests();
useLocalStorage.mockReturnValue([jest.fn(), jest.fn()]);
});

beforeEach(() => {
Expand Down
21 changes: 21 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ export const NO_ACCOUNT_NAME = 'No account';

export const RECOVER_MESSAGES_INTERVAL = 3;

export const LOCAL_STORAGE_VIEW_SIZES = 'carbonio-mails-ui-list-view-sizes';

export const LOCAL_STORAGE_LAYOUT = 'carbonio-mails-ui-layout';

export const MAILS_VIEW_LAYOUTS = {
LEFT_TO_RIGHT: 'leftToRight',
TOP_TO_BOTTOM: 'topToBottom'
} as const;

export const MAILS_VIEW_ORIENTATIONS = {
VERTICAL: 'vertical',
HORIZONTAL: 'horizontal'
} as const;

export const BORDERS = {
EAST: 'e',
SOUTH: 's',
NORTH: 'n',
WEST: 'w'
} as const;

type AttachmentTypeItemsConstantProps = {
id: string;
label: string;
Expand Down
185 changes: 185 additions & 0 deletions src/hooks/use-resize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* SPDX-FileCopyrightText: 2024 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type React from 'react';
import { CSSProperties, useEffect, useCallback, useRef } from 'react';

import { useLocalStorage } from '@zextras/carbonio-shell-ui';
import { find } from 'lodash';

import { BORDERS } from '../constants';

/**
* Define the border following the cardinal points (north, south, west, east).
* Similar to the definition of the cursor for the pointer
*/
export type Border = 'n' | 's' | 'e' | 'w' | 'ne' | 'sw' | 'nw' | 'se';

export type ElementPosition = {
top: number;
left: number;
};

export type ElementSize = {
width: number;
height: number;
};

export type SizeAndPosition = ElementPosition & ElementSize;
type UseResizableReturnType = React.MouseEventHandler;

type ResizeOptions = {
localStorageKey?: string;
keepSyncedWithStorage?: boolean;
};

export function getCursorFromBorder(border: Border): NonNullable<CSSProperties['cursor']> {
const direction = find(
[
['n', 's'],
['e', 'w'],
['ne', 'sw'],
['nw', 'se']
],
(borders) => borders.includes(border)
)?.join('');
return direction?.concat('-resize') ?? '';
}

function calcNewSizeAndPosition(
border: Border,
from: { clientTop: number; clientLeft: number } & SizeAndPosition,
mouseEvent: MouseEvent
): SizeAndPosition {
const newSizeAndPosition = {
top: from.top,
left: from.left,
height: from.height,
width: from.width
};
if (border.includes('n')) {
const heightDifference = from.clientTop - mouseEvent.clientY;
newSizeAndPosition.height = from.height + heightDifference;
newSizeAndPosition.top = from.top - heightDifference;
}
if (border.includes('s')) {
newSizeAndPosition.height = mouseEvent.clientY - from.clientTop;
}
if (border.includes('e')) {
newSizeAndPosition.width = mouseEvent.clientX - from.clientLeft;
}
if (border.includes('w')) {
const widthDifference = from.clientLeft - mouseEvent.clientX;
newSizeAndPosition.width = from.width + widthDifference;
newSizeAndPosition.left = from.left - widthDifference;
}
return newSizeAndPosition;
}

export function setGlobalCursor(cursor: CSSProperties['cursor']): void {
// remove previously set cursor
const cursors: string[] = [];
document.body.classList.forEach((item) => {
if (item.startsWith('global-cursor-')) {
cursors.push(item);
}
});
document.body.classList.remove(...cursors);
if (cursor) {
document.body.classList.add(`global-cursor-${cursor}`);
}
}

export const useResize = (
elementToResizeRef: React.RefObject<HTMLElement>,
border: Border,
options?: ResizeOptions
): UseResizableReturnType => {
const initialSizeAndPositionRef = useRef<Parameters<typeof calcNewSizeAndPosition>[1]>();
const [lastSavedSizeAndPosition, setLastSavedSizeAndPosition] = useLocalStorage<
Partial<SizeAndPosition>
>(
options?.localStorageKey ?? 'use-resize-data',
{},
{ keepSyncedWithStorage: options?.keepSyncedWithStorage }
);
const lastSizeAndPositionRef = useRef<Partial<SizeAndPosition>>(lastSavedSizeAndPosition);

useEffect(() => {
lastSizeAndPositionRef.current = { ...lastSavedSizeAndPosition };
}, [lastSavedSizeAndPosition]);

const resizeElement = useCallback(
({ width, height, top, left }: SizeAndPosition) => {
if (elementToResizeRef.current) {
const elementToResize = elementToResizeRef.current;
const sizeAndPositionToApply: Partial<SizeAndPosition> = lastSizeAndPositionRef.current;
if (top >= 0 && border === BORDERS.SOUTH) {
sizeAndPositionToApply.height = height;
sizeAndPositionToApply.top = top;
elementToResize.style.height = `${height}px`;
elementToResize.style.top = `${top}px`;
}
if (left >= 0 && border === BORDERS.EAST) {
sizeAndPositionToApply.width = width;
sizeAndPositionToApply.left = left;
elementToResize.style.width = `${width}px`;
elementToResize.style.left = `${left}px`;
}
// reset bottom in favor of top
elementToResize.style.bottom = '';
// reset right in favor of left
elementToResize.style.right = '';
lastSizeAndPositionRef.current = sizeAndPositionToApply;
}
},
[border, elementToResizeRef]
);

const onMouseMove = useCallback(
(mouseMoveEvent: MouseEvent) => {
if (initialSizeAndPositionRef.current) {
const newSizeAndPosition = calcNewSizeAndPosition(
border,
initialSizeAndPositionRef.current,
mouseMoveEvent
);
resizeElement(newSizeAndPosition);
}
},
[border, resizeElement]
);

const onMouseUp = useCallback(() => {
setGlobalCursor(undefined);
document.body.removeEventListener('mousemove', onMouseMove);
document.body.removeEventListener('mouseup', onMouseUp);
if (options?.localStorageKey) {
setLastSavedSizeAndPosition(lastSizeAndPositionRef.current);
}
}, [onMouseMove, options?.localStorageKey, setLastSavedSizeAndPosition]);

return useCallback(
(mouseDownEvent: React.MouseEvent | MouseEvent) => {
if (!mouseDownEvent.defaultPrevented && elementToResizeRef.current) {
mouseDownEvent.preventDefault();
const clientRect = elementToResizeRef.current.getBoundingClientRect();
initialSizeAndPositionRef.current = {
height: initialSizeAndPositionRef?.current?.height ?? 0,
width: elementToResizeRef.current.offsetWidth,
// height: elementToResizeRef.current.offsetHeight,
top: elementToResizeRef.current.offsetTop,
left: elementToResizeRef.current.offsetLeft,
clientTop: clientRect.top,
clientLeft: clientRect.left
};
setGlobalCursor(getCursorFromBorder(border));
document.body.addEventListener('mousemove', onMouseMove);
document.body.addEventListener('mouseup', onMouseUp);
}
},
[border, elementToResizeRef, onMouseMove, onMouseUp]
);
};
53 changes: 29 additions & 24 deletions src/views/app-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,45 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { FC, Suspense, lazy, useEffect, useMemo, useState } from 'react';
import React, { FC, Suspense, lazy, useEffect, useMemo, useState, useRef } from 'react';

import { Container } from '@zextras/carbonio-design-system';
import { FOLDERS, Spinner, setAppContext, useUserSettings } from '@zextras/carbonio-shell-ui';
import {
FOLDERS,
Spinner,
setAppContext,
useUserSettings,
useLocalStorage
} from '@zextras/carbonio-shell-ui';
import { includes } from 'lodash';
import moment from 'moment';
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';

import { FolderView, MailsListLayout } from './folder-view';
import { LayoutSelector } from './layout-selector';
import { LOCAL_STORAGE_LAYOUT, MAILS_VIEW_LAYOUTS } from '../constants';
import { getFolderIdParts } from '../helpers/folders';
import { useAppSelector } from '../hooks/redux';
import { selectCurrentFolder } from '../store/conversations-slice';

const LazyFolderView = lazy(
() => import(/* webpackChunkName: "folder-panel-view" */ './app/folder-panel')
);

const LazyDetailPanel = lazy(
() => import(/* webpackChunkName: "folder-panel-view" */ './app/detail-panel')
);

const DetailPanel = (): React.JSX.Element => (
<Suspense fallback={<Spinner />}>
<LazyDetailPanel />
</Suspense>
);

const AppView: FC = () => {
const { path } = useRouteMatch();
const [count, setCount] = useState(0);
const { zimbraPrefGroupMailBy, zimbraPrefLocale } = useUserSettings().prefs;
const currentFolderId = useAppSelector(selectCurrentFolder);
const containerRef = useRef<HTMLDivElement>(null);

const [listLayout] = useLocalStorage<MailsListLayout>(
LOCAL_STORAGE_LAYOUT,
MAILS_VIEW_LAYOUTS.LEFT_TO_RIGHT
);

const isMessageView = useMemo(
() =>
Expand All @@ -45,21 +59,12 @@ const AppView: FC = () => {
}, [count, isMessageView]);

return (
<Container orientation="horizontal" mainAlignment="flex-start">
<Container width="40%">
<Switch>
<Route path={`${path}/folder/:folderId/:type?/:itemId?`}>
<Suspense fallback={<Spinner />}>
<LazyFolderView />
</Suspense>
</Route>
<Redirect strict from={path} to={`${path}/folder/2`} />
</Switch>
</Container>
<Suspense fallback={<Spinner />}>
<LazyDetailPanel />
</Suspense>
</Container>
<LayoutSelector
listLayout={listLayout}
folderView={<FolderView listLayout={listLayout} containerRef={containerRef} />}
detailPanel={<DetailPanel />}
containerRef={containerRef}
/>
);
};

Expand Down
2 changes: 1 addition & 1 deletion src/views/app/detail-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const DetailPanel: FC = () => {
const { path } = useRouteMatch();
const { count } = useAppContext<AppContext>();
return (
<Container width="60%" data-testid="third-panel">
<Container width="fill" data-testid="third-panel" style={{ overflowY: 'auto' }}>
<Switch>
<Route exact path={`${path}/folder/:folderId`}>
<SelectionInteractive count={count} />
Expand Down
2 changes: 1 addition & 1 deletion src/views/app/folder-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const FolderPanel: FC = () => {
crossAlignment="flex-start"
mainAlignment="flex-start"
width="fill"
background="gray6"
background={'gray6'}
borderRadius="none"
style={{
maxHeight: '100%'
Expand Down
Loading

0 comments on commit 55d294d

Please sign in to comment.