From a1a9384c0271c68d7644a75c221c5979d713e7ba Mon Sep 17 00:00:00 2001 From: davy-c Date: Mon, 29 Nov 2021 09:38:23 +0900 Subject: [PATCH 01/65] rework structure of basic components --- src/cloud/components/DashboardPage/index.tsx | 4 +- src/cloud/components/FolderPage/index.tsx | 4 +- src/cloud/components/Views/Manager.tsx | 213 ++++++ .../components/Views/Table/TableView.tsx | 449 ++++++++++++- .../Views/Table/TableViewContentManager.tsx | 622 ------------------ .../components/Views/ViewsFolderList.tsx | 133 ++++ src/cloud/components/Views/ViewsSelector.tsx | 4 +- src/cloud/components/Views/index.tsx | 124 +--- src/cloud/components/WorkspacePage/index.tsx | 4 +- 9 files changed, 776 insertions(+), 781 deletions(-) create mode 100644 src/cloud/components/Views/Manager.tsx delete mode 100644 src/cloud/components/Views/Table/TableViewContentManager.tsx create mode 100644 src/cloud/components/Views/ViewsFolderList.tsx diff --git a/src/cloud/components/DashboardPage/index.tsx b/src/cloud/components/DashboardPage/index.tsx index dde99de9f6..de2178ddb4 100644 --- a/src/cloud/components/DashboardPage/index.tsx +++ b/src/cloud/components/DashboardPage/index.tsx @@ -19,7 +19,6 @@ import ApplicationTopbar from '../ApplicationTopbar' import SmartViewFolderContextMenu from '../SmartViewContextMenu' import CreateSmartViewModal from '../Modal/contents/SmartView/CreateSmartViewModal' import UpdateSmartViewModal from '../Modal/contents/SmartView/UpdateSmartViewModal' -import Views from '../Views' import { getSmartViewListPageData, SmartViewListPageResponseBody, @@ -27,6 +26,7 @@ import { import { getDefaultTableView } from '../../lib/views/table' import { trackEvent } from '../../api/track' import { MixpanelActionTrackTypes } from '../../interfaces/analytics/mixpanel' +import { ViewsManager } from '../Views' const SmartViewPage = ({ data, @@ -182,7 +182,7 @@ const SmartViewPage = ({ Add filter - { const { pageFolder, team, currentUserIsCoreMember, pageData } = usePage() @@ -247,7 +247,7 @@ const FolderPage = () => { - + docs: SerializedDocWithSupplemental[] + folders?: SerializedFolderWithBookmark[] + currentUserIsCoreMember: boolean + currentWorkspaceId?: string + currentFolderId?: string + team: SerializedTeam +} + +export const ViewsManager = ({ + views, + parent, + team, + docs, + folders, + currentUserIsCoreMember, + currentFolderId, + currentWorkspaceId, + workspacesMap, +}: ViewsManagerProps) => { + const [selectedViewId, setSelectedViewId] = useState(() => + views.length > 0 ? views[0].id : undefined + ) + const [updating, setUpdating] = useState([]) + const { createViewApi } = useCloudApi() + const [ + selectedFolderSet, + { + add: addFolderInSelection, + has: hasFolderInSelection, + toggle: toggleFolderInSelection, + reset: resetFoldersInSelection, + remove: removeFolderInSelection, + }, + ] = useSet(new Set()) + const [ + selectedDocSet, + { + add: addDocinSelection, + has: hasDocInSelection, + toggle: toggleDocInSelection, + remove: removeDocInSelection, + reset: resetDocsInSelection, + }, + ] = useSet(new Set()) + + const currentDocumentsRef = useRef( + new Map( + docs.map((doc) => [doc.id, doc]) + ) + ) + const currentFoldersRef = useRef( + new Map( + (folders || []).map((folder) => [folder.id, folder]) + ) + ) + + useEffect(() => { + const newMap = new Map(docs.map((doc) => [doc.id, doc])) + const idsToClean: string[] = difference( + [...currentDocumentsRef.current.keys()], + [...newMap.keys()] + ) + idsToClean.forEach(removeDocInSelection) + currentDocumentsRef.current = newMap + }, [docs, removeDocInSelection]) + + useEffect(() => { + const newMap = new Map((folders || []).map((folder) => [folder.id, folder])) + const idsToClean: string[] = difference( + [...currentFoldersRef.current.keys()], + [...newMap.keys()] + ) + idsToClean.forEach(removeFolderInSelection) + currentFoldersRef.current = newMap + }, [folders, removeFolderInSelection]) + + const currentView = useMemo(() => { + if (selectedViewId == null) { + return undefined + } + + return views.find((view) => view.id === selectedViewId) + }, [selectedViewId, views]) + + const toolbarColumns = useMemo(() => { + if (currentView == null || currentView.type !== 'table') { + return [] + } + + return sortTableViewColumns(currentView.data.columns || {}) + }, [currentView]) + + const selectViewId = useCallback( + (id: number) => { + setSelectedViewId(id) + resetDocsInSelection() + resetFoldersInSelection() + }, + [resetDocsInSelection, resetFoldersInSelection] + ) + + return ( + + + + + + {currentView != null && ( + <> + {currentView.type === 'table' ? ( + + ) : null} + + )} + + +
+ + + {currentUserIsCoreMember && ( + + )} + + ) +} + +const Container = styled.div` + width: 100%; + height: 100%; + overflow: hidden; + position: relative; + + .views__header { + flex: 0 0 auto; + width: 100%; + } + + .view__scroller { + height: 100%; + } + + .views__placeholder { + height: 40px; + width: 100%; + } + + .content__manager__list__header--margin { + margin-top: ${({ theme }) => theme.sizes.spaces.df}px; + } +` diff --git a/src/cloud/components/Views/Table/TableView.tsx b/src/cloud/components/Views/Table/TableView.tsx index 9a26c1d1fe..d16bed06d7 100644 --- a/src/cloud/components/Views/Table/TableView.tsx +++ b/src/cloud/components/Views/Table/TableView.tsx @@ -1,43 +1,102 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import styled from '../../../../design/lib/styled' import { SerializedDocWithSupplemental } from '../../../interfaces/db/doc' import { SerializedView } from '../../../interfaces/db/view' -import { ViewTableData } from '../../../lib/views/table' +import { + isStaticPropCol, + sortTableViewColumns, + ViewTableData, +} from '../../../lib/views/table' import { SerializedTeam } from '../../../interfaces/db/team' -import { SerializedWorkspace } from '../../../interfaces/db/workspace' -import Scroller from '../../../../design/components/atoms/Scroller' import { useTableView } from '../../../lib/hooks/views/tableView' import { SerializedFolderWithBookmark } from '../../../interfaces/db/folder' -import TableViewContentManager from './TableViewContentManager' import { buildSmartViewQueryCheck } from '../../../lib/smartViews' +import { docToDataTransferItem, getDocTitle } from '../../../lib/utils/patterns' +import { + sortByAttributeAsc, + sortByAttributeDesc, +} from '../../../../design/lib/utils/array' +import { useModal } from '../../../../design/lib/stores/modal' +import { useRouter } from '../../../lib/router' +import { usePreferences } from '../../../lib/stores/preferences' +import SortingOption, { + sortingOrders, +} from '../../ContentManager/SortingOption' +import { useCloudDnd } from '../../../lib/hooks/sidebar/useCloudDnd' +import { DraggedTo } from '../../../../design/lib/dnd' +import { FormSelectOption } from '../../../../design/components/molecules/Form/atoms/FormSelect' +import { StyledContentManagerList } from '../../ContentManager/styled' +import Flexbox from '../../../../design/components/atoms/Flexbox' +import Button from '../../../../design/components/atoms/Button' +import TablePropertiesContext from './TablePropertiesContext' +import ColumnSettingsContext from './ColSettingsContext' +import { getDocLinkHref } from '../../Link/DocLink' +import NavigationItem from '../../../../design/components/molecules/Navigation/NavigationItem' +import { mdiFileDocumentOutline } from '@mdi/js' +import DocTagsList from '../../DocPage/DocTagsList' +import { getFormattedBoosthubDateTime } from '../../../lib/date' +import PropPicker from '../../Props/PropPicker' +import TableAddPropertyContext from './TableAddPropertyContext' +import TableViewContentManagerNewDocRow from './TableViewContentManagerNewDocRow' +import EmptyRow from '../../ContentManager/Rows/EmptyRow' +import { + getIconPathOfPropType, + getInitialPropDataOfPropType, +} from '../../../lib/props' +import { overflowEllipsis } from '../../../../design/lib/styled/styleFunctions' +import Table from '../../../../design/components/organisms/Table' +import Icon from '../../../../design/components/atoms/Icon' type TableViewProps = { view: SerializedView docs: SerializedDocWithSupplemental[] folders?: SerializedFolderWithBookmark[] team: SerializedTeam - workspacesMap: Map currentUserIsCoreMember: boolean currentWorkspaceId?: string currentFolderId?: string selectViewId: (viewId: number) => void + addDocInSelection: (key: string) => void + hasDocInSelection: (key: string) => boolean + toggleDocInSelection: (key: string) => void + resetDocsInSelection: () => void } const TableView = ({ view, docs, - workspacesMap, currentUserIsCoreMember, - folders, currentWorkspaceId, currentFolderId, team, selectViewId, + addDocInSelection, + hasDocInSelection, + toggleDocInSelection, + resetDocsInSelection, }: TableViewProps) => { const currentStateRef = useRef(view.data) const [state, setState] = useState( Object.assign({}, view.data as ViewTableData) ) + const { openContextModal, closeAllModals } = useModal() + const { push } = useRouter() + const { preferences, setPreferences } = usePreferences() + const [order, setOrder] = useState( + preferences.folderSortingOrder + ) + + const { + dropInDocOrFolder, + saveDocTransferData, + clearDragTransferData, + } = useCloudDnd() + + const { actionsRef } = useTableView({ + view, + state, + selectNewView: selectViewId, + }) const filteredDocs = useMemo(() => { if (state.filter == null || state.filter.length === 0) { @@ -47,11 +106,74 @@ const TableView = ({ return docs.filter(buildSmartViewQueryCheck(state.filter)) }, [state.filter, docs]) - const { actionsRef } = useTableView({ - view, - state, - selectNewView: selectViewId, - }) + const columns = useMemo(() => { + return view.data.columns || {} + }, [view.data.columns]) + + const orderedColumns = useMemo(() => { + return sortTableViewColumns(columns) + }, [columns]) + + const orderedDocs = useMemo(() => { + const docs = filteredDocs.map((doc) => { + return { + ...doc, + title: getDocTitle(doc, 'untitled'), + } + }) + switch (order) { + case 'Title A-Z': + return sortByAttributeAsc('title', docs) + case 'Title Z-A': + return sortByAttributeDesc('title', docs) + case 'Latest Updated': + default: + return sortByAttributeDesc('updatedAt', docs) + } + }, [order, filteredDocs]) + + const selectingAllDocs = useMemo(() => { + return ( + filteredDocs.length > 0 && + filteredDocs.every((doc) => hasDocInSelection(doc.id)) + ) + }, [filteredDocs, hasDocInSelection]) + + const selectAllDocs = useCallback(() => { + filteredDocs.forEach((doc) => addDocInSelection(doc.id)) + }, [filteredDocs, addDocInSelection]) + + const onDragStartDoc = useCallback( + (event: any, doc: SerializedDocWithSupplemental) => { + saveDocTransferData(event, doc) + }, + [saveDocTransferData] + ) + + const onDropDoc = useCallback( + (event: any, doc: SerializedDocWithSupplemental) => + dropInDocOrFolder( + event, + { type: 'doc', resource: docToDataTransferItem(doc) }, + DraggedTo.beforeItem + ), + [dropInDocOrFolder] + ) + + const onDragEnd = useCallback( + (event: any) => { + clearDragTransferData(event) + }, + [clearDragTransferData] + ) + + const onChangeOrder = useCallback( + (val: FormSelectOption) => { + setOrder(val.value) + setPreferences({ folderSortingOrder: val.value as any }) + }, + [setPreferences] + ) useEffect(() => { currentStateRef.current = Object.assign({}, view.data) @@ -62,31 +184,300 @@ const TableView = ({ }, [view.data]) return ( - - - + +
+ + + + + + + + + Documents, + width: 300, + }, + ...orderedColumns.map((col) => { + const icon = getIconPathOfPropType(col.id.split(':').pop() as any) + return { + id: col.id, + children: ( + + {icon != null && ( + + )} + {col.name} + + ), + width: 200, + onClick: (ev: any) => + openContextModal( + ev, + + actionsRef.current.moveColumn(col, type) + } + close={closeAllModals} + />, + { + width: 250, + hideBackground: true, + removePadding: true, + alignment: 'bottom-left', + } + ), + } + }), + ]} + rows={orderedDocs.map((doc) => { + const docLink = getDocLinkHref(doc, team, 'index') + return { + checked: hasDocInSelection(doc.id), + onCheckboxToggle: () => toggleDocInSelection(doc.id), + onDragStart: (ev) => onDragStartDoc(ev, doc), + onDragEnd: onDragEnd, + onDrop: (ev) => onDropDoc(ev, doc), + cells: [ + { + children: ( + push(docLink)} + label={getDocTitle(doc, 'Untitled')} + icon={ + doc.emoji != null + ? { type: 'emoji', path: doc.emoji } + : { type: 'icon', path: mdiFileDocumentOutline } + } + /> + ), + }, + ...orderedColumns.map((col) => { + if (isStaticPropCol(col)) { + switch (col.prop) { + case 'creation_date': + case 'update_date': + return { + children: ( + + {getFormattedBoosthubDateTime( + doc[ + col.prop === 'creation_date' + ? 'createdAt' + : 'updatedAt' + ] + )} + + ), + } + case 'label': + default: + return { + children: ( + + ), + } + } + } else { + const propType = col.id.split(':').pop() as any + const propName = col.id.split(':')[1] + const propData = + (doc.props || {})[propName] || + getInitialPropDataOfPropType(propType) + + const isPropDataAccurate = + propData.type === propType || + (propData.type === 'json' && + propData.data.dataType === propType) + return { + children: ( + + ), + } + } + }), + ], + } + })} + onAddColButtonClick={(ev) => + openContextModal( + ev, + , + { + width: 250, + hideBackground: true, + removePadding: true, + alignment: 'bottom-left', + } + ) + } + disabledAddRow={true} /> - + {orderedDocs.length === 0 && } + {currentWorkspaceId != null && ( + + )} + ) } const Container = styled.div` + display: block; width: 100%; - height: 100%; + position: relative; + + .table { + flex: 0 0 auto; + } + + .content__manager__list__header--margin { + margin-top: ${({ theme }) => theme.sizes.spaces.l}px !important; + } + + .content__manager--no-border { + border: none; + } - .view__scroller { + .content__manager--no-padding { + padding: 0; + } + + .item__property__button.item__property__button--empty + .item__property__button__label { + display: none; + } + + .property--errored { + justify-content: center; + } + + .react-datepicker-popper { + z-index: 2; + } + + .navigation__item { + height: 100%; + } + + .table__col { + .th__cell { + .th__cell__icon { + margin-right: ${({ theme }) => theme.sizes.spaces.sm}px; + color: ${({ theme }) => theme.colors.text.subtle}; + flex: 0 0 auto; + } + + span { + ${overflowEllipsis()} + } + } + } + + .static__dates { + height: 100%; + justify-content: center; + color: ${({ theme }) => theme.colors.text.subtle}; + } + + .table__row__cell > *, + .table__row__cell .react-datepicker-wrapper, + .table__row__cell .react-datepicker__input-container { + height: 100%; + } + + .doc__tags__icon { + display: none; + } + + .table__col { + min-height: 46px; + } + .table__row__cell { + min-height: 38px; + .item__property__button, + .react-datepicker-wrapper { + width: 100%; + border-radius: 0 !important; + } + .item__property__button { + padding: 8px ${({ theme }) => theme.sizes.spaces.sm}px; + height: 100% !important; + min-height: 30px; + border: 0 !important; + } + + .doc__tags__list__item, + .doc__tags__create:not(.doc__tags__create--empty) { + margin-top: ${({ theme }) => theme.sizes.spaces.xsm}px !important; + margin-bottom: ${({ theme }) => theme.sizes.spaces.xsm}px !important; + } + } + + .doc__tags__wrapper--empty, + .doc__tags__create--empty { height: 100%; - padding: ${({ theme }) => theme.sizes.spaces.xsm}px 0; + margin: 0 !important; + width: 100%; + } + + .sorting-options__select .form__select__single-value { + display: flex; } ` diff --git a/src/cloud/components/Views/Table/TableViewContentManager.tsx b/src/cloud/components/Views/Table/TableViewContentManager.tsx deleted file mode 100644 index 51ec741bd9..0000000000 --- a/src/cloud/components/Views/Table/TableViewContentManager.tsx +++ /dev/null @@ -1,622 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { SerializedDocWithSupplemental } from '../../../interfaces/db/doc' -import { SerializedFolderWithBookmark } from '../../../interfaces/db/folder' -import { useSet } from 'react-use' -import { - sortByAttributeAsc, - sortByAttributeDesc, -} from '../../../lib/utils/array' -import { - docToDataTransferItem, - folderToDataTransferItem, - getDocTitle, - getFolderId, -} from '../../../lib/utils/patterns' -import { SerializedWorkspace } from '../../../interfaces/db/workspace' -import { SerializedTeam } from '../../../interfaces/db/team' -import { difference } from 'ramda' -import styled from '../../../../design/lib/styled' -import { lngKeys } from '../../../lib/i18n/types' -import { useI18n } from '../../../lib/hooks/useI18n' -import Flexbox from '../../../../design/components/atoms/Flexbox' -import { useCloudDnd } from '../../../lib/hooks/sidebar/useCloudDnd' -import { DraggedTo } from '../../../../design/lib/dnd' -import Scroller from '../../../../design/components/atoms/Scroller' -import Table from '../../../../design/components/organisms/Table' -import { isStaticPropCol, sortTableViewColumns } from '../../../lib/views/table' -import { - getIconPathOfPropType, - getInitialPropDataOfPropType, -} from '../../../lib/props' -import Icon from '../../../../design/components/atoms/Icon' -import { useModal } from '../../../../design/lib/stores/modal' -import ColumnSettingsContext from './ColSettingsContext' -import { getDocLinkHref } from '../../Link/DocLink' -import PropPicker from '../../Props/PropPicker' -import DocTagsList from '../../DocPage/DocTagsList' -import { getFormattedBoosthubDateTime } from '../../../lib/date' -import { mdiFileDocumentOutline } from '@mdi/js' -import NavigationItem from '../../../../design/components/molecules/Navigation/NavigationItem' -import TableAddPropertyContext from './TableAddPropertyContext' -import { TableViewActionsRef } from '../../../lib/hooks/views/tableView' -import { SerializedView } from '../../../interfaces/db/view' -import { useRouter } from '../../../lib/router' -import { StyledContentManagerList } from '../../ContentManager/styled' -import EmptyRow from '../../ContentManager/Rows/EmptyRow' -import ContentManagerToolbar from '../../ContentManager/ContentManagerToolbar' -import Button from '../../../../design/components/atoms/Button' -import TablePropertiesContext from './TablePropertiesContext' -import TableViewContentManagerFolderRow from './TableViewContentManagerFolderRow' -import TableViewContentManagerRow from './TableViewContentManagerRow' -import { overflowEllipsis } from '../../../../design/lib/styled/styleFunctions' -import { usePreferences } from '../../../lib/stores/preferences' -import SortingOption, { - sortingOrders, -} from '../../ContentManager/SortingOption' -import { FormSelectOption } from '../../../../design/components/molecules/Form/atoms/FormSelect' -import TableViewContentManagerNewFolderRow from './TableViewContentManagerNewFolderRow' -import TableViewContentManagerNewDocRow from './TableViewContentManagerNewDocRow' - -interface ContentManagerProps { - team: SerializedTeam - documents: SerializedDocWithSupplemental[] - folders?: SerializedFolderWithBookmark[] - workspacesMap: Map - currentUserIsCoreMember: boolean - currentWorkspaceId?: string - currentFolderId?: string - tableActionsRef: TableViewActionsRef - view: SerializedView - page?: string -} - -const TableViewContentManager = ({ - team, - documents, - folders, - workspacesMap, - currentUserIsCoreMember, - view, - tableActionsRef: actionsRef, - currentFolderId, - currentWorkspaceId, -}: ContentManagerProps) => { - const { translate } = useI18n() - const { openContextModal, closeAllModals } = useModal() - const { push } = useRouter() - const { preferences, setPreferences } = usePreferences() - const [order, setOrder] = useState( - preferences.folderSortingOrder - ) - - const [ - selectedFolderSet, - { - add: addFolder, - has: hasFolder, - toggle: toggleFolder, - reset: resetFolders, - remove: removeFolder, - }, - ] = useSet(new Set()) - const [ - selectedDocSet, - { - add: addDoc, - has: hasDoc, - toggle: toggleDoc, - remove: removeDoc, - reset: resetDocs, - }, - ] = useSet(new Set()) - - const currentDocumentsRef = useRef( - new Map( - documents.map((doc) => [doc.id, doc]) - ) - ) - const currentFoldersRef = useRef( - new Map( - (folders || []).map((folder) => [folder.id, folder]) - ) - ) - const [updating, setUpdating] = useState([]) - - useEffect(() => { - const newMap = new Map(documents.map((doc) => [doc.id, doc])) - const idsToClean: string[] = difference( - [...currentDocumentsRef.current.keys()], - [...newMap.keys()] - ) - idsToClean.forEach(removeDoc) - currentDocumentsRef.current = newMap - }, [documents, removeDoc]) - - useEffect(() => { - const newMap = new Map((folders || []).map((folder) => [folder.id, folder])) - const idsToClean: string[] = difference( - [...currentFoldersRef.current.keys()], - [...newMap.keys()] - ) - idsToClean.forEach(removeFolder) - currentFoldersRef.current = newMap - }, [folders, removeFolder]) - - const columns = useMemo(() => { - return view.data.columns || {} - }, [view.data.columns]) - - const orderedColumns = useMemo(() => { - return sortTableViewColumns(columns) - }, [columns]) - - const orderedDocs = useMemo(() => { - const filteredDocs = documents.map((doc) => { - return { - ...doc, - title: getDocTitle(doc, 'untitled'), - } - }) - switch (order) { - case 'Title A-Z': - return sortByAttributeAsc('title', filteredDocs) - case 'Title Z-A': - return sortByAttributeDesc('title', filteredDocs) - case 'Latest Updated': - default: - return sortByAttributeDesc('updatedAt', filteredDocs) - } - }, [order, documents]) - - const orderedFolders = useMemo(() => { - if (folders == null) { - return [] - } - - return sortByAttributeAsc('name', folders) - }, [folders]) - - const selectingAllDocs = useMemo(() => { - return orderedDocs.length > 0 && orderedDocs.every((doc) => hasDoc(doc.id)) - }, [orderedDocs, hasDoc]) - - const selectingAllFolders = useMemo(() => { - return ( - orderedFolders.length > 0 && - orderedFolders.every((folder) => hasFolder(folder.id)) - ) - }, [orderedFolders, hasFolder]) - - const selectAllDocs = useCallback(() => { - orderedDocs.forEach((doc) => addDoc(doc.id)) - }, [orderedDocs, addDoc]) - - const selectAllFolders = useCallback(() => { - orderedFolders.forEach((folder) => addFolder(folder.id)) - }, [orderedFolders, addFolder]) - - const { - dropInDocOrFolder, - saveFolderTransferData, - saveDocTransferData, - clearDragTransferData, - } = useCloudDnd() - - const onDragStartFolder = useCallback( - (event: any, folder: SerializedFolderWithBookmark) => { - saveFolderTransferData(event, folder) - }, - [saveFolderTransferData] - ) - - const onDropFolder = useCallback( - (event, folder: SerializedFolderWithBookmark) => - dropInDocOrFolder( - event, - { type: 'folder', resource: folderToDataTransferItem(folder) }, - DraggedTo.insideFolder - ), - [dropInDocOrFolder] - ) - - const onDragStartDoc = useCallback( - (event: any, doc: SerializedDocWithSupplemental) => { - saveDocTransferData(event, doc) - }, - [saveDocTransferData] - ) - - const onDropDoc = useCallback( - (event: any, doc: SerializedDocWithSupplemental) => - dropInDocOrFolder( - event, - { type: 'doc', resource: docToDataTransferItem(doc) }, - DraggedTo.beforeItem - ), - [dropInDocOrFolder] - ) - - const onDragEnd = useCallback( - (event: any) => { - clearDragTransferData(event) - }, - [clearDragTransferData] - ) - - const onChangeOrder = useCallback( - (val: FormSelectOption) => { - setOrder(val.value) - setPreferences({ folderSortingOrder: val.value as any }) - }, - [setPreferences] - ) - - return ( - - - -
- - - - - - - - -
Documents - ), - width: 300, - }, - ...orderedColumns.map((col) => { - const icon = getIconPathOfPropType( - col.id.split(':').pop() as any - ) - return { - id: col.id, - children: ( - - {icon != null && ( - - )} - {col.name} - - ), - width: 200, - onClick: (ev: any) => - openContextModal( - ev, - - actionsRef.current.moveColumn(col, type) - } - close={closeAllModals} - />, - { - width: 250, - hideBackground: true, - removePadding: true, - alignment: 'bottom-left', - } - ), - } - }), - ]} - rows={orderedDocs.map((doc) => { - const docLink = getDocLinkHref(doc, team, 'index') - return { - checked: hasDoc(doc.id), - onCheckboxToggle: () => toggleDoc(doc.id), - onDragStart: (ev) => onDragStartDoc(ev, doc), - onDragEnd: onDragEnd, - onDrop: (ev) => onDropDoc(ev, doc), - cells: [ - { - children: ( - push(docLink)} - label={getDocTitle(doc, 'Untitled')} - icon={ - doc.emoji != null - ? { type: 'emoji', path: doc.emoji } - : { type: 'icon', path: mdiFileDocumentOutline } - } - /> - ), - }, - ...orderedColumns.map((col) => { - if (isStaticPropCol(col)) { - switch (col.prop) { - case 'creation_date': - case 'update_date': - return { - children: ( - - {getFormattedBoosthubDateTime( - doc[ - col.prop === 'creation_date' - ? 'createdAt' - : 'updatedAt' - ] - )} - - ), - } - case 'label': - default: - return { - children: ( - - ), - } - } - } else { - const propType = col.id.split(':').pop() as any - const propName = col.id.split(':')[1] - const propData = - (doc.props || {})[propName] || - getInitialPropDataOfPropType(propType) - - const isPropDataAccurate = - propData.type === propType || - (propData.type === 'json' && - propData.data.dataType === propType) - return { - children: ( - - ), - } - } - }), - ], - } - })} - onAddColButtonClick={(ev) => - openContextModal( - ev, - , - { - width: 250, - hideBackground: true, - removePadding: true, - alignment: 'bottom-left', - } - ) - } - disabledAddRow={true} - /> - {orderedDocs.length === 0 && } - {currentWorkspaceId != null && ( - - )} - - {folders != null && ( - <> - - - {orderedFolders.map((folder) => ( - toggleFolder(folder.id)} - currentUserIsCoreMember={currentUserIsCoreMember} - onDrop={onDropFolder} - onDragEnd={onDragEnd} - onDragStart={onDragStartFolder} - /> - ))} - - {currentWorkspaceId != null && ( - - )} - - )} - - - - {currentUserIsCoreMember && ( - - )} - - ) -} - -export default React.memo(TableViewContentManager) - -const Container = styled.div` - display: block; - width: 100%; - position: relative; - height: 100%; - - .table { - flex: 0 0 auto; - } - - .content__manager__list__header--margin { - margin-top: ${({ theme }) => theme.sizes.spaces.l}px !important; - } - - .cm__scroller { - height: 100%; - } - - .content__manager--no-border { - border: none; - } - - .content__manager--no-padding { - padding: 0; - } - - .item__property__button.item__property__button--empty - .item__property__button__label { - display: none; - } - - .property--errored { - justify-content: center; - } - - .views__header { - width: 100%; - margin-bottom: ${({ theme }) => theme.sizes.spaces.sm}px; - } - - .react-datepicker-popper { - z-index: 2; - } - - .navigation__item { - height: 100%; - } - - .table__col { - .th__cell { - .th__cell__icon { - margin-right: ${({ theme }) => theme.sizes.spaces.sm}px; - color: ${({ theme }) => theme.colors.text.subtle}; - flex: 0 0 auto; - } - - span { - ${overflowEllipsis()} - } - } - } - - .static__dates { - height: 100%; - justify-content: center; - color: ${({ theme }) => theme.colors.text.subtle}; - } - - .table__row__cell > *, - .table__row__cell .react-datepicker-wrapper, - .table__row__cell .react-datepicker__input-container { - height: 100%; - } - - .doc__tags__icon { - display: none; - } - - .table__col { - min-height: 46px; - } - .table__row__cell { - min-height: 38px; - .item__property__button, - .react-datepicker-wrapper { - width: 100%; - border-radius: 0 !important; - } - .item__property__button { - padding: 8px ${({ theme }) => theme.sizes.spaces.sm}px; - height: 100% !important; - min-height: 30px; - border: 0 !important; - } - - .doc__tags__list__item, - .doc__tags__create:not(.doc__tags__create--empty) { - margin-top: ${({ theme }) => theme.sizes.spaces.xsm}px !important; - margin-bottom: ${({ theme }) => theme.sizes.spaces.xsm}px !important; - } - } - - .doc__tags__wrapper--empty, - .doc__tags__create--empty { - height: 100%; - margin: 0 !important; - width: 100%; - } - - .sorting-options__select .form__select__single-value { - display: flex; - } -` diff --git a/src/cloud/components/Views/ViewsFolderList.tsx b/src/cloud/components/Views/ViewsFolderList.tsx new file mode 100644 index 0000000000..268e9902f3 --- /dev/null +++ b/src/cloud/components/Views/ViewsFolderList.tsx @@ -0,0 +1,133 @@ +import React, { useCallback, useMemo } from 'react' +import { sortByAttributeAsc } from '../../../design/lib/utils/array' +import { SerializedFolderWithBookmark } from '../../interfaces/db/folder' +import { SerializedTeam } from '../../interfaces/db/team' +import { DraggedTo } from '../../lib/dnd' +import { useCloudDnd } from '../../lib/hooks/sidebar/useCloudDnd' +import { useI18n } from '../../lib/hooks/useI18n' +import { lngKeys } from '../../lib/i18n/types' +import { folderToDataTransferItem, getFolderId } from '../../lib/utils/patterns' +import TableViewContentManagerFolderRow from './Table/TableViewContentManagerFolderRow' +import TableViewContentManagerNewFolderRow from './Table/TableViewContentManagerNewFolderRow' +import TableViewContentManagerRow from './Table/TableViewContentManagerRow' + +interface ViewsFolderListProps { + folders?: SerializedFolderWithBookmark[] + currentUserIsCoreMember: boolean + team: SerializedTeam + currentWorkspaceId?: string + currentFolderId?: string + updating: string[] + setUpdating: React.Dispatch> + addFolderInSelection: (key: string) => void + hasFolderInSelection: (key: string) => boolean + toggleFolderInSelection: (key: string) => void + resetFoldersInSelection: () => void +} + +export const ViewsFolderList = ({ + folders, + updating, + team, + currentUserIsCoreMember, + currentFolderId, + currentWorkspaceId, + setUpdating, + addFolderInSelection, + hasFolderInSelection, + toggleFolderInSelection, + resetFoldersInSelection, +}: ViewsFolderListProps) => { + const { translate } = useI18n() + + const { + dropInDocOrFolder, + saveFolderTransferData, + clearDragTransferData, + } = useCloudDnd() + + const orderedFolders = useMemo(() => { + if (folders == null) { + return [] + } + + return sortByAttributeAsc('name', folders) + }, [folders]) + + const selectingAllFolders = useMemo(() => { + return ( + orderedFolders.length > 0 && + orderedFolders.every((folder) => hasFolderInSelection(folder.id)) + ) + }, [orderedFolders, hasFolderInSelection]) + + const selectAllFolders = useCallback(() => { + if (folders == null) { + return + } + folders.forEach((folder) => addFolderInSelection(folder.id)) + }, [folders, addFolderInSelection]) + + const onDragStartFolder = useCallback( + (event: any, folder: SerializedFolderWithBookmark) => { + saveFolderTransferData(event, folder) + }, + [saveFolderTransferData] + ) + + const onDropFolder = useCallback( + (event, folder: SerializedFolderWithBookmark) => + dropInDocOrFolder( + event, + { type: 'folder', resource: folderToDataTransferItem(folder) }, + DraggedTo.insideFolder + ), + [dropInDocOrFolder] + ) + + if (folders == null) { + return null + } + + return ( + <> + + + {orderedFolders.map((folder) => ( + toggleFolderInSelection(folder.id)} + currentUserIsCoreMember={currentUserIsCoreMember} + onDrop={onDropFolder} + onDragEnd={(event) => clearDragTransferData(event)} + onDragStart={onDragStartFolder} + /> + ))} + + {currentWorkspaceId != null && ( + + )} + + ) +} + +export default ViewsFolderList diff --git a/src/cloud/components/Views/ViewsSelector.tsx b/src/cloud/components/Views/ViewsSelector.tsx index d7f276d420..20d82ecf5d 100644 --- a/src/cloud/components/Views/ViewsSelector.tsx +++ b/src/cloud/components/Views/ViewsSelector.tsx @@ -16,7 +16,7 @@ import { export interface ViewsSelectorProps { selectedViewId: number | undefined - setSelectedViewId: React.Dispatch> + setSelectedViewId: (id: number) => void views: SerializedView[] parent: ViewParent createViewApi: (target: CreateViewRequestBody) => Promise @@ -39,6 +39,8 @@ const ViewsSelector = ({ await createViewApi( parent.type === 'folder' ? { folder: parent.target.id, type } + : parent.type === 'workspace' + ? { workspace: parent.target.id, type } : { smartView: parent.target.id, type } ) setSending(false) diff --git a/src/cloud/components/Views/index.tsx b/src/cloud/components/Views/index.tsx index 074a984ef7..fb000a2b99 100644 --- a/src/cloud/components/Views/index.tsx +++ b/src/cloud/components/Views/index.tsx @@ -1,123 +1 @@ -import { isEqual } from 'lodash' -import React, { useEffect, useMemo, useRef, useState } from 'react' -import Flexbox from '../../../design/components/atoms/Flexbox' -import styled from '../../../design/lib/styled' -import { SerializedDocWithSupplemental } from '../../interfaces/db/doc' -import { SerializedFolderWithBookmark } from '../../interfaces/db/folder' -import { SerializedTeam } from '../../interfaces/db/team' -import { SerializedView, ViewParent } from '../../interfaces/db/view' -import { SerializedWorkspace } from '../../interfaces/db/workspace' -import { useCloudApi } from '../../lib/hooks/useCloudApi' -import TableView from './Table/TableView' -import ViewsSelector from './ViewsSelector' - -interface ViewsListProps { - views: SerializedView[] - parent: ViewParent - workspacesMap: Map - docs: SerializedDocWithSupplemental[] - folders?: SerializedFolderWithBookmark[] - currentUserIsCoreMember: boolean - currentWorkspaceId?: string - currentFolderId?: string - team: SerializedTeam -} - -const ViewsList = ({ - views, - parent, - docs, - folders, - workspacesMap, - currentUserIsCoreMember, - currentFolderId, - currentWorkspaceId, - team, -}: ViewsListProps) => { - const targetIdRef = useRef(parent.target.id) - const [selectedViewId, setSelectedViewId] = useState(() => - views.length > 0 ? views[0].id : undefined - ) - const { createViewApi } = useCloudApi() - - const selectedViewIdRef = useRef(selectedViewId) - const viewsIdsRef = useRef(views.map((v) => v.id)) - - useEffect(() => { - selectedViewIdRef.current = selectedViewId - }, [selectedViewId]) - - useEffect(() => { - if (parent.target.id === targetIdRef.current) { - return - } - - targetIdRef.current = parent.target.id - setSelectedViewId(views.length > 0 ? views[0].id : undefined) - }, [parent.target, views]) - - useEffect(() => { - const newViewsIds = views.map((v) => v.id) - if (!isEqual(newViewsIds, viewsIdsRef.current)) { - if ( - selectedViewIdRef.current != null && - !newViewsIds.includes(selectedViewIdRef.current) - ) { - setSelectedViewId(views.length > 0 ? views[0].id : undefined) - } - viewsIdsRef.current = newViewsIds - } - }, [views]) - - const currentView = useMemo(() => { - if (selectedViewId == null) { - return undefined - } - - return views.find((view) => view.id === selectedViewId) - }, [selectedViewId, views]) - - return ( - - {selectedViewId == null && ( - - - - )} - {currentView == null ? null : currentView.type === 'table' ? ( - - ) : null} - - ) -} - -const Container = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - width: 100%; - height: 100%; - - .views__header { - width: 100%; - } -` - -export default ViewsList +export * from './Manager' diff --git a/src/cloud/components/WorkspacePage/index.tsx b/src/cloud/components/WorkspacePage/index.tsx index 217a9f92f6..c0c1489d7f 100644 --- a/src/cloud/components/WorkspacePage/index.tsx +++ b/src/cloud/components/WorkspacePage/index.tsx @@ -14,7 +14,7 @@ import ApplicationTopbar from '../ApplicationTopbar' import ApplicationContent from '../ApplicationContent' import { getDefaultTableView } from '../../lib/views/table' import { getMapValues } from '../../../design/lib/utils/array' -import ViewsList from '../Views' +import { ViewsManager } from '../Views' const WorkspacePage = ({ workspace }: { workspace: SerializedWorkspace }) => { const { team, currentUserIsCoreMember } = usePage() @@ -124,7 +124,7 @@ const WorkspacePage = ({ workspace }: { workspace: SerializedWorkspace }) => { - Date: Mon, 29 Nov 2021 09:39:56 +0900 Subject: [PATCH 02/65] activate view creation --- src/cloud/components/Views/Manager.tsx | 1 + src/cloud/components/Views/ViewsSelector.tsx | 29 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cloud/components/Views/Manager.tsx b/src/cloud/components/Views/Manager.tsx index 8fab31ff53..047a552adf 100644 --- a/src/cloud/components/Views/Manager.tsx +++ b/src/cloud/components/Views/Manager.tsx @@ -133,6 +133,7 @@ export const ViewsManager = ({ views={views} /> + {currentView != null && ( <> {currentView.type === 'table' ? ( diff --git a/src/cloud/components/Views/ViewsSelector.tsx b/src/cloud/components/Views/ViewsSelector.tsx index 20d82ecf5d..32b47cd9b5 100644 --- a/src/cloud/components/Views/ViewsSelector.tsx +++ b/src/cloud/components/Views/ViewsSelector.tsx @@ -64,21 +64,19 @@ const ViewsSelector = ({ {capitalize(view.type)} ))} - {!views.map((view) => view.type).includes('table') && ( - - openContextModal(ev, , { - alignment: 'bottom-left', - width: 300, - }) - } - /> - )} + + openContextModal(ev, , { + alignment: 'bottom-left', + width: 300, + }) + } + /> ) } @@ -88,6 +86,7 @@ const Container = styled.div` flex: 1 1 auto; align-items: center; flex-wrap: wrap; + margin-top: ${({ theme }) => theme.sizes.spaces.df}px; ` const ViewModal = ({ From 53e8257120c0e9f3245acc6e9bc93075999c9e67 Mon Sep 17 00:00:00 2001 From: davy-c Date: Mon, 29 Nov 2021 09:52:34 +0900 Subject: [PATCH 03/65] remove view folder component --- .../TableViewContentManagerFolderRow.tsx | 54 ------------------- .../components/Views/ViewsFolderList.tsx | 41 +++++++------- 2 files changed, 22 insertions(+), 73 deletions(-) delete mode 100644 src/cloud/components/Views/Table/TableViewContentManagerFolderRow.tsx diff --git a/src/cloud/components/Views/Table/TableViewContentManagerFolderRow.tsx b/src/cloud/components/Views/Table/TableViewContentManagerFolderRow.tsx deleted file mode 100644 index 60c67a919b..0000000000 --- a/src/cloud/components/Views/Table/TableViewContentManagerFolderRow.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useMemo } from 'react' -import { SerializedFolderWithBookmark } from '../../../interfaces/db/folder' -import { SerializedTeam } from '../../../interfaces/db/team' -import { getFolderHref } from '../../Link/FolderLink' -import { useRouter } from '../../../lib/router' -import TableViewContentManagerRow from './TableViewContentManagerRow' - -interface ContentManagerFolderRowProps { - team: SerializedTeam - folder: SerializedFolderWithBookmark - updating: boolean - setUpdating: React.Dispatch> - checked?: boolean - onSelect: (val: boolean) => void - currentUserIsCoreMember: boolean - onDragStart: (event: any, folder: SerializedFolderWithBookmark) => void - onDragEnd: (event: any) => void - onDrop: (event: any, folder: SerializedFolderWithBookmark) => void -} - -const ContentmanagerFolderRow = ({ - team, - folder, - checked, - currentUserIsCoreMember, - onSelect, - onDragStart, - onDragEnd, - onDrop, -}: ContentManagerFolderRowProps) => { - const { push } = useRouter() - - const href = useMemo(() => getFolderHref(folder, team, 'index'), [ - folder, - team, - ]) - - return ( - push(href)} - onDragStart={(event: any) => onDragStart(event, folder)} - onDragEnd={(event: any) => onDragEnd(event)} - onDrop={(event: any) => onDrop(event, folder)} - /> - ) -} - -export default React.memo(ContentmanagerFolderRow) diff --git a/src/cloud/components/Views/ViewsFolderList.tsx b/src/cloud/components/Views/ViewsFolderList.tsx index 268e9902f3..ead05b5db7 100644 --- a/src/cloud/components/Views/ViewsFolderList.tsx +++ b/src/cloud/components/Views/ViewsFolderList.tsx @@ -6,8 +6,9 @@ import { DraggedTo } from '../../lib/dnd' import { useCloudDnd } from '../../lib/hooks/sidebar/useCloudDnd' import { useI18n } from '../../lib/hooks/useI18n' import { lngKeys } from '../../lib/i18n/types' -import { folderToDataTransferItem, getFolderId } from '../../lib/utils/patterns' -import TableViewContentManagerFolderRow from './Table/TableViewContentManagerFolderRow' +import { useRouter } from '../../lib/router' +import { folderToDataTransferItem } from '../../lib/utils/patterns' +import { getFolderHref } from '../Link/FolderLink' import TableViewContentManagerNewFolderRow from './Table/TableViewContentManagerNewFolderRow' import TableViewContentManagerRow from './Table/TableViewContentManagerRow' @@ -27,18 +28,17 @@ interface ViewsFolderListProps { export const ViewsFolderList = ({ folders, - updating, team, currentUserIsCoreMember, currentFolderId, currentWorkspaceId, - setUpdating, addFolderInSelection, hasFolderInSelection, toggleFolderInSelection, resetFoldersInSelection, }: ViewsFolderListProps) => { const { translate } = useI18n() + const { push } = useRouter() const { dropInDocOrFolder, @@ -102,21 +102,24 @@ export const ViewsFolderList = ({ className='content__manager__list__header--margin' /> - {orderedFolders.map((folder) => ( - toggleFolderInSelection(folder.id)} - currentUserIsCoreMember={currentUserIsCoreMember} - onDrop={onDropFolder} - onDragEnd={(event) => clearDragTransferData(event)} - onDragStart={onDragStartFolder} - /> - ))} + {orderedFolders.map((folder) => { + const href = getFolderHref(folder, team, 'index') + return ( + toggleFolderInSelection(folder.id)} + showCheckbox={currentUserIsCoreMember} + label={folder.name} + emoji={folder.emoji} + labelHref={href} + labelOnclick={() => push(href)} + onDragStart={(event: any) => onDragStartFolder(event, folder)} + onDragEnd={(event: any) => clearDragTransferData(event)} + onDrop={(event: any) => onDropFolder(event, folder)} + /> + ) + })} {currentWorkspaceId != null && ( Date: Mon, 29 Nov 2021 10:26:32 +0900 Subject: [PATCH 04/65] refactor form toggable input --- .../components/Views/Table/TableView.tsx | 4 - .../TableViewContentManagerNewDocRow.tsx | 101 +++------------- .../TableViewContentManagerNewFolderRow.tsx | 108 ++++-------------- ...nagerRow.tsx => ViewManagerContentRow.tsx} | 16 +-- .../components/Views/ViewsFolderList.tsx | 6 +- .../Form/atoms/FormToggableInput.tsx | 108 ++++++++++++++++++ 6 files changed, 157 insertions(+), 186 deletions(-) rename src/cloud/components/Views/{Table/TableViewContentManagerRow.tsx => ViewManagerContentRow.tsx} (90%) create mode 100644 src/design/components/molecules/Form/atoms/FormToggableInput.tsx diff --git a/src/cloud/components/Views/Table/TableView.tsx b/src/cloud/components/Views/Table/TableView.tsx index d16bed06d7..e52c2fef7b 100644 --- a/src/cloud/components/Views/Table/TableView.tsx +++ b/src/cloud/components/Views/Table/TableView.tsx @@ -394,10 +394,6 @@ const Container = styled.div` border: none; } - .content__manager--no-padding { - padding: 0; - } - .item__property__button.item__property__button--empty .item__property__button__label { display: none; diff --git a/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx b/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx index 3d7fb2e724..e399b6ac3e 100644 --- a/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx +++ b/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx @@ -1,13 +1,11 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react' +import React from 'react' import { mdiPlus } from '@mdi/js' -import Button from '../../../../design/components/atoms/Button' import TableContentManagerRow from './TableContentManagerRow' import { useCloudApi } from '../../../lib/hooks/useCloudApi' import { SerializedTeam } from '../../../interfaces/db/team' -import FormInput from '../../../../design/components/molecules/Form/atoms/FormInput' -import Spinner from '../../../../design/components/atoms/Spinner' import { useI18n } from '../../../lib/hooks/useI18n' import { lngKeys } from '../../../lib/i18n/types' +import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' interface TableViewContentManagerNewFolderRowProps { team: SerializedTeam @@ -24,88 +22,25 @@ const TableViewContentManagerNewDocRow = ({ }: TableViewContentManagerNewFolderRowProps) => { const { translate } = useI18n() const { createDoc } = useCloudApi() - const [formState, setFormState] = useState<'idle' | 'editing' | 'submitting'>( - 'idle' - ) - const [newTitle, setNewTitle] = useState('') - const compositionStateRef = useRef(false) - const newTitleInputRef = useRef(null) - - useEffect(() => { - if (newTitleInputRef.current != null && formState === 'editing') { - newTitleInputRef.current.focus() - } - }, [formState]) - - const createNewDoc = useCallback(async () => { - setFormState('submitting') - if (workspaceId != null) { - await createDoc( - team, - { - title: newTitle, - workspaceId, - parentFolderId: folderId, - }, - { skipRedirect: true } - ) - } - setNewTitle('') - setFormState('idle') - }, [workspaceId, folderId, createDoc, team, newTitle]) - - const cancelEditing = useCallback(() => { - setFormState('idle') - setNewTitle('') - }, []) return ( - {formState === 'idle' ? ( - - ) : formState === 'editing' ? ( - { - setNewTitle(event.target.value) - }} - onCompositionStart={() => { - compositionStateRef.current = true - }} - onCompositionEnd={() => { - compositionStateRef.current = false - if (newTitleInputRef.current != null) { - newTitleInputRef.current.focus() - } - }} - onKeyPress={(event) => { - if (compositionStateRef.current) { - return - } - switch (event.key) { - case 'Escape': - event.preventDefault() - cancelEditing() - return - case 'Enter': - event.preventDefault() - createNewDoc() - return - } - }} - onBlur={cancelEditing} - /> - ) : ( - - )} + + createDoc( + team, + { + title: val, + workspaceId, + parentFolderId: folderId, + }, + { skipRedirect: true } + ) + } + /> ) } diff --git a/src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx b/src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx index 359c892ded..2b92f11bb6 100644 --- a/src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx +++ b/src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx @@ -1,13 +1,11 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react' -import { mdiPlus } from '@mdi/js' -import Button from '../../../../design/components/atoms/Button' +import React from 'react' import TableContentManagerRow from './TableContentManagerRow' import { useCloudApi } from '../../../lib/hooks/useCloudApi' import { SerializedTeam } from '../../../interfaces/db/team' -import FormInput from '../../../../design/components/molecules/Form/atoms/FormInput' -import Spinner from '../../../../design/components/atoms/Spinner' import { useI18n } from '../../../lib/hooks/useI18n' import { lngKeys } from '../../../lib/i18n/types' +import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' +import { mdiPlus } from '@mdi/js' interface TableViewContentManagerNewFolderRowProps { team: SerializedTeam @@ -22,94 +20,28 @@ const TableViewContentManagerNewFolderRow = ({ folderId, className, }: TableViewContentManagerNewFolderRowProps) => { - const [formState, setFormState] = useState<'idle' | 'editing' | 'submitting'>( - 'idle' - ) - const { translate } = useI18n() const { createFolder } = useCloudApi() - const [newName, setNewName] = useState('') - const compositionStateRef = useRef(false) - const newNameInputRef = useRef(null) - - useEffect(() => { - if (newNameInputRef.current != null && formState === 'editing') { - newNameInputRef.current.focus() - } - }, [formState]) - - const createNewFolder = useCallback(async () => { - setFormState('submitting') - if (workspaceId != null) { - await createFolder( - team, - { - folderName: newName, - description: '', - workspaceId: workspaceId, - parentFolderId: folderId, - }, - { skipRedirect: true } - ) - } - setNewName('') - setFormState('idle') - }, [workspaceId, folderId, createFolder, team, newName]) - - const cancelEditing = useCallback(() => { - setFormState('idle') - setNewName('') - }, []) return ( - {formState === 'idle' ? ( - - ) : formState === 'editing' ? ( - { - setNewName(event.target.value) - }} - onCompositionStart={() => { - compositionStateRef.current = true - }} - onCompositionEnd={() => { - compositionStateRef.current = false - if (newNameInputRef.current != null) { - newNameInputRef.current.focus() - } - }} - onKeyPress={(event) => { - if (compositionStateRef.current) { - return - } - switch (event.key) { - case 'Escape': - event.preventDefault() - cancelEditing() - return - case 'Enter': - event.preventDefault() - createNewFolder() - return - } - }} - onBlur={cancelEditing} - /> - ) : ( - - )} + + createFolder( + team, + { + folderName: val, + description: '', + workspaceId: workspaceId, + parentFolderId: folderId, + }, + { skipRedirect: true } + ) + } + /> ) } diff --git a/src/cloud/components/Views/Table/TableViewContentManagerRow.tsx b/src/cloud/components/Views/ViewManagerContentRow.tsx similarity index 90% rename from src/cloud/components/Views/Table/TableViewContentManagerRow.tsx rename to src/cloud/components/Views/ViewManagerContentRow.tsx index 448bdc5c16..cb319a1ff4 100644 --- a/src/cloud/components/Views/Table/TableViewContentManagerRow.tsx +++ b/src/cloud/components/Views/ViewManagerContentRow.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useRef, useState } from 'react' import cc from 'classcat' -import { onDragLeaveCb } from '../../../../design/lib/dnd' -import styled from '../../../../design/lib/styled' -import { AppComponent } from '../../../../design/lib/types' -import EmojiIcon from '../../EmojiIcon' -import Checkbox from '../../../../design/components/molecules/Form/atoms/FormCheckbox' +import { onDragLeaveCb } from '../../../design/lib/dnd' +import styled from '../../../design/lib/styled' +import { AppComponent } from '../../../design/lib/types' +import EmojiIcon from '../EmojiIcon' +import Checkbox from '../../../design/components/molecules/Form/atoms/FormCheckbox' -interface ContentManagerRowProps { +interface ViewManagerContentRowProps { type?: 'header' | 'row' checked?: boolean onSelect: (val: boolean) => void @@ -21,7 +21,7 @@ interface ContentManagerRowProps { onDrop?: (event: any) => void } -const TableViewContentManagerRow: AppComponent = ({ +const ViewManagerContentRow: AppComponent = ({ type = 'row', className, children, @@ -125,7 +125,7 @@ const TableViewContentManagerRow: AppComponent = ({ ) } -export default TableViewContentManagerRow +export default ViewManagerContentRow const rowHeight = 40 const StyledContentManagerRow = styled.div` diff --git a/src/cloud/components/Views/ViewsFolderList.tsx b/src/cloud/components/Views/ViewsFolderList.tsx index ead05b5db7..bf01597753 100644 --- a/src/cloud/components/Views/ViewsFolderList.tsx +++ b/src/cloud/components/Views/ViewsFolderList.tsx @@ -10,7 +10,7 @@ import { useRouter } from '../../lib/router' import { folderToDataTransferItem } from '../../lib/utils/patterns' import { getFolderHref } from '../Link/FolderLink' import TableViewContentManagerNewFolderRow from './Table/TableViewContentManagerNewFolderRow' -import TableViewContentManagerRow from './Table/TableViewContentManagerRow' +import ViewManagerContentRow from './ViewManagerContentRow' interface ViewsFolderListProps { folders?: SerializedFolderWithBookmark[] @@ -91,7 +91,7 @@ export const ViewsFolderList = ({ return ( <> - { const href = getFolderHref(folder, team, 'index') return ( - toggleFolderInSelection(folder.id)} diff --git a/src/design/components/molecules/Form/atoms/FormToggableInput.tsx b/src/design/components/molecules/Form/atoms/FormToggableInput.tsx new file mode 100644 index 0000000000..45bdffd85f --- /dev/null +++ b/src/design/components/molecules/Form/atoms/FormToggableInput.tsx @@ -0,0 +1,108 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react' +import FormInput from './FormInput' +import Spinner from '../../../atoms/Spinner' +import Button, { ButtonVariant } from '../../../atoms/Button' +import cc from 'classcat' + +type FormToggableInputProps = { + className?: string + variant?: ButtonVariant + label: string + labelAsDefaultInputValue?: boolean + iconPath?: string + submit: (val: string) => Promise +} + +const FormToggableInput = ({ + className, + variant = 'transparent', + iconPath, + label, + labelAsDefaultInputValue = false, + submit, +}: FormToggableInputProps) => { + const [formState, setFormState] = useState<'idle' | 'editing' | 'submitting'>( + 'idle' + ) + const [value, setValue] = useState('') + const compositionStateRef = useRef(false) + const inputRef = useRef(null) + + useEffect(() => { + if (inputRef.current != null && formState === 'editing') { + inputRef.current.focus() + } + }, [formState]) + + const cancelEditing = useCallback(() => { + setFormState('idle') + setValue('') + }, []) + + const submitValue = useCallback( + async (value: string) => { + setFormState('submitting') + await submit(value) + setValue('') + setFormState('idle') + }, + [submit] + ) + + return ( +
+ {formState === 'idle' ? ( + + ) : formState === 'editing' ? ( + { + setValue(event.target.value) + }} + onCompositionStart={() => { + compositionStateRef.current = true + }} + onCompositionEnd={() => { + compositionStateRef.current = false + if (inputRef.current != null) { + inputRef.current.focus() + } + }} + onKeyPress={(event) => { + if (compositionStateRef.current) { + return + } + switch (event.key) { + case 'Escape': + event.preventDefault() + cancelEditing() + return + case 'Enter': + event.preventDefault() + submitValue(value) + return + } + }} + onBlur={cancelEditing} + /> + ) : ( + + )} +
+ ) +} + +export default FormToggableInput From a2f8fc87df8f5fa6801433b009c5c2ec7731999b Mon Sep 17 00:00:00 2001 From: davy-c Date: Mon, 29 Nov 2021 10:30:43 +0900 Subject: [PATCH 05/65] remove new folder content row --- .../TableViewContentManagerNewDocRow.tsx | 6 +-- .../TableViewContentManagerNewFolderRow.tsx | 49 ------------------- ...ntentManagerRow.tsx => ViewManagerRow.tsx} | 11 ++--- .../components/Views/ViewsFolderList.tsx | 31 +++++++++--- 4 files changed, 31 insertions(+), 66 deletions(-) delete mode 100644 src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx rename src/cloud/components/Views/{Table/TableContentManagerRow.tsx => ViewManagerRow.tsx} (61%) diff --git a/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx b/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx index e399b6ac3e..9c15617872 100644 --- a/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx +++ b/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx @@ -1,6 +1,6 @@ import React from 'react' import { mdiPlus } from '@mdi/js' -import TableContentManagerRow from './TableContentManagerRow' +import ViewManagerRow from '../ViewManagerRow' import { useCloudApi } from '../../../lib/hooks/useCloudApi' import { SerializedTeam } from '../../../interfaces/db/team' import { useI18n } from '../../../lib/hooks/useI18n' @@ -24,7 +24,7 @@ const TableViewContentManagerNewDocRow = ({ const { createDoc } = useCloudApi() return ( - + - + ) } diff --git a/src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx b/src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx deleted file mode 100644 index 2b92f11bb6..0000000000 --- a/src/cloud/components/Views/Table/TableViewContentManagerNewFolderRow.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import TableContentManagerRow from './TableContentManagerRow' -import { useCloudApi } from '../../../lib/hooks/useCloudApi' -import { SerializedTeam } from '../../../interfaces/db/team' -import { useI18n } from '../../../lib/hooks/useI18n' -import { lngKeys } from '../../../lib/i18n/types' -import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' -import { mdiPlus } from '@mdi/js' - -interface TableViewContentManagerNewFolderRowProps { - team: SerializedTeam - workspaceId: string - folderId?: string - className?: string -} - -const TableViewContentManagerNewFolderRow = ({ - team, - workspaceId, - folderId, - className, -}: TableViewContentManagerNewFolderRowProps) => { - const { translate } = useI18n() - const { createFolder } = useCloudApi() - - return ( - - - createFolder( - team, - { - folderName: val, - description: '', - workspaceId: workspaceId, - parentFolderId: folderId, - }, - { skipRedirect: true } - ) - } - /> - - ) -} - -export default TableViewContentManagerNewFolderRow diff --git a/src/cloud/components/Views/Table/TableContentManagerRow.tsx b/src/cloud/components/Views/ViewManagerRow.tsx similarity index 61% rename from src/cloud/components/Views/Table/TableContentManagerRow.tsx rename to src/cloud/components/Views/ViewManagerRow.tsx index 89df3ce594..1cd6af5e8c 100644 --- a/src/cloud/components/Views/Table/TableContentManagerRow.tsx +++ b/src/cloud/components/Views/ViewManagerRow.tsx @@ -1,18 +1,15 @@ import React from 'react' -import styled from '../../../../design/lib/styled' +import styled from '../../../design/lib/styled' -type TableContentManagerRowProps = React.PropsWithChildren<{ +type ViewManagerRowProps = React.PropsWithChildren<{ className?: string }> -const TableContentManagerRow = ({ - className, - children, -}: TableContentManagerRowProps) => { +const ViewManagerRow = ({ className, children }: ViewManagerRowProps) => { return {children} } -export default TableContentManagerRow +export default ViewManagerRow const Container = styled.div` height: 40px; diff --git a/src/cloud/components/Views/ViewsFolderList.tsx b/src/cloud/components/Views/ViewsFolderList.tsx index bf01597753..f650cf3e81 100644 --- a/src/cloud/components/Views/ViewsFolderList.tsx +++ b/src/cloud/components/Views/ViewsFolderList.tsx @@ -1,16 +1,19 @@ +import { mdiPlus } from '@mdi/js' import React, { useCallback, useMemo } from 'react' +import FormToggableInput from '../../../design/components/molecules/Form/atoms/FormToggableInput' import { sortByAttributeAsc } from '../../../design/lib/utils/array' import { SerializedFolderWithBookmark } from '../../interfaces/db/folder' import { SerializedTeam } from '../../interfaces/db/team' import { DraggedTo } from '../../lib/dnd' import { useCloudDnd } from '../../lib/hooks/sidebar/useCloudDnd' +import { useCloudApi } from '../../lib/hooks/useCloudApi' import { useI18n } from '../../lib/hooks/useI18n' import { lngKeys } from '../../lib/i18n/types' import { useRouter } from '../../lib/router' import { folderToDataTransferItem } from '../../lib/utils/patterns' import { getFolderHref } from '../Link/FolderLink' -import TableViewContentManagerNewFolderRow from './Table/TableViewContentManagerNewFolderRow' import ViewManagerContentRow from './ViewManagerContentRow' +import ViewManagerRow from './ViewManagerRow' interface ViewsFolderListProps { folders?: SerializedFolderWithBookmark[] @@ -39,6 +42,7 @@ export const ViewsFolderList = ({ }: ViewsFolderListProps) => { const { translate } = useI18n() const { push } = useRouter() + const { createFolder } = useCloudApi() const { dropInDocOrFolder, @@ -122,12 +126,25 @@ export const ViewsFolderList = ({ })} {currentWorkspaceId != null && ( - + + + createFolder( + team, + { + folderName: val, + description: '', + workspaceId: currentWorkspaceId, + parentFolderId: currentFolderId, + }, + { skipRedirect: true } + ) + } + /> + )} ) From 4729f952385c750fb6419176f34d68c6b4d5c0ea Mon Sep 17 00:00:00 2001 From: davy-c Date: Mon, 29 Nov 2021 10:32:22 +0900 Subject: [PATCH 06/65] remove new doc content row --- .../components/Views/Table/TableView.tsx | 37 +++++++++----- .../TableViewContentManagerNewDocRow.tsx | 48 ------------------- 2 files changed, 26 insertions(+), 59 deletions(-) delete mode 100644 src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx diff --git a/src/cloud/components/Views/Table/TableView.tsx b/src/cloud/components/Views/Table/TableView.tsx index e52c2fef7b..4915372bf1 100644 --- a/src/cloud/components/Views/Table/TableView.tsx +++ b/src/cloud/components/Views/Table/TableView.tsx @@ -32,12 +32,11 @@ import TablePropertiesContext from './TablePropertiesContext' import ColumnSettingsContext from './ColSettingsContext' import { getDocLinkHref } from '../../Link/DocLink' import NavigationItem from '../../../../design/components/molecules/Navigation/NavigationItem' -import { mdiFileDocumentOutline } from '@mdi/js' +import { mdiFileDocumentOutline, mdiPlus } from '@mdi/js' import DocTagsList from '../../DocPage/DocTagsList' import { getFormattedBoosthubDateTime } from '../../../lib/date' import PropPicker from '../../Props/PropPicker' import TableAddPropertyContext from './TableAddPropertyContext' -import TableViewContentManagerNewDocRow from './TableViewContentManagerNewDocRow' import EmptyRow from '../../ContentManager/Rows/EmptyRow' import { getIconPathOfPropType, @@ -46,6 +45,11 @@ import { import { overflowEllipsis } from '../../../../design/lib/styled/styleFunctions' import Table from '../../../../design/components/organisms/Table' import Icon from '../../../../design/components/atoms/Icon' +import ViewManagerRow from '../ViewManagerRow' +import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' +import { lngKeys } from '../../../lib/i18n/types' +import { useI18n } from '../../../lib/hooks/useI18n' +import { useCloudApi } from '../../../lib/hooks/useCloudApi' type TableViewProps = { view: SerializedView @@ -79,6 +83,8 @@ const TableView = ({ const [state, setState] = useState( Object.assign({}, view.data as ViewTableData) ) + const { translate } = useI18n() + const { createDoc } = useCloudApi() const { openContextModal, closeAllModals } = useModal() const { push } = useRouter() const { preferences, setPreferences } = usePreferences() @@ -366,11 +372,24 @@ const TableView = ({ /> {orderedDocs.length === 0 && } {currentWorkspaceId != null && ( - + + + createDoc( + team, + { + title: val, + workspaceId: currentWorkspaceId, + parentFolderId: currentFolderId, + }, + { skipRedirect: true } + ) + } + /> + )} @@ -390,10 +409,6 @@ const Container = styled.div` margin-top: ${({ theme }) => theme.sizes.spaces.l}px !important; } - .content__manager--no-border { - border: none; - } - .item__property__button.item__property__button--empty .item__property__button__label { display: none; diff --git a/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx b/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx deleted file mode 100644 index 9c15617872..0000000000 --- a/src/cloud/components/Views/Table/TableViewContentManagerNewDocRow.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react' -import { mdiPlus } from '@mdi/js' -import ViewManagerRow from '../ViewManagerRow' -import { useCloudApi } from '../../../lib/hooks/useCloudApi' -import { SerializedTeam } from '../../../interfaces/db/team' -import { useI18n } from '../../../lib/hooks/useI18n' -import { lngKeys } from '../../../lib/i18n/types' -import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' - -interface TableViewContentManagerNewFolderRowProps { - team: SerializedTeam - workspaceId: string - folderId?: string - className?: string -} - -const TableViewContentManagerNewDocRow = ({ - team, - workspaceId, - folderId, - className, -}: TableViewContentManagerNewFolderRowProps) => { - const { translate } = useI18n() - const { createDoc } = useCloudApi() - - return ( - - - createDoc( - team, - { - title: val, - workspaceId, - parentFolderId: folderId, - }, - { skipRedirect: true } - ) - } - /> - - ) -} - -export default TableViewContentManagerNewDocRow From dfde4c7709a4c20418dff0a5d9507515d3719e9a Mon Sep 17 00:00:00 2001 From: davy-c Date: Mon, 29 Nov 2021 10:36:43 +0900 Subject: [PATCH 07/65] remove styled component for add row --- src/cloud/components/Views/Manager.tsx | 21 +++++++++++++++++++ .../components/Views/Table/TableView.tsx | 5 ++--- src/cloud/components/Views/ViewManagerRow.tsx | 21 ------------------- .../components/Views/ViewsFolderList.tsx | 5 ++--- 4 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 src/cloud/components/Views/ViewManagerRow.tsx diff --git a/src/cloud/components/Views/Manager.tsx b/src/cloud/components/Views/Manager.tsx index 047a552adf..92b311ad8c 100644 --- a/src/cloud/components/Views/Manager.tsx +++ b/src/cloud/components/Views/Manager.tsx @@ -211,4 +211,25 @@ const Container = styled.div` .content__manager__list__header--margin { margin-top: ${({ theme }) => theme.sizes.spaces.df}px; } + + .content__manager__add-row { + height: 40px; + display: flex; + align-items: center; + padding: 0 ${({ theme }) => theme.sizes.spaces.xl}px; + color: ${({ theme }) => theme.colors.text.subtle}; + border-bottom: 1px solid ${({ theme }) => theme.colors.border.second}; + width: 100%; + + button { + padding: 0; + justify-content: flex-start; + } + + .form__toggable__input, + button, + input { + width: 100%; + } + } ` diff --git a/src/cloud/components/Views/Table/TableView.tsx b/src/cloud/components/Views/Table/TableView.tsx index 4915372bf1..a281a9940e 100644 --- a/src/cloud/components/Views/Table/TableView.tsx +++ b/src/cloud/components/Views/Table/TableView.tsx @@ -45,7 +45,6 @@ import { import { overflowEllipsis } from '../../../../design/lib/styled/styleFunctions' import Table from '../../../../design/components/organisms/Table' import Icon from '../../../../design/components/atoms/Icon' -import ViewManagerRow from '../ViewManagerRow' import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' import { lngKeys } from '../../../lib/i18n/types' import { useI18n } from '../../../lib/hooks/useI18n' @@ -372,7 +371,7 @@ const TableView = ({ /> {orderedDocs.length === 0 && } {currentWorkspaceId != null && ( - +
- +
)} diff --git a/src/cloud/components/Views/ViewManagerRow.tsx b/src/cloud/components/Views/ViewManagerRow.tsx deleted file mode 100644 index 1cd6af5e8c..0000000000 --- a/src/cloud/components/Views/ViewManagerRow.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import styled from '../../../design/lib/styled' - -type ViewManagerRowProps = React.PropsWithChildren<{ - className?: string -}> - -const ViewManagerRow = ({ className, children }: ViewManagerRowProps) => { - return {children} -} - -export default ViewManagerRow - -const Container = styled.div` - height: 40px; - display: flex; - align-items: center; - padding: 0 ${({ theme }) => theme.sizes.spaces.xl}px; - color: ${({ theme }) => theme.colors.text.subtle}; - border-bottom: 1px solid ${({ theme }) => theme.colors.border.second}; -` diff --git a/src/cloud/components/Views/ViewsFolderList.tsx b/src/cloud/components/Views/ViewsFolderList.tsx index f650cf3e81..f183855d82 100644 --- a/src/cloud/components/Views/ViewsFolderList.tsx +++ b/src/cloud/components/Views/ViewsFolderList.tsx @@ -13,7 +13,6 @@ import { useRouter } from '../../lib/router' import { folderToDataTransferItem } from '../../lib/utils/patterns' import { getFolderHref } from '../Link/FolderLink' import ViewManagerContentRow from './ViewManagerContentRow' -import ViewManagerRow from './ViewManagerRow' interface ViewsFolderListProps { folders?: SerializedFolderWithBookmark[] @@ -126,7 +125,7 @@ export const ViewsFolderList = ({ })} {currentWorkspaceId != null && ( - +
- +
)} ) From 9ca7e6129c6eb23da486fcb30ada2898a914c3d7 Mon Sep 17 00:00:00 2001 From: davy-c Date: Mon, 29 Nov 2021 10:40:46 +0900 Subject: [PATCH 08/65] finalize structure --- .../ViewManagerContentRow.tsx | 10 +- .../{ => FolderList}/ViewsFolderList.tsx | 24 +- src/cloud/components/Views/Manager.tsx | 235 ----------------- src/cloud/components/Views/index.tsx | 236 +++++++++++++++++- 4 files changed, 252 insertions(+), 253 deletions(-) rename src/cloud/components/Views/{ => FolderList}/ViewManagerContentRow.tsx (93%) rename src/cloud/components/Views/{ => FolderList}/ViewsFolderList.tsx (83%) delete mode 100644 src/cloud/components/Views/Manager.tsx diff --git a/src/cloud/components/Views/ViewManagerContentRow.tsx b/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx similarity index 93% rename from src/cloud/components/Views/ViewManagerContentRow.tsx rename to src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx index cb319a1ff4..1ad369d8cc 100644 --- a/src/cloud/components/Views/ViewManagerContentRow.tsx +++ b/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useRef, useState } from 'react' import cc from 'classcat' -import { onDragLeaveCb } from '../../../design/lib/dnd' -import styled from '../../../design/lib/styled' -import { AppComponent } from '../../../design/lib/types' -import EmojiIcon from '../EmojiIcon' -import Checkbox from '../../../design/components/molecules/Form/atoms/FormCheckbox' +import { onDragLeaveCb } from '../../../../design/lib/dnd' +import styled from '../../../../design/lib/styled' +import { AppComponent } from '../../../../design/lib/types' +import EmojiIcon from '../../EmojiIcon' +import Checkbox from '../../../../design/components/molecules/Form/atoms/FormCheckbox' interface ViewManagerContentRowProps { type?: 'header' | 'row' diff --git a/src/cloud/components/Views/ViewsFolderList.tsx b/src/cloud/components/Views/FolderList/ViewsFolderList.tsx similarity index 83% rename from src/cloud/components/Views/ViewsFolderList.tsx rename to src/cloud/components/Views/FolderList/ViewsFolderList.tsx index f183855d82..d8c91f4e37 100644 --- a/src/cloud/components/Views/ViewsFolderList.tsx +++ b/src/cloud/components/Views/FolderList/ViewsFolderList.tsx @@ -1,17 +1,17 @@ import { mdiPlus } from '@mdi/js' import React, { useCallback, useMemo } from 'react' -import FormToggableInput from '../../../design/components/molecules/Form/atoms/FormToggableInput' -import { sortByAttributeAsc } from '../../../design/lib/utils/array' -import { SerializedFolderWithBookmark } from '../../interfaces/db/folder' -import { SerializedTeam } from '../../interfaces/db/team' -import { DraggedTo } from '../../lib/dnd' -import { useCloudDnd } from '../../lib/hooks/sidebar/useCloudDnd' -import { useCloudApi } from '../../lib/hooks/useCloudApi' -import { useI18n } from '../../lib/hooks/useI18n' -import { lngKeys } from '../../lib/i18n/types' -import { useRouter } from '../../lib/router' -import { folderToDataTransferItem } from '../../lib/utils/patterns' -import { getFolderHref } from '../Link/FolderLink' +import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' +import { sortByAttributeAsc } from '../../../../design/lib/utils/array' +import { SerializedFolderWithBookmark } from '../../../interfaces/db/folder' +import { SerializedTeam } from '../../../interfaces/db/team' +import { DraggedTo } from '../../../lib/dnd' +import { useCloudDnd } from '../../../lib/hooks/sidebar/useCloudDnd' +import { useCloudApi } from '../../../lib/hooks/useCloudApi' +import { useI18n } from '../../../lib/hooks/useI18n' +import { lngKeys } from '../../../lib/i18n/types' +import { useRouter } from '../../../lib/router' +import { folderToDataTransferItem } from '../../../lib/utils/patterns' +import { getFolderHref } from '../../Link/FolderLink' import ViewManagerContentRow from './ViewManagerContentRow' interface ViewsFolderListProps { diff --git a/src/cloud/components/Views/Manager.tsx b/src/cloud/components/Views/Manager.tsx deleted file mode 100644 index 92b311ad8c..0000000000 --- a/src/cloud/components/Views/Manager.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' -import { useSet } from 'react-use' -import { difference } from 'lodash' -import Flexbox from '../../../design/components/atoms/Flexbox' -import styled from '../../../design/lib/styled' -import { SerializedDocWithSupplemental } from '../../interfaces/db/doc' -import { SerializedFolderWithBookmark } from '../../interfaces/db/folder' -import { useCloudApi } from '../../lib/hooks/useCloudApi' -import ContentManagerToolbar from '../ContentManager/ContentManagerToolbar' -import ViewsSelector from './ViewsSelector' -import { sortTableViewColumns } from '../../lib/views/table' -import { SerializedView, ViewParent } from '../../interfaces/db/view' -import { SerializedWorkspace } from '../../interfaces/db/workspace' -import { SerializedTeam } from '../../interfaces/db/team' -import TableView from './Table/TableView' -import ViewsFolderList from './ViewsFolderList' -import Scroller from '../../../design/components/atoms/Scroller' - -type ViewsManagerProps = { - views: SerializedView[] - parent: ViewParent - workspacesMap: Map - docs: SerializedDocWithSupplemental[] - folders?: SerializedFolderWithBookmark[] - currentUserIsCoreMember: boolean - currentWorkspaceId?: string - currentFolderId?: string - team: SerializedTeam -} - -export const ViewsManager = ({ - views, - parent, - team, - docs, - folders, - currentUserIsCoreMember, - currentFolderId, - currentWorkspaceId, - workspacesMap, -}: ViewsManagerProps) => { - const [selectedViewId, setSelectedViewId] = useState(() => - views.length > 0 ? views[0].id : undefined - ) - const [updating, setUpdating] = useState([]) - const { createViewApi } = useCloudApi() - const [ - selectedFolderSet, - { - add: addFolderInSelection, - has: hasFolderInSelection, - toggle: toggleFolderInSelection, - reset: resetFoldersInSelection, - remove: removeFolderInSelection, - }, - ] = useSet(new Set()) - const [ - selectedDocSet, - { - add: addDocinSelection, - has: hasDocInSelection, - toggle: toggleDocInSelection, - remove: removeDocInSelection, - reset: resetDocsInSelection, - }, - ] = useSet(new Set()) - - const currentDocumentsRef = useRef( - new Map( - docs.map((doc) => [doc.id, doc]) - ) - ) - const currentFoldersRef = useRef( - new Map( - (folders || []).map((folder) => [folder.id, folder]) - ) - ) - - useEffect(() => { - const newMap = new Map(docs.map((doc) => [doc.id, doc])) - const idsToClean: string[] = difference( - [...currentDocumentsRef.current.keys()], - [...newMap.keys()] - ) - idsToClean.forEach(removeDocInSelection) - currentDocumentsRef.current = newMap - }, [docs, removeDocInSelection]) - - useEffect(() => { - const newMap = new Map((folders || []).map((folder) => [folder.id, folder])) - const idsToClean: string[] = difference( - [...currentFoldersRef.current.keys()], - [...newMap.keys()] - ) - idsToClean.forEach(removeFolderInSelection) - currentFoldersRef.current = newMap - }, [folders, removeFolderInSelection]) - - const currentView = useMemo(() => { - if (selectedViewId == null) { - return undefined - } - - return views.find((view) => view.id === selectedViewId) - }, [selectedViewId, views]) - - const toolbarColumns = useMemo(() => { - if (currentView == null || currentView.type !== 'table') { - return [] - } - - return sortTableViewColumns(currentView.data.columns || {}) - }, [currentView]) - - const selectViewId = useCallback( - (id: number) => { - setSelectedViewId(id) - resetDocsInSelection() - resetFoldersInSelection() - }, - [resetDocsInSelection, resetFoldersInSelection] - ) - - return ( - - - - - - - {currentView != null && ( - <> - {currentView.type === 'table' ? ( - - ) : null} - - )} - - -
- - - {currentUserIsCoreMember && ( - - )} - - ) -} - -const Container = styled.div` - width: 100%; - height: 100%; - overflow: hidden; - position: relative; - - .views__header { - flex: 0 0 auto; - width: 100%; - } - - .view__scroller { - height: 100%; - } - - .views__placeholder { - height: 40px; - width: 100%; - } - - .content__manager__list__header--margin { - margin-top: ${({ theme }) => theme.sizes.spaces.df}px; - } - - .content__manager__add-row { - height: 40px; - display: flex; - align-items: center; - padding: 0 ${({ theme }) => theme.sizes.spaces.xl}px; - color: ${({ theme }) => theme.colors.text.subtle}; - border-bottom: 1px solid ${({ theme }) => theme.colors.border.second}; - width: 100%; - - button { - padding: 0; - justify-content: flex-start; - } - - .form__toggable__input, - button, - input { - width: 100%; - } - } -` diff --git a/src/cloud/components/Views/index.tsx b/src/cloud/components/Views/index.tsx index fb000a2b99..fb9e2a6fff 100644 --- a/src/cloud/components/Views/index.tsx +++ b/src/cloud/components/Views/index.tsx @@ -1 +1,235 @@ -export * from './Manager' +import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react' +import { useSet } from 'react-use' +import { difference } from 'lodash' +import Flexbox from '../../../design/components/atoms/Flexbox' +import styled from '../../../design/lib/styled' +import { SerializedDocWithSupplemental } from '../../interfaces/db/doc' +import { SerializedFolderWithBookmark } from '../../interfaces/db/folder' +import { useCloudApi } from '../../lib/hooks/useCloudApi' +import ContentManagerToolbar from '../ContentManager/ContentManagerToolbar' +import ViewsSelector from './ViewsSelector' +import { sortTableViewColumns } from '../../lib/views/table' +import { SerializedView, ViewParent } from '../../interfaces/db/view' +import { SerializedWorkspace } from '../../interfaces/db/workspace' +import { SerializedTeam } from '../../interfaces/db/team' +import TableView from './Table/TableView' +import ViewsFolderList from './FolderList/ViewsFolderList' +import Scroller from '../../../design/components/atoms/Scroller' + +type ViewsManagerProps = { + views: SerializedView[] + parent: ViewParent + workspacesMap: Map + docs: SerializedDocWithSupplemental[] + folders?: SerializedFolderWithBookmark[] + currentUserIsCoreMember: boolean + currentWorkspaceId?: string + currentFolderId?: string + team: SerializedTeam +} + +export const ViewsManager = ({ + views, + parent, + team, + docs, + folders, + currentUserIsCoreMember, + currentFolderId, + currentWorkspaceId, + workspacesMap, +}: ViewsManagerProps) => { + const [selectedViewId, setSelectedViewId] = useState(() => + views.length > 0 ? views[0].id : undefined + ) + const [updating, setUpdating] = useState([]) + const { createViewApi } = useCloudApi() + const [ + selectedFolderSet, + { + add: addFolderInSelection, + has: hasFolderInSelection, + toggle: toggleFolderInSelection, + reset: resetFoldersInSelection, + remove: removeFolderInSelection, + }, + ] = useSet(new Set()) + const [ + selectedDocSet, + { + add: addDocinSelection, + has: hasDocInSelection, + toggle: toggleDocInSelection, + remove: removeDocInSelection, + reset: resetDocsInSelection, + }, + ] = useSet(new Set()) + + const currentDocumentsRef = useRef( + new Map( + docs.map((doc) => [doc.id, doc]) + ) + ) + const currentFoldersRef = useRef( + new Map( + (folders || []).map((folder) => [folder.id, folder]) + ) + ) + + useEffect(() => { + const newMap = new Map(docs.map((doc) => [doc.id, doc])) + const idsToClean: string[] = difference( + [...currentDocumentsRef.current.keys()], + [...newMap.keys()] + ) + idsToClean.forEach(removeDocInSelection) + currentDocumentsRef.current = newMap + }, [docs, removeDocInSelection]) + + useEffect(() => { + const newMap = new Map((folders || []).map((folder) => [folder.id, folder])) + const idsToClean: string[] = difference( + [...currentFoldersRef.current.keys()], + [...newMap.keys()] + ) + idsToClean.forEach(removeFolderInSelection) + currentFoldersRef.current = newMap + }, [folders, removeFolderInSelection]) + + const currentView = useMemo(() => { + if (selectedViewId == null) { + return undefined + } + + return views.find((view) => view.id === selectedViewId) + }, [selectedViewId, views]) + + const toolbarColumns = useMemo(() => { + if (currentView == null || currentView.type !== 'table') { + return [] + } + + return sortTableViewColumns(currentView.data.columns || {}) + }, [currentView]) + + const selectViewId = useCallback( + (id: number) => { + setSelectedViewId(id) + resetDocsInSelection() + resetFoldersInSelection() + }, + [resetDocsInSelection, resetFoldersInSelection] + ) + + return ( + + + + + + + {currentView != null && ( + <> + {currentView.type === 'table' ? ( + + ) : null} + + )} + + +
+ + + {currentUserIsCoreMember && ( + + )} + + ) +} + +const Container = styled.div` + width: 100%; + height: 100%; + overflow: hidden; + position: relative; + + .views__header { + flex: 0 0 auto; + width: 100%; + } + + .view__scroller { + height: 100%; + } + + .views__placeholder { + height: 40px; + width: 100%; + } + + .content__manager__list__header--margin { + margin-top: ${({ theme }) => theme.sizes.spaces.df}px; + } + + .content__manager__add-row { + height: 40px; + display: flex; + align-items: center; + padding: 0 ${({ theme }) => theme.sizes.spaces.xl}px; + color: ${({ theme }) => theme.colors.text.subtle}; + border-bottom: 1px solid ${({ theme }) => theme.colors.border.second}; + width: 100%; + + button { + padding: 0; + justify-content: flex-start; + } + + .form__toggable__input, + button, + input { + width: 100%; + } + } +` From 80fb70965e57157a888499b84f45fe4ae80ec6b7 Mon Sep 17 00:00:00 2001 From: davy-c Date: Mon, 29 Nov 2021 11:17:40 +0900 Subject: [PATCH 09/65] Activate creation for views Selector --- src/cloud/api/teams/views/index.ts | 1 + src/cloud/components/Views/ViewsSelector.tsx | 26 ++++++++++++++------ src/cloud/interfaces/db/view.ts | 1 + src/cloud/lib/hooks/views/tableView.ts | 2 ++ src/cloud/lib/views/table.ts | 1 + 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/cloud/api/teams/views/index.ts b/src/cloud/api/teams/views/index.ts index aa6d08f049..b41d99fd31 100644 --- a/src/cloud/api/teams/views/index.ts +++ b/src/cloud/api/teams/views/index.ts @@ -26,6 +26,7 @@ export type CreateViewRequestBody = { smartView?: string folder?: string workspace?: string + name: string } export interface CreateViewResponseBody { diff --git a/src/cloud/components/Views/ViewsSelector.tsx b/src/cloud/components/Views/ViewsSelector.tsx index 32b47cd9b5..58407fa211 100644 --- a/src/cloud/components/Views/ViewsSelector.tsx +++ b/src/cloud/components/Views/ViewsSelector.tsx @@ -7,12 +7,16 @@ import NavigationItem from '../../../design/components/molecules/Navigation/Navi import { BulkApiActionRes } from '../../../design/lib/hooks/useBulkApi' import { useModal } from '../../../design/lib/stores/modal' import styled from '../../../design/lib/styled' -import { CreateViewRequestBody } from '../../api/teams/views' +import { + CreateViewRequestBody, + CreateViewResponseBody, +} from '../../api/teams/views' import { SerializedView, SupportedViewTypes, ViewParent, } from '../../interfaces/db/view' +import { filterIter } from '../../lib/utils/iterator' export interface ViewsSelectorProps { selectedViewId: number | undefined @@ -36,16 +40,24 @@ const ViewsSelector = ({ async (type: SupportedViewTypes) => { closeAllModals() setSending(true) - await createViewApi( + const viewsOfTheSameType = filterIter((view) => view.type === type, views) + .length + const name = `${capitalize(type)}${ + viewsOfTheSameType === 0 ? '' : ` ${viewsOfTheSameType}` + }` + const res = await createViewApi( parent.type === 'folder' - ? { folder: parent.target.id, type } + ? { folder: parent.target.id, type, name } : parent.type === 'workspace' - ? { workspace: parent.target.id, type } - : { smartView: parent.target.id, type } + ? { workspace: parent.target.id, type, name } + : { smartView: parent.target.id, type, name } ) setSending(false) + if (!res.err) { + setSelectedViewId((res.data as CreateViewResponseBody).data.id) + } }, - [closeAllModals, parent, createViewApi] + [views, closeAllModals, parent, createViewApi, setSelectedViewId] ) return ( @@ -61,7 +73,7 @@ const ViewsSelector = ({ onClick={() => setSelectedViewId(view.id)} active={selectedViewId === view.id} > - {capitalize(view.type)} + {view.name} ))} Date: Mon, 29 Nov 2021 11:22:51 +0900 Subject: [PATCH 10/65] memo for views selector --- src/cloud/components/Views/index.tsx | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/cloud/components/Views/index.tsx b/src/cloud/components/Views/index.tsx index fb9e2a6fff..4272a7b919 100644 --- a/src/cloud/components/Views/index.tsx +++ b/src/cloud/components/Views/index.tsx @@ -121,19 +121,24 @@ export const ViewsManager = ({ [resetDocsInSelection, resetFoldersInSelection] ) + const viewsSelector = useMemo(() => { + return ( + + + + ) + }, [createViewApi, parent, views, selectedViewId, selectViewId]) + return ( - - - - + {viewsSelector} {currentView != null && ( <> {currentView.type === 'table' ? ( From 353041ea7ee1e9893c878aa7427a66b1773ae7df Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Mon, 29 Nov 2021 10:13:34 +0900 Subject: [PATCH 11/65] LabelLike manager component --- .../components/Props/Pickers/StatusSelect.tsx | 315 +---------------- .../components/molecules/LabelManager.tsx | 316 ++++++++++++++++++ src/design/components/atoms/Label.tsx | 20 ++ 3 files changed, 354 insertions(+), 297 deletions(-) create mode 100644 src/cloud/components/molecules/LabelManager.tsx create mode 100644 src/design/components/atoms/Label.tsx diff --git a/src/cloud/components/Props/Pickers/StatusSelect.tsx b/src/cloud/components/Props/Pickers/StatusSelect.tsx index d4f523ef7e..08c9c57403 100644 --- a/src/cloud/components/Props/Pickers/StatusSelect.tsx +++ b/src/cloud/components/Props/Pickers/StatusSelect.tsx @@ -1,25 +1,13 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import cc from 'classcat' -import styled from '../../../../design/lib/styled' import PropertyValueButton from './PropertyValueButton' import { SerializedStatus } from '../../../interfaces/db/status' import { useStatuses } from '../../../lib/stores/status' import { usePage } from '../../../lib/stores/pageStore' -import FormInput from '../../../../design/components/molecules/Form/atoms/FormInput' -import Spinner from '../../../../design/components/atoms/Spinner' -import { useUpDownNavigationListener } from '../../../lib/keyboard' -import MetadataContainer from '../../../../design/components/organisms/MetadataContainer' -import MetadataContainerRow from '../../../../design/components/organisms/MetadataContainer/molecules/MetadataContainerRow' -import { - mdiArrowDownDropCircleOutline, - mdiDotsHorizontal, - mdiTrashCanOutline, -} from '@mdi/js' +import { mdiArrowDownDropCircleOutline } from '@mdi/js' import { useModal } from '../../../../design/lib/stores/modal' -import Flexbox from '../../../../design/components/atoms/Flexbox' -import Icon from '../../../../design/components/atoms/Icon' -import { useEffectOnce } from 'react-use' -import FormColorSelect from '../../../../design/components/molecules/Form/atoms/FormColorSelect' +import LabelManager, { LabelLike } from '../../molecules/LabelManager' +import { Label } from '../../../../design/components/atoms/Label' interface StatusSelectProps { sending?: boolean @@ -76,14 +64,11 @@ const StatusSelect = ({ iconPath={showIcon ? mdiArrowDownDropCircleOutline : undefined} > {status != null ? ( - +
@@ -99,291 +84,27 @@ const StatusSelector = ({ }) => { const { team } = usePage() const { state, addStatus, removeStatus, editStatus } = useStatuses(team!.id) - const inputRef = useRef(null) - const containerRef = useRef(null) const [sending, setSending] = useState(false) - const [tagText, setTagText] = useState('') - const { openContextModal, closeLastModal } = useModal() - const createStatusHandler = useCallback( - async (name: string) => { - if (sending || tagText.trim() === '') { + const createStatus = useCallback( + async ({ name, backgroundColor }: LabelLike) => { + if (sending) { return } setSending(true) - await addStatus({ name, team: team!.id }) + await addStatus({ name, backgroundColor, team: team!.id }) setSending(false) }, - [team, addStatus, sending, tagText] - ) - - const inputOnChangeEvent = useCallback( - (event: React.ChangeEvent) => { - event.preventDefault() - setTagText(event.target.value) - }, - [setTagText] + [team, addStatus, sending] ) - const openEditor = useCallback( - (ev: React.MouseEvent, status: SerializedStatus) => { - ev.stopPropagation() - ev.preventDefault() - openContextModal( - ev, - { - removeStatus(status) - closeLastModal() - }} - onSave={(status) => { - editStatus(status) - }} - />, - { - removePadding: true, - width: 200, - keepAll: true, - } - ) - }, - [openContextModal, removeStatus, editStatus, closeLastModal] - ) - - const options = useMemo(() => { - return state.statuses.filter((status) => status.name.startsWith(tagText)) - }, [state.statuses, tagText]) - - const showCreate = useMemo(() => { - return ( - tagText != '' && !state.statuses.some((status) => status.name === tagText) - ) - }, [state.statuses, tagText]) - - const showNoStatus = useMemo(() => { - return tagText === '' || 'No Status'.startsWith(tagText) - }, [tagText]) - - useUpDownNavigationListener(containerRef, { - overrideInput: true, - }) - return ( - - - {state.isWorking && ( -
- -
- )} - -
+ ) } - -interface StatusEditorProps { - status: SerializedStatus - onDelete: (status: SerializedStatus) => void - onSave: (status: SerializedStatus) => void -} - -const StatusEditor = ({ status, onDelete, onSave }: StatusEditorProps) => { - const [editingStatus, setEditingStatus] = useState(status) - const editingStatusRef = useRef(status) - const onSaveRef = useRef(onSave) - - useEffect(() => { - editingStatusRef.current = editingStatus - }, [editingStatus]) - - useEffect(() => { - onSaveRef.current = onSave - }, [onSave]) - - useEffectOnce(() => { - return () => { - if ( - editingStatusRef.current.name !== status.name || - editingStatusRef.current.backgroundColor !== status.backgroundColor - ) { - onSaveRef.current(editingStatusRef.current) - } - } - }) - - const setName: React.ChangeEventHandler = useCallback( - (ev) => { - const name = ev.target.value - setEditingStatus((prev) => ({ ...prev, name })) - }, - [] - ) - const setColor = useCallback((backgroundColor: string) => { - setEditingStatus((prev) => ({ - ...prev, - backgroundColor: backgroundColor === '' ? undefined : backgroundColor, - })) - }, []) - - return ( - - - , - }} - /> - - - ), - }} - /> - onDelete(editingStatus), - }, - }} - /> - - ) -} - -const Container = styled.div` - .autocomplete__input { - line-height: inherit !important; - height: 28px !important; - width: 100%; - } - - .autocomplete__container { - padding: ${({ theme }) => theme.sizes.spaces.xsm}px 0; - max-width: auto; - max-height: 300px; - overflow-y: auto; - border-style: solid; - border-width: 1px; - border-radius: 4px; - display: inline-flex; - flex-direction: column; - border: none; - top: 100%; - background-color: ${({ theme }) => theme.colors.background.primary}; - box-shadow: ${({ theme }) => theme.colors.shadow}; - } - - .autocomplete__option { - width: 100%; - display: block; - flex-shrink: 0; - padding: ${({ theme }) => theme.sizes.spaces.sm}px - ${({ theme }) => theme.sizes.spaces.df}px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: ${({ theme }) => theme.colors.text.subtle}; - text-decoration: none; - position: relative; - - &:hover, - &:focus { - background: ${({ theme }) => theme.colors.background.quaternary}; - color: ${({ theme }) => theme.colors.text.primary}; - } - } - - .autocomplete__spinner__wrapper { - margin: 0 auto; - padding: ${({ theme }) => theme.sizes.spaces.sm}px - ${({ theme }) => theme.sizes.spaces.df}px; - - & > .autocomplete__spinner { - border-color: ${({ theme }) => theme.colors.variants.primary.text}; - border-right-color: transparent; - display: inline-flex; - } - } - - .status__editor { - position: absolute; - top: 0; - left: 100%; - } -` - -export const StatusView = ({ - name, - backgroundColor, -}: { - name: string - backgroundColor?: string -}) => { - return {name} -} - -const StyledStatus = styled.span` - display: inline-block; - padding: 0.25em 0.5em; - border-radius: 4px; - color: white; - background-color: #353940; -` diff --git a/src/cloud/components/molecules/LabelManager.tsx b/src/cloud/components/molecules/LabelManager.tsx new file mode 100644 index 0000000000..925708de7d --- /dev/null +++ b/src/cloud/components/molecules/LabelManager.tsx @@ -0,0 +1,316 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { mdiDotsHorizontal, mdiTrashCanOutline } from '@mdi/js' +import { useEffectOnce } from 'react-use' +import { getColorFromString } from '../../lib/utils/string' +import { useModal } from '../../../design/lib/stores/modal' +import Flexbox from '../../../design/components/atoms/Flexbox' +import Icon from '../../../design/components/atoms/Icon' +import Spinner from '../../../design/components/atoms/Spinner' +import FormInput from '../../../design/components/molecules/Form/atoms/FormInput' +import styled from '../../../design/lib/styled' +import MetadataContainerRow from '../../../design/components/organisms/MetadataContainer/molecules/MetadataContainerRow' +import FormColorSelect from '../../../design/components/molecules/Form/atoms/FormColorSelect' +import MetadataContainer from '../../../design/components/organisms/MetadataContainer' +import { useUpDownNavigationListener } from '../../lib/keyboard' +import { Label } from '../../../design/components/atoms/Label' + +export interface LabelLike { + name: string + backgroundColor?: string +} + +interface LabelManagerProps { + labels: T[] + onSelect: (label: T | null) => void + onCreate: (label: LabelLike) => void + onUpdate: (label: T) => void + onDelete: (label: T) => void + sending?: boolean + type?: string +} + +const LabelManager = ({ + labels, + onSelect, + onCreate, + onUpdate, + onDelete, + type = 'label', + sending = false, +}: LabelManagerProps) => { + const inputRef = useRef(null) + const containerRef = useRef(null) + const [labelName, setLabelName] = useState('') + const { openContextModal, closeLastModal } = useModal() + + const createStatusHandler = useCallback( + async (name: string) => { + if (sending || labelName.trim() === '') { + return + } + onCreate({ name, backgroundColor: getColorFromString(name) }) + }, + [sending, labelName, onCreate] + ) + + const inputOnChangeEvent = useCallback( + (event: React.ChangeEvent) => { + event.preventDefault() + setLabelName(event.target.value) + }, + [setLabelName] + ) + + const openEditor = useCallback( + (ev: React.MouseEvent, label: T) => { + ev.stopPropagation() + ev.preventDefault() + openContextModal( + ev, + { + onDelete(label) + closeLastModal() + }} + onSave={onUpdate} + type={type} + />, + { + removePadding: true, + width: 200, + keepAll: true, + } + ) + }, + [openContextModal, onDelete, onUpdate, closeLastModal, type] + ) + + const options = useMemo(() => { + return labels.filter((label) => label.name.startsWith(labelName)) + }, [labels, labelName]) + + const showCreate = useMemo(() => { + return ( + labelName != '' && !labels.some((status) => status.name === labelName) + ) + }, [labels, labelName]) + + const showNoStatus = useMemo(() => { + return labelName === '' || 'No Status'.startsWith(labelName) + }, [labelName]) + + useUpDownNavigationListener(containerRef, { + overrideInput: true, + }) + + return ( + + + {sending && ( +
+ +
+ )} + +
+ ) +} + +export default LabelManager + +interface StatusEditorProps { + label: T + onDelete: (status: T) => void + onSave: (status: T) => void + type?: string +} + +const StatusEditor = ({ + label, + onDelete, + onSave, + type = 'label', +}: StatusEditorProps) => { + const [editingStatus, setEditingStatus] = useState(label) + const editingStatusRef = useRef(label) + const onSaveRef = useRef(onSave) + + useEffect(() => { + editingStatusRef.current = editingStatus + }, [editingStatus]) + + useEffect(() => { + onSaveRef.current = onSave + }, [onSave]) + + useEffectOnce(() => { + return () => { + if ( + editingStatusRef.current.name !== label.name || + editingStatusRef.current.backgroundColor !== label.backgroundColor + ) { + onSaveRef.current(editingStatusRef.current) + } + } + }) + + const setName: React.ChangeEventHandler = useCallback( + (ev) => { + const name = ev.target.value + setEditingStatus((prev) => ({ ...prev, name })) + }, + [] + ) + const setColor = useCallback((backgroundColor: string) => { + setEditingStatus((prev) => ({ + ...prev, + backgroundColor: backgroundColor === '' ? undefined : backgroundColor, + })) + }, []) + + return ( + + + , + }} + /> + + + ), + }} + /> + onDelete(editingStatus), + }, + }} + /> + + ) +} + +const Container = styled.div` + .autocomplete__input { + line-height: inherit !important; + height: 28px !important; + width: 100%; + } + + .autocomplete__container { + padding: ${({ theme }) => theme.sizes.spaces.xsm}px 0; + max-width: auto; + max-height: 300px; + overflow-y: auto; + border-style: solid; + border-width: 1px; + border-radius: 4px; + display: inline-flex; + flex-direction: column; + border: none; + top: 100%; + background-color: ${({ theme }) => theme.colors.background.primary}; + box-shadow: ${({ theme }) => theme.colors.shadow}; + } + + .autocomplete__option { + width: 100%; + display: block; + flex-shrink: 0; + padding: ${({ theme }) => theme.sizes.spaces.sm}px + ${({ theme }) => theme.sizes.spaces.df}px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: ${({ theme }) => theme.colors.text.subtle}; + text-decoration: none; + position: relative; + + &:hover, + &:focus { + background: ${({ theme }) => theme.colors.background.quaternary}; + color: ${({ theme }) => theme.colors.text.primary}; + } + } + + .autocomplete__spinner__wrapper { + margin: 0 auto; + padding: ${({ theme }) => theme.sizes.spaces.sm}px + ${({ theme }) => theme.sizes.spaces.df}px; + + & > .autocomplete__spinner { + border-color: ${({ theme }) => theme.colors.variants.primary.text}; + border-right-color: transparent; + display: inline-flex; + } + } + + .status__editor { + position: absolute; + top: 0; + left: 100%; + } +` diff --git a/src/design/components/atoms/Label.tsx b/src/design/components/atoms/Label.tsx new file mode 100644 index 0000000000..49952862bd --- /dev/null +++ b/src/design/components/atoms/Label.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import styled from '../../lib/styled' + +export const Label = ({ + name, + backgroundColor, +}: { + name: string + backgroundColor?: string +}) => { + return {name} +} + +const StyledLabel = styled.span` + display: inline-block; + padding: 0.25em 0.5em; + border-radius: 4px; + color: white; + background-color: #353940; +` From 8213809f40997a9952851d0a7234255503d8d44b Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Mon, 29 Nov 2021 11:01:22 +0900 Subject: [PATCH 12/65] integrate label manager into doc label picker --- src/cloud/api/teams/tags/index.ts | 2 + .../DocTagsList/TagsAutoCompleteInput.tsx | 206 +++++------------- src/cloud/components/DocTagsListItem.tsx | 7 +- .../components/Props/Pickers/StatusSelect.tsx | 1 + src/cloud/interfaces/db/tag.ts | 1 + 5 files changed, 61 insertions(+), 156 deletions(-) diff --git a/src/cloud/api/teams/tags/index.ts b/src/cloud/api/teams/tags/index.ts index 8b449b6084..83d9ba3f53 100644 --- a/src/cloud/api/teams/tags/index.ts +++ b/src/cloud/api/teams/tags/index.ts @@ -6,6 +6,7 @@ import { callApi } from '../../../lib/client' export interface CreateTagRequestBody { docId: string text: string + backgroundColor?: string } export interface CreateTagResponseBody { @@ -33,6 +34,7 @@ export interface UpdateTagResponseBody { export interface UpdateTagRequestBody { text: string + backgroundColor?: string } export async function updateTag( diff --git a/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx b/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx index 8a55e13ec1..9a12ddeff4 100644 --- a/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx +++ b/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx @@ -7,16 +7,17 @@ import { SerializedDocWithSupplemental } from '../../../interfaces/db/doc' import { useUpDownNavigationListener } from '../../../lib/keyboard' import { useToast } from '../../../../design/lib/stores/toast' import styled from '../../../../design/lib/styled' -import FormInput from '../../../../design/components/molecules/Form/atoms/FormInput' import cc from 'classcat' import { lngKeys } from '../../../lib/i18n/types' import { useI18n } from '../../../lib/hooks/useI18n' -import Spinner from '../../../../design/components/atoms/Spinner' import DocPropertyValueButton from '../../Props/Pickers/PropertyValueButton' import Icon from '../../../../design/components/atoms/Icon' import { useModal } from '../../../../design/lib/stores/modal' import { useEffectOnce } from 'react-use' -import Flexbox from '../../../../design/components/atoms/Flexbox' +import LabelManager, { LabelLike } from '../../molecules/LabelManager' +import { filterIter } from '../../../lib/utils/iterator' +import { SerializedTag } from '../../../interfaces/db/tag' +import { useCloudApi } from '../../../lib/hooks/useCloudApi' interface TagsAutoCompleteInputProps { doc: SerializedDocWithSupplemental @@ -74,16 +75,16 @@ interface TagsSelectorModalProps { team: SerializedTeam } +type LabelLikeTag = LabelLike & SerializedTag + const TagsSelectorModal = ({ team, doc }: TagsSelectorModalProps) => { const inputRef = useRef(null) const containerRef = useRef(null) - const autocompleteRef = useRef(null) const { tagsMap, updateDocsMap, updateTagsMap } = useNav() const { pushApiErrorMessage } = useToast() const [sending, setSending] = useState(false) - const [tagText, setTagText] = useState('') - const { translate } = useI18n() const { closeLastModal } = useModal() + const { deleteTagApi, updateTagApi } = useCloudApi() useEffectOnce(() => { if (inputRef.current != null) { @@ -93,61 +94,32 @@ const TagsSelectorModal = ({ team, doc }: TagsSelectorModalProps) => { const tagsIdsAlreadyInDoc = useMemo(() => { if (doc.tags == null || doc.tags.length === 0) { - return [] + return new Set() } - return doc.tags.map((tag) => tag.id) + return new Set(doc.tags.map((tag) => tag.id)) }, [doc]) - const autoCompleteOptions: { - label: string - value: string - }[] = useMemo(() => { - const options: { label: string; value: string }[] = [] - if (tagsMap.size === 0) { - if (tagText.trim() !== '') { - options.push({ - label: `${translate(lngKeys.GeneralCreate)}: "${tagText}"`, - value: tagText, - }) - } - return options - } - - const allTags = [...tagsMap.values()] - const missingTags = allTags - .filter((tag) => !tagsIdsAlreadyInDoc.includes(tag.id)) - .filter((tag) => tag.text.toLowerCase().startsWith(tagText.toLowerCase())) - - const missingTagsTextArray = missingTags.map((tag) => tag.text) - - if (tagText.trim() !== '') { - const exactMatchIsFound = allTags.map((tag) => tag.text).includes(tagText) - if (!exactMatchIsFound) { - options.push({ label: `Create "${tagText}"`, value: tagText }) + const autoCompleteOptions: LabelLikeTag[] = useMemo(() => { + const allTags = filterIter( + (tag) => !tagsIdsAlreadyInDoc.has(tag.id), + tagsMap.values() + ).map((label) => ({ ...label, name: label.text })) + + allTags.sort((a, b) => { + if (a.text < b.text) { + return -1 + } else { + return 1 } - } - - options.push( - ...missingTagsTextArray - .map((tag) => { - return { label: tag, value: tag } - }) - .sort((a, b) => { - if (a.value < b.value) { - return -1 - } else { - return 1 - } - }) - ) + }) - return options - }, [tagsMap, tagsIdsAlreadyInDoc, tagText, translate]) + return allTags + }, [tagsMap, tagsIdsAlreadyInDoc]) const createTagHandler = useCallback( - async (tagText: string) => { - if (sending || tagText.trim() === '') { + async (newTag: LabelLike | null) => { + if (sending || newTag == null || newTag.name === '') { return } @@ -155,11 +127,11 @@ const TagsSelectorModal = ({ team, doc }: TagsSelectorModalProps) => { try { const { doc: newDoc, tag } = await createTag(team, { docId: doc.id, - text: tagText, + text: newTag.name, + backgroundColor: newTag.backgroundColor, }) updateTagsMap([tag.id, tag]) updateDocsMap([newDoc.id, newDoc]) - setTagText('') closeLastModal() } catch (error) { pushApiErrorMessage(error) @@ -178,21 +150,24 @@ const TagsSelectorModal = ({ team, doc }: TagsSelectorModalProps) => { ] ) - const selectOptionHandler = useCallback( - async (event: any, option: string) => { - event.preventDefault() - setTagText(option) - createTagHandler(option) - }, - [createTagHandler] - ) - - const inputOnChangeEvent = useCallback( - (event: React.ChangeEvent) => { - event.preventDefault() - setTagText(event.target.value) + const updateTagHandler = useCallback( + async (tag: LabelLikeTag) => { + if (sending) { + return + } + try { + setSending(true) + await updateTagApi(tag, { + text: tag.name, + backgroundColor: tag.backgroundColor, + }) + } catch (err) { + pushApiErrorMessage(err) + } finally { + setSending(false) + } }, - [setTagText] + [updateTagApi, pushApiErrorMessage, sending] ) useUpDownNavigationListener(containerRef, { @@ -200,95 +175,16 @@ const TagsSelectorModal = ({ team, doc }: TagsSelectorModalProps) => { }) return ( - - - - {sending && } - - {!sending && autoCompleteOptions.length > 0 && ( -
- {autoCompleteOptions.map((option, i) => ( - selectOptionHandler(e, option.value)} - > - {option.label} - - ))} -
- )} -
+ ) } -const ModalContainer = styled.div` - .autocomplete__input { - flex: 1 1 auto; - line-height: inherit !important; - height: 28px !important; - width: 100%; - } - - .autocomplete__container { - padding: ${({ theme }) => theme.sizes.spaces.xsm}px 0; - width: 100%; - height: auto; - max-width: auto; - max-height: 150px; - overflow-y: auto; - border-style: solid; - border-width: 1px; - border-radius: 4px; - display: inline-flex; - flex-direction: column; - border: none; - left: 0; - top: 100%; - background-color: ${({ theme }) => theme.colors.background.primary}; - box-shadow: ${({ theme }) => theme.colors.shadow}; - } - - .autocomplete__option { - width: 100%; - display: block; - flex-shrink: 0; - padding: ${({ theme }) => theme.sizes.spaces.sm}px - ${({ theme }) => theme.sizes.spaces.df}px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: ${({ theme }) => theme.colors.text.subtle}; - text-decoration: none; - - &:hover, - &:focus { - background: ${({ theme }) => theme.colors.background.quaternary}; - color: ${({ theme }) => theme.colors.text.primary}; - } - } - - .tag__add__input__spinner { - flex: 0 0 auto; - border-color: ${({ theme }) => theme.colors.variants.primary.text}; - border-right-color: transparent; - display: inline-flex; - margin-left: ${({ theme }) => theme.sizes.spaces.xsm}px; - margin-right: ${({ theme }) => theme.sizes.spaces.xsm}px; - } -` - const Container = styled.div` &.doc__tags__create--empty { width: 100%; diff --git a/src/cloud/components/DocTagsListItem.tsx b/src/cloud/components/DocTagsListItem.tsx index f7915b6fb3..cdb0f77023 100644 --- a/src/cloud/components/DocTagsListItem.tsx +++ b/src/cloud/components/DocTagsListItem.tsx @@ -42,7 +42,12 @@ const DocTagsListItem = ({ : 'doc__tags__list__item--white', className, ])} - style={{ backgroundColor: getColorFromString(tag.id) }} + style={{ + backgroundColor: + tag.backgroundColor != null + ? tag.backgroundColor + : getColorFromString(tag.id), + }} > {showLink ? ( ) } diff --git a/src/cloud/interfaces/db/tag.ts b/src/cloud/interfaces/db/tag.ts index e3b405b337..2c90e3cc7a 100644 --- a/src/cloud/interfaces/db/tag.ts +++ b/src/cloud/interfaces/db/tag.ts @@ -5,6 +5,7 @@ export interface SerializableTagProps { id: string text: string teamId: string + backgroundColor?: string } export interface SerializedUnserializableTagProps { From 7867cd1b3ca32aea4f84a2c65fcc033abdc31ba5 Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Mon, 29 Nov 2021 11:07:12 +0900 Subject: [PATCH 13/65] display empty option --- src/cloud/components/Props/Pickers/StatusSelect.tsx | 1 + src/cloud/components/molecules/LabelManager.tsx | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cloud/components/Props/Pickers/StatusSelect.tsx b/src/cloud/components/Props/Pickers/StatusSelect.tsx index e16b0be5a1..5fbba68e0f 100644 --- a/src/cloud/components/Props/Pickers/StatusSelect.tsx +++ b/src/cloud/components/Props/Pickers/StatusSelect.tsx @@ -106,6 +106,7 @@ const StatusSelector = ({ onUpdate={editStatus} onDelete={removeStatus} type='Status' + allowEmpty={true} /> ) } diff --git a/src/cloud/components/molecules/LabelManager.tsx b/src/cloud/components/molecules/LabelManager.tsx index 925708de7d..c6b922ec08 100644 --- a/src/cloud/components/molecules/LabelManager.tsx +++ b/src/cloud/components/molecules/LabelManager.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { mdiDotsHorizontal, mdiTrashCanOutline } from '@mdi/js' import { useEffectOnce } from 'react-use' -import { getColorFromString } from '../../lib/utils/string' +import { capitalize, getColorFromString } from '../../lib/utils/string' import { useModal } from '../../../design/lib/stores/modal' import Flexbox from '../../../design/components/atoms/Flexbox' import Icon from '../../../design/components/atoms/Icon' @@ -27,6 +27,7 @@ interface LabelManagerProps { onDelete: (label: T) => void sending?: boolean type?: string + allowEmpty?: boolean } const LabelManager = ({ @@ -37,6 +38,7 @@ const LabelManager = ({ onDelete, type = 'label', sending = false, + allowEmpty = false, }: LabelManagerProps) => { const inputRef = useRef(null) const containerRef = useRef(null) @@ -97,8 +99,11 @@ const LabelManager = ({ }, [labels, labelName]) const showNoStatus = useMemo(() => { - return labelName === '' || 'No Status'.startsWith(labelName) - }, [labelName]) + return ( + (labelName === '' || `No ${capitalize(type)}`.startsWith(labelName)) && + allowEmpty + ) + }, [labelName, allowEmpty, type]) useUpDownNavigationListener(containerRef, { overrideInput: true, From 428f3410623f99b4f7504c1c7be022a375fe58a4 Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Mon, 29 Nov 2021 11:24:09 +0900 Subject: [PATCH 14/65] move LabelManager to design folder --- .../DocTagsList/TagsAutoCompleteInput.tsx | 4 +++- .../Modal/contents/SmartView/StatusSelect.tsx | 9 +++---- .../components/Props/Pickers/StatusSelect.tsx | 4 +++- .../components/molecules/LabelManager.tsx | 24 +++++++++---------- 4 files changed, 21 insertions(+), 20 deletions(-) rename src/{cloud => design}/components/molecules/LabelManager.tsx (90%) diff --git a/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx b/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx index 9a12ddeff4..387c83aec5 100644 --- a/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx +++ b/src/cloud/components/DocPage/DocTagsList/TagsAutoCompleteInput.tsx @@ -14,7 +14,9 @@ import DocPropertyValueButton from '../../Props/Pickers/PropertyValueButton' import Icon from '../../../../design/components/atoms/Icon' import { useModal } from '../../../../design/lib/stores/modal' import { useEffectOnce } from 'react-use' -import LabelManager, { LabelLike } from '../../molecules/LabelManager' +import LabelManager, { + LabelLike, +} from '../../../../design/components/molecules/LabelManager' import { filterIter } from '../../../lib/utils/iterator' import { SerializedTag } from '../../../interfaces/db/tag' import { useCloudApi } from '../../../lib/hooks/useCloudApi' diff --git a/src/cloud/components/Modal/contents/SmartView/StatusSelect.tsx b/src/cloud/components/Modal/contents/SmartView/StatusSelect.tsx index eca8697d4d..11624b33ba 100644 --- a/src/cloud/components/Modal/contents/SmartView/StatusSelect.tsx +++ b/src/cloud/components/Modal/contents/SmartView/StatusSelect.tsx @@ -1,8 +1,8 @@ import React, { useMemo } from 'react' +import { Label } from '../../../../../design/components/atoms/Label' import FormSelect from '../../../../../design/components/molecules/Form/atoms/FormSelect' import { usePage } from '../../../../lib/stores/pageStore' import { useStatuses } from '../../../../lib/stores/status' -import { StatusView } from '../../../Props/Pickers/StatusSelect' interface StatusSelectProps { value: number @@ -12,7 +12,7 @@ interface StatusSelectProps { } const NO_STATUS = { - label: , + label:
Date: Thu, 9 Dec 2021 19:03:03 +0900 Subject: [PATCH 51/65] change kanban icon --- src/cloud/lib/views/index.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/cloud/lib/views/index.ts b/src/cloud/lib/views/index.ts index 6ecf32c5f1..d9968ea546 100644 --- a/src/cloud/lib/views/index.ts +++ b/src/cloud/lib/views/index.ts @@ -1,8 +1,4 @@ -import { - mdiCalendarMonthOutline, - mdiTable, - mdiViewParallelOutline, -} from '@mdi/js' +import { mdiCalendarMonthOutline, mdiTable, mdiViewWeek } from '@mdi/js' import { SupportedViewTypes } from '../../interfaces/db/view' export type ViewMoveType = @@ -18,7 +14,7 @@ export function getIconPathOfViewType(type: SupportedViewTypes) { case 'calendar': return mdiCalendarMonthOutline case 'kanban': - return mdiViewParallelOutline + return mdiViewWeek default: case 'table': return mdiTable From e2586c2f3a9e87c413d08097f5f0ad94f1243ce2 Mon Sep 17 00:00:00 2001 From: davy-c Date: Thu, 9 Dec 2021 19:03:24 +0900 Subject: [PATCH 52/65] padding view selector --- src/cloud/components/Views/ViewsSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cloud/components/Views/ViewsSelector.tsx b/src/cloud/components/Views/ViewsSelector.tsx index 7e08ff1ee6..b7d0f9aa80 100644 --- a/src/cloud/components/Views/ViewsSelector.tsx +++ b/src/cloud/components/Views/ViewsSelector.tsx @@ -115,7 +115,7 @@ const Container = styled.div` flex: 1 1 auto; align-items: center; flex-wrap: wrap; - padding-top: ${({ theme }) => theme.sizes.spaces.df}px; + padding-top: ${({ theme }) => theme.sizes.spaces.sm}px; padding-bottom: ${({ theme }) => theme.sizes.spaces.sm}px; padding-left: ${({ theme }) => theme.sizes.spaces.sm}px; From 6c843a5abbbef42475d0fc70409861020f519554 Mon Sep 17 00:00:00 2001 From: davy-c Date: Thu, 9 Dec 2021 19:20:32 +0900 Subject: [PATCH 53/65] toggable input for kanban list --- src/cloud/components/Views/Kanban/index.tsx | 51 +++++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/cloud/components/Views/Kanban/index.tsx b/src/cloud/components/Views/Kanban/index.tsx index 6855e579b1..f6080481c0 100644 --- a/src/cloud/components/Views/Kanban/index.tsx +++ b/src/cloud/components/Views/Kanban/index.tsx @@ -8,17 +8,20 @@ import Button from '../../../../design/components/atoms/Button' import Flexbox from '../../../../design/components/atoms/Flexbox' import Icon from '../../../../design/components/atoms/Icon' import { Label } from '../../../../design/components/atoms/Label' +import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' import Kanban from '../../../../design/components/organisms/Kanban' import { useModal } from '../../../../design/lib/stores/modal' import styled from '../../../../design/lib/styled' import { SerializedDocWithSupplemental } from '../../../interfaces/db/doc' import { SerializedTeam } from '../../../interfaces/db/team' import { SerializedView } from '../../../interfaces/db/view' -import { useCloudResourceModals } from '../../../lib/hooks/useCloudResourceModals' +import { useCloudApi } from '../../../lib/hooks/useCloudApi' +import { useI18n } from '../../../lib/hooks/useI18n' import { KanbanViewList, useKanbanView, } from '../../../lib/hooks/views/kanbanView' +import { lngKeys } from '../../../lib/i18n/types' import { useRouter } from '../../../lib/router' import { useStatuses } from '../../../lib/stores/status' import { getDocLinkHref } from '../../Link/DocLink' @@ -63,7 +66,8 @@ const KanbanView = ({ }) const { openContextModal, closeLastModal } = useModal() const { push } = useRouter() - const { openNewDocForm } = useCloudResourceModals() + const { createDoc } = useCloudApi() + const { translate } = useI18n() const addListRef = useRef(addList) useEffect(() => { @@ -159,13 +163,16 @@ const KanbanView = ({ ? statuses.find((status) => status.id === parseInt(list.id)) : undefined return ( - + /> ) }, - [openNewDocForm, prop, statuses, team, currentFolderId, currentWorkspaceId] + [ + createDoc, + translate, + prop, + statuses, + team, + currentFolderId, + currentWorkspaceId, + ] ) const setPropRef = useRef(setProp) @@ -285,4 +290,10 @@ const Container = styled.div` .kanban__item--header > span:hover { cursor: grab; } + + .kanban__list__footer > button, + .kanban__list__footer > input { + width: 100%; + justify-content: flex-start; + } ` From f8abe40add32f6ae15756cf426ff779b16787921 Mon Sep 17 00:00:00 2001 From: davy-c Date: Fri, 10 Dec 2021 05:31:39 +0900 Subject: [PATCH 54/65] prevent focus bg with kanban --- src/cloud/components/Views/Kanban/Item.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cloud/components/Views/Kanban/Item.tsx b/src/cloud/components/Views/Kanban/Item.tsx index 9f1e1eab96..3c3f34f367 100644 --- a/src/cloud/components/Views/Kanban/Item.tsx +++ b/src/cloud/components/Views/Kanban/Item.tsx @@ -27,6 +27,12 @@ const Item = ({ doc, onClick }: ItemProps) => { const Container = styled(NavigationItem)` min-height: 25px; background-color: ${({ theme }) => theme.colors.background.secondary}; + + &:focus, + &.navigation__item--focused { + background-color: ${({ theme }) => + theme.colors.background.secondary} !important; + } ` export default Item From 0906de54440c7cbdf5e920776c0a03a4bc9a8a24 Mon Sep 17 00:00:00 2001 From: davy-c Date: Fri, 10 Dec 2021 05:51:25 +0900 Subject: [PATCH 55/65] mixpanel for views --- src/cloud/components/Views/ViewsSelector.tsx | 15 ++++++++++++++- src/cloud/interfaces/analytics/mixpanel.ts | 11 +++++++++++ src/cloud/lib/hooks/views/viewHandler.ts | 17 ++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/cloud/components/Views/ViewsSelector.tsx b/src/cloud/components/Views/ViewsSelector.tsx index b7d0f9aa80..d17a2e6911 100644 --- a/src/cloud/components/Views/ViewsSelector.tsx +++ b/src/cloud/components/Views/ViewsSelector.tsx @@ -18,6 +18,8 @@ import MetadataContainerBreak from '../../../design/components/organisms/Metadat import MetadataContainerRow from '../../../design/components/organisms/MetadataContainer/molecules/MetadataContainerRow' import { useModal } from '../../../design/lib/stores/modal' import styled from '../../../design/lib/styled' +import { trackEvent } from '../../api/track' +import { MixpanelActionTrackTypes } from '../../interfaces/analytics/mixpanel' import { SerializedView, SupportedViewTypes, @@ -52,6 +54,17 @@ const ViewsSelector = ({ [closeLastModal, actionsRef] ) + const selectView = useCallback( + (view: SerializedView) => { + trackEvent(MixpanelActionTrackTypes.ViewOpen, { + trueEventName: `view.${view.type}.open`, + view: view.id, + }) + setSelectedViewId(view.id) + }, + [setSelectedViewId] + ) + return ( {orderedViews.map((view) => ( @@ -61,7 +74,7 @@ const ViewsSelector = ({ variant='icon' iconPath={getIconPathOfViewType(view.type)} iconSize={20} - onClick={() => setSelectedViewId(view.id)} + onClick={() => selectView(view)} active={selectedViewId === view.id} size='sm' className='view__item' diff --git a/src/cloud/interfaces/analytics/mixpanel.ts b/src/cloud/interfaces/analytics/mixpanel.ts index 8bdc63cb32..f4e9a62b05 100644 --- a/src/cloud/interfaces/analytics/mixpanel.ts +++ b/src/cloud/interfaces/analytics/mixpanel.ts @@ -115,8 +115,18 @@ export enum MixpanelActionTrackTypes { TableColAdd = 'table.cols.add', TableColDelete = 'table.cols.delete', TableColUpdateOrder = 'table.cols.update.order', + ViewCreate = 'view.create', + ViewEdit = 'view.edit', + ViewDelete = 'view.delete', + ViewOpen = 'view.open', } +export type MixpanelViewEvent = + | MixpanelActionTrackTypes.ViewCreate + | MixpanelActionTrackTypes.ViewEdit + | MixpanelActionTrackTypes.ViewDelete + | MixpanelActionTrackTypes.ViewOpen + export type MixpanelFrontEvent = | MixpanelActionTrackTypes.RevisionHistoryOpen | MixpanelActionTrackTypes.ThemeChangeApp @@ -159,6 +169,7 @@ export type MixpanelFrontEvent = | MixpanelActionTrackTypes.TableColAdd | MixpanelActionTrackTypes.TableColDelete | MixpanelActionTrackTypes.TableColUpdateOrder + | MixpanelViewEvent export type MixpanelUserEvent = MixpanelActionTrackTypes.AccountDelete diff --git a/src/cloud/lib/hooks/views/viewHandler.ts b/src/cloud/lib/hooks/views/viewHandler.ts index 6f90298476..4f02f2936e 100644 --- a/src/cloud/lib/hooks/views/viewHandler.ts +++ b/src/cloud/lib/hooks/views/viewHandler.ts @@ -16,6 +16,8 @@ import { filterIter } from '../../utils/iterator' import { useNav } from '../../stores/nav' import { ViewMoveType } from '../../views' import { sortByLexorankProperty } from '../../utils/string' +import { trackEvent } from '../../../api/track' +import { MixpanelActionTrackTypes } from '../../../interfaces/analytics/mixpanel' interface ViewHandlerStoreProps { parent: ViewParent @@ -88,7 +90,12 @@ export function useViewHandler({ : { smartView: parent.target.id, type, name } ) if (!res.err) { - selectNewView((res.data as CreateViewResponseBody).data.id) + const view = (res.data as CreateViewResponseBody).data + trackEvent(MixpanelActionTrackTypes.ViewCreate, { + trueEventName: `view.${view.type}.create`, + view: view.id, + }) + selectNewView(view.id) } return res }, @@ -99,6 +106,10 @@ export function useViewHandler({ async (view: SerializedView) => { const res = await deleteViewApi(view) if (!res.err) { + trackEvent(MixpanelActionTrackTypes.ViewDelete, { + trueEventName: `view.${view.type}.delete`, + view: view.id, + }) const children = filterIter((v) => v.id !== view.id, childrenViews) selectNewView( children.length !== 0 @@ -113,6 +124,10 @@ export function useViewHandler({ const updateView = useCallback( async (view: SerializedView, body: Omit) => { + trackEvent(MixpanelActionTrackTypes.ViewEdit, { + trueEventName: `view.${view.type}.edit`, + view: view.id, + }) return updateViewApi(view, body) }, [updateViewApi] From 085d9c83621e26b10826ca4e419286c5167315b5 Mon Sep 17 00:00:00 2001 From: davy-c Date: Fri, 10 Dec 2021 05:53:44 +0900 Subject: [PATCH 56/65] new doc button same size as kanban items --- src/cloud/components/Views/Kanban/index.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cloud/components/Views/Kanban/index.tsx b/src/cloud/components/Views/Kanban/index.tsx index f6080481c0..2b9aac2c60 100644 --- a/src/cloud/components/Views/Kanban/index.tsx +++ b/src/cloud/components/Views/Kanban/index.tsx @@ -291,9 +291,12 @@ const Container = styled.div` cursor: grab; } + .kanban__list__footer, .kanban__list__footer > button, .kanban__list__footer > input { width: 100%; justify-content: flex-start; + height: 26px; + min-height: 26px; } ` From e50c00164dff41dc85f5ce3652fc2ada261f96b1 Mon Sep 17 00:00:00 2001 From: davy-c Date: Fri, 10 Dec 2021 06:03:45 +0900 Subject: [PATCH 57/65] fix kanban item global margins --- src/cloud/components/Views/Kanban/index.tsx | 5 +++++ src/design/components/organisms/Kanban/index.tsx | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cloud/components/Views/Kanban/index.tsx b/src/cloud/components/Views/Kanban/index.tsx index 2b9aac2c60..c6dd3e91ce 100644 --- a/src/cloud/components/Views/Kanban/index.tsx +++ b/src/cloud/components/Views/Kanban/index.tsx @@ -299,4 +299,9 @@ const Container = styled.div` height: 26px; min-height: 26px; } + + .kanban__item { + cursor: grab; + margin: ${({ theme }) => theme.sizes.spaces.df}px 0; + } ` diff --git a/src/design/components/organisms/Kanban/index.tsx b/src/design/components/organisms/Kanban/index.tsx index 1320306e6e..557c4519fd 100644 --- a/src/design/components/organisms/Kanban/index.tsx +++ b/src/design/components/organisms/Kanban/index.tsx @@ -163,11 +163,6 @@ const KanbanContainer = styled.div` min-height: 250px; background-color: ${({ theme }) => theme.colors.background.primary}; padding-bottom: ${({ theme }) => theme.sizes.spaces.df}px; - - & .kanban__item { - cursor: grab; - margin: ${({ theme }) => theme.sizes.spaces.df}px 0; - } ` export default Kanban From 6027de5821e2df1e05be10c050c1170cfd38814f Mon Sep 17 00:00:00 2001 From: davy-c Date: Fri, 10 Dec 2021 06:10:09 +0900 Subject: [PATCH 58/65] change + button style in calendar --- .../components/Views/Calendar/CalendarView.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/cloud/components/Views/Calendar/CalendarView.tsx b/src/cloud/components/Views/Calendar/CalendarView.tsx index 04f4d63d5d..e62dfbb858 100644 --- a/src/cloud/components/Views/Calendar/CalendarView.tsx +++ b/src/cloud/components/Views/Calendar/CalendarView.tsx @@ -333,12 +333,19 @@ const Container = styled.div` .fc .fc-daygrid-day { position: relative; } + .fc .fc-daygrid-day:hover::before { content: '+'; + font-size: 18px; + border-radius: ${({ theme }) => theme.borders.radius}px; position: absolute; - top: 2px; - left: 2px; - color: ${({ theme }) => theme.colors.text.subtle}; + top: 4px; + left: 4px; + color: ${({ theme }) => theme.colors.text.primary}; + background-color: ${({ theme }) => theme.colors.background.tertiary}; + padding: 1px 7px; + display: flex; + align-items: center; } .fc-theme-standard td, From 319de0a87ba57e96fb7238f3990d309460c35caf Mon Sep 17 00:00:00 2001 From: davy-c Date: Fri, 10 Dec 2021 06:22:43 +0900 Subject: [PATCH 59/65] add number and text in add modal --- src/cloud/components/Props/PropAddModal.tsx | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/cloud/components/Props/PropAddModal.tsx b/src/cloud/components/Props/PropAddModal.tsx index 7c8011f2c0..b2d7de14c3 100644 --- a/src/cloud/components/Props/PropAddModal.tsx +++ b/src/cloud/components/Props/PropAddModal.tsx @@ -311,6 +311,33 @@ const PropsAddModal = ({ }, }} /> + + addNewPropCol(columnName, 'number'), + }, + }} + /> + addNewPropCol(columnName, 'string'), + }, + }} + /> ) : ( Date: Fri, 3 Dec 2021 20:05:49 +0900 Subject: [PATCH 60/65] Implement updating page ordering --- package.json | 1 + src/cloud/api/teams/folders/index.ts | 25 ++++ .../Views/FolderList/FolderListHeader.tsx | 99 ++++++++++++++++ .../FolderList/ViewManagerContentRow.tsx | 83 ++++++-------- .../Views/FolderList/ViewsFolderList.tsx | 108 ++++++++++++++---- src/cloud/interfaces/db/folder.ts | 1 + src/cloud/lib/hooks/useCloudApi.ts | 18 +++ 7 files changed, 263 insertions(+), 72 deletions(-) create mode 100644 src/cloud/components/Views/FolderList/FolderListHeader.tsx diff --git a/package.json b/package.json index 3d2829df4e..886e2c3f8d 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "@fullcalendar/react": "^5.10.1", "@dnd-kit/core": "^4.0.3", "@dnd-kit/sortable": "^5.1.0", + "@dnd-kit/utilities": "^3.0.1", "@mdi/js": "^5.9.55", "@mdi/react": "^1.2.1", "@stripe/react-stripe-js": "^1.2.0", diff --git a/src/cloud/api/teams/folders/index.ts b/src/cloud/api/teams/folders/index.ts index 5d90354aaf..073a551043 100644 --- a/src/cloud/api/teams/folders/index.ts +++ b/src/cloud/api/teams/folders/index.ts @@ -68,6 +68,31 @@ export async function updateFolder( ) return data } +export interface UpdateFolderEmojiResponseBody { + folder: SerializedFolderWithBookmark +} + +export interface UpdateFolderResponseBody { + folders: SerializedFolderWithBookmark[] + docs: SerializedDocWithSupplemental[] + workspaces?: SerializedWorkspace[] +} + +export async function updateFolderPageOrder( + folder: SerializedFolder, + moveAheadOf: string +) { + const data = await callApi( + `api/teams/${folder.teamId}/folders/${folder.id}/page-order`, + { + json: { + moveAheadOf, + }, + method: 'put', + } + ) + return data +} export interface DestroyFolderResponseBody { parentFolder?: SerializedFolderWithBookmark diff --git a/src/cloud/components/Views/FolderList/FolderListHeader.tsx b/src/cloud/components/Views/FolderList/FolderListHeader.tsx new file mode 100644 index 0000000000..1241aefb3f --- /dev/null +++ b/src/cloud/components/Views/FolderList/FolderListHeader.tsx @@ -0,0 +1,99 @@ +import React from 'react' +import cc from 'classcat' +import styled from '../../../../design/lib/styled' +import { AppComponent } from '../../../../design/lib/types' +import Checkbox from '../../../../design/components/molecules/Form/atoms/FormCheckbox' + +interface ViewManagerContentRowProps { + checked?: boolean + onSelect: (val: boolean) => void + label: string | React.ReactNode + showCheckbox: boolean + className?: string +} + +const FolderListHeader: AppComponent = ({ + className, + children, + checked, + label, + showCheckbox, + onSelect, +}) => { + return ( + + {showCheckbox && ( + onSelect(!checked)} + /> + )} +
+ {label} +
+ {children != null &&
{children}
} +
+ ) +} + +export default FolderListHeader + +const rowHeight = 40 +const StyledContentManagerRow = styled.div` + display: flex; + flex-wrap: nowrap; + align-items: center; + min-height: ${rowHeight}px; + flex: 1 1 auto; + flex-shrink: 0; + width: 100%; + font-size: 13px; + padding: 0 ${({ theme }) => theme.sizes.spaces.df}px; + + .cm__row__label { + color: ${({ theme }) => theme.colors.text.subtle}; + border-bottom-color: transparent; + } + + &:hover { + .custom-check::before { + border-color: ${({ theme }) => theme.colors.text.secondary}; + } + + .row__checkbox { + opacity: 1; + } + } + + .cm__row__emoji { + flex: 0 0 auto; + margin-right: ${({ theme }) => theme.sizes.spaces.xsm}px; + } + .emoji-icon { + color: ${({ theme }) => theme.colors.text.subtle}; + } + + .cm__row__label { + width: 100%; + display: flex; + flex: 1 1 auto; + align-items: center; + color: ${({ theme }) => theme.colors.text.secondary}; + text-decoration: none; + min-height: ${rowHeight}px; + } + + .row__checkbox { + opacity: 0; + margin-right: ${({ theme }) => theme.sizes.spaces.df}px; + + &.row__checkbox--checked { + opacity: 1; + } + } + + &.content__manager__row--draggedOver { + background: ${({ theme }) => theme.colors.background.quaternary}; + } +` diff --git a/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx b/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx index 1ad369d8cc..92e13ecbb3 100644 --- a/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx +++ b/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx @@ -1,13 +1,16 @@ -import React, { useCallback, useRef, useState } from 'react' +import React, { useCallback } from 'react' import cc from 'classcat' -import { onDragLeaveCb } from '../../../../design/lib/dnd' import styled from '../../../../design/lib/styled' import { AppComponent } from '../../../../design/lib/types' import EmojiIcon from '../../EmojiIcon' import Checkbox from '../../../../design/components/molecules/Form/atoms/FormCheckbox' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import Icon from '../../../../design/components/atoms/Icon' +import { mdiDragVertical } from '@mdi/js' interface ViewManagerContentRowProps { - type?: 'header' | 'row' + id: string checked?: boolean onSelect: (val: boolean) => void label: string | React.ReactNode @@ -22,7 +25,7 @@ interface ViewManagerContentRowProps { } const ViewManagerContentRow: AppComponent = ({ - type = 'row', + id, className, children, checked, @@ -33,14 +36,27 @@ const ViewManagerContentRow: AppComponent = ({ defaultIcon, showCheckbox, onSelect, - onDragStart, - onDragEnd, - onDrop, + // onDragStart, + // onDragEnd, + // onDrop, }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }) + const style = { transform: CSS.Transform.toString(transform), transition } + const LabelTag = labelHref != null || labelOnclick != null ? 'a' : 'div' const navigate: React.MouseEventHandler = useCallback( (e) => { + if (isDragging) { + return + } e.preventDefault() if (labelOnclick == null) { @@ -49,49 +65,22 @@ const ViewManagerContentRow: AppComponent = ({ return labelOnclick() }, - [labelOnclick] + [labelOnclick, isDragging] ) - const [draggedOver, setDraggedOver] = useState(false) - const dragRef = useRef(null) + // const [draggedOver, setDraggedOver] = useState(false) + // const dragRef = useRef(null) return ( { - event.stopPropagation() - if (onDrop != null) { - onDrop(event) - } - setDraggedOver(false) - }} - onDragStart={(event: any) => { - event.stopPropagation() - if (onDragStart != null) { - onDragStart(event) - } - }} - onDragOver={(event: any) => { - event.preventDefault() - event.stopPropagation() - setDraggedOver(true) - }} - onDragLeave={(event: any) => { - onDragLeaveCb(event, dragRef, () => { - setDraggedOver(false) - }) - }} - onDragEnd={(event: any) => { - if (onDragEnd != null) { - onDragEnd(event) - } - }} + ref={setNodeRef} + style={style} + {...attributes} > {showCheckbox && ( = ({ toggle={() => onSelect(!checked)} /> )} +
+ +
theme.sizes.spaces.xsm}px; } - &.cm__row--header .cm__row__label { - color: ${({ theme }) => theme.colors.text.subtle}; - border-bottom-color: transparent; - } - &:hover { - &:not(.cm__row--header) { - background: rgba(0, 0, 0, 0.1); - } + background: rgba(0, 0, 0, 0.1); .custom-check::before { border-color: ${({ theme }) => theme.colors.text.secondary}; } diff --git a/src/cloud/components/Views/FolderList/ViewsFolderList.tsx b/src/cloud/components/Views/FolderList/ViewsFolderList.tsx index 27ea06bb06..76d5627033 100644 --- a/src/cloud/components/Views/FolderList/ViewsFolderList.tsx +++ b/src/cloud/components/Views/FolderList/ViewsFolderList.tsx @@ -1,7 +1,6 @@ import { mdiPlus } from '@mdi/js' import React, { useCallback, useMemo } from 'react' import FormToggableInput from '../../../../design/components/molecules/Form/atoms/FormToggableInput' -import { sortByAttributeAsc } from '../../../../design/lib/utils/array' import { SerializedFolderWithBookmark } from '../../../interfaces/db/folder' import { SerializedTeam } from '../../../interfaces/db/team' import { DraggedTo } from '../../../lib/dnd' @@ -11,8 +10,25 @@ import { useI18n } from '../../../lib/hooks/useI18n' import { lngKeys } from '../../../lib/i18n/types' import { useRouter } from '../../../lib/router' import { folderToDataTransferItem } from '../../../lib/utils/patterns' +import { sortByLexorankProperty } from '../../../lib/utils/string' import { getFolderHref } from '../../Link/FolderLink' import ViewManagerContentRow from './ViewManagerContentRow' +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import FolderListHeader from './FolderListHeader' +import { updateFolderPageOrder } from '../../../api/teams/folders' interface ViewsFolderListProps { folders?: SerializedFolderWithBookmark[] @@ -54,7 +70,7 @@ export const ViewsFolderList = ({ return [] } - return sortByAttributeAsc('name', folders) + return sortByLexorankProperty(folders, 'pageOrder') }, [folders]) const selectingAllFolders = useMemo(() => { @@ -88,41 +104,87 @@ export const ViewsFolderList = ({ [dropInDocOrFolder] ) + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ) + + async function handleDragEnd(event: DragEndEvent) { + const { active, over } = event + + if (over == null || active.id === over.id) { + return + } + + let activeItemIndex = 0 + let overItemIndex = 0 + for (let i = 0; i < orderedFolders.length; i++) { + const folder = orderedFolders[i] + if (folder.id === active.id) { + activeItemIndex = i + } else if (folder.id === over.id) { + overItemIndex = i + } + } + + const movingForward = activeItemIndex < overItemIndex + + const moveAheadOf = movingForward + ? orderedFolders[overItemIndex + 1]?.id + : orderedFolders[overItemIndex].id + + await updateFolderPageOrder(orderedFolders[activeItemIndex], moveAheadOf) + } + if (folders == null) { return null } return ( <> - - {orderedFolders.map((folder) => { - const href = getFolderHref(folder, team, 'index') - return ( - toggleFolderInSelection(folder.id)} - showCheckbox={currentUserIsCoreMember} - label={folder.name} - emoji={folder.emoji} - labelHref={href} - labelOnclick={() => push(href)} - onDragStart={(event: any) => onDragStartFolder(event, folder)} - onDragEnd={(event: any) => clearDragTransferData(event)} - onDrop={(event: any) => onDropFolder(event, folder)} - /> - ) - })} + + folder.id)} + strategy={verticalListSortingStrategy} + > + {orderedFolders.map((folder) => { + const { id } = folder + const href = getFolderHref(folder, team, 'index') + return ( + toggleFolderInSelection(folder.id)} + showCheckbox={currentUserIsCoreMember} + label={folder.name} + emoji={folder.emoji} + labelHref={href} + labelOnclick={() => push(href)} + onDragStart={(event: any) => onDragStartFolder(event, folder)} + onDragEnd={(event: any) => clearDragTransferData(event)} + onDrop={(event: any) => onDropFolder(event, folder)} + /> + ) + })} + + {currentWorkspaceId != null && (
diff --git a/src/cloud/interfaces/db/folder.ts b/src/cloud/interfaces/db/folder.ts index ab0ee28537..4b5722807b 100644 --- a/src/cloud/interfaces/db/folder.ts +++ b/src/cloud/interfaces/db/folder.ts @@ -17,6 +17,7 @@ export interface SerializableFolderProps { childDocsIds: string[] childFoldersIds: string[] workspaceId: string + pageOrder?: string } export interface SerializedUnserializableFolderProps { diff --git a/src/cloud/lib/hooks/useCloudApi.ts b/src/cloud/lib/hooks/useCloudApi.ts index 8fa7b49abf..2be65e0769 100644 --- a/src/cloud/lib/hooks/useCloudApi.ts +++ b/src/cloud/lib/hooks/useCloudApi.ts @@ -34,6 +34,7 @@ import { UpdateFolderRequestBody, UpdateFolderResponseBody, updateFolderEmoji, + updateFolderPageOrder, } from '../../api/teams/folders' import { createFolderBookmark, @@ -627,6 +628,22 @@ export function useCloudApi() { [pageFolder, updateFoldersMap, setPartialPageData, send] ) + const updateFolderPageOrderApi = useCallback( + async (target: SerializedFolder, moveAheadOf: string) => { + await send(target.id, 'pageOrder', { + api: () => updateFolderPageOrder(target, moveAheadOf), + cb: ({ folder }: UpdateFolderEmojiResponseBody) => { + updateFoldersMap([folder.id, folder]) + + if (pageFolder != null && folder.id === pageFolder.id) { + setPartialPageData({ pageFolder: folder }) + } + }, + }) + }, + [pageFolder, updateFoldersMap, setPartialPageData, send] + ) + const deleteWorkspaceApi = useCallback( async (workspace: { id: string; teamId: string; default: boolean }) => { await send(workspace.id, 'delete', { @@ -896,6 +913,7 @@ export function useCloudApi() { updateDoc: updateDocApi, updateDocEmoji: updateDocEmojiApi, updateFolderEmoji: updateFolderEmojiApi, + updateFolderPageOrder: updateFolderPageOrderApi, deleteWorkspaceApi, deleteFolderApi, deleteDocApi, From deb47931118dd56320b5954423a6b0ed0ce251b4 Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Mon, 6 Dec 2021 16:10:17 +0900 Subject: [PATCH 61/65] Use updateFolderOrder method via cloud api hook --- src/cloud/components/Views/FolderList/ViewsFolderList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cloud/components/Views/FolderList/ViewsFolderList.tsx b/src/cloud/components/Views/FolderList/ViewsFolderList.tsx index 76d5627033..72f58c502a 100644 --- a/src/cloud/components/Views/FolderList/ViewsFolderList.tsx +++ b/src/cloud/components/Views/FolderList/ViewsFolderList.tsx @@ -28,7 +28,6 @@ import { verticalListSortingStrategy, } from '@dnd-kit/sortable' import FolderListHeader from './FolderListHeader' -import { updateFolderPageOrder } from '../../../api/teams/folders' interface ViewsFolderListProps { folders?: SerializedFolderWithBookmark[] @@ -57,7 +56,7 @@ export const ViewsFolderList = ({ }: ViewsFolderListProps) => { const { translate } = useI18n() const { push } = useRouter() - const { createFolder } = useCloudApi() + const { createFolder, updateFolderPageOrder } = useCloudApi() const { dropInDocOrFolder, From a9a9bd8dd502e68f32a7e53d44a1f8e3f391c2ff Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 7 Dec 2021 13:15:57 +0900 Subject: [PATCH 62/65] Fix dnd behavior of content row and ordering handle style --- .../FolderList/ViewManagerContentRow.tsx | 58 ++++++++++++++++--- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx b/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx index 92e13ecbb3..4cbe3e855b 100644 --- a/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx +++ b/src/cloud/components/Views/FolderList/ViewManagerContentRow.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useRef, useState } from 'react' import cc from 'classcat' import styled from '../../../../design/lib/styled' import { AppComponent } from '../../../../design/lib/types' @@ -8,6 +8,7 @@ import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import Icon from '../../../../design/components/atoms/Icon' import { mdiDragVertical } from '@mdi/js' +import { onDragLeaveCb } from '../../../../design/lib/dnd' interface ViewManagerContentRowProps { id: string @@ -36,9 +37,9 @@ const ViewManagerContentRow: AppComponent = ({ defaultIcon, showCheckbox, onSelect, - // onDragStart, - // onDragEnd, - // onDrop, + onDragStart, + onDragEnd, + onDrop, }) => { const { attributes, @@ -50,6 +51,8 @@ const ViewManagerContentRow: AppComponent = ({ } = useSortable({ id }) const style = { transform: CSS.Transform.toString(transform), transition } + const [draggedOver, setDraggedOver] = useState(false) + const dragRef = useRef(null) const LabelTag = labelHref != null || labelOnclick != null ? 'a' : 'div' const navigate: React.MouseEventHandler = useCallback( @@ -68,19 +71,45 @@ const ViewManagerContentRow: AppComponent = ({ [labelOnclick, isDragging] ) - // const [draggedOver, setDraggedOver] = useState(false) - // const dragRef = useRef(null) - return ( { + event.stopPropagation() + if (onDrop != null) { + onDrop(event) + } + setDraggedOver(false) + }} + onDragStart={(event: any) => { + event.stopPropagation() + if (onDragStart != null) { + onDragStart(event) + } + }} + onDragOver={(event: any) => { + event.preventDefault() + event.stopPropagation() + setDraggedOver(true) + }} + onDragLeave={(event: any) => { + onDragLeaveCb(event, dragRef, () => { + setDraggedOver(false) + }) + }} + onDragEnd={(event: any) => { + if (onDragEnd != null) { + onDragEnd(event) + } + }} > {showCheckbox && ( = ({ toggle={() => onSelect(!checked)} /> )} -
+
theme.sizes.spaces.df}px; + .cm__row__orderingHandle { + opacity: 0; + } + .cm__row__status, .cm__row__emoji { height: 100%; @@ -149,6 +182,13 @@ const StyledContentManagerRow = styled.div` .row__checkbox { opacity: 1; } + .cm__row__orderingHandle { + opacity: 1; + cursor: grab; + &:active { + cursor: grabbing; + } + } } .cm__row__emoji { From e9d38dcb1dced535d30fea3c8756b6f513d3ec5a Mon Sep 17 00:00:00 2001 From: Junyoung Choi Date: Tue, 7 Dec 2021 17:55:13 +0900 Subject: [PATCH 63/65] Rename components of FolderList --- .../{ViewsFolderList.tsx => FolderList.tsx} | 12 ++-- ...nagerContentRow.tsx => FolderListItem.tsx} | 70 ++++++++++--------- src/cloud/components/Views/index.tsx | 4 +- 3 files changed, 45 insertions(+), 41 deletions(-) rename src/cloud/components/Views/FolderList/{ViewsFolderList.tsx => FolderList.tsx} (96%) rename src/cloud/components/Views/FolderList/{ViewManagerContentRow.tsx => FolderListItem.tsx} (76%) diff --git a/src/cloud/components/Views/FolderList/ViewsFolderList.tsx b/src/cloud/components/Views/FolderList/FolderList.tsx similarity index 96% rename from src/cloud/components/Views/FolderList/ViewsFolderList.tsx rename to src/cloud/components/Views/FolderList/FolderList.tsx index 72f58c502a..c940c433b0 100644 --- a/src/cloud/components/Views/FolderList/ViewsFolderList.tsx +++ b/src/cloud/components/Views/FolderList/FolderList.tsx @@ -12,7 +12,7 @@ import { useRouter } from '../../../lib/router' import { folderToDataTransferItem } from '../../../lib/utils/patterns' import { sortByLexorankProperty } from '../../../lib/utils/string' import { getFolderHref } from '../../Link/FolderLink' -import ViewManagerContentRow from './ViewManagerContentRow' +import FolderListItem from './FolderListItem' import { closestCenter, DndContext, @@ -29,7 +29,7 @@ import { } from '@dnd-kit/sortable' import FolderListHeader from './FolderListHeader' -interface ViewsFolderListProps { +interface FolderListProps { folders?: SerializedFolderWithBookmark[] currentUserIsCoreMember: boolean team: SerializedTeam @@ -43,7 +43,7 @@ interface ViewsFolderListProps { resetFoldersInSelection: () => void } -export const ViewsFolderList = ({ +export const FolderList = ({ folders, team, currentUserIsCoreMember, @@ -53,7 +53,7 @@ export const ViewsFolderList = ({ hasFolderInSelection, toggleFolderInSelection, resetFoldersInSelection, -}: ViewsFolderListProps) => { +}: FolderListProps) => { const { translate } = useI18n() const { push } = useRouter() const { createFolder, updateFolderPageOrder } = useCloudApi() @@ -166,7 +166,7 @@ export const ViewsFolderList = ({ const { id } = folder const href = getFolderHref(folder, team, 'index') return ( - void @@ -25,7 +25,7 @@ interface ViewManagerContentRowProps { onDrop?: (event: any) => void } -const ViewManagerContentRow: AppComponent = ({ +const FolderListItem: AppComponent = ({ id, className, children, @@ -72,11 +72,11 @@ const ViewManagerContentRow: AppComponent = ({ ) return ( - = ({ > {showCheckbox && ( onSelect(!checked)} /> )} -
+
-
+
= ({ />
{typeof label === 'string' ? ( - {label} + {label} ) : ( -
{label}
+
{label}
)} - {children != null &&
{children}
} - + {children != null && ( +
{children}
+ )} + ) } -export default ViewManagerContentRow +export default FolderListItem const rowHeight = 40 -const StyledContentManagerRow = styled.div` +const StyledContainer = styled.div` display: flex; flex-wrap: nowrap; align-items: center; @@ -160,12 +165,12 @@ const StyledContentManagerRow = styled.div` font-size: 13px; padding: 0 ${({ theme }) => theme.sizes.spaces.df}px; - .cm__row__orderingHandle { + .folder-list-item__ordering-handle { opacity: 0; } - .cm__row__status, - .cm__row__emoji { + .folder-list-item__status, + .folder-list-item__emoji { height: 100%; display: flex; flex: 0 0 auto; @@ -175,31 +180,30 @@ const StyledContentManagerRow = styled.div` &:hover { background: rgba(0, 0, 0, 0.1); - .custom-check::before { - border-color: ${({ theme }) => theme.colors.text.secondary}; - } - .row__checkbox { - opacity: 1; - } - .cm__row__orderingHandle { + .folder-list-item__ordering-handle { opacity: 1; cursor: grab; &:active { cursor: grabbing; } } + + .folder-list-item__checkbox { + opacity: 1; + } } - .cm__row__emoji { + .folder-list-item__label__emoji { flex: 0 0 auto; margin-right: ${({ theme }) => theme.sizes.spaces.xsm}px; - } - .emoji-icon { - color: ${({ theme }) => theme.colors.text.subtle}; + + .emoji-icon { + color: ${({ theme }) => theme.colors.text.subtle}; + } } - .cm__row__label { + .folder-list-item__label { width: 100%; display: flex; flex: 1 1 auto; @@ -209,16 +213,16 @@ const StyledContentManagerRow = styled.div` min-height: ${rowHeight}px; } - .row__checkbox { + .folder-list-item__checkbox { opacity: 0; margin-right: ${({ theme }) => theme.sizes.spaces.df}px; - &.row__checkbox--checked { + &.folder-list-item__checkbox--checked { opacity: 1; } } - &.content__manager__row--draggedOver { + &.folder-list-item--dragged-over { background: ${({ theme }) => theme.colors.background.quaternary}; } ` diff --git a/src/cloud/components/Views/index.tsx b/src/cloud/components/Views/index.tsx index 01146af6a1..8f381a34bc 100644 --- a/src/cloud/components/Views/index.tsx +++ b/src/cloud/components/Views/index.tsx @@ -11,7 +11,7 @@ import { SerializedView, ViewParent } from '../../interfaces/db/view' import { SerializedWorkspace } from '../../interfaces/db/workspace' import { SerializedTeam } from '../../interfaces/db/team' import TableView from './Table/TableView' -import ViewsFolderList from './FolderList/ViewsFolderList' +import FolderList from './FolderList/FolderList' import Scroller from '../../../design/components/atoms/Scroller' import { sortByLexorankProperty } from '../../lib/utils/string' import CalendarView from './Calendar/CalendarView' @@ -181,7 +181,7 @@ export const ViewsManager = ({ )} - Date: Fri, 10 Dec 2021 08:40:17 +0900 Subject: [PATCH 64/65] change style of kanban item --- src/cloud/components/Views/Kanban/Item.tsx | 3 +-- src/cloud/components/Views/Kanban/index.tsx | 15 +++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/cloud/components/Views/Kanban/Item.tsx b/src/cloud/components/Views/Kanban/Item.tsx index 3c3f34f367..b64e8a4da3 100644 --- a/src/cloud/components/Views/Kanban/Item.tsx +++ b/src/cloud/components/Views/Kanban/Item.tsx @@ -25,9 +25,8 @@ const Item = ({ doc, onClick }: ItemProps) => { } const Container = styled(NavigationItem)` - min-height: 25px; background-color: ${({ theme }) => theme.colors.background.secondary}; - + cursor: grab; &:focus, &.navigation__item--focused { background-color: ${({ theme }) => diff --git a/src/cloud/components/Views/Kanban/index.tsx b/src/cloud/components/Views/Kanban/index.tsx index c6dd3e91ce..59948b6221 100644 --- a/src/cloud/components/Views/Kanban/index.tsx +++ b/src/cloud/components/Views/Kanban/index.tsx @@ -296,12 +296,19 @@ const Container = styled.div` .kanban__list__footer > input { width: 100%; justify-content: flex-start; - height: 26px; - min-height: 26px; } - .kanban__item { - cursor: grab; + .kanban__item, + .kanban__item .navigation__item, + .kanban__list__footer, + .kanban__list__footer > button, + .kanban__list__footer > input { + height: 32px; + min-height: 32px; + } + + .kanban__item, + .kanban__list__footer { margin: ${({ theme }) => theme.sizes.spaces.df}px 0; } ` From f9e1c7059e9b8f2b3446d241a191b87915f68160 Mon Sep 17 00:00:00 2001 From: davy-c Date: Fri, 10 Dec 2021 09:08:04 +0900 Subject: [PATCH 65/65] insert to bottom of list --- src/cloud/components/Views/Kanban/index.tsx | 4 +++- src/cloud/lib/hooks/useCloudApi.ts | 2 +- src/cloud/lib/hooks/views/kanbanView.ts | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/cloud/components/Views/Kanban/index.tsx b/src/cloud/components/Views/Kanban/index.tsx index 59948b6221..2e708acd14 100644 --- a/src/cloud/components/Views/Kanban/index.tsx +++ b/src/cloud/components/Views/Kanban/index.tsx @@ -185,7 +185,9 @@ const KanbanView = ({ } : undefined, }, - { skipRedirect: true } + { + skipRedirect: true, + } ) } /> diff --git a/src/cloud/lib/hooks/useCloudApi.ts b/src/cloud/lib/hooks/useCloudApi.ts index 2be65e0769..b0c5084792 100644 --- a/src/cloud/lib/hooks/useCloudApi.ts +++ b/src/cloud/lib/hooks/useCloudApi.ts @@ -173,7 +173,7 @@ export function useCloudApi() { body: CreateDocRequestBody, options?: { skipRedirect?: boolean - afterSuccess?: (doc: SerializedDoc) => void + afterSuccess?: (doc: SerializedDocWithSupplemental) => void } ) => { await send(shortid.generate(), 'create', { diff --git a/src/cloud/lib/hooks/views/kanbanView.ts b/src/cloud/lib/hooks/views/kanbanView.ts index da0ebd387a..6ea44cffab 100644 --- a/src/cloud/lib/hooks/views/kanbanView.ts +++ b/src/cloud/lib/hooks/views/kanbanView.ts @@ -203,15 +203,15 @@ function sortWithOrdering(ordering: KanbanList['ordering']) { const aRank = ordering[a.id] const bRank = ordering[b.id] if (aRank == null && bRank == null) { - return a.title.localeCompare(b.title) + return a.createdAt.localeCompare(b.createdAt) } if (aRank == null) { - return -1 + return 1 } if (bRank == null) { - return 1 + return -1 } if (aRank === bRank) {