feat(templates/projects): add permissions modal and restricted mode logic

Implement permission configuration UI and enforce restricted movement rules
This commit is contained in:
sankalp committed 2026-06-28 19:35:30 +05:45
1 parent 7600b7b773
commit ce0c1349ca
2 files changed
+206 -6

No files matched your search

+107 -6
View File
@@ -33,7 +33,10 @@
newTicketOpen: false, newTicketOpen: false,
exportOpen: false, exportOpen: false,
filterOpen: false, filterOpen: false,
filterOpen: false,
manageStatusesOpen: false, manageStatusesOpen: false,
permissionsOpen: false,
allowedMoveToStatusIds: {{ allowed_move_to_status_ids }},
createStatusName: '', createStatusName: '',
createStatusColor: '#6B7280', createStatusColor: '#6B7280',
@@ -199,8 +202,8 @@
touch.clientY >= rect.top && touch.clientY <= rect.bottom) targetColumn = col; touch.clientY >= rect.top && touch.clientY <= rect.bottom) targetColumn = col;
col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20'); col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20');
}); });
{% if not can_move_to_completed %} {% if can_move %}
if (targetColumn && targetColumn.getAttribute('data-status-completed') === 'true') targetColumn = null; if (targetColumn && targetColumn.getAttribute('data-status-id') && !this.allowedMoveToStatusIds.includes(parseInt(targetColumn.getAttribute('data-status-id')))) targetColumn = null;
{% endif %} {% endif %}
if (targetColumn && targetColumn !== originalColumn) if (targetColumn && targetColumn !== originalColumn)
this.moveTicketToStatus(this.draggingTicket, targetColumn.getAttribute('data-status-id')); this.moveTicketToStatus(this.draggingTicket, targetColumn.getAttribute('data-status-id'));
@@ -224,8 +227,8 @@
col.addEventListener('drop', (e) => { col.addEventListener('drop', (e) => {
e.preventDefault(); e.preventDefault();
col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20'); col.classList.remove('bg-blue-50', 'dark:bg-blue-900/20');
{% if not can_move_to_completed %} {% if can_move %}
if (col.getAttribute('data-status-completed') === 'true') return; if (col.getAttribute('data-status-id') && !this.allowedMoveToStatusIds.includes(parseInt(col.getAttribute('data-status-id')))) return;
{% endif %} {% endif %}
if (this.draggingTicket) { if (this.draggingTicket) {
this.moveTicketToStatus(this.draggingTicket, col.getAttribute('data-status-id')); this.moveTicketToStatus(this.draggingTicket, col.getAttribute('data-status-id'));
@@ -316,6 +319,9 @@ class="space-y-6"
<i class="fas fa-filter"></i> Filter {% if selected_user_ids %}<span class="ml-1 bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded">{{ selected_user_ids|length }}</span>{% endif %} <i class="fas fa-filter"></i> Filter {% if selected_user_ids %}<span class="ml-1 bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded">{{ selected_user_ids|length }}</span>{% endif %}
</button> </button>
{% if is_admin %} {% if is_admin %}
<button @click="permissionsOpen = true" class="dk-btn dk-btn-ghost text-sm text-orange-600">
<i class="fas fa-shield-halved"></i> Permissions
</button>
<button @click="manageStatusesOpen = true" class="dk-btn dk-btn-ghost text-sm text-purple-600"> <button @click="manageStatusesOpen = true" class="dk-btn dk-btn-ghost text-sm text-purple-600">
<i class="fas fa-cog"></i> Manage Statuses <i class="fas fa-cog"></i> Manage Statuses
</button> </button>
@@ -368,10 +374,10 @@ class="space-y-6"
<i class="fas fa-eye"></i><span class="text-sm font-medium">View Only Mode</span><span class="text-xs opacity-75">You can view tickets but cannot move them</span> <i class="fas fa-eye"></i><span class="text-sm font-medium">View Only Mode</span><span class="text-xs opacity-75">You can view tickets but cannot move them</span>
</div> </div>
</div> </div>
{% elif not can_move_to_completed %} {% elif can_move and allowed_move_to_status_ids|length < columns|length %}
<div class="flex justify-center mb-4"> <div class="flex justify-center mb-4">
<div class="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-blue-800 dark:text-blue-200"> <div class="inline-flex items-center gap-2 px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-blue-800 dark:text-blue-200">
<i class="fas fa-info-circle"></i><span class="text-sm font-medium">Restricted Mode</span><span class="text-xs opacity-75">You can move tickets to active statuses only</span> <i class="fas fa-info-circle"></i><span class="text-sm font-medium">Restricted Mode</span><span class="text-xs opacity-75">You can only move tickets to certain statuses</span>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -758,6 +764,101 @@ class="space-y-6"
</div> </div>
</div> </div>
<!-- Board Permissions Modal -->
<div x-cloak x-show="permissionsOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="permissionsOpen = false">
<div class="fixed inset-0 bg-black/50" @click="permissionsOpen = false"></div>
<div class="relative bg-white dark:bg-gray-950 rounded-xl shadow-xl border border-gray-200 dark:border-gray-600 w-full max-w-6xl mx-auto my-auto max-h-[90vh] overflow-y-auto z-10 p-6 md:p-8">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Board Permissions</h3>
<button @click="permissionsOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times text-xl"></i></button>
</div>
<!-- Add Permission Form -->
<div class="rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 mb-4">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">Add / Update Permission</h4>
<form method="post" action="{% url 'board-permissions' selected_project.id %}">
{% csrf_token %}
<div class="grid grid-cols-12 gap-3 items-start">
<div class="col-span-3">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Role</label>
<select name="role" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
<option value="">Select a role</option>
{% for role_key, role_label in role_choices %}
{% if role_key not in admin_roles %}
<option value="{{ role_key }}">{{ role_label }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="col-span-7">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Can move to</label>
<div class="grid grid-cols-3 gap-x-4 gap-y-1.5">
{% for col in columns %}
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="move_to_statuses" value="{{ col.status.id }}"
class="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
<span class="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0" style="background-color:{{ col.status.color }}"></span>
<span class="text-xs text-gray-700 dark:text-gray-300 truncate">{{ col.status.name }}</span>
</label>
{% endfor %}
</div>
</div>
<div class="col-span-2 pt-5">
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors w-full">Save</button>
</div>
</div>
</form>
</div>
<!-- Permissions List -->
<div class="rounded-xl bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Role</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Can move to</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
{% for perm in permissions %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ perm.get_role_display }}</td>
<td class="px-4 py-3">
<div class="flex flex-wrap gap-1.5">
{% for status in perm.move_to_statuses.all %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium" style="background-color:{{ status.color }}20;color:{{ status.color }}">
<span class="w-1.5 h-1.5 rounded-full" style="background-color:{{ status.color }}"></span>
{{ status.name }}
</span>
{% empty %}
<span class="text-xs text-gray-400 italic">None</span>
{% endfor %}
</div>
</td>
<td class="px-4 py-3 text-right">
<form method="post" action="{% url 'board-permission-delete' selected_project.id perm.id %}" class="inline">
{% csrf_token %}
<button type="button" @click="confirmAction({title:'Delete Permission', message:'Delete permission for <strong>' + '{{ perm.get_role_display|escapejs }}' + '</strong>?', confirmText:'Delete', danger:true, onConfirm:function(){ $el.closest('form').submit() }})" class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors shadow-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No permissions configured. All non-admin roles are restricted by default.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-4"><i class="fas fa-info-circle"></i> Superadmin and manager roles always have full access. Select the statuses a developer role can move tickets to.</p>
</div>
</div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}
+99
View File
@@ -175,6 +175,9 @@
<h3 class="text-base font-semibold text-gray-900 dark:text-white">Ticket Statuses</h3> <h3 class="text-base font-semibold text-gray-900 dark:text-white">Ticket Statuses</h3>
<div class="flex items-center gap-3 flex-wrap"> <div class="flex items-center gap-3 flex-wrap">
{% if is_admin %} {% if is_admin %}
<button type="button" @click="$store.projectDetail.permissionsOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-orange-600 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg hover:bg-orange-100 dark:hover:bg-orange-900/30 transition-colors">
<i class="fas fa-shield-halved"></i> Permissions
</button>
<button type="button" @click="$store.projectDetail.manageStatusesOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-purple-600 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors"> <button type="button" @click="$store.projectDetail.manageStatusesOpen = true" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-purple-600 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors">
<i class="fas fa-cog"></i> Manage <i class="fas fa-cog"></i> Manage
</button> </button>
@@ -698,6 +701,101 @@
</div> </div>
</div> </div>
<!-- Board Permissions Modal -->
<div x-cloak x-show="$store.projectDetail.permissionsOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
@keydown.escape.window="$store.projectDetail.permissionsOpen = false">
<div class="fixed inset-0 bg-black/50" @click="$store.projectDetail.permissionsOpen = false"></div>
<div class="relative bg-white dark:bg-gray-950 rounded-xl shadow-xl border border-gray-200 dark:border-gray-600 w-full max-w-6xl mx-auto my-auto max-h-[90vh] overflow-y-auto z-10 p-6 md:p-8">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Board Permissions</h3>
<button @click="$store.projectDetail.permissionsOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><i class="fas fa-times text-xl"></i></button>
</div>
<!-- Add Permission Form -->
<div class="rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-4 mb-4">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">Add / Update Permission</h4>
<form method="post" action="{% url 'board-permissions' project.id %}">
{% csrf_token %}
<div class="grid grid-cols-12 gap-3 items-start">
<div class="col-span-3">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Role</label>
<select name="role" required
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-950 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none">
<option value="">Select a role</option>
{% for role_key, role_label in role_choices %}
{% if role_key not in admin_roles %}
<option value="{{ role_key }}">{{ role_label }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="col-span-7">
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Can move to</label>
<div class="grid grid-cols-3 gap-x-4 gap-y-1.5">
{% for s in project.ticket_statuses.all %}
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="move_to_statuses" value="{{ s.id }}"
class="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
<span class="w-2.5 h-2.5 rounded-full inline-block flex-shrink-0" style="background-color:{{ s.color }}"></span>
<span class="text-xs text-gray-700 dark:text-gray-300 truncate">{{ s.name }}</span>
</label>
{% endfor %}
</div>
</div>
<div class="col-span-2 pt-5">
<button type="submit" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors w-full">Save</button>
</div>
</div>
</form>
</div>
<!-- Permissions List -->
<div class="rounded-xl bg-white dark:bg-gray-950 border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Role</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Can move to</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
{% for perm in permissions %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30">
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">{{ perm.get_role_display }}</td>
<td class="px-4 py-3">
<div class="flex flex-wrap gap-1.5">
{% for status in perm.move_to_statuses.all %}
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium" style="background-color:{{ status.color }}20;color:{{ status.color }}">
<span class="w-1.5 h-1.5 rounded-full" style="background-color:{{ status.color }}"></span>
{{ status.name }}
</span>
{% empty %}
<span class="text-xs text-gray-400 italic">None</span>
{% endfor %}
</div>
</td>
<td class="px-4 py-3 text-right">
<form method="post" action="{% url 'board-permission-delete' project.id perm.id %}" class="inline">
{% csrf_token %}
<button type="button" @click="confirmAction({title:'Delete Permission', message:'Delete permission for <strong>' + '{{ perm.get_role_display|escapejs }}' + '</strong>?', confirmText:'Delete', danger:true, onConfirm:function(){ $el.closest('form').submit() }})" class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors shadow-sm">
<i class="fas fa-trash"></i>
</button>
</form>
</td>
</tr>
{% empty %}
<tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No permissions configured. All non-admin roles are restricted by default.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-4"><i class="fas fa-info-circle"></i> Superadmin and manager roles always have full access. Select the statuses a developer role can move tickets to.</p>
</div>
</div>
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
@@ -712,6 +810,7 @@
viewNoteOpen: false, viewNoteOpen: false,
viewNoteData: { id: '', title: '', content: '' }, viewNoteData: { id: '', title: '', content: '' },
manageStatusesOpen: false, manageStatusesOpen: false,
permissionsOpen: false,
createStatusName: '', createStatusName: '',
createStatusColor: '#6B7280', createStatusColor: '#6B7280',
createStatusOrder: '0', createStatusOrder: '0',