fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
299
resources/js/modules/admin/duplicate-management.js
Normal file
299
resources/js/modules/admin/duplicate-management.js
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Duplicate Management Module
|
||||
*
|
||||
* Handles duplicate asset management (merge, delete)
|
||||
*/
|
||||
|
||||
export class DuplicateManagement {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupDeleteDuplicate();
|
||||
this.setupDeleteGroup();
|
||||
this.setupMergeGroup();
|
||||
this.setupRefresh();
|
||||
}
|
||||
|
||||
setupDeleteDuplicate() {
|
||||
document.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('[data-delete-duplicate]');
|
||||
if (!button) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const assetId = button.dataset.assetId;
|
||||
if (!confirm(`Are you sure you want to delete this duplicate asset?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteAsset(assetId, button);
|
||||
});
|
||||
}
|
||||
|
||||
setupDeleteGroup() {
|
||||
document.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('[data-delete-group]');
|
||||
if (!button) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const sha256 = button.dataset.sha256;
|
||||
const groupCard = button.closest('.duplicate-group');
|
||||
const assetCount = groupCard?.querySelectorAll('.duplicate-asset-card').length || 0;
|
||||
|
||||
if (!confirm(`Are you sure you want to delete all ${assetCount} duplicate assets in this group?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deleteGroup(sha256, button);
|
||||
});
|
||||
}
|
||||
|
||||
setupMergeGroup() {
|
||||
document.addEventListener('click', async (e) => {
|
||||
const button = e.target.closest('[data-merge-group]');
|
||||
if (!button) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const sha256 = button.dataset.sha256;
|
||||
const groupCard = button.closest('.duplicate-group');
|
||||
const assets = Array.from(groupCard?.querySelectorAll('[data-asset-id]') || [])
|
||||
.map(el => el.dataset.assetId);
|
||||
|
||||
if (assets.length < 2) {
|
||||
if (window.Toast) {
|
||||
window.Toast.warning('Need at least 2 assets to merge');
|
||||
} else {
|
||||
alert('Need at least 2 assets to merge');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show merge dialog
|
||||
const keepAssetId = await this.showMergeDialog(assets);
|
||||
if (!keepAssetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.mergeGroup(sha256, assets, keepAssetId, button);
|
||||
});
|
||||
}
|
||||
|
||||
setupRefresh() {
|
||||
document.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('[data-refresh-duplicates]');
|
||||
if (!button) return;
|
||||
|
||||
e.preventDefault();
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
async deleteAsset(assetId, button) {
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Deleting...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/api/assets/${assetId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
if (window.Toast) {
|
||||
window.Toast.success('Duplicate asset deleted');
|
||||
}
|
||||
|
||||
// Remove asset card
|
||||
button.closest('.duplicate-asset-card')?.remove();
|
||||
|
||||
// Check if group is now empty or has only one asset
|
||||
const groupCard = button.closest('.duplicate-group');
|
||||
const remainingAssets = groupCard?.querySelectorAll('.duplicate-asset-card').length || 0;
|
||||
if (remainingAssets <= 1) {
|
||||
groupCard?.remove();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete asset:', error);
|
||||
if (window.Toast) {
|
||||
window.Toast.error(`Failed to delete asset: ${error.message}`);
|
||||
} else {
|
||||
alert(`Failed to delete asset: ${error.message}`);
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteGroup(sha256, button) {
|
||||
const groupCard = button.closest('.duplicate-group');
|
||||
const assetCards = groupCard?.querySelectorAll('.duplicate-asset-card') || [];
|
||||
const assetIds = Array.from(assetCards).map(card =>
|
||||
card.querySelector('[data-asset-id]')?.dataset.assetId
|
||||
).filter(Boolean);
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = 'Deleting...';
|
||||
|
||||
try {
|
||||
// Delete all assets in parallel
|
||||
const deletePromises = assetIds.map(id =>
|
||||
fetch(`/admin/api/assets/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
if (window.Toast) {
|
||||
window.Toast.success(`Deleted ${assetIds.length} duplicate assets`);
|
||||
}
|
||||
|
||||
// Remove group card
|
||||
groupCard?.remove();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete group:', error);
|
||||
if (window.Toast) {
|
||||
window.Toast.error(`Failed to delete group: ${error.message}`);
|
||||
} else {
|
||||
alert(`Failed to delete group: ${error.message}`);
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Delete All';
|
||||
}
|
||||
}
|
||||
|
||||
async showMergeDialog(assetIds) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'merge-dialog';
|
||||
dialog.innerHTML = `
|
||||
<div class="merge-dialog__overlay"></div>
|
||||
<div class="merge-dialog__content">
|
||||
<h3>Merge Duplicate Assets</h3>
|
||||
<p>Select which asset to keep. All other duplicates will be deleted.</p>
|
||||
<div class="merge-dialog__assets" id="merge-assets-list"></div>
|
||||
<div class="merge-dialog__actions">
|
||||
<button class="btn btn--secondary" data-merge-cancel>Cancel</button>
|
||||
<button class="btn btn--primary" data-merge-confirm disabled>Merge</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
const assetsList = dialog.querySelector('#merge-assets-list');
|
||||
let selectedAssetId = null;
|
||||
|
||||
// Load asset details and create radio buttons
|
||||
assetIds.forEach(assetId => {
|
||||
const assetCard = document.querySelector(`[data-asset-id="${assetId}"]`)?.closest('.duplicate-asset-card');
|
||||
if (!assetCard) return;
|
||||
|
||||
const assetIdEl = assetCard.querySelector('.asset-id code');
|
||||
const assetBucket = assetCard.querySelector('.asset-bucket');
|
||||
const assetPreview = assetCard.querySelector('.asset-preview-small')?.innerHTML;
|
||||
|
||||
const radio = document.createElement('div');
|
||||
radio.className = 'merge-asset-option';
|
||||
radio.innerHTML = `
|
||||
<input type="radio" name="keep-asset" value="${assetId}" id="asset-${assetId}">
|
||||
<label for="asset-${assetId}">
|
||||
<div class="merge-asset-preview">${assetPreview || ''}</div>
|
||||
<div class="merge-asset-info">
|
||||
<strong>${assetIdEl?.textContent || assetId}</strong>
|
||||
<small>${assetBucket?.textContent || ''}</small>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
|
||||
radio.querySelector('input').addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
selectedAssetId = e.target.value;
|
||||
dialog.querySelector('[data-merge-confirm]').disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
assetsList.appendChild(radio);
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
document.body.removeChild(dialog);
|
||||
};
|
||||
|
||||
dialog.querySelector('[data-merge-confirm]').addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve(selectedAssetId);
|
||||
});
|
||||
|
||||
dialog.querySelector('[data-merge-cancel]').addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
dialog.querySelector('.merge-dialog__overlay').addEventListener('click', () => {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async mergeGroup(sha256, assetIds, keepAssetId, button) {
|
||||
button.disabled = true;
|
||||
button.textContent = 'Merging...';
|
||||
|
||||
try {
|
||||
// Delete all assets except the one to keep
|
||||
const deleteIds = assetIds.filter(id => id !== keepAssetId);
|
||||
const deletePromises = deleteIds.map(id =>
|
||||
fetch(`/admin/api/assets/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
if (window.Toast) {
|
||||
window.Toast.success(`Merged ${deleteIds.length} duplicates into one asset`);
|
||||
}
|
||||
|
||||
// Remove group card
|
||||
button.closest('.duplicate-group')?.remove();
|
||||
} catch (error) {
|
||||
console.error('Failed to merge group:', error);
|
||||
if (window.Toast) {
|
||||
window.Toast.error(`Failed to merge group: ${error.message}`);
|
||||
} else {
|
||||
alert(`Failed to merge group: ${error.message}`);
|
||||
}
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = 'Merge Group';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.querySelector('.duplicates-list')) {
|
||||
new DuplicateManagement();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user