From 9bf2085b096ddebca1592e56c7e70e573f05295f Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Wed, 9 Nov 2022 17:13:39 +0530 Subject: [PATCH 1/8] Add support for templated notes --- apps/webapp/src/Components/Modals.tsx | 2 + .../src/Components/Sidebar/SidebarList.tsx | 45 +++-- .../Sidebar/TreeWithContextMenu.tsx | 35 +++- .../Template/TemplateModal.styles.tsx | 31 ++++ .../src/Components/Template/TemplateModal.tsx | 173 ++++++++++++++++++ .../src/Editor/EditorPreviewRenderer.tsx | 6 +- apps/webapp/src/Hooks/API/useNodeAPI.ts | 68 +------ apps/webapp/src/Hooks/useCreateNewNote.tsx | 9 +- apps/webapp/src/Stores/useAnalysis.ts | 16 +- apps/webapp/src/Stores/useModalStore.ts | 3 - libs/core/src/Types/Editor.ts | 11 +- libs/core/src/Utils/content.ts | 7 +- libs/shared/src/Style/Form.tsx | 12 +- libs/shared/src/Style/Integrations.tsx | 18 +- libs/shared/src/Style/SidebarList.style.tsx | 15 +- 15 files changed, 338 insertions(+), 113 deletions(-) create mode 100644 apps/webapp/src/Components/Template/TemplateModal.styles.tsx create mode 100644 apps/webapp/src/Components/Template/TemplateModal.tsx diff --git a/apps/webapp/src/Components/Modals.tsx b/apps/webapp/src/Components/Modals.tsx index aa9e227c5..c86e1304e 100644 --- a/apps/webapp/src/Components/Modals.tsx +++ b/apps/webapp/src/Components/Modals.tsx @@ -10,6 +10,7 @@ import PreviewNoteModal from './PreviewNoteModal' import Delete from './Refactor/DeleteModal' import CreateReminderModal from './Reminders/CreateReminderModal' import TaskViewModal from './TaskViewModal' +import TemplateModal from './Template/TemplateModal' const Modals = () => { const isAuthenticated = useAuthStore((store) => store.authenticated) @@ -28,6 +29,7 @@ const Modals = () => { + ) } diff --git a/apps/webapp/src/Components/Sidebar/SidebarList.tsx b/apps/webapp/src/Components/Sidebar/SidebarList.tsx index 8cf1d4e1f..a64376ffb 100644 --- a/apps/webapp/src/Components/Sidebar/SidebarList.tsx +++ b/apps/webapp/src/Components/Sidebar/SidebarList.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import { useEffect, useRef, useState, ChangeEventHandler } from 'react' import searchLine from '@iconify/icons-ri/search-line' import { Icon, IconifyIcon } from '@iconify/react' @@ -6,7 +6,6 @@ import Tippy, { useSingleton } from '@tippyjs/react' import { debounce } from 'lodash' import { useTheme } from 'styled-components' -import { MexIcon } from '@workduck-io/mex-components' import { tinykeys } from '@workduck-io/tinykeys' import { fuzzySearch, mog } from '@mexit/core' @@ -50,7 +49,7 @@ export interface SidebarListProps { selectedItemId?: string // If true, the list will be preceded by the default item - defaultItems?: SidebarListItem[] + defaultItem?: SidebarListItem // To render the context menu if the item is right-clicked ItemContextMenu?: (props: { item: SidebarListItem }) => JSX.Element @@ -59,6 +58,7 @@ export interface SidebarListProps { showSearch?: boolean searchPlaceholder?: string emptyMessage?: string + noMargin?: boolean } const SidebarList = ({ @@ -66,10 +66,11 @@ const SidebarList = ({ selectedItemId, onClick, items, - defaultItems, + defaultItem, showSearch, searchPlaceholder, - emptyMessage + emptyMessage, + noMargin }: SidebarListProps) => { const [contextOpenViewId, setContextOpenViewId] = useState(null) const [search, setSearch] = useState('') @@ -80,10 +81,10 @@ const SidebarList = ({ const [source, target] = useSingleton() const theme = useTheme() - const inputRef = React.useRef(null) + const inputRef = useRef(null) const expandSidebar = useLayoutStore((store) => store.expandSidebar) - const onSearchChange: React.ChangeEventHandler = (e) => { + const onSearchChange: ChangeEventHandler = (e) => { setSearch(e.target.value) } @@ -95,7 +96,6 @@ const SidebarList = ({ if (inpEl) inpEl.value = '' setContextOpenViewId(null) } - const onSelectItem = (id: string) => { setSelected(-1) setContextOpenViewId(null) @@ -114,7 +114,6 @@ const SidebarList = ({ } } }, [search, showSearch, items]) - useEffect(() => { if (inputRef.current) { const unsubscribe = tinykeys(inputRef.current, { @@ -173,23 +172,22 @@ const SidebarList = ({ }, []) return ( - + - {defaultItems && - defaultItems.map((defaultItem) => ( - - onSelectItem(defaultItem.id)}> - - - {defaultItem.label} - - - - ))} + {defaultItem && ( + + onSelectItem(defaultItem.id)}> + + + {defaultItem.label} + + + + )} {showSearch && items.length > 0 && ( - + - )} - + {listItems.map((item, index) => ( { const { createNewNote } = useCreateNewNote() const openShareModal = useShareModalStore((store) => store.openModal) // const { onPinNote, onUnpinNote, isPinned } = usePinnedWindows() - // const toggleModal = useModalStore((store) => store.toggleOpen) + const toggleModal = useModalStore((store) => store.toggleOpen) const { goTo } = useRouting() const namespaces = useDataStore((store) => store.namespaces) const { getNamespaceIcon } = useNamespaces() @@ -44,6 +51,16 @@ export const TreeContextMenu = ({ item }: TreeContextMenuProps) => { // prefillRefactorModal({ path: item?.data?.path, namespaceID: item.data?.namespace }) // // openRefactorModal() // } + const contents = useContentStore((store) => store.contents) + const hasTemplate = useMemo(() => { + const metadata = contents[item.data.nodeid]?.metadata + + const templates = useSnippetStore + .getState() + .snippets.filter((item) => item?.template && item.id === metadata?.templateID) + + return templates.length !== 0 + }, [item.data.nodeid, contents]) const isInSharedNamespace = itemNamespace?.granterID !== undefined const isReadonly = itemNamespace?.access === 'READ' @@ -75,6 +92,14 @@ export const TreeContextMenu = ({ item }: TreeContextMenuProps) => { } } + const handleTemplate = (item: TreeItem) => { + if (item.data.path !== 'Drafts') { + toggleModal(ModalsType.template, item.data) + } else { + toast.error('Template cannot be set for Drafts hierarchy') + } + } + return ( @@ -96,6 +121,14 @@ export const TreeContextMenu = ({ item }: TreeContextMenuProps) => { New Note + { + handleTemplate(item) + }} + > + + {hasTemplate ? 'Change Template' : 'Set Template'} + { diff --git a/apps/webapp/src/Components/Template/TemplateModal.styles.tsx b/apps/webapp/src/Components/Template/TemplateModal.styles.tsx new file mode 100644 index 000000000..65b146fad --- /dev/null +++ b/apps/webapp/src/Components/Template/TemplateModal.styles.tsx @@ -0,0 +1,31 @@ +import { transparentize } from 'polished' +import styled from 'styled-components' + +import { LoadingButton } from '@workduck-io/mex-components' + +export const TemplateContainer = styled.div` + display: flex; + max-height: 350px; + margin: 1rem -0.5rem; + + & > section { + height: 30vh !important; + width: 300px; + overflow-y: auto; + overflow-x: hidden; + margin: 0 1rem; + + background-color: ${({ theme }) => transparentize(0.5, theme.colors.gray[8])}; + border-radius: ${({ theme }) => theme.borderRadius.large}; + } +` + +export const RemovalButton = styled(LoadingButton)` + background-color: ${({ theme }) => theme.colors.palette.red}; + + :hover { + color: ${({ theme }) => theme.colors.text.subheading}; + background-color: ${({ theme }) => transparentize(0.3, theme.colors.palette.red)}; + box-shadow: 0px 6px 12px ${({ theme }) => transparentize(0.75, theme.colors.palette.red)}; + } +` diff --git a/apps/webapp/src/Components/Template/TemplateModal.tsx b/apps/webapp/src/Components/Template/TemplateModal.tsx new file mode 100644 index 000000000..05d7e7472 --- /dev/null +++ b/apps/webapp/src/Components/Template/TemplateModal.tsx @@ -0,0 +1,173 @@ +import React, { useEffect, useState } from 'react' + +import { useForm } from 'react-hook-form' +import toast from 'react-hot-toast' +import Modal from 'react-modal' + +import { Title, LoadingButton } from '@workduck-io/mex-components' + +import type { Snippet } from '@mexit/core' +import { TemplateContainer, ButtonFields } from '@mexit/shared' + +import { defaultContent } from '../../Data/baseData' +import EditorPreviewRenderer from '../../Editor/EditorPreviewRenderer' +import { useApi } from '../../Hooks/API/useNodeAPI' +import { useLinks, getTitleFromPath } from '../../Hooks/useLinks' +import { useContentStore } from '../../Stores/useContentStore' +import useModalStore, { ModalsType } from '../../Stores/useModalStore' +import { useSnippetStore } from '../../Stores/useSnippetStore' +import { PrimaryText } from '../EditorInfobar/BlockInfobar' +import { InviteWrapper, InviteFormWrapper } from '../Mentions/styles' +import SidebarList from '../Sidebar/SidebarList' +import { RemovalButton } from './TemplateModal.styles' + +const TemplateModal = () => { + const { getILinkFromNodeid } = useLinks() + const { toggleOpen, open, data } = useModalStore() + + const { nodeid, namespace } = data ?? {} + + const node = getILinkFromNodeid(nodeid) + const templates = useSnippetStore((state) => state.snippets).filter((item) => item?.template) + + const [currentTemplate, setCurrentTemplate] = useState() + const [selectedTemplate, setSelectedTemplate] = useState() + + const getMetadata = useContentStore((store) => store.getMetadata) + const getContent = useContentStore((store) => store.getContent) + const { saveDataAPI } = useApi() + + useEffect(() => { + const contents = useContentStore.getState().contents + const metadata = contents[nodeid]?.metadata + if (metadata?.templateID) { + const template = templates.find((item) => item.id === metadata.templateID) + setCurrentTemplate(template) + setSelectedTemplate(template) + } else { + setSelectedTemplate(templates[0]) + } + + return () => { + setCurrentTemplate(undefined) + } + }, [nodeid, open]) + + const { + handleSubmit, + control, + formState: { errors, isSubmitting } + } = useForm() + + const onSelectItem = (id: string) => { + setSelectedTemplate(templates.find((item) => item.id === id)) + } + + const onSubmit = async () => { + const content = getContent(nodeid) + + if (nodeid) { + saveDataAPI(nodeid, namespace, content.content, undefined, undefined, selectedTemplate?.id) + toast('Template Set!') + } + + toggleOpen(ModalsType.template) + } + + const onRemove = async () => { + const content = getContent(nodeid) + + if (nodeid) { + // For why '__null__' see useSaveApi.tsx line 151 + saveDataAPI(nodeid, namespace, content.content, undefined, undefined, '__null__') + toast('Template Removed!') + } + + toggleOpen(ModalsType.template) + } + + return ( + toggleOpen(ModalsType.template)} + isOpen={open === ModalsType.template} + > + + {templates.length !== 0 ? ( + !currentTemplate ? ( + <> + Set Template for {getTitleFromPath(node?.path)} +

Auto fill new notes using template

+ + ) : ( + <> + Update Template for {getTitleFromPath(node?.path)} +

+ Currently using {currentTemplate.title} +

+ + ) + ) : ( + No templates found + )} + + {templates.length !== 0 && ( + + ({ ...t, label: title, data: t }))} + onClick={onSelectItem} + selectedItemId={selectedTemplate?.id} + noMargin + showSearch + searchPlaceholder="Filter Templates..." + emptyMessage="No Templates Found" + /> +
+ +
+
+ )} + + {currentTemplate && ( + + Remove Template + + )} + + Set Template + + +
+
+
+ ) +} + +export default TemplateModal diff --git a/apps/webapp/src/Editor/EditorPreviewRenderer.tsx b/apps/webapp/src/Editor/EditorPreviewRenderer.tsx index e7259e264..c5f3b6bb3 100644 --- a/apps/webapp/src/Editor/EditorPreviewRenderer.tsx +++ b/apps/webapp/src/Editor/EditorPreviewRenderer.tsx @@ -33,6 +33,7 @@ interface EditorPreviewRendererProps { plugins?: PlatePlugin[] readOnly?: boolean draftView?: boolean + placeholder?: string } const PreviewStyles = styled(EditorStyles)<{ draftView?: boolean; readOnly?: boolean }>` @@ -69,11 +70,12 @@ const EditorPreviewRenderer = ({ onChange, onDoubleClick, readOnly = true, - draftView = true + draftView = true, + placeholder }: EditorPreviewRendererProps) => { const editableProps = useMemo( () => ({ - placeholder: 'Murmuring the mex hype... ', + placeholder: placeholder ?? 'Murmuring the mex hype... ', style: noStyle ? {} : { diff --git a/apps/webapp/src/Hooks/API/useNodeAPI.ts b/apps/webapp/src/Hooks/API/useNodeAPI.ts index 9a80bd670..c156089fd 100644 --- a/apps/webapp/src/Hooks/API/useNodeAPI.ts +++ b/apps/webapp/src/Hooks/API/useNodeAPI.ts @@ -32,6 +32,7 @@ import { useAPIHeaders } from './useAPIHeaders' export const useApi = () => { const getWorkspaceId = useAuthStore((store) => store.getWorkspaceId) + const getMetadata = useContentStore((store) => store.getMetadata) const setMetadata = useContentStore((store) => store.setMetadata) const updateMetadata = useContentStore((store) => store.updateMetadata) const setContent = useContentStore((store) => store.setContent) @@ -162,14 +163,19 @@ export const useApi = () => { namespaceID: string, content: NodeEditorContent, isShared = false, - title?: string + title?: string, + templateID?: string ) => { const reqData = { id: noteID, title: title || getTitleFromNoteId(noteID), namespaceID: namespaceID, tags: getTagsFromContent(content), - data: serializeContent(content ?? defaultContent.content, noteID) + data: serializeContent(content ?? defaultContent.content, noteID), + // Because we have to send templateID with every node save call so that it doesn't get unset + // We are checking if the id is __null__ for the case when the user wants to remove the template + // If not, we send what was passed as prop, if nothing then from metadata + metadata: { templateID: templateID === '__null__' ? null : templateID ?? getMetadata(noteID)?.templateID } } if (isShared) { @@ -429,64 +435,6 @@ export const useApi = () => { return data } - // const getNodesByWorkspace = async () => { - // const updatedILinks: any[] = await client - // .get(apiURLs.namespaces.getHierarchy, { - // headers: { - // 'mex-workspace-id': getWorkspaceId() - // } - // }) - // .then((res: any) => { - // return res.data - // }) - // .catch(console.error) - - // mog(`UpdatedILinks`, { updatedILinks }) - - // const { nodes, namespaces } = Object.entries(updatedILinks).reduce( - // (p, [namespaceid, namespaceData]) => { - // return { - // namespaces: [ - // ...p.namespaces, - // { - // id: namespaceid, - // name: namespaceData.name, - // ...namespaceData?.namespaceMetadata - // } - // ], - // nodes: [ - // ...p.nodes, - // ...namespaceData.nodeHierarchy.map((ilink) => ({ - // ...ilink, - // namespace: namespaceid - // })) - // ] - // } - // }, - // { nodes: [], namespaces: [] } - // ) - // mog('UpdatingILinks', { nodes, namespaces }) - // if (nodes && nodes.length > 0) { - // const localILinks = useDataStore.getState().ilinks - // const { toUpdateLocal } = iLinksToUpdate(localILinks, nodes) - // const ids = toUpdateLocal.map((i) => i.nodeid) - - // const { fulfilled } = await runBatchWorker(WorkerRequestType.GET_NODES, 6, ids) - // const requestData = { time: Date.now(), method: 'GET' } - - // fulfilled.forEach((node) => { - // const { rawResponse, nodeid } = node - // setRequest(apiURLs.getNode(nodeid), { ...requestData, url: apiURLs.getNode(nodeid) }) - // const content = deserializeContent(rawResponse.data) - // const metadata = extractMetadata(rawResponse) // added by Varshitha - // updateFromContent(nodeid, content, metadata) - // }) - // } - - // // setNamespaces(namespaces) - // setILinks(nodes) - // } - return { saveDataAPI, getDataAPI, diff --git a/apps/webapp/src/Hooks/useCreateNewNote.tsx b/apps/webapp/src/Hooks/useCreateNewNote.tsx index 595234a8d..2f14b3ab0 100644 --- a/apps/webapp/src/Hooks/useCreateNewNote.tsx +++ b/apps/webapp/src/Hooks/useCreateNewNote.tsx @@ -32,13 +32,15 @@ export const useCreateNewNote = () => { const { goTo } = useRouting() const addILink = useDataStore((s) => s.addILink) const checkValidILink = useDataStore((s) => s.checkValidILink) - const getMetadata = useContentStore((s) => s.getMetadata) const { saveNodeName } = useLoad() const { getParentILink } = useLinks() const { addInHierarchy } = useHierarchy() // const { addLastOpened } = useLastOpened() const { getDefaultNamespace } = useNamespaces() + const getMetadata = useContentStore((s) => s.getMetadata) + const { getSnippet } = useSnippets() + const createNewNote = (options?: NewNoteOptions) => { const childNodepath = options?.parent !== undefined ? getUntitledKey(options?.parent.path) : getUntitledDraftKey() const defaultNamespace = getDefaultNamespace() @@ -57,9 +59,10 @@ export const useCreateNewNote = () => { const parentNoteId = parentNote?.nodeid const nodeMetadata = getMetadata(parentNoteId) - // Filling note content by template if nothing in options and notepath is not Drafts (it may cause problems with capture otherwise) - const noteContent = options?.noteContent + const noteContent = + options?.noteContent || + (nodeMetadata?.templateID && parentNote?.path !== 'Drafts' && getSnippet(nodeMetadata.templateID)?.content) const namespace = options?.namespace ?? parentNote?.namespace ?? defaultNamespace?.id diff --git a/apps/webapp/src/Stores/useAnalysis.ts b/apps/webapp/src/Stores/useAnalysis.ts index 46273e398..28d354a33 100644 --- a/apps/webapp/src/Stores/useAnalysis.ts +++ b/apps/webapp/src/Stores/useAnalysis.ts @@ -1,14 +1,16 @@ import { useEffect } from 'react' + import create from 'zustand' +import { TodoType, checkIfUntitledDraftNode, getParentNodePath } from '@mexit/core' + import { useBufferStore, useEditorBuffer } from '../Hooks/useEditorBuffer' +import { useLinks } from '../Hooks/useLinks' +import { useSearchExtra } from '../Hooks/useSearch' import { areEqual } from '../Utils/hash' -import { TodoType, checkIfUntitledDraftNode } from '@mexit/core' - import { analyseContent, AnalysisOptions } from '../Workers/controller' import { useEditorStore, getContent } from './useEditorStore' import { useTodoStore } from './useTodoStore' -import { useSearchExtra } from '../Hooks/useSearch' export interface OutlineItem { id: string @@ -61,7 +63,13 @@ export const useAnalysis = () => { const setAnalysis = useAnalysisStore((s) => s.setAnalysis) const { getSearchExtra } = useSearchExtra() + const { getNodeidFromPath } = useLinks() + useEffect(() => { + const parentNodePath = getParentNodePath(node.path) + const parentNodeId = getNodeidFromPath(parentNodePath, node.namespace) + const parentMetadata = getContent(parentNodeId)?.metadata + const bufferContent = getBufferVal(node.nodeid) const content = getContent(node.nodeid) const metadata = content.metadata @@ -72,7 +80,7 @@ export const useAnalysis = () => { const isNewDraftNode = metadata?.createdAt === metadata?.updatedAt // * New Draft node, get Title from its content - if (isUntitledDraftNode && isNewDraftNode) { + if (isUntitledDraftNode && isNewDraftNode && !parentMetadata?.templateID) { options['title'] = true } diff --git a/apps/webapp/src/Stores/useModalStore.ts b/apps/webapp/src/Stores/useModalStore.ts index 8a61ac35d..cb9be841b 100644 --- a/apps/webapp/src/Stores/useModalStore.ts +++ b/apps/webapp/src/Stores/useModalStore.ts @@ -35,9 +35,6 @@ const useModalStore = create((set, get) => ({ setData: (modalData) => set({ data: modalData }), toggleOpen: (modalType, modalData, initialize) => { const open = get().open - const init = get().init - - // if (init) ipcRenderer.send(IpcAction.SHOW_RELEASE_NOTES) const changeModalState = open === modalType ? undefined : modalType // As only one modal is going to be open at any time, better to reset data on Modal close diff --git a/libs/core/src/Types/Editor.ts b/libs/core/src/Types/Editor.ts index d8e6298e4..f18eb47ad 100644 --- a/libs/core/src/Types/Editor.ts +++ b/libs/core/src/Types/Editor.ts @@ -33,6 +33,10 @@ export interface NodeMetadata { updatedAt: number elementMetadata: ElementHighlightMetadata + publicAccess?: boolean + iconUrl?: string + // The snippet ID with which all the children nodes should be populated + templateID?: string } export interface Block { @@ -140,13 +144,6 @@ export interface BlockIndexData { text: string } -export interface NodeMetadata { - createdBy: string - createdAt: number - lastEditedBy: string - updatedAt: number -} - export type NodeEditorContent = any[] export interface NodeContent { diff --git a/libs/core/src/Utils/content.ts b/libs/core/src/Utils/content.ts index cf5c6eaa8..b43ad5c3b 100644 --- a/libs/core/src/Utils/content.ts +++ b/libs/core/src/Utils/content.ts @@ -91,12 +91,15 @@ export const removeNulls = (obj: any): any => { export const extractMetadata = (data: any): NodeMetadata => { if (data) { - const metadata: any = { + const metadata: NodeMetadata = { lastEditedBy: data.lastEditedBy, updatedAt: data.updatedAt, createdBy: data.createdBy, createdAt: data.createdAt, - elementMetadata: data?.elementMetadata + elementMetadata: data?.elementMetadata, + publicAccess: data?.publicAccess, + iconUrl: data?.metadata?.iconUrl, + templateID: data?.metadata?.templateID } return removeNulls(metadata) diff --git a/libs/shared/src/Style/Form.tsx b/libs/shared/src/Style/Form.tsx index ae45c3c7e..69bc5f8a6 100644 --- a/libs/shared/src/Style/Form.tsx +++ b/libs/shared/src/Style/Form.tsx @@ -184,10 +184,20 @@ export const Label = styled.label` max-width: max-content; ` -export const ButtonFields = styled.div` +export const ButtonFields = styled.div<{ position?: string }>` display: flex; align-items: center; margin: ${({ theme }) => theme.spacing.large} 0 ${({ theme }) => theme.spacing.medium}; + gap: ${({ theme }) => theme.spacing.medium}; + + ${({ position }) => { + switch (position) { + case 'end': + return css` + justify-content: end; + ` + } + }} ` export const ReactSelectStyles = (theme: DefaultTheme) => ({ diff --git a/libs/shared/src/Style/Integrations.tsx b/libs/shared/src/Style/Integrations.tsx index b92d3f214..e854ed380 100644 --- a/libs/shared/src/Style/Integrations.tsx +++ b/libs/shared/src/Style/Integrations.tsx @@ -1,4 +1,5 @@ import { Icon } from '@iconify/react' +import { transparentize } from 'polished' import styled, { css } from 'styled-components' export const IntegrationContainer = styled.section` @@ -6,8 +7,21 @@ export const IntegrationContainer = styled.section` height: calc(100vh - ${({ theme }) => (theme.additional.hasBlocks ? '6rem' : '2rem')}); ` -export const TemplateContainer = styled.section` - margin: 0 4rem; +export const TemplateContainer = styled.div` + display: flex; + max-height: 350px; + margin: 1rem -0.5rem; + + & > section { + height: 30vh !important; + width: 300px; + overflow-y: auto; + overflow-x: hidden; + margin: 0 1rem; + + background-color: ${({ theme }) => transparentize(0.5, theme.colors.gray[8])}; + border-radius: ${({ theme }) => theme.borderRadius.large}; + } ` export const TemplateList = styled.div` diff --git a/libs/shared/src/Style/SidebarList.style.tsx b/libs/shared/src/Style/SidebarList.style.tsx index 9fb13cd9c..1fa45fef6 100644 --- a/libs/shared/src/Style/SidebarList.style.tsx +++ b/libs/shared/src/Style/SidebarList.style.tsx @@ -2,12 +2,17 @@ import styled from 'styled-components' import { Input } from './Form' -export const SidebarListWrapper = styled.div` +interface SidebarListWrapperProps { + noMargin?: boolean +} + +export const SidebarListWrapper = styled.div` + margin-top: ${({ noMargin }) => (noMargin ? '0' : '1rem')}; + height: inherit; + flex-grow: 1; display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing.small}; - padding: ${({ theme }) => theme.spacing.small}; - flex-grow: 1; ` export const EmptyMessage = styled.div` @@ -32,10 +37,12 @@ export const FilteredItemsWrapper = styled.div<{ hasDefault?: boolean }>` overflow-x: hidden; ` -export const SidebarListFilter = styled.div` +export const SidebarListFilter = styled.div` display: flex; align-items: center; padding: 0 ${({ theme }) => theme.spacing.small}; + margin: ${({ theme }) => `0 0`}; + margin-top: ${({ noMargin, theme }) => (noMargin ? '0' : theme.spacing.medium)}; background: ${({ theme }) => theme.colors.form.input.bg}; border-radius: ${({ theme }) => theme.borderRadius.small}; From 111d94fcd24f09cfa549d7ab516b752d7edc80dc Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Wed, 9 Nov 2022 20:52:18 +0530 Subject: [PATCH 2/8] User preference store; Add last opened store --- apps/extension/src/Hooks/useRaju.ts | 13 +- apps/extension/src/Hooks/useThemeStore.ts | 10 -- apps/extension/src/index.tsx | 21 +++- apps/webapp/src/App.tsx | 21 +++- apps/webapp/src/Components/Chotu.tsx | 4 +- .../src/Components/Editor/ContentEditor.tsx | 5 +- apps/webapp/src/Components/Init.tsx | 2 + .../Components/Sidebar/SharedNotes/index.tsx | 12 +- .../src/Components/Sidebar/SidebarList.tsx | 1 - .../Components/Sidebar/SidebarListItem.tsx | 28 +++-- .../Sidebar/TreeWithContextMenu.tsx | 42 ++++++- apps/webapp/src/Components/Themes.tsx | 21 ++-- apps/webapp/src/Hooks/API/useNodeAPI.ts | 10 +- apps/webapp/src/Hooks/API/useUserAPI.ts | 75 +++++++++++- apps/webapp/src/Hooks/useCreateNewNote.tsx | 5 +- apps/webapp/src/Hooks/useLastOpened.tsx | 112 ++++++++++++++++++ apps/webapp/src/Hooks/useLoad.ts | 5 +- .../src/Hooks/useSyncUserPreferences.ts | 50 ++++++++ apps/webapp/src/Stores/index.ts | 2 - apps/webapp/src/Stores/useThemeStore.ts | 17 --- apps/webapp/src/Utils/tree.ts | 43 ++----- libs/core/src/Stores/blockStoreConstructor.ts | 2 +- .../src/Stores/contentStoreConstructor.ts | 1 - .../src/Stores/preferenceStoreConstructor.ts | 65 ++++++---- libs/shared/src/Types/userPreference.ts | 11 +- 25 files changed, 419 insertions(+), 159 deletions(-) delete mode 100644 apps/extension/src/Hooks/useThemeStore.ts create mode 100644 apps/webapp/src/Hooks/useLastOpened.tsx create mode 100644 apps/webapp/src/Hooks/useSyncUserPreferences.ts delete mode 100644 apps/webapp/src/Stores/useThemeStore.ts diff --git a/apps/extension/src/Hooks/useRaju.ts b/apps/extension/src/Hooks/useRaju.ts index f25149997..d2284dab2 100644 --- a/apps/extension/src/Hooks/useRaju.ts +++ b/apps/extension/src/Hooks/useRaju.ts @@ -3,8 +3,6 @@ import { useEffect } from 'react' import { addMinutes } from 'date-fns' import { connectToChild, Methods } from 'penpal' -import { useAuthStore as useDwindleAuthStore } from '@workduck-io/dwindle' - import { Contents, idxKey, @@ -21,8 +19,7 @@ import { UserDetails, WorkspaceDetails, Link, - Description, - Highlighted + Description } from '@mexit/core' import { Theme } from '@mexit/shared' @@ -34,12 +31,12 @@ import { useLinkStore } from '../Stores/useLinkStore' import { useRecentsStore } from '../Stores/useRecentsStore' import { useReminderStore } from '../Stores/useReminderStore' import { useSputlitStore } from '../Stores/useSputlitStore' +import { useUserPreferenceStore } from '../Stores/userPreferenceStore' import { getElementById, styleSlot } from '../contentScript' import { useAuthStore } from './useAuth' import useInternalAuthStore from './useAuthStore' import { useReminders } from './useReminders' import { useSnippets } from './useSnippets' -import useThemeStore from './useThemeStore' export interface ParentMethods { SEARCH: (key: idxKey | idxKey[], query: string) => Promise @@ -64,11 +61,9 @@ export default function useRaju() { // For some reason, using useState wasn't making dispatch() make use of the new variable // So added in the context for now const setChild = useSputlitStore((s) => s.setChild) - const setTheme = useThemeStore((store) => store.setTheme) + const setTheme = useUserPreferenceStore((store) => store.setTheme) const setAuthenticated = useAuthStore((store) => store.setAuthenticated) const setInternalAuthStore = useInternalAuthStore((store) => store.setAllStore) - const setUserPool = useDwindleAuthStore((store) => store.setUserPool) - const setUserCred = useDwindleAuthStore((store) => store.setUserCred) const initContents = useContentStore((store) => store.initContents) const setIlinks = useDataStore((store) => store.setIlinks) const setNamespaces = useDataStore((store) => store.setNamespaces) @@ -121,7 +116,7 @@ export default function useRaju() { bootAuth(userDetails: UserDetails, workspaceDetails: WorkspaceDetails) { setAuthenticated(userDetails, workspaceDetails) }, - bootTheme(theme: Theme) { + bootTheme(theme: string) { setTheme(theme) }, bootDwindle(authAWS: { userPool; userCred }) { diff --git a/apps/extension/src/Hooks/useThemeStore.ts b/apps/extension/src/Hooks/useThemeStore.ts deleted file mode 100644 index cb6046c16..000000000 --- a/apps/extension/src/Hooks/useThemeStore.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { themeStoreConstructor, ThemeStoreState } from '@mexit/shared' -import create from 'zustand' -import { persist } from 'zustand/middleware' -import { asyncLocalStorage } from '../Utils/chromeStorageAdapter' - -const useThemeStore = create( - persist(themeStoreConstructor, { name: 'mexit-theme-store', getStorage: () => asyncLocalStorage }) -) - -export default useThemeStore diff --git a/apps/extension/src/index.tsx b/apps/extension/src/index.tsx index 28e2cdd4e..d6744c488 100644 --- a/apps/extension/src/index.tsx +++ b/apps/extension/src/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import { ThemeProvider } from 'styled-components' @@ -16,14 +16,23 @@ import { TooltipPortal } from './Components/Tooltip/TooltipPortal' import { EditorProvider } from './Hooks/useEditorContext' import { HighlighterProvider } from './Hooks/useHighlighterContext' import { SputlitProvider } from './Hooks/useSputlitContext' -import useThemeStore from './Hooks/useThemeStore' +import { useUserPreferenceStore } from './Stores/userPreferenceStore' import { GlobalStyle } from './Styles/GlobalStyle' -export default function Index() { - const theme = useThemeStore((state) => state.theme) +const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const theme = useUserPreferenceStore((state) => state.theme) + + const themeData = useMemo(() => { + const ctheme = defaultThemes.find((t) => t.id === theme) + return ctheme ? ctheme.themeData : defaultThemes[0].themeData + }, [theme]) + return {children} +} + +export default function Index() { return ( - + @@ -49,6 +58,6 @@ export default function Index() { - + ) } diff --git a/apps/webapp/src/App.tsx b/apps/webapp/src/App.tsx index 7e1935879..2b152a595 100644 --- a/apps/webapp/src/App.tsx +++ b/apps/webapp/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React, { useMemo } from 'react' import { BrowserRouter as Router } from 'react-router-dom' import { ThemeProvider } from 'styled-components' @@ -11,16 +11,25 @@ import Init from './Components/Init' import Main from './Components/Main' import Modals from './Components/Modals' import './Stores' -import useThemeStore from './Stores/useThemeStore' +import { useUserPreferenceStore } from './Stores/userPreferenceStore' import GlobalStyle from './Style/GlobalStyle' import Switch from './Switch' -function App() { - const theme = useThemeStore((state) => state.theme) +const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const theme = useUserPreferenceStore((state) => state.theme) + const themeData = useMemo(() => { + const ctheme = defaultThemes.find((t) => t.id === theme) + return ctheme ? ctheme.themeData : defaultThemes[0].themeData + }, [theme]) + + return {children} +} + +const App = () => { return ( - +
@@ -29,7 +38,7 @@ function App() {
-
+
) } diff --git a/apps/webapp/src/Components/Chotu.tsx b/apps/webapp/src/Components/Chotu.tsx index 2a4d404c7..690a3fd65 100644 --- a/apps/webapp/src/Components/Chotu.tsx +++ b/apps/webapp/src/Components/Chotu.tsx @@ -18,13 +18,13 @@ import { useLinkStore } from '../Stores/useLinkStore' import { useRecentsStore } from '../Stores/useRecentsStore' import { useReminderStore } from '../Stores/useReminderStore' import { useSnippetStore } from '../Stores/useSnippetStore' -import useThemeStore from '../Stores/useThemeStore' +import { useUserPreferenceStore } from '../Stores/userPreferenceStore' import { initSearchIndex, searchWorker } from '../Workers/controller' export default function Chotu() { const [parent, setParent] = useState(null) const { userDetails, workspaceDetails } = useAuthStore() - const theme = useThemeStore((state) => state.theme) + const theme = useUserPreferenceStore((store) => store.theme) const snippets = useSnippetStore((store) => store.snippets) const reminders = useReminderStore((store) => store.reminders) const descriptions = useDescriptionStore((store) => store.descriptions) diff --git a/apps/webapp/src/Components/Editor/ContentEditor.tsx b/apps/webapp/src/Components/Editor/ContentEditor.tsx index 3df78aa1b..06d96b8c5 100644 --- a/apps/webapp/src/Components/Editor/ContentEditor.tsx +++ b/apps/webapp/src/Components/Editor/ContentEditor.tsx @@ -14,6 +14,7 @@ import { useComboboxOpen } from '../../Editor/Hooks/useComboboxOpen' import { useApi } from '../../Hooks/API/useNodeAPI' import { useKeyListener } from '../../Hooks/useChangeShortcutListener' import { useEditorBuffer } from '../../Hooks/useEditorBuffer' +import { useLastOpened } from '../../Hooks/useLastOpened' import useLayout from '../../Hooks/useLayout' import useLoad from '../../Hooks/useLoad' import { usePermissions, isReadonly } from '../../Hooks/usePermissions' @@ -48,7 +49,7 @@ const ContentEditor = () => { const infobar = useLayoutStore((store) => store.infobar) const editorWrapperRef = useRef(null) - // const { debouncedAddLastOpened } = useLastOpened() + const { debouncedAddLastOpened } = useLastOpened() const { addOrUpdateValBuffer, getBufferVal, saveAndClearBuffer } = useEditorBuffer() const nodeid = useParams()?.nodeId @@ -68,7 +69,7 @@ const ContentEditor = () => { async (val: any[]) => { if (val && nodeid !== '__null__') { addOrUpdateValBuffer(nodeid, val) - // debouncedAddLastOpened(node.nodeid) + debouncedAddLastOpened(nodeid) } }, [nodeid] diff --git a/apps/webapp/src/Components/Init.tsx b/apps/webapp/src/Components/Init.tsx index 69025262d..cb8544320 100644 --- a/apps/webapp/src/Components/Init.tsx +++ b/apps/webapp/src/Components/Init.tsx @@ -5,6 +5,7 @@ import { useAuth } from '@workduck-io/dwindle' import { addIconsToIconify } from '@mexit/shared' import { useInitLoader } from '../Hooks/useInitLoader' +import { useAutoSyncUserPreference } from '../Hooks/useSyncUserPreferences' import config from '../config' const Init = () => { @@ -30,6 +31,7 @@ const Init = () => { }, []) useInitLoader() + useAutoSyncUserPreference() return null } diff --git a/apps/webapp/src/Components/Sidebar/SharedNotes/index.tsx b/apps/webapp/src/Components/Sidebar/SharedNotes/index.tsx index 57aceedf7..b87a6e220 100644 --- a/apps/webapp/src/Components/Sidebar/SharedNotes/index.tsx +++ b/apps/webapp/src/Components/Sidebar/SharedNotes/index.tsx @@ -11,6 +11,7 @@ import { useRouting, ROUTE_PATHS, NavigationType } from '../../../Hooks/useRouti import { useDataStore } from '../../../Stores/useDataStore' import { useEditorStore } from '../../../Stores/useEditorStore' import SidebarList, { SidebarListItem } from '../SidebarList' +import { MuteMenuItem } from '../TreeWithContextMenu' export const ItemContent = styled.div` cursor: pointer; @@ -29,11 +30,9 @@ interface SharedNoteContextMenuProps { const SharedNoteContextMenu = ({ item }: SharedNoteContextMenuProps) => { return ( - <> - - {/* */} - - + + + ) } @@ -53,6 +52,7 @@ const SharedNotes = () => { return sharedNodes.length > 0 ? ( ({ id: node.nodeid, label: node.path, @@ -70,7 +70,7 @@ const SharedNotes = () => { ) : ( - No one has shared Notes with you yet! + No one has shared Notes with you yet HAHAHAHA! ) } diff --git a/apps/webapp/src/Components/Sidebar/SidebarList.tsx b/apps/webapp/src/Components/Sidebar/SidebarList.tsx index a64376ffb..ecf86041a 100644 --- a/apps/webapp/src/Components/Sidebar/SidebarList.tsx +++ b/apps/webapp/src/Components/Sidebar/SidebarList.tsx @@ -80,7 +80,6 @@ const SidebarList = ({ const [source, target] = useSingleton() - const theme = useTheme() const inputRef = useRef(null) const expandSidebar = useLayoutStore((store) => store.expandSidebar) diff --git a/apps/webapp/src/Components/Sidebar/SidebarListItem.tsx b/apps/webapp/src/Components/Sidebar/SidebarListItem.tsx index feb04046a..7d80e60ad 100644 --- a/apps/webapp/src/Components/Sidebar/SidebarListItem.tsx +++ b/apps/webapp/src/Components/Sidebar/SidebarListItem.tsx @@ -8,9 +8,11 @@ import { Entity } from 'rc-tree/lib/interface' import { ItemContent, ItemTitle, ItemCount } from '@workduck-io/mex-components' -import { ItemTitleText, StyledTreeItem } from '@mexit/shared' +import { ItemTitleText, LastOpenedState, StyledTreeItem, UnreadIndicator } from '@mexit/shared' -import { SidebarListItem, LastOpenedState } from './SidebarList' +import { useLastOpened } from '../../Hooks/useLastOpened' +import { useUserPreferenceStore } from '../../Stores/userPreferenceStore' +import { SidebarListItem } from './SidebarList' import { TooltipContent } from './Tree' interface SidebarListItemProps { @@ -42,17 +44,17 @@ const SidebarListItemComponent = ({ const { ItemContextMenu, setContextOpenViewId, contextOpenViewId } = contextMenu const { selectedItemId, selectIndex, onSelect } = select - // const lastOpenedNote = useUserPreferenceStore((state) => state.lastOpenedNotes[item?.lastOpenedId]) - // const { getLastOpened } = useLastOpened() + const lastOpenedNote = useUserPreferenceStore((state) => state.lastOpenedNotes[item?.lastOpenedId]) + const { getLastOpened } = useLastOpened() - // const lastOpenedState = useMemo(() => { - // const loState = getLastOpened(item?.lastOpenedId, lastOpenedNote) - // return loState - // }, [lastOpenedNote, item?.lastOpenedId]) + const lastOpenedState = useMemo(() => { + const loState = getLastOpened(item?.lastOpenedId, lastOpenedNote) + return loState + }, [lastOpenedNote, item?.lastOpenedId]) - // const isUnread = useMemo(() => { - // return lastOpenedState === LastOpenedState.UNREAD - // }, [lastOpenedState]) + const isUnread = useMemo(() => { + return lastOpenedState === LastOpenedState.UNREAD + }, [lastOpenedState]) return ( ({ {item.label} - {/* {isUnread && ( + {isUnread && ( - )} */} + )} {ItemContextMenu && ( diff --git a/apps/webapp/src/Components/Sidebar/TreeWithContextMenu.tsx b/apps/webapp/src/Components/Sidebar/TreeWithContextMenu.tsx index b2d77e169..6028eee73 100644 --- a/apps/webapp/src/Components/Sidebar/TreeWithContextMenu.tsx +++ b/apps/webapp/src/Components/Sidebar/TreeWithContextMenu.tsx @@ -5,12 +5,17 @@ import addCircleLine from '@iconify/icons-ri/add-circle-line' import archiveLine from '@iconify/icons-ri/archive-line' import magicLine from '@iconify/icons-ri/magic-line' import shareLine from '@iconify/icons-ri/share-line' +import volumeDownLine from '@iconify/icons-ri/volume-down-line' +import volumeMuteLine from '@iconify/icons-ri/volume-mute-line' import { Icon } from '@iconify/react' import * as ContextMenuPrimitive from '@radix-ui/react-context-menu' import 'react-contexify/dist/ReactContexify.css' import toast from 'react-hot-toast' +import { LastOpenedState } from '@mexit/shared' + import { useCreateNewNote } from '../../Hooks/useCreateNewNote' +import { useLastOpened } from '../../Hooks/useLastOpened' import { useNamespaces } from '../../Hooks/useNamespaces' import { useNavigation } from '../../Hooks/useNavigation' import { useRefactor } from '../../Hooks/useRefactor' @@ -25,6 +30,39 @@ import { useDeleteStore } from '../Refactor/DeleteModal' import { doesLinkRemain } from '../Refactor/doesLinkRemain' import ContextMenuListWithFilter from './ContextMenuListWithFilter' +interface MuteMenuItemProps { + nodeid: string + lastOpenedState: LastOpenedState +} + +export const MuteMenuItem = ({ nodeid, lastOpenedState }: MuteMenuItemProps) => { + const { muteNode, unmuteNode } = useLastOpened() + + const isMuted = useMemo(() => { + return lastOpenedState === LastOpenedState.MUTED + }, [lastOpenedState]) + + const handleMute = () => { + // mog('handleMute', { item }) + if (isMuted) { + unmuteNode(nodeid) + } else { + muteNode(nodeid) + } + } + + return ( + { + handleMute() + }} + > + + {isMuted ? 'Unmute' : 'Mute'} + + ) +} + interface TreeContextMenuProps { item: TreeItem } @@ -160,8 +198,8 @@ export const TreeContextMenu = ({ item }: TreeContextMenuProps) => { }} filter={false} /> - {' '} - {/* */} + + { - const themes = useThemeStore((state) => state.themes) - const theme = useThemeStore((state) => state.theme) - const setTheme = useThemeStore((state) => state.setTheme) + const themes = defaultThemes + const theme = useUserPreferenceStore((state) => state.theme) + const setTheme = useUserPreferenceStore((state) => state.setTheme) + + const { updateUserPreferences } = useUserService() const transition = useTransition(themes, { from: { @@ -34,8 +37,10 @@ const Themes = () => { const onThemeSelect = (i: number) => { if (themes[i]) { - setTheme(themes[i]) + setTheme(themes[i].id) } + + updateUserPreferences() } return ( @@ -43,7 +48,9 @@ const Themes = () => { {transition((styles, t, _t, i) => { return ( - onThemeSelect(i)} style={styles}> + {/* eslint-disable-next-line */} + {/* @ts-ignore */} + onThemeSelect(i)} style={styles}>
diff --git a/apps/webapp/src/Hooks/API/useNodeAPI.ts b/apps/webapp/src/Hooks/API/useNodeAPI.ts index c156089fd..6a46d7258 100644 --- a/apps/webapp/src/Hooks/API/useNodeAPI.ts +++ b/apps/webapp/src/Hooks/API/useNodeAPI.ts @@ -23,6 +23,7 @@ import { deserializeContent, serializeContent } from '../../Utils/serializer' import { WorkerRequestType } from '../../Utils/worker' import { runBatchWorker } from '../../Workers/controller' import { useInternalLinks } from '../useInternalLinks' +import { useLastOpened } from '../useLastOpened' import { useLinks } from '../useLinks' import { useNodes } from '../useNodes' import { useSearch } from '../useSearch' @@ -38,11 +39,9 @@ export const useApi = () => { const setContent = useContentStore((store) => store.setContent) const { getTitleFromNoteId } = useLinks() const { updateILinksFromAddedRemovedPaths } = useInternalLinks() - const { setNodePublic, setNodePrivate, checkNodePublic, setNamespaces, addInArchive } = useDataStore() + const { setNodePublic, setNodePrivate, checkNodePublic } = useDataStore() const { updateFromContent } = useUpdater() - const setILinks = useDataStore((store) => store.setIlinks) const { getSharedNode } = useNodes() - const { updateDocument, removeDocument } = useSearch() const initSnippets = useSnippetStore((store) => store.initSnippets) const { updateSnippet } = useSnippets() @@ -51,6 +50,8 @@ export const useApi = () => { const { workspaceHeaders } = useAPIHeaders() const currentUser = useAuthStore((store) => store.userDetails) + const { addLastOpened } = useLastOpened() + /* * Saves new node data in the backend * Also updates the incoming data in the store @@ -84,6 +85,7 @@ export const useApi = () => { const metadata = extractMetadata(d.data) const content = deserializeContent(d.data.data ?? options.content) updateFromContent(noteID, content, metadata) + addLastOpened(noteID) return d.data }) .catch((e) => { @@ -133,6 +135,7 @@ export const useApi = () => { updateILinksFromAddedRemovedPaths(addedILinks, removedILinks) setMetadata(noteID, extractMetadata(node)) + addLastOpened(noteID) }) return data @@ -198,6 +201,7 @@ export const useApi = () => { lastEditedBy: currentUser.userID }) } + addLastOpened(noteID) return d.data }) .catch((e) => { diff --git a/apps/webapp/src/Hooks/API/useUserAPI.ts b/apps/webapp/src/Hooks/API/useUserAPI.ts index db446c5fe..6c43a2ab3 100644 --- a/apps/webapp/src/Hooks/API/useUserAPI.ts +++ b/apps/webapp/src/Hooks/API/useUserAPI.ts @@ -1,9 +1,12 @@ import { client } from '@workduck-io/dwindle' import { apiURLs, mog } from '@mexit/core' +import { UserPreferences } from '@mexit/shared' +import { version } from '../../../package.json' import { useAuthStore } from '../../Stores/useAuth' import { useUserCacheStore } from '../../Stores/useUserCacheStore' +import { useUserPreferenceStore } from '../../Stores/userPreferenceStore' export interface TempUser { email: string @@ -19,30 +22,41 @@ export interface TempUserUserID { name?: string } +export interface UserDetails { + /** User ID */ + id: string + /** Workspace ID */ + group: string + entity: 'User' + email: string + name: string + alias: string + preference: UserPreferences +} + export const useUserService = () => { const addUser = useUserCacheStore((s) => s.addUser) const getUser = useUserCacheStore((s) => s.getUser) const updateUserDetails = useAuthStore((s) => s.updateUserDetails) - const getUserDetails = async (email: string): Promise => { const user = getUser({ email }) if (user) return user try { - return await client.get(apiURLs.user.getFromEmail(email)).then((resp: any) => { + return await client.get(apiURLs.user.getFromEmail(email)).then((resp) => { mog('Response', { data: resp.data }) if (resp?.data?.userId && resp?.data?.name) { addUser({ email, userID: resp?.data?.userId, - alias: resp?.data?.alias ?? resp?.data?.properties?.alias ?? resp?.data?.name, + alias: resp?.data?.alias ?? resp?.data?.name, name: resp?.data?.name }) } return { email, userID: resp?.data?.userId, - alias: resp?.data?.alias ?? resp?.data?.properties?.alias ?? resp?.data?.name, + alias: resp?.data?.alias ?? resp?.data?.name, name: resp?.data?.name } }) @@ -55,9 +69,10 @@ export const useUserService = () => { const getUserDetailsUserId = async (userID: string): Promise => { const user = getUser({ userID }) if (user) return user + try { return await client.get(apiURLs.user.getFromUserId(userID)).then((resp: any) => { - mog('Response', { data: resp.data }) + // mog('Response', { data: resp.data }) if (resp?.data?.email && resp?.data?.name) { addUser({ userID, @@ -78,6 +93,7 @@ export const useUserService = () => { return { userID } } } + const updateUserInfo = async (userID: string, name?: string, alias?: string): Promise => { try { if (name === undefined && alias === undefined) return false @@ -92,5 +108,52 @@ export const useUserService = () => { } } - return { getUserDetails, getUserDetailsUserId, updateUserInfo } + const updateUserPreferences = async (): Promise => { + const lastOpenedNotes = useUserPreferenceStore.getState().lastOpenedNotes + const lastUsedSnippets = useUserPreferenceStore.getState().lastUsedSnippets + const theme = useUserPreferenceStore.getState().theme + const userID = useAuthStore.getState().userDetails.userID + + const userPreferences: UserPreferences = { + version, + lastOpenedNotes, + lastUsedSnippets, + theme + } + + try { + return await client.put(apiURLs.user.updateInfo, { id: userID, preference: userPreferences }).then((resp) => { + return true + }) + } catch (e) { + mog('Error Updating User Info', { error: e, userID }) + return false + } + } + + const getCurrentUser = async (): Promise => { + try { + return await client.get(apiURLs.getUserRecords).then((resp) => { + mog('Response', { data: resp.data }) + return resp?.data + }) + } catch (e) { + mog('Error Fetching Current User Info', { error: e }) + return undefined + } + } + + const getAllKnownUsers = () => { + const cache = useUserCacheStore.getState().cache + return cache + } + + return { + getAllKnownUsers, + getUserDetails, + getUserDetailsUserId, + updateUserInfo, + updateUserPreferences, + getCurrentUser + } } diff --git a/apps/webapp/src/Hooks/useCreateNewNote.tsx b/apps/webapp/src/Hooks/useCreateNewNote.tsx index 2f14b3ab0..4538072f5 100644 --- a/apps/webapp/src/Hooks/useCreateNewNote.tsx +++ b/apps/webapp/src/Hooks/useCreateNewNote.tsx @@ -6,6 +6,7 @@ import { useContentStore } from '../Stores/useContentStore' import { useDataStore } from '../Stores/useDataStore' import { useEditorStore } from '../Stores/useEditorStore' import { useHierarchy } from './useHierarchy' +import { useLastOpened } from './useLastOpened' import { useLinks } from './useLinks' import useLoad from './useLoad' import { useNamespaces } from './useNamespaces' @@ -35,7 +36,7 @@ export const useCreateNewNote = () => { const { saveNodeName } = useLoad() const { getParentILink } = useLinks() const { addInHierarchy } = useHierarchy() - // const { addLastOpened } = useLastOpened() + const { addLastOpened } = useLastOpened() const { getDefaultNamespace } = useNamespaces() const getMetadata = useContentStore((s) => s.getMetadata) @@ -89,7 +90,7 @@ export const useCreateNewNote = () => { }).then(() => { saveNodeName(useEditorStore.getState().node.nodeid) - // addLastOpened(node.nodeid) + addLastOpened(node.nodeid) }) if (!options?.noRedirect) { push(node.nodeid, { withLoading: false, fetch: false }) diff --git a/apps/webapp/src/Hooks/useLastOpened.tsx b/apps/webapp/src/Hooks/useLastOpened.tsx new file mode 100644 index 000000000..542bdf79e --- /dev/null +++ b/apps/webapp/src/Hooks/useLastOpened.tsx @@ -0,0 +1,112 @@ +import { useCallback } from 'react' + +import { debounce } from 'lodash' + +import { LastOpenedData, LastOpenedState } from '@mexit/shared' + +import { getInitialNode, NodeType } from '../../../../libs/core/src' +import { useUserPreferenceStore } from '../Stores/userPreferenceStore' +import { useLinks } from './useLinks' +import { useNodes } from './useNodes' + +const DEBOUNCE_TIME = 3000 + +const INIT_LAST_OPENED = { + freq: 0, + ts: 0, + muted: false +} + +export const getLastOpenedState = (updatedAt: number, lastOpenedNote: LastOpenedData): LastOpenedState => { + // mog('getLastOpenedState', { updatedAt, lastOpenedNote }) + if (lastOpenedNote.muted) { + return LastOpenedState.MUTED + } else if (updatedAt > lastOpenedNote.ts) { + return LastOpenedState.UNREAD + } else if (updatedAt < lastOpenedNote.ts) { + return LastOpenedState.OPENED + } else { + // Default to unread + return LastOpenedState.UNREAD + } +} + +/** + * Hook to update the last opened timestamp of a node + */ +export const useLastOpened = () => { + const setLastOpenedNotes = useUserPreferenceStore((state) => state.setLastOpenedNotes) + const { getNodeType, getSharedNode } = useNodes() + const { getILinkFromNodeid } = useLinks() + /** + * Update the last opened timestamp of a node + * The current timestamp is used as the last opened timestamp + */ + const addLastOpened = (nodeId: string) => { + const lastOpenedNotes = useUserPreferenceStore.getState().lastOpenedNotes + const initNode = getInitialNode() + if (nodeId === initNode.nodeid) { + return + } + const lastOpenedNote = lastOpenedNotes[nodeId] || { ...INIT_LAST_OPENED } + // This replaces any previous timestamp with the current timestamp + const newLastOpenedNotes = { + ...lastOpenedNotes, + [nodeId]: { + ...lastOpenedNote, + ts: Date.now(), + freq: lastOpenedNote.freq + 1 + } + } + // mog('addLastOpened', { nodeId, lastOpenedNotes }) + setLastOpenedNotes(newLastOpenedNotes) + } + + const setMuteNode = (nodeId: string, muted: boolean) => { + const lastOpenedNotes = useUserPreferenceStore.getState().lastOpenedNotes + // const storeMeta = useUserPreferenceStore.getState().meta + const lastOpenedNote = lastOpenedNotes[nodeId] || { ...INIT_LAST_OPENED } + const newLastOpenedNotes = { + ...lastOpenedNotes, + [nodeId]: { + ...lastOpenedNote, + muted + } + } + // mog('setMuteNode', { nodeId, muted, newLastOpenedNotes }) + setLastOpenedNotes(newLastOpenedNotes) + } + + const muteNode = (nodeId: string) => { + setMuteNode(nodeId, true) + } + const unmuteNode = (nodeId: string) => { + setMuteNode(nodeId, false) + } + + const getLastOpened = (nodeId: string, lastOpenedNote: LastOpenedData) => { + const nodeType = getNodeType(nodeId) + switch (nodeType) { + case NodeType.DEFAULT: { + const metadata = getILinkFromNodeid(nodeId) + const updatedAt = metadata?.updatedAt ?? undefined + const lastOpenedState = lastOpenedNote ? getLastOpenedState(updatedAt, lastOpenedNote) : undefined + return lastOpenedState + } + case NodeType.SHARED: { + const sharedNote = getSharedNode(nodeId) + const updatedAt = sharedNote?.updatedAt ?? undefined + const lastOpenedState = lastOpenedNote ? getLastOpenedState(updatedAt, lastOpenedNote) : undefined + return lastOpenedState + } + default: { + return undefined + } + } + } + + // Callback so that the debounced function is only generated once + const debouncedAddLastOpened = useCallback(debounce(addLastOpened, DEBOUNCE_TIME, { trailing: true }), []) + + return { addLastOpened, debouncedAddLastOpened, muteNode, unmuteNode, getLastOpened } +} diff --git a/apps/webapp/src/Hooks/useLoad.ts b/apps/webapp/src/Hooks/useLoad.ts index d68effeee..64cfff68c 100644 --- a/apps/webapp/src/Hooks/useLoad.ts +++ b/apps/webapp/src/Hooks/useLoad.ts @@ -26,6 +26,7 @@ import { useUserPreferenceStore } from '../Stores/userPreferenceStore' import { useApi } from './API/useNodeAPI' import { useBufferStore, useEditorBuffer } from './useEditorBuffer' import { useFetchShareData } from './useFetchShareData' +import { useLastOpened } from './useLastOpened' import { getLinkFromNodeIdHookless } from './useLinks' import { useRefactor } from './useRefactor' import useToggleElements from './useToggleElements' @@ -58,7 +59,7 @@ const useLoad = () => { const infobar = useLayoutStore((store) => store.infobar) const setHighlights = useBlockHighlightStore((store) => store.setHighlightedBlockIds) const { fetchSharedUsers } = useFetchShareData() - // const { debouncedAddLastOpened } = useLastOpened() + const { debouncedAddLastOpened } = useLastOpened() const changeSpace = useUserPreferenceStore((store) => store.setActiveNamespace) const setLoadingNodeid = useEditorStore((store) => store.setLoadingNodeid) @@ -286,7 +287,7 @@ const useLoad = () => { expandNodes(allParents) } - // debouncedAddLastOpened(nodeid) + debouncedAddLastOpened(nodeid) mog('Loading that here', { node }) changeSpace(node.namespace) diff --git a/apps/webapp/src/Hooks/useSyncUserPreferences.ts b/apps/webapp/src/Hooks/useSyncUserPreferences.ts new file mode 100644 index 000000000..732cb063b --- /dev/null +++ b/apps/webapp/src/Hooks/useSyncUserPreferences.ts @@ -0,0 +1,50 @@ +import { useEffect } from 'react' + +import { mog } from '@mexit/core' +import { mergeUserPreferences } from '@mexit/shared' + +import { useUserPreferenceStore } from '../Stores/userPreferenceStore' +import { useUserService } from './API/useUserAPI' + +const USER_PREF_AUTO_SAVE_MS = 30 * 60 * 1000 // 30 minutes + +export const useAutoSyncUserPreference = () => { + const getUserPreferences = useUserPreferenceStore((s) => s.getUserPreferences) + const setUserPreferences = useUserPreferenceStore((store) => store.setUserPreferences) + const hasHydrated = useUserPreferenceStore((s) => s._hasHydrated) + const { updateUserPreferences, getCurrentUser } = useUserService() + + const updateCurrentUserPreferences = async () => { + const user = await getCurrentUser() + if (user) { + const userPreferences = user.preference + mog('User Preferences Fetched: ', { userPreferences }) + if (userPreferences) { + const localUserPreferences = getUserPreferences() + const mergedUserPreferences = mergeUserPreferences(localUserPreferences, userPreferences) + setUserPreferences(mergedUserPreferences) + } + } + } + + /** + * Fetches the user preference once + */ + useEffect(() => { + // mog(`Fetching User Preferences`) + if (hasHydrated) { + // mog('Hydration finished') + updateCurrentUserPreferences() + } + }, [hasHydrated]) + + /** + * Saves the user preference at every interval + */ + useEffect(() => { + const intervalId = setInterval(() => { + updateUserPreferences() + }, USER_PREF_AUTO_SAVE_MS) + return () => clearInterval(intervalId) + }, []) +} diff --git a/apps/webapp/src/Stores/index.ts b/apps/webapp/src/Stores/index.ts index c17c4c7f7..5727216e2 100644 --- a/apps/webapp/src/Stores/index.ts +++ b/apps/webapp/src/Stores/index.ts @@ -1,7 +1,6 @@ import { share } from 'shared-zustand' import { useAuthStore } from './useAuth' -import useThemeStore from './useThemeStore' import { useContentStore } from './useContentStore' import { useDataStore } from './useDataStore' import { useSnippetStore } from './useSnippetStore' @@ -15,7 +14,6 @@ import { useHighlightStore } from './useHighlightStore' // progressive enhancement check. if ('BroadcastChannel' in globalThis /* || isSupported() */) { // share the property "count" of the state with other tabs - share('theme', useThemeStore, { ref: 'share-theme' }) share('ilinks', useDataStore, { ref: 'share-ilinks' }) share('namespaces', useDataStore, { ref: 'share-namespaces' }) share('archive', useDataStore, { ref: 'share-archive' }) diff --git a/apps/webapp/src/Stores/useThemeStore.ts b/apps/webapp/src/Stores/useThemeStore.ts deleted file mode 100644 index 20642f30c..000000000 --- a/apps/webapp/src/Stores/useThemeStore.ts +++ /dev/null @@ -1,17 +0,0 @@ -import create from 'zustand' -import { persist } from 'zustand/middleware' - -import { themeStoreConstructor, ThemeStoreState, defaultThemes } from '@mexit/shared' - -const useThemeStore = create( - persist(themeStoreConstructor, { - name: 'mexit-theme-store', - version: 5, - migrate: (persistedState: ThemeStoreState, version: number) => { - persistedState.themes = defaultThemes - return persistedState - } - }) -) - -export default useThemeStore diff --git a/apps/webapp/src/Utils/tree.ts b/apps/webapp/src/Utils/tree.ts index 48679b6d4..90ba56d1d 100644 --- a/apps/webapp/src/Utils/tree.ts +++ b/apps/webapp/src/Utils/tree.ts @@ -2,7 +2,7 @@ import { TreeItem, TreeData } from '@atlaskit/tree' import { ItemId } from '@atlaskit/tree/dist/types/types' import { Contents, mog, NodeMetadata, isElder, isParent, getNameFromPath, getParentNodePath } from '@mexit/core' -import { TreeNode } from '@mexit/shared' +import { LastOpenedState, TreeNode } from '@mexit/shared' import { useReminderStore } from '../Stores/useReminderStore' import { useTodoStore } from '../Stores/useTodoStore' @@ -21,15 +21,12 @@ export const sortTree = (tree: TreeNode[], contents: Contents): TreeNode[] => { if (aMeta.createdAt && bMeta.createdAt) { return bMeta.createdAt - aMeta.createdAt } - if (aMeta.createdAt && !bMeta.createdAt) { return -1 } - if (bMeta.createdAt && !aMeta.createdAt) { return 1 } - return 0 } @@ -115,35 +112,15 @@ const insertInNested = (iNode: BaseTreeNode, nestedTree: BaseTreeNode[]) => { export interface FlatItem { id: string nodeid: string - namespace: string + namespace?: string parentNodeId?: string tasks?: number reminders?: number - // lastOpenedState?: LastOpenedState + lastOpenedState?: LastOpenedState icon?: string stub?: boolean } -const getItem = (treeFlat: FlatItem[], id: string): number | undefined => { - const item = treeFlat.findIndex((n) => n.id === id) - return item ? item : null -} - -const getParentItemId = (path: string, treeData: TreeData): string | undefined => { - const parentPath = getParentNodePath(path) - - if (parentPath) { - const parentItem = Object.entries(treeData.items).find(([_k, n]) => { - return n.data.path === parentPath - }) - // mog('parentPath', { parentPath, parentItem, treeData }) - if (parentItem) { - return parentItem[0] - } - } - return undefined -} - export const TREE_SEPARATOR = '-' interface BaseTreeNode { @@ -154,7 +131,7 @@ interface BaseTreeNode { icon?: string tasks?: number reminders?: number - // lastOpenedState?: LastOpenedState + lastOpenedState?: LastOpenedState } const getItemFromBaseNestedTree = ( @@ -234,7 +211,6 @@ export const getBaseNestedTree = (flatTree: FlatItem[]): BaseTreeNode[] => { let baseNestedTree: BaseTreeNode[] = [] flatTree.forEach((n) => { - // console.log('n', n.id) const parentId = getParentNodePath(n.id) const tasks = todos[n.nodeid] ? todos[n.nodeid].filter(filterIncompleteTodos).length : 0 const reminders = reminderGroups[n.nodeid] ? reminderGroups[n.nodeid].length : 0 @@ -271,7 +247,7 @@ export const generateTree = ( sort?: (a: BaseTreeNode, b: BaseTreeNode) => number ): TreeData => { // tree should be sorted - mog('FLAT TREE', { treeFlat }) + // mog('GenerateTree ', { treeFlat }) const unsortedBaseNestedTree = getBaseNestedTree(treeFlat) const baseNestedTree = sort ? unsortedBaseNestedTree.sort(sort) : sortTreeWithPriority(unsortedBaseNestedTree) const nestedTree: TreeData = { @@ -310,8 +286,8 @@ export const generateTree = ( namespace: n.namespace, stub: n.stub, tasks: nestedItem.tasks, - reminders: nestedItem.reminders - // lastOpenedState: nestedItem.lastOpenedState + reminders: nestedItem.reminders, + lastOpenedState: nestedItem.lastOpenedState }), isExpanded: expanded.includes(n.id) } @@ -336,11 +312,10 @@ export const generateTree = ( path: n.id, mex_icon: n.icon, namespace: n.namespace, - parentId, stub: n.stub, tasks: nestedItem.tasks, - reminders: nestedItem.reminders - // lastOpenedState: nestedItem.lastOpenedState + reminders: nestedItem.reminders, + lastOpenedState: nestedItem.lastOpenedState }), isExpanded: expanded.includes(n.id) } diff --git a/libs/core/src/Stores/blockStoreConstructor.ts b/libs/core/src/Stores/blockStoreConstructor.ts index 05f30b0cb..f6ccdd7cc 100644 --- a/libs/core/src/Stores/blockStoreConstructor.ts +++ b/libs/core/src/Stores/blockStoreConstructor.ts @@ -39,7 +39,7 @@ export const blockStoreConstructor = (set, get) => ({ }, getBlocks: () => { const blocks = get().blocks - return Object.values(blocks) + return Object.values(blocks) }, deleteBlock: (blockId: string) => { const blocks = get().blocks diff --git a/libs/core/src/Stores/contentStoreConstructor.ts b/libs/core/src/Stores/contentStoreConstructor.ts index 661feb9cd..f9f2a8206 100644 --- a/libs/core/src/Stores/contentStoreConstructor.ts +++ b/libs/core/src/Stores/contentStoreConstructor.ts @@ -1,5 +1,4 @@ import { Contents, NodeContent, NodeEditorContent, NodeMetadata } from '../Types/Editor' -import { mog } from '../Utils/mog' export interface ContentStoreState { contents: Contents diff --git a/libs/shared/src/Stores/preferenceStoreConstructor.ts b/libs/shared/src/Stores/preferenceStoreConstructor.ts index b62e45623..0aeabcb6f 100644 --- a/libs/shared/src/Stores/preferenceStoreConstructor.ts +++ b/libs/shared/src/Stores/preferenceStoreConstructor.ts @@ -1,28 +1,33 @@ -import { LastOpenedNotes, UserPreferences } from '../Types/userPreference' +import { LastOpenedData, LastOpenedNotes, LastUsedSnippets, UserPreferences } from '../Types/userPreference' export interface UserPreferenceStore extends UserPreferences { _hasHydrated: boolean + smartCaptureExcludedFields?: any setHasHydrated: (state) => void setTheme: (theme: string) => void setLastOpenedNotes: (lastOpenedNotes: LastOpenedNotes) => void + setLastUsedSnippets: (lastUsedSnippets: LastUsedSnippets) => void getUserPreferences: () => UserPreferences setUserPreferences: (userPreferences: UserPreferences) => void + setActiveNamespace: (namespace: string) => void excludeSmartCaptureField: (page: string, fieldId: string) => void removeExcludedSmartCaptureField: (page: string, fieldId: string) => void - setActiveNamespace: (namespace: string) => void } export const preferenceStoreConstructor = (set, get) => ({ - lastOpenedNotes: {}, + _hasHydrated: false, version: 'unset', - smartCaptureExcludedFields: {}, theme: 'xeM', - _hasHydrated: false, + lastOpenedNotes: {}, + lastUsedSnippets: {}, + smartCaptureExcludedFields: {}, + excludeSmartCaptureField: (page: string, fieldId: string) => { const webPageConfigs = get().smartCaptureExcludedFields set({ smartCaptureExcludedFields: { ...webPageConfigs, [page]: [...(webPageConfigs[page] ?? []), fieldId] } }) }, + removeExcludedSmartCaptureField: (page: string, fieldId: string) => { const webPageConfigs = get().smartCaptureExcludedFields @@ -40,7 +45,7 @@ export const preferenceStoreConstructor = (set, get) => ({ return { lastOpenedNotes: get().lastOpenedNotes, version: get().version, - smartCapture: get().smartCapture, + lastUsedSnippets: get().lastUsedSnippets, theme: get().theme } }, @@ -57,23 +62,19 @@ export const preferenceStoreConstructor = (set, get) => ({ set({ lastOpenedNotes: lastOpenedNotes }) + }, + setLastUsedSnippets: (lastUsedSnippets) => { + set({ lastUsedSnippets }) } }) -/** - * Merging user preferences from the remote server with the local preferences - * - * The remote user preferences may be lagging as the local preferences - * have not been saved on exit - */ -export const mergeUserPreferences = (local: UserPreferences, remote: UserPreferences): UserPreferences => { - const { version, lastOpenedNotes, theme, smartCaptureExcludedFields } = local - const { lastOpenedNotes: remoteLastOpenedNotes } = remote - - // For all lastOpened of remote - const mergedLastOpenedNotes = Object.keys(remoteLastOpenedNotes).reduce((acc, key) => { - const localLastOpenedNote = lastOpenedNotes[key] - const remoteLastOpenedNote = remoteLastOpenedNotes[key] +export const mergeLastOpenedData = ( + remote: Record, + local: Record +): Record => { + const merged = Object.keys(remote).reduce((acc, key) => { + const localLastOpenedNote = local[key] + const remoteLastOpenedNote = remote[key] // If a local lastOpenedNote exists if (localLastOpenedNote) { // Get the latest of the two which has the latest lastOpened @@ -89,12 +90,28 @@ export const mergeUserPreferences = (local: UserPreferences, remote: UserPrefere return acc }, {}) + return merged +} + +/** + * Merging user preferences from the remote server with the local preferences + * + * The remote user preferences may be lagging as the local preferences + * have not been saved on exit + */ +export const mergeUserPreferences = (local: UserPreferences, remote: UserPreferences): UserPreferences => { + // For all lastOpened of remote + const mergedLastOpenedNotes = mergeLastOpenedData(remote.lastOpenedNotes, local.lastOpenedNotes) + const mergedLastUsedSnippets = mergeLastOpenedData(remote.lastUsedSnippets, local.lastUsedSnippets) + + // mog('mergedLastOpenedNotes', { localLastOpenedNotes, mergedLastOpenedNotes, local, remote }) return { - version, + version: local.version, // Overwrite all notes with the remote notes which exist // The local notes which do not exist in the remote notes will be left alone - lastOpenedNotes: { ...lastOpenedNotes, ...mergedLastOpenedNotes }, - theme, - smartCaptureExcludedFields + lastOpenedNotes: { ...local.lastOpenedNotes, ...mergedLastOpenedNotes }, + lastUsedSnippets: { ...local.lastUsedSnippets, ...mergedLastUsedSnippets }, + theme: local.theme, + smartCaptureExcludedFields: local.smartCaptureExcludedFields } } diff --git a/libs/shared/src/Types/userPreference.ts b/libs/shared/src/Types/userPreference.ts index 54a59cb83..9529fbfb5 100644 --- a/libs/shared/src/Types/userPreference.ts +++ b/libs/shared/src/Types/userPreference.ts @@ -7,7 +7,7 @@ export enum LastOpenedState { /** * Last opened note details */ -export interface LastOpenedNote { +export interface LastOpenedData { /** Number of times opened */ freq: number @@ -22,14 +22,19 @@ export interface LastOpenedNote { * Last opened note mapped to their nodeid */ export interface LastOpenedNotes { - [nodeid: string]: LastOpenedNote + [nodeid: string]: LastOpenedData +} + +export interface LastUsedSnippets { + [snippetid: string]: LastOpenedData } export interface UserPreferences { version: string lastOpenedNotes: LastOpenedNotes + lastUsedSnippets: LastUsedSnippets /** Current mex Theme */ theme?: string - smartCaptureExcludedFields?: Record> activeNamespace?: string // * Namespace Id + smartCaptureExcludedFields?: any } From 03374cdbcb061238a11fa2c4dff2033404b249a9 Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Wed, 9 Nov 2022 22:29:29 +0530 Subject: [PATCH 3/8] Add extra check in getParentPathFromNode in useAnalysis --- libs/core/src/Utils/treeUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/core/src/Utils/treeUtils.ts b/libs/core/src/Utils/treeUtils.ts index 885ffc470..f2514fac2 100644 --- a/libs/core/src/Utils/treeUtils.ts +++ b/libs/core/src/Utils/treeUtils.ts @@ -37,6 +37,7 @@ export const getNodeIcon = (path: string) => { export const DefaultNodeIcon = 'ri:file-list-2-line' export const getParentNodePath = (path: string, separator = SEPARATOR) => { + if (!path) return null const lastIndex = path.lastIndexOf(separator) if (lastIndex === -1) return null return path.slice(0, lastIndex) From 6a069062452a15a6a2509c099578a2292bb98373 Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Thu, 10 Nov 2022 18:57:20 +0530 Subject: [PATCH 4/8] Add theme to broadcast; Use asyncLocalStorage in extension userPreferenceStore --- apps/extension/src/Stores/userPreferenceStore.ts | 5 +++-- apps/webapp/src/Stores/index.ts | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/extension/src/Stores/userPreferenceStore.ts b/apps/extension/src/Stores/userPreferenceStore.ts index 3656948ca..aad2f4bc8 100644 --- a/apps/extension/src/Stores/userPreferenceStore.ts +++ b/apps/extension/src/Stores/userPreferenceStore.ts @@ -1,15 +1,16 @@ import create from 'zustand' import { devtools, persist } from 'zustand/middleware' -import { IDBStorage } from '@mexit/core' import { preferenceStoreConstructor, UserPreferenceStore } from '@mexit/shared' +import { asyncLocalStorage } from '../Utils/chromeStorageAdapter' + export const USER_PREF_STORE_KEY = 'mex-user-preference-store' export const useUserPreferenceStore = create( persist(devtools(preferenceStoreConstructor, { name: 'User Preferences' }), { name: USER_PREF_STORE_KEY, - getStorage: () => IDBStorage, + getStorage: () => asyncLocalStorage, onRehydrateStorage: () => (state) => { state?.setHasHydrated(true) } diff --git a/apps/webapp/src/Stores/index.ts b/apps/webapp/src/Stores/index.ts index 5727216e2..c71c16c64 100644 --- a/apps/webapp/src/Stores/index.ts +++ b/apps/webapp/src/Stores/index.ts @@ -1,12 +1,13 @@ +import { nanoid } from 'nanoid' import { share } from 'shared-zustand' import { useAuthStore } from './useAuth' import { useContentStore } from './useContentStore' import { useDataStore } from './useDataStore' -import { useSnippetStore } from './useSnippetStore' -import { useReminderStore } from './useReminderStore' -import { nanoid } from 'nanoid' import { useHighlightStore } from './useHighlightStore' +import { useReminderStore } from './useReminderStore' +import { useSnippetStore } from './useSnippetStore' +import { useUserPreferenceStore } from './userPreferenceStore' // This is required for event driven messaging, as the tabs or in our // case a tab and a iframe don't know about their state updates, we @@ -28,6 +29,7 @@ if ('BroadcastChannel' in globalThis /* || isSupported() */) { share('reminders', useReminderStore, { ref: 'share-reminders' }) share('publicNodes', useDataStore, { ref: 'share-publicNodes' }) share('highlighted', useHighlightStore, { ref: 'share-highlighted' }) + share('theme', useUserPreferenceStore, { ref: 'share-theme' }) } export default {} From d1dd0a8ac48c99b153a4bd958b0e3f1405783a4a Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Thu, 10 Nov 2022 19:50:22 +0530 Subject: [PATCH 5/8] Add smartCaptureExcludedFields to getUserPreferences store call --- libs/shared/src/Stores/preferenceStoreConstructor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/shared/src/Stores/preferenceStoreConstructor.ts b/libs/shared/src/Stores/preferenceStoreConstructor.ts index 0aeabcb6f..fdba3854f 100644 --- a/libs/shared/src/Stores/preferenceStoreConstructor.ts +++ b/libs/shared/src/Stores/preferenceStoreConstructor.ts @@ -44,6 +44,7 @@ export const preferenceStoreConstructor = (set, get) => ({ getUserPreferences: () => { return { lastOpenedNotes: get().lastOpenedNotes, + smartCaptureExcludedFields: get().smartCaptureExcludedFields, version: get().version, lastUsedSnippets: get().lastUsedSnippets, theme: get().theme From 4db0694568a29cfae9bc6d9344d1fff7b3186808 Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Thu, 10 Nov 2022 20:10:14 +0530 Subject: [PATCH 6/8] Add smartCaptureExcludedFields to API call --- apps/extension/src/Stores/userPreferenceStore.ts | 2 +- apps/webapp/src/Hooks/API/useUserAPI.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/extension/src/Stores/userPreferenceStore.ts b/apps/extension/src/Stores/userPreferenceStore.ts index aad2f4bc8..451acec71 100644 --- a/apps/extension/src/Stores/userPreferenceStore.ts +++ b/apps/extension/src/Stores/userPreferenceStore.ts @@ -8,7 +8,7 @@ import { asyncLocalStorage } from '../Utils/chromeStorageAdapter' export const USER_PREF_STORE_KEY = 'mex-user-preference-store' export const useUserPreferenceStore = create( - persist(devtools(preferenceStoreConstructor, { name: 'User Preferences' }), { + persist(devtools(preferenceStoreConstructor, { name: 'User Preferences Extensions' }), { name: USER_PREF_STORE_KEY, getStorage: () => asyncLocalStorage, onRehydrateStorage: () => (state) => { diff --git a/apps/webapp/src/Hooks/API/useUserAPI.ts b/apps/webapp/src/Hooks/API/useUserAPI.ts index 6c43a2ab3..1894c7d57 100644 --- a/apps/webapp/src/Hooks/API/useUserAPI.ts +++ b/apps/webapp/src/Hooks/API/useUserAPI.ts @@ -112,13 +112,15 @@ export const useUserService = () => { const lastOpenedNotes = useUserPreferenceStore.getState().lastOpenedNotes const lastUsedSnippets = useUserPreferenceStore.getState().lastUsedSnippets const theme = useUserPreferenceStore.getState().theme + const smartCaptureExcludedFields = useUserPreferenceStore.getState().smartCaptureExcludedFields const userID = useAuthStore.getState().userDetails.userID - + const userPreferences: UserPreferences = { version, lastOpenedNotes, lastUsedSnippets, - theme + smartCaptureExcludedFields, + theme, } try { From e4c36a12f3251e53fd908f0e3e41f01f3b02f2aa Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Thu, 10 Nov 2022 20:11:54 +0530 Subject: [PATCH 7/8] Add changeset --- .changeset/wet-dogs-shave.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wet-dogs-shave.md diff --git a/.changeset/wet-dogs-shave.md b/.changeset/wet-dogs-shave.md new file mode 100644 index 000000000..1e3fabd4b --- /dev/null +++ b/.changeset/wet-dogs-shave.md @@ -0,0 +1,6 @@ +--- +'mexit': patch +'mexit-webapp': patch +--- + +Templated Notes; User Preferences Sync with Cloud From aade277551c36845773d56afd4402c997de85ca3 Mon Sep 17 00:00:00 2001 From: Mukul Mehta Date: Thu, 10 Nov 2022 20:23:02 +0530 Subject: [PATCH 8/8] Change SidebarListItem defaultItems to array --- .../src/Components/Sidebar/SidebarList.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/webapp/src/Components/Sidebar/SidebarList.tsx b/apps/webapp/src/Components/Sidebar/SidebarList.tsx index ecf86041a..b9e03a8af 100644 --- a/apps/webapp/src/Components/Sidebar/SidebarList.tsx +++ b/apps/webapp/src/Components/Sidebar/SidebarList.tsx @@ -49,7 +49,7 @@ export interface SidebarListProps { selectedItemId?: string // If true, the list will be preceded by the default item - defaultItem?: SidebarListItem + defaultItems?: SidebarListItem[] // To render the context menu if the item is right-clicked ItemContextMenu?: (props: { item: SidebarListItem }) => JSX.Element @@ -66,7 +66,7 @@ const SidebarList = ({ selectedItemId, onClick, items, - defaultItem, + defaultItems, showSearch, searchPlaceholder, emptyMessage, @@ -174,16 +174,17 @@ const SidebarList = ({ - {defaultItem && ( - - onSelectItem(defaultItem.id)}> - - - {defaultItem.label} - - - - )} + {defaultItems && + defaultItems.map((defaultItem) => ( + + onSelectItem(defaultItem.id)}> + + + {defaultItem.label} + + + + ))} {showSearch && items.length > 0 && ( @@ -197,7 +198,7 @@ const SidebarList = ({ )} - + {listItems.map((item, index) => (