diff --git a/src/components/QueryBuilder.tsx b/src/components/QueryBuilder.tsx deleted file mode 100644 index 2ee39fe..0000000 --- a/src/components/QueryBuilder.tsx +++ /dev/null @@ -1,437 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Button, MenuItem, Popover } from "@blueprintjs/core"; -import { Select } from "@blueprintjs/select"; -import getUidsFromId from "roamjs-components/dom/getUidsFromId"; -import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; -import { Icon } from "@blueprintjs/core"; -import isControl from "roamjs-components/util/isControl"; -import ReactDOM from "react-dom"; -import PageInput from "roamjs-components/components/PageInput"; - -enum NODES { - OR = "OR", - AND = "AND", - NOT = "NOT", - BETWEEN = "BETWEEN", - TAG = "TAG", -} - -const NodeSelect = Select.ofType(); - -type QueryState = { - type: NODES; - key: number; - value?: string; - children?: QueryState[]; -}; - -const toQueryString = (queryState: QueryState): string => { - if (queryState.type === NODES.TAG) { - return `[[${queryState.value}]]`; - } else { - const operator = queryState.type.toLocaleString().toLowerCase(); - const children = (queryState.children || []) - .map((q) => toQueryString(q)) - .join(" "); - return `{${operator}:${children}}`; - } -}; - -const areEqual = (a: QueryState, b: QueryState): boolean => { - if (a.type !== b.type) { - return false; - } - if (a.type === NODES.TAG) { - return a.value === b.value; - } - const aChildren = a.children || []; - const bChildren = b.children || []; - return ( - aChildren.length === bChildren.length && - aChildren.every((aa, i) => areEqual(aa, bChildren[i])) - ); -}; - -const colors = ["red", "green", "blue"]; - -const SubqueryContent = ({ - value, - onChange, - level, - onDelete, -}: { - value: QueryState; - onChange: (e: QueryState) => void; - level: number; - onDelete?: () => void; -}) => { - const [key, setKey] = useState(value.children?.length || 0); - const incrementKey = useCallback(() => { - setKey(key + 1); - return key + 1; - }, [key, setKey]); - const [queryState, setQueryState] = useState(value); - useEffect(() => { - if (!areEqual(value, queryState)) { - onChange(queryState); - } - }, [queryState, onChange, value]); - const onItemSelect = useCallback( - (item) => - setQueryState({ - ...(item === NODES.TAG - ? { value: queryState.value || "" } - : { children: queryState.children || [] }), - type: item, - key: queryState.key, - }), - [setQueryState, queryState] - ); - const onSelectKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "a") { - onItemSelect(NODES.AND); - } else if (e.key === "o") { - onItemSelect(NODES.OR); - } else if (e.key === "b") { - onItemSelect(NODES.BETWEEN); - } else if (e.key === "t" && level > 0) { - onItemSelect(NODES.TAG); - } else if (e.key === "n" && level > 0) { - onItemSelect(NODES.NOT); - } - }, - [onItemSelect, level] - ); - const onContainerKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (!!onDelete && e.key === "Backspace" && isControl(e.nativeEvent)) { - onDelete(); - e.stopPropagation(); - } - }, - [onDelete] - ); - const onAddChild = useCallback( - () => - setQueryState({ - type: queryState.type, - children: [ - ...(queryState.children || []), - { type: NODES.TAG, children: [], key: incrementKey() }, - ], - key: queryState.key, - }), - [setQueryState, incrementKey, queryState] - ); - const addChildButtonRef = useRef(null); - const setValue = useCallback( - (value: string) => - setQueryState((qs) => ({ - type: qs.type, - value, - key: qs.key, - })), - [setQueryState] - ); - return ( -
-
- ( - - )} - filterable={false} - popoverProps={{ minimal: true, captureDismiss: true }} - > -
- {queryState.type !== NODES.TAG && ( -
- {(queryState.children || []).map((q, i) => ( - { - const children = queryState.children || []; - children[i] = newQ; - setQueryState({ - type: queryState.type, - key: queryState.key, - children, - }); - }} - level={level + 1} - key={q.key} - onDelete={() => { - const children = queryState.children || []; - if (i === children.length - 1) { - addChildButtonRef.current?.focus(); - } - delete children[i]; - setQueryState({ - type: queryState.type, - key: queryState.key, - children: children.filter((c) => !!c), - }); - }} - /> - ))} - {(queryState.type !== NODES.NOT || - (queryState.children?.length || 0) < 1) && ( -
- )} -
- ); -}; - -const toQueryStateChildren = (v: string): QueryState[] => { - let inParent = 0; - let inTag = 0; - let inHashTag = false; - const children = []; - let content = ""; - for (let pointer = 0; pointer < v.length; pointer++) { - const c = v.charAt(pointer); - if (c === "{") { - inParent++; - } else if ( - !inTag && - ((c === "#" && - v.charAt(pointer + 1) === "[" && - v.charAt(pointer + 2) === "[") || - (c === "[" && v.charAt(pointer + 1) === "[")) - ) { - inTag++; - } else if (!inHashTag && c === "#" && !inTag) { - inHashTag = true; - } - if (inParent || inTag || inHashTag) { - content = `${content}${c}`; - } - if (inParent > 0 && c === "}") { - inParent--; - } else if (inTag > 0 && c === "]" && v.charAt(pointer - 1) === "]") { - inTag--; - } else if (inHashTag && /(\s|\])/.test(c)) { - inHashTag = false; - } - if (inParent === 0 && inTag === 0 && !inHashTag && content) { - children.push({ ...toQueryState(content.trim()), key: children.length }); - content = ""; - } - } - - return children; -}; - -const QUERY_REGEX = /{{(?:query|\[\[query\]\]):(.*)}}/; -const toQueryState = (v = ""): QueryState => { - if (!v) { - return { - type: NODES.AND, - children: [] as QueryState[], - key: 0, - }; - } - if (QUERY_REGEX.test(v)) { - const content = v.match(QUERY_REGEX)?.[1]; - return toQueryState(content?.trim()); - } else if (v.startsWith("{and:")) { - const andContent = v.substring("{and:".length, v.length - "}".length); - const children = toQueryStateChildren(andContent.trim()); - return { - type: NODES.AND, - children, - key: 0, - }; - } else if (v.startsWith("{or:")) { - const orContent = v.substring("{or:".length, v.length - "}".length); - const children = toQueryStateChildren(orContent.trim()); - return { - type: NODES.OR, - children, - key: 0, - }; - } else if (v.startsWith("{between:")) { - const betweenContent = v.substring( - "{between:".length, - v.length - "}".length - ); - const children = toQueryStateChildren(betweenContent.trim()); - return { - type: NODES.BETWEEN, - children, - key: 0, - }; - } else if (v.startsWith("{not:")) { - const notContent = v.substring("{not:".length, v.length - "}".length); - const children = toQueryStateChildren(notContent.trim()); - return { - type: NODES.NOT, - children, - key: 0, - }; - } else if (v.startsWith("#[[")) { - const value = v.substring("#[[".length, v.length - "]]".length); - return { - type: NODES.TAG, - value, - key: 0, - }; - } else if (v.startsWith("[[")) { - const value = v.substring("[[".length, v.length - "]]".length); - return { - type: NODES.TAG, - value, - key: 0, - }; - } else if (v.startsWith("#")) { - const value = v.substring("#".length); - return { - type: NODES.TAG, - value, - key: 0, - }; - } else { - return { - type: NODES.AND, - children: [] as QueryState[], - key: 0, - }; - } -}; - -const QueryContent = ({ - blockId, - initialValue, - close, -}: { - blockId: string; - initialValue: string; - close: () => void; -}) => { - const [queryState, setQueryState] = useState( - toQueryState(initialValue) - ); - const onSave = useCallback(async () => { - const outputText = `{{[[query]]: ${toQueryString(queryState)}}}`; - const { blockUid } = getUidsFromId(blockId); - const text = getTextByBlockUid(blockUid); - const newText = initialValue - ? text.replace(initialValue, outputText) - : text.replace(/{{(\[\[)?(qb|query builder)(\]\])?}}/, outputText); - window.roamAlphaAPI.updateBlock({ - block: { string: newText, uid: blockUid }, - }); - close(); - }, [queryState, close, initialValue]); - - return ( -
- -
-
-
- ); -}; - -const QueryBuilder = ({ - blockId, - defaultIsOpen, - initialValue = "", -}: { - blockId: string; - defaultIsOpen: boolean; - initialValue?: string; -}): JSX.Element => { - const [isOpen, setIsOpen] = useState(defaultIsOpen); - const open = useCallback(() => setIsOpen(true), [setIsOpen]); - const close = useCallback(() => setIsOpen(false), [setIsOpen]); - return ( - - } - target={ -