From 56f55fde646ad0932f19b4167154ea09640e63d9 Mon Sep 17 00:00:00 2001 From: davy-c Date: Wed, 19 Jan 2022 10:53:27 +0900 Subject: [PATCH 1/4] routing for doc preview modal --- .../Views/Calendar/CalendarView.tsx | 6 ++-- src/cloud/components/Views/Kanban/index.tsx | 6 ++-- .../components/Views/Table/TableView.tsx | 4 +-- src/cloud/components/Views/index.tsx | 24 ++++++++++++- .../lib/hooks/useCloudResourceModals.tsx | 23 +++++++++++- .../components/organisms/Modal/index.tsx | 35 ++++++++++++++++++- src/design/lib/stores/modal/types.ts | 8 +++++ 7 files changed, 95 insertions(+), 11 deletions(-) diff --git a/src/cloud/components/Views/Calendar/CalendarView.tsx b/src/cloud/components/Views/Calendar/CalendarView.tsx index c9fdbf2b20..eb45432c00 100644 --- a/src/cloud/components/Views/Calendar/CalendarView.tsx +++ b/src/cloud/components/Views/Calendar/CalendarView.tsx @@ -50,7 +50,7 @@ const CalendarView = ({ }: CalendarViewProps) => { const { openNewDocForm } = useCloudResourceModals() const { openContextModal, closeLastModal } = useModal() - const { openDocPreview } = useCloudResourceModals() + const { goToDocPreview } = useCloudResourceModals() const { watchedProp, actionsRef } = useCalendarView({ view, @@ -96,7 +96,7 @@ const CalendarView = ({ extendedProps: { doc, displayedProps, - onClick: () => openDocPreview(doc, team), + onClick: () => goToDocPreview(doc), onContextClick: (event: React.MouseEvent) => openContextModal( event, @@ -115,7 +115,7 @@ const CalendarView = ({ team, view.data.props, watchedProp, - openDocPreview, + goToDocPreview, openContextModal, ]) diff --git a/src/cloud/components/Views/Kanban/index.tsx b/src/cloud/components/Views/Kanban/index.tsx index 92fd0874a7..79496f5992 100644 --- a/src/cloud/components/Views/Kanban/index.tsx +++ b/src/cloud/components/Views/Kanban/index.tsx @@ -69,7 +69,7 @@ const KanbanView = ({ const { openContextModal, closeLastModal } = useModal() const { createDoc } = useCloudApi() const { translate } = useI18n() - const { openDocPreview } = useCloudResourceModals() + const { goToDocPreview } = useCloudResourceModals() const addListRef = useRef(addList) useEffect(() => { @@ -151,13 +151,13 @@ const KanbanView = ({ (doc: SerializedDocWithSupplemental) => { return ( openDocPreview(doc, team)} + onClick={() => goToDocPreview(doc)} doc={doc} displayedProps={view.data.props || {}} /> ) }, - [team, openDocPreview, view.data.props] + [goToDocPreview, view.data.props] ) const renderListFooter = useCallback( diff --git a/src/cloud/components/Views/Table/TableView.tsx b/src/cloud/components/Views/Table/TableView.tsx index 46e895614e..f9d8da1895 100644 --- a/src/cloud/components/Views/Table/TableView.tsx +++ b/src/cloud/components/Views/Table/TableView.tsx @@ -79,7 +79,7 @@ const TableView = ({ const { translate } = useI18n() const { createDoc } = useCloudApi() const { openContextModal, closeLastModal } = useModal() - const { openDocPreview } = useCloudResourceModals() + const { goToDocPreview } = useCloudResourceModals() const { permissions = [] } = usePage() const { @@ -270,7 +270,7 @@ const TableView = ({ children: ( openDocPreview(doc, team)} + labelClick={() => goToDocPreview(doc)} label={getDocTitle(doc, 'Untitled')} icon={ doc.emoji != null diff --git a/src/cloud/components/Views/index.tsx b/src/cloud/components/Views/index.tsx index c4884a176b..38a15c7a9c 100644 --- a/src/cloud/components/Views/index.tsx +++ b/src/cloud/components/Views/index.tsx @@ -19,6 +19,8 @@ import KanbanView from './Kanban' import ListView from './List' import { sortListViewProps } from '../../lib/views/list' import { useRouter } from '../../lib/router' +import { useNav } from '../../lib/stores/nav' +import { useCloudResourceModals } from '../../lib/hooks/useCloudResourceModals' type ViewsManagerProps = { views: SerializedView[] @@ -67,8 +69,9 @@ export const ViewsManager = ({ reset: resetDocsInSelection, }, ] = useSet(new Set()) - const { query, push, pathname } = useRouter() + const { docsMap } = useNav() + const { openDocPreview } = useCloudResourceModals() const currentDocumentsRef = useRef( new Map( @@ -81,6 +84,18 @@ export const ViewsManager = ({ ) ) + const openDocInPreview = useCallback( + (docId: string) => { + const doc = docsMap.get(docId) + if (doc == null) { + return + } + return openDocPreview(doc, team) + }, + [openDocPreview, docsMap, team] + ) + const openDocInPreviewRef = useRef(openDocInPreview) + useEffect(() => { if (!query || typeof query.view !== 'string') { return @@ -95,6 +110,13 @@ export const ViewsManager = ({ } }, [query, views]) + useEffect(() => { + if (typeof query.preview !== 'string') { + return + } + openDocInPreviewRef.current(query.preview) + }, [query.preview]) + useEffect(() => { const newMap = new Map(docs.map((doc) => [doc.id, doc])) const idsToClean: string[] = difference( diff --git a/src/cloud/lib/hooks/useCloudResourceModals.tsx b/src/cloud/lib/hooks/useCloudResourceModals.tsx index 3b0eb95716..1b8d0a763a 100644 --- a/src/cloud/lib/hooks/useCloudResourceModals.tsx +++ b/src/cloud/lib/hooks/useCloudResourceModals.tsx @@ -19,6 +19,7 @@ import { PropData } from '../../interfaces/db/props' import { SerializedTeam } from '../../interfaces/db/team' import { SerializedWorkspace } from '../../interfaces/db/workspace' import { lngKeys } from '../i18n/types' +import { useRouter } from '../router' import { useNav } from '../stores/nav' import { usePage } from '../stores/pageStore' import { resourceDeleteEventEmitter } from '../utils/events' @@ -31,6 +32,7 @@ import { } from '../utils/patterns' import { useCloudApi } from './useCloudApi' import { useI18n } from './useI18n' +import { stringify } from 'querystring' export function useCloudResourceModals() { const { openModal, closeLastModal } = useModal() @@ -48,6 +50,7 @@ export function useCloudResourceModals() { const { translate } = useI18n() const { team } = usePage() const { foldersMap, workspacesMap } = useNav() + const { pathname, push, query } = useRouter() const openWorkspaceCreateForm = useCallback(() => { openModal(, { @@ -414,12 +417,29 @@ export function useCloudResourceModals() { const openDocPreview = useCallback( (doc: SerializedDocWithSupplemental, team: SerializedTeam) => { + const cleanedupQuery = Object.assign({}, query) + delete cleanedupQuery.preview + const fallbackQuery = stringify(cleanedupQuery) + const fallbackUrl = `${pathname}${ + fallbackQuery.trim() !== '' ? `?${fallbackQuery}` : '' + }` return openModal(, { showCloseIcon: false, removePadding: true, + navigation: { + url: `${pathname}?preview=${doc.id}`, + fallbackUrl, + }, }) }, - [openModal] + [openModal, pathname, query] + ) + + const goToDocPreview = useCallback( + (doc: SerializedDocWithSupplemental) => { + return push(`${pathname}?preview=${doc.id}`) + }, + [pathname, push] ) return { @@ -428,6 +448,7 @@ export function useCloudResourceModals() { openNewDocForm, openNewFolderForm, openDocPreview, + goToDocPreview, openRenameFolderForm, openRenameDocForm, deleteFolder, diff --git a/src/design/components/organisms/Modal/index.tsx b/src/design/components/organisms/Modal/index.tsx index a711aa520b..3a45fa9dce 100644 --- a/src/design/components/organisms/Modal/index.tsx +++ b/src/design/components/organisms/Modal/index.tsx @@ -1,4 +1,10 @@ -import React, { CSSProperties, useCallback, useMemo, useRef } from 'react' +import React, { + CSSProperties, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react' import { mdiClose } from '@mdi/js' import cc from 'classcat' import { ModalElement, useModal } from '../../../lib/stores/modal' @@ -10,6 +16,7 @@ import Scroller from '../../atoms/Scroller' import { useWindow } from '../../../lib/stores/window' import { OverlayScrollbarsComponent } from 'overlayscrollbars-react' import { useEffectOnce } from 'react-use' +import { useRouter } from '../../../../cloud/lib/router' const Modal = () => { const { modals, closeLastModal } = useModal() @@ -208,6 +215,8 @@ const ModalItem = ({ modal: ModalElement }) => { const contentRef = useRef(null) + const { push, goBack } = useRouter() + const willUnmountRef = useRef(false) const onScrollClickHandler: React.MouseEventHandler = useCallback( (event) => { @@ -223,6 +232,30 @@ const ModalItem = ({ [closeModal] ) + useEffectOnce(() => { + if (modal.navigation != null) { + push(modal.navigation.url) + } + }) + + useEffect(() => { + return () => { + willUnmountRef.current = true + } + }, []) + + useEffect(() => { + return () => { + if (willUnmountRef.current && modal.navigation != null) { + if (modal.navigation.fallbackUrl != null) { + push(modal.navigation.fallbackUrl) + } else if (goBack != null) { + goBack() + } + } + } + }, [push, goBack, modal.navigation]) + return ( void } @@ -32,6 +36,10 @@ export type ModalOpeningOptions = { width?: 'large' | 'default' | 'small' | 'full' | number hideBackground?: boolean title?: string + navigation?: { + url: string + fallbackUrl?: string + } onClose?: () => void } From c4ba27549c04a47629dd2209f800ffc9c8ac29ba Mon Sep 17 00:00:00 2001 From: davy-c Date: Wed, 19 Jan 2022 14:16:37 +0900 Subject: [PATCH 2/4] feedback for clarity and extraction --- .../components/organisms/Modal/index.tsx | 67 +++++++++---------- src/design/lib/stores/modal/types.ts | 15 ++--- src/lib/hooks.ts | 30 +++++++++ 3 files changed, 70 insertions(+), 42 deletions(-) diff --git a/src/design/components/organisms/Modal/index.tsx b/src/design/components/organisms/Modal/index.tsx index 3a45fa9dce..6c2a8ec4f5 100644 --- a/src/design/components/organisms/Modal/index.tsx +++ b/src/design/components/organisms/Modal/index.tsx @@ -1,13 +1,11 @@ -import React, { - CSSProperties, - useCallback, - useEffect, - useMemo, - useRef, -} from 'react' +import React, { CSSProperties, useCallback, useMemo, useRef } from 'react' import { mdiClose } from '@mdi/js' import cc from 'classcat' -import { ModalElement, useModal } from '../../../lib/stores/modal' +import { + ModalElement, + ModalNavigationProps, + useModal, +} from '../../../lib/stores/modal' import { isActiveElementAnInput } from '../../../lib/dom' import { useGlobalKeyDownHandler } from '../../../lib/keyboard' import styled from '../../../lib/styled' @@ -17,6 +15,7 @@ import { useWindow } from '../../../lib/stores/window' import { OverlayScrollbarsComponent } from 'overlayscrollbars-react' import { useEffectOnce } from 'react-use' import { useRouter } from '../../../../cloud/lib/router' +import { useEffectOnUnmount } from '../../../../lib/hooks' const Modal = () => { const { modals, closeLastModal } = useModal() @@ -215,9 +214,6 @@ const ModalItem = ({ modal: ModalElement }) => { const contentRef = useRef(null) - const { push, goBack } = useRouter() - const willUnmountRef = useRef(false) - const onScrollClickHandler: React.MouseEventHandler = useCallback( (event) => { if ( @@ -232,29 +228,7 @@ const ModalItem = ({ [closeModal] ) - useEffectOnce(() => { - if (modal.navigation != null) { - push(modal.navigation.url) - } - }) - - useEffect(() => { - return () => { - willUnmountRef.current = true - } - }, []) - - useEffect(() => { - return () => { - if (willUnmountRef.current && modal.navigation != null) { - if (modal.navigation.fallbackUrl != null) { - push(modal.navigation.fallbackUrl) - } else if (goBack != null) { - goBack() - } - } - } - }, [push, goBack, modal.navigation]) + useModalNavigationHistory(modal.navigation) return ( { + if (navigation == null) { + return + } + push(navigation.url) + }) + + //on modal's closure, goes back to wanted URL + useEffectOnUnmount(() => { + if (navigation == null) { + return + } + + if (navigation.fallbackUrl != null) { + push(navigation.fallbackUrl) + } else if (goBack != null) { + goBack() + } + }) +} + export const zIndexModals = 8001 const Container = styled.div` z-index: ${zIndexModals}; diff --git a/src/design/lib/stores/modal/types.ts b/src/design/lib/stores/modal/types.ts index 3d95073e0d..71e2253c25 100644 --- a/src/design/lib/stores/modal/types.ts +++ b/src/design/lib/stores/modal/types.ts @@ -16,13 +16,15 @@ export interface ModalElement { hideBackground?: boolean removePadding?: boolean onBlur?: boolean - navigation?: { - url: string - fallbackUrl?: string - } + navigation?: ModalNavigationProps onClose?: () => void } +export type ModalNavigationProps = { + url: string + fallbackUrl?: string +} + export type ContextModalAlignment = | 'bottom-left' | 'bottom-right' @@ -36,10 +38,7 @@ export type ModalOpeningOptions = { width?: 'large' | 'default' | 'small' | 'full' | number hideBackground?: boolean title?: string - navigation?: { - url: string - fallbackUrl?: string - } + navigation?: ModalNavigationProps onClose?: () => void } diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts index 9301c783e5..9c65b11874 100644 --- a/src/lib/hooks.ts +++ b/src/lib/hooks.ts @@ -33,3 +33,33 @@ export function usePrevious(value: S) { }, [value]) return ref.current } + +export function useWillUnmountRef() { + const willUnmountRef = useRef(false) + + useEffect(() => { + return () => { + willUnmountRef.current = true + } + }, []) + + return { + willUnmountRef, + } +} + +export function useEffectOnUnmount( + callback: (() => () => void) | (() => void) +) { + const { willUnmountRef } = useWillUnmountRef() + + useEffect(() => { + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + if (!willUnmountRef.current) { + return + } + callback() + } + }, [callback, willUnmountRef]) +} From 1f003bdb77a824d0d545298aedf6ce307b12f882 Mon Sep 17 00:00:00 2001 From: davy-c Date: Thu, 20 Jan 2022 10:42:34 +0900 Subject: [PATCH 3/4] ref for manual closure --- .../components/organisms/Modal/index.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/design/components/organisms/Modal/index.tsx b/src/design/components/organisms/Modal/index.tsx index 6c2a8ec4f5..39c1bde4bd 100644 --- a/src/design/components/organisms/Modal/index.tsx +++ b/src/design/components/organisms/Modal/index.tsx @@ -214,6 +214,12 @@ const ModalItem = ({ modal: ModalElement }) => { const contentRef = useRef(null) + const manualClosing = useRef(false) + + const closing = useCallback(() => { + manualClosing.current = true + closeModal() + }, [closeModal]) const onScrollClickHandler: React.MouseEventHandler = useCallback( (event) => { if ( @@ -222,13 +228,12 @@ const ModalItem = ({ ) { return } - - closeModal() + closing() }, - [closeModal] + [closing] ) - useModalNavigationHistory(modal.navigation) + useModalNavigationHistory(manualClosing, modal.navigation) return ( @@ -265,7 +270,10 @@ const ModalItem = ({ ) } -function useModalNavigationHistory(navigation?: ModalNavigationProps) { +function useModalNavigationHistory( + manualClosing: React.MutableRefObject, + navigation?: ModalNavigationProps +) { const { push, goBack } = useRouter() //push modal's url to history on load @@ -278,7 +286,7 @@ function useModalNavigationHistory(navigation?: ModalNavigationProps) { //on modal's closure, goes back to wanted URL useEffectOnUnmount(() => { - if (navigation == null) { + if (!manualClosing.current || navigation == null) { return } From 1e662b4b12055c9b108de7e9bb9abb0408d31b8f Mon Sep 17 00:00:00 2001 From: davy-c Date: Thu, 20 Jan 2022 12:22:35 +0900 Subject: [PATCH 4/4] add query safecases --- src/cloud/components/DashboardPage/index.tsx | 3 ++ src/cloud/components/DocPreview/index.tsx | 22 ++++++++++- src/cloud/components/Views/index.tsx | 26 ++----------- src/cloud/lib/hooks/useCloudDocPreview.tsx | 38 +++++++++++++++++++ src/cloud/lib/utils/events.ts | 8 ++++ .../components/organisms/Modal/index.tsx | 1 + 6 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 src/cloud/lib/hooks/useCloudDocPreview.tsx diff --git a/src/cloud/components/DashboardPage/index.tsx b/src/cloud/components/DashboardPage/index.tsx index e6e2fd0139..a9abe6ec11 100644 --- a/src/cloud/components/DashboardPage/index.tsx +++ b/src/cloud/components/DashboardPage/index.tsx @@ -31,6 +31,7 @@ import { import UnlockDashboardModal from '../Modal/contents/Subscription/UnlockDashboardModal' import { useNav } from '../../lib/stores/nav' import DashboardSubscriptionBanner from './DashboardSubscriptionBanner' +import { useCloudDocPreview } from '../../lib/hooks/useCloudDocPreview' const DashboardPage = ({ dashboard: propsDashboard, @@ -88,6 +89,8 @@ const DashboardPage = ({ openModal, ]) + useCloudDocPreview(propsTeam) + const renderSmartview = useCallback( (smartview: SerializedSmartView) => { return ( diff --git a/src/cloud/components/DocPreview/index.tsx b/src/cloud/components/DocPreview/index.tsx index 28e317347a..6b0ed69781 100644 --- a/src/cloud/components/DocPreview/index.tsx +++ b/src/cloud/components/DocPreview/index.tsx @@ -1,5 +1,5 @@ import { mdiArrowExpand, mdiClose, mdiPencil } from '@mdi/js' -import React from 'react' +import React, { useEffect } from 'react' import { useState } from 'react' import { useCallback } from 'react' import { useMemo } from 'react' @@ -14,10 +14,12 @@ import { overflowEllipsis } from '../../../design/lib/styled/styleFunctions' import { getDocCollaborationToken } from '../../api/docs/token' import { SerializedDocWithSupplemental } from '../../interfaces/db/doc' import { SerializedTeam } from '../../interfaces/db/team' +import { docPreviewCloseEvent } from '../../lib/hooks/useCloudDocPreview' import { useRouter } from '../../lib/router' import { useGlobalData } from '../../lib/stores/globalData' import { useNav } from '../../lib/stores/nav' import { usePage } from '../../lib/stores/pageStore' +import { ModalEventDetails, modalEventEmitter } from '../../lib/utils/events' import { getDocTitle } from '../../lib/utils/patterns' import DocProperties from '../DocProperties' import { getDocLinkHref } from '../Link/DocLink' @@ -67,6 +69,24 @@ const DocPreviewModal = ({ doc, team }: DocPreviewModalProps) => { return closeLastModal() }, [push, closeLastModal, team, doc]) + const closePreviewModal = useCallback( + (event: CustomEvent) => { + if (event.detail.type !== docPreviewCloseEvent) { + return + } + + closeLastModal() + }, + [closeLastModal] + ) + + useEffect(() => { + modalEventEmitter.listen(closePreviewModal) + return () => { + modalEventEmitter.unlisten(closePreviewModal) + } + }, [closePreviewModal]) + if (currentDoc == null) { return ( diff --git a/src/cloud/components/Views/index.tsx b/src/cloud/components/Views/index.tsx index 38a15c7a9c..07079e6dc0 100644 --- a/src/cloud/components/Views/index.tsx +++ b/src/cloud/components/Views/index.tsx @@ -19,8 +19,7 @@ import KanbanView from './Kanban' import ListView from './List' import { sortListViewProps } from '../../lib/views/list' import { useRouter } from '../../lib/router' -import { useNav } from '../../lib/stores/nav' -import { useCloudResourceModals } from '../../lib/hooks/useCloudResourceModals' +import { useCloudDocPreview } from '../../lib/hooks/useCloudDocPreview' type ViewsManagerProps = { views: SerializedView[] @@ -70,8 +69,8 @@ export const ViewsManager = ({ }, ] = useSet(new Set()) const { query, push, pathname } = useRouter() - const { docsMap } = useNav() - const { openDocPreview } = useCloudResourceModals() + + useCloudDocPreview(team) const currentDocumentsRef = useRef( new Map( @@ -84,18 +83,6 @@ export const ViewsManager = ({ ) ) - const openDocInPreview = useCallback( - (docId: string) => { - const doc = docsMap.get(docId) - if (doc == null) { - return - } - return openDocPreview(doc, team) - }, - [openDocPreview, docsMap, team] - ) - const openDocInPreviewRef = useRef(openDocInPreview) - useEffect(() => { if (!query || typeof query.view !== 'string') { return @@ -110,13 +97,6 @@ export const ViewsManager = ({ } }, [query, views]) - useEffect(() => { - if (typeof query.preview !== 'string') { - return - } - openDocInPreviewRef.current(query.preview) - }, [query.preview]) - useEffect(() => { const newMap = new Map(docs.map((doc) => [doc.id, doc])) const idsToClean: string[] = difference( diff --git a/src/cloud/lib/hooks/useCloudDocPreview.tsx b/src/cloud/lib/hooks/useCloudDocPreview.tsx new file mode 100644 index 0000000000..ce915fb9b1 --- /dev/null +++ b/src/cloud/lib/hooks/useCloudDocPreview.tsx @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useRef } from 'react' +import { SerializedTeam } from '../../interfaces/db/team' +import { useRouter } from '../router' +import { useNav } from '../stores/nav' +import { modalEventEmitter } from '../utils/events' +import { useCloudResourceModals } from './useCloudResourceModals' + +export const docPreviewCloseEvent = 'doc-preview-close' + +export function useCloudDocPreview(team: SerializedTeam) { + const { query } = useRouter() + const { openDocPreview } = useCloudResourceModals() + const { docsMap } = useNav() + const prevPreviewRef = useRef('') + + const openDocInPreview = useCallback( + (docId: string) => { + const doc = docsMap.get(docId) + if (doc == null) { + return modalEventEmitter.dispatch({ type: docPreviewCloseEvent }) + } + return openDocPreview(doc, team) + }, + [openDocPreview, docsMap, team] + ) + const openDocInPreviewRef = useRef(openDocInPreview) + + useEffect(() => { + if ( + typeof query.preview !== 'string' || + query.preview === prevPreviewRef.current + ) { + return + } + prevPreviewRef.current = query.preview + openDocInPreviewRef.current(query.preview) + }, [query.preview]) +} diff --git a/src/cloud/lib/utils/events.ts b/src/cloud/lib/utils/events.ts index c63ad3d43a..78f19b1d59 100644 --- a/src/cloud/lib/utils/events.ts +++ b/src/cloud/lib/utils/events.ts @@ -116,3 +116,11 @@ export type SignInViaAccessTokenDetails = { export const signInViaAccessTokenEventEmitter = createCustomEventEmitter< SignInViaAccessTokenDetails >('sign-in-via-access-token') + +export const modalEventEmitter = createCustomEventEmitter( + 'modal' +) + +export type ModalEventDetails = { + type: string +} diff --git a/src/design/components/organisms/Modal/index.tsx b/src/design/components/organisms/Modal/index.tsx index 39c1bde4bd..13116c17d3 100644 --- a/src/design/components/organisms/Modal/index.tsx +++ b/src/design/components/organisms/Modal/index.tsx @@ -220,6 +220,7 @@ const ModalItem = ({ manualClosing.current = true closeModal() }, [closeModal]) + const onScrollClickHandler: React.MouseEventHandler = useCallback( (event) => { if (