Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/commands/AddSortOrderCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* Command to add sort_order field to existing CustAttrSchemaModel tables
*/
class AddSortOrderCommand implements Asatru\Commands\Command {
/**
* Command handler method
*
* @param $args
* @return void
*/
public function handle($args)
{
try {
echo "Adding sort_order field to CustAttrSchemaModel table...\n";

// Check if sort_order column already exists
$columnExists = CustAttrSchemaModel::raw("SHOW COLUMNS FROM `@THIS` LIKE 'sort_order'")->first();

if ($columnExists) {
echo "sort_order column already exists. Skipping...\n";
return;
}

// Add the sort_order column
CustAttrSchemaModel::raw("ALTER TABLE `@THIS` ADD COLUMN `sort_order` INT NOT NULL DEFAULT 0 AFTER `active`");
echo "Added sort_order column.\n";

// Update existing records with sequential sort_order values
$records = CustAttrSchemaModel::raw("SELECT id FROM `@THIS` ORDER BY id ASC");

$index = 0;
foreach ($records as $record) {
CustAttrSchemaModel::raw("UPDATE `@THIS` SET sort_order = ? WHERE id = ?", [$index, $record->get('id')]);
$index++;
}

echo "Updated " . $index . " existing records with sort_order values.\n";
echo "Done!\n";

} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
}
}
3 changes: 2 additions & 1 deletion app/config/commands.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
['migrate:specific', 'Perform version specific migration upgrade', 'MigrationSpecific'],
['calendar:classes', 'Add default calendar classes', 'CalendarClsCommand'],
['plants:attributes', 'Add default plant attributes', 'AttributesCommand'],
['cache:clear', 'Clear the entire cache', 'CacheClearCommand']
['cache:clear', 'Clear the entire cache', 'CacheClearCommand'],
['attributes:sort-order', 'Add sort_order field to existing attribute schemas', 'AddSortOrderCommand']
];
2 changes: 2 additions & 0 deletions app/config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
array('/plants/attributes/add', 'POST', 'plants@add_custom_attribute'),
array('/plants/attributes/edit', 'POST', 'plants@edit_custom_attribute'),
array('/plants/attributes/remove', 'ANY', 'plants@remove_custom_attribute'),
array('/plants/attributes/reorder', 'POST', 'plants@reorder_attributes'),
array('/plants/remove', 'ANY', 'plants@remove_plant'),
array('/plants/history', 'GET', 'plants@view_history'),
array('/plants/history/add', 'ANY', 'plants@add_to_history'),
Expand Down Expand Up @@ -177,6 +178,7 @@
array('/api/plants/attributes/add', 'ANY', 'api@add_attribute'),
array('/api/plants/attributes/edit', 'ANY', 'api@edit_attribute'),
array('/api/plants/attributes/remove', 'ANY', 'api@remove_attribute'),
array('/api/attributes/reorder', 'ANY', 'api@reorder_attributes'),
array('/api/plants/photo/update', 'ANY', 'api@update_plant_photo'),
array('/api/plants/gallery/add', 'ANY', 'api@add_plant_gallery_photo'),
array('/api/plants/gallery/edit', 'ANY', 'api@edit_plant_gallery_photo'),
Expand Down
35 changes: 35 additions & 0 deletions app/controller/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -1107,4 +1107,39 @@ public function import_backup($request)
]);
}
}

/**
* Handles URL: /api/attributes/reorder
*
* @param Asatru\Controller\ControllerArg $request
* @return Asatru\View\JsonHandler
*/
public function reorder_attributes($request)
{
try {
$attributeIds = $request->params()->query('attribute_ids', null);

if (!$attributeIds) {
throw new \Exception('Attribute IDs are required');
}

// Parse the JSON string
$ids = json_decode($attributeIds, true);
if (!is_array($ids)) {
throw new \Exception('Invalid attribute IDs format');
}

CustAttrSchemaModel::reorderAttributes($ids);

return json([
'code' => 200,
'msg' => 'Attributes reordered successfully'
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => $e->getMessage()
]);
}
}
}
35 changes: 35 additions & 0 deletions app/controller/plants.php
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,41 @@ public function remove_custom_attribute($request)
}
}

