Merge pull request #34532 from frappe/gantt-integration

feat: improve calendar and gantt view
This commit is contained in:
Safwan 2026-02-17 11:05:46 +05:30 committed by GitHub
commit b9e3296078
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 74 additions and 36 deletions

View file

@ -2,4 +2,9 @@
// For license information, please see license.txt
frappe.views.calendar["{doctype}"] = {{
// field_map: {{
// start: "start_date",
// end: "end_date",
// }},
// gantt: true
}};

View file

@ -8,7 +8,6 @@ frappe.views.calendar["ToDo"] = {
id: "name",
title: "description",
allDay: "allDay",
progress: "progress",
},
gantt: true,
filters: [

View file

@ -75,7 +75,9 @@ frappe.views.ListViewSelect = class ListViewSelect {
action: () => this.set_route("dashboard"),
},
Calendar: {
condition: frappe.views.calendar[this.doctype],
condition:
frappe.views.calendar[this.doctype] ||
frappe.get_meta(this.doctype).is_calendar_and_gantt,
action: () => this.set_route("calendar", "default"),
current_view_handler: () => {
this.get_calendars().then((calendars) => {
@ -84,7 +86,9 @@ frappe.views.ListViewSelect = class ListViewSelect {
},
},
Gantt: {
condition: frappe.views.calendar[this.doctype],
condition:
frappe.views.calendar[this.doctype] ||
frappe.get_meta(this.doctype).is_calendar_and_gantt,
action: () => this.set_route("gantt"),
},
Inbox: {

View file

@ -1,5 +1,12 @@
frappe.provide("frappe.views");
const DEFAULT_FIELD_MAP = {
start: "start",
end: "end",
id: "name",
progress: "progress",
};
frappe.views.GanttView = class GanttView extends frappe.views.ListView {
get view_name() {
return "Gantt";
@ -9,6 +16,10 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
return super.setup_defaults().then(() => {
this.page_title = this.page_title + " " + __("Gantt");
this.calendar_settings = frappe.views.calendar[this.doctype] || {};
this.calendar_settings.field_map = {
...DEFAULT_FIELD_MAP,
...(this.calendar_settings.field_map || {}),
};
if (typeof this.calendar_settings.gantt == "object") {
Object.assign(this.calendar_settings, this.calendar_settings.gantt);
@ -35,32 +46,44 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
prepare_tasks() {
var me = this;
var meta = this.meta;
var field_map = this.calendar_settings.field_map;
let field_map = this.calendar_settings.field_map || DEFAULT_FIELD_MAP;
if (!this.data[0]?.[field_map.progress]) {
this.progress_disabled = true;
}
this.tasks = this.data.map(function (item) {
// set progress
var progress = 0;
if (field_map.progress && $.isFunction(field_map.progress)) {
if (typeof field_map.progress === "function") {
progress = field_map.progress(item);
} else if (field_map.progress) {
progress = item[field_map.progress];
}
// title
var label;
if (meta.title_field) {
let label;
if (field_map.title) {
label = item[field_map.title];
} else if (meta.title_field) {
label = item.progress
? __("{0} ({1}) - {2}%", [item[meta.title_field], item.name, item.progress])
: __("{0} ({1})", [item[meta.title_field], item.name]);
} else {
label = item[field_map.title];
label = item["name"];
}
var r = {
if (!item[field_map.start]) {
frappe.msgprint({
title: __("Incorrect configuration"),
message: __(
"Please configure the start field for this Doctype in the controller file."
),
indicator: "red",
});
}
const r = {
start: item[field_map.start],
end: item[field_map.end],
end: item[field_map.end] || item[field_map.start],
name: label,
id: item[field_map.id || "name"],
id: item[field_map.id],
doctype: me.doctype,
progress: progress,
dependencies: item.depends_on_tasks || "",
@ -93,20 +116,22 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
const date_format = "YYYY-MM-DD";
this.$result.empty();
this.$result.addClass("gantt-modern");
this.gantt = new Gantt(this.$result[0], this.tasks, {
bar_height: 35,
bar_corner_radius: 4,
resize_handle_width: 8,
resize_handle_height: 28,
resize_handle_corner_radius: 3,
resize_handle_offset: 4,
hover_on_date: true,
view_mode: gantt_view_mode,
date_format: "YYYY-MM-DD",
on_click: (task) => {
readonly: !me.can_write,
readonly_progress: this.progress_disabled,
fixed_duration: field_map.start == field_map.end,
on_double_click: (task) => {
frappe.set_route("Form", task.doctype, task.id);
},
on_date_click: (date) => {
console.log(date);
if (date) frappe.new_doc("ToDo", { date: new Date(date) });
},
on_date_change: (task, start, end) => {
if (!me.can_write) return;
frappe.db.set_value(task.doctype, task.id, {
@ -116,11 +141,11 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
},
on_progress_change: (task, progress) => {
if (!me.can_write) return;
var progress_fieldname = "progress";
let progress_fieldname;
if ($.isFunction(field_map.progress)) {
if (typeof field_map.progress === "function") {
progress_fieldname = null;
} else if (field_map.progress) {
} else if (field_map.progress && task[field_map.progress]) {
progress_fieldname = field_map.progress;
}
@ -133,14 +158,13 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
on_view_change: (mode) => {
// save view mode
me.save_view_user_settings({
gantt_view_mode: mode,
gantt_view_mode: mode.name,
});
},
custom_popup_html: (task) => {
popup: ({ task }) => {
var item = me.get_item(task.id);
var html = `<div class="title">${task.name}</div>
<div class="subtitle">${moment(task._start).format("MMM D")} - ${moment(task._end).format(
<div class="subtitle">${moment(task.start).format("MMM D")} - ${moment(task.end).format(
"MMM D"
)}</div>`;
@ -170,9 +194,9 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
${view_modes
.map(
(value) => `<button type="button"
class="btn btn-default btn-sm btn-view-mode ${active_class(value)}"
data-value="${value}">
${__(value)}
class="btn btn-default btn-sm btn-view-mode ${active_class(value.name)}"
data-value="${value.name}">
${__(value.name)}
</button>`
)
.join("")}
@ -226,7 +250,7 @@ frappe.views.GanttView = class GanttView extends frappe.views.ListView {
get required_libs() {
return [
"assets/frappe/node_modules/frappe-gantt/dist/frappe-gantt.css",
"assets/frappe/node_modules/frappe-gantt/dist/frappe-gantt.min.js",
"assets/frappe/node_modules/frappe-gantt/dist/frappe-gantt.umd.js",
];
}
};

View file

@ -1,3 +1,9 @@
.gantt {
.grid-column:hover {
cursor: copy;
}
}
.gantt-modern .gantt {
.bar {
fill: white;

View file

@ -53,7 +53,7 @@
"fast-glob": "^3.2.5",
"frappe-charts": "2.0.0-rc27",
"frappe-datatable": "1.19.0",
"frappe-gantt": "^0.6.0",
"frappe-gantt": "^1.1.0",
"frappe-quill-image-resize": "^3.0.9",
"gemoji": "^8.1.0",
"highlight.js": "^10.4.1",

View file

@ -1440,10 +1440,10 @@ frappe-datatable@1.19.0:
lodash "^4.17.5"
sortablejs "^1.7.0"
frappe-gantt@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/frappe-gantt/-/frappe-gantt-0.6.1.tgz#57ae0b5f024101fc7cd5ba92f605de97dba9c9a1"
integrity sha512-1cSU9vLbwypjzaxnCfnEE03Xr3HlAV2S8dRtjxw62o+amkx1A8bBIFd2jp84mcDdTCM77Ij4LzZBslAKZB8oMg==
frappe-gantt@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/frappe-gantt/-/frappe-gantt-1.1.0.tgz#b889053357a117606a74d934d288aad605df1b0d"
integrity sha512-ex3QNuYU7nTNKtkC5MSoUhnW8YhZOCmNC1W+Xp4hSJTOyiZhC405JChkvDh66CkMSPlMHaASdaWQZ2nC0MhMFA==
frappe-quill-image-resize@^3.0.9:
version "3.0.9"