tasks-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 9b0d0f7b9a93
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone f6f2112e77bc8c011e38c83a
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/TableView.tsx
// The list view: every card as a row, sortable by list, title, due date or checklist.
import { useState } from "react";
import { useBoard } from "./lib/board";
import { matches, type Filter } from "./lib/filter";
import { DueBadge, initials } from "./CardTile";
import { COLORS, parseChecklist, type Card } from "../functions/lib/tasks";

type Sort = "list" | "title" | "due" | "checklist";

export function TableView({ filter, onOpen }: { filter: Filter; onOpen: (cardId: string) => void }) {
  const b = useBoard();
  const [sort, setSort] = useState<{ by: Sort; dir: 1 | -1 }>({ by: "list", dir: 1 });
  const listIndex = new Map(b.openLists.map((l, i) => [l.id, i]));
  const rows = b.cards.filter((c) => listIndex.has(c.listId) && matches(c, filter, b.labels));
  const progress = (c: Card) => {
    const items = parseChecklist(c.checklist);
    return items.length ? items.filter((x) => x.done).length / items.length : -1;
  };
  const cmp: Record<Sort, (a: Card, x: Card) => number> = {
    list: (a, x) => (listIndex.get(a.listId)! - listIndex.get(x.listId)!) || a.pos - x.pos,
    title: (a, x) => a.title.localeCompare(x.title),
    due: (a, x) => (a.due || "9999") < (x.due || "9999") ? -1 : (a.due || "9999") > (x.due || "9999") ? 1 : 0,
    checklist: (a, x) => progress(a) - progress(x),
  };
  rows.sort((a, x) => cmp[sort.by](a, x) * sort.dir || cmp.list(a, x));
  const head = (by: Sort, text: string) => (
    <th scope="col" aria-sort={sort.by === by ? (sort.dir === 1 ? "ascending" : "descending") : "none"}>
      <button type="button" className="sort-btn" onClick={() => setSort((s) => ({ by, dir: s.by === by ? (s.dir === 1 ? -1 : 1) : 1 }))}>
        {text}{sort.by === by ? (sort.dir === 1 ? " ↑" : " ↓") : ""}
      </button>
    </th>
  );
  if (!rows.length) return <div className="view-empty"><p className="muted">{b.cards.length ? "No cards match the filters." : "No cards yet. Add some on the board view."}</p></div>;
  return (
    <div className="table-view">
      <table className="table cards-table">
        <thead>
          <tr>{head("title", "Card")}{head("list", "List")}<th scope="col">Labels</th><th scope="col">Assigned</th>{head("due", "Due")}{head("checklist", "Checklist")}</tr>
        </thead>
        <tbody>
          {rows.map((c) => {
            const items = parseChecklist(c.checklist);
            const labels = b.cardLabels(c);
            const people = b.cardPeople(c);
            return (
              <tr key={c.id}>
                <td className="t-title">
                  <button type="button" className="row-open" onClick={() => onOpen(c.id)}>
                    {c.cover && <span className="dot" style={{ background: COLORS[c.cover]?.solid }} aria-hidden="true" />}{c.title}
                  </button>
                </td>
                <td className="t-list" data-label="List">{b.lists.find((l) => l.id === c.listId)?.title}</td>
                <td data-label="Labels">{labels.length > 0 && <span className="prop-chips">{labels.map((l) => <span key={l.id} className="lbl" style={{ background: COLORS[l.color]?.bg, color: COLORS[l.color]?.fg }}>{l.name || COLORS[l.color]?.name}</span>)}</span>}</td>
                <td data-label="Assigned">{people.length > 0 && <span className="prop-chips">{people.map((p) => <span key={p.id} className="person person-sm"><span className="avatar">{initials(p.name)}</span>{p.name}</span>)}</span>}</td>
                <td data-label="Due"><DueBadge card={c} /></td>
                <td data-label="Checklist">{items.length ? `${items.filter((x) => x.done).length}/${items.length}` : ""}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

Change history

Point OpenApps links to openapps.free
9b0d0f7b9a · 17 hours ago
Bind agent address in policy
25f84633c0 · 19 hours ago
Use OpenApps service URLs and add contribution strip
07519eb348 · 19 hours ago
Bounded source sync
3296872001 · 1 day ago
Bounded source sync
ac8d2f5d18 · 1 day ago
Bounded source sync
2a5b08a001 · 1 day ago
Bounded source sync
d2242ddf34 · 1 day ago