/**
* Handles URL: /plants/attributes/reorder
*
* @param Asatru\Controller\ControllerArg $request
* @return Asatru\View\JsonHandler
*/
public function reorder_attributes($request)
{
try {
$attributeIds = $request->params()->query('attribute_ids', null);

if (!$attributeIds) {
throw new \Exception('Attribute IDs are required');
}

// Parse the JSON string
$ids = json_decode($attributeIds, true);
if (!is_array($ids)) {
throw new \Exception('Invalid attribute IDs format');
}

CustAttrSchemaModel::reorderAttributes($ids);

return json([
'code' => 200,
'msg' => 'Attributes reordered successfully'
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => $e->getMessage()
]);
}
}

/**
* Handles URL: /plants/remove
*
Expand Down
1 change: 1 addition & 0 deletions app/migrations/CustAttrSchemaModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function up()
$this->database->add('label VARCHAR(512) NOT NULL');
$this->database->add('datatype VARCHAR(512) NOT NULL');
$this->database->add('active BOOLEAN NOT NULL DEFAULT 1');
$this->database->add('sort_order INT NOT NULL DEFAULT 0');
$this->database->add('created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
$this->database->create();
}
Expand Down
43 changes: 39 additions & 4 deletions app/models/CustAttrSchemaModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ public static function getAll($filter_active = true)
{
try {
if ($filter_active) {
return static::raw('SELECT * FROM `@THIS` WHERE active = 1');
return static::raw('SELECT * FROM `@THIS` WHERE active = 1 ORDER BY sort_order ASC, id ASC');
} else {
return static::raw('SELECT * FROM `@THIS`');
return static::raw('SELECT * FROM `@THIS` ORDER BY sort_order ASC, id ASC');
}
} catch (\Exception $e) {
throw $e;
Expand All @@ -36,8 +36,12 @@ public static function addSchema($label, $datatype)
throw new \Exception(__('app.schema_attribute_already_exists'));
}

static::raw('INSERT INTO `@THIS` (label, datatype, active) VALUES(?, ?, 1)', [
$label, $datatype
// Get the next sort order
$maxOrder = static::raw('SELECT MAX(sort_order) as max_order FROM `@THIS`')->first();
$nextOrder = ($maxOrder && $maxOrder->get('max_order') !== null) ? $maxOrder->get('max_order') + 1 : 0;

static::raw('INSERT INTO `@THIS` (label, datatype, active, sort_order) VALUES(?, ?, 1, ?)', [
$label, $datatype, $nextOrder
]);
} catch (\Exception $e) {
throw $e;
Expand Down Expand Up @@ -100,4 +104,35 @@ public static function removeSchema($id)
throw $e;
}
}

/**
* @param $id
* @param $newOrder
* @return void
* @throws \Exception
*/
public static function updateSortOrder($id, $newOrder)
{
try {
static::raw('UPDATE `@THIS` SET sort_order = ? WHERE id = ?', [$newOrder, $id]);
} catch (\Exception $e) {
throw $e;
}
}

/**
* @param $attributeIds
* @return void
* @throws \Exception
*/
public static function reorderAttributes($attributeIds)
{
try {
foreach ($attributeIds as $index => $id) {
static::updateSortOrder($id, $index);
}
} catch (\Exception $e) {
throw $e;
}
}
}
81 changes: 81 additions & 0 deletions app/resources/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,82 @@ window.createVueInstance = function(element) {
});
},

reorderCustomAttributes: function(attributeIds)
{
window.vue.ajaxRequest('post', window.location.origin + '/plants/attributes/reorder', {
attribute_ids: JSON.stringify(attributeIds)
}, function(response){
if (response.code == 200) {
// Success - the order has been updated
console.log('Attributes reordered successfully');
} else {
alert(response.msg);
}
});
},

