Patch #44567 » 0002-Organize-Gantt-chart-DOM-structure-and-semantics.patch
| 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, ' '.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, ' '.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, ' '.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, ' '.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, ' '.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"]' |
|