Complete replacement of Thing types with tag system
This commit is contained in:
@@ -37,7 +37,6 @@
|
||||
<tr style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
|
||||
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Picture</th>
|
||||
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Name</th>
|
||||
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Type</th>
|
||||
<th style="padding: 15px 20px; text-align: left; font-weight: 600;">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -56,7 +55,6 @@
|
||||
<td style="padding: 15px 20px;">
|
||||
<a href="{% url 'thing_detail' thing.id %}" style="color: #667eea; text-decoration: none; font-weight: 500;">{{ thing.name }}</a>
|
||||
</td>
|
||||
<td style="padding: 15px 20px; color: #555;">{{ thing.thing_type.name }}</td>
|
||||
<td style="padding: 15px 20px; color: #777;">{{ thing.description|default:"-" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% load mptt_tags %}
|
||||
{% load dict_extras %}
|
||||
|
||||
{% block title %}LabHelper - Home{% endblock %}
|
||||
|
||||
@@ -44,36 +42,35 @@
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2><i class="fas fa-folder-tree"></i> Thing Types</h2>
|
||||
{% if thing_types %}
|
||||
<ul class="tree" style="list-style: none; padding-left: 0;">
|
||||
{% recursetree thing_types %}
|
||||
<li style="padding: 8px 0;">
|
||||
<div class="tree-item" style="display: flex; align-items: center; gap: 8px;">
|
||||
{% if children %}
|
||||
<span class="toggle-handle" style="display: inline-block; width: 24px; color: #667eea; font-weight: bold; cursor: pointer; transition: transform 0.2s;">[+]</span>
|
||||
{% else %}
|
||||
<span class="toggle-handle" style="display: inline-block; width: 24px; color: #ccc;"> </span>
|
||||
{% endif %}
|
||||
<a href="{% url 'thing_type_detail' node.pk %}" style="color: #667eea; text-decoration: none; font-size: 16px; font-weight: 500; transition: color 0.2s;">{{ node.name }}</a>
|
||||
{% with count=type_counts|get_item:node.pk %}
|
||||
{% if count and count > 0 %}
|
||||
<span style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600;">{{ count }}</span>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<h2><i class="fas fa-tags"></i> Tags</h2>
|
||||
{% if facet_tag_counts %}
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;">
|
||||
{% for facet, tags_with_counts in facet_tag_counts.items %}
|
||||
<div class="facet-card" style="background: white; border-radius: 12px; border: 1px solid #e0e0e0; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05);">
|
||||
<div class="facet-header" style="padding: 15px 20px; background: linear-gradient(135deg, {{ facet.color }} 0%, {{ facet.color }}dd 100%); color: white; display: flex; align-items: center; justify-content: space-between; cursor: pointer;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<i class="fas fa-chevron-right facet-toggle" style="transition: transform 0.3s;"></i>
|
||||
<span style="font-size: 18px; font-weight: 700;">{{ facet.name }}</span>
|
||||
</div>
|
||||
<span style="background: rgba(255,255,255,0.3); padding: 4px 12px; border-radius: 20px; font-size: 13px; font-weight: 600;">{{ facet.cardinality }}</span>
|
||||
</div>
|
||||
{% if children %}
|
||||
<ul style="list-style: none; padding-left: 32px; display: none;">
|
||||
{{ children }}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endrecursetree %}
|
||||
</ul>
|
||||
<div class="facet-tags" style="padding: 15px 20px; display: none;">
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
{% for tag, count in tags_with_counts %}
|
||||
<a href="/search/?q={{ facet.name }}:{{ tag.name }}" style="display: inline-block; padding: 6px 12px; background: {{ facet.color }}20; color: {{ facet.color }}; border: 1px solid {{ facet.color }}; border-radius: 15px; text-decoration: none; font-size: 14px; font-weight: 600; transition: all 0.2s;">
|
||||
{{ tag.name }}
|
||||
<span style="background: {{ facet.color }}; color: white; padding: 1px 8px; border-radius: 10px; margin-left: 6px; font-size: 12px;">{{ count }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="text-align: center; color: #888; font-size: 16px; padding: 40px;">
|
||||
<i class="fas fa-folder-open" style="font-size: 48px; margin-bottom: 15px; display: block;"></i>
|
||||
No thing types found.
|
||||
<i class="fas fa-tag" style="font-size: 48px; margin-bottom: 15px; display: block;"></i>
|
||||
No tags found.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -82,15 +79,27 @@
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.toggle-handle').click(function(e) {
|
||||
e.stopPropagation();
|
||||
var $ul = $(this).closest('li').children('ul');
|
||||
if ($ul.length) {
|
||||
$ul.slideToggle(200);
|
||||
$(this).text($ul.is(':visible') ? '[-]' : '[+]');
|
||||
$('.facet-header').click(function() {
|
||||
const $content = $(this).next('.facet-tags');
|
||||
const $icon = $(this).find('.facet-toggle');
|
||||
|
||||
$content.slideToggle(200);
|
||||
if ($content.is(':visible')) {
|
||||
$icon.css('transform', 'rotate(90deg)');
|
||||
} else {
|
||||
$icon.css('transform', 'rotate(0deg)');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$('.facet-card a').hover(
|
||||
function() {
|
||||
$(this).css('transform', 'scale(1.05)');
|
||||
},
|
||||
function() {
|
||||
$(this).css('transform', 'scale(1)');
|
||||
}
|
||||
);
|
||||
|
||||
$('.box-card').hover(
|
||||
function() {
|
||||
$(this).css('transform', 'translateY(-5px)');
|
||||
@@ -103,4 +112,4 @@ $(document).ready(function() {
|
||||
);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="section">
|
||||
<input type="text"
|
||||
id="search-input"
|
||||
placeholder="Search for things..."
|
||||
style="width: 100%; padding: 16px 20px; font-size: 18px; border: 2px solid #e0e0e0; border-radius: 12px; box-sizing: border-box; transition: all 0.3s;">
|
||||
<input type="text"
|
||||
id="search-input"
|
||||
placeholder="Search for things..."
|
||||
style="width: 100%; padding: 16px 20px; font-size: 18px; border: 2px solid #e0e0e0; border-radius: 12px; box-sizing: border-box; transition: all 0.3s;"
|
||||
{% if request.GET.q %}value="{{ request.GET.q }}"{% endif %}>
|
||||
<p style="color: #888; font-size: 14px; margin-top: 10px;">
|
||||
<i class="fas fa-info-circle"></i> Type at least 2 characters to search
|
||||
</p>
|
||||
@@ -55,58 +56,62 @@ const noResults = document.getElementById('no-results');
|
||||
|
||||
let searchTimeout = null;
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const query = this.value.trim();
|
||||
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
function performSearch(query) {
|
||||
if (query.length < 2) {
|
||||
resultsContainer.style.display = 'none';
|
||||
noResults.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
searchInput.style.borderColor = '#667eea';
|
||||
searchInput.style.boxShadow = '0 0 0 3px rgba(102, 126, 234, 0.1)';
|
||||
|
||||
|
||||
searchTimeout = setTimeout(function() {
|
||||
fetch('/search/api/?q=' + encodeURIComponent(query))
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
resultsBody.innerHTML = '';
|
||||
|
||||
|
||||
if (data.results.length === 0) {
|
||||
resultsContainer.style.display = 'none';
|
||||
noResults.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
noResults.style.display = 'none';
|
||||
resultsContainer.style.display = 'block';
|
||||
|
||||
|
||||
data.results.forEach(function(thing) {
|
||||
const row = document.createElement('tr');
|
||||
row.style.borderBottom = '1px solid #e0e0e0';
|
||||
row.style.transition = 'background 0.2s';
|
||||
row.innerHTML =
|
||||
row.innerHTML =
|
||||
'<td style="padding: 15px 20px;"><a href="/thing/' + thing.id + '/">' + escapeHtml(thing.name) + '</a></td>' +
|
||||
'<td style="padding: 15px 20px; color: #555;">' + escapeHtml(thing.type) + '</td>' +
|
||||
'<td style="padding: 15px 20px;"><a href="/box/' + escapeHtml(thing.box) + '/">' + escapeHtml(thing.box) + '</a></td>' +
|
||||
'<td style="padding: 15px 20px; color: #777;" class="description">' + escapeHtml(thing.description) + '</td>';
|
||||
|
||||
|
||||
row.addEventListener('mouseenter', function() {
|
||||
this.style.background = '#f8f9fa';
|
||||
});
|
||||
row.addEventListener('mouseleave', function() {
|
||||
this.style.background = 'white';
|
||||
});
|
||||
|
||||
|
||||
resultsBody.appendChild(row);
|
||||
});
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const query = this.value.trim();
|
||||
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout);
|
||||
}
|
||||
|
||||
performSearch(query);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('blur', function() {
|
||||
@@ -120,6 +125,15 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Check for query parameter on page load
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const initialQuery = urlParams.get('q');
|
||||
|
||||
if (initialQuery) {
|
||||
searchInput.value = initialQuery;
|
||||
performSearch(initialQuery.trim());
|
||||
}
|
||||
|
||||
searchInput.focus();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -53,10 +53,30 @@
|
||||
<div class="thing-details" style="flex-grow: 1; min-width: 300px;">
|
||||
<div class="detail-row" style="margin-bottom: 25px;">
|
||||
<div style="font-size: 14px; color: #888; font-weight: 600; margin-bottom: 8px;">
|
||||
<i class="fas fa-tag"></i> Type
|
||||
<i class="fas fa-tags"></i> Tags
|
||||
</div>
|
||||
<div style="font-size: 18px; color: #333; font-weight: 500;">
|
||||
{{ thing.thing_type.name }}
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
{% regroup thing.tags.all by facet as facet_list %}
|
||||
{% for facet in facet_list %}
|
||||
<div>
|
||||
<div style="font-size: 12px; color: {{ facet.grouper.color }}; font-weight: 700; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
{{ facet.grouper.name }}
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
{% for tag in facet.list %}
|
||||
<form method="post" style="display: inline;" onsubmit="return confirm('Remove tag {{ tag.facet.name }}:{{ tag.name }}?');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="remove_tag">
|
||||
<input type="hidden" name="tag_id" value="{{ tag.id }}">
|
||||
<button type="submit" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; background: {{ facet.grouper.color }}20; color: {{ facet.grouper.color }}; border: 2px solid {{ facet.grouper.color }}; border-radius: 20px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
|
||||
{{ tag.name }}
|
||||
<i class="fas fa-times" style="font-size: 12px;"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -133,6 +153,46 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 style="color: #667eea; font-size: 20px; font-weight: 700; margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px;">
|
||||
<i class="fas fa-plus-circle"></i> Add Tags
|
||||
</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px,1fr)); gap: 30px;">
|
||||
<div>
|
||||
<h3 style="margin: 0 0 15px 0; color: #667eea; font-size: 16px; font-weight: 600;">
|
||||
<i class="fas fa-tag"></i> Add Tag
|
||||
</h3>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="add_tag">
|
||||
<div style="display: flex; flex-direction: column; gap: 15px;">
|
||||
<div>
|
||||
<label for="tag_select" style="font-weight: 600; color: #666; font-size: 14px; margin-bottom: 8px; display: block;">Select Tag</label>
|
||||
<select name="tag_id" id="tag_select" style="width: 100%; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 10px; font-size: 15px; background: white; cursor: pointer; transition: all 0.3s;">
|
||||
<option value="">-- Select a tag --</option>
|
||||
{% for facet in facets %}
|
||||
<optgroup label="{{ facet.name }} ({{ facet.get_cardinality_display }})">
|
||||
{% for tag in facet.tags.all %}
|
||||
{% if tag not in thing.tags.all %}
|
||||
<option value="{{ tag.id }}">
|
||||
{{ tag.name }}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn">
|
||||
<i class="fas fa-plus"></i> Add Tag
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user