diff --git a/src/cloud/components/Views/EditableDocItemContainer.tsx b/src/cloud/components/Views/EditableDocItemContainer.tsx new file mode 100644 index 0000000000..48763a6de5 --- /dev/null +++ b/src/cloud/components/Views/EditableDocItemContainer.tsx @@ -0,0 +1,122 @@ +import { lngKeys } from '../../lib/i18n/types' +import { mdiDotsVertical, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js' +import React, { useCallback, useState } from 'react' +import { SerializedDocWithSupplemental } from '../../interfaces/db/doc' +import { useCloudApi } from '../../lib/hooks/useCloudApi' +import { useI18n } from '../../lib/hooks/useI18n' +import { + MenuItem, + MenuTypes, + useContextMenu, +} from '../../../design/lib/stores/contextMenu' +import Icon from '../../../design/components/atoms/Icon' +import styled from '../../../design/lib/styled' +import EditableInput from '../../../design/components/atoms/EditableInput' + +interface ItemProps { + doc: SerializedDocWithSupplemental + children?: React.ReactNode +} + +const EditableDocItemContainer = ({ doc, children }: ItemProps) => { + const [editingItemTitle, setEditingItemTitle] = useState(false) + const [showingContextMenuActions, setShowingContextMenuActions] = useState< + boolean + >(false) + + const { updateDoc, deleteDocApi, sendingMap } = useCloudApi() + const { translate } = useI18n() + const { popup } = useContextMenu() + + const updateDocTitle = useCallback( + async (doc, newTitle) => { + await updateDoc(doc, { + workspaceId: doc.workspaceId, + parentFolderId: doc.parentFolderId, + title: newTitle, + }) + setEditingItemTitle(false) + }, + [updateDoc] + ) + + const openActionMenu: ( + event: React.MouseEvent, + doc: SerializedDocWithSupplemental + ) => void = useCallback( + ( + event: React.MouseEvent, + doc: SerializedDocWithSupplemental + ) => { + const editTitleAction: MenuItem = { + icon: , + type: MenuTypes.Normal, + label: translate(lngKeys.GeneralEditTitle), + onClick: () => setEditingItemTitle(true), + } + const deleteDocAction: MenuItem = { + icon: , + type: MenuTypes.Normal, + label: translate(lngKeys.GeneralDelete), + onClick: () => deleteDocApi({ id: doc.id, teamId: doc.teamId }), + } + const actions: MenuItem[] = [editTitleAction, deleteDocAction] + + event.preventDefault() + event.stopPropagation() + popup(event, actions) + }, + [deleteDocApi, popup, translate] + ) + + const showInput = !sendingMap.has(doc.id) && editingItemTitle + return ( + setShowingContextMenuActions(true)} + onMouseLeave={() => setShowingContextMenuActions(false)} + > + {showInput && ( + updateDocTitle(doc, newText)} + onBlur={() => setEditingItemTitle(false)} + /> + )} + + {!showInput && <>{children}} + + {showingContextMenuActions && ( +
+
openActionMenu(event, doc)} + className='doc__action' + > + +
+
+ )} +
+ ) +} + +const ItemContainer = styled.div` + position: relative; + + .item__container__item__actions { + position: absolute; + right: 5px; + z-index: 1; + margin: 0; + top: 50%; + transform: translate(-50%, -50%); + + .doc__action { + width: 20px; + height: 20px; + } + } +` + +export default EditableDocItemContainer diff --git a/src/cloud/components/Views/Kanban/Item.tsx b/src/cloud/components/Views/Kanban/Item.tsx index d5b91d689e..2b6da72952 100644 --- a/src/cloud/components/Views/Kanban/Item.tsx +++ b/src/cloud/components/Views/Kanban/Item.tsx @@ -10,6 +10,7 @@ import { SerializedDocWithSupplemental } from '../../../interfaces/db/doc' import { getDocTitle } from '../../../lib/utils/patterns' import { isKanbanStaticProp, KanbanViewProp } from '../../../lib/views/kanban' import PropPicker from '../../Props/PropPicker' +import EditableDocItemContainer from '../EditableDocItemContainer' interface ItemProps { doc: SerializedDocWithSupplemental @@ -19,48 +20,53 @@ interface ItemProps { const Item = ({ doc, displayedProps, onClick }: ItemProps) => { return ( - onClick && onClick(doc)} - label={ - - -
- {doc.emoji != null ? ( - - ) : ( - - )} -
- - {getDocTitle(doc, 'Untitled')} - -
- {Object.values(displayedProps).map((prop, i) => { - const docProp = doc.props[prop.name] + + onClick && onClick(doc)} + label={ + + +
+ {doc.emoji != null ? ( + + ) : ( + + )} +
+ + {getDocTitle(doc, 'Untitled')} + +
+ {Object.values(displayedProps).map((prop, i) => { + const docProp = doc.props[prop.name] - if (isKanbanStaticProp(prop)) { - return - } + if (isKanbanStaticProp(prop)) { + return + } - if (docProp == null || docProp.data == null) { - return null - } + if (docProp == null || docProp.data == null) { + return null + } - return ( -
- -
- ) - })} -
- } - /> + return ( +
+ +
+ ) + })} +
+ } + /> + ) } diff --git a/src/cloud/components/Views/List/index.tsx b/src/cloud/components/Views/List/index.tsx index bed0b3c789..c21777c395 100644 --- a/src/cloud/components/Views/List/index.tsx +++ b/src/cloud/components/Views/List/index.tsx @@ -38,6 +38,7 @@ import { getDocLinkHref } from '../../Link/DocLink' import ListViewPropertiesContext from './ListViewPropertiesContext' import { useListView } from '../../../lib/hooks/views/listView' import ListDocProperties from './ListDocProperties' +import EditableDocItemContainer from '../EditableDocItemContainer' type ListViewProps = { view: SerializedView @@ -229,29 +230,30 @@ const ListView = ({ const { id } = doc const href = getDocLinkHref(doc, team, 'index') return ( - toggleDocInSelection(doc.id)} - showCheckbox={currentUserIsCoreMember} - label={doc.title} - defaultIcon={mdiFileDocumentOutline} - emoji={doc.emoji} - labelHref={href} - labelOnclick={() => push(href)} - onDragStart={(event: any) => saveDocTransferData(event, doc)} - onDragEnd={(event: any) => clearDragTransferData(event)} - onDrop={(event: any) => onDropDoc(event, doc)} - hideOrderingHandle={true} - > - - + + toggleDocInSelection(doc.id)} + showCheckbox={currentUserIsCoreMember} + label={doc.title} + defaultIcon={mdiFileDocumentOutline} + emoji={doc.emoji} + labelHref={href} + labelOnclick={() => push(href)} + onDragStart={(event: any) => saveDocTransferData(event, doc)} + onDragEnd={(event: any) => clearDragTransferData(event)} + onDrop={(event: any) => onDropDoc(event, doc)} + hideOrderingHandle={true} + > + + + ) })} {currentWorkspaceId != null && ( diff --git a/src/cloud/components/Views/Table/TableView.tsx b/src/cloud/components/Views/Table/TableView.tsx index c77d81f217..49048b2a1a 100644 --- a/src/cloud/components/Views/Table/TableView.tsx +++ b/src/cloud/components/Views/Table/TableView.tsx @@ -18,8 +18,6 @@ import { DraggedTo } from '../../../../design/lib/dnd' import { StyledContentManagerList } from '../../ContentManager/styled' import Flexbox from '../../../../design/components/atoms/Flexbox' import ColumnSettingsContext from './ColSettingsContext' -import { getDocLinkHref } from '../../Link/DocLink' -import NavigationItem from '../../../../design/components/molecules/Navigation/NavigationItem' import { mdiFileDocumentOutline, mdiPlus } from '@mdi/js' import DocTagsList from '../../DocPage/DocTagsList' import { getFormattedBoosthubDateTime } from '../../../lib/date' @@ -41,6 +39,9 @@ import Button from '../../../../design/components/atoms/Button' import TableViewPropertiesContext from './TableViewPropertiesContext' import TitleColumnSettingsContext from './TitleColumnSettingsContext' import { usePage } from '../../../lib/stores/pageStore' +import EditableDocItemContainer from '../EditableDocItemContainer' +import NavigationItem from '../../../../design/components/molecules/Navigation/NavigationItem' +import { getDocLinkHref } from '../../Link/DocLink' import { useCloudResourceModals } from '../../../lib/hooks/useCloudResourceModals' type TableViewProps = { @@ -72,6 +73,8 @@ const TableView = ({ toggleDocInSelection, resetDocsInSelection, }: TableViewProps) => { + const { goToDocPreview } = useCloudResourceModals() + const currentStateRef = useRef(view.data) const [state, setState] = useState( Object.assign({}, view.data as ViewTableData) @@ -79,7 +82,6 @@ const TableView = ({ const { translate } = useI18n() const { createDoc } = useCloudApi() const { openContextModal, closeLastModal } = useModal() - const { goToDocPreview } = useCloudResourceModals() const { permissions = [] } = usePage() const { @@ -268,16 +270,18 @@ const TableView = ({ cells: [ { children: ( - goToDocPreview(doc)} - label={getDocTitle(doc, 'Untitled')} - icon={ - doc.emoji != null - ? { type: 'emoji', path: doc.emoji } - : { type: 'icon', path: mdiFileDocumentOutline } - } - /> + + goToDocPreview(doc)} + label={getDocTitle(doc, 'Untitled')} + icon={ + doc.emoji != null + ? { type: 'emoji', path: doc.emoji } + : { type: 'icon', path: mdiFileDocumentOutline } + } + /> + ), }, ...orderedColumns.map((col) => { diff --git a/src/cloud/lib/i18n/enUS.ts b/src/cloud/lib/i18n/enUS.ts index 8963dd1492..23a10a96b9 100644 --- a/src/cloud/lib/i18n/enUS.ts +++ b/src/cloud/lib/i18n/enUS.ts @@ -405,6 +405,7 @@ const enTranslation: TranslationSource = { [lngKeys.GeneralContinueVerb]: 'Continue', [lngKeys.GeneralShared]: 'Shared', [lngKeys.GeneralRenameVerb]: 'Rename', + [lngKeys.GeneralEditTitle]: 'Edit title', [lngKeys.GeneralEditVerb]: 'Settings', [lngKeys.GeneralBookmarks]: 'Bookmarks', [lngKeys.GeneralUnbookmarkVerb]: 'Remove from Bookmarks', diff --git a/src/cloud/lib/i18n/fr.ts b/src/cloud/lib/i18n/fr.ts index b9f23554e4..54dbb100a0 100644 --- a/src/cloud/lib/i18n/fr.ts +++ b/src/cloud/lib/i18n/fr.ts @@ -417,6 +417,7 @@ const frTranslation: TranslationSource = { [lngKeys.GeneralMoveVerb]: 'Déplacer', [lngKeys.GeneralShared]: 'Partagé', [lngKeys.GeneralRenameVerb]: 'Renommer', + [lngKeys.GeneralEditTitle]: 'Modifier le titre', [lngKeys.GeneralEditVerb]: 'Editer', [lngKeys.GeneralDashboards]: 'Dashboards', [lngKeys.GeneralBookmarks]: 'Favoris', diff --git a/src/cloud/lib/i18n/ja.ts b/src/cloud/lib/i18n/ja.ts index 67abd72067..d6d891221c 100644 --- a/src/cloud/lib/i18n/ja.ts +++ b/src/cloud/lib/i18n/ja.ts @@ -390,6 +390,7 @@ const jpTranslation: TranslationSource = { [lngKeys.GeneralContinueVerb]: '次に進む', [lngKeys.GeneralShared]: 'シェア済', [lngKeys.GeneralRenameVerb]: '名前変更', + [lngKeys.GeneralEditTitle]: 'タイトルを編集', [lngKeys.GeneralEditVerb]: '編集', [lngKeys.GeneralBookmarks]: 'ブックマーク', [lngKeys.GeneralUnbookmarkVerb]: 'ブックマーク削除', diff --git a/src/cloud/lib/i18n/types.ts b/src/cloud/lib/i18n/types.ts index 2cc91c38eb..45743c024b 100644 --- a/src/cloud/lib/i18n/types.ts +++ b/src/cloud/lib/i18n/types.ts @@ -56,6 +56,7 @@ export enum lngKeys { GeneralLabels = 'general.Labels', GeneralMore = 'general.More', GeneralStatus = 'general.Status', + GeneralEditTitle = 'general.EditTitle', GeneralRenameVerb = 'general.Renameverb', GeneralEditVerb = 'general.Editverb', GeneralSettings = 'general.Settings', diff --git a/src/cloud/lib/i18n/zhCN.ts b/src/cloud/lib/i18n/zhCN.ts index 5b8aab80f8..0b261cb4ce 100644 --- a/src/cloud/lib/i18n/zhCN.ts +++ b/src/cloud/lib/i18n/zhCN.ts @@ -366,6 +366,7 @@ const zhTranslation: TranslationSource = { [lngKeys.GeneralContinueVerb]: '跳过', [lngKeys.GeneralShared]: '分享', [lngKeys.GeneralRenameVerb]: '重命名', + [lngKeys.GeneralEditTitle]: '编辑标题', [lngKeys.GeneralEditVerb]: '编辑', [lngKeys.GeneralBookmarks]: '书签', [lngKeys.GeneralUnbookmarkVerb]: '移除书签', diff --git a/src/design/components/atoms/EditableInput.tsx b/src/design/components/atoms/EditableInput.tsx index 37d47cb719..58c3191da4 100644 --- a/src/design/components/atoms/EditableInput.tsx +++ b/src/design/components/atoms/EditableInput.tsx @@ -22,13 +22,9 @@ interface EditableInputProps { onTextChange: (newText: string) => void disabled?: boolean onKeydownConfirm?: () => void + onBlur?: () => void } -type EditableInput = { - folderLabel: string - folderPathname: string -}[] - const EditableInput = ({ editOnStart = false, placeholder, @@ -36,6 +32,7 @@ const EditableInput = ({ text, onTextChange, onKeydownConfirm, + onBlur, }: EditableInputProps) => { const titleInputRef = useRef(null) const textRef = useRef(text) @@ -108,9 +105,13 @@ const EditableInput = ({ event.preventDefault() cancelEditingText() break + case 'Enter': + event.preventDefault() + onSubmit(event) + break } }, - [cancelEditingText] + [cancelEditingText, onSubmit] ) const maxWidth: string | number = useMemo(() => { @@ -139,6 +140,7 @@ const EditableInput = ({ value={newText} onKeyDown={handleTextInputKeyDown} disabled={disabled} + onBlur={onBlur} />