diff --git a/packages/ui/src/components/menu/datepicker.css b/packages/ui/src/components/menu/datepicker.css
new file mode 100644
index 000000000..63c0f43a7
--- /dev/null
+++ b/packages/ui/src/components/menu/datepicker.css
@@ -0,0 +1,6 @@
+.react-datepicker-wrapper,
+.react-datepicker__input-container,
+.react-datepicker__input-container input {
+ display: block;
+ width: auto;
+}
diff --git a/packages/ui/src/components/menu/filterinput.js b/packages/ui/src/components/menu/filterinput.js
new file mode 100644
index 000000000..617e5547c
--- /dev/null
+++ b/packages/ui/src/components/menu/filterinput.js
@@ -0,0 +1,327 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { useEffect, forwardRef, useRef } from "react";
+import { hexToRGB } from "../../utils/color";
+import DatePicker from "react-datepicker";
+import "react-datepicker/dist/react-datepicker.css";
+import "./datepicker.css";
+import { filterSearchEngine } from "./search";
+import { Text } from "@theme-ui/components";
+import { mainSearchEngine } from "./search";
+
+export function FilterInput(props) {
+ const {
+ filters,
+ focusInput,
+ index,
+ setFilters,
+ item,
+ getSuggestions,
+ setSuggestions,
+ onFocus,
+ onBlur,
+ inputRef,
+ searchDefinitions
+ } = props;
+
+ useEffect(() => {
+ focusInput(filters.length - 1);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [filters.length]);
+
+ console.log();
+ // useEffect(() => {
+ // (async () => {
+ // console.log(filterRef.current.target.innerText);
+ // //await checkErrors(props, inputRef.filter.target.innerText);
+ // //addDefinition(item.input, searchDefinitions);
+ // })();
+ // // eslint-disable-next-line react-hooks/exhaustive-deps
+ // }, [filterRef.current.target.innerText]);
+
+ const setCalenderState = (state) => {
+ setFilters((filters) => {
+ let inputs = [...filters];
+ inputs[index].input.isCalenderOpen = state;
+ return inputs;
+ });
+ };
+
+ const setCalenderDate = () => {
+ document.getElementById(`inputId_${index}`).textContent =
+ filters[index].input.date.formatted;
+ };
+ return !item.input.isDateFilter ? (
+ {
+ await checkErrors(props, e.target.textContent);
+ setSuggestions(await getSuggestions(e.target.textContent, item.input));
+ onFocus(e);
+ }}
+ onBlur={onBlur}
+ onKeyDown={async (e) => {
+ (await onKeyPress(e, props))[e.key]();
+ }}
+ />
+ ) : (
+ }
+ peekNextMonth
+ showMonthDropdown
+ showYearDropdown
+ dropdownMode="select"
+ id={`inputId_${index}`}
+ selected={filters[index].input.date.orignal}
+ onCalendarClose={() => {
+ setCalenderDate();
+ document.getElementById(`inputId_${index}`).focus();
+ }}
+ onCalendarOpen={() => {
+ setCalenderDate();
+ }}
+ onChange={(date) => {
+ let inputs = [...filters];
+ inputs[index].input.date.formatted = `${date.getDate()}/${
+ date.getMonth() + 1
+ }/${date.getFullYear()}`;
+ inputs[index].input.date.orignal = date;
+ setFilters(inputs);
+ }}
+ onBlur={(e) => {
+ setCalenderState(false);
+ onBlur(e);
+ }}
+ onFocus={async (e) => {
+ setCalenderState(true);
+ setSuggestions(
+ //shift to top
+ await getSuggestions(e.target.textContent, item.input)
+ );
+ onFocus(e);
+ }}
+ onKeyDown={async (e) => {
+ (await onKeyPress(e, props))[e.key]();
+ }}
+ />
+ );
+}
+
+const CustomInput = forwardRef((props, refs) => (
+
+));
+CustomInput.displayName = "CustomInput";
+
+const getCursorPosition = (editableDiv) => {
+ //it is a general method, it should be somehwre else
+ var caretPos = 0,
+ sel,
+ range;
+ if (window.getSelection) {
+ sel = window.getSelection();
+ if (sel.rangeCount) {
+ range = sel.getRangeAt(0);
+ if (range.commonAncestorContainer.parentNode == editableDiv) {
+ caretPos = range.endOffset;
+ }
+ }
+ } else if (document.selection && document.selection.createRange) {
+ range = document.selection.createRange();
+ if (range.parentElement() == editableDiv) {
+ var tempEl = document.createElement("span");
+ editableDiv.insertBefore(tempEl, editableDiv.firstChild);
+ var tempRange = range.duplicate();
+ tempRange.moveToElementText(tempEl);
+ tempRange.setEndPoint("EndToEnd", range);
+ caretPos = tempRange.text.length;
+ }
+ }
+ return caretPos;
+};
+
+const addDefinition = (input, definitions) => {
+ //hard to understand// minimize if else
+ //for (let filter of filters) console.log(filter.input.state.result);
+ if (input.state.result) {
+ let isArrayEmpty = false;
+ for (let index = 0; index < definitions.length; index++) {
+ if (definitions[index].srNo === input.id) {
+ isArrayEmpty = true;
+ input.state.result.srNo = input.id;
+ definitions[index] = input.state.result;
+ }
+ }
+
+ if (!isArrayEmpty) {
+ input.state.result.srNo = input.id;
+ definitions.push(input.state.result);
+ }
+ }
+};
+
+const onClick = (props, query) => {
+ checkErrors(props, query);
+};
+
+const checkErrors = async (props, query) => {
+ const { setFilters, index, item } = props;
+ query = query.trim();
+ let input = item.input;
+ let result = await (await mainSearchEngine(input.type, query)).result;
+ setFilters((filters) => {
+ let _filters = [...filters];
+ _filters[index].input.state = filterInputState(input, result, query);
+ return _filters;
+ });
+};
+
+const filterInputState = (input, result, query) => {
+ console.log(
+ "filterInputState",
+ input.id,
+ query,
+ !input.hasSuggestions,
+ result.length > 0
+ );
+ if (!input.hasSuggestions)
+ return {
+ error: false,
+ message: "",
+ result: { type: input.type, value: query }
+ };
+
+ if (result.length > 0)
+ return { error: false, message: "", result: result[0] };
+
+ return {
+ error: true,
+ message: `This ${input.type.replace(
+ "s",
+ ""
+ )} is not present in the database.`,
+ result: undefined
+ };
+};
+
+const deleteDefinition = (definitions, id) => {
+ let index = 0;
+ for (let definition of definitions) {
+ if (definition.srNo === id) {
+ definitions.splice(index, 1);
+ }
+ index++;
+ }
+};
+
+const deleteFilter = (advanceInputs, index) => {
+ advanceInputs.splice(index, 1);
+ return advanceInputs;
+};
+
+const onKeyPress = async (e, props) => {
+ const {
+ filters,
+ focusInput,
+ index,
+ setFilters,
+ item,
+ getSuggestions,
+ setSuggestions,
+ onSearch,
+ searchDefinitions,
+ setSelectionIndex,
+ suggestions,
+ moveSelection
+ } = props;
+ setSuggestions(await getSuggestions(e.target.textContent, item.input));
+ await checkErrors(props, e.target.textContent);
+ return {
+ Enter: async () => {
+ props.onKeyDown(e);
+ focusInput(index + 1);
+ let results = await filterSearchEngine(searchDefinitions);
+ onSearch(results);
+ setSuggestions([]);
+ //await checkErrors(props, e.target.innerText);
+ addDefinition(item.input, searchDefinitions, filters);
+ e.preventDefault();
+ },
+ Escape: () => {
+ setSuggestions([]);
+ },
+ ArrowDown: () => {
+ moveSelection(suggestions, setSelectionIndex).Down();
+ e.preventDefault();
+ },
+ ArrowUp: () => {
+ moveSelection(suggestions, setSelectionIndex).Up();
+ e.preventDefault();
+ },
+ ArrowLeft: () => {
+ if (getCursorPosition(document.getElementById(e.target.id)) == 0) {
+ addDefinition(item.input, searchDefinitions);
+ focusInput(index - 1);
+ }
+ },
+ ArrowRight: () => {
+ if (
+ getCursorPosition(document.getElementById(e.target.id)) ==
+ e.target.textContent.length
+ ) {
+ addDefinition(item.input, searchDefinitions);
+ focusInput(index + 1);
+ }
+ },
+ Backspace: () => {
+ if (e.target.textContent === "") {
+ setSuggestions([]);
+ setFilters(deleteFilter(filters, index));
+ deleteDefinition(searchDefinitions, item.input.id);
+ focusInput(index - 1);
+ }
+ }
+ };
+};
diff --git a/packages/ui/src/components/menu/maininput.js b/packages/ui/src/components/menu/maininput.js
new file mode 100644
index 000000000..2d99ae2ca
--- /dev/null
+++ b/packages/ui/src/components/menu/maininput.js
@@ -0,0 +1,113 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { useEffect, useState } from "react";
+import { db } from "../../common/db";
+import { addToSearchHistory } from "./search";
+import { Input } from "@theme-ui/components";
+
+export function MainInput(props) {
+ const [searchHistory, setSearchHistory] = useState();
+
+ useEffect(async () => {
+ //correct all the useEffects
+ //history file to be added in core for search history
+ let list = await db.searchHistory.getHistory();
+ if (props.filters.length < 1) {
+ setSearchHistory(list);
+ } else {
+ setSearchHistory([]);
+ }
+ }, [db.searchHistory, props.filters.length]);
+
+ return (
+ {
+ props.refreshFilters(e.target.value, props.setFilters);
+ props.setSuggestions(
+ await props.getSuggestions(e.target.value, undefined, searchHistory)
+ );
+ }}
+ onFocus={async (e) => {
+ //shift to index
+ props.setSuggestions(
+ await props.getSuggestions(e.target.value, undefined, searchHistory)
+ );
+ props.onFocus(e);
+ }}
+ onKeyDown={async (e) => {
+ //keyActions(e);
+ props.onKeyDown(e);
+ await onKeyPress[e.key](e, props);
+ }}
+ />
+ );
+}
+
+const onKeyPress = {
+ Enter: async (e, props) => {
+ props.onSearch(e.target.value);
+ await addToSearchHistory(e.target.value);
+ props.setSuggestions([]);
+ props.refreshFilters(e.target.value, props.setFilters);
+ e.preventDefault();
+ },
+ Escape: (e, props) => {
+ props.setSuggestions([]);
+ },
+ ArrowDown: (e, props) => {
+ props.moveSelection(props.suggestions, props.setSelectionIndex).Down();
+ e.preventDefault();
+ },
+ ArrowUp: (e, props) => {
+ props.moveSelection(props.suggestions, props.setSelectionIndex).Up();
+ e.preventDefault();
+ },
+ ArrowLeft: (e, props) => {
+ if (e.target.selectionStart == 0) {
+ props.focusInput(props.filters.length - 1);
+ }
+ },
+ ArrowRight: (e, props) => {
+ if (e.target.selectionStart == e.target.value.length) {
+ props.focusInput(0);
+ }
+ },
+ Backspace: (e, props) => {
+ if (e.target.selectionStart == 0) {
+ props.focusInput(props.filters.length - 1);
+ }
+ }
+};
diff --git a/packages/ui/src/components/menu/search.js b/packages/ui/src/components/menu/search.js
new file mode 100644
index 000000000..50c323b85
--- /dev/null
+++ b/packages/ui/src/components/menu/search.js
@@ -0,0 +1,325 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { store } from "../../stores/note-store";
+import { db } from "../../common/db";
+import { filter, parse } from "liqe";
+
+export const mainSearchEngine = async (searchType, value) => {
+ //name is cumbersome
+ // better name? fetchResults
+ const context = store.context;
+ const [lookupType, allData] = await filterItemsToType(searchType, context);
+ if (lookupType !== undefined && allData !== undefined) {
+ let result = await db.lookup[lookupType](allData, value);
+ return { result, allData };
+ } else {
+ return { result: [], allData: [] };
+ }
+};
+
+/*
+there is mainly two types of search:
+filter search
+main search
+filter search is again divided into two: Why not make it one?
+how to search?
+db.notes.all === all notes
+notebooks:all the notes in the notebook
+topics: all the notes in the topic
+tags: all notes in the tag
+dates:all notes between the dates
+notes: all notes with the word
+inTitle: all notes with the word in title
+the search will condense. The scope will decrease with the number of filters
+*/
+export const filterSearchEngine = async (definitions) => {
+ //name is cumbersome
+ let notes = [];
+ let searchlabels = [];
+ const organizingNotesFilters = ["notebook", "topic", "tag"];
+ let isOrganizingNotesFilter = false;
+ await db.notes.init();
+ console.log("search", definitions);
+ for (let definition of definitions) {
+ if (organizingNotesFilters.includes(definition.type))
+ isOrganizingNotesFilter = true;
+ }
+ for (let definition of definitions) {
+ switch (definition.type) {
+ case "notebook":
+ //isOrganizingNotesFilter = true; //this boolean should be done seperately
+ for (let topic of definition.topics) {
+ for (let note of topic.notes) {
+ if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+ }
+ }
+ break;
+ case "topic":
+ //isOrganizingNotesFilter = true;
+ for (let note of definition.notes) {
+ if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+ }
+ break;
+ case "tag":
+ //isOrganizingNotesFilter = true;
+ for (let note of definition.noteIds) {
+ if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+ }
+ break;
+ default:
+ searchlabels.push(definition);
+ }
+ }
+
+ // for (let definition of definitions) { //switch statements or consts
+ // if (definition.type === "notebook") {
+ // isHigherlabelPresent = true;
+ // for (let topic of definition.topics) {
+ // for (let note of topic.notes) {
+ // if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+ // }
+ // }
+ // } else if (definition.type === "topic") {
+ // isHigherlabelPresent = true;
+ // for (let note of definition.notes) {
+ // if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+ // }
+ // } else if (definition.type === "tag") {
+ // isHigherlabelPresent = true;
+ // for (let note of definition.noteIds) {
+ // if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+ // }
+ // } else if (
+ // definition.type === "before" ||
+ // definition.type === "after" ||
+ // definition.type === "during" ||
+ // definition.type === "notes" ||
+ // definition.type === "intitle"
+ // ) {
+ // searchlabels.push(definition);
+ // //notes = await dateSearch(definition.value, definition.type);
+ // }
+ // }
+
+ let result = [];
+ if (searchlabels.length > 0) {
+ for (let label of searchlabels) {
+ if (notes.length > 0) {
+ if (label.type === "notes") {
+ result = await db.lookup["notes"](notes, label.value);
+ } else if (label.type === "intitle") {
+ result = db.lookup["_byTitle"](notes, label.value);
+ } else {
+ result = await dateSearchEngine(label.value, label.type, notes);
+ }
+ } else if (!isOrganizingNotesFilter) {
+ let allNotes = db.notes.all;
+ if (label.type === "notes") {
+ let search = await db.lookup["notes"](allNotes, label.value);
+ result.push(...search);
+ } else if (label.type === "intitle") {
+ let search = await db.lookup["_byTitle"](allNotes, label.value);
+ result.push(...search);
+ } else {
+ result = await dateSearchEngine(label.value, label.type, allNotes);
+ }
+ }
+ }
+ return result;
+ } else {
+ return notes;
+ }
+};
+
+//definitions will first be sorted in following order: nbks> topics>tags>times>notes>intitle,
+// const someMethod = async (definition,notes) => {
+// await db.notes.init();
+// return {
+// notebook: () => {
+// for (let topic of definition.topics) { // noltebooks and topics will be sorted for search in one method
+// for (let note of topic.notes) {
+// if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+// }
+// }
+// },
+// topic: () => {
+// for (let note of definition.notes) {
+// if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+// }
+// },
+// tag: () => { //all the notes from tag will be retrived and then mathched with already present notes to sort out relevent notes
+// for (let note of definition.noteIds) {
+// if (db.notes.note(note)) notes.push(db.notes.note(note)._note);
+// }
+// },
+// before: () => {//all the notes within dates will be retrived but only those will be sorted out which match with already present notes
+// let _notes = notes === [] ? db.notes.all : notes;
+// for (let note of _notes) {
+// if (note.dateCreated < unixTime) {
+// result.push(note);
+// }
+// }
+// },
+// after: () => {},
+// during: () => {}
+// };
+// };
+
+export const dateSearchEngine = async (date, label, notes) => {
+ let unixTime = new Date(date).getTime();
+ let notesArc = notes ? notes : db.notes.all;
+ let result = [];
+
+ switch (label) {
+ case "before":
+ for (let note of notesArc) {
+ if (note.dateCreated < unixTime) {
+ result.push(note);
+ }
+ }
+ break;
+ case "after":
+ for (let note of notesArc) {
+ if (note.dateCreated > unixTime) {
+ result.push(note);
+ }
+ }
+ break;
+ case "during":
+ for (let note of notesArc) {
+ let dateCreated = new Date(note.dateCreated);
+ let noteDate = dateFormat(dateCreated).DDMMYY;
+ let selectedUnix = new Date(unixTime);
+ let selectedDate = dateFormat(selectedUnix).DDMMYY;
+ if (noteDate === selectedDate) {
+ result.push(note);
+ }
+ }
+ break;
+ }
+ return result;
+ // if (label === "before") {
+ // for (let note of notesArc) {
+ // if (note.dateCreated < unixTime) {
+ // result.push(note);
+ // }
+ // }
+ // } else if (label === "after") {
+ // for (let note of notesArc) {
+ // if (note.dateCreated > unixTime) {
+ // result.push(note);
+ // }
+ // }
+ // } else if (label === "during") {
+ // for (let note of notesArc) {
+ // let dateCreated = new Date(note.dateCreated);
+ // let noteDate =
+ // dateCreated.getDate() +
+ // "/" +
+ // dateCreated.getMonth() +
+ // "/" +
+ // dateCreated.getFullYear();
+ // let selectedUnix = new Date(unixTime);
+ // let selectedDate =
+ // selectedUnix.getDate() +
+ // "/" +
+ // selectedUnix.getMonth() +
+ // "/" +
+ // selectedUnix.getFullYear();
+ // if (noteDate === selectedDate) {
+ // result.push(note);
+ // }
+ // }
+ // }
+};
+
+const dateFormat = (date) => {
+ //this should be someplace else
+ return {
+ DDMMYY: date.getDate() + "/" + date.getMonth() + "/" + date.getFullYear()
+ };
+};
+
+export async function filterItemsToType(type, context) {
+ switch (type) {
+ case "notes":
+ await db.notes.init();
+ if (!context) return ["notes", db.notes.all];
+ const notes = context.notes;
+ return ["notes", notes];
+ case "notebooks":
+ return ["notebooks", db.notebooks.all];
+ case "topics":
+ const notebooks = db.notebooks.all;
+ if (!notebooks) return ["topics", []];
+ let topics = [];
+ for (let notebook of notebooks) {
+ let notebookTopics = db.notebooks.notebook(notebook.id).topics.all;
+ if (notebookTopics.length > 0) {
+ for (let notebookTopic of notebookTopics) {
+ topics.push(notebookTopic);
+ }
+ }
+ }
+ return ["topics", topics];
+ case "tags":
+ return ["tags", db.tags.all];
+ case "trash":
+ return ["trash", db.trash.all];
+ case "_byTitle":
+ await db.notes.init();
+ if (!context) return ["notes", db.notes.all];
+ let title = context.notes;
+ return ["_byTitle", title];
+ default:
+ return [];
+ }
+}
+
+export function filterItems(query, items) {
+ //naming is wrong
+ //this is fetch suggestions search
+ try {
+ return filter(
+ parse(`text:"${query.toLowerCase()}"`),
+ items.map((item) => {
+ return { item, text: item.query };
+ })
+ ).map((v) => {
+ return v.item.query;
+ });
+ } catch {
+ return [];
+ }
+}
+
+export const addToSearchHistory = async (value) => {
+ // const history = await db.searchHistory.getHistory();
+ // value = value.trim();
+ // let isValueAlreadyPresent = false;
+ // history.map((item) => {
+ // if (item.query === value) {
+ // isValueAlreadyPresent = true;
+ // }
+ // });
+ // if (isValueAlreadyPresent) {
+ // return;
+ // }
+ // await db.searchHistory.add(value);
+};
diff --git a/packages/ui/src/components/menu/suggestionrow.js b/packages/ui/src/components/menu/suggestionrow.js
new file mode 100644
index 000000000..dd7d34f35
--- /dev/null
+++ b/packages/ui/src/components/menu/suggestionrow.js
@@ -0,0 +1,56 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { Button, Text } from "@theme-ui/components";
+
+export function SuggestionRow(props) {
+ return (
+
+ );
+}