Project

General

Profile

Patch #44567 » 0002-Organize-Gantt-chart-DOM-structure-and-semantics.patch

Katsuya HIDAKA, 2026-10-01 10:10

View differences:

app/assets/stylesheets/context_menu.css
46 46
.context-menu-selection a, .context-menu-selection a:hover { color:var(--oc-gray-0) !important; }
47 47
.context-menu-selection:hover { background-color:var(--oc-blue-7) !important; color:var(--oc-gray-0)  !important; }
48 48
.context-menu-selection svg.icon-svg { stroke: var(--oc-white) !important; }
49
div#gantt_area .context-menu-selection { background-color: rgba(var(--oc-blue-7-rgb), 0.4) !important; }
50
div#gantt_area .context-menu-selection:hover { background-color: rgba(var(--oc-blue-7-rgb), 0.4) !important; }
51
div#gantt_area .context-menu-selection a { color: var(--oc-blue-9) !important; }
49
.gantt-timeline .context-menu-selection { background-color: rgba(var(--oc-blue-7-rgb), 0.4) !important; }
50
.gantt-timeline .context-menu-selection:hover { background-color: rgba(var(--oc-blue-7-rgb), 0.4) !important; }
51
.gantt-timeline .context-menu-selection a { color: var(--oc-blue-9) !important; }
app/assets/stylesheets/gantt.css
4 4
 * This code is released under the GNU General Public License.
5 5
 */
