This commit is contained in:
Lauri Ojansivu 2025-10-14 11:56:11 +03:00
parent 06a5a8f70d
commit 6592102e8f
11 changed files with 789 additions and 43 deletions

View file

@ -19,7 +19,7 @@ Fixing other platforms In Progress.
[Upgrade WeKan](https://wekan.fi/upgrade/)
# Upcoming WeKan ® release
# v8.02 2025-10-14 WeKan ® release
This release adds the following new features:

View file

@ -249,9 +249,9 @@ cd /home/wekan/app
# Remove legacy webbroser bundle, so that Wekan works also at Android Firefox, iOS Safari, etc.
#rm -rf /home/wekan/app_build/bundle/programs/web.browser.legacy
#mv /home/wekan/app_build/bundle /build
wget "https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64.zip"
unzip wekan-8.01-amd64.zip
rm wekan-8.01-amd64.zip
wget "https://github.com/wekan/wekan/releases/download/v8.02/wekan-8.02-amd64.zip"
unzip wekan-8.02-amd64.zip
rm wekan-8.02-amd64.zip
mv /home/wekan/app/bundle /build
# Put back the original tar

View file

@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v8.01.0"
appVersion: "v8.02.0"
files:
userUploads:
- README.md

View file

@ -10,7 +10,7 @@ This is without container (without Docker or Snap).
Right click and download files 1-4:
1. [wekan-8.01-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64-windows.zip)
1. [wekan-8.02-amd64-windows.zip](https://github.com/wekan/wekan/releases/download/v8.02/wekan-8.02-amd64-windows.zip)
2. [node.exe](https://nodejs.org/dist/latest-v14.x/win-x64/node.exe)
@ -22,7 +22,7 @@ Right click and download files 1-4:
6. Double click `mongodb-windows-x86_64-7.0.25-signed.msi` . In installer, uncheck downloading MongoDB compass.
7. Unzip `wekan-8.01-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
7. Unzip `wekan-8.02-amd64-windows.zip` , inside it is directory `bundle`, to it copy other files:
```
bundle (directory)

406
openapi/generate_openapi.js Normal file
View file

@ -0,0 +1,406 @@
#!/usr/bin/env node
/*
Node.js port of openapi/generate_openapi.py (minimal, Node 14 compatible).
Parses models to produce an OpenAPI 2.0 YAML on stdout.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const esprima = require('esprima');
function cleanupJsdocs(jsdoc) {
const lines = jsdoc.value.split('\n')
.map(s => s.replace(/^\s*/, ''))
.map(s => s.replace(/^\*/, ''));
while (lines.length && !lines[0].trim()) lines.shift();
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
return lines;
}
function loadReturnTypeJsdocJson(data) {
let s = data;
const repl = [
[/\n/g, ' '],
[/([\{\s,])(\w+)(:)/g, '$1"$2"$3'],
[/(:)\s*([^:\},\]]+)\s*([\},\]])/g, '$1"$2"$3'],
[/([\[])\s*([^\{].+)\s*(\])/g, '$1"$2"$3'],
[/^\s*([^\[{].+)\s*/, '"$1"']
];
for (const [r, rep] of repl) s = s.replace(r, rep);
try { return JSON.parse(s); } catch { return data; }
}
class Context {
constructor(filePath) {
this.path = filePath;
this._txt = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
const data = this._txt.join('\n');
this.program = esprima.parseModule(data, { comment: true, loc: true, range: true });
}
textAt(begin, end) { return this._txt.slice(begin - 1, end).join('\n'); }
}
function parseFile(filePath) {
try { return new Context(filePath); } catch { return undefined; }
}
function getReqBodyElems(node, acc) {
if (!node) return '';
switch (node.type) {
case 'FunctionExpression':
case 'ArrowFunctionExpression': return getReqBodyElems(node.body, acc);
case 'BlockStatement': node.body.forEach(s => getReqBodyElems(s, acc)); return '';
case 'TryStatement': return getReqBodyElems(node.block, acc);
case 'ExpressionStatement': return getReqBodyElems(node.expression, acc);
case 'MemberExpression': {
const left = getReqBodyElems(node.object, acc);
const right = node.property && node.property.name;
if (left === 'req.body' && right && !acc.includes(right)) acc.push(right);
return `${left}.${right}`;
}
case 'VariableDeclaration': node.declarations.forEach(s => getReqBodyElems(s, acc)); return '';
case 'VariableDeclarator': return getReqBodyElems(node.init, acc);
case 'Property': return getReqBodyElems(node.value, acc);
case 'ObjectExpression': node.properties.forEach(s => getReqBodyElems(s, acc)); return '';
case 'CallExpression': node.arguments.forEach(s => getReqBodyElems(s, acc)); return '';
case 'ArrayExpression': node.elements.forEach(s => getReqBodyElems(s, acc)); return '';
case 'IfStatement': getReqBodyElems(node.test, acc); if (node.consequent) getReqBodyElems(node.consequent, acc); if (node.alternate) getReqBodyElems(node.alternate, acc); return '';
case 'LogicalExpression':
case 'BinaryExpression':
case 'AssignmentExpression': getReqBodyElems(node.left, acc); getReqBodyElems(node.right, acc); return '';
case 'ChainExpression': return getReqBodyElems(node.expression, acc);
case 'ReturnStatement':
case 'UnaryExpression': if (node.argument) return getReqBodyElems(node.argument, acc); return '';
case 'Identifier': return node.name;
default: return '';
}
}
class EntryPoint {
constructor(schema, [method, pathLit, body]) {
this.schema = schema;
this.method = method; this._path = pathLit; this.body = body;
this._rawDoc = null; this._doc = {}; this._jsdoc = null;
this.path = (this._path.value || '').replace(/\/$/, '');
this.method_name = (this.method.value || '').toLowerCase();
this.body_params = [];
if (['post','put'].includes(this.method_name)) getReqBodyElems(this.body, this.body_params);
let url = this.path.replace(/:([^\/]*)Id/g, '{$1}').replace(/:([^\/]*)/g, '{$1}');
this.url = url;
const tokens = url.split('/');
const reduced = [];
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
if (t === 'api') continue;
if (i < tokens.length - 1 && tokens[i+1].startsWith('{')) continue;
reduced.push(t.replace(/[{}]/g, ''));
}
this.reduced_function_name = reduced.join('_');
schema.used = true;
}
set doc(doc) { this._rawDoc = doc; this._jsdoc = cleanupJsdocs(doc); this._doc = this.parseDoc(); }
get doc() { return this._doc; }
parseDoc() {
if (!this._jsdoc) return {};
const result = {};
let currentTag = 'description';
let current = '';
const store = (tag, data) => {
if (data == null) return; const s = (data + '').replace(/\s+$/,''); if (!s) return;
if (tag === 'param') {
result.params = result.params || {};
let nameDesc = s.trim();
let paramType = null;
let name = nameDesc; let desc = '';
const mType = nameDesc.match(/^\{([^}]+)\}\s*(.*)$/);
if (mType) { paramType = mType[1]; nameDesc = mType[2]; }
const sp = nameDesc.split(/\s+/, 2); name = sp[0]; desc = sp[1] || '';
const optional = /^\[.*\]$/.test(name); if (optional) name = name.slice(1,-1);
result.params[name] = [paramType, optional, desc];
if (name.endsWith('Id')) { const base = name.slice(0,-2); if (!result.params[base]) result.params[base] = [paramType, optional, desc]; }
return;
}
if (tag === 'tag') { (result.tag = result.tag || []).push(s); return; }
if (tag === 'return_type') { result[tag] = loadReturnTypeJsdocJson(s); return; }
result[tag] = s;
};
for (const lineRaw of this._jsdoc) {
let line = lineRaw;
if (/^@/.test(line.trim())) {
const parts = line.trim().split(/\s+/, 2);
const tag = parts[0]; const rest = parts[1] || '';
if (['@operation','@summary','@description','@param','@return_type','@tag'].includes(tag)) {
store(currentTag, current);
currentTag = tag.slice(1); current = ''; line = rest;
}
}
current += line + '\n';
}
store(currentTag, current);
return result;
}
get summary() { return this._doc.summary ? this._doc.summary.replace(/\n/g,' ') : null; }
docParam(name) { const p = (this._doc.params||{})[name]; return p ? p : [null,null,null]; }
operationId() { return this._doc.operation || `${this.method_name}_${this.reduced_function_name}`; }
description() { return this._doc.description || null; }
returns() { return this._doc.return_type || null; }
tags() { const tags = []; if (this.schema.fields) tags.push(this.schema.name); if (this._doc.tag) tags.push(...this._doc.tag); return tags; }
}
class SchemaProperty {
constructor(statement, schema, context) {
this.schema = schema;
this.statement = statement;
this.name = (statement.key.name || statement.key.value);
this.type = 'object'; this.blackbox = false; this.required = true; this.elements = [];
(statement.value.properties || []).forEach(p => {
try {
const key = p.key && (p.key.name || p.key.value);
if (key === 'type') {
if (p.value.type === 'Identifier') this.type = (p.value.name || '').toLowerCase();
else if (p.value.type === 'ArrayExpression') { this.type = 'array'; this.elements = (p.value.elements||[]).map(e => (e.name||'object').toLowerCase()); }
} else if (key === 'blackbox') { this.blackbox = true; }
else if (key === 'optional' && p.value && p.value.value) { this.required = false; }
} catch(e) { /* ignore minor parse errors */ }
});
this._doc = null; this._raw_doc = null;
}
set doc(jsdoc){ this._raw_doc = jsdoc; this._doc = cleanupJsdocs(jsdoc); }
get doc(){ return this._doc; }
}
class Schemas {
constructor(context, statement, jsdocs, name) {
this.name = name || null; this._data = statement; this.fields = null; this.used = false;
if (statement) {
if (!this.name) this.name = statement.expression.callee.object.name;
const content = statement.expression.arguments[0].arguments[0];
this.fields = (content.properties || []).map(p => new SchemaProperty(p, this, context));
}
this._doc = null; this._raw_doc = null;
if (jsdocs) this.processJsdocs(jsdocs);
}
get doc(){ return this._doc ? this._doc.join(' ') : null; }
set doc(jsdoc){ this._raw_doc = jsdoc; this._doc = cleanupJsdocs(jsdoc); }
processJsdocs(jsdocs){
if (!this._data) return;
const start = this._data.loc.start.line, end = this._data.loc.end.line;
for (const doc of jsdocs){ if (doc.loc.end.line + 1 === start) { this.doc = doc; break; } }
const inRange = jsdocs.filter(doc => doc.loc.start.line >= start && doc.loc.end.line <= end);
for (const f of (this.fields||[]))
for (let i=0;i<inRange.length;i++){ const doc=inRange[i]; if (f.statement && f.statement.loc && f.statement.loc.start && f.statement.loc.start.line === (f.statement.key && f.statement.key.loc && f.statement.key.loc.start.line)) { f.doc = doc; inRange.splice(i,1); break; } }
}
}
function parseSchemas(schemasDir){
const schemas = {}; const entryPoints = [];
const walk = dir => {
const items = fs.readdirSync(dir, { withFileTypes: true }).sort((a,b)=>a.name.localeCompare(b.name));
for (const it of items){
const p = path.join(dir, it.name);
if (it.isDirectory()) walk(p);
else if (it.isFile()){
const context = parseFile(p); if (!context) continue; const program = context.program;
let currentSchema = null;
const jsdocs = (program.comments||[]).filter(c => c.type === 'Block' && c.value.startsWith('*\n'));
for (const statement of program.body){
try {
if (statement.type === 'ExpressionStatement' && statement.expression && statement.expression.callee && statement.expression.callee.property && statement.expression.callee.property.name === 'attachSchema' && statement.expression.arguments[0] && statement.expression.arguments[0].type === 'NewExpression' && statement.expression.arguments[0].callee.name === 'SimpleSchema'){
const schema = new Schemas(context, statement, jsdocs);
currentSchema = schema.name; schemas[currentSchema] = schema;
} else if (statement.type === 'IfStatement' && statement.test && statement.test.type === 'MemberExpression' && statement.test.object && statement.test.object.name === 'Meteor' && statement.test.property && statement.test.property.name === 'isServer'){
const conseq = statement.consequent && statement.consequent.body || [];
const data = conseq.filter(s => s.type === 'ExpressionStatement' && s.expression && s.expression.type === 'CallExpression' && s.expression.callee && s.expression.callee.object && s.expression.callee.object.name === 'JsonRoutes').map(s => s.expression.arguments);
if (data.length){
if (!currentSchema){ currentSchema = path.basename(p); schemas[currentSchema] = new Schemas(context, null, null, currentSchema); }
const eps = data.map(d => new EntryPoint(schemas[currentSchema], d)); entryPoints.push(...eps);
let endOfPrev = -1;
for (const ep of eps){
const op = ep.method; const prior = jsdocs.filter(j => j.loc.end.line + 1 <= op.loc.start.line && j.loc.start.line > endOfPrev);
if (prior.length) ep.doc = prior[prior.length - 1];
endOfPrev = op.loc.end.line;
}
}
}
} catch(e){ /* ignore parse hiccups per file */ }
}
}
}
};
walk(schemasDir);
return { schemas, entryPoints };
}
function printOpenapiReturn(obj, indent){
const pad = ' '.repeat(indent);
if (Array.isArray(obj)){
console.log(`${pad}type: array`); console.log(`${pad}items:`); printOpenapiReturn(obj[0], indent+2); return;
}
if (obj && typeof obj === 'object'){
console.log(`${pad}type: object`); console.log(`${pad}properties:`);
for (const k of Object.keys(obj)){ console.log(`${pad} ${k}:`); printOpenapiReturn(obj[k], indent+4); }
return;
}
if (typeof obj === 'string') console.log(`${pad}type: ${obj}`);
}
function generateOpenapi(schemas, entryPoints, version){
console.log(`swagger: '2.0'
info:
title: Wekan REST API
version: ${version}
description: |
The REST API allows you to control and extend Wekan with ease.
schemes:
- http
securityDefinitions:
UserSecurity:
type: apiKey
in: header
name: Authorization
paths:
/users/login:
post:
operationId: login
summary: Login with REST API
consumes:
- application/json
- application/x-www-form-urlencoded
tags:
- Login
parameters:
- name: loginRequest
in: body
required: true
description: Login credentials
schema:
type: object
required:
- username
- password
properties:
username:
description: |
Your username
type: string
password:
description: |
Your password
type: string
format: password
responses:
200:
description: |-
Successful authentication
schema:
type: object
required:
- id
- token
- tokenExpires
properties:
id:
type: string
description: User ID
token:
type: string
description: |
Authentication token
tokenExpires:
type: string
format: date-time
description: |
Token expiration date
400:
description: |
Error in authentication
schema:
type: object
properties:
error:
type: string
reason:
type: string
default:
description: |
Error in authentication`);
const methods = {};
for (const ep of entryPoints){ (methods[ep.path] = methods[ep.path] || []).push(ep); }
const sorted = Object.keys(methods).sort();
for (const pth of sorted){
console.log(` ${methods[pth][0].url}:`);
for (const ep of methods[pth]){
const parameters = pth.split('/').filter(t => t.startsWith(':')).map(t => t.endsWith('Id') ? t.slice(1,-2) : t.slice(1));
console.log(` ${ep.method_name}:`);
console.log(` operationId: ${ep.operationId()}`);
const sum = ep.summary(); if (sum) console.log(` summary: ${sum}`);
const desc = ep.description(); if (desc){ console.log(` description: |`); desc.split('\n').forEach(l => console.log(` ${l.trim() ? l : ''}`)); }
const tags = ep.tags(); if (tags.length){ console.log(' tags:'); tags.forEach(t => console.log(` - ${t}`)); }
if (['post','put'].includes(ep.method_name)) console.log(` consumes:\n - multipart/form-data\n - application/json`);
if (parameters.length || ['post','put'].includes(ep.method_name)) console.log(' parameters:');
if (['post','put'].includes(ep.method_name)){
for (const f of ep.body_params){
console.log(` - name: ${f}\n in: formData`);
const [ptype, optional, pdesc] = ep.docParam(f);
if (pdesc) console.log(` description: |\n ${pdesc}`); else console.log(` description: the ${f} value`);
console.log(` type: ${ptype || 'string'}`);
console.log(` ${optional ? 'required: false' : 'required: true'}`);
}
}
for (const p of parameters){
console.log(` - name: ${p}\n in: path`);
const [ptype, optional, pdesc] = ep.docParam(p);
if (pdesc) console.log(` description: |\n ${pdesc}`); else console.log(` description: the ${p} value`);
console.log(` type: ${ptype || 'string'}`);
console.log(` ${optional ? 'required: false' : 'required: true'}`);
}
console.log(` produces:\n - application/json\n security:\n - UserSecurity: []\n responses:\n '200':\n description: |-\n 200 response`);
const ret = ep.returns();
if (ret){ console.log(' schema:'); printOpenapiReturn(ret, 12); }
}
}
console.log('definitions:');
for (const schema of Object.values(schemas)){
if (!schema.used || !schema.fields) continue;
console.log(` ${schema.name}:`);
console.log(' type: object');
if (schema.doc) console.log(` description: ${schema.doc}`);
console.log(' properties:');
const props = schema.fields.filter(f => !f.name.includes('.'));
const req = [];
for (const prop of props){
const name = prop.name; console.log(` ${name}:`);
if (prop.doc){ console.log(' description: |'); prop.doc.forEach(l => console.log(` ${l.trim() ? l : ''}`)); }
let ptype = prop.type; if (ptype === 'enum' || ptype === 'date') ptype = 'string';
if (ptype !== 'object') console.log(` type: ${ptype}`);
if (prop.type === 'array'){
console.log(' items:');
for (const el of prop.elements){ if (el === 'object') console.log(` $ref: "#/definitions/${schema.name + name.charAt(0).toUpperCase() + name.slice(1)}"`); else console.log(` type: ${el}`); }
} else if (prop.type === 'object'){
if (prop.blackbox) console.log(' type: object');
else console.log(` $ref: "#/definitions/${schema.name + name.charAt(0).toUpperCase() + name.slice(1)}"`);
}
if (!prop.name.includes('.') && !prop.required) console.log(' x-nullable: true');
if (prop.required) req.push(name);
}
if (req.length){ console.log(' required:'); req.forEach(f => console.log(` - ${f}`)); }
}
}
function main(){
const argv = process.argv.slice(2);
let version = 'git-master';
let dir = path.resolve(__dirname, '../models');
for (let i = 0; i < argv.length; i++){
if (argv[i] === '--release' && argv[i+1]) { version = argv[i+1]; i++; continue; }
if (!argv[i].startsWith('--')) { dir = path.resolve(argv[i]); }
}
const { schemas, entryPoints } = parseSchemas(dir);
generateOpenapi(schemas, entryPoints, version);
}
if (require.main === module) main();

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "wekan",
"version": "v8.01.0",
"version": "v8.02.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "wekan",
"version": "v8.01.0",
"version": "v8.02.0",
"description": "Open-Source kanban",
"private": true,
"repository": {

View file

@ -1,7 +1,325 @@
{
type: "ConditionalExpression",
test: {
type: "LogicalExpression",
operator: "&&",
left: {
type: "Identifier",
name: "TAPi18n",
loc: {start: {line: 2217,column: 15},end: {line: 2217,column: 22}}
},
right: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "TAPi18n",
loc: {start: {line: 2217,column: 26},end: {line: 2217,column: 33}}
},
property: {
type: "Identifier",
name: "i18n",
loc: {start: {line: 2217,column: 34},end: {line: 2217,column: 38}}
},
loc: {start: {line: 2217,column: 26},end: {line: 2217,column: 38}}
},
loc: {start: {line: 2217,column: 15},end: {line: 2217,column: 38}}
},
consequent: {
type: "CallExpression",
callee: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "TAPi18n",
loc: {start: {line: 2217,column: 41},end: {line: 2217,column: 48}}
},
property: {
type: "Identifier",
name: "__",
loc: {start: {line: 2217,column: 49},end: {line: 2217,column: 51}}
},
loc: {start: {line: 2217,column: 41},end: {line: 2217,column: 51}}
},
arguments: [
{
type: "Literal",
value: "default",
raw: "'default'",
loc: {start: {line: 2217,column: 52},end: {line: 2217,column: 61}}
}
],
loc: {start: {line: 2217,column: 41},end: {line: 2217,column: 62}}
},
alternate: {
type: "Literal",
value: "Default",
raw: "'Default'",
loc: {start: {line: 2217,column: 65},end: {line: 2217,column: 74}}
},
loc: {start: {line: 2217,column: 15},end: {line: 2217,column: 74}}
}
{
type: "ConditionalExpression",
test: {
type: "BinaryExpression",
operator: ">",
left: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "req",
loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 28}}
},
property: {
type: "Identifier",
name: "body",
loc: {start: {line: 3587,column: 29},end: {line: 3587,column: 33}}
},
loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 33}}
},
property: {
type: "Identifier",
name: "title",
loc: {start: {line: 3587,column: 34},end: {line: 3587,column: 39}}
},
loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 39}}
},
property: {
type: "Identifier",
name: "length",
loc: {start: {line: 3587,column: 40},end: {line: 3587,column: 46}}
},
loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 46}}
},
right: {
type: "Literal",
value: 1000,
raw: "1000",
loc: {start: {line: 3587,column: 49},end: {line: 3587,column: 53}}
},
loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 53}}
},
consequent: {
type: "CallExpression",
callee: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "req",
loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 59}}
},
property: {
type: "Identifier",
name: "body",
loc: {start: {line: 3587,column: 60},end: {line: 3587,column: 64}}
},
loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 64}}
},
property: {
type: "Identifier",
name: "title",
loc: {start: {line: 3587,column: 65},end: {line: 3587,column: 70}}
},
loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 70}}
},
property: {
type: "Identifier",
name: "substring",
loc: {start: {line: 3587,column: 71},end: {line: 3587,column: 80}}
},
loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 80}}
},
arguments: [
{
type: "Literal",
value: 0,
raw: "0",
loc: {start: {line: 3587,column: 81},end: {line: 3587,column: 82}}
},
{
type: "Literal",
value: 1000,
raw: "1000",
loc: {start: {line: 3587,column: 84},end: {line: 3587,column: 88}}
}
],
loc: {start: {line: 3587,column: 56},end: {line: 3587,column: 89}}
},
alternate: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "req",
loc: {start: {line: 3587,column: 92},end: {line: 3587,column: 95}}
},
property: {
type: "Identifier",
name: "body",
loc: {start: {line: 3587,column: 96},end: {line: 3587,column: 100}}
},
loc: {start: {line: 3587,column: 92},end: {line: 3587,column: 100}}
},
property: {
type: "Identifier",
name: "title",
loc: {start: {line: 3587,column: 101},end: {line: 3587,column: 106}}
},
loc: {start: {line: 3587,column: 92},end: {line: 3587,column: 106}}
},
loc: {start: {line: 3587,column: 25},end: {line: 3587,column: 106}}
}
{
type: "ConditionalExpression",
test: {
type: "BinaryExpression",
operator: ">",
left: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "req",
loc: {start: {line: 655,column: 25},end: {line: 655,column: 28}}
},
property: {
type: "Identifier",
name: "body",
loc: {start: {line: 655,column: 29},end: {line: 655,column: 33}}
},
loc: {start: {line: 655,column: 25},end: {line: 655,column: 33}}
},
property: {
type: "Identifier",
name: "title",
loc: {start: {line: 655,column: 34},end: {line: 655,column: 39}}
},
loc: {start: {line: 655,column: 25},end: {line: 655,column: 39}}
},
property: {
type: "Identifier",
name: "length",
loc: {start: {line: 655,column: 40},end: {line: 655,column: 46}}
},
loc: {start: {line: 655,column: 25},end: {line: 655,column: 46}}
},
right: {
type: "Literal",
value: 1000,
raw: "1000",
loc: {start: {line: 655,column: 49},end: {line: 655,column: 53}}
},
loc: {start: {line: 655,column: 25},end: {line: 655,column: 53}}
},
consequent: {
type: "CallExpression",
callee: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "req",
loc: {start: {line: 655,column: 56},end: {line: 655,column: 59}}
},
property: {
type: "Identifier",
name: "body",
loc: {start: {line: 655,column: 60},end: {line: 655,column: 64}}
},
loc: {start: {line: 655,column: 56},end: {line: 655,column: 64}}
},
property: {
type: "Identifier",
name: "title",
loc: {start: {line: 655,column: 65},end: {line: 655,column: 70}}
},
loc: {start: {line: 655,column: 56},end: {line: 655,column: 70}}
},
property: {
type: "Identifier",
name: "substring",
loc: {start: {line: 655,column: 71},end: {line: 655,column: 80}}
},
loc: {start: {line: 655,column: 56},end: {line: 655,column: 80}}
},
arguments: [
{
type: "Literal",
value: 0,
raw: "0",
loc: {start: {line: 655,column: 81},end: {line: 655,column: 82}}
},
{
type: "Literal",
value: 1000,
raw: "1000",
loc: {start: {line: 655,column: 84},end: {line: 655,column: 88}}
}
],
loc: {start: {line: 655,column: 56},end: {line: 655,column: 89}}
},
alternate: {
type: "MemberExpression",
computed: False,
object: {
type: "MemberExpression",
computed: False,
object: {
type: "Identifier",
name: "req",
loc: {start: {line: 655,column: 92},end: {line: 655,column: 95}}
},
property: {
type: "Identifier",
name: "body",
loc: {start: {line: 655,column: 96},end: {line: 655,column: 100}}
},
loc: {start: {line: 655,column: 92},end: {line: 655,column: 100}}
},
property: {
type: "Identifier",
name: "title",
loc: {start: {line: 655,column: 101},end: {line: 655,column: 106}}
},
loc: {start: {line: 655,column: 92},end: {line: 655,column: 106}}
},
loc: {start: {line: 655,column: 25},end: {line: 655,column: 106}}
}
swagger: '2.0'
info:
title: Wekan REST API
version: v8.01
version: v8.02
description: |
The REST API allows you to control and extend Wekan with ease.
@ -2853,6 +3171,11 @@ definitions:
The default board ID assigned to subtasks.
type: string
x-nullable: true
migrationVersion:
description: |
The migration version of the board structure.
New boards are created with the latest version and don't need migration.
type: number
subtasksDefaultListId:
description: |
The default List ID assigned to subtasks.
@ -3041,6 +3364,7 @@ definitions:
- color
- allowsCardCounterList
- allowsBoardMemberList
- migrationVersion
- allowsSubtasks
- allowsAttachments
- allowsChecklists
@ -3821,8 +4145,9 @@ definitions:
type: string
swimlaneId:
description: |
the swimlane associated to this list. Required for per-swimlane list titles
the swimlane associated to this list. Optional for backward compatibility
type: string
x-nullable: true
createdAt:
description: |
creation date
@ -3887,7 +4212,6 @@ definitions:
- title
- archived
- boardId
- swimlaneId
- createdAt
- modifiedAt
- type

