@operato/gantt 7.1.27 → 7.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ox-gantt.ts DELETED
@@ -1,532 +0,0 @@
1
- import { LitElement, html, css, PropertyValues, PropertyValueMap, nothing, TemplateResult } from 'lit'
2
- import * as d3 from 'd3'
3
- import { customElement, property, query, state } from 'lit/decorators.js'
4
- import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
5
- import { i18next, localize } from '@operato/i18n'
6
- import { ScrollbarStyles } from '@operato/styles'
7
-
8
- import './ox-gantt-chart.js'
9
- import './ox-resource-chart.js'
10
- import './ox-gantt-resize-splitter.js'
11
- import { OxGanttChart } from './ox-gantt-chart.js'
12
- import { OxResourceChart } from './ox-resource-chart.js'
13
- import { ColumnConfig, Task, TimeScale, Resource } from './types.js'
14
-
15
- @customElement('ox-gantt')
16
- export class OxGantt extends localize(i18next)(LitElement) {
17
- static styles = [
18
- MDTypeScaleStyles,
19
- ScrollbarStyles,
20
- css`
21
- :host {
22
- display: block;
23
- font-family: Arial, sans-serif;
24
- text-wrap: nowrap;
25
- position: relative;
26
- }
27
-
28
- .container {
29
- flex: 1;
30
-
31
- display: flex;
32
- flex-direction: column;
33
- width: 100%;
34
- height: 100%;
35
- box-sizing: border-box;
36
- }
37
-
38
- .resource-container {
39
- height: 300px;
40
-
41
- display: flex;
42
- overflow: hidden;
43
- box-sizing: border-box;
44
- }
45
-
46
- .resource-table-container {
47
- padding: 0;
48
- overflow: auto;
49
- overflow-y: overlay;
50
- box-sizing: border-box;
51
- border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-outline));
52
- }
53
-
54
- .resource-chart-container {
55
- flex: 1;
56
- padding: 0;
57
- box-sizing: border-box;
58
- border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-outline));
59
- }
60
-
61
- .task-container {
62
- flex: 1;
63
-
64
- display: flex;
65
- overflow: hidden;
66
- box-sizing: border-box;
67
- }
68
-
69
- .task-table-container {
70
- padding: 0;
71
- overflow: auto;
72
- overflow-y: overlay;
73
- box-sizing: border-box;
74
- border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-outline));
75
- }
76
-
77
- .task-chart-container {
78
- flex: 1;
79
- padding: 0;
80
- box-sizing: border-box;
81
- border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-outline));
82
- }
83
-
84
- table {
85
- width: 100%;
86
- border-collapse: collapse;
87
- border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-outline));
88
- margin: -1px;
89
- margin-right: 0;
90
- }
91
-
92
- thead th {
93
- position: sticky;
94
- top: -1px;
95
- z-index: 1;
96
- color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
97
- background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
98
- border-color: var(--ox-gantt-header-border-color, var(--md-sys-color-outline));
99
- text-transform: capitalize;
100
- }
101
-
102
- th,
103
- td {
104
- border: 1px solid var(--ox-gantt-border-color, var(--md-sys-color-outline));
105
- padding: 4px;
106
- text-align: left;
107
- }
108
-
109
- th {
110
- height: 40px;
111
- box-sizing: border-box;
112
- text-align: center;
113
- }
114
-
115
- td {
116
- height: 30px;
117
- box-sizing: border-box;
118
- }
119
-
120
- tr.selected {
121
- color: var(--ox-gantt-selected-color, var(--md-sys-color-on-surface));
122
- background-color: var(--ox-gantt-selected-background-color, var(--md-sys-color-surface-variant));
123
- }
124
-
125
- .resource-table-container th {
126
- height: 30px;
127
- }
128
-
129
- svg.toggle-icon[collapsed] {
130
- transform: rotate(-90deg);
131
- }
132
-
133
- span[group] {
134
- cursor: pointer;
135
- user-select: none;
136
- }
137
- `
138
- ]
139
-
140
- @property({ type: Array }) tasks: Task[] = []
141
- @property({ type: Array }) resources: Resource[] = []
142
- @property({ type: String, attribute: 'from-date' })
143
- fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0] /* beginning of this year */
144
- @property({ type: String, attribute: 'to-date' })
145
- toDate: string = new Date(new Date().getFullYear(), 11, 31).toISOString().split('T')[0] /* end of this year */
146
- @property({ type: Boolean, attribute: 'extend-grid-lines' }) extendGridLines: boolean = false
147
- @property({ type: String, attribute: 'time-scale' }) timeScale: TimeScale = 'week-day'
148
- @property({ type: Object }) colorProvider: (task: Task) => string = (task: Task) => {
149
- const { tags = [] } = task
150
- if (tags.includes('critical')) {
151
- return 'red'
152
- } else if (tags.includes('milestone')) {
153
- return 'blue'
154
- } else if (tags.includes('done')) {
155
- return 'green'
156
- }
157
- return 'grey'
158
- }
159
- @property({ type: Object }) columnConfigProvider: () => ColumnConfig[] = function () {
160
- return [
161
- { name: 'id', label: i18next.t('label.gantt-task-id') || 'ID', visible: true, width: 50, order: 1 },
162
- { name: 'title', label: i18next.t('label.gantt-task-title') || 'title', visible: true, width: 150, order: 2 },
163
- { name: 'type', label: i18next.t('label.gantt-task-type') || 'type', visible: true, width: 100, order: 3 },
164
- {
165
- name: 'startDate',
166
- label: i18next.t('label.gantt-task-start-date') || 'start date',
167
- visible: true,
168
- width: 100,
169
- order: 4
170
- },
171
- {
172
- name: 'endDate',
173
- label: i18next.t('label.gantt-task-end-date') || 'end date',
174
- visible: true,
175
- width: 100,
176
- order: 5
177
- },
178
- {
179
- name: 'duration',
180
- label: i18next.t('label.gantt-task-duration') || 'duration',
181
- visible: true,
182
- width: 30,
183
- order: 6
184
- },
185
- { name: 'tags', label: i18next.t('label.gantt-task-tags') || 'tags', visible: true, width: 100, order: 7 },
186
- {
187
- name: 'progress',
188
- label: i18next.t('label.gantt-task-progress') || 'progress',
189
- visible: true,
190
- width: 100,
191
- order: 8
192
- },
193
- {
194
- name: 'dependsOn',
195
- label: i18next.t('label.gantt-task-previous') || 'previous',
196
- visible: true,
197
- width: 100,
198
- order: 9
199
- },
200
- {
201
- name: 'resources',
202
- label: i18next.t('label.gantt-task-resources') || 'resources',
203
- visible: true,
204
- width: 100,
205
- order: 10
206
- }
207
- ]
208
- }
209
-
210
- @state() selectedTaskId?: string
211
-
212
- @query('.task-container') taskContainer!: HTMLDivElement
213
- @query('.task-table-container') taskTableContainer!: HTMLDivElement
214
- @query('.resource-container') resourceContainer!: HTMLDivElement
215
- @query('.resource-table-container') resourceTableContainer!: HTMLDivElement
216
- @query('.task-table') taskTable!: HTMLTableElement
217
- @query('ox-gantt-chart') ganttChart!: OxGanttChart
218
- @query('ox-resource-chart') resourceChart!: OxResourceChart
219
-
220
- private _startWidth: number = 0
221
- private _startHeight: number = 0
222
- private _isDragging: boolean = false
223
-
224
- render() {
225
- return html`
226
- <div class="container md-typescale-body-medium">
227
- <div class="task-container">
228
- <div class="task-table-container" @scroll=${this.syncScroll}>${this.renderTaskTable()}</div>
229
- <ox-gantt-resize-splitter
230
- vertical
231
- @splitter-dragstart=${(e: CustomEvent) => this.resizeHorizontalStart(e)}
232
- @splitter-drag=${(e: CustomEvent) => this.resizeHorizontalDrag(e)}
233
- @splitter-dragend=${(e: CustomEvent) => this.resizeHorizontalEnd(e)}
234
- ></ox-gantt-resize-splitter>
235
- <ox-gantt-chart
236
- class="task-chart-container"
237
- @scroll=${this.syncScroll}
238
- .fromDate=${this.fromDate}
239
- .toDate=${this.toDate}
240
- .tasks=${this.tasks}
241
- .selectedTaskId=${this.selectedTaskId}
242
- .axisScale=${this.timeScale}
243
- .colorProvider=${this.colorProvider}
244
- ?extend-grid-lines=${this.extendGridLines}
245
- @task-clicked=${this.handleTaskClicked}
246
- @timescale-changed=${(e: CustomEvent) => this.syncTimeScale(e)}
247
- ></ox-gantt-chart>
248
- </div>
249
- <ox-gantt-resize-splitter
250
- horizontal
251
- @splitter-dragstart=${(e: CustomEvent) => this.resizeVerticalStart(e)}
252
- @splitter-drag=${(e: CustomEvent) => this.resizeVerticalDrag(e)}
253
- @splitter-dragend=${(e: CustomEvent) => this.resizeVerticalEnd(e)}
254
- ></ox-gantt-resize-splitter>
255
- <div class="resource-container">
256
- <div class="resource-table-container" @scroll=${this.syncScroll}>
257
- <table>
258
- <thead>
259
- <tr>
260
- <th style="width: 100px">${i18next.t('label.gantt-resource-type')}</th>
261
- <th>${i18next.t('label.gantt-resource-allocated')}</th>
262
- </tr>
263
- </thead>
264
- <tbody>
265
- ${this.resources.map(resource => this.renderResourceRow(resource))}
266
- </tbody>
267
- </table>
268
- </div>
269
- <ox-gantt-resize-splitter
270
- vertical
271
- @splitter-dragstart=${(e: CustomEvent) => this.resizeHorizontalStart(e)}
272
- @splitter-drag=${(e: CustomEvent) => this.resizeHorizontalDrag(e)}
273
- @splitter-dragend=${(e: CustomEvent) => this.resizeHorizontalEnd(e)}
274
- ></ox-gantt-resize-splitter>
275
- <ox-resource-chart
276
- class="resource-chart-container"
277
- .fromDate=${this.fromDate}
278
- .toDate=${this.toDate}
279
- .tasks=${this.tasks}
280
- .resources=${this.resources}
281
- .axisScale=${this.timeScale}
282
- @scroll=${this.syncScroll}
283
- ></ox-resource-chart>
284
- </div>
285
- </div>
286
- `
287
- }
288
-
289
- renderTaskTable() {
290
- const columnConfig = this.columnConfigProvider()
291
- if (!columnConfig) {
292
- return
293
- }
294
-
295
- // Sort columns by the configured order
296
- const sortedColumns = [...columnConfig].sort((a, b) => a.order - b.order)
297
-
298
- return html`
299
- <table class="task-table">
300
- <thead>
301
- <tr>
302
- ${sortedColumns.map(col =>
303
- col.visible ? html`<th style="width: ${col.width}px">${col.label}</th>` : nothing
304
- )}
305
- </tr>
306
- </thead>
307
- <tbody>
308
- ${this.renderTaskRows(this.tasks, 0, columnConfig)}
309
- </tbody>
310
- </table>
311
- `
312
- }
313
-
314
- renderTaskRows(tasks: Task[], level: number = 0, columnConfig: ColumnConfig[]): TemplateResult[] {
315
- return tasks.flatMap(task => {
316
- if (task.collapsed) {
317
- return [this.renderTaskRow(task, level, columnConfig)]
318
- } else {
319
- return [
320
- this.renderTaskRow(task, level, columnConfig),
321
- ...this.renderTaskRows(task.children || [], level + 1, columnConfig)
322
- ]
323
- }
324
- })
325
- }
326
-
327
- renderTaskRow(task: Task, level: number, columnConfig: ColumnConfig[]) {
328
- const isGroupTask = task.children && task.children.length > 0
329
- const isCollapsed = !!task.collapsed
330
- const paddingLeft = level * 16 + 4 // 트리 구조를 위한 패딩
331
-
332
- return html`
333
- <tr class=${this.selectedTaskId === task.id ? 'selected' : ''} @click=${() => this.selectTask(task.id)}>
334
- ${columnConfig.map(col =>
335
- col.visible
336
- ? html`
337
- <td class="task-row" style="padding-left: ${col.name === 'title' ? paddingLeft : 4}px">
338
- ${col.name === 'title'
339
- ? html`
340
- ${isGroupTask
341
- ? html`
342
- <span class="toggle-icon" @click=${(e: Event) => this.toggleCollapse(e, task)} group>
343
- ${isGroupTask
344
- ? html`
345
- <svg
346
- class="toggle-icon"
347
- width="10"
348
- height="10"
349
- viewBox="0 0 10 10"
350
- xmlns="http://www.w3.org/2000/svg"
351
- ?collapsed=${isCollapsed}
352
- >
353
- <polygon
354
- points="0,1 10,1 5,9"
355
- fill="var(--md-sys-color-on-primary-container)"
356
- opacity="0.5"
357
- />
358
- </svg>
359
- `
360
- : nothing}
361
- </span>
362
- `
363
- : nothing}
364
- ${this.getTaskFieldValue(task, col.name)}
365
- `
366
- : this.getTaskFieldValue(task, col.name)}
367
- </td>
368
- `
369
- : nothing
370
- )}
371
- </tr>
372
- `
373
- }
374
-
375
- toggleCollapse(e: Event, task: Task) {
376
- e.stopPropagation() // 클릭 이벤트 전파 방지
377
-
378
- task.collapsed = !task.collapsed
379
- this.requestUpdate()
380
-
381
- this.ganttChart.tasks = [...this.tasks]
382
- }
383
-
384
- renderResourceRow(resource: Resource) {
385
- return html`
386
- <tr>
387
- <td>${resource.type}</td>
388
- <td>${resource.allocated}</td>
389
- </tr>
390
- `
391
- }
392
-
393
- getTaskFieldValue(task: Task, field: string) {
394
- switch (field) {
395
- case 'id':
396
- return task.id
397
- case 'title':
398
- return task.title
399
- case 'type':
400
- return task.type
401
- case 'startDate':
402
- return new Date(task.startDate).toISOString().split('T')[0]
403
- case 'endDate':
404
- return new Date(task.endDate).toISOString().split('T')[0]
405
- case 'duration':
406
- return task.duration || 0
407
- case 'tags':
408
- return task.tags?.join(', ') || ''
409
- case 'progress':
410
- return task.progress || ''
411
- case 'dependsOn':
412
- return task.dependsOn || ''
413
- case 'resources':
414
- return task.resources?.map(res => `${res.type}: ${res.allocated}`).join(', ') || ''
415
- default:
416
- return ''
417
- }
418
- }
419
-
420
- selectTask(taskId: string) {
421
- this.selectedTaskId = taskId
422
- }
423
-
424
- handleTaskClicked(event: CustomEvent) {
425
- this.selectedTaskId = event.detail.taskId
426
- }
427
-
428
- protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
429
- this.taskTableContainer.style.width = '254px'
430
- this.resourceTableContainer.style.width = '254px'
431
- }
432
-
433
- updated(changedProperties: PropertyValues<this>) {
434
- if (changedProperties.has('tasks')) {
435
- this.resources = this.aggregateResources(this.tasks)
436
- }
437
- }
438
-
439
- aggregateResources(tasks: Task[]): Resource[] {
440
- const resourceMap: { [key: string]: number } = {}
441
-
442
- const traverseTasks = (tasks: Task[]) => {
443
- tasks.forEach(task => {
444
- if (task.resources) {
445
- const startDate = new Date(task.startDate)
446
- const endDate = new Date(task.endDate)
447
- const durationDays = d3.timeDay.count(startDate, d3.timeDay.offset(endDate, 1))
448
-
449
- task.resources.forEach(resource => {
450
- const totalAllocation = resource.allocated * durationDays
451
- if (!resourceMap[resource.type]) {
452
- resourceMap[resource.type] = 0
453
- }
454
- resourceMap[resource.type] += totalAllocation
455
- })
456
- }
457
-
458
- if (task.children) {
459
- traverseTasks(task.children)
460
- }
461
- })
462
- }
463
-
464
- traverseTasks(tasks)
465
-
466
- return Object.keys(resourceMap).map(type => ({
467
- type,
468
- allocated: resourceMap[type]
469
- }))
470
- }
471
-
472
- syncScroll(e: Event) {
473
- const target = e.target as HTMLElement
474
-
475
- if (target === this.taskTableContainer) {
476
- const newScrollTop = this.taskTableContainer.scrollTop
477
- this.ganttChart.setVerticalOffset(newScrollTop)
478
- } else if (target === this.ganttChart) {
479
- const newScrollTop = this.ganttChart.getVerticalOffset()
480
- this.taskTableContainer.scrollTop = newScrollTop
481
-
482
- const newScrollLeft = this.ganttChart.getHorizontalOffset()
483
- this.resourceChart.setHorizontalOffset(newScrollLeft)
484
- } else if (target === this.resourceChart) {
485
- const newScrollTop = this.resourceChart.getVerticalOffset()
486
- this.resourceTableContainer.scrollTop = newScrollTop
487
-
488
- const newScrollLeft = this.resourceChart.getHorizontalOffset()
489
- this.ganttChart.setHorizontalOffset(newScrollLeft)
490
- } else if (target === this.resourceTableContainer) {
491
- const newScrollTop = this.resourceTableContainer.scrollTop
492
- this.resourceChart.setVerticalOffset(newScrollTop)
493
- }
494
- }
495
-
496
- syncTimeScale(e: CustomEvent) {
497
- const detail = e.detail
498
- this.resourceChart.syncTimeScale(detail)
499
- }
500
-
501
- resizeHorizontalStart(e: CustomEvent) {
502
- this._startWidth = ((e.target as HTMLElement)?.previousElementSibling as HTMLElement)?.offsetWidth
503
- this._isDragging = true
504
- }
505
-
506
- resizeHorizontalDrag(e: CustomEvent) {
507
- if (!this._isDragging) return
508
- const delta = e.detail
509
- this.taskTableContainer.style.width = `${this._startWidth + delta.x}px`
510
- this.resourceTableContainer.style.width = `${this._startWidth + delta.x}px`
511
- }
512
-
513
- resizeHorizontalEnd(e: CustomEvent) {
514
- this._isDragging = false
515
- }
516
-
517
- resizeVerticalStart(e: CustomEvent) {
518
- this._startHeight = ((e.target as HTMLElement)?.nextElementSibling as HTMLElement)?.offsetHeight
519
- this._isDragging = true
520
- }
521
-
522
- resizeVerticalDrag(e: CustomEvent) {
523
- if (!this._isDragging) return
524
- const delta = e.detail
525
-
526
- this.resourceContainer.style.height = `${this._startHeight - delta.y}px`
527
- }
528
-
529
- resizeVerticalEnd(e: CustomEvent) {
530
- this._isDragging = false
531
- }
532
- }
@@ -1,145 +0,0 @@
1
- import { LitElement, html, css } from 'lit'
2
- import { customElement, property, query } from 'lit/decorators.js'
3
- import { ResourceChartController } from './resource-chart-controller'
4
- import { Task, Resource, TimeScale } from './types'
5
-
6
- import { styles as MDTypeScaleStyles } from '@material/web/typography/md-typescale-styles'
7
- import { ScrollbarStyles } from '@operato/styles'
8
-
9
- @customElement('ox-resource-chart')
10
- export class OxResourceChart extends LitElement {
11
- static styles = [
12
- MDTypeScaleStyles,
13
- ScrollbarStyles,
14
- css`
15
- :host {
16
- display: flex;
17
- overflow: hidden;
18
- position: relative;
19
- }
20
-
21
- div[chart-container] {
22
- flex-direction: column;
23
- overflow-x: auto;
24
- overflow-y: hidden;
25
- }
26
-
27
- svg[header] {
28
- display: block;
29
- min-width: 100%;
30
- box-sizing: border-box;
31
- border-bottom: 1px solid var(--ox-gantt-header-border-color, var(--md-sys-color-outline));
32
- height: 30px;
33
- background-color: var(--ox-gantt-header-background-color, var(--md-sys-color-secondary-container));
34
- color: var(--ox-gantt-header-color, var(--md-sys-color-on-secondary-container));
35
- position: sticky;
36
- top: 0;
37
- z-index: 1;
38
- }
39
-
40
- svg[body] {
41
- display: block;
42
- min-width: 100%;
43
- }
44
-
45
- .grid line {
46
- stroke: #e0e0e0;
47
- }
48
-
49
- .grid path {
50
- stroke-width: 0;
51
- }
52
- `
53
- ]
54
-
55
- @property({ type: String }) fromDate: string = new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0]
56
- @property({ type: String }) toDate: string = new Date(new Date().getFullYear(), 11, 31).toISOString().split('T')[0]
57
- @property({ type: String }) axisScale: TimeScale = 'week-day'
58
- @property({ type: Array }) resources: Resource[] = []
59
- @property({ type: Array }) tasks: Task[] = []
60
-
61
- @query('[chart-container]') container!: HTMLDivElement
62
- @query('svg[header]') svgHeader!: SVGSVGElement
63
- @query('svg[body]') svgBody!: SVGSVGElement
64
-
65
- private resourceChartController: ResourceChartController
66
-
67
- constructor() {
68
- super()
69
- this.resourceChartController = new ResourceChartController(this, this.fromDate, this.toDate, this.axisScale)
70
- }
71
-
72
- updated(changedProperties: Map<string | number | symbol, unknown>) {
73
- if (changedProperties.has('fromDate') || changedProperties.has('toDate')) {
74
- this.resourceChartController.updateRange(this.fromDate, this.toDate)
75
- }
76
-
77
- if (changedProperties.has('resources')) {
78
- this.resourceChartController.setResources(this.resources)
79
- this.updateChartHeight()
80
- }
81
-
82
- if (changedProperties.has('tasks')) {
83
- this.resourceChartController.setTasks(this.flattenTasks(this.tasks))
84
- this.updateChartHeight()
85
- }
86
-
87
- if (changedProperties.has('axisScale')) {
88
- this.resourceChartController.level = this.axisScale
89
- this.resourceChartController.updateTimeScale()
90
- }
91
-
92
- this.resourceChartController.drawAxes(this.svgHeader)
93
- this.resourceChartController.drawBody(this.svgBody)
94
- }
95
-
96
- updateChartHeight() {
97
- const resourceCount = this.resources.length
98
- const rowHeight = 30 // 리소스 하나당 할당된 높이 (픽셀)
99
- const minHeight = 200 // 최소 높이
100
- const chartHeight = Math.max(resourceCount * rowHeight, minHeight)
101
-
102
- this.svgBody.style.height = `${chartHeight}px`
103
- }
104
-
105
- render() {
106
- return html`
107
- <div
108
- chart-container
109
- @wheel=${(e: WheelEvent) => this.resourceChartController.handleWheelForBody(e, this.container)}
110
- @touchstart=${(e: TouchEvent) => this.resourceChartController.handleTouchStartForBody(e, this.container)}
111
- @touchmove=${(e: TouchEvent) => this.resourceChartController.handleTouchMoveForBody(e, this.container)}
112
- >
113
- <svg header></svg>
114
- <svg body></svg>
115
- </div>
116
- `
117
- }
118
-
119
- syncTimeScale(detail: { baseWidth: number; level: TimeScale }) {
120
- this.resourceChartController.syncTimeScale(detail)
121
- this.requestUpdate()
122
- }
123
-
124
- getHorizontalOffset(): number {
125
- return this.container.scrollLeft
126
- }
127
-
128
- setHorizontalOffset(offset: number) {
129
- this.container.scrollLeft = offset
130
- }
131
-
132
- getVerticalOffset(): number {
133
- return this.container.scrollTop
134
- }
135
-
136
- setVerticalOffset(offset: number) {
137
- this.container.scrollTop = offset
138
- }
139
-
140
- flattenTasks(tasks: Task[], level: number = 0): Task[] {
141
- return tasks.flatMap(task =>
142
- task.collapsed ? [{ ...task, level }] : [{ ...task, level }, ...this.flattenTasks(task.children || [], level + 1)]
143
- )
144
- }
145
- }