6 6

  
7
.gantt_subjects div > span.expander {
7
[data-gantt-column="subjects"] .gantt-row > span.expander {
8 8
  margin-inline-start: 0;
9 9
  cursor: pointer;
10 10
}
11 11

  
12
.gantt_subjects .avatar {
12
[data-gantt-column="subjects"] .avatar {
13 13
  margin-inline-end: 4px;
14 14
}
15 15

  
16
.gantt_subjects div.project-name a,
17
.gantt_subjects div.version-name a {
16
[data-gantt-column="subjects"] .gantt-row:is([data-gantt-row-type="project"], [data-gantt-row-type="version"]) a {
18 17
  margin-inline-start: 4px;
19 18
}
20 19

  
......
33 32
  display: none !important;
34 33
}
35 34

  
36
.gantt_subjects_column,
37
.gantt_selected_column {
35
.gantt-column {
38 36
  position: relative;
39 37
  flex: 0 0 var(--gantt-column-width);
40 38
  inline-size: var(--gantt-column-width);
......
53 51
  overflow: hidden;
54 52
}
55 53

  
56
.gantt_subjects_container {
54
[data-gantt-column="subjects"] .gantt-pane {
57 55
  border-inline-start: 1px solid var(--oc-gray-4);
58 56
}
59 57

  
60
.gantt-pane-header {
58
.gantt-pane > header {
61 59
  position: relative;
62 60
  z-index: 2;
63 61
  display: flex;
......
71 69
  overflow: hidden;
72 70
}
73 71

  
74
.gantt-selected-column-header {
72
.gantt-selected-column .gantt-pane > header {
75 73
  padding-inline: 3px;
76 74
  text-overflow: ellipsis;
77 75
  white-space: nowrap;
......
80 78
.gantt-pane-body {
81 79
  position: relative;
82 80
  block-size: var(--gantt-content-height);
81
  font-size: 0.8em;
83 82
}
84 83

  
85 84
.gantt-timeline {
......
101 100
  box-sizing: border-box;
102 101
}
103 102

  
104
.gantt-timeline-header {
103
.gantt-timeline-canvas > header {
105 104
  position: relative;
106 105
  z-index: 1;
107 106
  block-size: var(--gantt-headers-height);
......
149 148
  pointer-events: auto;
150 149
}
151 150

  
152
.gantt-period-day,
153
.gantt-period-day-number {
151
.gantt-header-row:is([data-gantt-period="day"], [data-gantt-period="day-number"]) .gantt-period {
154 152
  font-size: 0.7em;
155 153
}
156 154

  
......
168 166
  block-size: var(--gantt-content-height);
169 167
}
170 168

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

  
178
.gantt_subjects form,
179
.gantt_selected_column_content,
169
.gantt-pane-body form,
180 170
.gantt-timeline-body form {
181 171
  position: relative;
182 172
  display: block;
......
191 181
  box-sizing: border-box;
192 182
}
193 183

  
194
.gantt-subject-row {
184
[data-gantt-column="subjects"] .gantt-row {
195 185
  inset-inline-start: var(--gantt-row-indent);
196
  inline-size: var(--gantt-row-width);
186
  inline-size: calc(100% - var(--gantt-row-indent));
197 187
  line-height: 16px;
198 188
  overflow: hidden;
199 189
  white-space: nowrap;
200 190
}
201 191

  
202
.gantt-column-row {
192
.gantt-selected-column .gantt-row {
203 193
  inset-inline-start: 0;
204 194
  inline-size: 100%;
205 195
  padding-inline-start: 3px;
......
208 198
  white-space: nowrap;
209 199
}
210 200

  
211
.gantt_subjects div.issue-subject:hover {
201
[data-gantt-column="subjects"] .gantt-row[data-gantt-row-type="issue"]:hover {
212 202
  background-color: var(--oc-yellow-0);
213 203
}
214 204

  
215
.gantt-timeline-row {
205
.gantt-timeline-body .gantt-row {
216 206
  inset-inline-start: 0;
217 207
  inline-size: var(--gantt-chart-width);
218 208
}
219 209

  
220
#today_line {
210
.gantt-today {
221 211
  position: absolute;
222 212
  inset-block-start: 1px;
223 213
  inset-inline-start: var(--gantt-today-start);
......
227 217
  pointer-events: none;
228 218
}
229 219

  
230
#gantt_draw_area {
220
.gantt-relations {
231 221
  position: absolute;
232 222
  inset-block-start: 1px;
233 223
  inset-inline-start: 0;
......
236 226
  pointer-events: none;
237 227
}
238 228

  
239
.gantt_watcher_users_column div.issue_watcher_users ul {
229
[data-gantt-column="watcher_users"] div.issue_watcher_users ul {
240 230
  margin: 0;
241 231
  padding: 0;
242 232
  list-style: none;
243 233
}
244 234

  
245
.gantt_watcher_users_column div.issue_watcher_users ul li {
235
[data-gantt-column="watcher_users"] div.issue_watcher_users ul li {
246 236
  display: inline;
247 237
}
248 238

  
249
.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
239
[data-gantt-column="watcher_users"] div.issue_watcher_users ul li:not(:last-child)::after {
250 240
  content: ', ';
251 241
  white-space: pre;
252 242
}
253 243

  
254
.task {
244
.gantt-task {
255 245
  position: absolute;
256
  inset-block-start: var(--gantt-row-top);
246
  inset-block-start: 0;
257 247
  inset-inline-start: var(--gantt-task-start);
258 248
  inline-size: var(--gantt-task-width);
259 249
  block-size: 8px;
......
267 257

  
268 258
.gantt-timeline-body .tooltip {
269 259
  position: absolute;
270
  inset-block-start: var(--gantt-row-top);
260
  inset-block-start: 0;
271 261
  inset-inline-start: var(--gantt-task-start);
272 262
  inline-size: var(--gantt-task-width);
273 263
  block-size: 12px;
274 264
}
275 265

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

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

  
284
.task.label {
270
.gantt-task-label {
285 271
  inline-size: var(--gantt-task-width);
286 272
}
287 273

  
288
.task.label.project,
289
.task.label.version {
274
.gantt-row:is([data-gantt-row-type="project"], [data-gantt-row-type="version"]) .gantt-task-label {
290 275
  font-weight: bold;
291 276
}
292 277

  
293
.task_late {
278
.gantt-task-late {
294 279
  background: var(--oc-red-5) url(/task_late.png);
295 280
  border: 1px solid var(--oc-red-5);
296 281
}
297 282

  
298
.task_done {
283
.gantt-task-done {
299 284
  background: var(--oc-green-7) url(/task_done.png);
300 285
  border: 1px solid var(--oc-green-7);
301 286
}
302 287

  
303
.task_todo {
288
.gantt-task-todo {
304 289
  background: var(--oc-gray-5) url(/task_todo.png);
305 290
  border: 1px solid var(--oc-gray-5);
306 291
}
307 292

  
308
.task_todo.parent {
293
.gantt-task-todo.gantt-task-parent {
309 294
  background: var(--oc-gray-6);
310 295
  border: 1px solid var(--oc-gray-6);
311 296
  block-size: 3px;
312 297
}
313 298

  
314
.task_late.parent,
315
.task_done.parent {
299
.gantt-task-late.gantt-task-parent,
300
.gantt-task-done.gantt-task-parent {
316 301
  block-size: 3px;
317 302
}
318 303

  
319
.task.parent.marker.starting,
320
.task.parent.marker.ending {
304
.gantt-task.gantt-task-parent.gantt-task-marker {
321 305
  position: absolute;
322 306
  background: url(/task_parent_end.png) no-repeat 0 0;
323 307
  inline-size: 8px;
......
325 309
  margin-inline-start: -4px;
326 310
}
327 311

  
328
.version.task_late {
312
.gantt-row[data-gantt-row-type="version"] .gantt-task-late {
329 313
  background: var(--oc-red-5) url(/milestone_late.png);
330 314
  border: 1px solid var(--oc-red-5);
331 315
  block-size: 2px;
332 316
  margin-block-start: 3px;
333 317
}
334 318

  
335
.version.task_done {
319
.gantt-row[data-gantt-row-type="version"] .gantt-task-done {
336 320
  background: var(--oc-green-7) url(/milestone_done.png);
337 321
  border: 1px solid var(--oc-green-7);
338 322
  block-size: 2px;
339 323
  margin-block-start: 3px;
340 324
}
341 325

  
342
.version.task_todo {
326
.gantt-row[data-gantt-row-type="version"] .gantt-task-todo {
343 327
  background: var(--oc-white) url(/milestone_todo.png);
344 328
  border: 1px solid var(--oc-white);
345 329
  block-size: 2px;
346 330
  margin-block-start: 3px;
347 331
}
348 332

  
349
.version.marker {
333
.gantt-row[data-gantt-row-type="version"] .gantt-task-marker {
350 334
  background-image: url(/version_marker.png);
351 335
  background-repeat: no-repeat;
352 336
  border: 0;
......
354 338
  margin-block-start: 1px;
355 339
}
356 340

  
357
.project.task_late {
341
.gantt-row[data-gantt-row-type="project"] .gantt-task-late {
358 342
  background: var(--oc-red-5) url(/milestone_late.png);
359 343
  border: 1px solid var(--oc-red-5);
360 344
  block-size: 2px;
361 345
  margin-block-start: 3px;
362 346
}
363 347

  
364
.project.task_done {
348
.gantt-row[data-gantt-row-type="project"] .gantt-task-done {
365 349
  background: var(--oc-green-7) url(/milestone_done.png);
366 350
  border: 1px solid var(--oc-green-7);
367 351
  block-size: 2px;
368 352
  margin-block-start: 3px;
369 353
}
370 354

  
371
.project.task_todo {
355
.gantt-row[data-gantt-row-type="project"] .gantt-task-todo {
372 356
  background: var(--oc-white) url(/milestone_todo.png);
373 357
  border: 1px solid var(--oc-white);
374 358
  block-size: 2px;
375 359
  margin-block-start: 3px;
376 360
}
377 361

  
378
.project.marker {
362
.gantt-row[data-gantt-row-type="project"] .gantt-task-marker {
379 363
  background-image: url(/project_marker.png);
380 364
  background-repeat: no-repeat;
381 365
  border: 0;
......
384 368
}
385 369

  
386 370
/* RTL adjustment (disabled while Gantt is forced LTR).  Enable when RTL is supported.
387
[dir="rtl"] :is(.marker, .task.parent.marker) {
371
  [dir="rtl"] .gantt-task-marker {
388 372
  background-position: right;
389 373
}
390 374
*/
app/assets/stylesheets/responsive.css
667 667
  }
668 668

  
669 669
  /* Gantt charts */
670
  .gantt_subjects_column {
670
  .gantt-column[data-gantt-column="subjects"] {
671 671
    --gantt-column-width: 60% !important;
672 672
  }
673 673

  
674
  .gantt_subjects_container {
674
  [data-gantt-column="subjects"] .gantt-pane {
675 675
    inline-size: 100% !important;
676 676
    overflow: hidden;
677 677
  }
app/helpers/gantt_helper.rb
231 231
  end
232 232

  
233 233
  def gantt_column_tag(column_name, min_width: nil, **options, &)
234
    options[:data] = {
234
    options[:data] = options.fetch(:data, {}).merge(
235 235
      controller: 'gantt--column',
236 236
      action: 'resize@window->gantt--column#handleWindowResize',
237 237
      'gantt--column-min-width-value': min_width,
238
      'gantt--column-column-value': column_name
239
    }
240
    options[:class] = ["gantt_#{column_name}_column", options[:class]]
238
      'gantt-column': column_name
239
    )
240
    options[:class] = ['gantt-column', options[:class]]
241 241

  
242 242
    options[:style] = gantt_css_variables('gantt-column-width': options.delete(:width)) if options[:width]
243 243

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

  
247
  def gantt_subjects_tag(&)
248
    data_attributes = {
249
      controller: 'gantt--subjects',
250
      action: 'gantt--column:resize-column-subjects@document->gantt--subjects#handleResizeColumn'
251
    }
252
    tag.div(class: "gantt_subjects", data: data_attributes, &)
253
  end
254 246
end
app/javascript/controllers/gantt/chart_controller.js
4 4
const SVG_NS = "http://www.w3.org/2000/svg"
5 5

  
6 6
export default class extends Controller {
7
  static targets = ["ganttArea", "drawArea", "subjectsContainer"]
7
  static targets = ["relations", "selectedColumn", "today"]
8 8

  
9 9
  static values = {
10 10
    issueRelationTypes: Object,
......
88 88
  }
89 89

  
90 90
  #setupDrawPaper() {
91
    const width = Math.ceil(this.$(this.drawAreaTarget).width() || 0)
92
    const height = Math.ceil(this.$(this.drawAreaTarget).height() || 0)
91
    const width = Math.ceil(this.$(this.relationsTarget).width() || 0)
92
    const height = Math.ceil(this.$(this.relationsTarget).height() || 0)
93 93

  
94 94
    if (!this.#drawPaper) {
95 95
      this.#drawPaper = document.createElementNS(SVG_NS, "svg")
......
100 100

  
101 101
      this.#drawPaperGroup = document.createElementNS(SVG_NS, "g")
102 102
      this.#drawPaper.appendChild(this.#drawPaperGroup)
103
      this.drawAreaTarget.appendChild(this.#drawPaper)
103
      this.relationsTarget.appendChild(this.#drawPaper)
104 104
    }
105 105

  
106 106
    const safeWidth = Math.max(width, 1)
......
124 124
  }
125 125

  
126 126
  #setupDrawArea() {
127
    const $drawArea = this.$(this.drawAreaTarget)
127
    const $drawArea = this.$(this.relationsTarget)
128 128

  
129 129
    this.#drawTop = $drawArea.position().top
130 130
    this.#drawRight = $drawArea.width()
131 131
  }
132 132

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

  
137 134
    const isMobileDevice = typeof window.isMobile === "function" && window.isMobile()
138 135

  
139 136
    if (this.showSelectedColumnsValue) {
140 137
      if (isMobileDevice) {
141
        selectedColumns.forEach((element) => { element.hidden = true })
138
        this.selectedColumnTargets.forEach((element) => { element.hidden = true })
142 139
      } else {
143
        $subjectsContainer.addClass("draw_selected_columns")
144
        selectedColumns.forEach((element) => { element.hidden = false })
140
        this.selectedColumnTargets.forEach((element) => { element.hidden = false })
145 141
      }
146 142
    } else {
147
      selectedColumns.forEach((element) => { element.hidden = true })
148
      $subjectsContainer.removeClass("draw_selected_columns")
143
      this.selectedColumnTargets.forEach((element) => { element.hidden = true })
149 144
    }
150 145
  }
151 146

  
152 147
  get #relationsArray() {
153 148
    const relations = []
154 149

  
155
    this.$("div.task_todo[data-rels]").each((_, element) => {
150
    this.$(".gantt-task-todo[data-gantt-relations]").each((_, element) => {
156 151
      const $element = this.$(element)
157 152

  
158 153
      if (!$element.is(":visible")) return
159 154

  
160
      const elementId = $element.attr("id")
161

  
162
      if (!elementId) return
163

  
164
      const issueId = elementId.replace("task-todo-issue-", "")
165
      const dataRels = $element.data("rels") || {}
155
      const issueId = element.dataset.ganttIssueId
156
      const dataRels = JSON.parse(element.dataset.ganttRelations || "{}")
166 157

  
167 158
      Object.keys(dataRels).forEach((relTypeKey) => {
168 159
        this.$.each(dataRels[relTypeKey], (_, relatedIssue) => {
......
178 169
    const relations = this.#relationsArray
179 170

  
180 171
    relations.forEach((relation) => {
181
      const issueFrom = this.$(`#task-todo-issue-${relation.issue_from}`)
182
      const issueTo = this.$(`#task-todo-issue-${relation.issue_to}`)
172
      const issueFrom = this.$(`.gantt-task-todo[data-gantt-issue-id='${relation.issue_from}']`)
173
      const issueTo = this.$(`.gantt-task-todo[data-gantt-issue-id='${relation.issue_to}']`)
183 174

  
184 175
      if (issueFrom.length === 0 || issueTo.length === 0) return
185 176
      if (!issueTo.is(":visible")) return
......
305 296

  
306 297
  get #progressLinesArray() {
307 298
    const lines = []
308
    const todayLeft = this.$("#today_line").position().left
299
    const todayLeft = this.$(this.todayTarget).position().left
309 300

  
310 301
    lines.push({ left: todayLeft, top: 0 })
311 302

  
312
    this.$("div.issue-subject, div.version-name").each((_, element) => {
303
    this.$("[data-gantt-column='subjects'] .gantt-row[data-gantt-row-type='issue'], [data-gantt-column='subjects'] .gantt-row[data-gantt-row-type='version']").each((_, element) => {
313 304
      const $element = this.$(element)
314 305

  
315 306
      if (!$element.is(":visible")) return true
......
325 316
      if (issueClosed || versionClosed) {
326 317
        lines.push({ left: todayLeft, top: elementTopCenter })
327 318
      } else {
328
        const issueDone = this.$(`#task-done-${$element.attr("id")}`)
319
        const rowKey = element.dataset.ganttRowKey
320
        const issueDone = this.$(`.gantt-row[data-gantt-row-key='${rowKey}'] .gantt-task-done`)
329 321
        const isBehindStart = $element.children("span").hasClass("behind-start-date")
330 322
        const isOverEnd = $element.children("span").hasClass("over-end-date")
331 323

  
......
350 342
          })
351 343
        } else {
352 344
          let todoLeft = todayLeft
353
          const issueTodo = this.$(`#task-todo-${$element.attr("id")}`)
345
          const issueTodo = this.$(`.gantt-row[data-gantt-row-key='${rowKey}'] .gantt-task-todo`)
354 346
          if (issueTodo.length > 0) {
355 347
            todoLeft = issueTodo.first().position().left
356 348
          }
......
363 355
  }
364 356

  
365 357
  #drawGanttProgressLines() {
366
    if (this.$("#today_line").length === 0) return
358
    if (!this.hasTodayTarget) return
367 359

  
368 360
    const progressLines = this.#progressLinesArray
369
    const color = this.$("#today_line").css("border-inline-start-color") || "#ff0000"
361
    const color = this.$(this.todayTarget).css("border-inline-start-color") || "#ff0000"
370 362

  
371 363
    for (let index = 1; index < progressLines.length; index += 1) {
372 364
      const current = progressLines[index]
app/javascript/controllers/gantt/column_controller.js
3 3
export default class extends Controller {
4 4
  static values = {
5 5
    minWidth: Number,
6
    column: String,
7 6
    // Local value
8 7
    mobileMode: { type: Boolean, default: false }
9 8
  }
......
17 16
  connect() {
18 17
    this.#$element = this.$(this.element)
19 18
    this.#setupResizable()
20
    this.#dispatchResizeColumn()
21 19
  }
22 20

  
23 21
  disconnect() {
......
27 25

  
28 26
  handleWindowResize(_event) {
29 27
    this.mobileModeValue = this.#isMobile()
30

  
31
    this.#dispatchResizeColumn()
32 28
  }
33 29

  
34 30
  mobileModeValueChanged(current, old) {
......
52 48
      }
53 49
    }
54 50

  
55
    this.#$element
56
      .resizable(options)
57
      .on("resize", (event) => {
58
        event.stopPropagation()
59
        this.#dispatchResizeColumn()
60
      })
61
  }
62

  
63
  #dispatchResizeColumn() {
64
    if (!this.#$element) return
65

  
66
    this.dispatch(`resize-column-${this.columnValue}`, { detail: { width: this.#$element.width() } })
51
    this.#$element.resizable(options)
67 52
  }
68 53

  
69 54
  #isMobile() {
app/javascript/controllers/gantt/subjects_controller.js
1 1
import { Controller } from "@hotwired/stimulus"
2 2

  
3 3
export default class extends Controller {
4
  initialize() {
5
    this.$ = window.jQuery
6
  }
7

  
8
  handleResizeColumn(event) {
9
    const columnWidth = event.detail.width;
10

  
11
    this.$(".issue-subject, .project-name, .version-name").each((_, element) => {
12
      const $element = this.$(element)
13
      $element.width(columnWidth - $element.position().left)
14
    })
15
  }
4
  static targets = ["row"]
16 5

  
17 6
  handleEntryClick(event) {
18
    const iconExpander = event.currentTarget
19
    const $subject = this.$(iconExpander.parentElement)
20
    const subjectInlineStart =
21
      this.#readInlineStart($subject) + parseInt(iconExpander.offsetWidth, 10)
22

  
23
    let targetShown = null
24
    let targetTop = 0
25
    let totalHeight = 0
26
    let outOfHierarchy = false
27

  
28
    const willOpen = !$subject.hasClass("open")
29

  
30
    this.#setIconState($subject, willOpen)
31

  
32
    $subject.nextAll("div").each((_, element) => {
33
      const $element = this.$(element)
34
      const json = $element.data("collapse-expand")
35
      const numberOfRows = $element.data("number-of-rows")
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

  
39
      if (outOfHierarchy || this.#readInlineStart($element) <= subjectInlineStart) {
40
        outOfHierarchy = true
41

  
42
        if (targetShown === null) return false
43

  
44
        const newTopVal = this.#readBlockStart($element) + totalHeight * (targetShown ? -1 : 1)
45

  
46
        this.#setBlockStart($element, newTopVal)
47
        this.$([barsSelector, selectedColumnsSelector].join()).each((__, el) => {
48
          this.#setBlockStart(this.$(el), newTopVal)
49
        })
50

  
51
        return true
52
      }
53

  
54
      const isShown = $element.is(":visible")
55

  
56
      if (targetShown === null) {
57
        targetShown = isShown
58
        targetTop = this.#readBlockStart($element)
59
        totalHeight = 0
60
      }
61

  
62
      if (isShown === targetShown) {
63
        this.$(barsSelector).each((__, task) => {
64
          const $task = this.$(task)
65

  
66
          if (!isShown && willOpen) {
67
            this.#setBlockStart($task, targetTop + totalHeight)
68
          }
69
          if (!$task.hasClass("tooltip")) {
70
            $task.toggle(willOpen)
71
          }
72
        })
73

  
74
        this.$(selectedColumnsSelector).each((__, attr) => {
75
          const $attr = this.$(attr)
76

  
77
          if (!isShown && willOpen) {
78
            this.#setBlockStart($attr, targetTop + totalHeight)
79
          }
80
          $attr.toggle(willOpen)
81
        })
82

  
83
        if (!isShown && willOpen) {
84
          this.#setBlockStart($element, targetTop + totalHeight)
85
        }
7
    const subject = event.currentTarget.closest(".gantt-row")
8
    if (!subject) return
9

  
10
    const chart = this.element.closest(".gantt-chart")
11
    const subjectRows = this.rowTargets
12
    const subjectKey = subject.dataset.ganttRowKey
13
    const descendantKeys = new Set([subjectKey])
14
    const descendants = []
15
    const willOpen = !subject.classList.contains("is-expanded")
16

  
17
    subjectRows.forEach((row) => {
18
      if (row === subject) return
19
      if (!descendantKeys.has(row.dataset.ganttParentRowKey)) return
20

  
21
      descendantKeys.add(row.dataset.ganttRowKey)
22
      descendants.push(row)
23
    })
86 24

  
87
        this.#setIconState($element, willOpen)
88
        $element.toggle(willOpen)
89
        totalHeight += parseInt(json.top_increment, 10)
90
      }
25
    this.#setIconState(subject, willOpen)
26
    descendants.forEach((row) => {
27
      this.#setIconState(row, willOpen)
28
      chart.querySelectorAll(this.#rowSelector(row.dataset.ganttRowKey)).forEach((matchingRow) => {
29
        matchingRow.hidden = !willOpen
30
      })
91 31
    })
92 32

  
33
    this.#positionVisibleRows(chart, subjectRows)
34

  
93 35
    this.dispatch("toggle-tree", { bubbles: true })
94 36
  }
95 37

  
96
  #readInlineStart(el) {
97
    const node = el.jquery ? el[0] : el
98
    return parseFloat(window.getComputedStyle(node).getPropertyValue("inset-inline-start"))
99
  }
38
  #positionVisibleRows(chart, subjectRows) {
39
    const chartStyle = window.getComputedStyle(chart)
40
    const contentTop = parseFloat(chartStyle.getPropertyValue("--gantt-content-top")) || 0
41
    const rowHeight = parseFloat(window.getComputedStyle(subjectRows[0]).blockSize) || 20
42
    let top = contentTop
100 43

  
101
  #readBlockStart(el) {
102
    const node = el.jquery ? el[0] : el
103
    return parseFloat(window.getComputedStyle(node).getPropertyValue("inset-block-start"))
44
    subjectRows.forEach((row) => {
45
      if (row.hidden) return
46

  
47
      chart.querySelectorAll(this.#rowSelector(row.dataset.ganttRowKey)).forEach((matchingRow) => {
48
        matchingRow.style.setProperty("--gantt-row-top", `${top}px`)
49
      })
50
      top += rowHeight
51
    })
104 52
  }
105 53

  
106
  #setBlockStart(el, value) {
107
    const node = el.jquery ? el[0] : el
108
    const px = typeof value === "number" ? `${value}px` : value
109
    node.style.setProperty("--gantt-row-top", px)
54
  #rowSelector(rowKey) {
55
    return `.gantt-row[data-gantt-row-key="${CSS.escape(rowKey)}"]`
110 56
  }
111 57

  
112 58
  #setIconState(element, open) {
113
    const $element = element.jquery ? element : this.$(element)
114
    const expander = $element.find(".expander")
115

  
116
    if (open) {
117
      $element.addClass("open")
118

  
119
      if (expander.length > 0) {
120
        expander.removeClass("icon-collapsed").addClass("icon-expanded")
121

  
122
        if (expander.find("svg").length === 1) {
123
          window.updateSVGIcon(expander[0], "angle-down")
124
        }
125
      }
126
    } else {
127
      $element.removeClass("open")
128

  
129
      if (expander.length > 0) {
130
        expander.removeClass("icon-expanded").addClass("icon-collapsed")
131

  
132
        if (expander.find("svg").length === 1) {
133
          window.updateSVGIcon(expander[0], "angle-right")
134
        }
135
      }
59
    const expander = element.querySelector(".expander")
60
    if (!expander) return
61

  
62
    element.classList.toggle("is-expanded", open)
63
    expander.classList.toggle("icon-expanded", open)
64
    expander.classList.toggle("icon-collapsed", !open)
65
    if (expander.querySelectorAll("svg").length === 1) {
66
      window.updateSVGIcon(expander, open ? "angle-down" : "angle-right")
136 67
    }
137 68
  }
138 69
}
app/views/gantts/_chart.html.erb
14 14

  
15 15
<%= gantt_chart_tag(@query, layout) do %>
16 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">
21
        <%= gantt_subjects_tag do %>
22
          <%= form_tag({}, data: {cm_url: issues_context_menu_path}) do %>
23
            <%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %>
24
            <%= gantt.subjects.html_safe %>
25
          <% end %>
17
    <section class="gantt-pane">
18
      <header aria-hidden="true"></header>
19
      <div class="gantt-pane-body" data-controller="gantt--subjects">
20
        <%= form_tag({}, data: {cm_url: issues_context_menu_path}) do %>
21
          <%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %>
22
          <%= gantt.subjects.html_safe %>
26 23
        <% end %>
27 24
      </div>
28 25
    </section>
......
33 30
      next if Redmine::Helpers::Gantt::UNAVAILABLE_COLUMNS.include?(column.name)
34 31
      column_name = column.name.to_s.tr('.', '_')
35 32
  %>
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 %>">
33
    <%= gantt_column_tag(column_name, min_width: 20, width: '50px', class: 'gantt-selected-column',
34
                         hidden: !query.draw_selected_columns,
35
                         data: {'gantt--chart-target': 'selectedColumn'}) do %>
36
      <section class="gantt-pane">
37
        <header><%= column.caption %></header>
38
        <div class="gantt-pane-body">
41 39
          <%= gantt.selected_column_content(column: column, top: layout.content_top, zoom: layout.zoom, g_width: layout.chart_width).html_safe %>
42 40
        </div>
43 41
      </section>
44 42
    <% end %>
45 43
  <% end %>
46 44

  
47
  <div id="gantt_area" class="gantt-timeline" data-gantt--chart-target="ganttArea">
45
  <div class="gantt-timeline">
48 46
    <div class="gantt-timeline-canvas">
49
      <header class="gantt-timeline-header">
50
        <div class="gantt-header-row gantt-header-months">
47
      <header>
48
        <div class="gantt-header-row" data-gantt-period="month">
51 49
          <% 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 %>"
50
            <div class="gantt-period<%= ' gantt-period-spans-body' if period.spans_body %><%= ' gantt-period-last' if period.last %>"
53 51
                 style="<%= gantt_period_style(period) %>">
54 52
              <%= link_to "#{period.date.year}-#{period.date.month}",
55 53
                          gantt.params.merge(year: period.date.year, month: period.date.month),
......
59 57
        </div>
60 58

  
61 59
        <% if layout.show_weeks? %>
62
          <div class="gantt-header-row gantt-header-weeks">
60
          <div class="gantt-header-row" data-gantt-period="week">
63 61
            <% 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 %>"
62
              <div class="gantt-period<%= ' gantt-period-spans-body' if period.spans_body %><%= ' gantt-period-last' if period.last %>"
65 63
                   style="<%= gantt_period_style(period) %>">
66 64
                <small><%= period.date.cweek if period.date && period.width >= 16 %></small>
67 65
              </div>
......
70 68
        <% end %>
71 69

  
72 70
        <% if layout.show_day_numbers? %>
73
          <div class="gantt-header-row gantt-header-day-numbers">
71
          <div class="gantt-header-row" data-gantt-period="day-number">
74 72
            <% 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 %>"
73
              <div class="gantt-period<%= ' gantt-period-non-working' if period.non_working %><%= ' gantt-period-last' if period.last %>"
76 74
                   style="<%= gantt_period_style(period) %>"><%= period.date.day %></div>
77 75
            <% end %>
78 76
          </div>
79 77
        <% end %>
80 78

  
81 79
        <% if layout.show_days? %>
82
          <div class="gantt-header-row gantt-header-days">
80
          <div class="gantt-header-row" data-gantt-period="day">
83 81
            <% 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 %>"
82
              <div class="gantt-period gantt-period-spans-body<%= ' gantt-period-non-working' if period.non_working %><%= ' gantt-period-last' if period.last %>"
85 83
                   style="<%= gantt_period_style(period) %>"><%= day_letter(period.date.cwday) %></div>
86 84
            <% end %>
87 85
          </div>
......
95 93
        <% end %>
96 94

  
97 95
        <% if layout.today_start %>
98
          <div id="today_line" style="<%= gantt_css_variables('gantt-today-start': "#{layout.today_start}px") %>" aria-hidden="true"></div>
96
          <div class="gantt-today" data-gantt--chart-target="today" style="<%= gantt_css_variables('gantt-today-start': "#{layout.today_start}px") %>" aria-hidden="true"></div>
99 97
        <% end %>
100 98

  
101
        <div id="gantt_draw_area" data-gantt--chart-target="drawArea" aria-hidden="true"></div>
99
        <div class="gantt-relations" data-gantt--chart-target="relations" aria-hidden="true"></div>
102 100
      </div>
103 101
    </div>
104 102
  </div>
lib/redmine/helpers/gantt.rb
230 230
        begin
231 231
          Project.project_tree(projects) do |project, level|
232 232
            options[:indent] = indent + level * options[:indent_increment]
233
            render_project(project, options)
233
            parent_row_key =
234
              if level > 0 && project.parent
235
                gantt_project_row_key(project.parent)
236
              end
237
            render_project(project, options, parent_row_key: parent_row_key)
234 238
          end
235 239
        rescue MaxLinesLimitReached
236 240
          @truncated = true
......
240 244
        render_end(options)
241 245
      end
242 246

  
243
      def render_project(project, options={})
244
        render_object_row(project, options)
247
      def render_project(project, options={}, parent_row_key: nil)
248
        row = gantt_project_row(project, parent_row_key: parent_row_key)
249
        render_object_row(project, options, row: row)
245 250
        increment_indent(options) do
246 251
          # render issue that are not assigned to a version
247 252
          issues = project_issues(project).select {|i| i.fixed_version_id.nil?}
248
          render_issues(issues, options)
253
          render_issues(issues, options, parent_row_key: row[:gantt_row_key])
249 254
          # then render project versions and their issues
250 255
          versions = project_versions(project)
251 256
          self.class.sort_versions!(versions)
......
256 261
      end
257 262

  
258 263
      def render_version(project, version, options={})
259
        render_object_row(version, options)
264
        row = gantt_version_row(version, project)
265
        render_object_row(version, options, row: row)
260 266
        increment_indent(options) do
261 267
          issues = version_issues(project, version)
262
          render_issues(issues, options)
268
          render_issues(issues, options, parent_row_key: row[:gantt_row_key])
263 269
        end
264 270
      end
265 271

  
266
      def render_issues(issues, options={})
272
      def render_issues(issues, options={}, parent_row_key: nil)
267 273
        self.class.sort_issues!(issues)
268 274
        ancestors = []
269 275
        issues.each do |issue|
......
271 277
            ancestors.pop
272 278
            decrement_indent(options)
273 279
          end
274
          render_object_row(issue, options)
280
          issue_parent_row_key =
281
            ancestors.any? ? gantt_issue_row_key(ancestors.last) : parent_row_key
282
          row = gantt_issue_row(issue, parent_row_key: issue_parent_row_key)
283
          render_object_row(issue, options, row: row)
275 284
          unless issue.leaf?
276 285
            ancestors << issue
277 286
            increment_indent(options)
......
280 289
        decrement_indent(options, ancestors.size)
281 290
      end
282 291

  
283
      def render_object_row(object, options)
292
      def render_object_row(object, options, row:)
284 293
        class_name = object.class.name.downcase
285
        send(:"subject_for_#{class_name}", object, options) unless options[:only] == :lines || options[:only] == :selected_columns
294
        subject_options =
295
          if options[:format] == :html
296
            options.merge(:gantt_row => row)
297
          else
298
            options
299
          end
300
        send(:"subject_for_#{class_name}", object, subject_options) unless options[:only] == :lines || options[:only] == :selected_columns
286 301
        unless options[:only] == :subjects || options[:only] == :selected_columns
287 302
          line = send(:"line_for_#{class_name}", object, options)
288
          @lines << html_gantt_row(object, options, line) if options[:format] == :html
303
          if options[:format] == :html
304
            @lines << html_gantt_row(options, line, row: row)
305
          end
306
        end
307
        if options[:only] == :selected_columns && options[:column].present?
308
          column_content_for_object(object, options, row: row)
289 309
        end
290
        column_content_for_object(object, options) if options[:only] == :selected_columns && options[:column].present?
291 310
        options[:top] += options[:top_increment]
292 311
        @number_of_rows += 1
293 312
        if @max_rows && @number_of_rows >= @max_rows
......
302 321
        end
303 322
      end
304 323

  
324
      def gantt_project_row_key(project)
325
        "project-#{project.id}"
326
      end
327

  
328
      def gantt_version_row_key(version, project)
329
        "version-#{version.id}-project-#{project.id}"
330
      end
331

  
332
      def gantt_issue_row_key(issue)
333
        "issue-#{issue.id}"
334
      end
335

  
336
      def gantt_project_row(project, parent_row_key: nil)
337
        row = {
338
          :gantt_row_key => gantt_project_row_key(project),
339
          :gantt_row_type => 'project'
340
        }
341
        row[:gantt_parent_row_key] = parent_row_key if parent_row_key
342
        row
343
      end
344

  
345
      def gantt_version_row(version, project)
346
        {
347
          :gantt_row_key => gantt_version_row_key(version, project),
348
          :gantt_row_type => 'version',
349
          :gantt_parent_row_key => gantt_project_row_key(project)
350
        }
351
      end
352

  
353
      def gantt_issue_row(issue, parent_row_key: nil)
354
        row = {
355
          :gantt_row_key => gantt_issue_row_key(issue),
356
          :gantt_row_type => 'issue'
357
        }
358
        row[:gantt_parent_row_key] = parent_row_key if parent_row_key
359
        row
360
      end
361

  
305 362
      def increment_indent(options, factor=1)
306 363
        options[:indent] += options[:indent_increment] * factor
307 364
        if block_given?
......
357 414
        end
358 415
      end
359 416

  
360
      def column_content_for_object(object, options)
417
      def column_content_for_object(object, options, row:)
361 418
        if options[:format] == :html
362
          data_options = {
363
            :collapse_expand => "#{object.class}-#{object.id}".downcase,
364
            :number_of_rows => number_of_rows
365
          }
366 419
          value = object.is_a?(Issue) ? view.column_content(options[:column], object) : nil
367 420
          content =
368 421
            view.content_tag(
369 422
              :div, value,
370 423
              :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)),
377
              :data => data_options
424
              :class => 'gantt-row',
425
              :data => row
378 426
            )
379 427
          @columns[options[:column].name] << content if @columns.has_key?(options[:column].name)
380 428
          content
......
382 430
      end
383 431

  
384 432
      def column_content_for_issue(issue, options)
385
        column_content_for_object(issue, options)
433
        column_content_for_object(issue, options, row: gantt_issue_row(issue))
386 434
      end
387 435

  
388 436
      def subject(label, options, object=nil)
......
797 845

  
798 846
      def html_subject(params, subject, object)
799 847
        content = html_subject_content(object) || subject
800
        tag_options = {}
848
        tag_options = {:class => []}
801 849
        case object
802 850
        when Issue
803
          tag_options[:id] = "issue-#{object.id}"
804
          tag_options[:class] = "issue-subject hascontextmenu"
851
          tag_options[:class] << 'hascontextmenu'
805 852
          tag_options[:title] = object.subject
806 853
          has_children =
807 854
            if object.leaf?
......
812 859
              children.any? {|child| child.fixed_version_id == fixed_version_id}
813 860
            end
814 861
        when Version
815
          tag_options[:id] = "version-#{object.id}"
816
          tag_options[:class] = "version-name"
817 862
          has_children = object.fixed_issues.exists?
818 863
        when Project
819
          tag_options[:class] = "project-name"
820 864
          has_children = object.issues.exists? || object.versions.exists?
821 865
        end
822
        if object
823
          tag_options[:data] = {
824
            :collapse_expand => {
825
              :top_increment => params[:top_increment],
826
              :obj_id => "#{object.class}-#{object.id}".downcase,
827
            },
828
            :number_of_rows => number_of_rows,
829
          }
830
        end
831 866
        if has_children
832 867
          content = view.content_tag(:span,
833 868
                                     view.sprite_icon('angle-down', rtl: true).html_safe,
834 869
                                     :class => 'icon icon-expanded expander',
835 870
                                     :data => {:action => 'click->gantt--subjects#handleEntryClick'}) + content
836
          tag_options[:class] += ' open'
871
          tag_options[:class] << 'is-expanded'
837 872
        else
838 873
          if params[:indent]
839 874
            params = params.dup
840 875
            params[:indent] += 18
841 876
          end
842 877
        end
843
        tag_options[:class] = [tag_options[:class], 'gantt-row', 'gantt-subject-row'].compact.join(' ')
878
        tag_options[:class] << 'gantt-row'
879
        tag_options[:class] = tag_options[:class].join(' ')
880
        if object
881
          tag_options[:data] = params[:gantt_row].to_h.merge(
882
            'gantt--subjects-target': 'row'
883
          )
884
        end
844 885
        row_variables = {
845 886
          'gantt-row-top': "#{params[:top] || 0}px",
846 887
          'gantt-row-indent': "#{params[:indent]}px"
847 888
        }
848
        if params[:subject_width]
849
          row_variables['gantt-row-width'] = "#{params[:subject_width] - params[:indent]}px"
850
        end
851 889
        tag_options[:style] = css_variables(row_variables)
852 890
        output = view.content_tag(:div, content, tag_options)
853 891
        @subjects << output
......
889 927

  
890 928
      def html_task(params, coords, markers, label, object)
891 929
        output = +''
892
        data_options = {}
893
        if object
894
          data_options[:collapse_expand] = "#{object.class}-#{object.id}".downcase
895
          data_options[:number_of_rows] = number_of_rows
896
        end
897
        css = "task " +
898
          case object
899
          when Project
900
            "project"
901
          when Version
902
            "version"
903
          when Issue
904
            object.leaf? ? 'leaf' : 'parent'
905
          else
906
            ""
907
          end
930
        task_classes = ['gantt-task']
931
        task_classes << 'gantt-task-parent' if object.is_a?(Issue) && !object.leaf?
908 932
        # Renders the task bar, with progress and late
909 933
        if coords[:bar_start] && coords[:bar_end]
910 934
          width = coords[:bar_end] - coords[:bar_start] - 2
911 935
          style = task_style(coords[:bar_start], width)
912
          html_id = "task-todo-issue-#{object.id}" if object.is_a?(Issue)
913
          html_id = "task-todo-version-#{object.id}" if object.is_a?(Version)
914
          content_opt = {:style => style,
915
                         :class => "#{css} task_todo",
916
                         :id => html_id,
917
                         :data => {}}
936
          todo_data = {}
918 937
          if object.is_a?(Issue)
938
            todo_data['gantt-issue-id'] = object.id
919 939
            rels = issue_relations(object)
920
            if rels.present?
921
              content_opt[:data] = {"rels" => rels.to_json}
922
            end
940
            todo_data['gantt-relations'] = rels.to_json if rels.present?
923 941
          end
924
          content_opt[:data].merge!(data_options)
942
          content_opt = {:style => style,
943
                         :class => (task_classes + ['gantt-task-todo']).join(' '),
944
                         :data => todo_data}
925 945
          output << view.content_tag(:div, '&nbsp;'.html_safe, content_opt)
926 946
          if coords[:bar_late_end]
927 947
            width = coords[:bar_late_end] - coords[:bar_start] - 2
928 948
            style = task_style(coords[:bar_start], width)
929 949
            output << view.content_tag(:div, '&nbsp;'.html_safe,
930 950
                                       :style => style,
931
                                       :class => "#{css} task_late",
932
                                       :data => data_options)
951
                                       :class => (task_classes + ['gantt-task-late']).join(' '))
933 952
          end
934 953
          if coords[:bar_progress_end]
935 954
            width = coords[:bar_progress_end] - coords[:bar_start] - 2
936 955
            style = task_style(coords[:bar_start], width)
937
            html_id = "task-done-issue-#{object.id}" if object.is_a?(Issue)
938
            html_id = "task-done-version-#{object.id}" if object.is_a?(Version)
939 956
            output << view.content_tag(:div, '&nbsp;'.html_safe,
940 957
                                       :style => style,
941
                                       :class => "#{css} task_done",
942
                                       :id => html_id,
943
                                       :data => data_options)
958
                                       :class => (task_classes + ['gantt-task-done']).join(' '))
944 959
          end
945 960
        end
946 961
        # Renders the markers
......
949 964
            style = task_style(coords[:start], 15)
950 965
            output << view.content_tag(:div, '&nbsp;'.html_safe,
951 966
                                       :style => style,
952
                                       :class => "#{css} marker starting",
953
                                       :data => data_options)
967
                                       :class => (task_classes + ['gantt-task-marker', 'gantt-task-start']).join(' '))
954 968
          end
955 969
          if coords[:end]
956 970
            style = task_style(coords[:end], 15)
957 971
            output << view.content_tag(:div, '&nbsp;'.html_safe,
958 972
                                       :style => style,
959
                                       :class => "#{css} marker ending",
960
                                       :data => data_options)
973
                                       :class => (task_classes + ['gantt-task-marker', 'gantt-task-end']).join(' '))
961 974
          end
962 975
        end
963 976
        # Renders the label on the right
......
965 978
          style = task_style((coords[:bar_end] || 0) + 8, 15)
966 979
          output << view.content_tag(:div, label,
967 980
                                     :style => style,
968
                                     :class => "#{css} label",
969
                                     :data => data_options)
981
                                     :class => (task_classes + ['gantt-task-label']).join(' '))
970 982
        end
971 983
        # Renders the tooltip
972 984
        if object.is_a?(Issue) && coords[:bar_start] && coords[:bar_end]
973 985
          s = view.content_tag(:span,
974 986
                               view.render_issue_tooltip(object).html_safe,
975
                               :class => "tip")
987
                               :class => 'tip')
976 988
          s += view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
977 989
                                :value => object.id,
978 990
                                :class => 'toggle-selection')
979 991
          style = task_style(coords[:bar_start], coords[:bar_end] - coords[:bar_start])
980 992
          output << view.content_tag(:div, s.html_safe,
981 993
                                     :style => style,
982
                                     :class => "tooltip hascontextmenu",
983
                                     :data => data_options)
994
                                     :class => 'tooltip hascontextmenu')
984 995
        end
985 996
        output
986 997
      end
987 998

  
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
        }
999
      def html_gantt_row(params, content, row:)
993 1000
        style = css_variables('gantt-row-top': "#{params[:top] || 0}px")
994 1001
        view.content_tag(
995 1002
          :div,
996 1003
          content.to_s.html_safe,
997
          :class => 'gantt-row gantt-timeline-row',
1004
          :class => 'gantt-row',
998 1005
          :style => style,
999
          :data => data_options
1006
          :data => row
1000 1007
        )
1001 1008
      end
1002 1009

  
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.issue-subject.hascontextmenu'
59
    assert_select 'div.gantt-column[data-gantt-column=subjects] div.gantt-row[data-gantt-row-type=issue].hascontextmenu'
60 60
    assert_select 'div.tooltip.hascontextmenu' do
61 61
      assert_select 'img[class="gravatar avatar"]'
62 62
    end
......
155 155
    get :show
156 156
    assert_response :success
157 157

  
158
    assert_select 'div.task_todo[id=?][data-rels*=?]', "task-todo-issue-#{issue1.id}", issue2.id.to_s
159
    assert_select 'div.task_todo[id=?]:not([data-rels])', "task-todo-issue-#{issue2.id}"
158
    assert_select 'div.gantt-task-todo[data-gantt-issue-id=?][data-gantt-relations*=?]', issue1.id.to_s, issue2.id.to_s
159
    assert_select 'div.gantt-task-todo[data-gantt-issue-id=?]:not([data-gantt-relations])', issue2.id.to_s
160 160
  end
161 161

  
162 162
  def test_gantt_should_export_to_pdf
......
223 223
        }
224 224
      )
225 225
      assert_response :success
226
      assert_select 'div.gantt-period-month>a', :text => /^[\d-]+$/, :count => 40
226
      assert_select 'div.gantt-header-row[data-gantt-period=month] .gantt-period>a', :text => /^[\d-]+$/, :count => 40
227 227

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

  
......
251 251
    assert_response :success
252 252

  
253 253
    # eCookbook
254
    assert_subject_row('div.project-name', row: '0', text: project.name)
255
    assert_chart_row('div.task.project.task_todo', row: '0', style_substring: 'inset-inline-start:0px;width:138px')
254
    assert_subject_row('project-1', row: '0', text: project.name)
255
    assert_chart_row('div.gantt-task-todo', row: '0', style_substring: 'inset-inline-start:0px;width:138px')
256 256

  
257 257
    assert_issue_row(3, 'Bug #3', row: '1')
258
    assert_chart_row('div.task.leaf.task_todo', row: '1', style_substring: 'inset-inline-start:0px;width:38px')
258
    assert_chart_row('div.gantt-task-todo', row: '1', style_substring: 'inset-inline-start:0px;width:38px')
259 259

  
260 260
    assert_issue_row(7, 'Bug #7', row: '2')
261
    assert_chart_row('div.task.leaf.task_todo', row: '2', style_substring: 'inset-inline-start:16px;width:42px')
261
    assert_chart_row('div.gantt-task-todo', row: '2', style_substring: 'inset-inline-start:16px;width:42px')
262 262

  
263 263
    assert_issue_row(1, 'Bug #1', row: '3')
264
    assert_chart_row('div.task.leaf.task_todo', row: '3', style_substring: 'inset-inline-start:52px;width:46px')
264
    assert_chart_row('div.gantt-task-todo', row: '3', style_substring: 'inset-inline-start:52px;width:46px')
265 265

  
266 266
    # Version 1.0
267
    assert_subject_row('div#version-2', row: '4', text: '1.0')
268
    assert_chart_row('div.task.version', row: '4', style_substring: 'inset-inline-start:48px;width:90px')
267
    assert_subject_row('version-2-project-1', row: '4', text: '1.0')
268
    assert_chart_row('div.gantt-task-todo', row: '4', style_substring: 'inset-inline-start:48px;width:90px')
269 269

  
270 270
    assert_issue_row(2, 'Feature request #2', row: '5')
271
    assert_chart_row('div.task.leaf.task_todo', row: '5', style_substring: 'inset-inline-start:48px;width:90px')
271
    assert_chart_row('div.gantt-task-todo', row: '5', style_substring: 'inset-inline-start:48px;width:90px')
272 272

  
273 273
    # Private child of eCookbook
274 274
    assert_subject_row(
275
      'div.project-name[data-collapse-expand*="project-5"]',
275
      'project-5',
276 276
      row: '6',
277 277
      text: projects(:projects_005).name
278 278
    )
279
    assert_chart_row('div.task.project.task_todo', row: '6', style_substring: 'inset-inline-start:56px;width:6px')
279
    assert_chart_row('div.gantt-task-todo', row: '6', style_substring: 'inset-inline-start:56px;width:6px')
280 280

  
281 281
    assert_issue_row(6, 'Bug #6', row: '7')
282
    assert_chart_row('div.task.leaf.task_todo', row: '7', style_substring: 'inset-inline-start:56px;width:6px')
282
    assert_chart_row('div.gantt-task-todo', row: '7', style_substring: 'inset-inline-start:56px;width:6px')
283 283

  
284 284
    assert_issue_row(9, 'Bug #9', row: '8')
285
    assert_chart_row('div.task.leaf.task_todo', row: '8', style_substring: 'inset-inline-start:56px;width:6px')
285
    assert_chart_row('div.gantt-task-todo', row: '8', style_substring: 'inset-inline-start:56px;width:6px')
286 286

  
287 287
    assert_issue_row(10, 'Bug #10', row: '9')
288
    assert_chart_row('div.task.leaf.task_todo', row: '9', style_substring: 'inset-inline-start:56px;width:6px')
289
    assert_select 'div.task[id=?][data-rels*=9]', 'task-todo-issue-10'
288
    assert_chart_row('div.gantt-task-todo', row: '9', style_substring: 'inset-inline-start:56px;width:6px')
289
    assert_select 'div.gantt-task-todo[data-gantt-issue-id=?][data-gantt-relations*=?]', '10', '9'
290 290

  
291 291
    # eCookbook Subproject1
292 292
    assert_subject_row(
293
      'div.project-name[data-collapse-expand*="project-3"]',
293
      'project-3',
294 294
      row: '10',
295 295
      text: projects(:projects_003).name
296 296
    )
......
322 322

  
323 323
    6.times do |offset|
324 324
      m = selected_start.since(offset.month)
325
      assert_select 'div.gantt-period-month > a', text: "#{m.year}-#{m.month}"
325
      assert_select 'div.gantt-header-row[data-gantt-period=month] .gantt-period > a', text: "#{m.year}-#{m.month}"
326 326
    end
327 327

  
328 328
    # eCookbook
329
    assert_subject_row('div.project-name', row: '0', text: projects(:projects_001).name)
330
    assert_chart_row('div.task.project.task_todo', row: '0', style_substring: 'inset-inline-start:0px;width:258px')
329
    assert_subject_row('project-1', row: '0', text: projects(:projects_001).name)
330
    assert_chart_row('div.gantt-task-todo', row: '0', style_substring: 'inset-inline-start:0px;width:258px')
331 331

  
332 332
    # Private child of eCookbook
333 333
    assert_subject_row(
334
      'div.project-name[data-collapse-expand*="project-5"]',
334
      'project-5',
335 335
      row: '1',
336 336
      text: project.name
337 337
    )
338
    assert_chart_row('div.task.project.task_todo', row: '1', style_substring: 'inset-inline-start:176px;width:6px')
338
    assert_chart_row('div.gantt-task-todo', row: '1', style_substring: 'inset-inline-start:176px;width:6px')
339 339

  
340 340
    # Bug #6
341 341
    assert_issue_row(6, 'Bug #6', row: '2')
342
    assert_chart_row('div.task.leaf.task_todo', row: '2', style_substring: 'inset-inline-start:176px;width:6px')
342
    assert_chart_row('div.gantt-task-todo', row: '2', style_substring: 'inset-inline-start:176px;width:6px')
343 343

  
344 344
    # Bug #9
345 345
    assert_issue_row(9, 'Bug #9', row: '3')
346
    assert_chart_row('div.task.leaf.task_todo', row: '3', style_substring: 'inset-inline-start:176px;width:6px')
346
    assert_chart_row('div.gantt-task-todo', row: '3', style_substring: 'inset-inline-start:176px;width:6px')
347 347

  
348 348
    # Bug #10
349 349
    assert_issue_row(10, 'Bug #10', row: '4')
350
    assert_chart_row('div.task.leaf.task_todo', row: '4', style_substring: 'inset-inline-start:176px;width:6px')
350
    assert_chart_row('div.gantt-task-todo', row: '4', style_substring: 'inset-inline-start:176px;width:6px')
351 351

  
352
    assert_select 'div.task[id=?][data-rels*=9]', 'task-todo-issue-10'
352
    assert_select 'div.gantt-task-todo[data-gantt-issue-id=?][data-gantt-relations*=?]', '10', '9'
353 353
  end
354 354

  
355 355
  test 'shows six months starting from current month' do
......
366 366
    6.times do |offset|
367 367
      m = start_of_month.since(offset.months)
368 368

  
369
      assert_select 'div.gantt-period-month > a', text: "#{m.year}-#{m.month}"
369
      assert_select 'div.gantt-header-row[data-gantt-period=month] .gantt-period > a', text: "#{m.year}-#{m.month}"
370 370
    end
371 371

  
372 372
    assert_select 'input#months[value=?]', '6'
......
377 377

  
378 378
  private
379 379

  
380
  def assert_subject_row(selector, row:, text:)
381
    assert_select "div.gantt_subjects form #{selector}[data-number-of-rows=?]", row do
380
  def assert_subject_row(row_key, row:, text:)
381
    selector = "div.gantt-column[data-gantt-column=subjects] form > div.gantt-row:nth-of-type(#{row.to_i + 1})[data-gantt-row-key=?]"
382
    assert_select selector, row_key do
382 383
      assert_select 'a', text: text
383 384
    end
384 385
  end
385 386

  
386 387
  def assert_issue_row(issue_id, link_text, row:)
387
    selector = "div.gantt_subjects form div#issue-#{issue_id}[data-number-of-rows=\"#{row}\"]"
388
    assert_select selector do
388
    selector = "div.gantt-column[data-gantt-column=subjects] form > div.gantt-row:nth-of-type(#{row.to_i + 1})[data-gantt-row-key=?]"
389
    assert_select selector, "issue-#{issue_id}" do
389 390
      assert_select 'a.issue', text: link_text
390 391
    end
391 392
  end
392 393

  
393 394
  def assert_chart_row(selector, row:, style_substring:)
394 395
    style_substring = "--gantt-task-start:#{style_substring.delete_prefix('inset-inline-start:').sub(';width:', ';--gantt-task-width:')}"
395
    matcher = "#gantt_area #{selector}[data-number-of-rows=?][style*=?]"
396
    assert_select matcher, row, style_substring, minimum: 1
396
    matcher = "div.gantt-timeline form > div.gantt-row:nth-of-type(#{row.to_i + 1}) #{selector}[style*=?]"
397
    assert_select matcher, style_substring, minimum: 1
397 398
  end
398 399

  
399 400
  # Freezes today and resets the start and due dates of issues and versions in the eCookbook project and its descendants to fixed values
test/system/gantts_test.rb
11 11
    visit_gantt
12 12
    expand_options
13 13

  
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'
14
    assert_no_selector 'div.gantt-column[data-gantt-column=status]'
15
    assert_no_selector 'div.gantt-column[data-gantt-column=priority]'
16
    assert_no_selector 'div.gantt-column[data-gantt-column=assigned_to]'
17
    assert_no_selector 'div.gantt-column[data-gantt-column=updated_on]'
18 18

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

  
21
    assert_selector '.gantt_subjects_container.draw_selected_columns'
22
    assert_selector 'div#status'
23
    assert_selector 'div#priority'
24
    assert_selector 'div#assigned_to'
25
    assert_selector 'div#updated_on'
21
    assert_selector 'div.gantt-column[data-gantt-column=subjects]'
22
    assert_selector 'div.gantt-column[data-gantt-column=status]'
23
    assert_selector 'div.gantt-column[data-gantt-column=priority]'
24
    assert_selector 'div.gantt-column[data-gantt-column=assigned_to]'
25
    assert_selector 'div.gantt-column[data-gantt-column=updated_on]'
26
  end
27

  
28
  test 'tree toggle updates the same logical rows across every pane' do
29
    visit_gantt
30
    expand_options
31
    find('#draw_selected_columns').check
32

  
33
    project_row = find('[data-gantt-column="subjects"] .gantt-row[data-gantt-row-key="project-1"]')
34
    subject_issue = '[data-gantt-column="subjects"] .gantt-row[data-gantt-row-key="issue-3"]'
35
    column_issue = '[data-gantt-column="status"] .gantt-row[data-gantt-row-key="issue-3"]'
36
    timeline_issue = '.gantt-timeline-body .gantt-row[data-gantt-row-key="issue-3"]'
... This diff was truncated because it exceeds the maximum size that can be displayed.
(3-3/5)