Files
project-managment/templates/projects/partials/board_columns.html
T
sankalp 4c1ebbf357 feat(templates/projects/partials): replace hard-coded status completion check with configurable allowed_move_to_status_ids
Prevents brute-force attacks by only allowing moves to statuses explicitly permitted.
2026-06-28 19:35:54 +05:45

97 lines
6.1 KiB
HTML

<div id="board-container">
<div class="mb-4 flex items-center justify-between flex-wrap gap-3">
<div class="flex items-center gap-3">
<h1 class="text-xl font-bold">{{ project.name }}</h1>
<span class="dk-badge text-white text-xs font-mono" style="background-color:{{ project.color|default:'#3B82F6' }}">{{ project.ticket_prefix }}</span>
</div>
<div class="flex items-center gap-2">
<select x-model="sort" @change="changeSort($el.value)" class="text-sm border border-gray-200 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-950">
<option value="created_at_desc">Newest First</option>
<option value="created_at_asc">Oldest First</option>
<option value="name_asc">Name A-Z</option>
<option value="name_desc">Name Z-A</option>
<option value="due_date_asc">Due Date</option>
<option value="priority">Priority</option>
</select>
<a href="/dashboard/tickets/create/?project={{ project.id }}" class="dk-btn-primary text-sm"><i class="fas fa-plus"></i> New Ticket</a>
</div>
</div>
<div class="flex gap-4 overflow-x-auto pb-4" style="min-height: 700px;">
{% for col in columns %}
<div class="status-column shrink-0 w-[280px] max-w-[350px] bg-gray-50 dark:bg-gray-950/50 rounded-xl border border-gray-200 dark:border-gray-600 flex flex-col" data-status-id="{{ col.status.id }}" data-status-completed="{{ col.status.is_completed|lower }}"
@dragover.prevent="$el.classList.add('ring-2', 'ring-blue-500')"
@dragleave="$el.classList.remove('ring-2', 'ring-blue-500')"
@drop.prevent="
$el.classList.remove('ring-2', 'ring-blue-500');
if (!dragging) return;
{% if allowed_move_to_status_ids is not None %}
if (![{{ allowed_move_to_status_ids|join:"," }}].includes(parseInt($el.dataset.statusId))) return;
{% endif %}
const tid = dragging;
dragging = null;
const csrf = (document.cookie.split('; ').find(c => c.startsWith('csrftoken')) || '').split('=')[1];
fetch('/api/tickets/' + tid + '/move/', {
method: 'PATCH',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrf},
body: JSON.stringify({status_id: $el.dataset.statusId})
}).then(r => {
if (r.ok) {
const s = document.querySelector('[x-model=sort]');
htmx.ajax('GET', '/dashboard/board/{{ project.id }}/data/?sort=' + (s ? s.value : 'created_at_desc'), {target: '#board-container', swap: 'outerHTML'});
}
});
">
<div class="px-3 py-3 flex items-center justify-between border-b border-gray-200 dark:border-gray-600">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full" style="background-color:{{ col.status.color }}"></div>
<span class="font-semibold text-sm">{{ col.status.name }}</span>
{% if col.status.is_completed %}<i class="fas fa-check-circle text-emerald-500 text-xs"></i>{% endif %}
<span class="dk-badge bg-gray-200 dark:bg-gray-800 text-xs">{{ col.count }}</span>
<button @click="openNewTicket({{ col.status.id }})" class="text-gray-400 hover:text-blue-500 text-xs ml-1" title="Add ticket"><i class="fas fa-plus"></i></button>
</div>
</div>
<div class="flex-1 overflow-y-auto p-2 space-y-2" style="max-height: 650px;">
{% for ticket in col.tickets %}
<div class="ticket-card dk-card p-3 hover:shadow-md transition-shadow cursor-grab active:cursor-grabbing" data-ticket-id="{{ ticket.id }}" draggable="true"
@dragstart="dragging = $el.dataset.ticketId; $el.classList.add('opacity-50'); $event.dataTransfer.effectAllowed = 'move'; $event.dataTransfer.setData('text/plain', $el.dataset.ticketId)"
@dragend="$el.classList.remove('opacity-50'); dragging = null">
<div class="flex items-center justify-between mb-1.5">
<span class="text-xs font-mono text-gray-400">{{ ticket.uuid }}</span>
{% if ticket.priority %}
<span class="dk-badge text-xs text-white" style="background-color:{{ ticket.priority.color|default:'#6B7280' }}">{{ ticket.priority.name }}</span>
{% endif %}
</div>
<h4 class="text-sm font-medium mb-1 leading-snug">{{ ticket.name }}</h4>
{% if ticket.description %}
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2 line-clamp-2">{{ ticket.description|striptags|truncatechars:100 }}</p>
{% endif %}
<div class="flex items-center justify-between mt-2">
<div class="flex -space-x-1.5">
{% for assignee in ticket.assignees.all|slice:":3" %}
<div title="{{ assignee.get_full_name|default:assignee.username }}" class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-700 flex items-center justify-center text-[10px] font-bold text-white" style="background-color:{% cycle '#3B82F6' '#10B981' '#F59E0B' '#EF4444' '#8B5CF6' %}">
{{ assignee.get_full_name|default:assignee.username|make_list|first|upper }}
</div>
{% endfor %}
{% if ticket.assignees.count > 3 %}
<div title="View all assignees" class="w-6 h-6 rounded-full border-2 border-white dark:border-gray-700 bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-[10px] font-bold text-gray-600 dark:text-gray-300">+{{ ticket.assignees.count|add:"-3" }}</div>
{% endif %}
</div>
{% if ticket.due_date %}
<span class="text-xs {% if ticket.due_date < today %}text-red-500{% else %}text-gray-400{% endif %}">
<i class="far fa-calendar mr-0.5"></i>{{ ticket.due_date|date:"M d" }}
</span>
{% endif %}
</div>
</div>
{% empty %}
<div class="text-center py-8 text-gray-400 border-2 border-dashed border-gray-200 dark:border-gray-600 rounded-lg">
<p class="text-xs">No tickets</p>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>