Add tags support for domains with filtering and bulk actions

Introduces a 'tags' field to the domains table and UI, allowing users to organize domains with custom tags. Adds tag input and display to create, edit, bulk-add, and view pages, as well as tag-based filtering and bulk tag management (add/remove) in the domain list. Updates backend validation, controller logic, and migrations to support tags, including a new migration and index for efficient tag searches.
This commit is contained in:
Hosteroid
2025-10-12 12:46:16 +03:00
parent 823248f025
commit df2942b356
13 changed files with 868 additions and 16 deletions

View File

@@ -37,6 +37,57 @@ ob_start();
</p>
</div>
<!-- Tags -->
<div>
<label for="tags-input" class="block text-sm font-medium text-gray-700 mb-1.5">
Tags
<span class="text-gray-400 font-normal">(Optional)</span>
</label>
<!-- Tag Display Area -->
<div id="tags-display" class="min-h-[40px] p-2 border border-gray-300 rounded-lg mb-2 flex flex-wrap gap-1.5 bg-gray-50"></div>
<!-- Tag Input -->
<div class="relative">
<i class="fas fa-tag absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 text-sm"></i>
<input type="text"
id="tags-input"
class="w-full pl-10 pr-20 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
placeholder="Type any tag and press Enter or comma..."
onkeydown="handleTagInput(event)">
<button type="button" onclick="addTagFromInput()" class="absolute right-2 top-1/2 transform -translate-y-1/2 px-3 py-1 bg-primary text-white text-xs rounded hover:bg-primary-dark">
Add
</button>
</div>
<!-- Hidden input to store tags for form submission -->
<input type="hidden" id="tags" name="tags" value="">
<p class="mt-1.5 text-xs text-gray-500">
<i class="fas fa-info-circle mr-1"></i>
All imported domains will be tagged with these tags. Type any custom tag or use suggestions below.
</p>
<!-- Suggested Tags -->
<div class="mt-2">
<p class="text-xs text-gray-600 mb-1.5">💡 Suggestions:</p>
<div class="flex flex-wrap gap-1.5">
<button type="button" onclick="addTag('production')" class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border bg-green-50 text-green-700 border-green-200 hover:bg-green-100 transition-colors">
<i class="fas fa-plus mr-1" style="font-size: 8px;"></i>
Production
</button>
<button type="button" onclick="addTag('staging')" class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border bg-yellow-50 text-yellow-700 border-yellow-200 hover:bg-yellow-100 transition-colors">
<i class="fas fa-plus mr-1" style="font-size: 8px;"></i>
Staging
</button>
<button type="button" onclick="addTag('client')" class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border bg-purple-50 text-purple-700 border-purple-200 hover:bg-purple-100 transition-colors">
<i class="fas fa-plus mr-1" style="font-size: 8px;"></i>
Client
</button>
</div>
</div>
</div>
<!-- Notification Group -->
<div>
<label for="notification_group_id" class="block text-sm font-medium text-gray-700 mb-1.5">
@@ -122,6 +173,96 @@ ob_start();
</div>
</div>
<script>
let tags = [];
const tagColors = {
'production': 'bg-green-100 text-green-700 border-green-300',
'staging': 'bg-yellow-100 text-yellow-700 border-yellow-300',
'development': 'bg-blue-100 text-blue-700 border-blue-300',
'client': 'bg-purple-100 text-purple-700 border-purple-300',
'personal': 'bg-orange-100 text-orange-700 border-orange-300',
'archived': 'bg-gray-100 text-gray-700 border-gray-300'
};
function addTag(tagName) {
tagName = tagName.trim().toLowerCase();
// Validate tag (alphanumeric and hyphens only)
if (!/^[a-z0-9-]+$/.test(tagName)) {
return;
}
// Check if tag already exists
if (tags.includes(tagName)) {
return;
}
tags.push(tagName);
updateTagsDisplay();
updateHiddenInput();
// Clear input
document.getElementById('tags-input').value = '';
}
function removeTag(tagName) {
tags = tags.filter(t => t !== tagName);
updateTagsDisplay();
updateHiddenInput();
}
function updateTagsDisplay() {
const display = document.getElementById('tags-display');
display.innerHTML = '';
if (tags.length === 0) {
display.innerHTML = '<span class="text-xs text-gray-400 italic">No tags added yet</span>';
return;
}
tags.forEach(tag => {
const colorClass = tagColors[tag] || 'bg-gray-100 text-gray-700 border-gray-300';
const tagElement = document.createElement('span');
tagElement.className = `inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium border ${colorClass}`;
tagElement.innerHTML = `
<i class="fas fa-tag mr-1" style="font-size: 9px;"></i>
${tag}
<button type="button" onclick="removeTag('${tag}')" class="ml-1.5 hover:text-red-600">
<i class="fas fa-times" style="font-size: 9px;"></i>
</button>
`;
display.appendChild(tagElement);
});
}
function updateHiddenInput() {
document.getElementById('tags').value = tags.join(',');
}
function handleTagInput(event) {
if (event.key === 'Enter' || event.key === ',') {
event.preventDefault();
addTagFromInput();
}
}
function addTagFromInput() {
const input = document.getElementById('tags-input');
const value = input.value.trim();
if (value) {
// Handle multiple tags separated by commas
const newTags = value.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
newTags.forEach(tag => addTag(tag));
input.value = '';
}
}
// Initialize display
updateTagsDisplay();
</script>
<?php
$content = ob_get_clean();
include __DIR__ . '/../layout/base.php';