-
-
Notifications
You must be signed in to change notification settings - Fork 287
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
626 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* Set the width to 100% instead of using flex. This prevents auto-expanding width of timeline. */ | ||
.layout__region--top { | ||
width: 100%; | ||
} | ||
|
||
.sg-table-header-cell, | ||
.column-header-cell { | ||
font: var(--gin-font) !important; | ||
} | ||
.sg-table-header-cell { | ||
font-size: var(--gin-font-size-m) !important; | ||
font-weight: var(--gin-font-weight-bold) !important; | ||
} | ||
.column-header-cell { | ||
font-size: var(--gin-font-size-s) !important; | ||
font-weight: var(--gin-font-weight-normal) !important; | ||
} | ||
|
||
.sg-task { | ||
--task-bg-color: #74bfff; | ||
box-shadow: var(--gin-shadow-l1); | ||
background-color: var(--task-bg-color) !important; | ||
border-left: 2px solid #666666; | ||
} | ||
.sg-task:hover { | ||
background-color: var(--task-bg-color) !important; | ||
} | ||
.sg-task-selected { | ||
box-shadow: inset 0 0 0 1px var(--gin-color-focus-border), inset 0 0 0 4px var(--gin-color-focus), var(--gin-shadow-l2); | ||
} | ||
|
||
.sg-task .sg-task-content { | ||
color: var(--gin-color-text-light); | ||
} | ||
|
||
.sg-task.last-location, | ||
.sg-task.last-location:hover { | ||
background-color: unset !important; | ||
background: linear-gradient(90deg, var(--task-bg-color), rgba(255,255,255,0)); | ||
} | ||
|
||
.sg-task.log { | ||
margin-top: 4px; | ||
margin-left: -5px; | ||
width: 10px !important; | ||
height: 10px !important; | ||
background-color: var(--task-bg-color); | ||
border: 2px solid var(--task-bg-color); | ||
border-radius: 50% !important; | ||
} | ||
.sg-task.log.sg-task-selected { | ||
--task-bg-color: var(--gin-color-focus); | ||
background-color: var(--task-bg-color); | ||
border: none; | ||
} | ||
|
||
.sg-task.log--status-pending { | ||
background-color: transparent !important; | ||
} | ||
|
||
.sg-task.log--activity { | ||
--task-bg-color: #f1c40f; | ||
} | ||
.sg-task.log--harvest { | ||
--task-bg-color: #e67e22; | ||
} | ||
.sg-task.log--input { | ||
--task-bg-color: #9b59b6; | ||
} | ||
.sg-task.log--observation { | ||
--task-bg-color: #2980b9; | ||
} | ||
.sg-task.log--seeding { | ||
--task-bg-color: #2ecc71; | ||
} | ||
.sg-task.log--transplanting { | ||
--task-bg-color: #2ecc71; | ||
} | ||
|
||
.sg-popup { | ||
z-index: 1000; | ||
background-color: var(--gin-bg-layer); | ||
border: 1px solid var(--gin-border-color-layer); | ||
box-shadow: var(--gin-shadow-l2); | ||
border-radius: var(--gin-border-s); | ||
padding: var(--gin-spacing-xs); | ||
font-size: var(--gin-font-size-s); | ||
} | ||
|
||
.sg-tree-expander { | ||
/*@TODO do this properly.*/ | ||
mask-image: url('/themes/gin/dist/media/sprite.svg#handle-view'); | ||
-webkit-mask-image: url('/themes/gin/dist/media/sprite.svg#handle-view'); | ||
mask-repeat: no-repeat; | ||
-webkit-mask-repeat: no-repeat; | ||
mask-position: center center; | ||
-webkit-mask-position: center center; | ||
margin-right: 0.5em; | ||
background-color: var(--gin-color-text); | ||
transform: rotate(90deg); | ||
transition: transform var(--details-transform-transition-duration) ease-in 0s; | ||
} | ||
.sg-row-expanded .sg-tree-expander { | ||
transform: rotate(270deg); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
name: farmOS Timeline | ||
description: Provides farmOS timeline features. | ||
type: module | ||
package: farmOS (Experimental) | ||
lifecycle: experimental | ||
core_version_requirement: ^10 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
farmOS-timeline: | ||
remote: https://github.com/farmOS/farmOS-timeline | ||
license: | ||
name: MIT | ||
url: https://github.com/farmOS/farmOS-timeline/blob/main/LICENSE | ||
gpl-compatible: true | ||
js: | ||
/libraries/farmOS-timeline/farmOS-timeline.js: | ||
minified: true | ||
farm_timeline: | ||
js: | ||
js/farm_timeline.js: { } | ||
css: | ||
theme: | ||
css/farm_timeline.css: { } | ||
dependencies: | ||
- core/drupal.dialog | ||
- core/drupal.dialog.ajax | ||
- core/once | ||
- farm_timeline/farmOS-timeline |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<?php | ||
|
||
/** | ||
* @file | ||
* The farm_timeline module. | ||
*/ | ||
|
||
/** | ||
* Implements hook_theme(). | ||
*/ | ||
function farm_timeline_theme($existing, $type, $theme, $path) { | ||
return [ | ||
'farm_timeline' => [ | ||
'variables' => [ | ||
'attributes' => [], | ||
], | ||
], | ||
]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
(function (Drupal, drupalSettings, once, farmOS) { | ||
Drupal.behaviors.farm_timeline = { | ||
attach: function (context, settings) { | ||
once('timelineGantt', '.farm-timeline', context).forEach(async function (element) { | ||
const opts = { | ||
props: { | ||
taskElementHook: (node, task) => { | ||
let popup; | ||
|
||
function onHover() { | ||
popup = createPopup(task, node); | ||
} | ||
|
||
function onLeave() { | ||
if (popup) { | ||
popup.remove(); | ||
} | ||
} | ||
|
||
node.addEventListener('mouseenter', onHover); | ||
node.addEventListener('mouseleave', onLeave); | ||
return { | ||
destroy() { | ||
node.removeEventListener('mouseenter', onHover); | ||
node.removeEventListener('mouseleave', onLeave); | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
|
||
// Create the timeline instance. | ||
const timeline = farmOS.timeline.create(element, opts); | ||
|
||
// Helper function to process a single row and its children recursively. | ||
const processRow = async function(row) { | ||
|
||
// Handle URL string to fetch row data dynamically. | ||
if (typeof row === "string") { | ||
|
||
// Fetch and process the array of returned rows. | ||
const data = await fetch(row) | ||
.then(res => res.json()) | ||
.then(data => data?.rows ?? []); | ||
const awaitedRows = await Promise.all(data.map(processRow)); | ||
|
||
// Aggregate all rows and tasks from processed rows. | ||
return awaitedRows.reduce((accumulator, current) => { | ||
return { | ||
rows: [...accumulator.rows, ...current.rows], | ||
tasks: [...accumulator.tasks, ...current.tasks], | ||
}; | ||
}, { | ||
rows: [], | ||
tasks: [] | ||
}); | ||
} else if (!row) { | ||
// Handle potential null/undefined rows | ||
return { rows: [], tasks: [] }; | ||
} | ||
|
||
// Begin processing a single parent row. | ||
// First process all child rows. | ||
const awaitedChildren = await Promise.all((row.children ?? []).map(processRow)); | ||
|
||
// Aggregate child rows and tasks. | ||
const aggregatedChildren = awaitedChildren.reduce((accumulator, current) => { | ||
return { | ||
rows: [...accumulator.rows, ...current.rows], | ||
tasks: [...accumulator.tasks, ...current.tasks], | ||
}; | ||
}, { | ||
rows: [], | ||
tasks: [] | ||
}); | ||
|
||
// Map the parent row to a row object and include children rows. | ||
row.children = aggregatedChildren.rows; | ||
const mappedRow = Drupal.behaviors.farm_timeline.mapRow(row); | ||
|
||
// Collect all tasks from the parent row and add all child tasks. | ||
const tasks = [ | ||
...(row.tasks?.map(Drupal.behaviors.farm_timeline.mapTask) ?? []), | ||
...aggregatedChildren.tasks, | ||
]; | ||
|
||
// Return the final processed parent row and all tasks. | ||
return { | ||
rows: [mappedRow], | ||
tasks: tasks, | ||
}; | ||
}; | ||
|
||
// Helper function to add row to timeline after processing. | ||
const addRow = async function(row) { | ||
return processRow(row) | ||
.then(data => { | ||
timeline.addRows(data.rows); | ||
timeline.addTasks(data.tasks); | ||
}) | ||
} | ||
|
||
// Add all provided timeline rows to the timeline. | ||
const timelineRows = JSON.parse(element.dataset?.timelineRows) ?? []; | ||
await Promise.all(timelineRows.map(addRow)); | ||
|
||
function createPopup(task, node) { | ||
const rect = node.getBoundingClientRect(); | ||
const div = document.createElement('div'); | ||
div.className = 'sg-popup'; | ||
div.innerHTML = ` | ||
<div class="sg-popup-title">${task.label}</div> | ||
<div class="sg-popup-item">From: ${new Date(task.from).toLocaleDateString()}</div> | ||
<div class="sg-popup-item">To: ${new Date(task.to).toLocaleDateString()}</div> | ||
`; | ||
div.style.position = 'absolute'; | ||
div.style.top = `${rect.bottom + window.scrollY + 5}px`; | ||
div.style.left = `${rect.left + rect.width / 2}px`; | ||
|
||
if (task?.meta?.entity_type === 'log') { | ||
div.innerHTML = ` | ||
<div class="sg-popup-title">Log: ${task.meta.label}</div> | ||
<div class="sg-popup-item">Type: ${task.meta.entity_bundle}</div> | ||
<div class="sg-popup-item">Timestamp: ${new Date(task.from).toLocaleDateString()}</div> | ||
`; | ||
} | ||
|
||
if (task?.meta?.stage) { | ||
div.innerHTML = ` | ||
<div class="sg-popup-title">Stage: ${task.meta.stage}</div> | ||
<div class="sg-popup-item">From: ${new Date(task.from).toLocaleDateString()}</div> | ||
<div class="sg-popup-item">To: ${new Date(task.to).toLocaleDateString()}</div> | ||
`; | ||
} | ||
|
||
document.body.appendChild(div); | ||
return div; | ||
} | ||
|
||
// Open entity page on click. | ||
timeline.timeline.api.tasks.on.select((task) => { | ||
task = task[0]; | ||
if (task.model?.editUrl) { | ||
var ajaxSettings = { | ||
url: task.model.editUrl, | ||
dialogType: 'dialog', | ||
dialogRenderer: 'off_canvas', | ||
}; | ||
var myAjaxObject = Drupal.ajax(ajaxSettings); | ||
myAjaxObject.execute(); | ||
} else { | ||
let dialog = document.getElementById('drupal-off-canvas'); | ||
if (dialog) { | ||
Drupal.dialog(dialog, {}).close(); | ||
} | ||
} | ||
}); | ||
}); | ||
}, | ||
// Helper function to map row properties. | ||
mapRow: function(row) { | ||
return { | ||
id: row.id, | ||
label: row.label, | ||
headerHtml: row.link, | ||
expanded: row.expanded ?? false, | ||
draggable: row.draggable ?? false, | ||
resizable: row.resizable ?? false, | ||
// Only provide a children array if there are children | ||
// otherwise an expanded icon will appear for rows without children. | ||
children: row.children.length ? row.children : null, | ||
}; | ||
}, | ||
// Helper function to map task properties. | ||
mapTask: function(task) { | ||
return { | ||
id: task.id, | ||
resourceId: task.resource_id, | ||
from: task.start, | ||
to: task.end, | ||
label: task.label ?? '', | ||
editUrl: task.edit_url, | ||
draggable: task.draggable ?? false, | ||
resizable: task.resizable ?? false, | ||
meta: task?.meta, | ||
classes: task.classes, | ||
}; | ||
}, | ||
}; | ||
}(Drupal, drupalSettings, once, farmOS)); |
Oops, something went wrong.