Project

General

Profile

Patch #44567 » 0001-Refactor-and-modernize-Gantt-chart-layout.patch

Katsuya HIDAKA, 2026-10-01 10:10

View differences:

app/assets/stylesheets/gantt.css
19 19
}
20 20

  
21 21
/***** Gantt chart *****/
22
table.gantt-table {
22
.gantt-chart {
23
  display: flex;
24
  align-items: flex-start;
23 25
  inline-size: 100%;
24
  border-collapse: collapse;
26
  margin-block-end: 1em;
27
  overflow: hidden;
25 28
  /* Force LTR for Gantt even in RTL locales because column resizing, relation lines, and the progress line don't work in RTL yet. */
26 29
  direction: ltr;
27 30
}
28 31

  
29
table.gantt-table td {
30
  padding: 0;
32
.gantt-chart [hidden] {
33
  display: none !important;
31 34
}
32 35

  
33
.gantt_hdr {
34
  position: absolute;
35
  inset-block-start: 0;
36
  block-size: 16px;
36
.gantt_subjects_column,
37
.gantt_selected_column {
38
  position: relative;
39
  flex: 0 0 var(--gantt-column-width);
40
  inline-size: var(--gantt-column-width);
41
  min-inline-size: 0;
42
}
43

  
44
.gantt-chart .ui-resizable-e {
45
  cursor: ew-resize;
46
}
47

  
48
.gantt-pane {
49
  block-size: calc(var(--gantt-pane-height) + 2px);
37 50
  border-block: 1px solid var(--oc-gray-4);
51
  border-inline-end: 1px solid var(--oc-gray-4);
52
  box-sizing: border-box;
53
  overflow: hidden;
54
}
55

  
56
.gantt_subjects_container {
38 57
  border-inline-start: 1px solid var(--oc-gray-4);
39
  text-align: center;
58
}
59

  
60
.gantt-pane-header {
61
  position: relative;
62
  z-index: 2;
63
  display: flex;
64
  align-items: center;
65
  justify-content: center;
66
  block-size: var(--gantt-headers-height);
67
  border-block-end: 1px solid var(--oc-gray-4);
68
  box-sizing: border-box;
69
  background: var(--oc-gray-1);
70
  font-size: 0.8em;
40 71
  overflow: hidden;
41 72
}
42 73

  
43
#gantt_area .gantt_hdr {
44
  border-inline-start: 0;
74
.gantt-selected-column-header {
75
  padding-inline: 3px;
76
  text-overflow: ellipsis;
77
  white-space: nowrap;
78
}
79

  
80
.gantt-pane-body {
81
  position: relative;
82
  block-size: var(--gantt-content-height);
83
}
84

  
85
.gantt-timeline {
86
  position: relative;
87
  flex: 1 1 auto;
88
  min-inline-size: 0;
89
  /* Keep the canvas border inside the overflow clipping edge. */
90
  padding-block-end: 1px;
91
  overflow-x: auto;
92
  overflow-y: hidden;
93
}
94

  
95
.gantt-timeline-canvas {
96
  position: relative;
97
  inline-size: calc(var(--gantt-chart-width) + 1px);
98
  block-size: calc(var(--gantt-pane-height) + 2px);
99
  border-block: 1px solid var(--oc-gray-4);
45 100
  border-inline-end: 1px solid var(--oc-gray-4);
101
  box-sizing: border-box;
102
}
103

  
104
.gantt-timeline-header {
105
  position: relative;
106
  z-index: 1;
107
  block-size: var(--gantt-headers-height);
108
  border-block-end: 1px solid var(--oc-gray-4);
109
  box-sizing: border-box;
110
  background: var(--oc-gray-1);
46 111
}
47 112

  
48
.gantt_subjects_container:not(.draw_selected_columns) .gantt_hdr,
49
.last_gantt_selected_column .gantt_hdr {
113
.gantt-header-row {
114
  display: flex;
115
  block-size: var(--gantt-header-height);
116
}
117

  
118
.gantt-period {
119
  position: relative;
120
  flex: 0 0 var(--gantt-period-width);
121
  inline-size: var(--gantt-period-width);
122
  min-inline-size: 0;
123
  block-size: 100%;
50 124
  border-inline-end: 1px solid var(--oc-gray-4);
125
  border-block-end: 1px solid var(--oc-gray-4);
126
  text-align: center;
127
  box-sizing: border-box;
128
  overflow: hidden;
129
  white-space: nowrap;
130
  pointer-events: none;
51 131
}
52 132

  
53
.last_gantt_selected_column .gantt_selected_column_container,
54
.gantt_subjects_container .gantt_subjects * {
55
  z-index: 10;
133
.gantt-header-row:last-child .gantt-period {
134
  border-block-end: 0;
56 135
}
57 136

  
58
.gantt_subjects_column + td {
59
  padding: 0;
137
.gantt-period-last {
138
  border-inline-end: 0;
139
}
140

  
141
.gantt-period-spans-body {
142
  block-size: calc(100% + var(--gantt-content-height));
143
  border-block-end: 0;
144
}
145

  
146
.gantt-period a {
147
  position: relative;
148
  z-index: 1;
149
  pointer-events: auto;
150
}
151

  
152
.gantt-period-day,
153
.gantt-period-day-number {
154
  font-size: 0.7em;
60 155
}
61 156

  
62
.gantt_hdr.nwday {
157
.gantt-period-non-working {
63 158
  background-color: var(--oc-gray-1);
64 159
  color: var(--oc-gray-6);
65 160
}
66 161

  
162
.gantt-timeline-body {
163
  position: absolute;
164
  z-index: 2;
165
  inset-block-start: var(--gantt-headers-height);
166
  inset-inline-start: 0;
167
  inline-size: var(--gantt-chart-width);
168
  block-size: var(--gantt-content-height);
169
}
170

  
67 171
.gantt_subjects,
68
.gantt_selected_column_content.gantt_hdr {
172
.gantt_selected_column_content {
173
  position: relative;
174
  block-size: 100%;
69 175
  font-size: 0.8em;
176
}
177

  
178
.gantt_subjects form,
179
.gantt_selected_column_content,
180
.gantt-timeline-body form {
70 181
  position: relative;
71
  z-index: 1;
182
  display: block;
183
  inline-size: 100%;
184
  block-size: 100%;
72 185
}
73 186

  
74
.gantt_subjects div,
75
.gantt_selected_column_content div {
187
.gantt-row {
188
  position: absolute;
189
  inset-block-start: var(--gantt-row-top);
190
  block-size: 20px;
191
  box-sizing: border-box;
192
}
193

  
194
.gantt-subject-row {
195
  inset-inline-start: var(--gantt-row-indent);
196
  inline-size: var(--gantt-row-width);
76 197
  line-height: 16px;
77
  block-size: 16px;
78 198
  overflow: hidden;
79 199
  white-space: nowrap;
80
  text-overflow: clip;
200
}
201

  
202
.gantt-column-row {
203
  inset-inline-start: 0;
81 204
  inline-size: 100%;
205
  padding-inline-start: 3px;
206
  line-height: 16px;
207
  overflow: hidden;
208
  white-space: nowrap;
82 209
}
83 210

  
84 211
.gantt_subjects div.issue-subject:hover {
85 212
  background-color: var(--oc-yellow-0);
86 213
}
87 214

  
88
.gantt_selected_column_content > div {
89
  padding-inline-start: 3px;
90
  box-sizing: border-box;
215
.gantt-timeline-row {
216
  inset-inline-start: 0;
217
  inline-size: var(--gantt-chart-width);
91 218
}
92 219

  
93
.gantt_hdr_selected_column_name {
220
#today_line {
94 221
  position: absolute;
95
  inset-block-start: 50%;
96
  inline-size: 100%;
97
  transform: translateY(-50%);
98
  -webkit-transform: translateY(-50%);
99
  font-size: 0.8em;
100
  overflow: hidden;
101
  text-overflow: ellipsis;
102
  white-space: nowrap;
103
}
104

  
105
td.gantt_selected_column {
106
  inline-size: 50px;
222
  inset-block-start: 1px;
223
  inset-inline-start: var(--gantt-today-start);
224
  inline-size: 10px;
225
  block-size: var(--gantt-content-height);
226
  border-inline-start: 1px dashed red;
227
  pointer-events: none;
107 228
}
108 229

  
109
td.gantt_selected_column .gantt_hdr,
110
.gantt_selected_column_container {
111
  inline-size: 49px;
230
#gantt_draw_area {
231
  position: absolute;
232
  inset-block-start: 1px;
233
  inset-inline-start: 0;
234
  inline-size: var(--gantt-chart-width);
235
  block-size: var(--gantt-content-height);
236
  pointer-events: none;
112 237
}
113 238

  
114
td.gantt_watcher_users_column div.issue_watcher_users ul {
239
.gantt_watcher_users_column div.issue_watcher_users ul {
115 240
  margin: 0;
116 241
  padding: 0;
117 242
  list-style: none;
118 243
}
119 244

  
120
td.gantt_watcher_users_column div.issue_watcher_users ul li {
245
.gantt_watcher_users_column div.issue_watcher_users ul li {
121 246
  display: inline;
122 247
}
123 248

  
124
td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
249
.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
125 250
  content: ', ';
126 251
  white-space: pre;
127 252
}
128 253

  
129 254
.task {
130 255
  position: absolute;
256
  inset-block-start: var(--gantt-row-top);
257
  inset-inline-start: var(--gantt-task-start);
258
  inline-size: var(--gantt-task-width);
131 259
  block-size: 8px;
132 260
  font-size: 0.8em;
133 261
  color: var(--oc-gray-6);
......
137 265
  white-space: nowrap;
138 266
}
139 267

  
268
.gantt-timeline-body .tooltip {
269
  position: absolute;
270
  inset-block-start: var(--gantt-row-top);
271
  inset-inline-start: var(--gantt-task-start);
272
  inline-size: var(--gantt-task-width);
273
  block-size: 12px;
274
}
275

  
276
.gantt-row > :is(.task, .tooltip) {
277
  inset-block-start: 0;
278
}
279

  
280
.gantt-chart .toggle-selection {
281
  display: none;
282
}
283

  
140 284
.task.label {
141
  inline-size: 100%;
285
  inline-size: var(--gantt-task-width);
142 286
}
143 287

  
144 288
.task.label.project,
......
172 316
  block-size: 3px;
173 317
}
174 318

  
175
.task.parent.marker.starting {
176
  position: absolute;
177
  background: url(/task_parent_end.png) no-repeat 0 0;
178
  inline-size: 8px;
179
  block-size: 16px;
180
  margin-inline-start: -4px;
181
  inset-inline-start: 0;
182
  inset-block-start: -1px;
183
}
184

  
319
.task.parent.marker.starting,
185 320
.task.parent.marker.ending {
186 321
  position: absolute;
187 322
  background: url(/task_parent_end.png) no-repeat 0 0;
188 323
  inline-size: 8px;
189 324
  block-size: 16px;
190 325
  margin-inline-start: -4px;
191
  inset-inline-end: 0;
192
  inset-block-start: -1px;
193 326
}
194 327

  
195 328
.version.task_late {
......
266 399
.project-overdue a {
267 400
  color: var(--oc-red-8);
268 401
}
402

  
403
@media print {
404
  /* Include the full timeline in the browser print layout. */
405
  body.controller-gantts.action-show #wrapper {
406
    overflow: visible;
407
  }
408

  
409
  .gantt-chart {
410
    inline-size: max-content;
411
    overflow: visible;
412
  }
413

  
414
  .gantt-timeline {
415
    flex: 0 0 calc(var(--gantt-chart-width) + 1px);
416
    inline-size: calc(var(--gantt-chart-width) + 1px);
417
    overflow: visible;
418
  }
419
}
app/assets/stylesheets/responsive.css
667 667
  }
668 668

  
669 669
  /* Gantt charts */
670
  /*
671
   * [1] override inline styles with important
672
   * [2] keep border between subjects and gantt area
673
   * [3] remove whitespace between subjects and gantt area
674
   * [4] maintain width due to [3]
675
   */
676 670
  .gantt_subjects_column {
677
    inline-size: 60% !important; /* [1] */
671
    --gantt-column-width: 60% !important;
678 672
  }
679 673

  
680 674
  .gantt_subjects_container {
......
682 676
    overflow: hidden;
683 677
  }
684 678

  
685
  .gantt_subjects_column .gantt_hdr {
686
    inline-size: 100% !important;
687
    inset-inline-end: 0 !important; /* [2] */
688
    border-inline-end: solid 1px var(--oc-gray-4);
689
  }
690

  
691
  #gantt_area {
692
    inset-inline-start: -2px; /* [3] */
693
    margin-inline-end: -2px; /* [4] */
694
  }
695

  
696 679
  /*----------------------------------------*\
697 680
    G) FORMS
698 681
  \*----------------------------------------*/
app/helpers/gantt_helper.rb
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20 20
module GanttHelper
21
  class ChartLayout
22
    # A calendar period displayed in the timeline header.
23
    Period = Struct.new(
24
      # The first date of the period, or nil for a leading partial week
25
      :date,
26
      # The period width in pixels
27
      :width,
28
      # Whether the period represents a non-working day
29
      :non_working,
30
      # Whether the period's grid line extends through the chart body
31
      :spans_body,
32
      # Whether the period is the last one in its header row
33
      :last,
34
      keyword_init: true
35
    )
36

  
37
    # The full timeline width in pixels
38
    attr_reader :chart_width
39
    # The height of one timeline header row in pixels
40
    attr_reader :header_height
41
    # The combined height of all visible header rows in pixels
42
    attr_reader :headers_height
43
    # The gap between the header and the first chart row in pixels
44
    attr_reader :content_top
45
    # The subject pane width used to render its rows in pixels
46
    attr_reader :subject_width
47
    # The number of pixels representing one day
48
    attr_reader :zoom
49

  
50
    def initialize(gantt)
51
      @gantt = gantt
52
      @zoom = 2**gantt.zoom
53
      @subject_width = 330
54
      @header_height = 18
55
      @content_top = 8
56
      @headers_height = header_rows * header_height
57
      @chart_width = ((gantt.date_to - gantt.date_from + 1) * zoom).to_i
58
    end
59

  
60
    def content_height
61
      @content_height ||= [(20 * (@gantt.number_of_rows + 6)) + 150, 206].max
62
    end
63

  
64
    def pane_height
65
      headers_height + content_height
66
    end
67

  
68
    def months
69
      date = @gantt.date_from
70

  
71
      Array.new(@gantt.months) do |index|
72
        width = (((date >> 1) - date) * zoom).to_i
73
        period = Period.new(
74
          date: date,
75
          width: width,
76
          spans_body: !show_weeks?,
77
          last: index == @gantt.months - 1
78
        )
79
        date >>= 1
80
        period
81
      end
82
    end
83

  
84
    def weeks
85
      return [] unless show_weeks?
86

  
87
      date = @gantt.date_from
88
      periods = []
89
      unless date.cwday == 1
90
        width = (7 - date.cwday + 1) * zoom
91
        periods << period(
92
          nil, width,
93
          spans_body: !show_days?,
94
          last: date + 7 - date.cwday >= @gantt.date_to
95
        )
96
        date += 7 - date.cwday + 1
97
      end
98

  
99
      while date <= @gantt.date_to
100
        width = ((date + 6 <= @gantt.date_to) ? 7 * zoom : (@gantt.date_to - date + 1) * zoom).to_i
101
        periods << period(
102
          date, width,
103
          spans_body: !show_days?,
104
          last: date + 6 >= @gantt.date_to
105
        )
106
        date += 7
107
      end
108
      periods
109
    end
110

  
111
    def day_numbers
112
      return [] unless show_day_numbers?
113

  
114
      day_periods
115
    end
116

  
117
    def days
118
      return [] unless show_days?
119

  
120
      day_periods(spans_body: true)
121
    end
122

  
123
    def today_start
124
      return unless User.current.today.between?(@gantt.date_from, @gantt.date_to)
125

  
126
      (((User.current.today - @gantt.date_from + 1) * zoom).floor - 1).to_i
127
    end
128

  
129
    def show_weeks?
130
      @gantt.zoom > 1
131
    end
132

  
133
    def show_days?
134
      @gantt.zoom > 2
135
    end
136

  
137
    def show_day_numbers?
138
      @gantt.zoom > 3
139
    end
140

  
141
    private
142

  
143
    def header_rows
144
      1 + (@gantt.zoom > 1 ? 1 : 0) + (@gantt.zoom > 2 ? 1 : 0) + (@gantt.zoom > 3 ? 1 : 0)
145
    end
146

  
147
    def day_periods(spans_body: false)
148
      (@gantt.date_from..@gantt.date_to).map do |date|
149
        Period.new(
150
          date: date,
151
          width: zoom,
152
          non_working: @gantt.non_working_week_days.include?(date.cwday),
153
          spans_body: spans_body,
154
          last: date == @gantt.date_to
155
        )
156
      end
157
    end
158

  
159
    def period(date, width, spans_body: false, last: false)
160
      Period.new(date: date, width: width, spans_body: spans_body, last: last)
161
    end
162
  end
163

  
164
  def gantt_css_variables(variables)
165
    variables.map {|name, value| "--#{name}:#{value}"}.join(';')
166
  end
167

  
168
  def gantt_period_style(period)
169
    gantt_css_variables(
170
      'gantt-period-width': "#{period.width}px"
171
    )
172
  end
173

  
21 174
  def gantt_zoom_link(gantt, in_or_out)
22 175
    case in_or_out
23 176
    when :in
......
42 195
    end
43 196
  end
44 197

  
45
  def gantt_chart_tag(query, &)
198
  def gantt_chart_tag(query, layout, &block)
46 199
    data_attributes = {
47 200
      controller: 'gantt--chart',
48 201
      # Events emitted by child controllers the chart listens to.
......
62 215
      'gantt--chart-show-progress-value': query.draw_progress_line ? 'true' : 'false'
63 216
    }
64 217

  
65
    tag.table(class: 'gantt-table', data: data_attributes, &)
218
    style = gantt_css_variables(
219
      'gantt-subject-width': "#{layout.subject_width + 1}px",
220
      'gantt-header-height': "#{layout.header_height}px",
221
      'gantt-headers-height': "#{layout.headers_height}px",
222
      'gantt-chart-width': "#{layout.chart_width}px",
223
      'gantt-content-top': "#{layout.content_top}px",
224
      'gantt-content-height': "#{layout.content_height}px",
225
      'gantt-pane-height': "#{layout.pane_height}px"
226
    )
227

  
228
    tag.div(class: 'gantt-chart', style: style, data: data_attributes) do
229
      capture(layout, &block)
230
    end
66 231
  end
67 232

  
68 233
  def gantt_column_tag(column_name, min_width: nil, **options, &)
......
74 239
    }
75 240
    options[:class] = ["gantt_#{column_name}_column", options[:class]]
76 241

  
77
    tag.td(**options, &)
242
    options[:style] = gantt_css_variables('gantt-column-width': options.delete(:width)) if options[:width]
243

  
244
    tag.div(**options, &)
78 245
  end
79 246

  
80 247
  def gantt_subjects_tag(&)
app/javascript/controllers/gantt/chart_controller.js
15 15

  
16 16
  #drawTop = 0
17 17
  #drawRight = 0
18
  #drawLeft = 0
19 18
  #drawPaper = null
20 19
  #drawPaperGroup = null
21 20

  
......
26 25
  connect() {
27 26
    this.#drawTop = 0
28 27
    this.#drawRight = 0
29
    this.#drawLeft = 0
30 28

  
31 29
    this.#drawProgressLineAndRelations()
32 30
    this.#drawSelectedColumns()
......
127 125

  
128 126
  #setupDrawArea() {
129 127
    const $drawArea = this.$(this.drawAreaTarget)
130
    const $ganttArea = this.hasGanttAreaTarget ? this.$(this.ganttAreaTarget) : null
131 128

  
132 129
    this.#drawTop = $drawArea.position().top
133 130
    this.#drawRight = $drawArea.width()
134
    this.#drawLeft = $ganttArea ? $ganttArea.scrollLeft() : 0
135 131
  }
136 132

  
137 133
  #drawSelectedColumns() {
138
    const $selectedColumns = this.$("td.gantt_selected_column")
134
    const selectedColumns = this.element.querySelectorAll(".gantt_selected_column")
139 135
    const $subjectsContainer = this.$(".gantt_subjects_container")
140 136

  
141 137
    const isMobileDevice = typeof window.isMobile === "function" && window.isMobile()
142 138

  
143 139
    if (this.showSelectedColumnsValue) {
144 140
      if (isMobileDevice) {
145
        $selectedColumns.each((_, element) => {
146
          this.$(element).hide()
147
        })
141
        selectedColumns.forEach((element) => { element.hidden = true })
148 142
      } else {
149 143
        $subjectsContainer.addClass("draw_selected_columns")
150
        $selectedColumns.show()
144
        selectedColumns.forEach((element) => { element.hidden = false })
151 145
      }
152 146
    } else {
153
      $selectedColumns.each((_, element) => {
154
        this.$(element).hide()
155
      })
147
      selectedColumns.forEach((element) => { element.hidden = true })
156 148
      $subjectsContainer.removeClass("draw_selected_columns")
157 149
    }
158 150
  }
......
193 185
      if (!issueTo.is(":visible")) return
194 186

  
195 187
      const issueHeight = issueFrom.height()
196
      const issueFromTop = issueFrom.position().top + issueHeight / 2 - this.#drawTop
188
      const issueFromTop = this.#taskTop(issueFrom) + issueHeight / 2 - this.#drawTop
197 189
      const issueFromRight = issueFrom.position().left + issueFrom.width()
198
      const issueToTop = issueTo.position().top + issueHeight / 2 - this.#drawTop
190
      const issueToTop = this.#taskTop(issueTo) + issueHeight / 2 - this.#drawTop
199 191
      const issueToLeft = issueTo.position().left
200 192
      const relationConfig = this.issueRelationTypesValue[relation.rel_type] || {}
201 193
      const color = relationConfig.color || "#000"
......
206 198
      this.#drawPath(
207 199
        [
208 200
          "M",
209
          issueFromRight + this.#drawLeft,
201
          issueFromRight,
210 202
          issueFromTop,
211 203
          "L",
212
          issueFromRightRel + this.#drawLeft,
204
          issueFromRightRel,
213 205
          issueFromTop
214 206
        ],
215 207
        { stroke: color, "stroke-width": RELATION_STROKE_WIDTH, fill: "none" }
......
219 211
        this.#drawPath(
220 212
          [
221 213
            "M",
222
            issueFromRightRel + this.#drawLeft,
214
            issueFromRightRel,
223 215
            issueFromTop,
224 216
            "L",
225
            issueFromRightRel + this.#drawLeft,
217
            issueFromRightRel,
226 218
            issueToTop
227 219
          ],
228 220
          { stroke: color, "stroke-width": RELATION_STROKE_WIDTH, fill: "none" }
......
230 222
        this.#drawPath(
231 223
          [
232 224
            "M",
233
            issueFromRightRel + this.#drawLeft,
225
            issueFromRightRel,
234 226
            issueToTop,
235 227
            "L",
236
            issueToLeft + this.#drawLeft,
228
            issueToLeft,
237 229
            issueToTop
238 230
          ],
239 231
          { stroke: color, "stroke-width": RELATION_STROKE_WIDTH, fill: "none" }
......
243 235
        this.#drawPath(
244 236
          [
245 237
            "M",
246
            issueFromRightRel + this.#drawLeft,
238
            issueFromRightRel,
247 239
            issueFromTop,
248 240
            "L",
249
            issueFromRightRel + this.#drawLeft,
241
            issueFromRightRel,
250 242
            issueMiddleTop
251 243
          ],
252 244
          { stroke: color, "stroke-width": RELATION_STROKE_WIDTH, fill: "none" }
......
254 246
        this.#drawPath(
255 247
          [
256 248
            "M",
257
            issueFromRightRel + this.#drawLeft,
249
            issueFromRightRel,
258 250
            issueMiddleTop,
259 251
            "L",
260
            issueToLeftRel + this.#drawLeft,
252
            issueToLeftRel,
261 253
            issueMiddleTop
262 254
          ],
263 255
          { stroke: color, "stroke-width": RELATION_STROKE_WIDTH, fill: "none" }
......
265 257
        this.#drawPath(
266 258
          [
267 259
            "M",
268
            issueToLeftRel + this.#drawLeft,
260
            issueToLeftRel,
269 261
            issueMiddleTop,
270 262
            "L",
271
            issueToLeftRel + this.#drawLeft,
263
            issueToLeftRel,
272 264
            issueToTop
273 265
          ],
274 266
          { stroke: color, "stroke-width": RELATION_STROKE_WIDTH, fill: "none" }
......
276 268
        this.#drawPath(
277 269
          [
278 270
            "M",
279
            issueToLeftRel + this.#drawLeft,
271
            issueToLeftRel,
280 272
            issueToTop,
281 273
            "L",
282
            issueToLeft + this.#drawLeft,
274
            issueToLeft,
283 275
            issueToTop
284 276
          ],
285 277
          { stroke: color, "stroke-width": RELATION_STROKE_WIDTH, fill: "none" }
......
288 280
      this.#drawPath(
289 281
        [
290 282
          "M",
291
          issueToLeft + this.#drawLeft,
283
          issueToLeft,
292 284
          issueToTop,
293 285
          "l",
294 286
          -4 * RELATION_STROKE_WIDTH,
......
306 298
    })
307 299
  }
308 300

  
301
  #taskTop($task) {
302
    const row = $task.closest(".gantt-row")
303
    return row.position().top + $task.position().top
304
  }
305

  
309 306
  get #progressLinesArray() {
310 307
    const lines = []
311 308
    const todayLeft = this.$("#today_line").position().left
......
382 379
          (previous.is_left_edge && current.is_left_edge)
383 380
        )
384 381
      ) {
385
        const x1 = previous.left === 0 ? 0 : previous.left + this.#drawLeft
386
        const x2 = current.left === 0 ? 0 : current.left + this.#drawLeft
382
        const x1 = previous.left
383
        const x2 = current.left
387 384

  
388 385
        this.#drawPath(["M", x1, previous.top, "L", x2, current.top], {
389 386
          stroke: color,
......
393 390
      }
394 391
    }
