<%* // ---- Prompts ---- const rawName = await tp.system.prompt("Project name", "", true); const name = rawName.trim(); if (!name) { new Notice("Name is required."); throw new Error("Empty name."); } const status = (await tp.system.suggester( ["Active", "On hold", "Done"], ["active", "on hold", "done"], false, "Status (default: active)" )) || "active"; // ---- Helper: multi-select loop ---- async function pickMultiple(folderPaths, label) { const allItems = app.vault.getMarkdownFiles() .filter(f => folderPaths.some(p => f.path.startsWith(p))) .sort((a, b) => a.basename.localeCompare(b.basename)); if (allItems.length === 0) return []; const collected = []; while (true) { const remaining = allItems.filter(i => !collected.includes(i)); if (remaining.length === 0) break; const display = ["✓ Done", ...remaining.map(i => `${i.basename} (${i.parent.name})`)]; const items = [null, ...remaining]; const placeholder = collected.length === 0 ? `Link ${label} (pick Done to skip)` : `${collected.length} linked — add another or Done`; const choice = await tp.system.suggester(display, items, false, placeholder); if (!choice) break; collected.push(choice); } return collected; } const people = await pickMultiple(["People/"], "people"); const orgs = await pickMultiple([ "Organizations/Universities/", "Organizations/Colleges/", "Organizations/Companies/" ], "organizations"); const depts = await pickMultiple(["Organizations/Departments/"], "departments"); // ---- Build YAML lists ---- const yamlList = (items) => items.length === 0 ? "" : "\n" + items.map(i => ` - "[[${i.basename}]]"`).join("\n"); // ---- Move to Projects/ and emit frontmatter ---- await tp.file.move(`Projects/${name}`); tR += `--- type: project status: ${status} people:${yamlList(people)} organizations:${yamlList(orgs)} departments:${yamlList(depts)} ---`; -%> ## Overview ## Linked People ```dataview TABLE WITHOUT ID file.link AS "Person", role AS "Role", email AS "Email", phone AS "Phone" FROM "People" WHERE contains(this.people, file.link) SORT file.name ASC ``` ## Linked Organizations ```dataview TABLE WITHOUT ID file.link AS "Organization", default(org_type, type) AS "Kind", email AS "Email", phone AS "Phone" FROM "Organizations" WHERE contains(this.organizations, file.link) OR contains(this.departments, file.link) SORT type ASC, file.name ASC ``` ## Actions ```dataviewjs const btn = dv.el("button", "+ New task from this note"); btn.addEventListener("click", async () => { window._pendingTaskSource = dv.current().file.name; const templater = app.plugins.plugins["templater-obsidian"]; if (!templater) { new Notice("Templater plugin not enabled"); return; } const taskTpl = app.vault.getAbstractFileByPath("Templates/Task.md"); if (!taskTpl) { new Notice("Templates/Task.md not found"); return; } await templater.templater.create_new_note_from_template(taskTpl, "", "", true); }); ``` ## Open tasks ```dataviewjs const dim = "projects"; const tasks = dv.pages('"Activities/Tasks"') .where(p => { if (p.status !== "open") return false; const val = p[dim]; if (!val) return false; const arr = Array.isArray(val) ? val : [val]; return arr.some(d => d && d.path === dv.current().file.path); }) .sort(p => p.due, "asc"); const rows = tasks.map(p => { const cb = document.createElement("input"); cb.type = "checkbox"; cb.addEventListener("click", async () => { const file = app.vault.getAbstractFileByPath(p.file.path); if (!file) return; const content = await app.vault.read(file); const today = new Date().toISOString().slice(0, 10); let updated = content.replace(/^status:\s*["']?open["']?\s*$/m, "status: done"); if (/^closed:/m.test(updated)) { updated = updated.replace(/^closed:.*$/m, `closed: ${today}`); } else { updated = updated.replace(/^(---\n[\s\S]*?\n)---\n/, `$1closed: ${today}\n---\n`); } if (updated !== content) await app.vault.modify(file, updated); }); return [cb, p.file.link, p.due, p.date]; }); dv.table(["Done", "Task", "Due", "Created"], rows); ``` ## Closed tasks ```dataviewjs const dim = "projects"; const tasks = dv.pages('"Activities/Tasks"') .where(p => { if (p.status !== "done") return false; const val = p[dim]; if (!val) return false; const arr = Array.isArray(val) ? val : [val]; return arr.some(d => d && d.path === dv.current().file.path); }) .sort(p => p.closed, "desc"); const rows = tasks.map(p => { const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = true; cb.addEventListener("click", async () => { const file = app.vault.getAbstractFileByPath(p.file.path); if (!file) return; const content = await app.vault.read(file); let updated = content.replace(/^status:\s*["']?done["']?\s*$/m, "status: open"); updated = updated.replace(/^closed:.*$/m, "closed: "); if (updated !== content) await app.vault.modify(file, updated); }); return [cb, p.file.link, p.closed, p.due]; }); dv.table(["Reopen", "Task", "Closed", "Due"], rows); ``` ## Timeline ```dataviewjs const dim = "projects"; const all = dv.pages('"Activities"') .where(p => { const v = p[dim]; if (!v) return false; const arr = Array.isArray(v) ? v : [v]; return arr.some(l => l && l.path === dv.current().file.path); }) .sort(p => p.date, "desc"); // ===== Controls ===== const controls = dv.el("div", "", { attr: { style: "display: flex; gap: 1em; align-items: center; margin-bottom: 0.75em; flex-wrap: wrap;" } }); const typeWrap = controls.createEl("label", { attr: { style: "display: inline-flex; align-items: center; gap: 0.4em;" } }); typeWrap.createSpan({ text: "Type:" }); const typeSelect = typeWrap.createEl("select"); const typeOptions = ["All", ...[...new Set(all.map(p => p.type).filter(Boolean))].sort()]; for (const t of typeOptions) typeSelect.createEl("option", { text: t, value: t }); const rangeWrap = controls.createEl("label", { attr: { style: "display: inline-flex; align-items: center; gap: 0.4em;" } }); rangeWrap.createSpan({ text: "Range:" }); const rangeSelect = rangeWrap.createEl("select"); const ranges = [ { label: "All time", value: "all" }, { label: "Last 7 days", value: "7" }, { label: "Last 30 days", value: "30" }, { label: "Last 90 days", value: "90" }, { label: "Last 365 days", value: "365" }, ]; for (const r of ranges) rangeSelect.createEl("option", { text: r.label, value: r.value }); const tableContainer = dv.el("div", ""); // ===== Helpers ===== const dateISO = (p) => { const d = p.date; if (!d) return null; if (d.toISODate) return d.toISODate(); if (typeof d === "string") return d.slice(0, 10); return null; }; const localISO = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; const makeWikiLink = (file, label) => { const a = document.createElement("a"); a.className = "internal-link"; a.setAttribute("data-href", file.path); a.setAttribute("href", file.path); a.textContent = label || file.name; a.addEventListener("click", (e) => { e.preventDefault(); app.workspace.openLinkText(file.path, "", e.ctrlKey || e.metaKey); }); return a; }; // ===== Render ===== const render = () => { tableContainer.empty(); const typeFilter = typeSelect.value; const rangeFilter = rangeSelect.value; let items = [...all]; if (typeFilter !== "All") { items = items.filter(p => p.type === typeFilter); } if (rangeFilter !== "all") { const days = parseInt(rangeFilter, 10); const cutoff = new Date(); cutoff.setHours(0, 0, 0, 0); cutoff.setDate(cutoff.getDate() - days); const cutoffStr = localISO(cutoff); items = items.filter(p => { const d = dateISO(p); return d && d >= cutoffStr; }); } if (items.length === 0) { tableContainer.createEl("p", { text: "No activities match the current filters.", attr: { style: "color: var(--text-muted); font-style: italic; margin-top: 0.5em;" } }); return; } const table = tableContainer.createEl("table", { cls: "dataview" }); const thead = table.createEl("thead"); const hdr = thead.createEl("tr"); for (const h of ["Note", "Type", "Date", "Subject"]) hdr.createEl("th", { text: h }); const tbody = table.createEl("tbody"); for (const item of items) { const tr = tbody.createEl("tr"); const c1 = tr.createEl("td"); c1.appendChild(makeWikiLink(item.file)); tr.createEl("td", { text: item.type || "" }); tr.createEl("td", { text: String(item.date || "") }); const subject = item.subject || item.topic || item.file.name; tr.createEl("td", { text: String(subject) }); } }; typeSelect.addEventListener("change", render); rangeSelect.addEventListener("change", render); render(); ```