clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -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);
}

View File

@@ -0,0 +1,6 @@
name: farmOS Timeline
description: Provides farmOS timeline features.
type: module
package: farmOS (Experimental)
lifecycle: experimental
core_version_requirement: ^10

View File

@@ -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

View File

@@ -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' => [],
],
],
];
}

View File

@@ -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));

View File

@@ -0,0 +1,57 @@
<?php
namespace Drupal\farm_timeline\Element;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element\RenderElementBase;
/**
* Provides a farm timeline render element.
*
* @RenderElement("farm_timeline")
*/
class FarmTimeline extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#theme' => 'farm_timeline',
'#pre_render' => [
[get_class($this), 'preRenderTimeline'],
],
'#rows' => [],
];
}
/**
* Pre-render callback for the timeline render array.
*
* @param array $element
* A renderable array for the timeline.
*
* @return array
* The final render array for the timeline.
*/
public static function preRenderTimeline(array $element): array {
// Set a timeline ID.
if (empty($element['#attributes']['id'])) {
$element['#attributes']['id'] = Html::getUniqueId('timeline');
}
// Add timeline rows.
$element['#attributes']['data-timeline-rows'] = Json::encode($element['#rows'] ?? []);
// Add the farm-timeline class.
$element['#attributes']['class'][] = 'farm-timeline';
// Attach the farm_timeline library.
$element['#attached']['library'][] = 'farm_timeline/farm_timeline';
return $element;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Drupal\farm_timeline\Plugin\DataType;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\Plugin\DataType\Map;
/**
* Timeline row data type.
*
* @DataType(
* id = "farm_timeline_row",
* label = @Translation("Timeline row"),
* description = @Translation("Data definition for a timeline row."),
* definition_class = "\Drupal\farm_timeline\TypedData\TimelineRowDefinition"
* )
*/
class TimelineRow extends Map implements ComplexDataInterface {
/**
* {@inheritdoc}
*/
public function setValue($values, $notify = TRUE) {
if (isset($values) && !is_array($values)) {
throw new \InvalidArgumentException("Invalid values given. Values must be represented as an associative array.");
}
// Set default values.
$values += [
'draggable' => FALSE,
'resizable' => FALSE,
'expanded' => FALSE,
];
parent::setValue($values);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Drupal\farm_timeline\Plugin\DataType;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\Plugin\DataType\ItemList;
use Drupal\Core\TypedData\Plugin\DataType\Map;
/**
* Timeline task data type.
*
* @DataType(
* id = "farm_timeline_task",
* label = @Translation("Timeline Task"),
* definition_class = "\Drupal\farm_timeline\TypedData\TimelineTaskDefinition"
* )
*/
class TimelineTask extends Map implements ComplexDataInterface {
/**
* {@inheritdoc}
*/
public function setValue($values, $notify = TRUE) {
if (isset($values) && !is_array($values)) {
throw new \InvalidArgumentException("Invalid values given. Values must be represented as an associative array.");
}
// Inherit resource_id from Row parent ID.
if (!isset($values['resource_id']) && $this->parent instanceof ItemList && $this->parent->parent instanceof TimelineRow) {
$values['resource_id'] = $this->parent->parent->get('id')->getValue();
}
// Set default values.
$values += [
'draggable' => FALSE,
'resizable' => FALSE,
];
parent::setValue($values);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Drupal\farm_timeline\TypedData;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TypedData\ComplexDataDefinitionBase;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\ListDataDefinition;
/**
* Timeline row definition.
*/
class TimelineRowDefinition extends ComplexDataDefinitionBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
if (!isset($this->propertyDefinitions)) {
$this->propertyDefinitions['id'] = DataDefinition::create('string')
->setLabel($this->t('ID'))
->setRequired(TRUE);
$this->propertyDefinitions['label'] = DataDefinition::create('string')
->setLabel($this->t('Label'))
->setRequired(TRUE);
$this->propertyDefinitions['link'] = DataDefinition::create('uri')
->setLabel($this->t('Link'));
$this->propertyDefinitions['draggable'] = DataDefinition::create('boolean')
->setLabel($this->t('Enable dragging'))
->addConstraint('NotNull');
$this->propertyDefinitions['resizable'] = DataDefinition::create('boolean')
->setLabel($this->t('Enable resizing'))
->addConstraint('NotNull');
$this->propertyDefinitions['classes'] = ListDataDefinition::create('string')
->setLabel($this->t('Classes'));
$this->propertyDefinitions['tasks'] = ListDataDefinition::create('farm_timeline_task')
->setLabel($this->t('Tasks'));
$this->propertyDefinitions['expanded'] = DataDefinition::create('boolean')
->setLabel($this->t('Expanded'));
$this->propertyDefinitions['children'] = ListDataDefinition::create('farm_timeline_row')
->setLabel($this->t('Children'));
}
return $this->propertyDefinitions;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Drupal\farm_timeline\TypedData;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TypedData\ComplexDataDefinitionBase;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\ListDataDefinition;
/**
* Timeline task definition.
*/
class TimelineTaskDefinition extends ComplexDataDefinitionBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function getPropertyDefinitions() {
if (!isset($this->propertyDefinitions)) {
$this->propertyDefinitions['id'] = DataDefinition::create('string')
->setLabel($this->t('ID'))
->setRequired(TRUE);
$this->propertyDefinitions['resource_id'] = DataDefinition::create('string')
->setLabel($this->t('Resource ID'))
->setRequired(TRUE);
$this->propertyDefinitions['label'] = DataDefinition::create('string')
->setLabel($this->t('Label'));
$this->propertyDefinitions['edit_url'] = DataDefinition::create('uri')
->setLabel($this->t('Edit URL'));
$this->propertyDefinitions['start'] = DataDefinition::create('timestamp')
->setLabel($this->t('Start Time'))
->setRequired(TRUE);
$this->propertyDefinitions['end'] = DataDefinition::create('timestamp')
->setLabel($this->t('End Time'))
->setRequired(TRUE);
$this->propertyDefinitions['draggable'] = DataDefinition::create('boolean')
->setLabel($this->t('Enable dragging'))
->addConstraint('NotNull');
$this->propertyDefinitions['resizable'] = DataDefinition::create('boolean')
->setLabel($this->t('Enable resizing'))
->addConstraint('NotNull');
$this->propertyDefinitions['meta'] = DataDefinition::create('any')
->setLabel($this->t('Meta'));
$this->propertyDefinitions['classes'] = ListDataDefinition::create('string')
->setLabel($this->t('Classes'));
}
return $this->propertyDefinitions;
}
}

View File

@@ -0,0 +1,12 @@
{#
/**
* @file
* Default theme implementation for the farm_timeline render element.
*
* Available variables:
* - attributes: Attributes for the farm_timeline wrapper div.
*
* @ingroup themeable
*/
#}
<div{{ attributes }}></div>