diff --git a/client/components/boards/boardBody.jade b/client/components/boards/boardBody.jade
index d2118112b..8357c857a 100644
--- a/client/components/boards/boardBody.jade
+++ b/client/components/boards/boardBody.jade
@@ -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
\ No newline at end of file
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index b0af16e43..18ff7dc59 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -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 = `
${TAPi18n.__('no-cards-in-gantt')}
`;
+ 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 += '';
+ html += '';
+ html += '';
+ const taskHeader = `${TAPi18n.__('task')} ${TAPi18n.__('predicate-week')} ${weekInfo.week}`;
+ html += `| ${taskHeader} | `;
+ weekdayLabels.forEach((lbl, idx) => {
+ const formattedDate = formatDateByUserPreference(weekDates[idx], dateFormat, false);
+ html += `${formattedDate} ${lbl} | `;
+ });
+ html += '
';
+
+ // Rows: include cards that have any date in this week
+ html += '';
+ 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 += '';
+ html += `| ${card.title} | `;
+
+ // 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 += `${cellContent} | `;
+ });
+
+ // Close row
+ html += '
';
+ });
+
+ // Close section for this week
+ html += '
';
+ });
+
+ 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');
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index bac4216ed..1129282b3 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -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
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index c84b593c6..292a6b042 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -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({
diff --git a/client/components/boards/gantt.css b/client/components/boards/gantt.css
new file mode 100644
index 000000000..6a14f4a3b
--- /dev/null
+++ b/client/components/boards/gantt.css
@@ -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;
+ }
+}
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 43ee28473..e0854d3f7 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -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();
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 6825f7032..c0bfb779f 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -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();
diff --git a/client/lib/utils.js b/client/lib/utils.js
index 078dfe967..ad64a4057 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -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();
diff --git a/docs/Features/Gantt.md b/docs/Features/Gantt.md
index de342e5b8..2881a1b80 100644
--- a/docs/Features/Gantt.md
+++ b/docs/Features/Gantt.md
@@ -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
diff --git a/models/users.js b/models/users.js
index 3885638d1..e30689359 100644
--- a/models/users.js
+++ b/models/users.js
@@ -393,6 +393,7 @@ Users.attachSchema(
'board-view-swimlanes',
'board-view-lists',
'board-view-cal',
+ 'board-view-gantt',
],
},
'profile.listSortBy': {