diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx index 98b666280..028ab6322 100644 --- a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -9,6 +9,7 @@ export const IconNameToSvgMapping = { 'attachment-file': icons.AttachmentFileIcon, 'check-bold': icons.CheckBoldIcon, 'check-circle': icons.CheckCircleIcon, + 'chevron-up': icons.ChevronUpIcon, 'chevron-down': icons.ChevronDownIcon, 'chevron-left': icons.ChevronLeftIcon, 'chevron-right': icons.ChevronRightIcon, diff --git a/packages/web/src/javascripts/Components/Shared/CopyableCodeBlock.tsx b/packages/web/src/javascripts/Components/Shared/CopyableCodeBlock.tsx new file mode 100644 index 000000000..2417cb1e9 --- /dev/null +++ b/packages/web/src/javascripts/Components/Shared/CopyableCodeBlock.tsx @@ -0,0 +1,63 @@ +import { classNames } from '@/Utils/ConcatenateClassNames' +import { addToast, ToastType } from '@standardnotes/toast' +import { useRef, useState } from 'react' +import Icon from '../Icon/Icon' + +type Props = { + code: string +} + +const CopyableCodeBlock = ({ code }: Props) => { + const buttonRef = useRef(null) + const [didCopy, setDidCopy] = useState(false) + const [isCopyButtonVisible, setIsCopyButtonVisible] = useState(false) + + return ( +
setIsCopyButtonVisible(true)} + onMouseLeave={() => setIsCopyButtonVisible(false)} + > +
{code}
+
+ +
+ {didCopy ? 'Copied!' : 'Copy example to clipboard'} +
+
+
+ ) +} + +export default CopyableCodeBlock diff --git a/packages/web/src/javascripts/Components/SmartViewBuilder/AddSmartViewModal.tsx b/packages/web/src/javascripts/Components/SmartViewBuilder/AddSmartViewModal.tsx index fe9607219..d4e073843 100644 --- a/packages/web/src/javascripts/Components/SmartViewBuilder/AddSmartViewModal.tsx +++ b/packages/web/src/javascripts/Components/SmartViewBuilder/AddSmartViewModal.tsx @@ -10,35 +10,113 @@ import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' import Spinner from '@/Components/Spinner/Spinner' import { Platform } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' -import { useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { AddSmartViewModalController } from './AddSmartViewModalController' +import TabPanel from '../Tabs/TabPanel' +import { useTabState } from '../Tabs/useTabState' +import TabsContainer from '../Tabs/TabsContainer' +import CopyableCodeBlock from '../Shared/CopyableCodeBlock' +import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' +import { classNames } from '@/Utils/ConcatenateClassNames' type Props = { controller: AddSmartViewModalController platform: Platform } +const ConflictedNotesExampleCode = `{ + "keypath": "content.conflict_of.length", + "operator": ">", + "value": 0 +}` + +const ComplexCompoundExampleCode = `{ + "operator": "and", + "value": [ + { + "operator": "not", + "value": { + "keypath": "tags", + "operator": "includes", + "value": { + "keypath": "title", + "operator": "=", + "value": "completed" + } + } + }, + { + "keypath": "tags", + "operator": "includes", + "value": { + "keypath": "title", + "operator": "=", + "value": "todo" + } + } + ] +} +` + const AddSmartViewModal = ({ controller, platform }: Props) => { - const { isSaving, title, setTitle, icon, setIcon, closeModal, saveCurrentSmartView, predicateController } = controller + const { + isSaving, + title, + setTitle, + icon, + setIcon, + closeModal, + saveCurrentSmartView, + predicateController, + customPredicateJson, + setCustomPredicateJson, + isCustomJsonValidPredicate, + setIsCustomJsonValidPredicate, + validateAndPrettifyCustomPredicate, + } = controller const titleInputRef = useRef(null) + const customJsonInputRef = useRef(null) const [shouldShowIconPicker, setShouldShowIconPicker] = useState(false) const iconPickerButtonRef = useRef(null) + const [shouldShowJsonExamples, setShouldShowJsonExamples] = useState(false) + const toggleIconPicker = () => { setShouldShowIconPicker((shouldShow) => !shouldShow) } + const tabState = useTabState({ + defaultTab: 'builder', + }) + const save = () => { if (!title.length) { titleInputRef.current?.focus() return } + if (tabState.activeTab === 'custom' && !isCustomJsonValidPredicate) { + validateAndPrettifyCustomPredicate() + return + } + void saveCurrentSmartView() } + const canSave = tabState.activeTab === 'builder' || isCustomJsonValidPredicate + + useEffect(() => { + if (!customJsonInputRef.current) { + return + } + + if (tabState.activeTab === 'custom' && isCustomJsonValidPredicate === false) { + customJsonInputRef.current.focus() + } + }, [isCustomJsonValidPredicate, tabState.activeTab]) + return ( Add Smart View @@ -88,17 +166,68 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
Predicate:
- + + + + + +