From f24e56159a1c4994508e965367408b2ee9a95099 Mon Sep 17 00:00:00 2001 From: Anish Lakhwara Date: Sun, 10 Nov 2024 18:15:20 -0800 Subject: [PATCH] feat: parse multiple commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit basically at MVP now the react app can now add, complete and reactively filter tasks The filter command doesn't have a enter action, it should probably do something. maybe I do need the default to be select, instead of filter, and filter should _actually filter_. Syncing is next. I'll need to look at the vcln starter template to understand how they set up, and then maintain different databases for each user. What needs to be done is setting up rooms, and the backend such that everybody gets their own lil sql database, and the syncer can identify them. we don't really have any notion of security, which is uhhh, not great. After that we can look at some malleable features that lend themself to discoverability. ideally, I don't need to have a tutorial at all. reactivity needs to exist for almost every command. I want to save the command, to a dirty state sql table, and then restore it on load. that should cause all the elements to re-render the proposed change. this actually unlocks a thought for me: executing filters, sets them as a "context". a context makes up a view. reports are literally just saved contexts then. this allows the tanstack table to handle virtual ids for us! Then, for the malleable input, all we need some kind of triple scroll wheel that can be typed, for each of the three segments. ideally with desired autocomplete 🤔 i think i'm reimagining the CLI for the web almost! --- mast-react-vite/src/App.tsx | 84 +++++++++++++++++++++++++++++++------ parser/command_js.peg | 29 ++++++++++--- parser/command_parser.js | 27 ++++++++++-- 3 files changed, 118 insertions(+), 22 deletions(-) diff --git a/mast-react-vite/src/App.tsx b/mast-react-vite/src/App.tsx index e30b1a2..7f5d895 100644 --- a/mast-react-vite/src/App.tsx +++ b/mast-react-vite/src/App.tsx @@ -40,15 +40,18 @@ export const columns: ColumnDef[] = [ { id: "select", header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="translate-y-[2px]" - /> +
+

0

+ table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-[2px]" + /> +
), cell: ({ row, table }) => (
@@ -99,7 +102,7 @@ export const columns: ColumnDef[] = [ ] function App({ ctx }) { - const todos = useQuery(ctx, "SELECT * FROM todos").data; + const todos = useQuery(ctx, "SELECT * FROM todos where completed = 0").data; const [newText, setNewText] = useState(""); const [rowSelection, setRowSelection] = useState({}); @@ -118,15 +121,70 @@ function App({ ctx }) { } const parseTodos = (e) => { - if (e.target.value.trim() !== "") { + // On enter execute the command + if (e.key === "Enter") { + console.log("enter confirmed") try { const parsed = commandParser.parse(e.target.value); + // TODO: + // We need to identify the type of the command first + // Currently assumes that it is done + switch (parsed.type) { + case "done": + if (parsed.filters && parsed.filters.length > 0) { + const idFilters = parsed.filters.filter(f => f.type === "id"); + + // Get the row IDs from the table + const rowIds = idFilters.flatMap(filter => + filter.ids.map(id => todos[id - 1]?.id) + ).filter(Boolean); + + // Execute SQL update for each row + if (rowIds.length > 0) { + rowIds.forEach(id => { + ctx.db.exec( + `UPDATE todos SET completed = 1 WHERE id = ?`, + [id] + ); + }); + } + + setNewText(""); + setRowSelection({}); + } + case "add": + ctx.db.exec(`INSERT INTO todos (id, description, tags, project, completed) + VALUES (lower(hex(randomblob(16))), ?, ?, ?, 0)`, + [ + parsed.description, + parsed.tags, + parsed.project + ]); + setNewText(""); + } + } catch (error) { + // TODO: + // This is actually bad + // We want to throw an error to the user here + console.log("Unable to parse field"); + return; + } + } + // React to key presses for selection + else if (e.target.value.trim() !== "") { + try { + const parsed = commandParser.parse(e.target.value); + // TODO: + // We want to reactively update based on command still + // Right now this assumes the command is filter - console.log(parsed) if (parsed.filters && parsed.filters.length > 0) { const idFilters = parsed.filters.filter(f => f.type === "id"); - console.log(idFilters) + // TODO: + // We should probably have selection state react to changes on NewText + // Rather than manually updating here + // Create new selection state const newSelection = {}; idFilters.forEach(filter => { diff --git a/parser/command_js.peg b/parser/command_js.peg index 59655ff..614a2b4 100644 --- a/parser/command_js.peg +++ b/parser/command_js.peg @@ -4,24 +4,43 @@ const validParts = parts || []; // Extract pure description (without attributes) const description = validParts - .filter(part => typeof part === 'string') + .filter(part => part.type === 'text') + .map(part => part.value) .join(' ') .trim(); // Collect all attributes const attributes = validParts - .filter(part => typeof part === 'object') + .filter(part => part.type !== 'text') .filter(part => part !== null); + + const project = validParts + .filter(part => part.type === 'project') + .map(part => part.value) + .join('') + .trim(); + + const tags = validParts + .filter(part => part.type === 'tag') + .map(part => part.value); + // Store original parts for reconstruction const originalParts = validParts.filter(part => part !== null); - + let validFilters; + if (!!filters) { + validFilters = filters.flat(); + } else { + validFilters = []; + } return { type: type, - filters: filters.flat() || [], description: description || [], attributes: attributes || [], + filters: validFilters, + project: project, + tags: JSON.stringify(tags), parts: parts || [], reconstruct: function() { const filterStr = this.filters.map(f => f.reconstruct()).join(','); @@ -71,7 +90,7 @@ SingleId = id:Integer { Filter = IdFilter / Tag / Project / Priority / Due -IdFilter = first:(IdRange / SingleId) rest:("," (IdRange / SingleId))* trailing:"," ? { +IdFilter = first:(IdRange / SingleId) rest:("," (IdRange / SingleId))* trailing:"," ? { const ids = [first, ...rest.map(r => r[1])].flat(); return { type: 'id', diff --git a/parser/command_parser.js b/parser/command_parser.js index b8e04b3..bb19189 100644 --- a/parser/command_parser.js +++ b/parser/command_parser.js @@ -1219,24 +1219,43 @@ function peg$parse(input, options) { const validParts = parts || []; // Extract pure description (without attributes) const description = validParts - .filter(part => typeof part === 'string') + .filter(part => part.type === 'text') + .map(part => part.value) .join(' ') .trim(); // Collect all attributes const attributes = validParts - .filter(part => typeof part === 'object') + .filter(part => part.type !== 'text') .filter(part => part !== null); + + const project = validParts + .filter(part => part.type === 'project') + .map(part => part.value) + .join('') + .trim(); + + const tags = validParts + .filter(part => part.type === 'tag') + .map(part => part.value); + // Store original parts for reconstruction const originalParts = validParts.filter(part => part !== null); - + let validFilters; + if (!!filters) { + validFilters = filters.flat(); + } else { + validFilters = []; + } return { type: type, - filters: filters.flat() || [], description: description || [], attributes: attributes || [], + filters: validFilters, + project: project, + tags: JSON.stringify(tags), parts: parts || [], reconstruct: function() { const filterStr = this.filters.map(f => f.reconstruct()).join(',');