// Template Editor JavaScript
// Field Deletion Component function for Alpine.js (define before DOM ready)
window.fieldDeletionComponent = function() {
return {
showConfirmation: false,
deleting: false,
deleteField() {
this.deleting = true;
// Get field information from the current context
const fieldConfigContainer = document.getElementById('field-config-container');
const fieldForm = fieldConfigContainer.querySelector('form[action*="/attribute_fields/"]');
if (!fieldForm) {
console.error('Field form not found');
showNotification('Unable to delete field - form not found', 'error');
this.deleting = false;
return;
}
// Extract field ID from form action URL
const fieldIdMatch = fieldForm.action.match(/\/attribute_fields\/(\d+)/);
if (!fieldIdMatch) {
console.error('Field ID not found in form action');
showNotification('Unable to delete field - ID not found', 'error');
this.deleting = false;
return;
}
const fieldId = fieldIdMatch[1];
// Perform deletion via AJAX
fetch(`/plan/attribute_fields/${fieldId}`, {
method: 'DELETE',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Field deleted successfully:', data);
// Remove the field from the UI
const fieldItem = document.querySelector(`[data-field-id="${fieldId}"]`);
if (fieldItem) {
// Update field count in category header before removing
const categoryId = fieldItem.closest('.fields-container').dataset.categoryId;
fieldItem.remove();
updateCategoryFieldCount(categoryId);
}
// Update General Settings counts
updateGeneralSettingsCounts();
// Close the configuration panel
const alpineElement = document.querySelector('.attributes-editor');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine) {
alpine.selectedField = null;
alpine.configuring = false;
}
}
// Show success notification
if (data.message) {
showNotification(data.message, 'success');
} else {
showNotification('Field deleted successfully', 'success');
}
this.deleting = false;
})
.catch(error => {
console.error('Failed to delete field:', error);
showNotification('Failed to delete field', 'error');
this.deleting = false;
});
}
};
};
// Template Reset Component function for Alpine.js (define before DOM ready)
window.templateResetComponent = function() {
return {
resetOpen: false,
resetConfirm: false,
resetAnalysis: null,
loading: false,
confirmText: '',
toggleReset() {
this.resetOpen = !this.resetOpen;
if (this.resetOpen && !this.resetAnalysis) {
this.fetchAnalysis();
}
},
fetchAnalysis() {
console.log('Fetching reset analysis...');
this.loading = true;
const contentType = document.querySelector('.attributes-editor').dataset.contentType;
fetch(`/plan/${contentType}/template/reset`, {
method: 'DELETE',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
console.log('Analysis data:', data);
this.resetAnalysis = data;
this.loading = false;
})
.catch(error => {
console.error('Error:', error);
this.loading = false;
if (typeof showNotification === 'function') {
showNotification('Failed to analyze template reset impact', 'error');
} else {
alert('Failed to analyze template reset impact');
}
});
},
performReset() {
console.log('Performing reset...');
this.loading = true;
const contentType = document.querySelector('.attributes-editor').dataset.contentType;
fetch(`/plan/${contentType}/template/reset?confirm=true`, {
method: 'DELETE',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showNotification === 'function') {
showNotification(data.message, 'success');
} else {
alert(data.message);
}
setTimeout(() => window.location.reload(), 2000);
} else {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to reset template', 'error');
} else {
alert(data.error || 'Failed to reset template');
}
this.loading = false;
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to reset template', 'error');
} else {
alert('Failed to reset template');
}
this.loading = false;
});
}
};
};
// Initialize Alpine component data (global function)
window.initTemplateEditor = function() {
return {
selectedCategory: null,
selectedField: null,
configuring: false,
activePanel: window.innerWidth >= 768 ? 'both' : 'template',
// Initialize reset analysis as empty object to prevent Alpine errors
resetAnalysis: null,
// Category selection
selectCategory(categoryId) {
this.selectedCategory = categoryId;
this.selectedField = null;
this.configuring = true;
if (window.innerWidth < 768) {
this.activePanel = 'config';
}
// Show loading animation
showConfigLoadingAnimation('category-config-container');
// Load category configuration via AJAX
fetch(`/plan/attribute_categories/${categoryId}/edit`)
.then(response => response.text())
.then(html => {
document.getElementById('category-config-container').innerHTML = html;
// Bind remote form handlers to newly loaded forms
bindRemoteFormsInContainer('category-config-container');
// Hide loading animation
hideConfigLoadingAnimation('category-config-container');
})
.catch(error => {
console.error('Error loading category config:', error);
hideConfigLoadingAnimation('category-config-container');
showNotification('Failed to load category configuration', 'error');
});
},
// Field selection
selectField(fieldId) {
this.selectedField = fieldId;
this.selectedCategory = null;
this.configuring = true;
if (window.innerWidth < 768) {
this.activePanel = 'config';
}
// Show loading animation
showConfigLoadingAnimation('field-config-container');
// Load field configuration via AJAX
fetch(`/plan/attribute_fields/${fieldId}/edit`)
.then(response => response.text())
.then(html => {
document.getElementById('field-config-container').innerHTML = html;
// Bind remote form handlers to newly loaded forms
bindRemoteFormsInContainer('field-config-container');
// Hide loading animation
hideConfigLoadingAnimation('field-config-container');
})
.catch(error => {
console.error('Error loading field config:', error);
hideConfigLoadingAnimation('field-config-container');
showNotification('Failed to load field configuration', 'error');
});
}
};
};
document.addEventListener('DOMContentLoaded', function() {
// Initialize sortable for categories
initSortables();
// Handle remote form submissions for dynamically loaded forms
setupRemoteFormHandlers();
// Show category form
document.addEventListener('click', function(event) {
if (event.target.closest('[data-action="click->attributes-editor#showAddCategoryForm"]')) {
const form = document.getElementById('add-category-form');
form.classList.toggle('hidden');
}
});
// Handle select-category event dispatched from category cards
document.addEventListener('select-category', function(event) {
const alpineElement = document.querySelector('.attributes-editor');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine.selectCategory) {
alpine.selectCategory(event.detail.id);
}
}
});
// Handle select-field event dispatched from field items
document.addEventListener('select-field', function(event) {
const alpineElement = document.querySelector('.attributes-editor');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine.selectField) {
alpine.selectField(event.detail.id);
}
}
});
// Category suggestions
document.querySelectorAll('.js-show-category-suggestions').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const contentType = document.querySelector('.attributes-editor').dataset.contentType;
const resultContainer = this.closest('div').querySelector('.suggest-categories-container');
// Show loading animation in the suggestions container
showSuggestionsLoadingAnimation(resultContainer);
fetch(`/plan/attribute_categories/suggest?content_type=${contentType}`)
.then(response => response.json())
.then(data => {
const existingCategories = Array.from(document.querySelectorAll('.category-label')).map(el => el.textContent.trim());
const newCategories = data.filter(c => !existingCategories.includes(c));
if (newCategories.length > 0) {
resultContainer.innerHTML = '';
newCategories.forEach(category => {
const chip = document.createElement('span');
chip.className = 'category-suggestion-link px-3 py-1 bg-gray-100 text-sm text-gray-800 rounded-full hover:bg-gray-200 cursor-pointer';
chip.textContent = category;
chip.addEventListener('click', function() {
document.querySelector('.js-category-input').value = category;
});
resultContainer.appendChild(chip);
});
} else {
resultContainer.innerHTML = '
No suggestions available at the moment.
';
}
})
.catch(error => {
console.error('Error loading category suggestions:', error);
resultContainer.innerHTML = 'Failed to load suggestions. Please try again.
';
});
this.style.display = 'none';
});
});
// Field suggestions
document.querySelectorAll('.js-show-field-suggestions').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const contentType = document.querySelector('.attributes-editor').dataset.contentType;
const categoryContainer = this.closest('li') || this.closest('.category-card');
const categoryLabel = categoryContainer.querySelector('.category-label').textContent.trim();
const resultContainer = this.closest('div').querySelector('.suggest-fields-container');
// Show loading animation in the suggestions container
showSuggestionsLoadingAnimation(resultContainer);
fetch(`/plan/attribute_fields/suggest?content_type=${contentType}&category=${categoryLabel}`)
.then(response => response.json())
.then(data => {
const existingFields = Array.from(categoryContainer.querySelectorAll('.field-label')).map(el => el.textContent.trim());
const newFields = data.filter(f => !existingFields.includes(f));
if (newFields.length > 0) {
resultContainer.innerHTML = '';
newFields.forEach(field => {
const chip = document.createElement('span');
chip.className = 'field-suggestion-link px-3 py-1 bg-gray-100 text-sm text-gray-800 rounded-full hover:bg-gray-200 cursor-pointer';
chip.textContent = field;
chip.addEventListener('click', function() {
categoryContainer.querySelector('.js-field-input').value = field;
});
resultContainer.appendChild(chip);
});
} else {
resultContainer.innerHTML = 'No suggestions available at the moment.
';
}
})
.catch(error => {
console.error('Error loading field suggestions:', error);
resultContainer.innerHTML = 'Failed to load suggestions. Please try again.
';
});
this.style.display = 'none';
});
});
});
// Initialize sortable functionality using jQuery UI
function initSortables() {
if (typeof $ === 'undefined' || !$.fn.sortable) {
console.error('jQuery UI Sortable not found');
return;
}
// Categories sorting
$('#categories-container').sortable({
items: '.category-card',
handle: '.category-drag-handle',
placeholder: 'category-placeholder',
cursor: 'move',
opacity: 0.8,
tolerance: 'pointer',
update: function(event, ui) {
const categoryId = ui.item.attr('data-category-id');
const newPosition = ui.item.index();
// AJAX request to update position using internal endpoint
$.ajax({
url: '/internal/sort/categories',
type: 'PATCH',
contentType: 'application/json',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
data: JSON.stringify({
content_id: categoryId,
intended_position: newPosition
}),
success: function(data) {
console.log('Category position updated successfully:', data);
if (data.message) {
showNotification(data.message, 'success');
}
// Update the position field in the category config form if it's open
updateCategoryConfigPosition(categoryId, data.category.position);
},
error: function(xhr, status, error) {
console.error('Error updating category position:', error);
showErrorMessage('Failed to reorder categories. Please try again.');
}
});
}
});
// Fields sorting for each category
$('.fields-container').sortable({
items: '.field-item',
handle: '.field-drag-handle',
placeholder: 'field-placeholder',
cursor: 'move',
opacity: 0.8,
tolerance: 'pointer',
connectWith: '.fields-container',
update: function(event, ui) {
const fieldId = ui.item.attr('data-field-id');
const newPosition = ui.item.index();
const categoryId = ui.item.closest('.fields-container').attr('data-category-id');
// AJAX request to update position using internal endpoint
$.ajax({
url: '/internal/sort/fields',
type: 'PATCH',
contentType: 'application/json',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
data: JSON.stringify({
content_id: fieldId,
intended_position: newPosition,
attribute_category_id: categoryId
}),
success: function(data) {
console.log('Field position updated successfully:', data);
if (data.message) {
showNotification(data.message, 'success');
}
// Update the position field in the field config form if it's open
updateFieldConfigPosition(fieldId, data.field.position);
},
error: function(xhr, status, error) {
console.error('Error updating field position:', error);
showErrorMessage('Failed to reorder fields. Please try again.');
}
});
}
});
}
// Notification system
function showNotification(message, type = 'info') {
const bgColor = type === 'success' ? 'bg-green-500' : type === 'error' ? 'bg-red-500' : 'bg-blue-500';
const icon = type === 'success' ? 'check_circle' : type === 'error' ? 'error' : 'info';
const notification = $(`
${icon}
${message}
`);
$('body').append(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
notification.fadeOut(300, function() {
$(this).remove();
});
}, 5000);
}
// Make showNotification globally available
window.showNotification = showNotification;
// Category visibility toggle function
window.toggleCategoryVisibility = function(categoryId, isHidden) {
const categoryCard = document.querySelector(`[data-category-id="${categoryId}"]`);
const button = categoryCard.querySelector('.category-visibility-toggle');
fetch(`/plan/attribute_categories/${categoryId}`, {
method: 'PUT',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
attribute_category: {
hidden: !isHidden
}
})
})
.then(response => response.json())
.then(data => {
if (data.success || data.message) {
// Update the UI manually instead of reloading
const newHiddenState = !isHidden;
// Update button data attribute and title
button.setAttribute('data-hidden', newHiddenState);
button.setAttribute('title', newHiddenState ? 'Hidden category - Click to show' : 'Visible category - Click to hide');
// Update the eye icon
const eyeIcon = button.querySelector('svg');
if (newHiddenState) {
// Show closed eye icon
eyeIcon.innerHTML = '';
} else {
// Show open eye icon
eyeIcon.innerHTML = '';
}
// Update category card styling
if (newHiddenState) {
categoryCard.classList.add('border-gray-300');
categoryCard.style.borderColor = '';
categoryCard.querySelector('.category-header').style.backgroundColor = '#f9fafb';
categoryCard.querySelector('.category-icon i').classList.add('text-gray-400');
categoryCard.querySelector('.category-icon i').style.color = '';
} else {
categoryCard.classList.remove('border-gray-300');
const contentTypeColor = getComputedStyle(document.documentElement).getPropertyValue('--content-type-color') || '#6366f1';
categoryCard.style.borderColor = contentTypeColor;
categoryCard.querySelector('.category-header').style.backgroundColor = contentTypeColor + '20';
categoryCard.querySelector('.category-icon i').classList.remove('text-gray-400');
categoryCard.querySelector('.category-icon i').style.color = contentTypeColor;
}
// Update hidden status text
const statusText = categoryCard.querySelector('.category-label').parentElement.querySelector('p');
if (newHiddenState) {
if (!statusText.textContent.includes('— Hidden')) {
statusText.innerHTML += '— Hidden';
}
} else {
statusText.innerHTML = statusText.innerHTML.replace('— Hidden', '');
}
showNotification(data.message, 'success');
} else {
showNotification('Failed to update category visibility', 'error');
}
})
.catch(error => {
console.error('Error toggling category visibility:', error);
showNotification('Failed to update category visibility', 'error');
});
};
// Field visibility toggle function
window.toggleFieldVisibility = function(fieldId, isHidden) {
const fieldItem = document.querySelector(`[data-field-id="${fieldId}"]`);
const button = fieldItem.querySelector('.field-visibility-toggle');
fetch(`/plan/attribute_fields/${fieldId}`, {
method: 'PUT',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
attribute_field: {
hidden: !isHidden
}
})
})
.then(response => response.json())
.then(data => {
if (data.success || data.message) {
// Update the UI manually instead of reloading
const newHiddenState = !isHidden;
// Update button data attribute and title
button.setAttribute('data-hidden', newHiddenState);
button.setAttribute('title', newHiddenState ? 'Hidden field - Click to show' : 'Visible field - Click to hide');
// Update the eye icon
const eyeIcon = button.querySelector('svg');
if (newHiddenState) {
// Show closed eye icon
eyeIcon.innerHTML = '';
} else {
// Show open eye icon
eyeIcon.innerHTML = '';
}
// Update field item styling
if (newHiddenState) {
fieldItem.classList.add('bg-gray-50', 'border-gray-200');
fieldItem.classList.remove('bg-white');
fieldItem.querySelector('.field-label').classList.add('text-gray-500');
fieldItem.querySelector('.field-label').classList.remove('text-gray-800');
} else {
fieldItem.classList.remove('bg-gray-50', 'border-gray-200');
fieldItem.classList.add('bg-white');
fieldItem.querySelector('.field-label').classList.remove('text-gray-500');
fieldItem.querySelector('.field-label').classList.add('text-gray-800');
}
// Update hidden status text in field info
const fieldInfo = fieldItem.querySelector('.text-xs.text-gray-500');
if (newHiddenState) {
if (!fieldInfo.textContent.includes('— Hidden')) {
fieldInfo.innerHTML += '— Hidden';
}
} else {
fieldInfo.innerHTML = fieldInfo.innerHTML.replace('— Hidden', '');
}
showNotification(data.message, 'success');
} else {
showNotification('Failed to update field visibility', 'error');
}
})
.catch(error => {
console.error('Error toggling field visibility:', error);
showNotification('Failed to update field visibility', 'error');
});
};
// Category icon preview function
window.updateCategoryIconPreview = function(categoryId, iconName) {
// Update the icon preview in the category header
const categoryCard = document.querySelector(`[data-category-id="${categoryId}"]`);
if (categoryCard) {
const iconElement = categoryCard.querySelector('.category-icon i');
if (iconElement) {
iconElement.textContent = iconName;
}
}
// Update the form to show the selected icon
const iconPreview = document.getElementById('selected-icon-preview');
if (iconPreview) {
iconPreview.textContent = iconName;
}
};
// Function to show error messages to users (legacy compatibility)
function showErrorMessage(message) {
showNotification(message, 'error');
}
// Bind remote form handlers to dynamically loaded forms in a container
function bindRemoteFormsInContainer(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
// Find all forms with remote: true in the container
const remoteForms = container.querySelectorAll('form[data-remote="true"]');
remoteForms.forEach(form => {
// Remove any existing event listeners to prevent duplicates
form.removeEventListener('submit', handleRemoteFormSubmit);
// Add our custom submit handler
form.addEventListener('submit', handleRemoteFormSubmit);
});
}
// Handle remote form submission manually
function handleRemoteFormSubmit(event) {
event.preventDefault(); // Prevent default form submission
const form = event.target;
const formData = new FormData(form);
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
let originalText = '';
// Disable submit button to prevent double submission
if (submitButton) {
submitButton.disabled = true;
originalText = submitButton.value || submitButton.textContent;
if (submitButton.tagName === 'INPUT') {
submitButton.value = 'Saving...';
} else {
submitButton.textContent = 'Saving...';
}
// Restore button after 3 seconds as fallback
setTimeout(() => {
submitButton.disabled = false;
if (submitButton.tagName === 'INPUT') {
submitButton.value = originalText;
} else {
submitButton.textContent = originalText;
}
}, 3000);
}
// Convert FormData to JSON for Rails
const jsonData = {};
for (let [key, value] of formData.entries()) {
// Skip Rails form helper fields
if (key === 'utf8' || key === '_method' || key === 'authenticity_token') {
continue;
}
// Handle nested attributes properly - support multiple levels
if (key.includes('[') && key.includes(']')) {
// Parse nested field names like attribute_field[field_options][input_size]
const keyParts = key.split(/[\[\]]+/).filter(part => part !== '');
let current = jsonData;
for (let i = 0; i < keyParts.length - 1; i++) {
const part = keyParts[i];
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
const finalKey = keyParts[keyParts.length - 1];
// Handle checkbox arrays (multiple values with same key)
if (current[finalKey] !== undefined) {
// If key already exists, convert to array or append to existing array
if (Array.isArray(current[finalKey])) {
if (value !== '') { // Skip empty values (Rails hidden field)
current[finalKey].push(value);
}
} else {
current[finalKey] = [current[finalKey], value].filter(v => v !== '');
}
} else {
// First occurrence of this key - if it's empty, might be a checkbox array with no selections
current[finalKey] = value === '' && key.includes('[]') ? [] : value;
}
} else {
jsonData[key] = value;
}
}
console.log('Converted form data:', jsonData);
// Get the actual HTTP method from Rails form
let httpMethod = form.method.toUpperCase();
// Check for Rails method override (for PUT/PATCH/DELETE)
const methodInput = form.querySelector('input[name="_method"]');
if (methodInput) {
httpMethod = methodInput.value.toUpperCase();
}
console.log('Submitting form with method:', httpMethod, 'to:', form.action);
// Submit form via fetch
fetch(form.action, {
method: httpMethod,
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Form submitted successfully:', data);
// Re-enable submit button
if (submitButton) {
submitButton.disabled = false;
if (submitButton.tagName === 'INPUT') {
submitButton.value = originalText;
} else {
submitButton.textContent = originalText;
}
}
// Show success notification
if (data.message) {
showNotification(data.message, 'success');
}
// Handle category form updates
if (form.action.includes('/attribute_categories/')) {
handleCategoryFormSuccess(form, data);
}
// Handle field form updates
if (form.action.includes('/attribute_fields/')) {
handleFieldFormSuccess(form, data);
}
})
.catch(error => {
console.error('Form submission failed:', error);
// Re-enable submit button
if (submitButton) {
submitButton.disabled = false;
if (submitButton.tagName === 'INPUT') {
submitButton.value = originalText;
} else {
submitButton.textContent = originalText;
}
}
showNotification('Failed to save changes', 'error');
});
}
// Setup remote form handlers for dynamically loaded forms
function setupRemoteFormHandlers() {
// Handle successful form submissions (backup for Rails UJS if it works)
document.addEventListener('ajax:success', function(event) {
const form = event.target;
if (!form.matches('form[data-remote="true"]')) return;
const response = event.detail[0];
console.log('Form submitted successfully via Rails UJS:', response);
// Show success notification
if (response.message) {
showNotification(response.message, 'success');
}
// Handle category form updates
if (form.action.includes('/attribute_categories/')) {
handleCategoryFormSuccess(form, response);
}
// Handle field form updates
if (form.action.includes('/attribute_fields/')) {
handleFieldFormSuccess(form, response);
}
});
// Handle form submission errors (backup for Rails UJS if it works)
document.addEventListener('ajax:error', function(event) {
const form = event.target;
if (!form.matches('form[data-remote="true"]')) return;
const response = event.detail[0];
console.error('Form submission failed via Rails UJS:', response);
let errorMessage = 'Failed to save changes';
if (response && response.error) {
errorMessage = response.error;
}
showNotification(errorMessage, 'error');
});
}
// Handle successful category form submission
function handleCategoryFormSuccess(form, response) {
// Check if this is a new category creation (POST to /attribute_categories)
const isNewCategory = form.method.toUpperCase() === 'POST' && form.action.endsWith('/attribute_categories');
if (isNewCategory) {
// Handle new category creation
if (response.category) {
addNewCategoryToUI(response.category, response.rendered_html);
// Hide the add category form and reset it
const addCategoryForm = document.getElementById('add-category-form');
if (addCategoryForm) {
addCategoryForm.classList.add('hidden');
form.reset();
}
}
return;
}
// Handle existing category updates
const matches = form.action.match(/\/attribute_categories\/(\d+)/);
if (!matches) return;
const categoryId = matches[1];
const categoryCard = document.querySelector(`[data-category-id="${categoryId}"]`);
if (!categoryCard) return;
// Update category display if label changed
if (response.category && response.category.label) {
const labelElement = categoryCard.querySelector('.category-label');
if (labelElement) {
labelElement.textContent = response.category.label;
}
}
// Update category icon if changed
if (response.category && response.category.icon) {
const iconElement = categoryCard.querySelector('.category-icon i');
if (iconElement) {
iconElement.textContent = response.category.icon;
}
}
// Handle archive/visibility changes
if (response.category && typeof response.category.hidden !== 'undefined') {
const isArchived = response.category.hidden;
updateCategoryArchiveUI(categoryId, isArchived);
updateArchivedItemsCount();
// Auto-enable "Show archived items" when archiving from config panel
if (isArchived) {
const alpineElement = document.querySelector('[x-data*="showArchived"]');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine && !alpine.showArchived) {
alpine.showArchived = true;
toggleArchivedItems(true);
}
}
}
// Reload the configuration panel to show the updated archive state
reloadCategoryConfiguration(categoryId);
// Update General Settings counts when archive status changes
updateGeneralSettingsCounts();
}
}
// Reload category configuration panel to reflect updated state
function reloadCategoryConfiguration(categoryId) {
// Only reload if this category's configuration is currently open
const alpineElement = document.querySelector('.attributes-editor');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine && alpine.selectedCategory == categoryId) {
console.log(`Reloading configuration for category ${categoryId}`);
// Show loading animation
showConfigLoadingAnimation('category-config-container');
// Reload category configuration via AJAX
fetch(`/plan/attribute_categories/${categoryId}/edit`)
.then(response => response.text())
.then(html => {
document.getElementById('category-config-container').innerHTML = html;
// Bind remote form handlers to newly loaded forms
bindRemoteFormsInContainer('category-config-container');
// Hide loading animation
hideConfigLoadingAnimation('category-config-container');
})
.catch(error => {
console.error('Error reloading category config:', error);
hideConfigLoadingAnimation('category-config-container');
showNotification('Failed to reload category configuration', 'error');
});
}
}
}
// Add new field to the UI using server-rendered HTML
function addNewFieldToUI(field, renderedHtml, form) {
// Find the fields container for the category this field belongs to
const categoryId = field.attribute_category_id;
const fieldsContainer = document.querySelector(`.fields-container[data-category-id="${categoryId}"]`);
if (!fieldsContainer) {
console.error(`Fields container not found for category ${categoryId}`);
return;
}
// If we have rendered HTML from the server, use that
if (renderedHtml) {
// Add the new field to the fields container
fieldsContainer.insertAdjacentHTML('beforeend', renderedHtml);
// Update the category field count in the header
updateCategoryFieldCount(categoryId);
// Update General Settings counts
updateGeneralSettingsCounts();
console.log(`Added new field "${field.label}" to category ${categoryId} using server-rendered HTML`);
return;
}
// Fallback: If no rendered HTML provided, log error and skip
console.error('No rendered HTML provided for new field. Field not added to UI.');
}
// Update category field count in the header
function updateCategoryFieldCount(categoryId) {
const categoryCard = document.querySelector(`[data-category-id="${categoryId}"]`);
if (!categoryCard) return;
const fieldsContainer = categoryCard.querySelector(`.fields-container[data-category-id="${categoryId}"]`);
if (!fieldsContainer) return;
// Count visible fields (excluding archived ones if they're hidden)
const fieldItems = fieldsContainer.querySelectorAll('.field-item');
const visibleFields = Array.from(fieldItems).filter(field =>
field.style.display !== 'none'
);
// Update the count in the category header
const fieldCountElement = categoryCard.querySelector('.category-label').parentElement.querySelector('p');
if (fieldCountElement) {
const count = visibleFields.length;
const fieldText = count === 1 ? 'field' : 'fields';
// Update just the count part, preserving any status text (like "— Archived")
const statusMatch = fieldCountElement.innerHTML.match(/]*>.*?<\/span>/);
const statusText = statusMatch ? statusMatch[0] : '';
fieldCountElement.innerHTML = `${count} ${fieldText}${statusText ? ' ' + statusText : ''}`;
}
}
// Add new category to the UI using server-rendered HTML
function addNewCategoryToUI(category, renderedHtml) {
const categoriesContainer = document.getElementById('categories-container');
if (!categoriesContainer) return;
// If we have rendered HTML from the server, use that instead of generating our own
if (renderedHtml) {
// Find the "Add Category" card and insert the new category before it
const addCategoryCard = categoriesContainer.querySelector('.bg-white.rounded-lg.shadow-sm.border.border-dashed');
if (addCategoryCard) {
addCategoryCard.insertAdjacentHTML('beforebegin', renderedHtml);
} else {
// Fallback: add to the end of the container
categoriesContainer.insertAdjacentHTML('beforeend', renderedHtml);
}
console.log(`Added new category "${category.label}" to UI using server-rendered HTML`);
return;
}
// Fallback: If no rendered HTML provided, log error and skip
console.error('No rendered HTML provided for new category. Category not added to UI.');
}
// Handle new field form submission manually
window.submitFieldForm = function(event) {
const form = event.target;
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
// Disable submit button and show loading state
let originalText = '';
if (submitButton) {
submitButton.disabled = true;
originalText = submitButton.value || submitButton.textContent;
if (submitButton.tagName === 'INPUT') {
submitButton.value = 'Creating...';
} else {
submitButton.textContent = 'Creating...';
}
}
// Convert form data to JSON
const formData = new FormData(form);
const jsonData = {};
for (let [key, value] of formData.entries()) {
// Skip Rails form helper fields
if (key === 'utf8' || key === '_method' || key === 'authenticity_token') {
continue;
}
// Handle nested attributes properly
if (key.includes('[') && key.includes(']')) {
const keyParts = key.split(/[\[\]]+/).filter(part => part !== '');
let current = jsonData;
for (let i = 0; i < keyParts.length - 1; i++) {
const part = keyParts[i];
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
const finalKey = keyParts[keyParts.length - 1];
// Handle checkbox arrays (multiple values with same key)
if (current[finalKey] !== undefined) {
// If key already exists, convert to array or append to existing array
if (Array.isArray(current[finalKey])) {
if (value !== '') { // Skip empty values (Rails hidden field)
current[finalKey].push(value);
}
} else {
current[finalKey] = [current[finalKey], value].filter(v => v !== '');
}
} else {
// First occurrence of this key - if it's empty, might be a checkbox array with no selections
current[finalKey] = value === '' && key.includes('[]') ? [] : value;
}
} else {
jsonData[key] = value;
}
}
console.log('Submitting new field form:', jsonData);
// Submit form via fetch
fetch(form.action, {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Field created successfully:', data);
// Handle success
if (data.field && data.html) {
addNewFieldToUI(data.field, data.html, form);
// Update General Settings counts
updateGeneralSettingsCounts();
// Reset the form and close the add field section
form.reset();
const addingFieldSection = form.closest('[x-data*="addingField"]');
if (addingFieldSection) {
// Use Alpine.js to close the form
const alpineData = Alpine.$data(addingFieldSection);
if (alpineData) {
alpineData.addingField = false;
}
}
// Show success notification
if (data.message) {
showNotification(data.message, 'success');
} else {
showNotification(`Field "${data.field.label}" created successfully`, 'success');
}
}
})
.catch(error => {
console.error('Failed to create field:', error);
showNotification('Failed to create field', 'error');
})
.finally(() => {
// Re-enable submit button
if (submitButton) {
submitButton.disabled = false;
if (submitButton.tagName === 'INPUT') {
submitButton.value = originalText;
} else {
submitButton.textContent = originalText;
}
}
});
};
// Handle new category form submission manually
window.submitCategoryForm = function(event) {
const form = event.target;
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
// Disable submit button and show loading state
let originalText = '';
if (submitButton) {
submitButton.disabled = true;
originalText = submitButton.value || submitButton.textContent;
if (submitButton.tagName === 'INPUT') {
submitButton.value = 'Creating...';
} else {
submitButton.textContent = 'Creating...';
}
}
// Convert form data to JSON
const formData = new FormData(form);
const jsonData = {};
for (let [key, value] of formData.entries()) {
// Skip Rails form helper fields
if (key === 'utf8' || key === '_method' || key === 'authenticity_token') {
continue;
}
// Handle nested attributes properly
if (key.includes('[') && key.includes(']')) {
const keyParts = key.split(/[\[\]]+/).filter(part => part !== '');
let current = jsonData;
for (let i = 0; i < keyParts.length - 1; i++) {
const part = keyParts[i];
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
const finalKey = keyParts[keyParts.length - 1];
// Handle checkbox arrays (multiple values with same key)
if (current[finalKey] !== undefined) {
// If key already exists, convert to array or append to existing array
if (Array.isArray(current[finalKey])) {
if (value !== '') { // Skip empty values (Rails hidden field)
current[finalKey].push(value);
}
} else {
current[finalKey] = [current[finalKey], value].filter(v => v !== '');
}
} else {
// First occurrence of this key - if it's empty, might be a checkbox array with no selections
current[finalKey] = value === '' && key.includes('[]') ? [] : value;
}
} else {
jsonData[key] = value;
}
}
console.log('Submitting new category form:', jsonData);
// Submit form via fetch
fetch(form.action, {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
console.log('Category created successfully:', data);
// Handle success
if (data.category) {
addNewCategoryToUI(data.category, data.rendered_html);
// Update General Settings counts
updateGeneralSettingsCounts();
// Hide the form and reset it
document.getElementById('add-category-form').classList.add('hidden');
form.reset();
// Show success notification
if (data.message) {
showNotification(data.message, 'success');
} else {
showNotification(`Category "${data.category.label}" created successfully`, 'success');
}
}
})
.catch(error => {
console.error('Failed to create category:', error);
showNotification('Failed to create category', 'error');
})
.finally(() => {
// Re-enable submit button
if (submitButton) {
submitButton.disabled = false;
if (submitButton.tagName === 'INPUT') {
submitButton.value = originalText;
} else {
submitButton.textContent = originalText;
}
}
});
};
// Handle successful field form submission
function handleFieldFormSuccess(form, response) {
// Check if this is a new field creation (POST to /attribute_fields)
const isNewField = form.method.toUpperCase() === 'POST' && form.action.endsWith('/attribute_fields');
if (isNewField) {
// Handle new field creation
if (response.field && response.html) {
addNewFieldToUI(response.field, response.html, form);
// Update General Settings counts for new field
updateGeneralSettingsCounts();
// Reset the form and close the add field section
form.reset();
const addingFieldSection = form.closest('[x-data*="addingField"]');
if (addingFieldSection) {
// Use Alpine.js to close the form
const alpineData = Alpine.$data(addingFieldSection);
if (alpineData) {
alpineData.addingField = false;
}
}
}
return;
}
// Handle existing field updates
const matches = form.action.match(/\/attribute_fields\/(\d+)/);
if (!matches) return;
const fieldId = matches[1];
const fieldItem = document.querySelector(`[data-field-id="${fieldId}"]`);
if (!fieldItem) return;
// For field updates, reload the field item with fresh server-rendered HTML
// This ensures all changes (label, linkable_types, visibility, etc.) are reflected
if (response.field && response.html) {
// Replace the existing field item with the updated one
fieldItem.outerHTML = response.html;
console.log(`Updated field "${response.field.label}" UI with server-rendered HTML`);
return;
}
// Fallback: Manual updates if no HTML provided (legacy support)
// Update field display if label changed
if (response.field && response.field.label) {
const labelElement = fieldItem.querySelector('.field-label');
if (labelElement) {
labelElement.textContent = response.field.label;
}
}
// Handle visibility changes
if (response.field && typeof response.field.hidden !== 'undefined') {
const isHidden = response.field.hidden;
updateFieldVisibilityUI(fieldId, isHidden);
}
}
// Update category visibility UI elements
function updateCategoryVisibilityUI(categoryId, isHidden) {
const categoryCard = document.querySelector(`[data-category-id="${categoryId}"]`);
if (!categoryCard) return;
// Update visibility toggle button
const button = categoryCard.querySelector('.category-visibility-toggle');
if (button) {
button.setAttribute('data-hidden', isHidden);
button.setAttribute('title', isHidden ? 'Hidden category - Click to show' : 'Visible category - Click to hide');
const eyeIcon = button.querySelector('svg');
if (eyeIcon) {
if (isHidden) {
eyeIcon.innerHTML = '';
} else {
eyeIcon.innerHTML = '';
}
}
}
// Update category card styling
if (isHidden) {
categoryCard.classList.add('border-gray-300');
categoryCard.style.borderColor = '';
categoryCard.querySelector('.category-header').style.backgroundColor = '#f9fafb';
categoryCard.querySelector('.category-icon i').classList.add('text-gray-400');
categoryCard.querySelector('.category-icon i').style.color = '';
} else {
categoryCard.classList.remove('border-gray-300');
const contentTypeColor = getComputedStyle(document.documentElement).getPropertyValue('--content-type-color') || '#6366f1';
categoryCard.style.borderColor = contentTypeColor;
categoryCard.querySelector('.category-header').style.backgroundColor = contentTypeColor + '20';
categoryCard.querySelector('.category-icon i').classList.remove('text-gray-400');
categoryCard.querySelector('.category-icon i').style.color = contentTypeColor;
}
// Update hidden status text
const statusText = categoryCard.querySelector('.category-label').parentElement.querySelector('p');
if (statusText) {
if (isHidden) {
if (!statusText.textContent.includes('— Hidden')) {
statusText.innerHTML += '— Hidden';
}
} else {
statusText.innerHTML = statusText.innerHTML.replace('— Hidden', '');
}
}
}
// Update field visibility UI elements
function updateFieldVisibilityUI(fieldId, isHidden) {
const fieldItem = document.querySelector(`[data-field-id="${fieldId}"]`);
if (!fieldItem) return;
// Update visibility toggle button
const button = fieldItem.querySelector('.field-visibility-toggle');
if (button) {
button.setAttribute('data-hidden', isHidden);
button.setAttribute('title', isHidden ? 'Hidden field - Click to show' : 'Visible field - Click to hide');
const eyeIcon = button.querySelector('svg');
if (eyeIcon) {
if (isHidden) {
eyeIcon.innerHTML = '';
} else {
eyeIcon.innerHTML = '';
}
}
}
// Update field item styling
if (isHidden) {
fieldItem.classList.add('bg-gray-50', 'border-gray-200');
fieldItem.classList.remove('bg-white');
fieldItem.querySelector('.field-label').classList.add('text-gray-500');
fieldItem.querySelector('.field-label').classList.remove('text-gray-800');
} else {
fieldItem.classList.remove('bg-gray-50', 'border-gray-200');
fieldItem.classList.add('bg-white');
fieldItem.querySelector('.field-label').classList.remove('text-gray-500');
fieldItem.querySelector('.field-label').classList.add('text-gray-800');
}
// Update hidden status text
const fieldInfo = fieldItem.querySelector('.text-xs.text-gray-500');
if (fieldInfo) {
if (isHidden) {
if (!fieldInfo.textContent.includes('— Hidden')) {
fieldInfo.innerHTML += '— Hidden';
}
} else {
fieldInfo.innerHTML = fieldInfo.innerHTML.replace('— Hidden', '');
}
}
}
// Update category configuration form position field after drag and drop
function updateCategoryConfigPosition(categoryId, newPosition) {
// Check if the category config form is currently open
const alpineElement = document.querySelector('.attributes-editor');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
// Only update if this category's config form is currently open
if (alpine.selectedCategory == categoryId) {
const categoryConfigContainer = document.getElementById('category-config-container');
if (categoryConfigContainer) {
const positionInput = categoryConfigContainer.querySelector('input[name="attribute_category[position]"]');
if (positionInput) {
console.log(`Updating category ${categoryId} position input from ${positionInput.value} to ${newPosition}`);
positionInput.value = newPosition;
}
}
}
}
}
// Update field configuration form position field after drag and drop
function updateFieldConfigPosition(fieldId, newPosition) {
// Check if the field config form is currently open
const alpineElement = document.querySelector('.attributes-editor');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
// Only update if this field's config form is currently open
if (alpine.selectedField == fieldId) {
const fieldConfigContainer = document.getElementById('field-config-container');
if (fieldConfigContainer) {
const positionInput = fieldConfigContainer.querySelector('input[name="attribute_field[position]"]');
if (positionInput) {
console.log(`Updating field ${fieldId} position input from ${positionInput.value} to ${newPosition}`);
positionInput.value = newPosition;
}
}
}
}
}
// Show loading animation for configuration panels
function showConfigLoadingAnimation(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
// Create loading bar element
const loadingBar = document.createElement('div');
loadingBar.className = 'config-loading-bar';
loadingBar.innerHTML = `
`;
// Add loading styles
const style = document.createElement('style');
style.textContent = `
.config-loading-bar {
position: relative;
padding: 1rem;
}
.loading-bar-container {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background-color: #f3f4f6;
overflow: hidden;
}
.loading-bar-progress {
height: 100%;
background: linear-gradient(90deg, var(--content-type-color, #6366f1) 0%, rgba(99, 102, 241, 0.6) 50%, var(--content-type-color, #6366f1) 100%);
background-size: 200% 100%;
animation: loading-slide 1.5s ease-in-out infinite;
}
@keyframes loading-slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.loading-content {
margin-top: 1rem;
}
`;
// Add styles to head if not already present
if (!document.querySelector('.config-loading-styles')) {
style.classList.add('config-loading-styles');
document.head.appendChild(style);
}
// Replace container content with loading animation
container.innerHTML = '';
container.appendChild(loadingBar);
}
// Hide loading animation for configuration panels
function hideConfigLoadingAnimation(containerId) {
// The loading animation will be replaced when the actual content loads
// This function is called after the content is set, so it's mainly for cleanup
// and error handling scenarios
// Remove any loading-specific styles or elements if needed
const container = document.getElementById(containerId);
if (container) {
const loadingBar = container.querySelector('.config-loading-bar');
if (loadingBar) {
loadingBar.remove();
}
}
}
// Show loading animation for suggestions containers
function showSuggestionsLoadingAnimation(container) {
if (!container) return;
// Create compact loading indicator for suggestions
const loadingIndicator = document.createElement('div');
loadingIndicator.className = 'suggestions-loading-indicator';
loadingIndicator.innerHTML = `
`;
// Add suggestion loading styles if not already present
if (!document.querySelector('.suggestions-loading-styles')) {
const style = document.createElement('style');
style.classList.add('suggestions-loading-styles');
style.textContent = `
.suggestions-loading-indicator {
position: relative;
border: 1px solid #e5e7eb;
border-radius: 0.375rem;
background-color: #f9fafb;
margin: 0.5rem 0;
padding: 0.75rem;
}
.suggestions-loading-indicator .loading-bar-container {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background-color: #f3f4f6;
overflow: hidden;
border-radius: 0.375rem 0.375rem 0 0;
}
.suggestions-loading-indicator .loading-bar-progress {
height: 100%;
background: linear-gradient(90deg, var(--content-type-color, #6366f1) 0%, rgba(99, 102, 241, 0.6) 50%, var(--content-type-color, #6366f1) 100%);
background-size: 200% 100%;
animation: loading-slide 1.2s ease-in-out infinite;
}
.suggestions-loading-indicator .loading-content {
display: flex;
align-items: center;
justify-content: center;
padding-top: 0.25rem;
}
.suggestions-loading-indicator .loading-dots {
display: flex;
align-items: center;
margin-right: 0.5rem;
}
.suggestions-loading-indicator .loading-dots .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #9ca3af;
margin: 0 2px;
animation: loading-dots 1.4s ease-in-out infinite both;
}
.suggestions-loading-indicator .loading-dots .dot:nth-child(1) {
animation-delay: -0.32s;
}
.suggestions-loading-indicator .loading-dots .dot:nth-child(2) {
animation-delay: -0.16s;
}
.suggestions-loading-indicator .loading-text {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
}
@keyframes loading-dots {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
`;
document.head.appendChild(style);
}
// Replace container content with loading animation
container.innerHTML = '';
container.appendChild(loadingIndicator);
}
// Archive/restore functionality for fields
window.toggleFieldArchive = function(fieldId, isArchived) {
const fieldItem = document.querySelector(`[data-field-id="${fieldId}"]`);
const button = fieldItem.querySelector('.field-archive-toggle');
// Show loading state
const originalIcon = button.innerHTML;
button.innerHTML = '';
button.disabled = true;
fetch(`/plan/attribute_fields/${fieldId}`, {
method: 'PUT',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
attribute_field: {
hidden: !isArchived
}
})
})
.then(response => response.json())
.then(data => {
if (data.success || data.message) {
const newArchivedState = !isArchived;
updateFieldArchiveUI(fieldId, newArchivedState);
updateArchivedItemsCount();
updateGeneralSettingsCounts();
// Auto-enable "Show archived items" when archiving
if (newArchivedState) {
const alpineElement = document.querySelector('[x-data*="showArchived"]');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine && !alpine.showArchived) {
alpine.showArchived = true;
toggleArchivedItems(true);
}
}
}
const action = newArchivedState ? 'archived' : 'restored';
showNotification(`Field ${action} successfully`, 'success');
} else {
// Restore original state on error
button.innerHTML = originalIcon;
button.disabled = false;
showNotification('Failed to update field archive status', 'error');
}
})
.catch(error => {
console.error('Error toggling field archive:', error);
// Restore original state on error
button.innerHTML = originalIcon;
button.disabled = false;
showNotification('Failed to update field archive status', 'error');
});
};
// Archive/restore functionality for categories
window.toggleCategoryArchive = function(categoryId, isArchived) {
const categoryCard = document.querySelector(`[data-category-id="${categoryId}"]`);
const button = categoryCard.querySelector('.category-archive-toggle');
// Show loading state
const originalIcon = button.innerHTML;
button.innerHTML = '';
button.disabled = true;
fetch(`/plan/attribute_categories/${categoryId}`, {
method: 'PUT',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
attribute_category: {
hidden: !isArchived
}
})
})
.then(response => response.json())
.then(data => {
if (data.success || data.message) {
const newArchivedState = !isArchived;
// If archiving a category, close its configuration panel if it's currently open
if (newArchivedState) {
const alpineElement = document.querySelector('.attributes-editor');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine && alpine.selectedCategory == categoryId) {
// Deselect the category and close configuration panel
alpine.selectedCategory = null;
alpine.configuring = false;
console.log(`Closed configuration panel for archived category ${categoryId}`);
}
}
}
updateCategoryArchiveUI(categoryId, newArchivedState);
updateArchivedItemsCount();
updateGeneralSettingsCounts();
// Auto-enable "Show archived items" when archiving
if (newArchivedState) {
const alpineElement = document.querySelector('[x-data*="showArchived"]');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine && !alpine.showArchived) {
alpine.showArchived = true;
toggleArchivedItems(true);
}
}
}
const action = newArchivedState ? 'archived' : 'restored';
showNotification(`Category ${action} successfully`, 'success');
} else {
// Restore original state on error
button.innerHTML = originalIcon;
button.disabled = false;
showNotification('Failed to update category archive status', 'error');
}
})
.catch(error => {
console.error('Error toggling category archive:', error);
// Restore original state on error
button.innerHTML = originalIcon;
button.disabled = false;
showNotification('Failed to update category archive status', 'error');
});
};
// Toggle show/hide archived items (categories and fields)
window.toggleArchivedItems = function(show) {
const archivedFields = document.querySelectorAll('.field-item[data-archived="true"]');
const archivedCategories = document.querySelectorAll('.category-card[data-archived="true"]');
archivedFields.forEach(field => {
if (show) {
field.style.display = '';
field.classList.add('archived-field');
} else {
field.style.display = 'none';
field.classList.remove('archived-field');
}
});
archivedCategories.forEach(category => {
if (show) {
category.style.display = '';
category.classList.add('archived-category');
} else {
category.style.display = 'none';
category.classList.remove('archived-category');
}
});
// Show first-time user tip if showing archived items for the first time
if (show && !localStorage.getItem('seen_archive_tip')) {
showArchiveTip();
localStorage.setItem('seen_archive_tip', 'true');
}
};
// Update field archive UI elements
function updateFieldArchiveUI(fieldId, isArchived) {
const fieldItem = document.querySelector(`[data-field-id="${fieldId}"]`);
if (!fieldItem) return;
// Update data attribute
fieldItem.setAttribute('data-archived', isArchived);
// Update archive toggle button
const button = fieldItem.querySelector('.field-archive-toggle');
if (button) {
button.setAttribute('data-archived', isArchived);
button.setAttribute('title', isArchived ? 'Archived field - Click to restore' : 'Active field - Click to archive');
button.disabled = false; // Re-enable button after successful update
// Update button content with correct icon
if (isArchived) {
button.innerHTML = 'unarchive';
} else {
button.innerHTML = 'archive';
}
}
// Update field item styling
if (isArchived) {
fieldItem.classList.add('bg-gray-100', 'border-gray-300', 'opacity-60', 'archived-field');
fieldItem.classList.remove('bg-white');
fieldItem.querySelector('.field-label').classList.add('text-gray-500');
fieldItem.querySelector('.field-label').classList.remove('text-gray-800');
// Add archive icon to label if not present
const label = fieldItem.querySelector('.field-label');
if (!label.querySelector('.material-icons')) {
label.innerHTML += 'archive';
}
} else {
fieldItem.classList.remove('bg-gray-100', 'border-gray-300', 'opacity-60', 'archived-field');
fieldItem.classList.add('bg-white');
fieldItem.querySelector('.field-label').classList.remove('text-gray-500');
fieldItem.querySelector('.field-label').classList.add('text-gray-800');
// Remove archive icon from label
const archiveIcon = fieldItem.querySelector('.field-label .material-icons');
if (archiveIcon) {
archiveIcon.remove();
}
}
// Update archive status text
const fieldInfo = fieldItem.querySelector('.text-xs.text-gray-500');
if (fieldInfo) {
// Remove existing status spans
fieldInfo.querySelectorAll('span').forEach(span => {
if (span.textContent.includes('Hidden') || span.textContent.includes('Archived')) {
span.remove();
}
});
// Add archived status if needed
if (isArchived) {
const archivedSpan = document.createElement('span');
archivedSpan.className = 'ml-1.5 text-amber-600';
archivedSpan.textContent = '— Archived';
fieldInfo.appendChild(archivedSpan);
}
}
// Hide archived field if show archived is off
const showArchivedToggle = document.querySelector('input[x-model="showArchived"]');
if (showArchivedToggle && !showArchivedToggle.checked && isArchived) {
fieldItem.style.display = 'none';
} else if (!isArchived) {
fieldItem.style.display = '';
}
}
// Update category archive UI elements
function updateCategoryArchiveUI(categoryId, isArchived) {
const categoryCard = document.querySelector(`[data-category-id="${categoryId}"]`);
if (!categoryCard) return;
// Update data attribute
categoryCard.setAttribute('data-archived', isArchived);
// Update archive button data attribute and title
const archiveButton = categoryCard.querySelector('.category-archive-toggle');
if (archiveButton) {
archiveButton.setAttribute('data-archived', isArchived);
archiveButton.setAttribute('title', isArchived ? 'Archived category - Click to restore' : 'Active category - Click to archive');
archiveButton.disabled = false; // Re-enable button after successful update
// Update the button icon
if (isArchived) {
archiveButton.innerHTML = 'unarchive';
} else {
archiveButton.innerHTML = 'archive';
}
}
// Update category styling and archive icon
if (isArchived) {
categoryCard.classList.add('border-gray-300', 'opacity-60', 'archived-category');
categoryCard.style.borderColor = '';
categoryCard.querySelector('.category-header').style.backgroundColor = '#f3f4f6';
categoryCard.querySelector('.category-icon i').classList.add('text-gray-400');
categoryCard.querySelector('.category-icon i').style.color = '';
// Add archive icon to label if not present
const label = categoryCard.querySelector('.category-label');
if (!label.querySelector('.material-icons')) {
label.innerHTML += 'archive';
}
} else {
categoryCard.classList.remove('border-gray-300', 'opacity-60', 'archived-category');
const contentTypeColor = getComputedStyle(document.documentElement).getPropertyValue('--content-type-color') || '#6366f1';
categoryCard.style.borderColor = contentTypeColor;
categoryCard.querySelector('.category-header').style.backgroundColor = contentTypeColor + '20';
categoryCard.querySelector('.category-icon i').classList.remove('text-gray-400');
categoryCard.querySelector('.category-icon i').style.color = contentTypeColor;
// Remove archive icon from label
const archiveIcon = categoryCard.querySelector('.category-label .material-icons');
if (archiveIcon) {
archiveIcon.remove();
}
}
// Update archived status in category details
const statusText = categoryCard.querySelector('.category-label').parentElement.querySelector('p');
if (statusText) {
// Remove existing status spans
statusText.querySelectorAll('span').forEach(span => {
if (span.textContent.includes('Hidden') || span.textContent.includes('Archived')) {
span.remove();
}
});
// Add archived status if needed
if (isArchived) {
const archivedSpan = document.createElement('span');
archivedSpan.className = 'text-amber-600 ml-2';
archivedSpan.textContent = '— Archived';
statusText.appendChild(archivedSpan);
}
}
// Hide archived category if show archived is off
const showArchivedToggle = document.querySelector('input[x-model="showArchived"]');
if (showArchivedToggle && !showArchivedToggle.checked && isArchived) {
categoryCard.style.display = 'none';
} else if (!isArchived) {
categoryCard.style.display = '';
}
}
// Update archived items count in the settings panel
function updateArchivedItemsCount() {
const archivedFields = document.querySelectorAll('.field-item[data-archived="true"]');
const archivedCategories = document.querySelectorAll('.category-card[data-archived="true"]');
const totalArchived = archivedFields.length + archivedCategories.length;
// Update Alpine.js data for count display
const alpineElement = document.querySelector('[x-data*="showArchived"]');
if (alpineElement && alpineElement._x_dataStack) {
const alpine = alpineElement._x_dataStack[0];
if (alpine && typeof alpine.archivedItemsCount !== 'undefined') {
alpine.archivedItemsCount = totalArchived;
}
}
}
// Update General Settings counts for categories and fields
function updateGeneralSettingsCounts() {
// Count total categories (including archived ones)
const totalCategories = document.querySelectorAll('.category-card').length;
// Count total fields across all categories (including archived ones)
const totalFields = document.querySelectorAll('.field-item').length;
// Update the categories count in General Settings
const categoriesCountElement = document.querySelector('.general-settings-categories-count');
if (categoriesCountElement) {
categoriesCountElement.textContent = totalCategories;
}
// Update the total fields count in General Settings
const fieldsCountElement = document.querySelector('.general-settings-fields-count');
if (fieldsCountElement) {
fieldsCountElement.textContent = totalFields;
}
console.log(`Updated General Settings counts: ${totalCategories} categories, ${totalFields} fields`);
}
// Make functions globally available
window.updateArchivedItemsCount = updateArchivedItemsCount;
window.updateGeneralSettingsCounts = updateGeneralSettingsCounts;
// Legacy function name for backwards compatibility
window.updateArchivedFieldsCount = updateArchivedItemsCount;
// Select All / Select None functions for link field configuration
window.selectAllLinkableTypes = function() {
const checkboxes = document.querySelectorAll('input[name="attribute_field[field_options][linkable_types][]"]:not([value=""])');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
});
};
window.selectNoneLinkableTypes = function() {
const checkboxes = document.querySelectorAll('input[name="attribute_field[field_options][linkable_types][]"]:not([value=""])');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
};
// Show first-time archive tip
function showArchiveTip() {
const tip = document.createElement('div');
tip.className = 'archive-tip-popup fixed bottom-4 right-4 bg-blue-600 text-white p-4 rounded-lg shadow-lg max-w-sm z-50';
tip.innerHTML = `
lightbulb_outline
Archive System
Archived categories and fields are greyed out to show where they would restore to while keeping them out of your active workspace.
`;
document.body.appendChild(tip);
// Auto-remove tip after 8 seconds
setTimeout(() => {
if (tip.parentElement) {
tip.remove();
}
}, 8000);
}
// Initialize archived items count on page load
document.addEventListener('DOMContentLoaded', function() {
updateArchivedItemsCount();
});
// Detect screen size changes to adjust UI
window.addEventListener('resize', function() {
const alpine = Alpine.getRoot(document.querySelector('.attributes-editor'));
if (alpine && alpine.$data) {
if (window.innerWidth >= 768) {
alpine.$data.activePanel = 'both';
} else if (alpine.$data.configuring) {
alpine.$data.activePanel = 'config';
} else {
alpine.$data.activePanel = 'template';
}
}
});