mirror of
https://github.com/standardnotes/web.git
synced 2024-10-27 16:31:40 +03:00
refactor: mobile modals (#2173)
This commit is contained in:
parent
6af95ddfeb
commit
42db3592b6
@ -1,3 +1,3 @@
|
||||
export const classNames = (...values: (string | boolean | undefined)[]): string => {
|
||||
export const classNames = (...values: any[]): string => {
|
||||
return values.map((value) => (typeof value === 'string' ? value : null)).join(' ')
|
||||
}
|
||||
|
@ -4,14 +4,10 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Dropdown from '../Dropdown/Dropdown'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import Modal from '../Shared/Modal'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
@ -87,10 +83,43 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
close()
|
||||
}, [capturedPhoto, close, fileName, filesController])
|
||||
|
||||
const retryPhoto = () => {
|
||||
setCapturedPhoto(undefined)
|
||||
setRecorder(new PhotoRecorder())
|
||||
}
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Take a photo</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<Modal
|
||||
title="Take a photo"
|
||||
close={close}
|
||||
actions={[
|
||||
{
|
||||
label: 'Capture',
|
||||
onClick: takePhoto,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !!capturedPhoto,
|
||||
},
|
||||
{
|
||||
label: 'Upload',
|
||||
onClick: savePhoto,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !capturedPhoto,
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: close,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Retry',
|
||||
onClick: retryPhoto,
|
||||
type: 'secondary',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
@ -133,7 +162,7 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
onChange={(value: string) => {
|
||||
void recorder.setDevice(value)
|
||||
}}
|
||||
className={{
|
||||
classNameOverride={{
|
||||
wrapper: 'mt-1',
|
||||
popover: 'z-modal',
|
||||
}}
|
||||
@ -141,40 +170,8 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedPhoto && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void takePhoto()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Take photo
|
||||
</Button>
|
||||
)}
|
||||
{capturedPhoto && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedPhoto(undefined)
|
||||
setRecorder(new PhotoRecorder())
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={savePhoto}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,13 +4,9 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import Modal from '../Shared/Modal'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
@ -77,10 +73,59 @@ const VideoCaptureModal = ({ filesController, close }: Props) => {
|
||||
return URL.createObjectURL(capturedVideo)
|
||||
}, [capturedVideo])
|
||||
|
||||
const stopRecording = async () => {
|
||||
const capturedVideo = await recorder.stop()
|
||||
setIsRecording(false)
|
||||
setCapturedVideo(capturedVideo)
|
||||
}
|
||||
|
||||
const retryRecording = () => {
|
||||
setCapturedVideo(undefined)
|
||||
setRecorder(new VideoRecorder(fileName))
|
||||
setIsRecorderReady(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Record a video</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<Modal
|
||||
title="Record a video"
|
||||
close={close}
|
||||
actions={[
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: close,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Record',
|
||||
onClick: startRecording,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !!capturedVideo || isRecording,
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
onClick: stopRecording,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !!capturedVideo || !isRecording,
|
||||
},
|
||||
{
|
||||
label: 'Retry',
|
||||
onClick: retryRecording,
|
||||
type: 'secondary',
|
||||
hidden: !capturedVideo,
|
||||
},
|
||||
{
|
||||
label: 'Upload',
|
||||
onClick: saveVideo,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !capturedVideo,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
@ -111,55 +156,8 @@ const VideoCaptureModal = ({ filesController, close }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedVideo && !isRecording && (
|
||||
<Button
|
||||
primary
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void startRecording()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Start recording
|
||||
</Button>
|
||||
)}
|
||||
{!capturedVideo && isRecording && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={async () => {
|
||||
const capturedVideo = await recorder.stop()
|
||||
setIsRecording(false)
|
||||
setCapturedVideo(capturedVideo)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Stop recording
|
||||
</Button>
|
||||
)}
|
||||
{capturedVideo && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedVideo(undefined)
|
||||
setRecorder(new VideoRecorder(fileName))
|
||||
setIsRecorderReady(false)
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={saveVideo}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,10 @@ import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { ChallengeModalValues } from './ChallengeModalValues'
|
||||
import { InputValue } from './InputValue'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { isIOS, isMobileScreen } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import MobileModalAction from '../Shared/MobileModalAction'
|
||||
import { useModalAnimation } from '../Shared/useModalAnimation'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@ -209,98 +211,121 @@ const ChallengeModal: FunctionComponent<Props> = ({
|
||||
}
|
||||
}, [application, cancelChallenge, challenge.cancelable])
|
||||
|
||||
if (!challenge.prompts) {
|
||||
const [isMounted, setElement] = useModalAnimation(!!challenge.prompts.length)
|
||||
|
||||
if (!isMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isFullScreenBlocker = challenge.reason === ChallengeReason.ApplicationUnlock
|
||||
const isMobileOverlay = isMobileScreen() && !isFullScreenBlocker
|
||||
|
||||
const contentClasses = classNames(
|
||||
'challenge-modal relative flex flex-col items-center rounded border-solid border-border p-8 md:border',
|
||||
!isMobileScreen() && 'shadow-overlay-light',
|
||||
isMobileOverlay && 'border border-solid border-border shadow-overlay-light',
|
||||
isFullScreenBlocker && isMobileScreen() ? 'bg-passive-5' : 'bg-default',
|
||||
)
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={`sn-component ${isFullScreenBlocker ? 'bg-passive-5' : ''}`}
|
||||
className={`sn-component p-0 ${isFullScreenBlocker ? 'bg-passive-5' : ''}`}
|
||||
onDismiss={cancelChallenge}
|
||||
dangerouslyBypassFocusLock={bypassModalFocusLock}
|
||||
key={challenge.id}
|
||||
ref={setElement}
|
||||
>
|
||||
<DialogContent aria-label="Challenge modal" className={contentClasses}>
|
||||
<DialogContent
|
||||
aria-label="Challenge modal"
|
||||
className={classNames(
|
||||
'challenge-modal relative m-0 flex h-full w-full flex-col items-center rounded border-solid border-border bg-default p-0 md:h-auto md:w-auto md:border',
|
||||
!isMobileScreen() && 'shadow-overlay-light',
|
||||
isMobileOverlay && 'shadow-overlay-light border border-solid border-border',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'w-full border-b border-solid border-border py-1.5 px-1.5 md:hidden',
|
||||
isIOS() && 'pt-safe-top',
|
||||
)}
|
||||
>
|
||||
<div className="grid w-full grid-cols-[0.35fr_1fr_0.35fr] gap-2">
|
||||
{challenge.cancelable ? (
|
||||
<MobileModalAction slot="left" type="cancel" action={cancelChallenge}>
|
||||
Cancel
|
||||
</MobileModalAction>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex items-center justify-center text-base font-semibold text-text">Authenticate</div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
{challenge.cancelable && (
|
||||
<button
|
||||
onClick={cancelChallenge}
|
||||
aria-label="Close modal"
|
||||
className="absolute top-4 right-4 flex cursor-pointer border-0 bg-transparent p-1"
|
||||
className="absolute top-4 right-4 hidden cursor-pointer border-0 bg-transparent p-1 md:flex"
|
||||
>
|
||||
<Icon type="close" className="text-neutral" />
|
||||
</button>
|
||||
)}
|
||||
<ProtectedIllustration className="mb-4 h-30 w-30" />
|
||||
<div className="mb-3 max-w-76 text-center text-lg font-bold">{challenge.heading}</div>
|
||||
{challenge.subheading && (
|
||||
<div className="break-word mb-4 max-w-76 text-center text-sm">{challenge.subheading}</div>
|
||||
)}
|
||||
<form
|
||||
className="flex min-w-76 flex-col items-center"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
}}
|
||||
ref={promptsContainerRef}
|
||||
>
|
||||
{challenge.prompts.map((prompt, index) => (
|
||||
<ChallengeModalPrompt
|
||||
application={application}
|
||||
key={prompt.id}
|
||||
prompt={prompt}
|
||||
values={values}
|
||||
index={index}
|
||||
onValueChange={onValueChange}
|
||||
isInvalid={values[prompt.id].invalid}
|
||||
/>
|
||||
))}
|
||||
</form>
|
||||
<Button primary disabled={isProcessing} className="mt-1 mb-3.5 min-w-76" onClick={submit}>
|
||||
{isProcessing ? 'Generating Keys...' : 'Submit'}
|
||||
</Button>
|
||||
{shouldShowForgotPasscode && (
|
||||
<Button
|
||||
className="flex min-w-76 items-center justify-center"
|
||||
onClick={() => {
|
||||
setBypassModalFocusLock(true)
|
||||
application.alertService
|
||||
.confirm(
|
||||
'If you forgot your local passcode, your only option is to clear your local data from this device and sign back in to your account.',
|
||||
'Forgot passcode?',
|
||||
'Delete local data',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
.then((shouldDeleteLocalData) => {
|
||||
if (shouldDeleteLocalData) {
|
||||
application.user.signOut().catch(console.error)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
setBypassModalFocusLock(false)
|
||||
})
|
||||
<div className="flex min-h-0 w-full flex-grow flex-col items-center overflow-auto p-8">
|
||||
<ProtectedIllustration className="mb-4 h-30 w-30 flex-shrink-0" />
|
||||
<div className="mb-3 max-w-76 text-center text-lg font-bold">{challenge.heading}</div>
|
||||
{challenge.subheading && (
|
||||
<div className="break-word mb-4 max-w-76 text-center text-sm">{challenge.subheading}</div>
|
||||
)}
|
||||
<form
|
||||
className="flex w-full max-w-76 flex-col items-center md:min-w-76"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
}}
|
||||
ref={promptsContainerRef}
|
||||
>
|
||||
<Icon type="help" className="mr-2 text-neutral" />
|
||||
Forgot passcode?
|
||||
{challenge.prompts.map((prompt, index) => (
|
||||
<ChallengeModalPrompt
|
||||
application={application}
|
||||
key={prompt.id}
|
||||
prompt={prompt}
|
||||
values={values}
|
||||
index={index}
|
||||
onValueChange={onValueChange}
|
||||
isInvalid={values[prompt.id].invalid}
|
||||
/>
|
||||
))}
|
||||
</form>
|
||||
<Button primary disabled={isProcessing} className="mt-1 mb-3.5 min-w-76" onClick={submit}>
|
||||
{isProcessing ? 'Generating Keys...' : 'Submit'}
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowWorkspaceSwitcher && (
|
||||
<LockscreenWorkspaceSwitcher
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
)}
|
||||
{shouldShowForgotPasscode && (
|
||||
<Button
|
||||
className="flex min-w-76 items-center justify-center"
|
||||
onClick={() => {
|
||||
setBypassModalFocusLock(true)
|
||||
application.alertService
|
||||
.confirm(
|
||||
'If you forgot your local passcode, your only option is to clear your local data from this device and sign back in to your account.',
|
||||
'Forgot passcode?',
|
||||
'Delete local data',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
.then((shouldDeleteLocalData) => {
|
||||
if (shouldDeleteLocalData) {
|
||||
application.user.signOut().catch(console.error)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
setBypassModalFocusLock(false)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Icon type="help" className="mr-2 text-neutral" />
|
||||
Forgot passcode?
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowWorkspaceSwitcher && (
|
||||
<LockscreenWorkspaceSwitcher
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
)
|
||||
|
@ -99,14 +99,16 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
|
||||
return (
|
||||
<label
|
||||
key={option.label}
|
||||
className={`cursor-pointer rounded px-2 py-1.5 focus-within:ring-2 focus-within:ring-info ${
|
||||
className={`relative flex cursor-pointer items-center justify-center rounded px-2 py-1.5 text-center focus-within:ring-2 focus-within:ring-info ${
|
||||
selected ? 'bg-default font-semibold text-foreground' : 'text-passive-0 hover:bg-passive-3'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`session-duration-${prompt.id}`}
|
||||
className={'m-0 appearance-none focus:shadow-none focus:outline-none'}
|
||||
className={
|
||||
'absolute top-0 left-0 m-0 h-px w-px appearance-none focus:shadow-none focus:outline-none'
|
||||
}
|
||||
style={{
|
||||
marginRight: 0,
|
||||
}}
|
||||
|
@ -13,6 +13,7 @@ import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/Premium
|
||||
import { SuperNoteImporter } from '../NoteView/SuperEditor/SuperNoteImporter'
|
||||
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||
import { Pill } from '../Preferences/PreferencesComponents/Content'
|
||||
import ModalOverlay from '../Shared/ModalOverlay'
|
||||
|
||||
type ChangeEditorMenuProps = {
|
||||
application: WebApplication
|
||||
@ -174,6 +175,8 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
closeMenu()
|
||||
}, [note, pendingSuperItem, selectNonComponent, closeMenu])
|
||||
|
||||
const closeSuperNoteImporter = () => setShowSuperImporter(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu className="pt-0.5 pb-1" a11yLabel="Change note type menu" isOpen={isVisible}>
|
||||
@ -220,14 +223,16 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
{showSuperImporter && note && (
|
||||
<SuperNoteImporter
|
||||
note={note}
|
||||
application={application}
|
||||
onConvertComplete={handleSuperNoteConversionCompletion}
|
||||
closeDialog={() => setShowSuperImporter(false)}
|
||||
/>
|
||||
)}
|
||||
<ModalOverlay isOpen={showSuperImporter} onDismiss={closeSuperNoteImporter}>
|
||||
{note && (
|
||||
<SuperNoteImporter
|
||||
note={note}
|
||||
application={application}
|
||||
onConvertComplete={handleSuperNoteConversionCompletion}
|
||||
closeDialog={closeSuperNoteImporter}
|
||||
/>
|
||||
)}
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/u
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import { PaneController } from '@/Controllers/PaneController/PaneController'
|
||||
import EmptyFilesView from './EmptyFilesView'
|
||||
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
||||
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
@ -86,7 +87,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { toggleAppPane, panes } = useResponsiveAppPane()
|
||||
const { setPaneLayout, panes } = useResponsiveAppPane()
|
||||
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
||||
const { selected: selectedTag, selectedAsTag } = navigationController
|
||||
const {
|
||||
@ -185,9 +186,9 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
void filesController.selectAndUploadNewFiles()
|
||||
} else {
|
||||
await createNewNote()
|
||||
toggleAppPane(AppPaneId.Editor)
|
||||
setPaneLayout(PaneLayout.Editing)
|
||||
}
|
||||
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
|
||||
}, [isFilesSmartView, application, filesController, createNewNote, setPaneLayout])
|
||||
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
const shouldUseTableView = (isFilesSmartView || isTableViewEnabled) && !isMobileScreen
|
||||
|
@ -4,6 +4,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import ModalOverlay from '@/Components/Shared/ModalOverlay'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { PhotoRecorder } from '@/Controllers/Moments/PhotoRecorder'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
@ -40,6 +41,10 @@ const AddItemMenuButton = ({
|
||||
|
||||
const canShowMenu = isInFilesSmartView && deviceHasCamera
|
||||
|
||||
const closeCaptureModal = () => {
|
||||
setCaptureType(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@ -101,22 +106,12 @@ const AddItemMenuButton = ({
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Popover>
|
||||
{captureType === 'photo' && (
|
||||
<PhotoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{captureType === 'video' && (
|
||||
<VideoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ModalOverlay isOpen={captureType === 'photo'} onDismiss={closeCaptureModal}>
|
||||
<PhotoCaptureModal filesController={filesController} close={closeCaptureModal} />
|
||||
</ModalOverlay>
|
||||
<ModalOverlay isOpen={captureType === 'video'} onDismiss={closeCaptureModal}>
|
||||
<VideoCaptureModal filesController={filesController} close={closeCaptureModal} />
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ type DropdownProps = {
|
||||
value: string
|
||||
onChange: (value: string, item: DropdownItem) => void
|
||||
disabled?: boolean
|
||||
className?: {
|
||||
classNameOverride?: {
|
||||
wrapper?: string
|
||||
button?: string
|
||||
popover?: string
|
||||
@ -56,7 +56,7 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
className,
|
||||
classNameOverride = {},
|
||||
fullWidth,
|
||||
portal = true,
|
||||
}) => {
|
||||
@ -68,16 +68,12 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
onChange(value, selectedItem)
|
||||
}
|
||||
|
||||
const wrapperClassName = className?.wrapper ?? ''
|
||||
const buttonClassName = className?.button ?? ''
|
||||
const popoverClassName = className?.popover ?? ''
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<div className={classNameOverride.wrapper}>
|
||||
<VisuallyHidden id={labelId}>{label}</VisuallyHidden>
|
||||
<ListboxInput value={value} onChange={handleChange} aria-labelledby={labelId} disabled={disabled}>
|
||||
<StyledListboxButton
|
||||
className={classNames('w-full', !fullWidth && 'md:w-fit', buttonClassName)}
|
||||
className={classNames('w-full', !fullWidth && 'md:w-fit', classNameOverride.button)}
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value)
|
||||
const icon = current ? current?.icon : null
|
||||
@ -91,7 +87,10 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ListboxPopover portal={portal} className={classNames('sn-dropdown sn-dropdown-popover', popoverClassName)}>
|
||||
<ListboxPopover
|
||||
portal={portal}
|
||||
className={classNames('sn-dropdown sn-dropdown-popover', classNameOverride.popover)}
|
||||
>
|
||||
<div className="sn-component">
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
|
@ -1,6 +1,15 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
||||
import { FunctionComponent, KeyboardEventHandler, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { getFileIconComponent } from './getFileIconComponent'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import FilePreviewInfoPanel from './FilePreviewInfoPanel'
|
||||
@ -17,232 +26,248 @@ import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContaine
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||
import { useModalAnimation } from '../Shared/useModalAnimation'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
|
||||
const FilePreviewModal: FunctionComponent<Props> = observer(({ application, viewControllerManager }) => {
|
||||
const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController
|
||||
const FilePreviewModal = observer(
|
||||
forwardRef(({ application, viewControllerManager }: Props, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController
|
||||
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const renameInputRef = useRef<HTMLInputElement>(null)
|
||||
const [showLinkedBubblesContainer, setShowLinkedBubblesContainer] = useState(false)
|
||||
const [showOptionsMenu, setShowOptionsMenu] = useState(false)
|
||||
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const renameInputRef = useRef<HTMLInputElement>(null)
|
||||
const [showLinkedBubblesContainer, setShowLinkedBubblesContainer] = useState(false)
|
||||
const [showOptionsMenu, setShowOptionsMenu] = useState(false)
|
||||
const [showFileInfoPanel, setShowFileInfoPanel] = useState(false)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const keyDownHandler: KeyboardEventHandler = useCallback(
|
||||
(event) => {
|
||||
const KeysToHandle: string[] = [KeyboardKey.Left, KeyboardKey.Right, KeyboardKey.Escape]
|
||||
|
||||
if (!KeysToHandle.includes(event.key) || event.target === renameInputRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const currentFileIndex = otherFiles.findIndex((fileFromArray) => fileFromArray.uuid === currentFile.uuid)
|
||||
|
||||
switch (event.key) {
|
||||
case KeyboardKey.Left: {
|
||||
const previousFileIndex = currentFileIndex - 1 >= 0 ? currentFileIndex - 1 : otherFiles.length - 1
|
||||
const previousFile = otherFiles[previousFileIndex]
|
||||
if (previousFile) {
|
||||
setCurrentFile(previousFile)
|
||||
}
|
||||
break
|
||||
const keyDownHandler: KeyboardEventHandler = useCallback(
|
||||
(event) => {
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
case KeyboardKey.Right: {
|
||||
const nextFileIndex = currentFileIndex + 1 < otherFiles.length ? currentFileIndex + 1 : 0
|
||||
const nextFile = otherFiles[nextFileIndex]
|
||||
if (nextFile) {
|
||||
setCurrentFile(nextFile)
|
||||
}
|
||||
break
|
||||
|
||||
const KeysToHandle: string[] = [KeyboardKey.Left, KeyboardKey.Right, KeyboardKey.Escape]
|
||||
|
||||
if (!KeysToHandle.includes(event.key) || event.target === renameInputRef.current) {
|
||||
return
|
||||
}
|
||||
case KeyboardKey.Escape:
|
||||
dismiss()
|
||||
break
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
const currentFileIndex = otherFiles.findIndex((fileFromArray) => fileFromArray.uuid === currentFile.uuid)
|
||||
|
||||
switch (event.key) {
|
||||
case KeyboardKey.Left: {
|
||||
const previousFileIndex = currentFileIndex - 1 >= 0 ? currentFileIndex - 1 : otherFiles.length - 1
|
||||
const previousFile = otherFiles[previousFileIndex]
|
||||
if (previousFile) {
|
||||
setCurrentFile(previousFile)
|
||||
}
|
||||
break
|
||||
}
|
||||
case KeyboardKey.Right: {
|
||||
const nextFileIndex = currentFileIndex + 1 < otherFiles.length ? currentFileIndex + 1 : 0
|
||||
const nextFile = otherFiles[nextFileIndex]
|
||||
if (nextFile) {
|
||||
setCurrentFile(nextFile)
|
||||
}
|
||||
break
|
||||
}
|
||||
case KeyboardKey.Escape:
|
||||
dismiss()
|
||||
break
|
||||
}
|
||||
},
|
||||
[currentFile, dismiss, otherFiles, setCurrentFile],
|
||||
)
|
||||
|
||||
const IconComponent = useMemo(() => {
|
||||
return currentFile
|
||||
? getFileIconComponent(getIconForFileType(currentFile.mimeType), 'w-6 h-6 flex-shrink-0')
|
||||
: null
|
||||
}, [currentFile])
|
||||
|
||||
const focusInputOnMount = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
},
|
||||
[currentFile.uuid, dismiss, otherFiles, setCurrentFile],
|
||||
)
|
||||
}, [])
|
||||
|
||||
const IconComponent = useMemo(
|
||||
() => getFileIconComponent(getIconForFileType(currentFile.mimeType), 'w-6 h-6 flex-shrink-0'),
|
||||
[currentFile.mimeType],
|
||||
)
|
||||
|
||||
const focusInputOnMount = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleRename = useCallback(async () => {
|
||||
if (renameInputRef.current) {
|
||||
const newName = renameInputRef.current.value
|
||||
if (newName && newName !== currentFile.name) {
|
||||
await application.items.renameFile(currentFile, newName)
|
||||
const handleRename = useCallback(async () => {
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
if (renameInputRef.current) {
|
||||
const newName = renameInputRef.current.value
|
||||
if (newName && newName !== currentFile.name) {
|
||||
await application.items.renameFile(currentFile, newName)
|
||||
setIsRenaming(false)
|
||||
setCurrentFile(application.items.findSureItem(currentFile.uuid))
|
||||
return
|
||||
}
|
||||
setIsRenaming(false)
|
||||
setCurrentFile(application.items.findSureItem(currentFile.uuid))
|
||||
return
|
||||
}
|
||||
setIsRenaming(false)
|
||||
}
|
||||
}, [application.items, currentFile, setCurrentFile])
|
||||
}, [application.items, currentFile, setCurrentFile])
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className="sn-component"
|
||||
aria-label="File preview modal"
|
||||
onDismiss={dismiss}
|
||||
initialFocusRef={closeButtonRef}
|
||||
dangerouslyBypassScrollLock
|
||||
>
|
||||
<DialogContent
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className="sn-component p-0"
|
||||
aria-label="File preview modal"
|
||||
className="flex min-h-[90%] min-w-[90%] flex-col rounded bg-[color:var(--modal-background-color)] p-0 shadow-main "
|
||||
onDismiss={dismiss}
|
||||
initialFocusRef={closeButtonRef}
|
||||
dangerouslyBypassScrollLock
|
||||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className="flex h-full w-full flex-col focus:shadow-none focus:outline-none"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
onKeyDown={keyDownHandler}
|
||||
<DialogContent
|
||||
aria-label="File preview modal"
|
||||
className="m-0 flex h-full min-h-[90%] w-full min-w-[90%] flex-col rounded bg-[color:var(--modal-background-color)] p-0 shadow-main "
|
||||
>
|
||||
<div className="min-h-6 flex flex-shrink-0 flex-wrap items-center justify-between gap-2 border-0 border-b border-solid border-border px-4 py-3 focus:shadow-none">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6">{IconComponent}</div>
|
||||
{isRenaming ? (
|
||||
<DecoratedInput
|
||||
defaultValue={currentFile.name}
|
||||
className={{ container: 'ml-3', input: 'p-1', right: 'items-stretch !p-0' }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
void handleRename()
|
||||
}
|
||||
}}
|
||||
right={[
|
||||
<button
|
||||
className="flex h-full items-center justify-center border-l border-border px-2 py-1.5 text-neutral hover:bg-passive-4"
|
||||
title="Submit"
|
||||
onClick={handleRename}
|
||||
>
|
||||
<Icon type="check" size="small" />
|
||||
</button>,
|
||||
]}
|
||||
ref={mergeRefs([renameInputRef, focusInputOnMount])}
|
||||
/>
|
||||
) : (
|
||||
<span className="ml-3 font-medium">{currentFile.name}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<StyledTooltip label="Rename file" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
|
||||
aria-label="Rename file"
|
||||
>
|
||||
<Icon type="pencil-filled" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show linked items" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowLinkedBubblesContainer((show) => !show)}
|
||||
aria-label="Show linked items"
|
||||
>
|
||||
<Icon type="link" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show file options" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowOptionsMenu((show) => !show)}
|
||||
ref={menuButtonRef}
|
||||
aria-label="Show file options"
|
||||
>
|
||||
<Icon type="more" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<Popover
|
||||
title="File options"
|
||||
open={showOptionsMenu}
|
||||
anchorElement={menuButtonRef.current}
|
||||
togglePopover={() => {
|
||||
setShowOptionsMenu(false)
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="py-2"
|
||||
overrideZIndex="z-modal"
|
||||
>
|
||||
<Menu a11yLabel="File context menu" isOpen={showOptionsMenu}>
|
||||
<FileMenuOptions
|
||||
filesController={viewControllerManager.filesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
selectedFiles={[currentFile]}
|
||||
closeMenu={() => {
|
||||
setShowOptionsMenu(false)
|
||||
<div
|
||||
className="flex h-full w-full flex-col focus:shadow-none focus:outline-none"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
onKeyDown={keyDownHandler}
|
||||
>
|
||||
<div className="min-h-6 flex flex-shrink-0 flex-wrap items-center justify-between gap-2 border-0 border-b border-solid border-border px-4 py-3 focus:shadow-none">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6">{IconComponent}</div>
|
||||
{isRenaming ? (
|
||||
<DecoratedInput
|
||||
defaultValue={currentFile.name}
|
||||
className={{ container: 'ml-3', input: 'p-1', right: 'items-stretch !p-0' }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
void handleRename()
|
||||
}
|
||||
}}
|
||||
shouldShowRenameOption={false}
|
||||
shouldShowAttachOption={false}
|
||||
right={[
|
||||
<button
|
||||
className="flex h-full items-center justify-center border-l border-border px-2 py-1.5 text-neutral hover:bg-passive-4"
|
||||
title="Submit"
|
||||
onClick={handleRename}
|
||||
>
|
||||
<Icon type="check" size="small" />
|
||||
</button>,
|
||||
]}
|
||||
ref={mergeRefs([renameInputRef, focusInputOnMount])}
|
||||
/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
<StyledTooltip label="Show file info" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowFileInfoPanel((show) => !show)}
|
||||
aria-label="Show file info"
|
||||
) : (
|
||||
<span className="ml-3 font-medium">{currentFile.name}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<StyledTooltip label="Rename file" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
|
||||
aria-label="Rename file"
|
||||
>
|
||||
<Icon type="pencil-filled" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show linked items" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowLinkedBubblesContainer((show) => !show)}
|
||||
aria-label="Show linked items"
|
||||
>
|
||||
<Icon type="link" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show file options" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowOptionsMenu((show) => !show)}
|
||||
ref={menuButtonRef}
|
||||
aria-label="Show file options"
|
||||
>
|
||||
<Icon type="more" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<Popover
|
||||
title="File options"
|
||||
open={showOptionsMenu}
|
||||
anchorElement={menuButtonRef.current}
|
||||
togglePopover={() => {
|
||||
setShowOptionsMenu(false)
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="py-2"
|
||||
overrideZIndex="z-modal"
|
||||
>
|
||||
<Icon type="info" className="text-neutral" />
|
||||
<Menu a11yLabel="File context menu" isOpen={showOptionsMenu}>
|
||||
<FileMenuOptions
|
||||
filesController={viewControllerManager.filesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
selectedFiles={[currentFile]}
|
||||
closeMenu={() => {
|
||||
setShowOptionsMenu(false)
|
||||
}}
|
||||
shouldShowRenameOption={false}
|
||||
shouldShowAttachOption={false}
|
||||
/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
<StyledTooltip label="Show file info" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowFileInfoPanel((show) => !show)}
|
||||
aria-label="Show file info"
|
||||
>
|
||||
<Icon type="info" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
onClick={dismiss}
|
||||
aria-label="Close modal"
|
||||
className="flex cursor-pointer rounded border-0 bg-transparent p-1 hover:bg-contrast"
|
||||
>
|
||||
<Icon type="close" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
onClick={dismiss}
|
||||
aria-label="Close modal"
|
||||
className="flex cursor-pointer rounded border-0 bg-transparent p-1 hover:bg-contrast"
|
||||
>
|
||||
<Icon type="close" className="text-neutral" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showLinkedBubblesContainer && (
|
||||
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border py-1.5 px-3.5">
|
||||
<LinkedItemBubblesContainer
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
item={currentFile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-grow">
|
||||
<div className="relative flex max-w-full flex-grow items-center justify-center">
|
||||
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
|
||||
</div>
|
||||
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
|
||||
</div>
|
||||
</div>
|
||||
{showLinkedBubblesContainer && (
|
||||
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border py-1.5 px-3.5">
|
||||
<LinkedItemBubblesContainer
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
item={currentFile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-grow">
|
||||
<div className="relative flex max-w-full flex-grow items-center justify-center">
|
||||
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
|
||||
</div>
|
||||
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
)
|
||||
})
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
FilePreviewModal.displayName = 'FilePreviewModal'
|
||||
|
||||
const FilePreviewModalWrapper: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
|
||||
return viewControllerManager.filePreviewModalController.isOpen ? (
|
||||
<FilePreviewModal application={application} viewControllerManager={viewControllerManager} />
|
||||
) : null
|
||||
const [isMounted, setElement] = useModalAnimation(viewControllerManager.filePreviewModalController.isOpen)
|
||||
|
||||
if (!isMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <FilePreviewModal application={application} viewControllerManager={viewControllerManager} ref={setElement} />
|
||||
}
|
||||
|
||||
export default observer(FilePreviewModalWrapper)
|
||||
|
@ -2,17 +2,14 @@ import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { ContentType, DecryptedTransferPayload, pluralize, SNTag, TagContent, UuidGenerator } from '@standardnotes/snjs'
|
||||
import { Importer } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useReducer, useState } from 'react'
|
||||
import { useCallback, useMemo, useReducer, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Button from '../Button/Button'
|
||||
import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import { ImportModalFileItem } from './ImportModalFileItem'
|
||||
import ImportModalInitialPage from './InitialPage'
|
||||
import { ImportModalAction, ImportModalFile, ImportModalState } from './Types'
|
||||
import Modal, { ModalAction } from '../Shared/Modal'
|
||||
import ModalOverlay from '../Shared/ModalOverlay'
|
||||
|
||||
const reducer = (state: ImportModalState, action: ImportModalAction): ImportModalState => {
|
||||
switch (action.type) {
|
||||
@ -190,36 +187,44 @@ const ImportModal = ({ viewControllerManager }: { viewControllerManager: ViewCon
|
||||
})
|
||||
}, [state.importTag, viewControllerManager.isImportModalVisible, viewControllerManager.navigationController])
|
||||
|
||||
if (!viewControllerManager.isImportModalVisible.get()) {
|
||||
return null
|
||||
}
|
||||
const isReadyToImport = files.length > 0 && files.every((file) => file.status === 'ready')
|
||||
const importSuccessOrError =
|
||||
files.length > 0 && files.every((file) => file.status === 'success' || file.status === 'error')
|
||||
|
||||
const modalActions: ModalAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Import',
|
||||
type: 'primary',
|
||||
onClick: parseAndImport,
|
||||
hidden: !isReadyToImport,
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
{
|
||||
label: importSuccessOrError ? 'Close' : 'Cancel',
|
||||
type: 'cancel',
|
||||
onClick: closeDialog,
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
],
|
||||
[closeDialog, importSuccessOrError, isReadyToImport, parseAndImport],
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>Import</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
{!files.length && <ImportModalInitialPage dispatch={dispatch} />}
|
||||
{files.length > 0 && (
|
||||
<div className="divide-y divide-border">
|
||||
{files.map((file) => (
|
||||
<ImportModalFileItem file={file} key={file.id} dispatch={dispatch} importer={importer} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{files.length > 0 && files.every((file) => file.status === 'ready') && (
|
||||
<Button primary onClick={parseAndImport}>
|
||||
Import
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={closeDialog}>
|
||||
{files.length > 0 && files.every((file) => file.status === 'success' || file.status === 'error')
|
||||
? 'Close'
|
||||
: 'Cancel'}
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
<ModalOverlay isOpen={viewControllerManager.isImportModalVisible.get()} onDismiss={closeDialog}>
|
||||
<Modal title="Import" close={closeDialog} actions={modalActions}>
|
||||
<div className="px-4 py-4">
|
||||
{!files.length && <ImportModalInitialPage dispatch={dispatch} />}
|
||||
{files.length > 0 && (
|
||||
<div className="divide-y divide-border">
|
||||
{files.map((file) => (
|
||||
<ImportModalFileItem file={file} key={file.id} dispatch={dispatch} importer={importer} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ import { getPlaintextFontSize } from '@/Utils/getPlaintextFontSize'
|
||||
import ReadonlyPlugin from './Plugins/ReadonlyPlugin/ReadonlyPlugin'
|
||||
import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context'
|
||||
import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
|
||||
import ModalOverlay from '@/Components/Shared/ModalOverlay'
|
||||
|
||||
const NotePreviewCharLimit = 160
|
||||
|
||||
@ -205,7 +206,9 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
||||
</BlocksEditorComposer>
|
||||
</FilesControllerProvider>
|
||||
</LinkingControllerProvider>
|
||||
{showMarkdownPreview && <SuperNoteMarkdownPreview note={note.current} closeDialog={closeMarkdownPreview} />}
|
||||
<ModalOverlay isOpen={showMarkdownPreview} onDismiss={closeMarkdownPreview}>
|
||||
<SuperNoteMarkdownPreview note={note.current} closeDialog={closeMarkdownPreview} />
|
||||
</ModalOverlay>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NoteType, SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin'
|
||||
import { NoteViewController } from '../Controller/NoteViewController'
|
||||
import { spaceSeparatedStrings } from '@standardnotes/utils'
|
||||
import Modal, { ModalAction } from '@/Components/Shared/Modal'
|
||||
|
||||
const NotePreviewCharLimit = 160
|
||||
|
||||
@ -82,51 +78,55 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
|
||||
onConvertComplete()
|
||||
}, [closeDialog, application, note, onConvertComplete, performConvert])
|
||||
|
||||
const modalActions: ModalAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: closeDialog,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Convert',
|
||||
onClick: confirmConvert,
|
||||
mobileSlot: 'right',
|
||||
type: 'primary',
|
||||
},
|
||||
{
|
||||
label: 'Convert As-Is',
|
||||
onClick: convertAsIs,
|
||||
type: 'secondary',
|
||||
},
|
||||
],
|
||||
[closeDialog, confirmConvert, convertAsIs],
|
||||
)
|
||||
|
||||
if (isSeamlessConvert) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>
|
||||
Convert to Super note
|
||||
<p className="text-sm font-normal text-neutral">
|
||||
The following is a preview of how your note will look when converted to Super. Super notes use a custom format
|
||||
under the hood. Converting your note will transition it from plaintext to the custom Super format.
|
||||
</p>
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="relative w-full">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={undefined}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
onChange={handleChange}
|
||||
ignoreFirstChange={false}
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
previewLength={NotePreviewCharLimit}
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<ImportPlugin text={note.text} format={format} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<div className="flex w-full justify-between">
|
||||
<div>
|
||||
<Button onClick={convertAsIs}>Convert As-Is</Button>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button onClick={closeDialog}>Cancel</Button>
|
||||
<div className="min-w-3" />
|
||||
<Button primary onClick={confirmConvert}>
|
||||
Convert to Super
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
<Modal title="Convert to Super note" close={closeDialog} actions={modalActions}>
|
||||
<div className="border-b border-border px-4 py-4 text-sm font-normal text-neutral md:py-3">
|
||||
The following is a preview of how your note will look when converted to Super. Super notes use a custom format
|
||||
under the hood. Converting your note will transition it from plaintext to the custom Super format.
|
||||
</div>
|
||||
<div className="relative w-full px-4 py-4">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={undefined}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
onChange={handleChange}
|
||||
ignoreFirstChange={false}
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
previewLength={NotePreviewCharLimit}
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<ImportPlugin text={note.text} format={format} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import MarkdownPreviewPlugin from './Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin'
|
||||
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
||||
import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode'
|
||||
import { copyTextToClipboard } from '../../../Utils/copyTextToClipboard'
|
||||
import Modal, { ModalAction } from '@/Components/Shared/Modal'
|
||||
|
||||
type Props = {
|
||||
note: SNNote
|
||||
@ -33,33 +29,33 @@ export const SuperNoteMarkdownPreview: FunctionComponent<Props> = ({ note, close
|
||||
setMarkdown(markdown)
|
||||
}, [])
|
||||
|
||||
const modalActions: ModalAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: didCopy ? 'Copied' : 'Copy',
|
||||
type: 'primary',
|
||||
onClick: copy,
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
],
|
||||
[copy, didCopy],
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>Markdown Preview</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="relative w-full">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={note.text} nodes={[FileNode, BubbleNode]}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<MarkdownPreviewPlugin onMarkdown={onMarkdown} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<div className="flex">
|
||||
<Button onClick={closeDialog}>Close</Button>
|
||||
<div className="min-w-3" />
|
||||
<Button primary onClick={copy}>
|
||||
{didCopy ? 'Copied' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
<Modal title="Markdown Preview" close={closeDialog} actions={modalActions}>
|
||||
<div className="relative w-full px-4 py-4">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={note.text} nodes={[FileNode, BubbleNode]}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<MarkdownPreviewPlugin onMarkdown={onMarkdown} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { createRef } from 'react'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import Modal from '../Shared/Modal'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import Spinner from '../Spinner/Spinner'
|
||||
|
||||
interface Props {
|
||||
application: WebApplication
|
||||
@ -25,6 +23,8 @@ type State = {
|
||||
}
|
||||
|
||||
const DEFAULT_CONTINUE_TITLE = 'Continue'
|
||||
const GENERATING_CONTINUE_TITLE = 'Generating Keys...'
|
||||
const FINISH_CONTINUE_TITLE = 'Finish'
|
||||
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
@ -89,7 +89,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
this.setState({
|
||||
isContinuing: true,
|
||||
showSpinner: true,
|
||||
continueTitle: 'Generating Keys...',
|
||||
continueTitle: GENERATING_CONTINUE_TITLE,
|
||||
})
|
||||
|
||||
const valid = await this.validateCurrentPassword()
|
||||
@ -107,7 +107,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
this.setState({
|
||||
isContinuing: false,
|
||||
showSpinner: false,
|
||||
continueTitle: 'Finish',
|
||||
continueTitle: FINISH_CONTINUE_TITLE,
|
||||
step: Steps.FinishStep,
|
||||
})
|
||||
}
|
||||
@ -228,10 +228,32 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
|
||||
override render() {
|
||||
return (
|
||||
<div className="sn-component" id="password-wizard">
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={this.dismiss}>{this.state.title}</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="sn-component h-full w-full md:h-auto md:w-auto" id="password-wizard">
|
||||
<Modal
|
||||
title={this.state.title}
|
||||
close={this.dismiss}
|
||||
actions={[
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: this.dismiss,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label:
|
||||
this.state.continueTitle === GENERATING_CONTINUE_TITLE && isMobileScreen() ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
this.state.continueTitle
|
||||
),
|
||||
onClick: this.nextStep,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
disabled: this.state.lockContinue,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
{this.state.step === Steps.PasswordStep && (
|
||||
<div className="flex flex-col pb-1.5">
|
||||
<form>
|
||||
@ -284,13 +306,8 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button primary onClick={this.nextStep} disabled={this.state.lockContinue} className="min-w-20">
|
||||
{this.state.continueTitle}
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { SNComponent } from '@standardnotes/snjs'
|
||||
import { useCallback } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import Modal from '../Shared/Modal'
|
||||
|
||||
type Props = {
|
||||
callback: (approved: boolean) => void
|
||||
@ -25,9 +23,30 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
|
||||
}, [callback, dismiss])
|
||||
|
||||
return (
|
||||
<ModalDialog className="w-full md:!w-[350px]">
|
||||
<ModalDialogLabel closeDialog={deny}>Activate Component</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<Modal
|
||||
title="Activate Component"
|
||||
close={deny}
|
||||
actions={[
|
||||
{ label: 'Cancel', onClick: deny, type: 'cancel', mobileSlot: 'left' },
|
||||
{
|
||||
label: 'Continue',
|
||||
onClick: accept,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
]}
|
||||
className={{ content: 'md:!w-[350px]' }}
|
||||
customFooter={
|
||||
<div className="hidden md:block">
|
||||
<ModalDialogButtons>
|
||||
<Button primary fullWidth onClick={accept} className="block">
|
||||
Continue
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<div className="text-base">
|
||||
<strong>{component.displayName}</strong>
|
||||
{' would like to interact with your '}
|
||||
@ -41,13 +60,8 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button primary fullWidth onClick={accept} className="block">
|
||||
Continue
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ApplicationEvent, PermissionDialog } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import ModalOverlay from '../Shared/ModalOverlay'
|
||||
import PermissionsModal from './PermissionsModal'
|
||||
|
||||
type Props = {
|
||||
@ -42,14 +43,18 @@ const PermissionsModalWrapper: FunctionComponent<Props> = ({ application }) => {
|
||||
}
|
||||
}, [application, onAppStart])
|
||||
|
||||
return dialog ? (
|
||||
<PermissionsModal
|
||||
callback={dialog.callback}
|
||||
dismiss={dismissPermissionsDialog}
|
||||
component={dialog.component}
|
||||
permissionsString={dialog.permissionsString}
|
||||
/>
|
||||
) : null
|
||||
return (
|
||||
<ModalOverlay isOpen={!!dialog}>
|
||||
{dialog && (
|
||||
<PermissionsModal
|
||||
callback={dialog.callback}
|
||||
dismiss={dismissPermissionsDialog}
|
||||
component={dialog.component}
|
||||
permissionsString={dialog.permissionsString}
|
||||
/>
|
||||
)}
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionsModalWrapper
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
||||
import { useLifecycleAnimation } from '@/Hooks/useLifecycleAnimation'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { ReactNode } from 'react'
|
||||
import Portal from '../Portal/Portal'
|
||||
import { useModalAnimation } from '../Shared/useModalAnimation'
|
||||
|
||||
const DisableScroll = () => {
|
||||
useDisableBodyScrollOnMobile()
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const MobilePopoverContent = ({
|
||||
open,
|
||||
@ -17,54 +23,7 @@ const MobilePopoverContent = ({
|
||||
title: string
|
||||
className?: string
|
||||
}) => {
|
||||
const [isMounted, setPopoverElement] = useLifecycleAnimation({
|
||||
open,
|
||||
enter: {
|
||||
keyframes: [
|
||||
{
|
||||
opacity: 0.25,
|
||||
transform: 'translateY(1rem)',
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
],
|
||||
options: {
|
||||
easing: 'cubic-bezier(.36,.66,.04,1)',
|
||||
duration: 150,
|
||||
fill: 'forwards',
|
||||
},
|
||||
initialStyle: {
|
||||
transformOrigin: 'bottom',
|
||||
},
|
||||
},
|
||||
enterCallback: (element) => {
|
||||
element.scrollTop = 0
|
||||
},
|
||||
exit: {
|
||||
keyframes: [
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
{
|
||||
opacity: 0,
|
||||
transform: 'translateY(1rem)',
|
||||
},
|
||||
],
|
||||
options: {
|
||||
easing: 'cubic-bezier(.36,.66,.04,1)',
|
||||
duration: 150,
|
||||
fill: 'forwards',
|
||||
},
|
||||
initialStyle: {
|
||||
transformOrigin: 'bottom',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useDisableBodyScrollOnMobile()
|
||||
const [isMounted, setPopoverElement] = useModalAnimation(open)
|
||||
|
||||
if (!isMounted) {
|
||||
return null
|
||||
@ -72,14 +31,15 @@ const MobilePopoverContent = ({
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<DisableScroll />
|
||||
<div
|
||||
ref={setPopoverElement}
|
||||
className="absolute top-0 left-0 z-modal flex h-full w-full origin-bottom flex-col bg-default pt-safe-top pb-safe-bottom opacity-0"
|
||||
className="fixed top-0 left-0 z-modal flex h-full w-full flex-col bg-default pt-safe-top pb-safe-bottom"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-border py-2.5 px-3 text-base">
|
||||
<div />
|
||||
<div className="font-semibold">{title}</div>
|
||||
<button className="font-semibold active:shadow-none active:outline-none" onClick={requestClose}>
|
||||
<button className="font-semibold text-info active:shadow-none active:outline-none" onClick={requestClose}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
|
@ -45,6 +45,7 @@ const Popover = ({
|
||||
disableClickOutside,
|
||||
disableMobileFullscreenTakeover,
|
||||
maxHeight,
|
||||
portal,
|
||||
}: Props) => {
|
||||
const popoverId = useRef(UuidGenerator.GenerateUuid())
|
||||
|
||||
@ -123,6 +124,7 @@ const Popover = ({
|
||||
side={side}
|
||||
title={title}
|
||||
togglePopover={togglePopover}
|
||||
portal={portal}
|
||||
>
|
||||
{children}
|
||||
</PositionedPopoverContent>
|
||||
|
@ -2,9 +2,7 @@ import { useDocumentRect } from '@/Hooks/useDocumentRect'
|
||||
import { useAutoElementRect } from '@/Hooks/useElementRect'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useCallback, useLayoutEffect, useState } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Portal from '../Portal/Portal'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
|
||||
import { PopoverContentProps } from './Types'
|
||||
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
|
||||
@ -25,6 +23,7 @@ const PositionedPopoverContent = ({
|
||||
disableClickOutside,
|
||||
disableMobileFullscreenTakeover,
|
||||
maxHeight,
|
||||
portal = true,
|
||||
}: PopoverContentProps) => {
|
||||
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
|
||||
const popoverRect = useAutoElementRect(popoverElement)
|
||||
@ -72,7 +71,7 @@ const PositionedPopoverContent = ({
|
||||
}, [popoverElement, correctInitialScrollForOverflowedContent])
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Portal disabled={!portal}>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 flex w-full min-w-80 cursor-auto flex-col',
|
||||
@ -81,6 +80,7 @@ const PositionedPopoverContent = ({
|
||||
overrideZIndex ? overrideZIndex : 'z-dropdown-menu',
|
||||
!isDesktopScreen && !disableMobileFullscreenTakeover ? 'pt-safe-top pb-safe-bottom' : '',
|
||||
isDesktopScreen || disableMobileFullscreenTakeover ? 'invisible' : '',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
...styles,
|
||||
@ -97,15 +97,7 @@ const PositionedPopoverContent = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={classNames(disableMobileFullscreenTakeover && 'hidden', 'md:hidden')}>
|
||||
<div className="flex items-center justify-end px-3 pt-2">
|
||||
<button className="rounded-full border border-border p-1" onClick={togglePopover}>
|
||||
<Icon type="close" className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
</div>
|
||||
<div className={className}>{children}</div>
|
||||
{children}
|
||||
</div>
|
||||
</Portal>
|
||||
)
|
||||
|
@ -30,16 +30,6 @@ type PopoverAnchorPointProps = {
|
||||
anchorElement?: never
|
||||
}
|
||||
|
||||
type PopoverMutuallyExclusiveProps =
|
||||
| {
|
||||
togglePopover: () => void
|
||||
disableMobileFullscreenTakeover?: never
|
||||
}
|
||||
| {
|
||||
togglePopover?: never
|
||||
disableMobileFullscreenTakeover: boolean
|
||||
}
|
||||
|
||||
type CommonPopoverProps = {
|
||||
align?: PopoverAlignment
|
||||
children: ReactNode
|
||||
@ -48,7 +38,10 @@ type CommonPopoverProps = {
|
||||
className?: string
|
||||
disableClickOutside?: boolean
|
||||
maxHeight?: (calculatedMaxHeight: number) => number
|
||||
togglePopover?: () => void
|
||||
disableMobileFullscreenTakeover?: boolean
|
||||
title: string
|
||||
portal?: boolean
|
||||
}
|
||||
|
||||
export type PopoverContentProps = CommonPopoverProps & {
|
||||
@ -61,5 +54,5 @@ export type PopoverContentProps = CommonPopoverProps & {
|
||||
}
|
||||
|
||||
export type PopoverProps =
|
||||
| (CommonPopoverProps & PopoverMutuallyExclusiveProps & PopoverAnchorElementProps)
|
||||
| (CommonPopoverProps & PopoverMutuallyExclusiveProps & PopoverAnchorPointProps)
|
||||
| (CommonPopoverProps & PopoverAnchorElementProps)
|
||||
| (CommonPopoverProps & PopoverAnchorPointProps)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type Options = {
|
||||
@ -18,19 +17,14 @@ export const usePopoverCloseOnClickOutside = ({
|
||||
}: Options) => {
|
||||
useEffect(() => {
|
||||
const closeIfClickedOutside = (event: MouseEvent) => {
|
||||
const matchesMediumBreakpoint = matchMedia(MediaQueryBreakpoints.md).matches
|
||||
|
||||
if (!matchesMediumBreakpoint) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target as Element
|
||||
|
||||
const isDescendantOfMenu = popoverElement?.contains(target)
|
||||
const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false
|
||||
const closestPopoverId = target.closest('[data-popover]')?.getAttribute('data-popover')
|
||||
const isDescendantOfChildPopover = closestPopoverId && childPopovers.has(closestPopoverId)
|
||||
const isDescendantOfModal = !!target.closest('[aria-modal="true"]')
|
||||
const isPopoverInModal = popoverElement?.closest('[aria-modal="true"]')
|
||||
const isDescendantOfModal = isPopoverInModal ? false : !!target.closest('[aria-modal="true"]')
|
||||
|
||||
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfChildPopover && !isDescendantOfModal) {
|
||||
if (!disabled) {
|
||||
|
@ -3,11 +3,12 @@ import { createPortal } from 'react-dom'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const randomPortalId = () => Math.random()
|
||||
|
||||
const Portal = ({ children }: Props) => {
|
||||
const Portal = ({ children, disabled = false }: Props) => {
|
||||
const [container, setContainer] = useState<HTMLElement>()
|
||||
|
||||
useEffect(() => {
|
||||
@ -18,6 +19,10 @@ const Portal = ({ children }: Props) => {
|
||||
return () => container.remove()
|
||||
}, [])
|
||||
|
||||
if (disabled) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return container ? createPortal(children, container) : null
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,9 @@
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { useBeforeUnload } from '@/Hooks/useBeforeUnload'
|
||||
import ChangeEmailForm from './ChangeEmailForm'
|
||||
import ChangeEmailSuccess from './ChangeEmailSuccess'
|
||||
import Modal, { ModalAction } from '@/Components/Shared/Modal'
|
||||
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Continue',
|
||||
@ -37,7 +33,7 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
|
||||
const applicationAlertService = application.alertService
|
||||
|
||||
const validateCurrentPassword = async () => {
|
||||
const validateCurrentPassword = useCallback(async () => {
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
applicationAlertService.alert('Please enter your current password.').catch(console.error)
|
||||
|
||||
@ -54,14 +50,14 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
}, [application, applicationAlertService, currentPassword])
|
||||
|
||||
const resetProgressState = () => {
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Default)
|
||||
setIsContinuing(false)
|
||||
}
|
||||
|
||||
const processEmailChange = async () => {
|
||||
const processEmailChange = useCallback(async () => {
|
||||
await application.downloadBackup()
|
||||
|
||||
setLockContinue(true)
|
||||
@ -73,17 +69,17 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
setLockContinue(false)
|
||||
|
||||
return success
|
||||
}
|
||||
}, [application, currentPassword, newEmail])
|
||||
|
||||
const dismiss = () => {
|
||||
const dismiss = useCallback(() => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
|
||||
} else {
|
||||
onCloseDialog()
|
||||
}
|
||||
}
|
||||
}, [applicationAlertService, lockContinue, onCloseDialog])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (lockContinue || isContinuing) {
|
||||
return
|
||||
}
|
||||
@ -115,31 +111,43 @@ const ChangeEmail: FunctionComponent<Props> = ({ onCloseDialog, application }) =
|
||||
setIsContinuing(false)
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Finish)
|
||||
setCurrentStep(Steps.FinishStep)
|
||||
}
|
||||
}, [currentStep, dismiss, isContinuing, lockContinue, processEmailChange, validateCurrentPassword])
|
||||
|
||||
const handleDialogClose = () => {
|
||||
const handleDialogClose = useCallback(() => {
|
||||
if (lockContinue) {
|
||||
applicationAlertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
|
||||
} else {
|
||||
onCloseDialog()
|
||||
}
|
||||
}
|
||||
}, [applicationAlertService, lockContinue, onCloseDialog])
|
||||
|
||||
const modalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: handleDialogClose,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: submitButtonTitle,
|
||||
onClick: handleSubmit,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
],
|
||||
[handleDialogClose, handleSubmit, submitButtonTitle],
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={handleDialogClose}>Change Email</ModalDialogLabel>
|
||||
<ModalDialogDescription className="flex flex-row items-center px-4.5">
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<ChangeEmailForm setNewEmail={setNewEmail} setCurrentPassword={setCurrentPassword} />
|
||||
)}
|
||||
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons className="px-4.5">
|
||||
<Button className="min-w-20" primary label={submitButtonTitle} onClick={handleSubmit} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
<Modal title="Change Email" close={handleDialogClose} actions={modalActions}>
|
||||
<div className="px-4.5 py-4">
|
||||
{currentStep === Steps.InitialStep && (
|
||||
<ChangeEmailForm setNewEmail={setNewEmail} setCurrentPassword={setCurrentPassword} />
|
||||
)}
|
||||
{currentStep === Steps.FinishStep && <ChangeEmailSuccess />}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import PasswordWizard from '@/Components/PasswordWizard/PasswordWizard'
|
||||
import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup'
|
||||
import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment'
|
||||
import ModalOverlay from '@/Components/Shared/ModalOverlay'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@ -33,6 +34,8 @@ const Credentials: FunctionComponent<Props> = ({ application }: Props) => {
|
||||
setShouldShowPasswordWizard(false)
|
||||
}, [])
|
||||
|
||||
const closeChangeEmailDialog = () => setIsChangeEmailDialogOpen(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
@ -55,14 +58,14 @@ const Credentials: FunctionComponent<Props> = ({ application }: Props) => {
|
||||
Current password was set on <span className="font-bold">{passwordCreatedOn}</span>
|
||||
</Text>
|
||||
<Button className="mt-3 min-w-20" label="Change password" onClick={presentPasswordWizard} />
|
||||
{isChangeEmailDialogOpen && (
|
||||
<ChangeEmail onCloseDialog={() => setIsChangeEmailDialogOpen(false)} application={application} />
|
||||
)}
|
||||
<ModalOverlay isOpen={isChangeEmailDialogOpen} onDismiss={closeChangeEmailDialog}>
|
||||
<ChangeEmail onCloseDialog={closeChangeEmailDialog} application={application} />
|
||||
</ModalOverlay>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
{shouldShowPasswordWizard ? (
|
||||
<ModalOverlay isOpen={shouldShowPasswordWizard} onDismiss={dismissPasswordWizard}>
|
||||
<PasswordWizard application={application} dismissModal={dismissPasswordWizard} />
|
||||
) : null}
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,19 +1,15 @@
|
||||
import { FunctionComponent, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { isEmailValid } from '@/Utils'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
|
||||
import InviteForm from './InviteForm'
|
||||
import InviteSuccess from './InviteSuccess'
|
||||
import Modal, { ModalAction } from '@/Components/Shared/Modal'
|
||||
|
||||
enum SubmitButtonTitles {
|
||||
Default = 'Send Invite',
|
||||
Default = 'Invite',
|
||||
Sending = 'Sending...',
|
||||
Finish = 'Finish',
|
||||
}
|
||||
@ -36,7 +32,7 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
|
||||
const [lockContinue, setLockContinue] = useState(false)
|
||||
const [currentStep, setCurrentStep] = useState(Steps.InitialStep)
|
||||
|
||||
const validateInviteeEmail = async () => {
|
||||
const validateInviteeEmail = useCallback(async () => {
|
||||
if (!isEmailValid(inviteeEmail)) {
|
||||
application.alertService
|
||||
.alert('The email you entered has an invalid format. Please review your input and try again.')
|
||||
@ -46,22 +42,22 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}, [application.alertService, inviteeEmail])
|
||||
|
||||
const handleDialogClose = () => {
|
||||
const handleDialogClose = useCallback(() => {
|
||||
if (lockContinue) {
|
||||
application.alertService.alert('Cannot close window until pending tasks are complete.').catch(console.error)
|
||||
} else {
|
||||
onCloseDialog()
|
||||
}
|
||||
}
|
||||
}, [application.alertService, lockContinue, onCloseDialog])
|
||||
|
||||
const resetProgressState = () => {
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Default)
|
||||
setIsContinuing(false)
|
||||
}
|
||||
|
||||
const processInvite = async () => {
|
||||
const processInvite = useCallback(async () => {
|
||||
setLockContinue(true)
|
||||
|
||||
const success = await subscriptionState.sendSubscriptionInvitation(inviteeEmail)
|
||||
@ -69,9 +65,9 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
|
||||
setLockContinue(false)
|
||||
|
||||
return success
|
||||
}
|
||||
}, [inviteeEmail, subscriptionState])
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (lockContinue || isContinuing) {
|
||||
return
|
||||
}
|
||||
@ -107,21 +103,43 @@ const Invite: FunctionComponent<Props> = ({ onCloseDialog, application, subscrip
|
||||
setIsContinuing(false)
|
||||
setSubmitButtonTitle(SubmitButtonTitles.Finish)
|
||||
setCurrentStep(Steps.FinishStep)
|
||||
}
|
||||
}, [
|
||||
application.alertService,
|
||||
currentStep,
|
||||
handleDialogClose,
|
||||
isContinuing,
|
||||
lockContinue,
|
||||
processInvite,
|
||||
validateInviteeEmail,
|
||||
])
|
||||
|
||||
const modalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: submitButtonTitle,
|
||||
onClick: handleSubmit,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
disabled: lockContinue,
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: handleDialogClose,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
hidden: currentStep === Steps.FinishStep,
|
||||
},
|
||||
],
|
||||
[currentStep, handleDialogClose, handleSubmit, lockContinue, submitButtonTitle],
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={handleDialogClose}>Share your Subscription</ModalDialogLabel>
|
||||
<ModalDialogDescription className="flex flex-row items-center px-4.5">
|
||||
{currentStep === Steps.InitialStep && <InviteForm setInviteeEmail={setInviteeEmail} />}
|
||||
{currentStep === Steps.FinishStep && <InviteSuccess />}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons className="px-4.5">
|
||||
<Button className="min-w-20" primary label={submitButtonTitle} onClick={handleSubmit} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
<Modal title="Share your Subscription" close={handleDialogClose} actions={modalActions}>
|
||||
<div className="px-4.5 py-4">
|
||||
{currentStep === Steps.InitialStep && <InviteForm setInviteeEmail={setInviteeEmail} />}
|
||||
{currentStep === Steps.FinishStep && <InviteSuccess />}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import InvitationsList from './InvitationsList'
|
||||
import Invite from './Invite/Invite'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import SharingStatusText from './SharingStatusText'
|
||||
import ModalOverlay from '@/Components/Shared/ModalOverlay'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@ -28,6 +29,8 @@ const SubscriptionSharing: FunctionComponent<Props> = ({ application, viewContro
|
||||
const isSubscriptionSharingFeatureAvailable =
|
||||
application.features.getFeatureStatus(FeatureIdentifier.SubscriptionSharing) === FeatureStatus.Entitled
|
||||
|
||||
const closeInviteDialog = () => setIsInviteDialogOpen(false)
|
||||
|
||||
return (
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
@ -42,13 +45,13 @@ const SubscriptionSharing: FunctionComponent<Props> = ({ application, viewContro
|
||||
{!subscriptionState.allInvitationsUsed && (
|
||||
<Button className="min-w-20" label="Invite" onClick={() => setIsInviteDialogOpen(true)} />
|
||||
)}
|
||||
{isInviteDialogOpen && (
|
||||
<ModalOverlay isOpen={isInviteDialogOpen} onDismiss={closeInviteDialog}>
|
||||
<Invite
|
||||
onCloseDialog={() => setIsInviteDialogOpen(false)}
|
||||
onCloseDialog={closeInviteDialog}
|
||||
application={application}
|
||||
subscriptionState={subscriptionState}
|
||||
/>
|
||||
)}
|
||||
</ModalOverlay>
|
||||
</div>
|
||||
) : (
|
||||
<NoProSubscription application={application} />
|
||||
|
@ -1,15 +1,11 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import IconPicker from '@/Components/Icon/IconPicker'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Modal, { ModalAction } from '@/Components/Shared/Modal'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
import { Platform, SmartViewDefaultIconName, VectorIconNameOrEmoji } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { EditSmartViewModalController } from './EditSmartViewModalController'
|
||||
|
||||
type Props = {
|
||||
@ -63,15 +59,40 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
|
||||
}
|
||||
}, [isPredicateJsonValid])
|
||||
|
||||
const modalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: 'Delete',
|
||||
onClick: deleteView,
|
||||
disabled: isSaving,
|
||||
type: 'destructive',
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: closeDialog,
|
||||
disabled: isSaving,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save',
|
||||
onClick: saveSmartView,
|
||||
disabled: isSaving,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
],
|
||||
[closeDialog, deleteView, isSaving, saveSmartView],
|
||||
)
|
||||
|
||||
if (!view) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>Edit Smart View "{view.title}"</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Modal title={`Edit Smart View "${view.title}"`} close={closeDialog} actions={modalActions}>
|
||||
<div className="px-4 py-4">
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="text-sm font-semibold">Title:</div>
|
||||
<input
|
||||
@ -115,9 +136,9 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="flex flex-grow flex-col gap-2.5">
|
||||
<div className="text-sm font-semibold">Predicate:</div>
|
||||
<div className="flex flex-col overflow-hidden rounded-md border border-border">
|
||||
<div className="flex flex-grow flex-col overflow-hidden rounded-md border border-border">
|
||||
<textarea
|
||||
className="h-full min-h-[10rem] w-full flex-grow resize-none bg-default py-1.5 px-2.5 font-mono text-sm"
|
||||
value={predicateJson}
|
||||
@ -136,19 +157,8 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="mr-auto" disabled={isSaving} onClick={deleteView} colorStyle="danger">
|
||||
Delete
|
||||
</Button>
|
||||
<Button disabled={isSaving} onClick={saveSmartView} primary colorStyle="info">
|
||||
{isSaving ? <Spinner className="h-4.5 w-4.5" /> : 'Save'}
|
||||
</Button>
|
||||
<Button disabled={isSaving} onClick={closeDialog}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,8 +14,8 @@ const SmartViewItem = ({ view, onEdit, onDelete }: Props) => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1.5">
|
||||
<Icon type={view.iconString} size="custom" className="h-5.5 w-5.5" />
|
||||
<span className="mr-auto text-sm">{view.title}</span>
|
||||
<Icon type={view.iconString} size="custom" className="h-5.5 w-5.5 flex-shrink-0" />
|
||||
<span className="mr-auto overflow-hidden text-ellipsis text-sm">{view.title}</span>
|
||||
<Button small onClick={onEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
|
@ -15,6 +15,7 @@ import NoSubscriptionBanner from '@/Components/NoSubscriptionBanner/NoSubscripti
|
||||
import { EditSmartViewModalController } from './EditSmartViewModalController'
|
||||
import { STRING_DELETE_TAG } from '@/Constants/Strings'
|
||||
import { confirmDialog } from '@standardnotes/ui-services'
|
||||
import ModalOverlay from '@/Components/Shared/ModalOverlay'
|
||||
|
||||
type NewType = {
|
||||
application: WebApplication
|
||||
@ -88,12 +89,15 @@ const SmartViews = ({ application, featuresController }: Props) => {
|
||||
)}
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
{!!editSmartViewModalController.view && (
|
||||
<ModalOverlay isOpen={!!editSmartViewModalController.view} onDismiss={editSmartViewModalController.closeDialog}>
|
||||
<EditSmartViewModal controller={editSmartViewModalController} platform={application.platform} />
|
||||
)}
|
||||
{addSmartViewModalController.isAddingSmartView && (
|
||||
</ModalOverlay>
|
||||
<ModalOverlay
|
||||
isOpen={addSmartViewModalController.isAddingSmartView}
|
||||
onDismiss={addSmartViewModalController.closeModal}
|
||||
>
|
||||
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />
|
||||
)}
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import IconButton from '@/Components/Button/IconButton'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@ -7,10 +6,6 @@ import CopyButton from './CopyButton'
|
||||
import Bullet from './Bullet'
|
||||
import { downloadSecretKey } from './download-secret-key'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
@ -18,72 +13,59 @@ type Props = {
|
||||
}
|
||||
|
||||
const SaveSecretKey: FunctionComponent<Props> = ({ activation: act }) => {
|
||||
const download = (
|
||||
<IconButton
|
||||
focusable={false}
|
||||
title="Download"
|
||||
icon="download"
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
downloadSecretKey(act.secretKey)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel
|
||||
closeDialog={() => {
|
||||
act.cancelActivation()
|
||||
}}
|
||||
>
|
||||
Step 2 of 3 - Save secret key
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription className="h-33 flex flex-row items-center">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<Bullet />
|
||||
<div className="text-sm">
|
||||
<b>Save your secret key</b>{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
|
||||
>
|
||||
somewhere safe
|
||||
</a>
|
||||
:
|
||||
</div>
|
||||
<DecoratedInput
|
||||
disabled={true}
|
||||
right={[<CopyButton copyValue={act.secretKey} />, download]}
|
||||
value={act.secretKey}
|
||||
className={{ container: 'ml-2' }}
|
||||
/>
|
||||
<div className="h-33 flex flex-row items-center px-4 py-4">
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<Bullet />
|
||||
<div className="text-sm">
|
||||
<b>Save your secret key</b>{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
|
||||
>
|
||||
somewhere safe
|
||||
</a>
|
||||
:
|
||||
</div>
|
||||
<div className="h-2" />
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
You can use this key to generate codes if you lose access to your authenticator app.
|
||||
<br />
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="underline hover:no-underline"
|
||||
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
|
||||
>
|
||||
Learn more
|
||||
<Icon className="ml-1 inline" type="open-in" size="small" />
|
||||
</a>
|
||||
</div>
|
||||
<DecoratedInput
|
||||
disabled={true}
|
||||
right={[
|
||||
<CopyButton copyValue={act.secretKey} />,
|
||||
<IconButton
|
||||
focusable={false}
|
||||
title="Download"
|
||||
icon="download"
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
downloadSecretKey(act.secretKey)
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
value={act.secretKey}
|
||||
className={{ container: 'ml-2' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-2" />
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
You can use this key to generate codes if you lose access to your authenticator app.
|
||||
<br />
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="underline hover:no-underline"
|
||||
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
|
||||
>
|
||||
Learn more
|
||||
<Icon className="ml-1 inline" type="open-in" size="small" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="min-w-20" label="Back" onClick={() => act.openScanQRCode()} />
|
||||
<Button className="min-w-20" primary label="Next" onClick={() => act.openVerification()} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2,58 +2,53 @@ import { FunctionComponent } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import QRCode from 'qrcode.react'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
import AuthAppInfoTooltip from './AuthAppInfoPopup'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import CopyButton from './CopyButton'
|
||||
import Bullet from './Bullet'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
type Props = {
|
||||
activation: TwoFactorActivation
|
||||
}
|
||||
|
||||
const ScanQRCode: FunctionComponent<Props> = ({ activation: act }) => {
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 1 of 3 - Scan QR code</ModalDialogLabel>
|
||||
<ModalDialogDescription className="h-33 flex flex-col items-center gap-5 md:flex-row">
|
||||
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
||||
<QRCode className="border-2 border-solid border-neutral-contrast" value={act.qrCode} size={100} />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col gap-2">
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
Open your <b>authenticator app</b>.
|
||||
</div>
|
||||
<div className="min-w-2" />
|
||||
<AuthAppInfoTooltip />
|
||||
<div className="h-33 flex flex-col items-center gap-5 px-4 py-4 md:flex-row">
|
||||
<div className="flex items-center justify-center bg-info">
|
||||
<QRCode
|
||||
className="border-2 border-solid border-neutral-contrast"
|
||||
value={act.qrCode}
|
||||
size={isMobileScreen ? 200 : 150}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col gap-2">
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet />
|
||||
<div className="min-w-1" />
|
||||
<div className="text-sm">
|
||||
Open your <b>authenticator app</b>.
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet className="mt-2 self-start" />
|
||||
<div className="min-w-1" />
|
||||
<div className="flex-grow text-sm">
|
||||
<b>Scan this QR code</b> or <b>add this secret key</b>:
|
||||
</div>
|
||||
</div>
|
||||
<DecoratedInput
|
||||
className={{ container: 'w-92 ml-4' }}
|
||||
disabled={true}
|
||||
value={act.secretKey}
|
||||
right={[<CopyButton copyValue={act.secretKey} />]}
|
||||
/>
|
||||
<div className="min-w-2" />
|
||||
<AuthAppInfoTooltip />
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="min-w-20" label="Cancel" onClick={() => act.cancelActivation()} />
|
||||
<Button className="min-w-20" primary label="Next" onClick={() => act.openSaveSecretKey()} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
<div className="flex flex-row items-center">
|
||||
<Bullet className="mt-2 self-start" />
|
||||
<div className="min-w-1" />
|
||||
<div className="flex-grow text-sm">
|
||||
<b>Scan this QR code</b> or <b>add this secret key</b>:
|
||||
</div>
|
||||
</div>
|
||||
<DecoratedInput
|
||||
className={{ container: 'w-92 ml-4' }}
|
||||
disabled={true}
|
||||
value={act.secretKey}
|
||||
right={[<CopyButton copyValue={act.secretKey} />]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,8 @@ import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/Pre
|
||||
import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import RecoveryCodeBanner from '@/Components/RecoveryCodeBanner/RecoveryCodeBanner'
|
||||
import Modal, { ModalAction } from '@/Components/Shared/Modal'
|
||||
import ModalOverlay from '@/Components/Shared/ModalOverlay'
|
||||
|
||||
type Props = {
|
||||
auth: TwoFactorAuth
|
||||
@ -17,6 +19,74 @@ type Props = {
|
||||
}
|
||||
|
||||
const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
|
||||
const shouldShowActivationModal = auth.status !== 'fetching' && is2FAActivation(auth.status)
|
||||
|
||||
const activationModalTitle = shouldShowActivationModal
|
||||
? auth.status.activationStep === 'scan-qr-code'
|
||||
? 'Step 1 of 3 - Scan QR code'
|
||||
: auth.status.activationStep === 'save-secret-key'
|
||||
? 'Step 2 of 3 - Save secret key'
|
||||
: auth.status.activationStep === 'verification'
|
||||
? 'Step 3 of 3 - Verification'
|
||||
: auth.status.activationStep === 'success'
|
||||
? 'Successfully Enabled'
|
||||
: ''
|
||||
: ''
|
||||
|
||||
const closeActivationModal = () => {
|
||||
if (auth.status === 'fetching') {
|
||||
return
|
||||
}
|
||||
if (!is2FAActivation(auth.status)) {
|
||||
return
|
||||
}
|
||||
if (auth.status.activationStep === 'success') {
|
||||
auth.status.finishActivation()
|
||||
}
|
||||
auth.status.cancelActivation()
|
||||
}
|
||||
|
||||
const activationModalActions: ModalAction[] = shouldShowActivationModal
|
||||
? [
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: auth.status.cancelActivation,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
hidden: auth.status.activationStep !== 'scan-qr-code',
|
||||
},
|
||||
{
|
||||
label: 'Back',
|
||||
onClick:
|
||||
auth.status.activationStep === 'save-secret-key'
|
||||
? auth.status.openScanQRCode
|
||||
: auth.status.openSaveSecretKey,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
hidden: auth.status.activationStep !== 'save-secret-key' && auth.status.activationStep !== 'verification',
|
||||
},
|
||||
{
|
||||
label: 'Next',
|
||||
onClick:
|
||||
auth.status.activationStep === 'scan-qr-code'
|
||||
? auth.status.openSaveSecretKey
|
||||
: auth.status.activationStep === 'save-secret-key'
|
||||
? auth.status.openVerification
|
||||
: auth.status.enable2FA,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: auth.status.activationStep === 'success',
|
||||
},
|
||||
{
|
||||
label: 'Finish',
|
||||
onClick: auth.status.finishActivation,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: auth.status.activationStep !== 'success',
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreferencesGroup>
|
||||
@ -45,9 +115,11 @@ const TwoFactorAuthView: FunctionComponent<Props> = ({ auth, application }) => {
|
||||
</PreferencesSegment>
|
||||
)}
|
||||
</PreferencesGroup>
|
||||
{auth.status !== 'fetching' && is2FAActivation(auth.status) && (
|
||||
<TwoFactorActivationView activation={auth.status} />
|
||||
)}
|
||||
<ModalOverlay isOpen={shouldShowActivationModal} onDismiss={closeActivationModal}>
|
||||
<Modal title={activationModalTitle} close={closeActivationModal} actions={activationModalActions}>
|
||||
{shouldShowActivationModal && <TwoFactorActivationView activation={auth.status} />}
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,3 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import { Subtitle } from '@/Components/Preferences/PreferencesComponents/Content'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
@ -12,18 +7,12 @@ type Props = {
|
||||
activation: TwoFactorActivation
|
||||
}
|
||||
|
||||
const TwoFactorSuccess: FunctionComponent<Props> = ({ activation: act }) => (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={act.finishActivation}>Successfully Enabled</ModalDialogLabel>
|
||||
<ModalDialogDescription className="flex flex-row items-center">
|
||||
<div className="flex flex-row items-center justify-center pt-2">
|
||||
<Subtitle>Two-factor authentication has been successfully enabled for your account.</Subtitle>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button className="min-w-20" primary label="Finish" onClick={act.finishActivation} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
const TwoFactorSuccess: FunctionComponent<Props> = () => (
|
||||
<div className="flex flex-row items-center px-4 py-4">
|
||||
<div className="flex flex-row items-center justify-center pt-2">
|
||||
<Subtitle>Two-factor authentication has been successfully enabled for your account.</Subtitle>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default observer(TwoFactorSuccess)
|
||||
|
@ -1,13 +1,8 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Bullet from './Bullet'
|
||||
import { TwoFactorActivation } from './TwoFactorActivation'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
|
||||
type Props = {
|
||||
activation: TwoFactorActivation
|
||||
@ -17,47 +12,40 @@ const Verification: FunctionComponent<Props> = ({ activation: act }) => {
|
||||
const secretKeyClass = act.verificationStatus === 'invalid-secret' ? 'border-danger' : ''
|
||||
const authTokenClass = act.verificationStatus === 'invalid-auth-code' ? 'border-danger' : ''
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={act.cancelActivation}>Step 3 of 3 - Verification</ModalDialogLabel>
|
||||
<ModalDialogDescription className="h-33 flex flex-row items-center">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<div className="text-sm">
|
||||
<Bullet className="align-middle" />
|
||||
<span className="align-middle">
|
||||
Enter your <b>secret key</b>:
|
||||
</span>
|
||||
</div>
|
||||
<DecoratedInput
|
||||
className={{ container: `ml-2 w-full md:w-96 ${secretKeyClass}` }}
|
||||
onChange={act.setInputSecretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<div className="text-sm">
|
||||
<Bullet className="align-middle" />
|
||||
<span className="align-middle">
|
||||
Verify the <b>authentication code</b> generated by your authenticator app:
|
||||
</span>
|
||||
</div>
|
||||
<DecoratedInput
|
||||
className={{ container: `ml-2 w-full md:w-30 ${authTokenClass}` }}
|
||||
onChange={act.setInputOtpToken}
|
||||
/>
|
||||
<div className="h-33 flex flex-row items-center px-4 py-4">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<div className="text-sm">
|
||||
<Bullet className="align-middle" />
|
||||
<span className="align-middle">
|
||||
Enter your <b>secret key</b>:
|
||||
</span>
|
||||
</div>
|
||||
<DecoratedInput
|
||||
className={{ container: `ml-2 w-full md:w-96 ${secretKeyClass}` }}
|
||||
onChange={act.setInputSecretKey}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
<div className="text-sm">
|
||||
<Bullet className="align-middle" />
|
||||
<span className="align-middle">
|
||||
Verify the <b>authentication code</b> generated by your authenticator app:
|
||||
</span>
|
||||
</div>
|
||||
<DecoratedInput
|
||||
className={{ container: `ml-2 w-full md:w-30 ${authTokenClass}` }}
|
||||
onChange={act.setInputOtpToken}
|
||||
/>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{act.verificationStatus === 'invalid-auth-code' && (
|
||||
<div className="flex-grow text-sm text-danger">Incorrect authentication code, please try again.</div>
|
||||
)}
|
||||
{act.verificationStatus === 'invalid-secret' && (
|
||||
<div className="flex-grow text-sm text-danger">Incorrect secret key, please try again.</div>
|
||||
)}
|
||||
<Button className="min-w-20" label="Back" onClick={act.openSaveSecretKey} />
|
||||
<Button className="min-w-20" primary label="Next" onClick={act.enable2FA} />
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,8 @@ import PreferencesMenuItem from './PreferencesComponents/MenuItem'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import { PreferenceId } from '@standardnotes/ui-services'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { isIOS } from '@/Utils'
|
||||
|
||||
type Props = {
|
||||
menu: PreferencesMenu
|
||||
@ -53,7 +55,12 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
||||
}, [application])
|
||||
|
||||
return (
|
||||
<div className="border-t border-border bg-default px-5 pt-2 md:border-0 md:bg-contrast md:px-0 md:py-0">
|
||||
<div
|
||||
className={classNames(
|
||||
'border-t border-border bg-default px-5 pt-2 md:border-0 md:bg-contrast md:px-0 md:py-0',
|
||||
isIOS() ? 'pb-safe-bottom' : 'pb-2 md:pb-0',
|
||||
)}
|
||||
>
|
||||
<div className="hidden min-w-55 flex-col overflow-y-auto px-3 py-6 md:flex">
|
||||
{menuItems.map((pref) => (
|
||||
<PreferencesMenuItem
|
||||
@ -81,6 +88,11 @@ const PreferencesMenuView: FunctionComponent<Props> = ({ menu }) => {
|
||||
onChange={(paneId) => {
|
||||
selectPane(paneId as PreferenceId)
|
||||
}}
|
||||
classNameOverride={{
|
||||
wrapper: 'relative',
|
||||
popover: 'bottom-full w-full max-h-max',
|
||||
}}
|
||||
portal={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,12 +4,13 @@ import { observer } from 'mobx-react-lite'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import PreferencesCanvas from './PreferencesCanvas'
|
||||
import { PreferencesProps } from './PreferencesProps'
|
||||
import { isIOS } from '@/Utils'
|
||||
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { ESCAPE_COMMAND } from '@standardnotes/ui-services'
|
||||
import Modal from '../Shared/Modal'
|
||||
import { AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { isIOS } from '@/Utils'
|
||||
|
||||
const PreferencesView: FunctionComponent<PreferencesProps> = ({
|
||||
application,
|
||||
@ -18,8 +19,6 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
|
||||
userProvider,
|
||||
mfaProvider,
|
||||
}) => {
|
||||
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
|
||||
|
||||
const menu = useMemo(
|
||||
() => new PreferencesMenu(application, viewControllerManager.enableUnfinishedFeatures),
|
||||
[viewControllerManager.enableUnfinishedFeatures, application],
|
||||
@ -56,26 +55,32 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
|
||||
}, [addAndroidBackHandler, closePreferences])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 z-preferences flex h-full w-full flex-col bg-default pt-safe-top',
|
||||
isIOS() ? 'pb-safe-bottom' : 'pb-2 md:pb-0',
|
||||
)}
|
||||
style={{
|
||||
top: !isDesktopScreen ? `${document.documentElement.scrollTop}px` : '',
|
||||
<Modal
|
||||
close={closePreferences}
|
||||
title="Preferences"
|
||||
className={{
|
||||
content: 'md:h-full md:!max-h-full md:!w-full',
|
||||
description: 'flex flex-col',
|
||||
}}
|
||||
customHeader={
|
||||
<AlertDialogLabel
|
||||
className={classNames(
|
||||
'flex w-full flex-row items-center justify-between border-b border-solid border-border bg-default px-3 pb-2 md:p-3',
|
||||
isIOS() ? 'pt-safe-top' : 'pt-2',
|
||||
)}
|
||||
>
|
||||
<div className="hidden h-8 w-8 md:block" />
|
||||
<h1 className="text-base font-bold md:text-lg">Your preferences for Standard Notes</h1>
|
||||
<RoundIconButton
|
||||
onClick={() => {
|
||||
closePreferences()
|
||||
}}
|
||||
icon="close"
|
||||
label="Close preferences"
|
||||
/>
|
||||
</AlertDialogLabel>
|
||||
}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between border-b border-solid border-border bg-default px-3 py-2 md:p-3">
|
||||
<div className="hidden h-8 w-8 md:block" />
|
||||
<h1 className="text-base font-bold md:text-lg">Your preferences for Standard Notes</h1>
|
||||
<RoundIconButton
|
||||
onClick={() => {
|
||||
closePreferences()
|
||||
}}
|
||||
icon="close"
|
||||
label="Close preferences"
|
||||
/>
|
||||
</div>
|
||||
<PreferencesCanvas
|
||||
menu={menu}
|
||||
application={application}
|
||||
@ -84,7 +89,7 @@ const PreferencesView: FunctionComponent<PreferencesProps> = ({
|
||||
userProvider={userProvider}
|
||||
mfaProvider={mfaProvider}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import PreferencesView from './PreferencesView'
|
||||
import { PreferencesViewWrapperProps } from './PreferencesViewWrapperProps'
|
||||
import { useCommandService } from '../CommandProvider'
|
||||
import { OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
|
||||
import ModalOverlay from '../Shared/ModalOverlay'
|
||||
|
||||
const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = ({
|
||||
viewControllerManager,
|
||||
@ -18,18 +19,16 @@ const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = (
|
||||
})
|
||||
}, [commandService, viewControllerManager])
|
||||
|
||||
if (!viewControllerManager.preferencesController?.isOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<PreferencesView
|
||||
closePreferences={() => viewControllerManager.preferencesController.closePreferences()}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
mfaProvider={application}
|
||||
userProvider={application}
|
||||
/>
|
||||
<ModalOverlay isOpen={viewControllerManager.preferencesController?.isOpen} className="p-0">
|
||||
<PreferencesView
|
||||
closePreferences={() => viewControllerManager.preferencesController.closePreferences()}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
mfaProvider={application}
|
||||
userProvider={application}
|
||||
/>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,20 @@
|
||||
import { getPlatformString } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { DialogOverlay, DialogContent } from '@reach/dialog'
|
||||
import { ReactNode } from 'react'
|
||||
import { ForwardedRef, forwardRef, ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
const HistoryModalDialog = ({ children, onDismiss }: Props) => {
|
||||
const HistoryModalDialog = forwardRef(({ children, onDismiss }: Props, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={`sn-component ${getPlatformString()}`}
|
||||
onDismiss={onDismiss}
|
||||
aria-label="Note revision history"
|
||||
ref={ref}
|
||||
>
|
||||
<DialogContent
|
||||
aria-label="Note revision history"
|
||||
@ -26,6 +27,6 @@ const HistoryModalDialog = ({ children, onDismiss }: Props) => {
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default HistoryModalDialog
|
||||
|
@ -46,6 +46,7 @@ const HistoryModalDialogContent = ({
|
||||
>
|
||||
Content
|
||||
</button>
|
||||
<div className="mx-auto text-base font-semibold">History</div>
|
||||
<button className="ml-auto mr-2 rounded-full border border-border p-1.5" onClick={dismissModal}>
|
||||
<Icon type="close" className="h-4 w-4" />
|
||||
</button>
|
||||
|
@ -4,6 +4,7 @@ import HistoryModalDialogContent from './HistoryModalDialogContent'
|
||||
import HistoryModalDialog from './HistoryModalDialog'
|
||||
import { RevisionHistoryModalProps } from './RevisionHistoryModalProps'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { useModalAnimation } from '../Shared/useModalAnimation'
|
||||
|
||||
const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
application,
|
||||
@ -14,7 +15,9 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
}) => {
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
const isOpen = !!historyModalController.note
|
||||
const isOpen = Boolean(
|
||||
historyModalController.note && application.isAuthorizedToRenderItem(historyModalController.note),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let removeListener: (() => void) | undefined
|
||||
@ -33,24 +36,24 @@ const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
}
|
||||
}, [addAndroidBackHandler, historyModalController, isOpen])
|
||||
|
||||
if (!historyModalController.note) {
|
||||
return null
|
||||
}
|
||||
const [isMounted, setElement] = useModalAnimation(isOpen)
|
||||
|
||||
if (!application.isAuthorizedToRenderItem(historyModalController.note)) {
|
||||
if (!isMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<HistoryModalDialog onDismiss={historyModalController.dismissModal}>
|
||||
<HistoryModalDialogContent
|
||||
application={application}
|
||||
dismissModal={historyModalController.dismissModal}
|
||||
note={historyModalController.note}
|
||||
notesController={notesController}
|
||||
selectionController={selectionController}
|
||||
subscriptionController={subscriptionController}
|
||||
/>
|
||||
<HistoryModalDialog onDismiss={historyModalController.dismissModal} ref={setElement}>
|
||||
{!!historyModalController.note && (
|
||||
<HistoryModalDialogContent
|
||||
application={application}
|
||||
dismissModal={historyModalController.dismissModal}
|
||||
note={historyModalController.note}
|
||||
notesController={notesController}
|
||||
selectionController={selectionController}
|
||||
subscriptionController={subscriptionController}
|
||||
/>
|
||||
)}
|
||||
</HistoryModalDialog>
|
||||
)
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { SNApplication, SessionStrings, UuidString, isNullOrUndefined, RemoteSession } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useState, useEffect, useRef, useMemo } from 'react'
|
||||
import { FunctionComponent, useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
||||
import { Alert } from '@reach/alert'
|
||||
import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Modal, { ModalAction } from '../Shared/Modal'
|
||||
import ModalOverlay from '../Shared/ModalOverlay'
|
||||
|
||||
type Session = RemoteSession & {
|
||||
revoking?: true
|
||||
@ -82,7 +81,7 @@ const SessionsModalContent: FunctionComponent<{
|
||||
viewControllerManager: ViewControllerManager
|
||||
application: SNApplication
|
||||
}> = ({ viewControllerManager, application }) => {
|
||||
const close = () => viewControllerManager.closeSessionsModal()
|
||||
const close = useCallback(() => viewControllerManager.closeSessionsModal(), [viewControllerManager])
|
||||
|
||||
const [sessions, refresh, refreshing, revokeSession, errorMessage] = useSessions(application)
|
||||
|
||||
@ -106,20 +105,29 @@ const SessionsModalContent: FunctionComponent<{
|
||||
const closeRevokeConfirmationDialog = () => {
|
||||
setRevokingSessionUuid('')
|
||||
}
|
||||
|
||||
const sessionModalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: 'Close',
|
||||
onClick: close,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Refresh',
|
||||
onClick: refresh,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
],
|
||||
[close, refresh],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalDialog onDismiss={close} className="sessions-modal max-h-[90vh]">
|
||||
<ModalDialogLabel
|
||||
headerButtons={
|
||||
<Button small colorStyle="info" onClick={refresh}>
|
||||
Refresh
|
||||
</Button>
|
||||
}
|
||||
closeDialog={close}
|
||||
>
|
||||
Active Sessions
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription className="overflow-y-auto">
|
||||
<Modal title="Active Sessions" close={close} actions={sessionModalActions}>
|
||||
<div className="px-4 py-4">
|
||||
{refreshing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Spinner className="h-3 w-3" />
|
||||
@ -155,8 +163,8 @@ const SessionsModalContent: FunctionComponent<{
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
{confirmRevokingSessionUuid && (
|
||||
<AlertDialog onDismiss={closeRevokeConfirmationDialog} leastDestructiveRef={cancelRevokeRef} className="p-0">
|
||||
<div className="sk-modal-content">
|
||||
@ -206,11 +214,15 @@ const SessionsModal: FunctionComponent<{
|
||||
viewControllerManager: ViewControllerManager
|
||||
application: WebApplication
|
||||
}> = ({ viewControllerManager, application }) => {
|
||||
if (viewControllerManager.isSessionsModalVisible) {
|
||||
return <SessionsModalContent application={application} viewControllerManager={viewControllerManager} />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ModalOverlay
|
||||
isOpen={viewControllerManager.isSessionsModalVisible}
|
||||
onDismiss={viewControllerManager.closeSessionsModal}
|
||||
className="sessions-modal"
|
||||
>
|
||||
<SessionsModalContent application={application} viewControllerManager={viewControllerManager} />
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(SessionsModal)
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
action: () => void
|
||||
slot: 'left' | 'right'
|
||||
type?: 'primary' | 'secondary' | 'destructive' | 'cancel'
|
||||
} & Omit<ComponentPropsWithoutRef<'button'>, 'onClick' | 'type'>
|
||||
|
||||
const MobileModalAction = forwardRef(
|
||||
({ children, action, type = 'primary', slot, className, ...props }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'flex whitespace-nowrap py-1 px-1 text-base font-semibold focus:shadow-none focus:outline-none active:shadow-none active:outline-none disabled:text-neutral md:hidden',
|
||||
slot === 'left' ? 'justify-start text-left' : 'justify-end text-right',
|
||||
type === 'cancel' || type === 'destructive' ? 'text-danger' : 'text-info',
|
||||
className,
|
||||
)}
|
||||
onClick={action}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default MobileModalAction
|
215
packages/web/src/javascripts/Components/Shared/Modal.tsx
Normal file
215
packages/web/src/javascripts/Components/Shared/Modal.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { isIOS } from '@/Utils'
|
||||
import { AlertDialogContent, AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { ReactNode, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Popover from '../Popover/Popover'
|
||||
import MobileModalAction from './MobileModalAction'
|
||||
import ModalAndroidBackHandler from './ModalAndroidBackHandler'
|
||||
import ModalDialogDescription from './ModalDialogDescription'
|
||||
|
||||
export type ModalAction = {
|
||||
label: NonNullable<ReactNode>
|
||||
type: 'primary' | 'secondary' | 'destructive' | 'cancel'
|
||||
onClick: () => void
|
||||
mobileSlot?: 'left' | 'right'
|
||||
hidden?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
close: () => void
|
||||
actions?: ModalAction[]
|
||||
className?: {
|
||||
content?: string
|
||||
description?: string
|
||||
}
|
||||
customHeader?: ReactNode
|
||||
customFooter?: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Modal = ({ title, close, actions = [], className = {}, customHeader, customFooter, children }: Props) => {
|
||||
const sortedActions = useMemo(
|
||||
() =>
|
||||
actions
|
||||
.sort((a, b) => {
|
||||
if (a.type === 'cancel') {
|
||||
return -1
|
||||
}
|
||||
if (b.type === 'cancel') {
|
||||
return 1
|
||||
}
|
||||
if (a.type === 'destructive') {
|
||||
return -1
|
||||
}
|
||||
if (b.type === 'destructive') {
|
||||
return 1
|
||||
}
|
||||
if (a.type === 'secondary') {
|
||||
return -1
|
||||
}
|
||||
if (b.type === 'secondary') {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.filter((action) => !action.hidden),
|
||||
[actions],
|
||||
)
|
||||
|
||||
const primaryActions = sortedActions.filter((action) => action.type === 'primary')
|
||||
if (primaryActions.length > 1) {
|
||||
throw new Error('Modal can only have 1 primary action')
|
||||
}
|
||||
|
||||
const cancelActions = sortedActions.filter((action) => action.type === 'cancel')
|
||||
if (cancelActions.length > 1) {
|
||||
throw new Error('Modal can only have 1 cancel action')
|
||||
}
|
||||
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
const leftSlotAction = sortedActions.find((action) => action.mobileSlot === 'left')
|
||||
const rightSlotAction = sortedActions.find((action) => action.mobileSlot === 'right')
|
||||
const hasCancelAction = sortedActions.some((action) => action.type === 'cancel')
|
||||
const firstPrimaryActionIndex = sortedActions.findIndex((action) => action.type === 'primary')
|
||||
|
||||
const extraActions = sortedActions.filter((action) => action.type !== 'primary' && action.type !== 'cancel')
|
||||
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const advancedOptionRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalAndroidBackHandler close={close} />
|
||||
<AlertDialogContent
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'm-0 flex h-full w-full flex-col border-solid border-border bg-default p-0 md:h-auto md:max-h-[85vh] md:w-160 md:rounded md:border md:shadow-main',
|
||||
className.content,
|
||||
)}
|
||||
>
|
||||
{customHeader ? (
|
||||
customHeader
|
||||
) : (
|
||||
<AlertDialogLabel
|
||||
className={classNames(
|
||||
'flex flex-shrink-0 items-center justify-between rounded-t border-b border-solid border-border bg-default text-text md:px-4.5 md:py-3',
|
||||
isIOS() ? 'pt-safe-top' : 'py-1.5 px-2',
|
||||
)}
|
||||
>
|
||||
<div className="grid w-full grid-cols-[0.35fr_1fr_0.35fr] flex-row items-center justify-between gap-2 md:flex md:gap-0">
|
||||
{leftSlotAction ? (
|
||||
<MobileModalAction
|
||||
type={leftSlotAction.type}
|
||||
action={leftSlotAction.onClick}
|
||||
disabled={leftSlotAction.disabled}
|
||||
slot="left"
|
||||
>
|
||||
{leftSlotAction.label}
|
||||
</MobileModalAction>
|
||||
) : (
|
||||
<div className="md:hidden" />
|
||||
)}
|
||||
<div className="flex items-center justify-center gap-2 overflow-hidden text-center text-base font-semibold text-text md:flex-grow md:text-left md:text-lg">
|
||||
{extraActions.length > 0 && (
|
||||
<>
|
||||
<MobileModalAction
|
||||
type="secondary"
|
||||
action={() => setShowAdvanced((show) => !show)}
|
||||
slot="left"
|
||||
ref={advancedOptionRef}
|
||||
>
|
||||
<div className="rounded-full border border-border p-0.5">
|
||||
<Icon type="more" />
|
||||
</div>
|
||||
</MobileModalAction>
|
||||
<Popover
|
||||
title="Advanced"
|
||||
open={showAdvanced}
|
||||
anchorElement={advancedOptionRef.current}
|
||||
disableMobileFullscreenTakeover={true}
|
||||
togglePopover={() => setShowAdvanced((show) => !show)}
|
||||
align="start"
|
||||
portal={false}
|
||||
className="w-1/2 !min-w-0 divide-y divide-border border border-border"
|
||||
>
|
||||
{extraActions
|
||||
.filter((action) => action.type !== 'cancel')
|
||||
.map((action, index) => (
|
||||
<button
|
||||
className={classNames(
|
||||
'p-2 text-base font-semibold hover:bg-contrast focus:bg-info-backdrop focus:shadow-none focus:outline-none',
|
||||
action.type === 'destructive' && 'text-danger',
|
||||
)}
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap ">{title}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<button tabIndex={0} className="ml-2 rounded p-1 font-bold hover:bg-contrast" onClick={close}>
|
||||
<Icon type="close" />
|
||||
</button>
|
||||
</div>
|
||||
{rightSlotAction ? (
|
||||
<MobileModalAction
|
||||
type={rightSlotAction.type}
|
||||
action={rightSlotAction.onClick}
|
||||
disabled={rightSlotAction.disabled}
|
||||
slot="right"
|
||||
>
|
||||
{rightSlotAction.label}
|
||||
</MobileModalAction>
|
||||
) : sortedActions.length === 0 || !hasCancelAction ? (
|
||||
<MobileModalAction children="Done" action={close} slot="right" />
|
||||
) : null}
|
||||
</div>
|
||||
</AlertDialogLabel>
|
||||
)}
|
||||
<ModalDialogDescription className={className.description}>{children}</ModalDialogDescription>
|
||||
{customFooter
|
||||
? customFooter
|
||||
: sortedActions.length > 0 && (
|
||||
<div
|
||||
className={classNames(
|
||||
'hidden items-center justify-start gap-3 border-t border-border py-2 px-2.5 md:flex md:px-4 md:py-4',
|
||||
isIOS() && 'pb-safe-bottom',
|
||||
)}
|
||||
>
|
||||
{sortedActions.map((action, index) => (
|
||||
<Button
|
||||
primary={action.type === 'primary'}
|
||||
colorStyle={action.type === 'destructive' ? 'danger' : undefined}
|
||||
key={action.label.toString()}
|
||||
onClick={action.onClick}
|
||||
className={classNames(
|
||||
action.mobileSlot ? 'hidden md:block' : '',
|
||||
index === firstPrimaryActionIndex && 'ml-auto',
|
||||
)}
|
||||
data-type={action.type}
|
||||
disabled={action.disabled}
|
||||
small={isMobileScreen}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</AlertDialogContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
@ -0,0 +1,28 @@
|
||||
import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const ModalAndroidBackHandler = ({ close }: Props) => {
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
const closeFnRef = useStateRef(close)
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = addAndroidBackHandler(() => {
|
||||
closeFnRef.current()
|
||||
return true
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, closeFnRef])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default ModalAndroidBackHandler
|
@ -12,11 +12,11 @@ const ModalDialog = ({ children, onDismiss, className }: Props) => {
|
||||
const ldRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<AlertDialogOverlay className="px-4 md:px-0" leastDestructiveRef={ldRef} onDismiss={onDismiss}>
|
||||
<AlertDialogOverlay className="p-0 md:px-0" leastDestructiveRef={ldRef} onDismiss={onDismiss}>
|
||||
<AlertDialogContent
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'flex max-h-[85vh] w-full flex-col rounded border border-solid border-border bg-default p-0 shadow-main md:w-160',
|
||||
'm-0 flex w-full flex-col border-solid border-border bg-default p-0 shadow-main md:max-h-[85vh] md:w-160 md:rounded md:border',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { isIOS } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { FunctionComponent, ReactNode } from 'react'
|
||||
|
||||
@ -9,7 +10,11 @@ type Props = {
|
||||
const ModalDialogButtons: FunctionComponent<Props> = ({ children, className }) => (
|
||||
<>
|
||||
<hr className="m-0 h-[1px] border-none bg-border" />
|
||||
<div className={classNames('flex items-center justify-end gap-3 px-4 py-4', className)}>{children}</div>
|
||||
<div
|
||||
className={classNames('flex items-center justify-end gap-3 px-4 py-4', isIOS() && 'pb-safe-bottom', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
|
@ -7,7 +7,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const ModalDialogDescription: FunctionComponent<Props> = ({ children, className = '' }) => (
|
||||
<AlertDialogDescription className={`overflow-y-auto px-4 py-4 ${className}`}>{children}</AlertDialogDescription>
|
||||
<AlertDialogDescription className={`flex-grow overflow-y-auto ${className}`}>{children}</AlertDialogDescription>
|
||||
)
|
||||
|
||||
export default ModalDialogDescription
|
||||
|
@ -3,15 +3,26 @@ import { AlertDialogLabel } from '@reach/alert-dialog'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { isIOS } from '@/Utils'
|
||||
import MobileModalAction from './MobileModalAction'
|
||||
|
||||
type Props = {
|
||||
closeDialog: () => void
|
||||
className?: string
|
||||
headerButtons?: ReactNode
|
||||
leftMobileButton?: ReactNode
|
||||
rightMobileButton?: ReactNode
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const ModalDialogLabel: FunctionComponent<Props> = ({ children, closeDialog, className, headerButtons }) => {
|
||||
const ModalDialogLabel: FunctionComponent<Props> = ({
|
||||
children,
|
||||
closeDialog,
|
||||
className,
|
||||
headerButtons,
|
||||
leftMobileButton,
|
||||
rightMobileButton,
|
||||
}) => {
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
|
||||
useEffect(() => {
|
||||
@ -29,18 +40,31 @@ const ModalDialogLabel: FunctionComponent<Props> = ({ children, closeDialog, cla
|
||||
return (
|
||||
<AlertDialogLabel
|
||||
className={classNames(
|
||||
'flex flex-shrink-0 items-center justify-between rounded-t border-b border-solid border-border bg-default px-4.5 py-3 text-text',
|
||||
'flex flex-shrink-0 items-center justify-between rounded-t border-b border-solid border-border bg-default py-1.5 px-1 text-text md:px-4.5 md:py-3',
|
||||
isIOS() && 'pt-safe-top',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<div className="flex-grow text-lg font-semibold text-text">{children}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="grid w-full grid-cols-[0.35fr_1fr_0.35fr] flex-row items-center justify-between gap-2 md:flex md:gap-0">
|
||||
{leftMobileButton ? leftMobileButton : <div className="md:hidden" />}
|
||||
<div
|
||||
className={classNames(
|
||||
'overflow-hidden text-ellipsis whitespace-nowrap text-center text-base font-semibold text-text md:flex-grow md:text-left md:text-lg',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
{headerButtons}
|
||||
<button tabIndex={0} className="rounded p-1 font-bold hover:bg-contrast" onClick={closeDialog}>
|
||||
<button tabIndex={0} className="ml-2 rounded p-1 font-bold hover:bg-contrast" onClick={closeDialog}>
|
||||
<Icon type="close" />
|
||||
</button>
|
||||
</div>
|
||||
{rightMobileButton ? (
|
||||
rightMobileButton
|
||||
) : (
|
||||
<MobileModalAction slot="right" children="Done" action={closeDialog} />
|
||||
)}
|
||||
</div>
|
||||
<hr className="h-1px no-border m-0 bg-border" />
|
||||
</AlertDialogLabel>
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { AlertDialogOverlay } from '@reach/alert-dialog'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { ReactNode, useRef } from 'react'
|
||||
import { useModalAnimation } from './useModalAnimation'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onDismiss?: () => void
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const ModalOverlay = ({ isOpen, onDismiss, children, className }: Props) => {
|
||||
const ldRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isMounted, setElement] = useModalAnimation(isOpen)
|
||||
|
||||
if (!isMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialogOverlay
|
||||
className={classNames('p-0 md:px-0 md:opacity-100', className)}
|
||||
leastDestructiveRef={ldRef}
|
||||
onDismiss={onDismiss}
|
||||
ref={setElement}
|
||||
>
|
||||
{children}
|
||||
</AlertDialogOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalOverlay
|
@ -0,0 +1,52 @@
|
||||
import { useLifecycleAnimation } from '@/Hooks/useLifecycleAnimation'
|
||||
import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
|
||||
export const useModalAnimation = (isOpen: boolean) => {
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
return useLifecycleAnimation(
|
||||
{
|
||||
open: isOpen,
|
||||
enter: {
|
||||
keyframes: [
|
||||
{
|
||||
transform: 'translateY(100%)',
|
||||
},
|
||||
{
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
],
|
||||
options: {
|
||||
easing: 'cubic-bezier(.36,.66,.04,1)',
|
||||
duration: 250,
|
||||
fill: 'forwards',
|
||||
},
|
||||
initialStyle: {
|
||||
transformOrigin: 'bottom',
|
||||
},
|
||||
},
|
||||
enterCallback: (element) => {
|
||||
element.scrollTop = 0
|
||||
},
|
||||
exit: {
|
||||
keyframes: [
|
||||
{
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
{
|
||||
transform: 'translateY(100%)',
|
||||
},
|
||||
],
|
||||
options: {
|
||||
easing: 'cubic-bezier(.36,.66,.04,1)',
|
||||
duration: 250,
|
||||
fill: 'forwards',
|
||||
},
|
||||
initialStyle: {
|
||||
transformOrigin: 'bottom',
|
||||
},
|
||||
},
|
||||
},
|
||||
!isMobileScreen,
|
||||
)
|
||||
}
|
@ -1,16 +1,11 @@
|
||||
import Button from '@/Components/Button/Button'
|
||||
import CompoundPredicateBuilder from '@/Components/SmartViewBuilder/CompoundPredicateBuilder'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import IconPicker from '@/Components/Icon/IconPicker'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Spinner from '@/Components/Spinner/Spinner'
|
||||
import { Platform, SmartViewDefaultIconName, VectorIconNameOrEmoji } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AddSmartViewModalController } from './AddSmartViewModalController'
|
||||
import TabPanel from '../Tabs/TabPanel'
|
||||
import { useTabState } from '../Tabs/useTabState'
|
||||
@ -18,6 +13,7 @@ import TabsContainer from '../Tabs/TabsContainer'
|
||||
import CopyableCodeBlock from '../Shared/CopyableCodeBlock'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import Modal, { ModalAction } from '../Shared/Modal'
|
||||
|
||||
type Props = {
|
||||
controller: AddSmartViewModalController
|
||||
@ -91,7 +87,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
defaultTab: 'builder',
|
||||
})
|
||||
|
||||
const save = () => {
|
||||
const save = useCallback(() => {
|
||||
if (!title.length) {
|
||||
titleInputRef.current?.focus()
|
||||
return
|
||||
@ -103,7 +99,13 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
}
|
||||
|
||||
void saveCurrentSmartView()
|
||||
}
|
||||
}, [
|
||||
isCustomJsonValidPredicate,
|
||||
saveCurrentSmartView,
|
||||
tabState.activeTab,
|
||||
title.length,
|
||||
validateAndPrettifyCustomPredicate,
|
||||
])
|
||||
|
||||
const canSave = tabState.activeTab === 'builder' || isCustomJsonValidPredicate
|
||||
|
||||
@ -117,11 +119,30 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
}
|
||||
}, [isCustomJsonValidPredicate, tabState.activeTab])
|
||||
|
||||
const modalActions = useMemo(
|
||||
(): ModalAction[] => [
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: closeModal,
|
||||
disabled: isSaving,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: isSaving ? <Spinner className="h-4.5 w-4.5" /> : canSave ? 'Save' : 'Validate',
|
||||
onClick: save,
|
||||
disabled: isSaving,
|
||||
mobileSlot: 'right',
|
||||
type: 'primary',
|
||||
},
|
||||
],
|
||||
[canSave, closeModal, isSaving, save],
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeModal}>Add Smart View</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Modal title="Add Smart View" close={closeModal} actions={modalActions}>
|
||||
<div className="px-4 py-4">
|
||||
<div className="flex h-full flex-col gap-4">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="text-sm font-semibold">Title:</div>
|
||||
<input
|
||||
@ -165,9 +186,10 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="flex flex-grow flex-col gap-2.5">
|
||||
<div className="text-sm font-semibold">Predicate:</div>
|
||||
<TabsContainer
|
||||
className="flex flex-grow flex-col"
|
||||
tabs={[
|
||||
{
|
||||
id: 'builder',
|
||||
@ -183,7 +205,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
<TabPanel state={tabState} id="builder" className="flex flex-col gap-2.5 p-4">
|
||||
<CompoundPredicateBuilder controller={predicateController} />
|
||||
</TabPanel>
|
||||
<TabPanel state={tabState} id="custom" className="flex flex-col">
|
||||
<TabPanel state={tabState} id="custom" className="flex flex-grow flex-col">
|
||||
<textarea
|
||||
className="h-full min-h-[10rem] w-full flex-grow resize-none bg-default py-1.5 px-2.5 font-mono text-sm"
|
||||
value={customPredicateJson}
|
||||
@ -221,16 +243,8 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button disabled={isSaving} onClick={closeModal} className="mr-auto">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={isSaving} onClick={save} colorStyle={canSave ? 'info' : 'default'} primary={canSave}>
|
||||
{isSaving ? <Spinner className="h-4.5 w-4.5" /> : canSave ? 'Save' : 'Validate'}
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import Tab from './Tab'
|
||||
import TabList from './TabList'
|
||||
import { TabState } from './useTabState'
|
||||
@ -9,11 +10,12 @@ type Props = {
|
||||
}[]
|
||||
state: TabState
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TabsContainer = ({ tabs, state, children }: Props) => {
|
||||
const TabsContainer = ({ tabs, state, className, children }: Props) => {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border border-border">
|
||||
<div className={classNames('overflow-hidden rounded-md border border-border', className)}>
|
||||
<TabList state={state} className="border-b border-border">
|
||||
{tabs.map(({ id, title }) => (
|
||||
<Tab key={id} id={id} className="first:rounded-tl-md">
|
||||
|
@ -8,6 +8,7 @@ import { FunctionComponent, useCallback, useMemo } from 'react'
|
||||
import IconButton from '../Button/IconButton'
|
||||
import EditSmartViewModal from '../Preferences/Panes/General/SmartViews/EditSmartViewModal'
|
||||
import { EditSmartViewModalController } from '../Preferences/Panes/General/SmartViews/EditSmartViewModalController'
|
||||
import ModalOverlay from '../Shared/ModalOverlay'
|
||||
import AddSmartViewModal from '../SmartViewBuilder/AddSmartViewModal'
|
||||
import { AddSmartViewModalController } from '../SmartViewBuilder/AddSmartViewModalController'
|
||||
import SmartViewsList from './SmartViewsList'
|
||||
@ -53,12 +54,15 @@ const SmartViewsSection: FunctionComponent<Props> = ({ application, navigationCo
|
||||
featuresController={featuresController}
|
||||
setEditingSmartView={editSmartViewModalController.setView}
|
||||
/>
|
||||
{!!editSmartViewModalController.view && (
|
||||
<ModalOverlay isOpen={!!editSmartViewModalController.view} onDismiss={editSmartViewModalController.closeDialog}>
|
||||
<EditSmartViewModal controller={editSmartViewModalController} platform={application.platform} />
|
||||
)}
|
||||
{addSmartViewModalController.isAddingSmartView && (
|
||||
</ModalOverlay>
|
||||
<ModalOverlay
|
||||
isOpen={addSmartViewModalController.isAddingSmartView}
|
||||
onDismiss={addSmartViewModalController.closeModal}
|
||||
>
|
||||
<AddSmartViewModal controller={addSmartViewModalController} platform={application.platform} />
|
||||
)}
|
||||
</ModalOverlay>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
@ -20,21 +20,18 @@ type Options = {
|
||||
* @param exitCallback A callback to run after the exit animation finishes
|
||||
* @returns A tuple containing whether the element can be mounted and a ref callback to set the element
|
||||
*/
|
||||
export const useLifecycleAnimation = ({
|
||||
open,
|
||||
enter,
|
||||
enterCallback,
|
||||
exit,
|
||||
exitCallback,
|
||||
}: Options): [boolean, RefCallback<HTMLElement | null>] => {
|
||||
export const useLifecycleAnimation = (
|
||||
{ open, enter, enterCallback, exit, exitCallback }: Options,
|
||||
disabled = false,
|
||||
): [boolean, RefCallback<HTMLElement | null>] => {
|
||||
const [element, setElement] = useState<HTMLElement | null>(null)
|
||||
|
||||
const [isMounted, setIsMounted] = useState(() => open)
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (disabled || open) {
|
||||
setIsMounted(open)
|
||||
}
|
||||
}, [open])
|
||||
}, [disabled, open])
|
||||
|
||||
// Using "state ref"s to prevent changes from re-running the effect below
|
||||
// We only want changes to `open` and `element` to re-run the effect
|
||||
@ -48,6 +45,11 @@ export const useLifecycleAnimation = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
setIsMounted(open)
|
||||
return
|
||||
}
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
@ -88,7 +90,7 @@ export const useLifecycleAnimation = ({
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}, [open, element, enterRef, enterCallbackRef, exitRef, exitCallbackRef])
|
||||
}, [open, element, enterRef, enterCallbackRef, exitRef, exitCallbackRef, disabled])
|
||||
|
||||
return [isMounted, setElement]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user