Skip to content
Draft
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
5 changes: 5 additions & 0 deletions src/web/assets/inventory/InventoryAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public function registerAssetFiles($view): void
'Available',
'On Hand',
'Incoming',
'View',
'View settings',
'Table Columns',
'Purchasable',
'SKU',
]);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/web/assets/inventory/dist/css/inventory.css

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

2 changes: 1 addition & 1 deletion src/web/assets/inventory/dist/css/inventory.css.map

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

2 changes: 1 addition & 1 deletion src/web/assets/inventory/dist/inventory.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/web/assets/inventory/dist/inventory.js.map

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/web/assets/inventory/src/css/inventory.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Inventory levels toolbar (contains the View button)
.inventory-levels-toolbar {
margin-bottom: var(--m);
}

.inventory-headers {
.ltr & {
justify-content: flex-end;
Expand Down
252 changes: 250 additions & 2 deletions src/web/assets/inventory/src/js/InventoryLevelsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ Craft.Commerce.InventoryLevelsManager = Garnish.Base.extend({
containerId: null,
$container: null,
adminTableId: null,
viewPreferences: null,

// Required columns that cannot be hidden
requiredColumns: ['purchasable', 'sku'],

// Optional columns that can be toggled
optionalColumns: [
'reserved',
'damaged',
'safety',
'qualityControl',
'committed',
'available',
'onHand',
'incoming',
],

init: function (container, settings) {
this.containerId = container;
Expand All @@ -25,6 +41,12 @@ Craft.Commerce.InventoryLevelsManager = Garnish.Base.extend({
}
this.$container.data('inventoryLevelsManager', this);

// Load view preferences from localStorage
this.loadViewPreferences();

// Create the view menu first (outside the admin table)
this.initViewMenu();

// random id for the admin table
this.adminTableId =
'inventory-admin-table-' + Math.random().toString(36).substring(7);
Expand All @@ -35,8 +57,49 @@ Craft.Commerce.InventoryLevelsManager = Garnish.Base.extend({
this.initAdminTable();
},

initAdminTable: function () {
this.columns = [
getStorageKey: function () {
return (
'Craft-' +
Craft.siteUid +
'.Commerce.InventoryLevels.viewPreferences.' +
Craft.userId
);
},

loadViewPreferences: function () {
var storageKey = this.getStorageKey();
var stored = localStorage.getItem(storageKey);

if (stored) {
try {
this.viewPreferences = JSON.parse(stored);
} catch (e) {
this.viewPreferences = this.getDefaultViewPreferences();
}
} else {
this.viewPreferences = this.getDefaultViewPreferences();
}
},

getDefaultViewPreferences: function () {
// Default: all columns visible
var visibleColumns = {};
this.optionalColumns.forEach(function (col) {
visibleColumns[col] = true;
});

return {
visibleColumns: visibleColumns,
};
},

saveViewPreferences: function () {
var storageKey = this.getStorageKey();
localStorage.setItem(storageKey, JSON.stringify(this.viewPreferences));
},

getAllColumns: function () {
return [
{
name: 'purchasable',
sortField: 'item',
Expand Down Expand Up @@ -100,6 +163,24 @@ Craft.Commerce.InventoryLevelsManager = Garnish.Base.extend({
title: Craft.t('commerce', 'Incoming'),
},
];
},

getVisibleColumns: function () {
var self = this;
var allColumns = this.getAllColumns();

return allColumns.filter(function (col) {
// Required columns are always visible
if (self.requiredColumns.indexOf(col.name) !== -1) {
return true;
}
// Check view preferences for optional columns
return self.viewPreferences.visibleColumns[col.name] !== false;
});
},

initAdminTable: function () {
this.columns = this.getVisibleColumns();

this.adminTable = new Craft.VueAdminTable({
columns: this.columns,
Expand All @@ -125,6 +206,173 @@ Craft.Commerce.InventoryLevelsManager = Garnish.Base.extend({
});
},

initViewMenu: function () {
var self = this;
var allColumns = this.getAllColumns();

// Generate unique ID for the menu
this.viewMenuId =
'inventory-view-menu-' + Math.random().toString(36).substring(7);

// Create a toolbar container for the view button (positioned at top right)
this.$viewToolbar = $('<div/>', {
class: 'flex inventory-levels-toolbar',
}).appendTo(this.$container);

// Spacer to push button to the right
$('<div class="flex-grow"/>').appendTo(this.$viewToolbar);

// Create the view button (matching Craft's element index view button)
this.$viewBtn = $('<button/>', {
type: 'button',
class: 'btn menubtn',
text: Craft.t('commerce', 'View'),
'aria-label': Craft.t('commerce', 'View settings'),
'aria-controls': this.viewMenuId,
'data-icon': 'sliders',
}).appendTo(this.$viewToolbar);

// Create the menu container (matching Craft's element-index-view-menu)
this.$viewMenu = $('<div/>', {
id: this.viewMenuId,
class: 'menu menu--disclosure element-index-view-menu',
'data-align': 'right',
}).appendTo(Garnish.$bod);

// Build the menu content
this._buildViewMenu(allColumns);

// Initialize the disclosure menu (like Craft's element index)
this.viewMenu = new Garnish.DisclosureMenu(this.$viewBtn);

this.viewMenu.on('show', function () {
self.$viewBtn.addClass('active');
});

this.viewMenu.on('hide', function () {
self.$viewBtn.removeClass('active');
});

// Prevent menu from closing when clicking inside
this.$viewMenu.on('mousedown', function (ev) {
ev.stopPropagation();
});

// Bind events
this.bindViewMenuEvents();
},

_buildViewMenu: function (allColumns) {
var self = this;

// Meta container (like Craft's view menu)
var $metaContainer = $('<div class="meta"/>').appendTo(this.$viewMenu);

// Table columns field
this.$tableColumnsField =
this._createTableColumnsField(allColumns).appendTo($metaContainer);

// Footer with close button
var $footerContainer = $('<div/>', {
class: 'flex menu-footer',
}).appendTo(this.$viewMenu);

$('<div class="flex-grow"/>').appendTo($footerContainer);

this.$closeBtn = $('<button/>', {
type: 'button',
class: 'btn',
text: Craft.t('app', 'Close'),
})
.appendTo($footerContainer)
.on('click', function () {
self.viewMenu.hide();
});
},

_createTableColumnsField: function (allColumns) {
var self = this;

// Build checkbox options (only for optional columns)
var options = allColumns
.filter(function (col) {
return self.optionalColumns.indexOf(col.name) !== -1;
})
.map(function (col) {
return {
label: col.title,
value: col.name,
};
});

// Get currently visible columns
var visibleValues = options
.filter(function (opt) {
return self.viewPreferences.visibleColumns[opt.value] !== false;
})
.map(function (opt) {
return opt.value;
});

// Create checkbox select using Craft's UI helper
this.$tableColumnsContainer = Craft.ui.createCheckboxSelect({
options: options,
values: visibleValues,
});

// Wrap in a field
var $field = Craft.ui.createField(this.$tableColumnsContainer, {
label: Craft.t('commerce', 'Table Columns'),
fieldset: true,
});
$field.addClass('table-columns-field');

return $field;
},

bindViewMenuEvents: function () {
var self = this;

// Column visibility change
this.$tableColumnsContainer
.find('input[type="checkbox"]')
.on('change', function () {
var $checkbox = $(this);
var columnName = $checkbox.val();
var isChecked = $checkbox.is(':checked');

self.viewPreferences.visibleColumns[columnName] = isChecked;
self.saveViewPreferences();
self.rebuildTable();
});
},

rebuildTable: function () {
// Store current search value
var searchValue =
this.$container.find('.vue-admin-table input[type="search"]').val() || '';

// Remove the old table
this.$adminTable.empty();

// Reinitialize with new columns
this.initAdminTable();

// Reapply search if there was one
if (searchValue) {
var checkSearch = setInterval(() => {
var $searchInput = this.$container.find(
'.vue-admin-table input[type="search"]'
);
if ($searchInput.length) {
clearInterval(checkSearch);
$searchInput.val(searchValue);
$searchInput.trigger('input');
}
}, 100);
}
},

defaultSettings: {
inventoryLocationId: null,
inventoryItemId: null,
Expand Down
Loading