395 392
  }
393

  
396 394
}
app/javascript/controllers/gantt/column_controller.js
42 42
  }
43 43

  
44 44
  #setupResizable() {
45
    const alsoResize = [
46
      `.gantt_${this.columnValue}_container`,
47
      `.gantt_${this.columnValue}_container > .gantt_hdr`
48
    ]
49 45
    const options = {
50 46
      handles: "e",
51 47
      minWidth: this.minWidthValue,
52 48
      zIndex: 30,
53
      alsoResize: alsoResize.join(","),
54
      create: () => {
55
        this.$(".ui-resizable-e").css("cursor", "ew-resize")
49
      resize: (_event, ui) => {
50
        this.element.style.setProperty("--gantt-column-width", `${ui.size.width}px`)
51
        this.element.style.removeProperty("width")
56 52
      }
57 53
    }
58 54

  
app/javascript/controllers/gantt/subjects_controller.js
33 33
      const $element = this.$(element)
34 34
      const json = $element.data("collapse-expand")
35 35
      const numberOfRows = $element.data("number-of-rows")
36
      const barsSelector = `#gantt_area form > div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
37
      const selectedColumnsSelector = `td.gantt_selected_column div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
36
      const barsSelector = `#gantt_area .gantt-timeline-body form > div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
37
      const selectedColumnsSelector = `.gantt_selected_column div[data-collapse-expand='${json.obj_id}'][data-number-of-rows='${numberOfRows}']`
38 38

  
39 39
      if (outOfHierarchy || this.#readInlineStart($element) <= subjectInlineStart) {
40 40
        outOfHierarchy = true
......
106 106
  #setBlockStart(el, value) {
107 107
    const node = el.jquery ? el[0] : el
108 108
    const px = typeof value === "number" ? `${value}px` : value
109
    node.style.setProperty("inset-block-start", px)
109
    node.style.setProperty("--gantt-row-top", px)
110 110
  }
111 111

  
112 112
  #setIconState(element, open) {
app/views/gantts/_chart.html.erb
1 1
<%
2
  zoom = 1
3
  gantt.zoom.times { zoom *= 2 }
4

  
5
  subject_width = 330
6
  header_height = 18
7

  
8
  headers_height = header_height
9
  show_weeks = false
10
  show_days = false
11
  show_day_num = false
12

  
13
  if gantt.zoom > 1
14
    show_weeks = true
15
    headers_height = 2 * header_height
16
    if gantt.zoom > 2
17
      show_days = true
18
      headers_height = 3 * header_height
19
      if gantt.zoom > 3
20
        show_day_num = true
21
        headers_height = 4 * header_height
22
      end
23
    end
24
  end
25

  
26
  g_width = ((gantt.date_to - gantt.date_from + 1) * zoom).to_i
2
  layout = GanttHelper::ChartLayout.new(gantt)
27 3
  gantt.render(
28
    top: headers_height + 8,
29
    zoom: zoom,
30
    g_width: g_width,
31
    subject_width: subject_width
4
    top: layout.content_top,
5
    zoom: layout.zoom,
6
    g_width: layout.chart_width,
7
    subject_width: layout.subject_width
32 8
  )
33
  g_height = [(20 * (gantt.number_of_rows + 6)) + 150, 206].max
34
  t_height = g_height + headers_height
35 9
%>
36 10

  
37 11
<% if gantt.truncated %>
38 12
  <p class="warning"><%= l(:notice_gantt_chart_truncated, max: gantt.max_rows) %></p>
39 13
<% end %>
40 14

  
41
<%= gantt_chart_tag(@query) do %>
42
  <tr>
43
    <%= gantt_column_tag('subjects', min_width: 100,
44
                         style: "width:#{query.draw_selected_columns ? subject_width + 1 : subject_width + 2}px;") do %>
45
      <%
46
        subjects_container_classes = "gantt_subjects_container"
47
        subjects_container_classes << " draw_selected_columns" if query.draw_selected_columns
48
        subjects_container_style = +"position:relative;"
49
        subjects_container_style << "height: #{t_height + 24}px;"
50
        subjects_container_style << "width: #{subject_width + 1}px;"
51
      %>
52
      <%= content_tag(:div,
53
                      style: subjects_container_style,
54
                      class: subjects_container_classes,
55
                      data: {'gantt--chart-target': 'subjectsContainer'}) do %>
56
        <%
57
          header_bg_style = +"width: #{subject_width + 1}px;"
58
          header_bg_style << "height: #{headers_height}px;"
59
          header_bg_style << 'background: #f1f3f5;' # oc-gray-1
60
        %>
61
        <%= content_tag(:div, "", style: header_bg_style, class: "gantt_hdr") %>
62
        <%
63
          header_overlay_style = +"z-index: 1;"
64
          header_overlay_style << "width: #{subject_width + 1}px;"
65
          header_overlay_style << "height: #{t_height}px;"
66
          header_overlay_style << 'overflow: hidden;'
67
        %>
68
        <%= content_tag(:div, "", style: header_overlay_style, class: "gantt_hdr") %>
15
<%= gantt_chart_tag(@query, layout) do %>
16
  <%= gantt_column_tag('subjects', min_width: 100, width: 'var(--gantt-subject-width)') do %>
17
    <section class="gantt-pane gantt_subjects_container<%= ' draw_selected_columns' if query.draw_selected_columns %>"
18
             data-gantt--chart-target="subjectsContainer">
19
      <header class="gantt-pane-header" aria-hidden="true"></header>
20
      <div class="gantt-pane-body">
69 21
        <%= gantt_subjects_tag do %>
70 22
          <%= form_tag({}, data: {cm_url: issues_context_menu_path}) do %>
71 23
            <%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %>
72 24
            <%= gantt.subjects.html_safe %>
73 25
          <% end %>
74 26
        <% end %>
75
      <% end %>
76
    <% end %>
77
    <%
78
      query.columns.each do |column|
79
        next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name)
80
        column_name = column.name.to_s.tr('.', '_')
81
    %>
82
      <%= gantt_column_tag(column_name, min_width: 20, id: column_name,
83
                           class: ['gantt_selected_column', { 'last_gantt_selected_column': query.columns.last == column }]) do %>
84
        <%
85
          column_container_style = +"position: relative;"
86
          column_container_style << "height: #{t_height + 24}px;"
87
        %>
88
        <%= content_tag(:div, style: column_container_style, class: "gantt_#{column_name}_container gantt_selected_column_container") do %>
89
          <%
90
            column_header_overlay_style = +"height: #{t_height}px;"
91
            column_header_overlay_style << 'overflow: hidden;'
92
          %>
93
          <%= content_tag(:div, '', style: column_header_overlay_style, class: "gantt_hdr") %>
94
          <%
95
            column_header_style = +"height: #{headers_height}px;"
96
            column_header_style << 'background: #f1f3f5;' # oc-gray-1
97
          %>
98
          <%= content_tag(:div,
99
                          content_tag(:p, column.caption, class: 'gantt_hdr_selected_column_name'),
100
                          style: column_header_style,
101
                          class: "gantt_hdr") %>
102
          <%= content_tag(:div, class: "gantt_#{column_name} gantt_selected_column_content") do %>
103
            <%= gantt.selected_column_content({column: column, top: headers_height + 8, zoom: zoom, g_width: g_width}).html_safe %>
104
          <% end %>
105
        <% end %>
106
      <% end %>
27
      </div>
28
    </section>
29
  <% end %>
30

  
31
  <%
32
    query.columns.each do |column|
33
      next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name)
34
      column_name = column.name.to_s.tr('.', '_')
35
  %>
36
    <%= gantt_column_tag(column_name, min_width: 20, width: '50px', id: column_name,
37
                         class: 'gantt_selected_column', hidden: !query.draw_selected_columns) do %>
38
      <section class="gantt-pane gantt_selected_column_container gantt_<%= column_name %>_container">
39
        <header class="gantt-pane-header gantt-selected-column-header"><%= column.caption %></header>
40
        <div class="gantt-pane-body gantt_selected_column_content gantt_<%= column_name %>">
41
          <%= gantt.selected_column_content(column: column, top: layout.content_top, zoom: layout.zoom, g_width: layout.chart_width).html_safe %>
42
        </div>
43
      </section>
107 44
    <% end %>
108
    <td>
109
      <div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area" data-gantt--chart-target="ganttArea">
110
        <%
111
          months_header_style = +"width: #{g_width - 1}px;"
112
          months_header_style << "height: #{headers_height}px;"
113
          months_header_style << 'background: #f1f3f5;' # oc-gray-1
114
        %>
115
        <%= content_tag(:div, '&nbsp;'.html_safe, style: months_header_style, class: "gantt_hdr") %>
45
  <% end %>
116 46

  
117
        <% month_f = gantt.date_from %>
118
        <% left = 0 %>
119
        <% months_height = (show_weeks ? header_height : header_height + g_height) %>
120
        <% gantt.months.times do %>
121
          <% width = (((month_f >> 1) - month_f) * zoom - 1).to_i %>
122
          <% month_style = +"inset-inline-start: #{left}px;" %>
123
          <% month_style << "width: #{width}px;" %>
124
          <% month_style << "height: #{months_height}px;" %>
125
          <%= content_tag(:div, style: month_style, class: "gantt_hdr") do %>
126
            <%= link_to "#{month_f.year}-#{month_f.month}",
127
                        gantt.params.merge(year: month_f.year, month: month_f.month),
128
                        title: "#{month_name(month_f.month)} #{month_f.year}" %>
47
  <div id="gantt_area" class="gantt-timeline" data-gantt--chart-target="ganttArea">
48
    <div class="gantt-timeline-canvas">
49
      <header class="gantt-timeline-header">
50
        <div class="gantt-header-row gantt-header-months">
51
          <% layout.months.each do |period| %>
52
            <div class="gantt-period gantt-period-month<%= ' gantt-period-spans-body' if period.spans_body %><%= ' gantt-period-last' if period.last %>"
53
                 style="<%= gantt_period_style(period) %>">
54
              <%= link_to "#{period.date.year}-#{period.date.month}",
55
                          gantt.params.merge(year: period.date.year, month: period.date.month),
56
                          title: "#{month_name(period.date.month)} #{period.date.year}" %>
57
            </div>
129 58
          <% end %>
130
          <% left += width + 1 %>
131
          <% month_f = month_f >> 1 %>
132
        <% end %>
59
        </div>
133 60

  
134
        <% if show_weeks %>
135
          <% left = 0 %>
136
          <% weeks_height = (show_days ? header_height - 1 : header_height - 1 + g_height) %>
137
          <% if gantt.date_from.cwday == 1 %>
138
            <% week_f = gantt.date_from %>
139
          <% else %>
140
            <% week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1) %>
141
            <% width = (7 - gantt.date_from.cwday + 1) * zoom - 1 %>
142
            <% gap_style = +"inset-inline-start: #{left}px;" %>
143
            <% gap_style << "inset-block-start: 19px;" %>
144
            <% gap_style << "width: #{width}px;" %>
145
            <% gap_style << "height: #{weeks_height}px;" %>
146
            <%= content_tag(:div, '&nbsp;'.html_safe, style: gap_style, class: "gantt_hdr") %>
147
            <% left += width + 1 %>
148
          <% end %>
149
          <% while week_f <= gantt.date_to %>
150
            <% width = ((week_f + 6 <= gantt.date_to) ? 7 * zoom - 1 : (gantt.date_to - week_f + 1) * zoom - 1).to_i %>
151
            <% week_style = +"inset-inline-start: #{left}px;" %>
152
            <% week_style << "inset-block-start: 19px;" %>
153
            <% week_style << "width: #{width}px;" %>
154
            <% week_style << "height: #{weeks_height}px;" %>
155
            <%= content_tag(:div, style: week_style, class: "gantt_hdr") do %>
156
              <%= content_tag(:small) do %>
157
                <%= week_f.cweek if width >= 16 %>
158
              <% end %>
61
        <% if layout.show_weeks? %>
62
          <div class="gantt-header-row gantt-header-weeks">
63
            <% layout.weeks.each do |period| %>
64
              <div class="gantt-period gantt-period-week<%= ' gantt-period-spans-body' if period.spans_body %><%= ' gantt-period-last' if period.last %>"
65
                   style="<%= gantt_period_style(period) %>">
66
                <small><%= period.date.cweek if period.date && period.width >= 16 %></small>
67
              </div>
159 68
            <% end %>
160
            <% left += width + 1 %>
161
            <% week_f += 7 %>
162
          <% end %>
69
          </div>
163 70
        <% end %>
164 71

  
165
        <% if show_day_num %>
166
          <% left = 0 %>
167
          <% days_height = g_height + header_height * 2 - 1 %>
168
          <% wday = gantt.date_from.cwday %>
169
          <% day_num = gantt.date_from %>
170
          <% (gantt.date_to - gantt.date_from + 1).to_i.times do %>
171
            <% width = zoom - 1 %>
172
            <% day_style = +"inset-inline-start:#{left}px;" %>
173
            <% day_style << "inset-block-start:37px;" %>
174
            <% day_style << "width:#{width}px;" %>
175
            <% day_style << "height:#{days_height}px;" %>
176
            <% day_style << "font-size:0.7em;" %>
177
            <% day_classes = +"gantt_hdr" %>
178
            <% day_classes << " nwday" if gantt.non_working_week_days.include?(wday) %>
179
            <%= content_tag(:div, style: day_style, class: day_classes) do %>
180
              <%= day_num.day %>
72
        <% if layout.show_day_numbers? %>
73
          <div class="gantt-header-row gantt-header-day-numbers">
74
            <% layout.day_numbers.each do |period| %>
75
              <div class="gantt-period gantt-period-day-number<%= ' gantt-period-non-working' if period.non_working %><%= ' gantt-period-last' if period.last %>"
76
                   style="<%= gantt_period_style(period) %>"><%= period.date.day %></div>
181 77
            <% end %>
182
            <% left += width + 1 %>
183
            <% day_num += 1 %>
184
            <% wday += 1 %>
185
            <% wday = 1 if wday > 7 %>
186
          <% end %>
78
          </div>
187 79
        <% end %>
188 80

  
189
        <% if show_days %>
190
          <% left = 0 %>
191
          <% days_height = g_height + header_height - 1 %>
192
          <% days_top = (show_day_num ? 55 : 37) %>
193
          <% (gantt.date_from..gantt.date_to).each do |g_date| %>
194
            <% width = zoom - 1 %>
195
            <% day_style = +"inset-inline-start: #{left}px;" %>
196
            <% day_style << "inset-block-start: #{days_top}px;" %>
197
            <% day_style << "width: #{width}px;" %>
198
            <% day_style << "height: #{days_height}px;" %>
199
            <% day_style << "font-size:0.7em;" %>
200
            <% day_classes = +"gantt_hdr" %>
201
            <% day_classes << " nwday" if gantt.non_working_week_days.include?(g_date.cwday) %>
202
            <%= content_tag(:div, style: day_style, class: day_classes) do %>
203
              <%= day_letter(g_date.cwday) %>
81
        <% if layout.show_days? %>
82
          <div class="gantt-header-row gantt-header-days">
83
            <% layout.days.each do |period| %>
84
              <div class="gantt-period gantt-period-day gantt-period-spans-body<%= ' gantt-period-non-working' if period.non_working %><%= ' gantt-period-last' if period.last %>"
85
                   style="<%= gantt_period_style(period) %>"><%= day_letter(period.date.cwday) %></div>
204 86
            <% end %>
205
            <% left += width + 1 %>
206
          <% end %>
87
          </div>
207 88
        <% end %>
89
      </header>
208 90

  
91
      <div class="gantt-timeline-body">
209 92
        <%= form_tag({}, data: {cm_url: issues_context_menu_path}) do %>
210 93
          <%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %>
211 94
          <%= gantt.lines.html_safe %>
212 95
        <% end %>
213 96

  
214
        <% if User.current.today >= gantt.date_from && User.current.today <= gantt.date_to %>
215
          <% today_left = (((User.current.today - gantt.date_from + 1) * zoom).floor - 1).to_i %>
216
          <% today_style = +"position: absolute;" %>
217
          <% today_style << "height: #{g_height}px;" %>
218
          <% today_style << "inset-block-start: #{headers_height + 1}px;" %>
219
          <% today_style << "inset-inline-start: #{today_left}px;" %>
220
          <% today_style << "width:10px;" %>
221
          <% today_style << "border-inline-start: 1px dashed red;" %>
222
          <%= content_tag(:div, '&nbsp;'.html_safe, style: today_style, id: 'today_line') %>
97
        <% if layout.today_start %>
98
          <div id="today_line" style="<%= gantt_css_variables('gantt-today-start': "#{layout.today_start}px") %>" aria-hidden="true"></div>
223 99
        <% end %>
224 100

  
225
        <%
226
          draw_area_style = +"position: absolute;"
227
          draw_area_style << "height: #{g_height}px;"
228
          draw_area_style << "inset-block-start: #{headers_height + 1}px;"
229
          draw_area_style << 'inset-inline-start: 0px;'
230
          draw_area_style << "width: #{g_width - 1}px;"
231
        %>
232
        <%= content_tag(:div, '', style: draw_area_style, id: "gantt_draw_area", data: {'gantt--chart-target': 'drawArea'}) %>
101
        <div id="gantt_draw_area" data-gantt--chart-target="drawArea" aria-hidden="true"></div>
233 102
      </div>
234
    </td>
235
  </tr>
103
    </div>
104
  </div>
236 105
<% end %>
237 106

  
238 107
<span class="pagination">
lib/redmine/helpers/gantt.rb
283 283
      def render_object_row(object, options)
284 284
        class_name = object.class.name.downcase
285 285
        send(:"subject_for_#{class_name}", object, options) unless options[:only] == :lines || options[:only] == :selected_columns
286
        send(:"line_for_#{class_name}", object, options) unless options[:only] == :subjects || options[:only] == :selected_columns
287
        column_content_for_issue(object, options) if options[:only] == :selected_columns && options[:column].present? && object.is_a?(Issue)
286
        unless options[:only] == :subjects || options[:only] == :selected_columns
287
          line = send(:"line_for_#{class_name}", object, options)
288
          @lines << html_gantt_row(object, options, line) if options[:format] == :html
289
        end
290
        column_content_for_object(object, options) if options[:only] == :selected_columns && options[:column].present?
288 291
        options[:top] += options[:top_increment]
289 292
        @number_of_rows += 1
290 293
        if @max_rows && @number_of_rows >= @max_rows
......
354 357
        end
355 358
      end
356 359

  
357
      def column_content_for_issue(issue, options)
360
      def column_content_for_object(object, options)
358 361
        if options[:format] == :html
359
          data_options = {}
360
          data_options[:collapse_expand] = "issue-#{issue.id}"
361
          data_options[:number_of_rows] = number_of_rows
362
          style = "position: absolute;inset-block-start: #{options[:top]}px; font-size: 0.8em;"
362
          data_options = {
363
            :collapse_expand => "#{object.class}-#{object.id}".downcase,
364
            :number_of_rows => number_of_rows
365
          }
366
          value = object.is_a?(Issue) ? view.column_content(options[:column], object) : nil
363 367
          content =
364 368
            view.content_tag(
365
              :div, view.column_content(options[:column], issue),
366
              :style => style, :class => "issue_#{options[:column].name}",
367
              :id => "#{options[:column].name}_issue_#{issue.id}",
369
              :div, value,
370
              :style => css_variables('gantt-row-top': "#{options[:top] || 0}px"),
371
              :class => [
372
                "gantt-row",
373
                "gantt-column-row",
374
                ("issue_#{options[:column].name}" if object.is_a?(Issue))
375
              ],
376
              :id => ("#{options[:column].name}_issue_#{object.id}" if object.is_a?(Issue)),
368 377
              :data => data_options
369 378
            )
370 379
          @columns[options[:column].name] << content if @columns.has_key?(options[:column].name)
......
372 381
        end
373 382
      end
374 383

  
384
      def column_content_for_issue(issue, options)
385
        column_content_for_object(issue, options)
386
      end
387

  
375 388
      def subject(label, options, object=nil)
376 389
        send :"#{options[:format]}_subject", options, label, object
377 390
      end
......
750 763
          s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-avatar')
751 764
          s << view.link_to_issue(issue).html_safe
752 765
          s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
753
                                :value => issue.id, :style => 'display:none;',
766
                                :value => issue.id,
754 767
                                :class => 'toggle-selection')
755 768
          view.content_tag(:span, s, :class => css_classes).html_safe
756 769
        when Version
......
827 840
            params[:indent] += 18
828 841
          end
829 842
        end
830
        style = "position: absolute;inset-block-start:#{params[:top]}px;inset-inline-start:#{params[:indent]}px;"
831
        style += "width:#{params[:subject_width] - params[:indent]}px;" if params[:subject_width]
832
        tag_options[:style] = style
843
        tag_options[:class] = [tag_options[:class], 'gantt-row', 'gantt-subject-row'].compact.join(' ')
844
        row_variables = {
845
          'gantt-row-top': "#{params[:top] || 0}px",
846
          'gantt-row-indent': "#{params[:indent]}px"
847
        }
848
        if params[:subject_width]
849
          row_variables['gantt-row-width'] = "#{params[:subject_width] - params[:indent]}px"
850
        end
851
        tag_options[:style] = css_variables(row_variables)
833 852
        output = view.content_tag(:div, content, tag_options)
834 853
        @subjects << output
835 854
        output
......
889 908
        # Renders the task bar, with progress and late
890 909
        if coords[:bar_start] && coords[:bar_end]
891 910
          width = coords[:bar_end] - coords[:bar_start] - 2
892
          style = +""
893
          style << "inset-block-start:#{params[:top]}px;"
894
          style << "inset-inline-start:#{coords[:bar_start]}px;"
895
          style << "width:#{width}px;"
911
          style = task_style(coords[:bar_start], width)
896 912
          html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue)
897 913
          html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
898 914
          content_opt = {:style => style,
......
909 925
          output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
910 926
          if coords[:bar_late_end]
911 927
            width = coords[:bar_late_end] - coords[:bar_start] - 2
912
            style = +""
913
            style << "inset-block-start:#{params[:top]}px;"
914
            style << "inset-inline-start:#{coords[:bar_start]}px;"
915
            style << "width:#{width}px;"
928
            style = task_style(coords[:bar_start], width)
916 929
            output << view.content_tag(:div, '&nbsp;'.html_safe,
917 930
                                       :style => style,
918 931
                                       :class => "#{css} task_late",
......
920 933
          end
921 934
          if coords[:bar_progress_end]
922 935
            width = coords[:bar_progress_end] - coords[:bar_start] - 2
923
            style = +""
924
            style << "inset-block-start:#{params[:top]}px;"
925
            style << "inset-inline-start:#{coords[:bar_start]}px;"
926
            style << "width:#{width}px;"
936
            style = task_style(coords[:bar_start], width)
927 937
            html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue)
928 938
            html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
929 939
            output << view.content_tag(:div, '&nbsp;'.html_safe,
......
936 946
        # Renders the markers
937 947
        if markers
938 948
          if coords[:start]
939
            style = +""
940
            style << "inset-block-start:#{params[:top]}px;"
941
            style << "inset-inline-start:#{coords[:start]}px;"
942
            style << "width:15px;"
949
            style = task_style(coords[:start], 15)
943 950
            output << view.content_tag(:div, '&nbsp;'.html_safe,
944 951
                                       :style => style,
945 952
                                       :class => "#{css} marker starting",
946 953
                                       :data => data_options)
947 954
          end
948 955
          if coords[:end]
949
            style = +""
950
            style << "inset-block-start:#{params[:top]}px;"
951
            style << "inset-inline-start:#{coords[:end]}px;"
952
            style << "width:15px;"
956
            style = task_style(coords[:end], 15)
953 957
            output << view.content_tag(:div, '&nbsp;'.html_safe,
954 958
                                       :style => style,
955 959
                                       :class => "#{css} marker ending",
......
958 962
        end
959 963
        # Renders the label on the right
960 964
        if label
961
          style = +""
962
          style << "inset-block-start:#{params[:top]}px;"
963
          style << "inset-inline-start:#{(coords[:bar_end] || 0) + 8}px;"
964
          style << "width:15px;"
965
          style = task_style((coords[:bar_end] || 0) + 8, 15)
965 966
          output << view.content_tag(:div, label,
966 967
                                     :style => style,
967 968
                                     :class => "#{css} label",
......
973 974
                               view.render_issue_tooltip(object).html_safe,
974 975
                               :class => "tip")
975 976
          s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
976
                                :value => object.id, :style => 'display:none;',
977
                                :value => object.id,
977 978
                                :class => 'toggle-selection')
978
          style = +""
979
          style << "position: absolute;"
980
          style << "inset-block-start:#{params[:top]}px;"
981
          style << "inset-inline-start:#{coords[:bar_start]}px;"
982
          style << "width:#{coords[:bar_end] - coords[:bar_start]}px;"
983
          style << "height:12px;"
979
          style = task_style(coords[:bar_start], coords[:bar_end] - coords[:bar_start])
984 980
          output << view.content_tag(:div, s.html_safe,
985 981
                                     :style => style,
986 982
                                     :class => "tooltip hascontextmenu",
987 983
                                     :data => data_options)
988 984
        end
989
        @lines << output
990 985
        output
991 986
      end
992 987

  
988
      def html_gantt_row(object, params, content)
989
        data_options = {
990
          :collapse_expand => "#{object.class}-#{object.id}".downcase,
991
          :number_of_rows => number_of_rows
992
        }
993
        style = css_variables('gantt-row-top': "#{params[:top] || 0}px")
994
        view.content_tag(
995
          :div,
996
          content.to_s.html_safe,
997
          :class => 'gantt-row gantt-timeline-row',
998
          :style => style,
999
          :data => data_options
1000
        )
1001
      end
1002

  
1003
      def task_style(start, width)
1004
        css_variables(
1005
          'gantt-task-start': "#{start}px",
1006
          'gantt-task-width': "#{width}px"
1007
        )
1008
      end
1009

  
1010
      def css_variables(variables)
1011
        variables.map {|name, value| "--#{name}:#{value}"}.join(';')
1012
      end
1013

  
993 1014
      def pdf_task(params, coords, markers, label, object)
994 1015
        cell_height_ratio = params[:pdf].get_cell_height_ratio
995 1016
        params[:pdf].set_cell_height_ratio(0.1)
test/functional/gantts_controller_test.rb
56 56
    end
57 57

  
58 58
    # Assert context menu on issues subject and gantt bar
59
    assert_select 'div[class=?]', 'issue-subject hascontextmenu'
59
    assert_select 'div.issue-subject.hascontextmenu'
60 60
    assert_select 'div.tooltip.hascontextmenu' do
61 61
      assert_select 'img[class="gravatar avatar"]'
62 62
    end
......
108 108
    assert_response :success
109 109
  end
110 110

  
111
  def test_gantt_should_warn_when_truncated
112
    with_settings :gantt_items_limit => '1' do
113
      get(:show, :params => {:project_id => 1})
114
    end
115

  
116
    assert_response :success
117
    assert_select 'p.warning', :text => I18n.t(:notice_gantt_chart_truncated, :max => 1)
118
  end
119

  
111 120
  def test_show_should_run_custom_query
112 121
    query = IssueQuery.create!(:name => 'Gantt Query', :description => 'Description for Gantt Query', :visibility => IssueQuery::VISIBILITY_PUBLIC)
113 122
    get(
......
214 223
        }
215 224
      )
216 225
      assert_response :success
217
      assert_select 'div.gantt_hdr>a', :text => /^[\d-]+$/, :count => 40
226
      assert_select 'div.gantt-period-month>a', :text => /^[\d-]+$/, :count => 40
218 227

  
219 228
      # Displays 6 months (the default value for `months`) if `months` exceeds
220 229
      # gant_months_limit
......
227 236
        }
228 237
      )
229 238
      assert_response :success
230
      assert_select 'div.gantt_hdr>a', :text => /^[\d-]+$/, :count => 6
239
      assert_select 'div.gantt-period-month>a', :text => /^[\d-]+$/, :count => 6
231 240
    end
232 241
  end
233 242

  
......
313 322

  
314 323
    6.times do |offset|
315 324
      m = selected_start.since(offset.month)
316
      assert_select 'div.gantt_hdr > a', text: "#{m.year}-#{m.month}"
325
      assert_select 'div.gantt-period-month > a', text: "#{m.year}-#{m.month}"
317 326
    end
318 327

  
319 328
    # eCookbook
......
357 366
    6.times do |offset|
358 367
      m = start_of_month.since(offset.months)
359 368

  
360
      assert_select 'div.gantt_hdr > a', text: "#{m.year}-#{m.month}"
369
      assert_select 'div.gantt-period-month > a', text: "#{m.year}-#{m.month}"
361 370
    end
362 371

  
363 372
    assert_select 'input#months[value=?]', '6'
......
382 391
  end
383 392

  
384 393
  def assert_chart_row(selector, row:, style_substring:)
394
    style_substring = "--gantt-task-start:#{style_substring.delete_prefix('inset-inline-start:').sub(';width:', ';--gantt-task-width:')}"
385 395
    matcher = "#gantt_area #{selector}[data-number-of-rows=?][style*=?]"
386 396
    assert_select matcher, row, style_substring, minimum: 1
387 397
  end
test/system/gantts_test.rb
11 11
    visit_gantt
12 12
    expand_options
13 13

  
14
    assert_no_selector 'td#status'
15
    assert_no_selector 'td#priority'
16
    assert_no_selector 'td#assigned_to'
17
    assert_no_selector 'td#updated_on'
14
    assert_no_selector 'div#status'
15
    assert_no_selector 'div#priority'
16
    assert_no_selector 'div#assigned_to'
17
    assert_no_selector 'div#updated_on'
18 18

  
19 19
    find('#draw_selected_columns').check
20 20

  
21
    assert_selector 'div.gantt_subjects_container.draw_selected_columns'
22
    assert_selector 'td#status'
23
    assert_selector 'td#priority'
24
    assert_selector 'td#assigned_to'
25
    assert_selector 'td#updated_on'
21
    assert_selector '.gantt_subjects_container.draw_selected_columns'
22
    assert_selector 'div#status'
23
    assert_selector 'div#priority'
... This diff was truncated because it exceeds the maximum size that can be displayed.
(1-1/5)