feat(templates/projects): add automation UI and modal for board automation rules
Enable management of automation rules with UI buttons, modal, and status condition handling
This commit is contained in:
3 files changed
+184
-2
No files matched your search
@@ -36,6 +36,7 @@
|
|||||||
filterOpen: false,
|
filterOpen: false,
|
||||||
manageStatusesOpen: false,
|
manageStatusesOpen: false,
|
||||||
permissionsOpen: false,
|
permissionsOpen: false,
|
||||||
|
automationOpen: false,
|
||||||
allowedMoveToStatusIds: {{ allowed_move_to_status_ids }},
|
allowedMoveToStatusIds: {{ allowed_move_to_status_ids }},
|
||||||
|
|
||||||
createStatusName: '',
|
createStatusName: '',
|
||||||
@@ -322,6 +323,9 @@ class="space-y-6"
|
|||||||
<button @click="permissionsOpen = true" class="dk-btn dk-btn-ghost text-sm text-orange-600">
|
<button @click="permissionsOpen = true" class="dk-btn dk-btn-ghost text-sm text-orange-600">
|
||||||
<i class="fas fa-shield-halved"></i> Permissions
|
<i class="fas fa-shield-halved"></i> Permissions
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="automationOpen = true" class="dk-btn dk-btn-ghost text-sm text-cyan-600">
|
||||||
|
<i class="fas fa-robot"></i> Automation
|
||||||
|
</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>
|
||||||
@@ -859,6 +863,8 @@ class="space-y-6"
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include "projects/partials/board_automation_modal.html" %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -24,8 +24,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="{% url 'projects-list' %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-arrow-left mr-1"></i> Back</a>
|
<a href="{% url 'projects-list' %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-arrow-left mr-1"></i> Back</a>
|
||||||
<a href="{% url 'project-board-project' project.id %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-columns"></i> Project Board</a>
|
<a href="{% url 'project-board-project' project.id %}" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-columns"></i> Board</a>
|
||||||
<button type="button" @click="$store.projectDetail.externalModalOpen = true" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-globe"></i> External Dashboard</button>
|
<button @click="window.location.reload()" class="dk-btn dk-btn-ghost text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<i class="fas fa-sync"></i> Refresh
|
||||||
|
</button>
|
||||||
|
<button @click="$store.projectDetail.externalModalOpen = true" class="dk-btn dk-btn-ghost text-sm"><i class="fas fa-globe"></i> External</button>
|
||||||
|
{% if is_admin %}
|
||||||
|
<button @click="$store.projectDetail.permissionsOpen = true" class="dk-btn dk-btn-ghost text-sm text-orange-600">
|
||||||
|
<i class="fas fa-shield-halved"></i> Permissions
|
||||||
|
</button>
|
||||||
|
<button @click="$store.projectDetail.automationOpen = true" class="dk-btn dk-btn-ghost text-sm text-cyan-600">
|
||||||
|
<i class="fas fa-robot"></i> Automation
|
||||||
|
</button>
|
||||||
|
<button @click="$store.projectDetail.manageStatusesOpen = true" class="dk-btn dk-btn-ghost text-sm text-purple-600">
|
||||||
|
<i class="fas fa-cog"></i> Statuses
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'project-edit' project.id %}" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-pen"></i> Edit</a>
|
<a href="{% url 'project-edit' project.id %}" class="dk-btn dk-btn-primary text-sm"><i class="fas fa-pen"></i> Edit</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -796,6 +810,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include "projects/partials/board_automation_modal.html" with selected_project=project %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
@@ -811,6 +827,7 @@
|
|||||||
viewNoteData: { id: '', title: '', content: '' },
|
viewNoteData: { id: '', title: '', content: '' },
|
||||||
manageStatusesOpen: false,
|
manageStatusesOpen: false,
|
||||||
permissionsOpen: false,
|
permissionsOpen: false,
|
||||||
|
automationOpen: false,
|
||||||
createStatusName: '',
|
createStatusName: '',
|
||||||
createStatusColor: '#6B7280',
|
createStatusColor: '#6B7280',
|
||||||
createStatusOrder: '0',
|
createStatusOrder: '0',
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
{% load static %}
|
||||||
|
<!-- Automation Rules Modal -->
|
||||||
|
<div x-cloak x-show="automationOpen" class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
||||||
|
@keydown.escape.window="automationOpen = false">
|
||||||
|
<div class="fixed inset-0 bg-black/50" @click="automationOpen = false"></div>
|
||||||
|
<div class="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto 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">Automation Rules</h3>
|
||||||
|
<button @click="automationOpen = false" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||||
|
<i class="fas fa-times text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Rule Form -->
|
||||||
|
<form method="POST" action="{% url 'board-automation-rules' selected_project.id %}"
|
||||||
|
x-data="{ actionRows: [] }"
|
||||||
|
class="mb-6 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Rule Name</label>
|
||||||
|
<input type="text" name="name" required
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-500 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
|
||||||
|
placeholder="e.g. Assign QA on Done">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Trigger Event</label>
|
||||||
|
<select name="trigger_event"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-500 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-cyan-500 focus:border-transparent">
|
||||||
|
<option value="status_changed">Status becomes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Trigger Status</label>
|
||||||
|
<select name="trigger_status"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-500 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-cyan-500 focus:border-transparent">
|
||||||
|
<option value="">--- Any Status ---</option>
|
||||||
|
{% for col in columns %}
|
||||||
|
<option value="{{ col.status.id }}">{{ col.status.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions Builder -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Actions</label>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<template x-if="actionRows.length === 0">
|
||||||
|
<p class="text-sm text-gray-400 italic">No actions configured. Click "Add Action" to begin.</p>
|
||||||
|
</template>
|
||||||
|
<template x-for="(row, idx) in actionRows" :key="idx">
|
||||||
|
<div class="flex items-center gap-2 p-2 bg-white dark:bg-gray-700 rounded border border-gray-200 dark:border-gray-500">
|
||||||
|
<select x-model="row.type"
|
||||||
|
class="flex-1 min-w-[130px] px-2 py-1.5 border border-gray-300 dark:border-gray-500 rounded text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
|
||||||
|
<option value="assign_user">Assign user</option>
|
||||||
|
<option value="change_status">Change status</option>
|
||||||
|
<option value="notify_in_app">Notify in-app</option>
|
||||||
|
<option value="send_email">Send email</option>
|
||||||
|
</select>
|
||||||
|
<input x-show="row.type === 'assign_user'" x-model="row.user_id" type="number" placeholder="User ID"
|
||||||
|
class="w-24 px-2 py-1.5 border border-gray-300 dark:border-gray-500 rounded text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
|
||||||
|
<select x-show="row.type === 'change_status'" x-model="row.status_id"
|
||||||
|
class="w-40 px-2 py-1.5 border border-gray-300 dark:border-gray-500 rounded text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
|
||||||
|
<option value="">--- Status ---</option>
|
||||||
|
{% for col in columns %}
|
||||||
|
<option value="{{ col.status.id }}">{{ col.status.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div x-show="row.type === 'send_email' || row.type === 'notify_in_app'" class="flex gap-2 flex-1">
|
||||||
|
<input x-model="row.title" type="text" placeholder="Title"
|
||||||
|
class="flex-1 px-2 py-1.5 border border-gray-300 dark:border-gray-500 rounded text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
|
||||||
|
<input x-model="row.message" type="text" placeholder="Message"
|
||||||
|
class="flex-1 px-2 py-1.5 border border-gray-300 dark:border-gray-500 rounded text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
|
||||||
|
</div>
|
||||||
|
<button @click="actionRows.splice(idx, 1)" type="button" class="text-red-500 hover:text-red-700 px-1 flex-shrink-0">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<button @click="actionRows.push({type: 'assign_user', user_id: '', status_id: '', title: '', message: ''})" type="button"
|
||||||
|
class="mt-2 text-sm text-cyan-600 hover:text-cyan-800 font-medium">
|
||||||
|
<i class="fas fa-plus"></i> Add Action
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="actions" x-bind:value="JSON.stringify(actionRows)">
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-cyan-600 text-white text-sm font-medium rounded-lg hover:bg-cyan-700 transition-colors">
|
||||||
|
<i class="fas fa-plus"></i> Add Rule
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Rules List -->
|
||||||
|
{% if automation_rules %}
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-gray-600">
|
||||||
|
<th class="text-left py-2 px-3 font-medium text-gray-700 dark:text-gray-300">Name</th>
|
||||||
|
<th class="text-left py-2 px-3 font-medium text-gray-700 dark:text-gray-300">Trigger</th>
|
||||||
|
<th class="text-left py-2 px-3 font-medium text-gray-700 dark:text-gray-300">Actions</th>
|
||||||
|
<th class="text-center py-2 px-3 font-medium text-gray-700 dark:text-gray-300">Active</th>
|
||||||
|
<th class="text-right py-2 px-3 font-medium text-gray-700 dark:text-gray-300">Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rule in automation_rules %}
|
||||||
|
<tr class="border-b border-gray-100 dark:border-gray-700">
|
||||||
|
<td class="py-2 px-3 text-gray-900 dark:text-gray-100 font-medium">{{ rule.name }}</td>
|
||||||
|
<td class="py-2 px-3 text-gray-600 dark:text-gray-400">
|
||||||
|
When status becomes
|
||||||
|
{% if rule.trigger_status %}
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||||
|
style="background-color: {{ rule.trigger_status.color }}22; color: {{ rule.trigger_status.color }};">
|
||||||
|
{{ rule.trigger_status.name }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-gray-400 italic">any</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3">
|
||||||
|
{% for action in rule.actions %}
|
||||||
|
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 mr-1 mb-1">
|
||||||
|
{% if action.type == 'assign_user' %}<i class="fas fa-user-plus"></i> Assign{% elif action.type == 'change_status' %}<i class="fas fa-arrow-right"></i> Change status{% elif action.type == 'notify_in_app' %}<i class="fas fa-bell"></i> Notify{% elif action.type == 'send_email' %}<i class="fas fa-envelope"></i> Email{% else %}{{ action.type }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% empty %}
|
||||||
|
<span class="text-xs text-gray-400 italic">none</span>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3 text-center">
|
||||||
|
<form method="POST" action="{% url 'board-automation-rule-toggle' selected_project.id rule.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors cursor-pointer {% if rule.is_active %}bg-emerald-500{% else %}bg-gray-300 dark:bg-gray-600{% endif %}">
|
||||||
|
<span class="inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform {% if rule.is_active %}translate-x-[18px]{% else %}translate-x-1{% endif %}"></span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 px-3 text-right">
|
||||||
|
<form method="POST" action="{% url 'board-automation-rule-delete' selected_project.id rule.id %}" onsubmit="return confirm('Delete rule "{{ rule.name }}"?')">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="text-red-500 hover:text-red-700 text-sm cursor-pointer">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-sm text-gray-400 italic text-center py-8">No automation rules configured yet. Add one above.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in new issue
Block a user