mirror of
https://github.com/wekan/wekan.git
synced 2025-12-26 20:28:48 +01:00
Gantt chart view to one board view menu Swimlanes/Lists/Calendar/Gantt.
Thanks to xet7 ! Fixes #2870
This commit is contained in:
parent
1790918006
commit
f34e4c0e36
10 changed files with 511 additions and 112 deletions
|
|
@ -49,6 +49,8 @@ template(name="boardBody")
|
|||
+listsGroup(currentBoard)
|
||||
else if isViewCalendar
|
||||
+calendarView
|
||||
else if isViewGantt
|
||||
+ganttView
|
||||
else
|
||||
// Default view - show swimlanes if they exist, otherwise show lists
|
||||
if hasSwimlanes
|
||||
|
|
@ -64,3 +66,10 @@ template(name="calendarView")
|
|||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
+fullcalendar(calendarOptions)
|
||||
template(name="ganttView")
|
||||
if isViewGantt
|
||||
.gantt-view.swimlane
|
||||
if currentCard
|
||||
+cardDetails(currentCard)
|
||||
.gantt-container
|
||||
#gantt-chart
|
||||
|
|
@ -5,6 +5,7 @@ import { boardConverter } from '/client/lib/boardConverter';
|
|||
import { migrationManager } from '/client/lib/migrationManager';
|
||||
import { attachmentMigrationManager } from '/client/lib/attachmentMigrationManager';
|
||||
import { migrationProgressManager } from '/client/components/migrationProgress';
|
||||
import { formatDateByUserPreference } from '/imports/lib/dateUtils';
|
||||
import Swimlanes from '/models/swimlanes';
|
||||
import Lists from '/models/lists';
|
||||
|
||||
|
|
@ -978,6 +979,19 @@ BlazeComponent.extendComponent({
|
|||
return boardView === 'board-view-cal';
|
||||
},
|
||||
|
||||
isViewGantt() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
let boardView;
|
||||
|
||||
if (currentUser) {
|
||||
boardView = (currentUser.profile || {}).boardView;
|
||||
} else {
|
||||
boardView = window.localStorage.getItem('boardView');
|
||||
}
|
||||
|
||||
return boardView === 'board-view-gantt';
|
||||
},
|
||||
|
||||
hasSwimlanes() {
|
||||
const currentBoard = Utils.getCurrentBoard();
|
||||
if (!currentBoard) {
|
||||
|
|
@ -1408,3 +1422,263 @@ BlazeComponent.extendComponent({
|
|||
}
|
||||
},
|
||||
}).register('calendarView');
|
||||
/**
|
||||
* Gantt View Component
|
||||
* Displays cards as a Gantt chart with start/due dates
|
||||
*/
|
||||
BlazeComponent.extendComponent({
|
||||
template() {
|
||||
return 'ganttView';
|
||||
},
|
||||
|
||||
onCreated() {
|
||||
this.autorun(() => {
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (board) {
|
||||
// Subscribe to cards for the current board
|
||||
this.subscribe('allCards', board._id);
|
||||
this.subscribe('allLists', board._id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
this.autorun(() => {
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (board && this.subscriptionsReady()) {
|
||||
this.renderGanttChart();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renderGanttChart() {
|
||||
const board = Utils.getCurrentBoard();
|
||||
if (!board) return;
|
||||
|
||||
const ganttContainer = document.getElementById('gantt-chart');
|
||||
if (!ganttContainer) return;
|
||||
|
||||
// Clear previous content
|
||||
ganttContainer.innerHTML = '';
|
||||
|
||||
// Get all cards for the board
|
||||
const cards = Cards.find({ boardId: board._id }, { sort: { startAt: 1, dueAt: 1 } }).fetch();
|
||||
|
||||
if (cards.length === 0) {
|
||||
ganttContainer.innerHTML = `<p style="padding: 20px; text-align: center; color: #999;">${TAPi18n.__('no-cards-in-gantt')}</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a weekly HTML gantt view
|
||||
this.createWeeklyGanttView(cards, ganttContainer);
|
||||
},
|
||||
createWeeklyGanttView(cards, container) {
|
||||
const today = new Date();
|
||||
const currentUser = ReactiveCache.getCurrentUser && ReactiveCache.getCurrentUser();
|
||||
const dateFormat = currentUser ? currentUser.getDateFormat() : 'YYYY-MM-DD';
|
||||
|
||||
// Helpers to compute ISO week and start/end of week
|
||||
const getISOWeekInfo = d => {
|
||||
const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||||
const dayNum = date.getUTCDay() || 7;
|
||||
date.setUTCDate(date.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
||||
const week = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
|
||||
return { year: date.getUTCFullYear(), week };
|
||||
};
|
||||
const startOfISOWeek = d => {
|
||||
const date = new Date(d);
|
||||
const day = date.getDay() || 7; // Sunday -> 7
|
||||
if (day !== 1) date.setDate(date.getDate() - (day - 1));
|
||||
date.setHours(0,0,0,0);
|
||||
return date;
|
||||
};
|
||||
|
||||
// Collect weeks that have any dates on cards
|
||||
const weeksMap = new Map(); // key: `${year}-W${week}` -> { year, week, start }
|
||||
const relevantCards = cards.filter(c => c.receivedAt || c.startAt || c.dueAt || c.endAt);
|
||||
relevantCards.forEach(card => {
|
||||
['receivedAt','startAt','dueAt','endAt'].forEach(field => {
|
||||
if (card[field]) {
|
||||
const dt = new Date(card[field]);
|
||||
const info = getISOWeekInfo(dt);
|
||||
const key = `${info.year}-W${info.week}`;
|
||||
if (!weeksMap.has(key)) {
|
||||
weeksMap.set(key, { year: info.year, week: info.week, start: startOfISOWeek(dt) });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort weeks by start ascending (oldest first)
|
||||
const weeks = Array.from(weeksMap.values()).sort((a,b) => a.start - b.start);
|
||||
|
||||
// Weekday labels
|
||||
const weekdayKeys = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
|
||||
const weekdayLabels = weekdayKeys.map(k => TAPi18n.__(k));
|
||||
|
||||
// Build HTML for all week tables
|
||||
let html = '';
|
||||
weeks.forEach(weekInfo => {
|
||||
const weekStart = new Date(weekInfo.start);
|
||||
const weekDates = Array.from({length:7}, (_,i) => {
|
||||
const d = new Date(weekStart);
|
||||
d.setDate(d.getDate() + i);
|
||||
d.setHours(0,0,0,0);
|
||||
return d;
|
||||
});
|
||||
|
||||
// Table header
|
||||
html += '<table class="gantt-table">';
|
||||
html += '<thead>';
|
||||
html += '<tr>';
|
||||
const taskHeader = `${TAPi18n.__('task')} ${TAPi18n.__('predicate-week')} ${weekInfo.week}`;
|
||||
html += `<th>${taskHeader}</th>`;
|
||||
weekdayLabels.forEach((lbl, idx) => {
|
||||
const formattedDate = formatDateByUserPreference(weekDates[idx], dateFormat, false);
|
||||
html += `<th>${formattedDate} ${lbl}</th>`;
|
||||
});
|
||||
html += '</tr></thead>';
|
||||
|
||||
// Rows: include cards that have any date in this week
|
||||
html += '<tbody>';
|
||||
relevantCards.forEach(card => {
|
||||
const cardDates = {
|
||||
receivedAt: card.receivedAt ? new Date(card.receivedAt) : null,
|
||||
startAt: card.startAt ? new Date(card.startAt) : null,
|
||||
dueAt: card.dueAt ? new Date(card.dueAt) : null,
|
||||
endAt: card.endAt ? new Date(card.endAt) : null,
|
||||
};
|
||||
const isInWeek = Object.values(cardDates).some(dt => dt && getISOWeekInfo(dt).week === weekInfo.week && getISOWeekInfo(dt).year === weekInfo.year);
|
||||
if (!isInWeek) return;
|
||||
|
||||
// Row header cell (task title)
|
||||
html += '<tr>';
|
||||
html += `<td class="js-gantt-task-cell" data-card-id="${card._id}" title="${card.title}">${card.title}</td>`;
|
||||
|
||||
// Weekday cells with icons/colors only on exact matching dates
|
||||
weekDates.forEach((dayDate, idx) => {
|
||||
let cellContent = '';
|
||||
let cellClass = '';
|
||||
let cellStyle = '';
|
||||
let cellTitle = '';
|
||||
let cellDateType = '';
|
||||
|
||||
// Highlight today and weekends
|
||||
const isToday = dayDate.toDateString() === today.toDateString();
|
||||
if (isToday) {
|
||||
cellClass += ' ganttview-today';
|
||||
cellStyle += 'background-color: #fcf8e3 !important;';
|
||||
}
|
||||
const isWeekend = idx >= 5; // Saturday/Sunday
|
||||
if (isWeekend) {
|
||||
cellClass += ' ganttview-weekend';
|
||||
if (!isToday) cellStyle += 'background-color: #efefef !important;';
|
||||
}
|
||||
|
||||
// Match specific date types
|
||||
if (cardDates.receivedAt && cardDates.receivedAt.toDateString() === dayDate.toDateString()) {
|
||||
cellContent = '📥';
|
||||
cellStyle = 'background-color: #dbdbdb !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
|
||||
cellTitle = TAPi18n.__('card-received');
|
||||
cellDateType = 'received';
|
||||
}
|
||||
if (cardDates.startAt && cardDates.startAt.toDateString() === dayDate.toDateString()) {
|
||||
cellContent = '🚀';
|
||||
cellStyle = 'background-color: #90ee90 !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
|
||||
cellTitle = TAPi18n.__('card-start');
|
||||
cellDateType = 'start';
|
||||
}
|
||||
if (cardDates.dueAt && cardDates.dueAt.toDateString() === dayDate.toDateString()) {
|
||||
cellContent = '⏰';
|
||||
cellStyle = 'background-color: #ffd700 !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
|
||||
cellTitle = TAPi18n.__('card-due');
|
||||
cellDateType = 'due';
|
||||
}
|
||||
if (cardDates.endAt && cardDates.endAt.toDateString() === dayDate.toDateString()) {
|
||||
cellContent = '🏁';
|
||||
cellStyle = 'background-color: #ffb3b3 !important; color: #000 !important; font-size: 18px !important; font-weight: bold !important;';
|
||||
cellTitle = TAPi18n.__('card-end');
|
||||
cellDateType = 'end';
|
||||
}
|
||||
|
||||
if (cellDateType) {
|
||||
cellClass += ' js-gantt-date-icon';
|
||||
}
|
||||
const cellDataAttrs = cellDateType ? ` data-card-id="${card._id}" data-date-type="${cellDateType}"` : '';
|
||||
|
||||
html += `<td class="${cellClass}" style="${cellStyle}" title="${cellTitle}"${cellDataAttrs}>${cellContent}</td>`;
|
||||
});
|
||||
|
||||
// Close row
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
// Close section for this week
|
||||
html += '</tbody></table>';
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Add click handlers
|
||||
const taskCells = container.querySelectorAll('.js-gantt-task-cell');
|
||||
taskCells.forEach(cell => {
|
||||
cell.addEventListener('click', (e) => {
|
||||
const cardId = e.currentTarget.dataset.cardId;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
if (!card) return;
|
||||
|
||||
// Scroll the gantt container and viewport to top so the card details are visible
|
||||
if (container && typeof container.scrollIntoView === 'function') {
|
||||
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
if (typeof window !== 'undefined' && typeof window.scrollTo === 'function') {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
const contentEl = document.getElementById('content');
|
||||
if (contentEl && typeof contentEl.scrollTo === 'function') {
|
||||
contentEl.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Open card the same way as clicking a minicard - set currentCard session
|
||||
// This shows the full card details overlay, not a popup
|
||||
Session.set('currentCard', cardId);
|
||||
});
|
||||
});
|
||||
|
||||
// Date icon click handlers: open the same edit popups as in swimlane cards
|
||||
const dateIconCells = container.querySelectorAll('.js-gantt-date-icon');
|
||||
dateIconCells.forEach(cell => {
|
||||
cell.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const cardId = e.currentTarget.dataset.cardId;
|
||||
const dateType = e.currentTarget.dataset.dateType;
|
||||
const card = ReactiveCache.getCard(cardId);
|
||||
if (!card || !dateType) return;
|
||||
|
||||
const popupMap = {
|
||||
received: 'editCardReceivedDate',
|
||||
start: 'editCardStartDate',
|
||||
due: 'editCardDueDate',
|
||||
end: 'editCardEndDate',
|
||||
};
|
||||
const popupName = popupMap[dateType];
|
||||
if (!popupName || !Popup || typeof Popup.open !== 'function') return;
|
||||
|
||||
const openFn = Popup.open(popupName);
|
||||
// Supply the card as data context for the popup
|
||||
openFn.call({ currentData: () => card }, e, { dataContextIfCurrentDataIsUndefined: card });
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
isViewGantt() {
|
||||
const currentUser = ReactiveCache.getCurrentUser();
|
||||
if (currentUser) {
|
||||
return (currentUser.profile || {}).boardView === 'board-view-gantt';
|
||||
} else {
|
||||
return window.localStorage.getItem('boardView') === 'board-view-gantt';
|
||||
}
|
||||
},
|
||||
}).register('ganttView');
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ template(name="boardHeaderBar")
|
|||
| 📋
|
||||
if $eq boardView 'board-view-cal'
|
||||
| 📅
|
||||
if $eq boardView 'board-view-gantt'
|
||||
| 📊
|
||||
|
||||
if canModifyBoard
|
||||
a.board-header-btn.js-multiselection-activate(
|
||||
|
|
@ -208,6 +210,13 @@ template(name="boardChangeViewPopup")
|
|||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
| ✅
|
||||
li
|
||||
with "board-view-gantt"
|
||||
a.js-open-gantt-view
|
||||
| 📊
|
||||
| {{_ 'board-view-gantt'}}
|
||||
if $eq Utils.boardView "board-view-gantt"
|
||||
| ✅
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
|
|
|
|||
|
|
@ -208,6 +208,10 @@ Template.boardChangeViewPopup.events({
|
|||
Utils.setBoardView('board-view-cal');
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-open-gantt-view'() {
|
||||
Utils.setBoardView('board-view-gantt');
|
||||
Popup.back();
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
|
|
|
|||
178
client/components/boards/gantt.css
Normal file
178
client/components/boards/gantt.css
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/* Gantt View Styles */
|
||||
|
||||
.gantt-view {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.gantt-view.swimlane {
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.gantt-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
background-color: #fff;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gantt-container table,
|
||||
.gantt-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
border: 2px solid #666;
|
||||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.gantt-container thead {
|
||||
background-color: #e8e8e8;
|
||||
border-bottom: 2px solid #666;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.gantt-container thead th,
|
||||
.gantt-container thead tr > td:first-child {
|
||||
border-right: 2px solid #666;
|
||||
padding: 4px; /* half of 8px */
|
||||
width: 100px; /* half of 200px */
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
background-color: #e8e8e8;
|
||||
min-width: 100px; /* half of 200px */
|
||||
}
|
||||
|
||||
.gantt-container thead td {
|
||||
border-right: 1px solid #999;
|
||||
padding: 2px 1px; /* half */
|
||||
text-align: center;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 11px;
|
||||
min-width: 15px; /* half of 30px */
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr {
|
||||
border-bottom: 1px solid #999;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.gantt-container tbody tr:hover td {
|
||||
background-color: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
.gantt-container tbody td {
|
||||
border-right: 1px solid #ccc;
|
||||
padding: 1px; /* half */
|
||||
text-align: center;
|
||||
min-width: 15px; /* half of 30px */
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
line-height: 28px;
|
||||
background-color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child {
|
||||
border-right: 2px solid #666;
|
||||
padding: 4px; /* half of 8px */
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
background-color: #fafafa !important;
|
||||
text-align: left;
|
||||
width: 100px; /* half of 200px */
|
||||
min-width: 100px; /* half of 200px */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child:hover {
|
||||
background-color: #f0f0f0 !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.js-gantt-task-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.js-gantt-date-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gantt-container .ganttview-weekend {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.gantt-container .ganttview-today {
|
||||
background-color: #fcf8e3;
|
||||
border-right: 2px solid #ffb347;
|
||||
}
|
||||
|
||||
/* Task bar styling - VERY VISIBLE */
|
||||
.gantt-container tbody td.ganttview-block {
|
||||
background-color: #4CAF50 !important;
|
||||
color: #fff !important;
|
||||
font-size: 18px !important;
|
||||
font-weight: bold !important;
|
||||
padding: 2px !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.gantt-container table {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.gantt-container thead td {
|
||||
min-width: 20px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.gantt-container tbody td {
|
||||
min-width: 20px;
|
||||
padding: 1px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.gantt-container tbody td:first-child {
|
||||
width: 100px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.gantt-container {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.gantt-container table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
|
@ -297,7 +297,23 @@ BlazeComponent.extendComponent({
|
|||
{
|
||||
...events,
|
||||
'click .js-close-card-details'() {
|
||||
Utils.goBoardId(this.data().boardId);
|
||||
// Get board ID from either the card data or current board in session
|
||||
const card = this.currentData() || this.data();
|
||||
const boardId = (card && card.boardId) || Utils.getCurrentBoard()._id;
|
||||
|
||||
if (boardId) {
|
||||
// Clear the current card session to close the card
|
||||
Session.set('currentCard', null);
|
||||
|
||||
// Navigate back to board without card
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (board) {
|
||||
FlowRouter.go('board', {
|
||||
id: board._id,
|
||||
slug: board.slug,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-copy-link'(event) {
|
||||
event.preventDefault();
|
||||
|
|
|
|||
|
|
@ -212,6 +212,11 @@ window.Popup = new (class {
|
|||
|
||||
if (Utils.isMiniScreen()) return { left: 0, top: 0 };
|
||||
|
||||
// If the opener element is missing (e.g., programmatic open), fallback to viewport origin
|
||||
if (!$element || $element.length === 0) {
|
||||
return { left: 10, top: 10, maxHeight: $(window).height() - 20 };
|
||||
}
|
||||
|
||||
const offset = $element.offset();
|
||||
// Calculate actual popup width based on CSS: min(380px, 55vw)
|
||||
const viewportWidth = $(window).width();
|
||||
|
|
|
|||
|
|
@ -264,6 +264,9 @@ Utils = {
|
|||
} else if (view === 'board-view-cal') {
|
||||
window.localStorage.setItem('boardView', 'board-view-cal'); //true
|
||||
Utils.reload();
|
||||
} else if (view === 'board-view-gantt') {
|
||||
window.localStorage.setItem('boardView', 'board-view-gantt'); //true
|
||||
Utils.reload();
|
||||
} else {
|
||||
window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
|
||||
Utils.reload();
|
||||
|
|
@ -289,6 +292,8 @@ Utils = {
|
|||
return 'board-view-lists';
|
||||
} else if (window.localStorage.getItem('boardView') === 'board-view-cal') {
|
||||
return 'board-view-cal';
|
||||
} else if (window.localStorage.getItem('boardView') === 'board-view-gantt') {
|
||||
return 'board-view-gantt';
|
||||
} else {
|
||||
window.localStorage.setItem('boardView', 'board-view-swimlanes'); //true
|
||||
Utils.reload();
|
||||
|
|
|
|||
|
|
@ -1,122 +1,20 @@
|
|||
# What is this?
|
||||
# Gantt chart
|
||||
|
||||
Original WeKan is MIT-licensed software.
|
||||
This new Gantt feature was added to MIT WeKan 2025-12-22 at https://github.com/wekan/wekan
|
||||
|
||||
This different Gantt version here currently uses Gantt chart component that has GPL license, so this Wekan Gantt version is GPL licensed.
|
||||
At "All Boards" page, click board to open one board view. There, Gantt is at top dropdown menu Swimlanes/Lists/Calendar/Gantt.
|
||||
|
||||
Sometime later if that GPL licensed Gantt chart component will be changed to MIT licensed one, then that original MIT-licensed WeKan will get Gantt feature, and maybe this GPL version will be discontinued.
|
||||
Gantt shows all dates, according to selected date format at opened card: Received Start Due End.
|
||||
|
||||
# How to use
|
||||
Gantt dates are shown for every week where exist dates at the current opened board.
|
||||
|
||||
[Source](https://github.com/wekan/wekan/issues/2870#issuecomment-721690105)
|
||||
You can click task name to open card.
|
||||
|
||||
At cards, both Start and End dates should be set (not Due date) for the tasks to be displayed.
|
||||
You can click any date icon to change that date, like: Received Start Due End.
|
||||
|
||||
# Funding for more features?
|
||||
# Old WeKan Gantt GPL
|
||||
|
||||
You can fund development of more features of Gantt at https://wekan.fi/commercial-support, like for example:
|
||||
- more of day/week/month/year views
|
||||
- drag etc
|
||||
|
||||
# Issue
|
||||
|
||||
https://github.com/wekan/wekan/issues/2870
|
||||
|
||||
# Install
|
||||
|
||||
Wekan GPLv2 Gantt version:
|
||||
- https://github.com/wekan/wekan-gantt-gpl
|
||||
- https://snapcraft.io/wekan-gantt-gpl
|
||||
- https://hub.docker.com/repository/docker/wekanteam/wekan-gantt-gpl
|
||||
- https://quay.io/wekan/wekan-gantt-gpl
|
||||
|
||||
## How to install Snap
|
||||
|
||||
[Like Snap install](https://github.com/wekan/wekan-snap/wiki/Install) but with commands like:
|
||||
```
|
||||
sudo snap install wekan-gantt-gpl
|
||||
|
||||
sudo snap set wekan-gantt-gpl root-url='http://localhost'
|
||||
|
||||
sudo snap set wekan-gantt-gpl port='80'
|
||||
```
|
||||
Stopping all:
|
||||
```
|
||||
sudo snap stop wekan-gantt-gpl
|
||||
```
|
||||
Stopping only some part:
|
||||
```
|
||||
sudo snap stop wekan-gantt-gpl.caddy
|
||||
|
||||
sudo snap stop wekan-gantt-gpl.mongodb
|
||||
|
||||
sudo snap stop wekan-gantt-gpl.wekan
|
||||
```
|
||||
|
||||
## Changing from Wekan to Wekan Gantt GPL
|
||||
|
||||
1) Install newest MongoDB to have also mongorestore available
|
||||
|
||||
2) Backup database and settings:
|
||||
```
|
||||
sudo snap stop wekan.wekan
|
||||
|
||||
mongodump --port 27019
|
||||
|
||||
snap get wekan > snap-set.sh
|
||||
|
||||
sudo snap remove wekan
|
||||
|
||||
sudo snap install wekan-gantt-gpl
|
||||
|
||||
sudo snap stop wekan-gantt-gpl.wekan
|
||||
|
||||
nano snap-set.sh
|
||||
```
|
||||
Then edit that textfile so all commands will be similar to this:
|
||||
```
|
||||
sudo snap set wekan-gantt-gpl root-url='https://example.com'
|
||||
```
|
||||
And run settings:
|
||||
```
|
||||
chmod +x snap-set.sh
|
||||
|
||||
./snap-set.sh
|
||||
|
||||
sudo snap start wekan-gantt-gpl.wekan
|
||||
```
|
||||
## Changing from Wekan Gantt GPL to Wekan
|
||||
|
||||
1) Install newest MongoDB to have also mongorestore available
|
||||
|
||||
2) Backup database and settings:
|
||||
```
|
||||
sudo snap stop wekan-gantt-gpl.wekan
|
||||
|
||||
mongodump --port 27019
|
||||
|
||||
snap get wekan-gantt-gpl > snap-set.sh
|
||||
|
||||
sudo snap remove wekan-gantt-gpl
|
||||
|
||||
sudo snap install wekan
|
||||
|
||||
sudo snap stop wekan.wekan
|
||||
|
||||
nano snap-set.sh
|
||||
```
|
||||
Then edit that textfile so all commands will be similar to this:
|
||||
```
|
||||
sudo snap set wekan root-url='https://example.com'
|
||||
```
|
||||
And run settings:
|
||||
```
|
||||
chmod +x snap-set.sh
|
||||
|
||||
./snap-set.sh
|
||||
|
||||
sudo snap start wekan.wekan
|
||||
```
|
||||
Previous GPLv2 WeKan Gantt is deprecated https://github.com/wekan/wekan-gantt-gpl
|
||||
|
||||
# UCS
|
||||
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@ Users.attachSchema(
|
|||
'board-view-swimlanes',
|
||||
'board-view-lists',
|
||||
'board-view-cal',
|
||||
'board-view-gantt',
|
||||
],
|
||||
},
|
||||
'profile.listSortBy': {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue