├── kanban.plug.yaml ├── .vscode └── settings.json ├── deno.lock ├── deno.jsonc ├── LICENSE ├── import_map.json ├── README.md ├── kanban.ts └── kanban.plug.js /kanban.plug.yaml: -------------------------------------------------------------------------------- 1 | name: kanban 2 | functions: 3 | kanbanWidget: 4 | path: ./kanban.ts:widget 5 | codeWidget: kanban 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "editor.formatOnSave": false, 4 | "deno.config": "deno.jsonc" 5 | } 6 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "remote": { 4 | "https://get.silverbullet.md/": "512935e63cd32dccbeb7c2332c40324916493252289a9ba0229a80253728c61a" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": "import_map.json", 3 | "tasks": { 4 | "build": "silverbullet plug:compile --importmap import_map.json kanban.plug.yaml", 5 | "watch": "silverbullet plug:compile -w --importmap import_map.json kanban.plug.yaml" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daniel Diment 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@silverbulletmd/silverbullet/": "../silverbullet/", 4 | "@silverbulletmd/silverbullet/syscall": "../silverbullet/plug-api/syscall.ts", 5 | "@silverbulletmd/silverbullet/syscalls": "../silverbullet/plug-api/syscalls.ts", 6 | "@silverbulletmd/silverbullet/types": "../silverbullet/plug-api/types.ts", 7 | "@silverbulletmd/silverbullet/lib/json": "../silverbullet/plug-api/lib/json.ts", 8 | "@silverbulletmd/silverbullet/lib/tree": "../silverbullet/plug-api/lib/tree.ts", 9 | "@silverbulletmd/silverbullet/lib/attribute": "../silverbullet/plug-api/lib/attribute.ts", 10 | "@silverbulletmd/silverbullet/lib/parse_query": "../silverbullet/plug-api/lib/parse_query.ts", 11 | "@silverbulletmd/silverbullet/lib/page_ref": "../silverbullet/plug-api/lib/page_ref.ts", 12 | "@silverbulletmd/silverbullet/lib/resolve": "../silverbullet/plug-api/lib/resolve.ts", 13 | "@silverbulletmd/silverbullet/lib/query_expression": "../silverbullet/plug-api/lib/query_expression.ts", 14 | "@silverbulletmd/silverbullet/lib/yaml_page": "../silverbullet/plug-api/lib/yaml_page.ts", 15 | "@silverbulletmd/silverbullet/lib/query": "../silverbullet/plug-api/lib/query.ts", 16 | "@silverbulletmd/silverbullet/lib/frontmatter": "../silverbullet/plug-api/lib/frontmatter.ts", 17 | "@silverbulletmd/silverbullet/lib/markdown": "../silverbullet/plug-api/lib/markdown.ts", 18 | "@silverbulletmd/silverbullet/lib/tags": "../silverbullet/plug-api/lib/tags.ts", 19 | "@silverbulletmd/silverbullet/type/config": "../silverbullet/type/config.ts", 20 | "@silverbulletmd/silverbullet/type/rpc": "../silverbullet/type/rpc.ts", 21 | "@silverbulletmd/silverbullet/type/client": "../silverbullet/type/client.ts", 22 | "$lib/":"../silverbullet/lib/", 23 | "handlebars": "https://esm.sh/handlebars", 24 | "yaml": "https://deno.land/std@0.184.0/yaml/mod.ts" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Silver Bullet kanban plug 3 | 4 | ## Installation 5 | Run the {[Plugs: Add]} command and paste in: `github:dandimrod/silverbullet-kanban/kanban.plug.js` 6 | 7 | That's all! 8 | 9 | ## Use 10 | Create some tasks: 11 | ```md 12 | * [TODO] help with things 📅 2022-11-26 #test [user:test] 13 | * [ON GOING] help 14 | ``` 15 | Create a kanban: 16 | 17 | ```kanban 18 | query: 19 | task where page = @page.name 20 | ``` 21 | 22 | And move your cursor outside of the block to live preview it! 23 | 24 | ## API 25 | 26 | ### query 27 | You can access different tasks through queries. These queries are the same as other live queries in silverbullet 28 | 29 | 30 | ```kanban 31 | query: 32 | task where page = @page.name 33 | ``` 34 | ### template 35 | You can change how the text gets displayed on the kanban by using a template. For example create a page called template with the contents: 36 | 37 | ``` 38 | {{name}} 39 | {{user}} 40 | ``` 41 | 42 | And with the kanban: 43 | 44 | ```kanban 45 | template: 46 | template 47 | query: 48 | task where page = @page.name 49 | ``` 50 | 51 | ### columns 52 | You can change the default columns like this (The columns will be displayed in that order and with the different text): 53 | ```kanban 54 | query: 55 | task where page = @page.name 56 | columns: 57 | - id: TODO 58 | title: ToDo 59 | - id: ON GOING 60 | title: On Going 61 | - id: DONE 62 | title: Done 63 | ``` 64 | ### options: 65 | 66 | You can change any of the options as shown in the [jkanban documentation](https://github.com/riktar/jkanban?tab=readme-ov-file#usage) by using the option parameter: 67 | 68 | ```kanban 69 | query: 70 | task where page = @page.name 71 | options: 72 | dragItems: false 73 | dragTables: false 74 | ``` 75 | 76 | **Note:** [jkanban](https://github.com/riktar/jkanban) itself is not bundled with this plug, it pulls the JavaScript, CSS and fonts from the JSDelivr CDN. 77 | 78 | ## Build 79 | Assuming you have Deno and Silver Bullet installed, and you have silverbullet on a sibling folder to this plugin simply build using: 80 | 81 | ```shell 82 | deno task build 83 | ``` 84 | 85 | Or to watch for changes and rebuild automatically 86 | 87 | ```shell 88 | deno task watch 89 | ``` 90 | 91 | Then, load the locally built plug, add it to your `PLUGS` note with an absolute path, for instance: 92 | 93 | ``` 94 | - file:/Users/you/path/to/kanban.plug.json 95 | ``` 96 | 97 | And run the `Plugs: Update` command in SilverBullet. 98 | 99 | ## TODO 100 | 101 | Update, edit and add tasks directly from the kanban. 102 | Improve types -------------------------------------------------------------------------------- /kanban.ts: -------------------------------------------------------------------------------- 1 | import {parseQuery} from "../silverbullet/plug-api/lib/parse_query.ts"; 2 | import { YAML, system, markdown } from '../silverbullet/plug-api/syscalls.ts' 3 | import { CodeWidgetContent } from "../silverbullet/plug-api/types.ts"; 4 | import { loadPageObject } from "../silverbullet/plugs/template/page.ts"; 5 | 6 | export async function widget( 7 | bodyText: string, 8 | pageName: string 9 | ): Promise { 10 | const config = await system.getSpaceConfig(); 11 | const pageObject = await loadPageObject(pageName); 12 | try { 13 | const kanbanConfig:any = await YAML.parse(bodyText); 14 | const query = await parseQuery(kanbanConfig.query); 15 | const results = await system.invokeFunction( 16 | "query.renderQuery", 17 | query, 18 | { 19 | page: pageObject, 20 | config, 21 | }, 22 | ); 23 | let templates = {}; 24 | if(kanbanConfig.template){ 25 | for (const item of results) { 26 | const query = await parseQuery(`task where ref = '${item.ref}' render [[${kanbanConfig.template}]]`) 27 | const results = await system.invokeFunction( 28 | "query.renderQuery", 29 | query, 30 | { 31 | page: pageObject, 32 | config, 33 | }, 34 | ); 35 | const tree = await markdown.parseMarkdown(results); 36 | const data = await markdown.renderParseTree(tree); 37 | templates[item.ref]=data; 38 | } 39 | } 40 | return { 41 | html: ` 42 | 43 | 69 |
`, 70 | script: ` 71 | loadJsByUrl("https://cdn.jsdelivr.net/npm/jkanban@1.3.1/dist/jkanban.min.js").then(() => { 72 | const data = ${JSON.stringify(mapResultsToBoards(results, kanbanConfig.columns, templates))}; 73 | const kanban = new jKanban({ 74 | element: '#kanban', 75 | boards:data, 76 | ...${JSON.stringify(kanbanConfig.options||{})} 77 | }); 78 | 79 | updateHeight(); 80 | }); 81 | ` 82 | }; 83 | } catch (e: any) { 84 | return { markdown: `**Error:** ${e.message}` }; 85 | } 86 | } 87 | 88 | export function mapResultsToBoards(results: any, columns: any, templates: any) { 89 | let finalColums = columns; 90 | if(!columns){ 91 | finalColums = results.reduce((arr, val)=>{ 92 | if(val.state && !arr.find(a=>a.title === val.state)){ 93 | arr.push({title:val.state}); 94 | } 95 | return arr; 96 | }, []) 97 | } 98 | return finalColums.map(column=>({ 99 | id:column.id ?? column.title, 100 | title: column.title, 101 | class: column.class, 102 | item: results.filter(i=>i.state === (column.id ?? column.title)).map((item)=>({ 103 | id:item.ref, 104 | title: templates[item.ref] ?? item.text 105 | })) 106 | })); 107 | } -------------------------------------------------------------------------------- /kanban.plug.js: -------------------------------------------------------------------------------- 1 | var V=Object.defineProperty;var g=(e,t)=>{for(var r in t)V(e,r,{get:t[r],enumerable:!0})};var v=typeof window>"u"&&typeof globalThis.WebSocketPair>"u";typeof Deno>"u"&&(self.Deno={args:[],build:{arch:"x86_64"},env:{get(){}}});var b=new Map,T=0;function y(e){self.postMessage(e)}v&&(globalThis.syscall=async(e,...t)=>await new Promise((r,n)=>{T++,b.set(T,{resolve:r,reject:n}),y({type:"sys",id:T,name:e,args:t})}));function k(e,t){v&&(self.addEventListener("message",r=>{(async()=>{let n=r.data;switch(n.type){case"inv":{let o=e[n.name];if(!o)throw new Error(`Function not loaded: ${n.name}`);try{let i=await Promise.resolve(o(...n.args||[]));y({type:"invr",id:n.id,result:i})}catch(i){console.error("An exception was thrown as a result of invoking function",n.name,"error:",i.message),y({type:"invr",id:n.id,error:i.message})}}break;case"sysr":{let o=n.id,i=b.get(o);if(!i)throw Error("Invalid request id");b.delete(o),n.error?i.reject(new Error(n.error)):i.resolve(n.result)}break}})().catch(console.error)}),y({type:"manifest",manifest:t}))}function N(e){let t=atob(e),r=t.length,n=new Uint8Array(r);for(let o=0;o0?M(r):void 0;t={method:e.method,headers:Object.fromEntries(e.headers.entries()),base64Body:n},e=e.url}return syscall("sandboxFetch.fetch",e,t)}globalThis.nativeFetch=globalThis.fetch;function I(){globalThis.fetch=async function(e,t){let r=t&&t.body?M(new Uint8Array(await new Response(t.body).arrayBuffer())):void 0,n=await W(e,t&&{method:t.method,headers:t.headers,base64Body:r});return new Response(n.base64Body?N(n.base64Body):null,{status:n.status,headers:n.headers})}}v&&I();function $(e,t){return C(e,r=>r.type===t)}function C(e,t){if(t(e))return[e];let r=[];if(e.children)for(let n of e.children)r=[...r,...C(n,t)];return r}function x(e){if(!e)return"";let t=[];if(e.text!==void 0)return e.text;for(let r of e.children)t.push(x(r));return t.join("")}function w(e,t=!0){if($(e,"\u26A0").length>0)throw new Error(`Parse error in: ${x(e)}`);if(e.text!==void 0)return e.text;let n=[e.type];for(let o of e.children)o.type&&!o.type.endsWith("Mark")&&n.push(w(o,t)),o.text&&(t&&o.text.trim()||!t)&&n.push(o.text);return n}typeof self>"u"&&(self={syscall:()=>{throw new Error("Not implemented here")}});function s(e,...t){return globalThis.syscall(e,...t)}var p={};g(p,{parseMarkdown:()=>_,renderParseTree:()=>Y});function _(e){return s("markdown.parseMarkdown",e)}function Y(e){return s("markdown.renderParseTree",e)}var f={};g(f,{applyAttributeExtractors:()=>Z,getEnv:()=>ne,getMode:()=>oe,getSpaceConfig:()=>ee,getVersion:()=>ie,invokeCommand:()=>J,invokeFunction:()=>H,invokeSpaceFunction:()=>X,listCommands:()=>z,listSyscalls:()=>G,reloadConfig:()=>re,reloadPlugs:()=>te});function H(e,...t){return s("system.invokeFunction",e,...t)}function J(e,t){return s("system.invokeCommand",e,t)}function z(){return s("system.listCommands")}function G(){return s("system.listSyscalls")}function X(e,...t){return s("system.invokeSpaceFunction",e,...t)}function Z(e,t,r){return s("system.applyAttributeExtractors",e,t,r)}async function ee(e,t){return await s("system.getSpaceConfig",e)??t}function te(){return s("system.reloadPlugs")}function re(){return s("system.reloadConfig")}function ne(){return s("system.getEnv")}function oe(){return s("system.getMode")}function ie(){return s("system.getVersion")}var P={};g(P,{listLanguages:()=>ue,parseLanguage:()=>le});function le(e,t){return s("language.parseLanguage",e,t)}function ue(){return s("language.listLanguages")}var d={};g(d,{parse:()=>ge,stringify:()=>ye});function ge(e){return s("yaml.parse",e)}function ye(e){return s("yaml.stringify",e)}function E(e){let t={querySource:""},[r,n,...o]=e;if(r!=="Query")throw new Error(`Expected query type, got ${r}`);t.querySource=n[1];for(let i of o){let[l]=i;switch(l){case"WhereClause":{t.filter?t.filter=["and",t.filter,a(i[2])]:t.filter=a(i[2]);break}case"OrderClause":{t.orderBy||(t.orderBy=[]);for(let c of i.slice(2))if(c[0]==="OrderBy"){let u=c[1][1];c[2]?t.orderBy.push({expr:a(u),desc:c[2][1][1]==="desc"}):t.orderBy.push({expr:a(u),desc:!1})}break}case"LimitClause":{t.limit=a(i[2][1]);break}case"SelectClause":{for(let c of i.slice(2))c[0]==="Select"&&(t.select||(t.select=[]),c.length===2?t.select.push({name:m(c[1][1])}):t.select.push({name:m(c[3][1]),expr:a(c[1])}));break}case"RenderClause":{let c=i.find(u=>u[0]==="PageRef");t.render=c[1].slice(2,-2),t.renderAll=!!i.find(u=>u[0]==="all");break}default:throw new Error(`Unknown clause type: ${l}`)}}return t}function m(e){return e.startsWith("`")&&e.endsWith("`")?e.slice(1,-1):e}function a(e){if(["LVal","Expression","Value"].includes(e[0]))return a(e[1]);switch(e[0]){case"Attribute":return["attr",a(e[1]),m(e[3][1])];case"Identifier":return["attr",m(e[1])];case"String":return["string",e[1].slice(1,-1)];case"Number":return["number",+e[1]];case"Bool":return["boolean",e[1][1]==="true"];case"null":return["null"];case"Regex":return["regexp",e[1].slice(1,-1),"i"];case"List":{let t=[];for(let r of e.slice(2))r[0]==="Expression"&&t.push(r);return["array",t.map(a)]}case"Object":{let t=[];for(let r of e.slice(2)){if(typeof r=="string")continue;let[n,o,i,l]=r;t.push([o[1].slice(1,-1),a(l)])}return["object",t]}case"BinExpression":{let t=a(e[1]),r=e[2][0]==="InKW"?"in":e[2].trim(),n=a(e[3]);return[r,t,n]}case"LogicalExpression":{let t=a(e[1]),r=e[2],n=a(e[3]);return[r[1],t,n]}case"ParenthesizedExpression":return a(e[2]);case"Call":{let t=m(e[1][1]),r=[];for(let n of e.slice(2))n[0]==="Expression"&&r.push(n);return["call",t,r.map(a)]}case"UnaryExpression":{if(e[1][0]==="NotKW"||e[1][0]==="!")return["not",a(e[2])];if(e[1][0]==="-")return["-",a(e[2])];throw new Error(`Unknown unary expression: ${e[1][0]}`)}case"TopLevelVal":return["attr"];case"GlobalIdentifier":return["global",e[1].substring(1)];case"TernaryExpression":{let[t,r,n,o,i,l]=e;return["?",a(r),a(o),a(l)]}case"QueryExpression":return["query",E(e[2])];case"PageRef":return["pageref",e[1].slice(2,-2)];default:throw new Error(`Not supported: ${e[0]}`)}}async function A(e){let t=w(await P.parseLanguage("query",e));return E(t[1])}var h=class{constructor(t,r={}){this.maxSize=t;this.map=new Map(Object.entries(r))}map;set(t,r,n){let o={value:r,la:Date.now()};if(n){let i=this.map.get(t);i?.expTimer&&clearTimeout(i.expTimer),o.expTimer=setTimeout(()=>{this.map.delete(t)},n)}if(this.map.size>=this.maxSize){let i=this.getOldestKey();this.map.delete(i)}this.map.set(t,o)}get(t){let r=this.map.get(t);if(r)return r.la=Date.now(),r.value}remove(t){this.map.delete(t)}toJSON(){return Object.fromEntries(this.map.entries())}getOldestKey(){let t,r;for(let[n,o]of this.map.entries())(!r||o.la 3 | 29 |
`,script:` 30 | loadJsByUrl("https://cdn.jsdelivr.net/npm/jkanban@1.3.1/dist/jkanban.min.js").then(() => { 31 | const data = ${JSON.stringify(Te(l,o.columns,c))}; 32 | const kanban = new jKanban({ 33 | element: '#kanban', 34 | boards:data, 35 | ...${JSON.stringify(o.options||{})} 36 | }); 37 | 38 | updateHeight(); 39 | }); 40 | `}}catch(o){return{markdown:`**Error:** ${o.message}`}}}function Te(e,t,r){let n=t;return t||(n=e.reduce((o,i)=>(i.state&&!o.find(l=>l.title===i.state)&&o.push({title:i.state}),o),[])),n.map(o=>({id:o.id??o.title,title:o.title,class:o.class,item:e.filter(i=>i.state===(o.id??o.title)).map(i=>({id:i.ref,title:r[i.ref]??i.text}))}))}var U={kanbanWidget:R},q={name:"kanban",functions:{kanbanWidget:{path:"./kanban.ts:widget",codeWidget:"kanban"}},assets:{}},Ot={manifest:q,functionMapping:U};k(U,q);export{Ot as plug}; 41 | --------------------------------------------------------------------------------