refactor: repo (#1070)

This commit is contained in:
Mo
2022-06-07 07:18:41 -05:00
committed by GitHub
parent 4c65784421
commit f4ef63693c
1102 changed files with 5786 additions and 3366 deletions

View File

@@ -0,0 +1,112 @@
import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import VisuallyHidden from '@reach/visually-hidden'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon'
import ChangeEditorMenu from './ChangeEditorMenu'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
type Props = {
application: WebApplication
viewControllerManager: ViewControllerManager
onClickPreprocessing?: () => Promise<void>
}
const ChangeEditorButton: FunctionComponent<Props> = ({
application,
viewControllerManager,
onClickPreprocessing,
}: Props) => {
const note = viewControllerManager.notesController.firstSelectedNote
const [isOpen, setIsOpen] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen)
const toggleChangeEditorMenu = async () => {
const rect = buttonRef.current?.getBoundingClientRect()
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
const newOpenState = !isOpen
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setIsOpen(newOpenState)
setTimeout(() => {
setIsVisible(newOpenState)
})
}
}
return (
<div ref={containerRef}>
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsOpen(false)
}
}}
onBlur={closeOnBlur}
ref={buttonRef}
className="sn-icon-button border-contrast"
>
<VisuallyHidden>Change note type</VisuallyHidden>
<Icon type="dashboard" className="block" />
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsOpen(false)
buttonRef.current?.focus()
}
}}
ref={panelRef}
style={{
...position,
maxHeight,
}}
className="sn-dropdown sn-dropdown--animated min-w-68 max-h-120 max-w-xs flex flex-col overflow-y-auto fixed"
onBlur={closeOnBlur}
>
{isOpen && (
<ChangeEditorMenu
closeOnBlur={closeOnBlur}
application={application}
isVisible={isVisible}
note={note}
closeMenu={() => {
setIsOpen(false)
}}
/>
)}
</DisclosurePanel>
</Disclosure>
</div>
)
}
export default observer(ChangeEditorButton)

View File

@@ -0,0 +1,220 @@
import Icon from '@/Components/Icon/Icon'
import Menu from '@/Components/Menu/Menu'
import MenuItem from '@/Components/Menu/MenuItem'
import { MenuItemType } from '@/Components/Menu/MenuItemType'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
import { WebApplication } from '@/Application/Application'
import {
ComponentArea,
ItemMutator,
NoteMutator,
PrefKey,
SNComponent,
SNNote,
TransactionalMutation,
} from '@standardnotes/snjs'
import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { createEditorMenuGroups } from './createEditorMenuGroups'
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
import {
transactionForAssociateComponentWithCurrentNote,
transactionForDisassociateComponentWithCurrentNote,
} from '../NoteView/TransactionFunctions'
import { reloadFont } from '../NoteView/FontFunctions'
type ChangeEditorMenuProps = {
application: WebApplication
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
closeMenu: () => void
isVisible: boolean
note: SNNote | undefined
}
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
application,
closeOnBlur,
closeMenu,
isVisible,
note,
}) => {
const [editors] = useState<SNComponent[]>(() =>
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
}),
)
const [groups, setGroups] = useState<EditorMenuGroup[]>([])
const [currentEditor, setCurrentEditor] = useState<SNComponent>()
useEffect(() => {
setGroups(createEditorMenuGroups(application, editors))
}, [application, editors])
useEffect(() => {
if (note) {
setCurrentEditor(application.componentManager.editorForNote(note))
}
}, [application, note])
const premiumModal = usePremiumModal()
const isSelectedEditor = useCallback(
(item: EditorMenuItem) => {
if (currentEditor) {
if (item?.component?.identifier === currentEditor.identifier) {
return true
}
} else if (item.name === PLAIN_EDITOR_NAME) {
return true
}
return false
},
[currentEditor],
)
const selectComponent = useCallback(
async (component: SNComponent | null, note: SNNote) => {
if (component) {
if (component.conflictOf) {
application.mutator
.changeAndSaveItem(component, (mutator) => {
mutator.conflictOf = undefined
})
.catch(console.error)
}
}
const transactions: TransactionalMutation[] = []
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
if (note.locked) {
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
return
}
if (!component) {
if (!note.prefersPlainEditor) {
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.prefersPlainEditor = true
},
})
}
const currentEditor = application.componentManager.editorForNote(note)
if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) {
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
}
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
} else if (component.area === ComponentArea.Editor) {
const currentEditor = application.componentManager.editorForNote(note)
if (currentEditor && component.uuid !== currentEditor.uuid) {
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
}
const prefersPlain = note.prefersPlainEditor
if (prefersPlain) {
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.prefersPlainEditor = false
},
})
}
transactions.push(transactionForAssociateComponentWithCurrentNote(component, note))
}
await application.mutator.runTransactionalMutations(transactions)
/** Dirtying can happen above */
application.sync.sync().catch(console.error)
setCurrentEditor(application.componentManager.editorForNote(note))
},
[application],
)
const selectEditor = useCallback(
async (itemToBeSelected: EditorMenuItem) => {
if (!itemToBeSelected.isEntitled) {
premiumModal.activate(itemToBeSelected.name)
return
}
const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component
if (areBothEditorsPlain) {
return
}
let shouldSelectEditor = true
if (itemToBeSelected.component) {
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
currentEditor,
itemToBeSelected.component,
)
if (changeRequiresAlert) {
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
}
}
if (shouldSelectEditor && note) {
selectComponent(itemToBeSelected.component ?? null, note).catch(console.error)
}
closeMenu()
},
[application.componentManager, closeMenu, currentEditor, note, premiumModal, selectComponent],
)
return (
<Menu className="pt-0.5 pb-1" a11yLabel="Change note type menu" isOpen={isVisible}>
{groups
.filter((group) => group.items && group.items.length)
.map((group, index) => {
const groupId = getGroupId(group)
return (
<Fragment key={groupId}>
<div className={`py-1 border-0 border-t-1px border-solid border-main ${index === 0 ? 'border-t-0' : ''}`}>
{group.items.map((item) => {
const onClickEditorItem = () => {
selectEditor(item).catch(console.error)
}
return (
<MenuItem
key={item.name}
type={MenuItemType.RadioButton}
onClick={onClickEditorItem}
className={
'sn-dropdown-item py-2 text-input focus:bg-info-backdrop focus:shadow-none flex-row-reverse'
}
onBlur={closeOnBlur}
checked={isSelectedEditor(item)}
>
<div className="flex flex-grow items-center justify-between">
<div className="flex items-center">
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
{item.name}
</div>
{!item.isEntitled && <Icon type="premium-feature" />}
</div>
</MenuItem>
)
})}
</div>
</Fragment>
)
})}
</Menu>
)
}
export default ChangeEditorMenu

