feat: parse multiple commands
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!
This commit is contained in:
parent
791cd547ae
commit
f24e56159a
|
@ -40,15 +40,18 @@ export const columns: ColumnDef<Todo>[] = [
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
<Checkbox
|
<div className="flex items-center gap-2">
|
||||||
checked={
|
<p>0</p>
|
||||||
table.getIsAllPageRowsSelected() ||
|
<Checkbox
|
||||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
checked={
|
||||||
}
|
table.getIsAllPageRowsSelected() ||
|
||||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
aria-label="Select all"
|
}
|
||||||
className="translate-y-[2px]"
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
/>
|
aria-label="Select all"
|
||||||
|
className="translate-y-[2px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
cell: ({ row, table }) => (
|
cell: ({ row, table }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -99,7 +102,7 @@ export const columns: ColumnDef<Todo>[] = [
|
||||||
]
|
]
|
||||||
|
|
||||||
function App({ ctx }) {
|
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 [newText, setNewText] = useState("");
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
|
|
||||||
|
@ -118,15 +121,70 @@ function App({ ctx }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseTodos = (e) => {
|
const parseTodos = (e) => {
|
||||||
if (e.target.value.trim() !== "") {
|
// On enter execute the command
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
console.log("enter confirmed")
|
||||||
try {
|
try {
|
||||||
const parsed = commandParser.parse(e.target.value);
|
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) {
|
if (parsed.filters && parsed.filters.length > 0) {
|
||||||
const idFilters = parsed.filters.filter(f => f.type === "id");
|
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
|
// Create new selection state
|
||||||
const newSelection = {};
|
const newSelection = {};
|
||||||
idFilters.forEach(filter => {
|
idFilters.forEach(filter => {
|
||||||
|
|
|
@ -4,24 +4,43 @@
|
||||||
const validParts = parts || [];
|
const validParts = parts || [];
|
||||||
// Extract pure description (without attributes)
|
// Extract pure description (without attributes)
|
||||||
const description = validParts
|
const description = validParts
|
||||||
.filter(part => typeof part === 'string')
|
.filter(part => part.type === 'text')
|
||||||
|
.map(part => part.value)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Collect all attributes
|
// Collect all attributes
|
||||||
const attributes = validParts
|
const attributes = validParts
|
||||||
.filter(part => typeof part === 'object')
|
.filter(part => part.type !== 'text')
|
||||||
.filter(part => part !== null);
|
.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
|
// Store original parts for reconstruction
|
||||||
const originalParts = validParts.filter(part => part !== null);
|
const originalParts = validParts.filter(part => part !== null);
|
||||||
|
let validFilters;
|
||||||
|
if (!!filters) {
|
||||||
|
validFilters = filters.flat();
|
||||||
|
} else {
|
||||||
|
validFilters = [];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: type,
|
type: type,
|
||||||
filters: filters.flat() || [],
|
|
||||||
description: description || [],
|
description: description || [],
|
||||||
attributes: attributes || [],
|
attributes: attributes || [],
|
||||||
|
filters: validFilters,
|
||||||
|
project: project,
|
||||||
|
tags: JSON.stringify(tags),
|
||||||
parts: parts || [],
|
parts: parts || [],
|
||||||
reconstruct: function() {
|
reconstruct: function() {
|
||||||
const filterStr = this.filters.map(f => f.reconstruct()).join(',');
|
const filterStr = this.filters.map(f => f.reconstruct()).join(',');
|
||||||
|
@ -71,7 +90,7 @@ SingleId = id:Integer {
|
||||||
|
|
||||||
Filter = IdFilter / Tag / Project / Priority / Due
|
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();
|
const ids = [first, ...rest.map(r => r[1])].flat();
|
||||||
return {
|
return {
|
||||||
type: 'id',
|
type: 'id',
|
||||||
|
|
|
@ -1219,24 +1219,43 @@ function peg$parse(input, options) {
|
||||||
const validParts = parts || [];
|
const validParts = parts || [];
|
||||||
// Extract pure description (without attributes)
|
// Extract pure description (without attributes)
|
||||||
const description = validParts
|
const description = validParts
|
||||||
.filter(part => typeof part === 'string')
|
.filter(part => part.type === 'text')
|
||||||
|
.map(part => part.value)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// Collect all attributes
|
// Collect all attributes
|
||||||
const attributes = validParts
|
const attributes = validParts
|
||||||
.filter(part => typeof part === 'object')
|
.filter(part => part.type !== 'text')
|
||||||
.filter(part => part !== null);
|
.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
|
// Store original parts for reconstruction
|
||||||
const originalParts = validParts.filter(part => part !== null);
|
const originalParts = validParts.filter(part => part !== null);
|
||||||
|
let validFilters;
|
||||||
|
if (!!filters) {
|
||||||
|
validFilters = filters.flat();
|
||||||
|
} else {
|
||||||
|
validFilters = [];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: type,
|
type: type,
|
||||||
filters: filters.flat() || [],
|
|
||||||
description: description || [],
|
description: description || [],
|
||||||
attributes: attributes || [],
|
attributes: attributes || [],
|
||||||
|
filters: validFilters,
|
||||||
|
project: project,
|
||||||
|
tags: JSON.stringify(tags),
|
||||||
parts: parts || [],
|
parts: parts || [],
|
||||||
reconstruct: function() {
|
reconstruct: function() {
|
||||||
const filterStr = this.filters.map(f => f.reconstruct()).join(',');
|
const filterStr = this.filters.map(f => f.reconstruct()).join(',');
|
||||||
|
|
Loading…
Reference in a new issue