View file

@ -1,4 +1,6 @@
# Extract the OpenAPI specification.
#!/usr/bin/env bash
# Build API documentation using Node.js tooling only (Node 14.x compatible).
set -euo pipefail
# 1) Check that there is only one parameter
# of Wekan version number:
@ -10,26 +12,7 @@ if [ $# -ne 1 ]
exit 1
fi
# 2) If esprima-python directory does not exist,
# install dependencies.
if [ ! -d ~/python/esprima-python ]; then
sudo apt-get -y install python3-pip python3-swagger-spec-validator python3-wheel python3-setuptools
# Install older version of api2html that works with Node.js 14
sudo npm install -g api2html@0.3.0 || sudo npm install -g swagger-ui-watcher
(mkdir -p ~/python && cd ~/python && git clone --depth 1 -b master https://github.com/Kronuz/esprima-python)
(cd ~/python/esprima-python && git fetch origin pull/20/head:delete_fix && git checkout delete_fix && sudo python3 setup.py install --record files.txt)
#(cd ~/python/esprima-python && git fetch origin pull/20/head:delete_fix && git checkout delete_fix && sudo pip3 install .)
# temporary fix until https://github.com/Kronuz/esprima-python/pull/20 gets merged
# a) Generating docs works on Kubuntu 21.10 with this,
# but generating Sandstorm WeKan package does not work
# https://github.com/wekan/wekan/issues/4280
# https://github.com/sandstorm-io/sandstorm/issues/3600
# sudo pip3 install .
# b) Generating docs Works on Linux Mint with this,
# and also generating Sandstorm WeKan package works:
# sudo python3 setup.py install --record files.txt
fi
# 2) No Python dependencies; use npm/npx exclusively
# 2) Go to Wekan repo directory
cd ~/repos/wekan
@ -39,10 +22,43 @@ if [ ! -d public/api ]; then
mkdir -p public/api
fi
# 4) Generate docs with api2html or fallback to swagger-ui-watcher
python3 ./openapi/generate_openapi.py --release v$1 > ./public/api/wekan.yml
if ! api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml; then
swagger-ui-watcher ./public/api/wekan.yml -p 8080
# 4) Locate or generate an OpenAPI spec (YAML or JSON)
SPEC_YML="./public/api/wekan.yml"
SPEC_JSON="./public/openapi.json"
SPEC_ALT_YML="./public/openapi.yml"
if [ -s "$SPEC_YML" ]; then
SPEC="$SPEC_YML"
elif [ -s "$SPEC_JSON" ]; then
SPEC="$SPEC_JSON"
elif [ -s "$SPEC_ALT_YML" ]; then
SPEC="$SPEC_ALT_YML"
else
echo "No existing OpenAPI spec found. Generating from models with Node..."
mkdir -p ./public/api
node ./openapi/generate_openapi.js --release v$1 ./models > "$SPEC_YML"
SPEC="$SPEC_YML"
fi
chmod 644 "$SPEC" 2>/dev/null || true
# Build static HTML docs (no global installs)
# 1) Prefer Redocly CLI
if npx --yes @redocly/cli@latest build-docs "$SPEC" -o ./public/api/wekan.html; then
:
else
# 2) Fallback to redoc-cli
if npx --yes redoc-cli@latest bundle "$SPEC" -o ./public/api/wekan.html; then
:
else
# 3) Fallback to api2html
if npx --yes api2html@0.3.0 -c ./public/logo-header.png -o ./public/api/wekan.html "$SPEC"; then
:
else
echo "All HTML generators failed. You can preview locally with:" >&2
echo " npx --yes @redocly/cli@latest preview-docs $SPEC" >&2
exit 1
fi
fi
fi
# Copy docs to bundle

View file

@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
appTitle = (defaultText = "Wekan"),
# The name of the app as it is displayed to the user.
appVersion = 801,
appVersion = 802,
# Increment this for every release.
appMarketingVersion = (defaultText = "8.01.0~2025-10-11"),
appMarketingVersion = (defaultText = "8.02.0~2025-10-14"),
# Human-readable presentation of the app version.
minUpgradableAppVersion = 0,

View file

@ -1,5 +1,5 @@
name: wekan
version: '8.01'
version: '8.02'
base: core24
summary: Open Source kanban
description: |
@ -203,9 +203,9 @@ parts:
# Cleanup
mkdir .build
cd .build
wget https://github.com/wekan/wekan/releases/download/v8.01/wekan-8.01-amd64.zip
unzip wekan-8.01-amd64.zip
rm wekan-8.01-amd64.zip
wget https://github.com/wekan/wekan/releases/download/v8.02/wekan-8.02-amd64.zip
unzip wekan-8.02-amd64.zip
rm wekan-8.02-amd64.zip
cd ..
##cd .build/bundle
##find . -type d -name '*-garbage*' | xargs rm -rf