View File

@@ -0,0 +1,127 @@
import { WebApplication } from '@/Application/Application'
import {
ContentType,
FeatureStatus,
SNComponent,
ComponentArea,
FeatureDescription,
GetFeatures,
NoteType,
} from '@standardnotes/snjs'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
type EditorGroup = NoteType | 'plain' | 'others'
const getEditorGroup = (featureDescription: FeatureDescription): EditorGroup => {
if (featureDescription.note_type) {
return featureDescription.note_type
} else if (featureDescription.file_type) {
switch (featureDescription.file_type) {
case 'txt':
return 'plain'
case 'html':
return NoteType.RichText
case 'md':
return NoteType.Markdown
default:
return 'others'
}
}
return 'others'
}
export const createEditorMenuGroups = (application: WebApplication, editors: SNComponent[]) => {
const editorItems: Record<EditorGroup, EditorMenuItem[]> = {
plain: [
{
name: PLAIN_EDITOR_NAME,
isEntitled: true,
},
],
'rich-text': [],
markdown: [],
task: [],
code: [],
spreadsheet: [],
authentication: [],
others: [],
}
GetFeatures()
.filter((feature) => feature.content_type === ContentType.Component && feature.area === ComponentArea.Editor)
.forEach((editorFeature) => {
const notInstalled = !editors.find((editor) => editor.identifier === editorFeature.identifier)
const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier)
if (notInstalled && !isExperimental) {
editorItems[getEditorGroup(editorFeature)].push({
name: editorFeature.name as string,
isEntitled: false,
})
}
})
editors.forEach((editor) => {
const editorItem: EditorMenuItem = {
name: editor.displayName,
component: editor,
isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
}
editorItems[getEditorGroup(editor.package_info)].push(editorItem)
})
const editorMenuGroups: EditorMenuGroup[] = [
{
icon: 'plain-text',
iconClassName: 'color-accessory-tint-1',
title: 'Plain text',
items: editorItems.plain,
},
{
icon: 'rich-text',
iconClassName: 'color-accessory-tint-1',
title: 'Rich text',
items: editorItems['rich-text'],
},
{
icon: 'markdown',
iconClassName: 'color-accessory-tint-2',
title: 'Markdown text',
items: editorItems.markdown,
},
{
icon: 'tasks',
iconClassName: 'color-accessory-tint-3',
title: 'Todo',
items: editorItems.task,
},
{
icon: 'code',
iconClassName: 'color-accessory-tint-4',
title: 'Code',
items: editorItems.code,
},
{
icon: 'spreadsheets',
iconClassName: 'color-accessory-tint-5',
title: 'Spreadsheet',
items: editorItems.spreadsheet,
},
{
icon: 'authenticator',
iconClassName: 'color-accessory-tint-6',
title: 'Authentication',
items: editorItems.authentication,
},
{
icon: 'editor',
iconClassName: 'color-neutral',
title: 'Others',
items: editorItems.others,
},
]
return editorMenuGroups
}