mirror of https://github.com/wekan/wekan
The Open Source kanban (built with Meteor). Keep variable/table/field names camelCase. For translations, only add Pull Request changes to wekan/i18n/en.i18n.json , other translations are done at https://transifex.com/wekan/wekan only.
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
131 lines
5.3 KiB
131 lines
5.3 KiB
import { Meteor } from 'meteor/meteor';
|
|
import { WebApp } from 'meteor/webapp';
|
|
import { check } from 'meteor/check';
|
|
import Cards from '/models/cards';
|
|
import Lists from '/models/lists';
|
|
import Swimlanes from '/models/swimlanes';
|
|
import { ReactiveCache } from '/imports/reactiveCache';
|
|
import { icsToCards } from '/server/lib/icsImport';
|
|
import { Authentication } from '/server/authentication';
|
|
import { sendJsonResult } from '/server/apiMiddleware';
|
|
import { allowIsBoardMemberWithWriteAccess } from '/server/lib/utils';
|
|
|
|
// Shared import logic used by both the Meteor method and the REST endpoint.
|
|
// Validates that the target list/swimlane belong to the board (no cross-board
|
|
// writes), parses the .ics text into card shapes and inserts them.
|
|
async function importIcsCards(userId, boardId, listId, swimlaneId, icsText) {
|
|
const list = await Lists.findOneAsync(listId);
|
|
if (!list || list.boardId !== boardId) {
|
|
throw new Meteor.Error('list-not-found', 'List not found on this board.');
|
|
}
|
|
const swimlane = await Swimlanes.findOneAsync(swimlaneId);
|
|
if (!swimlane || swimlane.boardId !== boardId) {
|
|
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found on this board.');
|
|
}
|
|
const cardShapes = icsToCards(icsText, { boardId, listId, swimlaneId });
|
|
const cardIds = [];
|
|
for (const shape of cardShapes) {
|
|
const doc = {
|
|
title: shape.title,
|
|
description: shape.description,
|
|
boardId,
|
|
listId,
|
|
swimlaneId,
|
|
userId,
|
|
sort: cardIds.length,
|
|
};
|
|
if (shape.startAt) doc.startAt = shape.startAt;
|
|
if (shape.dueAt) doc.dueAt = shape.dueAt;
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const cardId = await Cards.insertAsync(doc);
|
|
cardIds.push(cardId);
|
|
}
|
|
return { created: cardIds.length, cardIds };
|
|
}
|
|
|
|
/**
|
|
* Server-side Meteor method for WeKan issue #6323 (part 1: import only).
|
|
*
|
|
* Parses an uploaded iCalendar (.ics) file and creates WeKan cards on the
|
|
* given board / list / swimlane, populating each card's startAt and dueAt so
|
|
* the imported events appear on the Calendar / Gantt views.
|
|
*
|
|
* This is import-only. Two-way Google Calendar sync is out of scope; for the
|
|
* read-only export direction (WeKan -> iCal feed) see `wekan-ical-server`.
|
|
*/
|
|
Meteor.methods({
|
|
/**
|
|
* Import an .ics file into a board as cards.
|
|
*
|
|
* @param {string} boardId target board id
|
|
* @param {string} listId target list id (cards are created here)
|
|
* @param {string} swimlaneId target swimlane id
|
|
* @param {string} icsText raw .ics file contents
|
|
* @returns {{ created: number, cardIds: string[] }}
|
|
*/
|
|
async importIcsToBoard(boardId, listId, swimlaneId, icsText) {
|
|
check(boardId, String);
|
|
check(listId, String);
|
|
check(swimlaneId, String);
|
|
check(icsText, String);
|
|
|
|
if (!this.userId) {
|
|
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
|
}
|
|
|
|
const board = await ReactiveCache.getBoard(boardId);
|
|
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
|
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
|
}
|
|
// Only board members (not read-only viewers) may create cards.
|
|
if (typeof board.hasMember === 'function' && !board.hasMember(this.userId)) {
|
|
throw new Meteor.Error('not-authorized', 'You are not a member of this board.');
|
|
}
|
|
if (typeof board.hasReadOnly === 'function' && board.hasReadOnly(this.userId)) {
|
|
throw new Meteor.Error('not-authorized', 'You have read-only access to this board.');
|
|
}
|
|
|
|
return importIcsCards(this.userId, boardId, listId, swimlaneId, icsText);
|
|
},
|
|
});
|
|
|
|
/**
|
|
* @operation import_ics
|
|
* @tag Cards
|
|
*
|
|
* @summary Import an iCalendar (.ics) file into a board as cards
|
|
*
|
|
* @description Parses the supplied iCalendar text and creates a card on the
|
|
* given board / list / swimlane for each VEVENT, populating each card's
|
|
* startAt (DTSTART) and dueAt (DTEND, or DTSTART when there is no DTEND) so the
|
|
* imported events appear on the Calendar and Gantt views. Requires write access
|
|
* to the board. See WeKan issue #6323. Import-only; two-way calendar sync is not
|
|
* provided.
|
|
*
|
|
* @param {string} boardId the board ID
|
|
* @param {string} swimlaneId the swimlane ID the cards are created in
|
|
* @param {string} listId the list ID the cards are created in
|
|
* @param {string} ics the raw .ics (iCalendar) file contents
|
|
* @return_type { created: number, cardIds: string[] }
|
|
*/
|
|
WebApp.handlers.post(
|
|
'/api/boards/:boardId/swimlanes/:swimlaneId/lists/:listId/ics',
|
|
async function(req, res) {
|
|
Authentication.checkLoggedIn(req.userId);
|
|
const { boardId, swimlaneId, listId } = req.params;
|
|
const board = await ReactiveCache.getBoard(boardId);
|
|
if (!board || !allowIsBoardMemberWithWriteAccess(req.userId, board)) {
|
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'not-authorized' }));
|
|
return;
|
|
}
|
|
const icsText = (req.body && (req.body.ics || req.body.icsText)) || '';
|
|
if (typeof icsText !== 'string' || icsText.trim() === '') {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'missing-ics', message: 'Provide the .ics file contents in the "ics" field.' }));
|
|
return;
|
|
}
|
|
const result = await importIcsCards(req.userId, boardId, listId, swimlaneId, icsText);
|
|
sendJsonResult(res, { code: 200, data: result });
|
|
},
|
|
);
|
|
|