initDragAndDrop: function()
{
const container = document.querySelector('[data-sortable="admin-attributes"]');
if (!container) return;

// Make attribute schemas sortable
let draggedElement = null;

container.addEventListener('dragstart', function(e) {
draggedElement = e.target.closest('.admin-attribute-schema');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', draggedElement.outerHTML);
draggedElement.style.opacity = '0.5';
});

container.addEventListener('dragend', function(e) {
if (draggedElement) {
draggedElement.style.opacity = '';
draggedElement = null;
}
});

container.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';

const afterElement = window.vue.getDragAfterElement(container, e.clientY);
if (afterElement == null) {
container.appendChild(draggedElement);
} else {
container.insertBefore(draggedElement, afterElement);
}
});

container.addEventListener('drop', function(e) {
e.preventDefault();

// Get the new order of attribute IDs
const elements = Array.from(container.querySelectorAll('.admin-attribute-schema[data-attribute-id]'));
const attributeIds = elements.map(element => parseInt(element.dataset.attributeId));

// Send the new order to the server
window.vue.reorderCustomAttributes(attributeIds);
});
},

getDragAfterElement: function(container, y)
{
const draggableElements = [...container.querySelectorAll('.admin-attribute-schema[data-attribute-id]:not(.dragging)')];

return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;

if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
},

saveAllAttributes: function(source) {
const forms = document.querySelector(source).getElementsByTagName('form');
window.vue.bulkSubmitForm(0, forms, 100);
Expand Down Expand Up @@ -2003,4 +2079,9 @@ window.createVueInstance = function(element) {

document.addEventListener('DOMContentLoaded', function() {
window.vue = window.createVueInstance('#app');

// Initialize drag and drop for custom attributes
setTimeout(function() {
window.vue.initDragAndDrop();
}, 100);
});
32 changes: 32 additions & 0 deletions app/resources/sass/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3670,4 +3670,36 @@ fieldset .field {

.button-margin-left {
margin-left: 5px;
}

/* Drag and Drop Styles for Admin Attribute Schemas */
.admin-attribute-schema[data-attribute-id] {
transition: all 0.2s ease;
border: 1px solid transparent;
border-radius: 4px;
padding: 8px;
margin-bottom: 8px;
}

.admin-attribute-schema[data-attribute-id]:hover {
background-color: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
}

.admin-attribute-schema[data-attribute-id].dragging {
opacity: 0.5;
transform: rotate(2deg);
}

.admin-attribute-schema[data-attribute-id] .fa-grip-vertical {
opacity: 0.6;
transition: opacity 0.2s ease;
}

.admin-attribute-schema[data-attribute-id]:hover .fa-grip-vertical {
opacity: 1;
}

.admin-attribute-schema[data-attribute-id]:active .fa-grip-vertical {
cursor: grabbing;
}
8 changes: 5 additions & 3 deletions app/views/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,15 +491,17 @@

<p>{{ __('app.attributes_schema_hint') }}</p>

<div class="admin-attribute-schema-list">
<div class="admin-attribute-schema-list" data-sortable="admin-attributes">
@foreach ($global_attributes as $global_attribute)
<div class="admin-attribute-schema">
<div class="admin-attribute-schema" data-attribute-id="{{ $global_attribute->get('id') }}" draggable="true" style="cursor: move;">
<form method="POST" action="{{ url('/admin/attribute/schema/edit') }}">
@csrf

<input type="hidden" name="id" value="{{ $global_attribute->get('id') }}"/>

<div class="admin-attribute-schema-item">#{{ $global_attribute->get('id') }}</div>
<div class="admin-attribute-schema-item">
<i class="fas fa-grip-vertical" style="color: #999; margin-right: 8px; cursor: grab;"></i>
</div>

<div class="admin-attribute-schema-item admin-attribute-schema-item-input">
<input type="text" class="input" name="label" value="{{ $global_attribute->get('label') }}"/>
Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading