fastapi-admin-kit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_admin_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
{# pages/users/form.html — create/edit admin user #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{% if user %}Edit User{% else %}Create User{% endif %} — {{ title | default("Admin") }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block breadcrumb %}
|
|
7
|
+
{{ super() }}
|
|
8
|
+
<span class="breadcrumb-separator">/</span>
|
|
9
|
+
<a href="{{ admin_path | default('/admin') }}/users/" class="breadcrumb-segment">Users</a>
|
|
10
|
+
<span class="breadcrumb-separator">/</span>
|
|
11
|
+
<span class="breadcrumb-current">{% if user %}{{ user.email }}{% else %}Create{% endif %}</span>
|
|
12
|
+
{% endblock %}
|
|
13
|
+
|
|
14
|
+
{% block content %}
|
|
15
|
+
<div class="space-y-6">
|
|
16
|
+
<div class="page-header">
|
|
17
|
+
<div class="page-header-left">
|
|
18
|
+
<a href="{{ admin_path | default('/admin') }}/users/" class="page-header-back">
|
|
19
|
+
{{ icon("chevron-left", size="16px") }}
|
|
20
|
+
Users
|
|
21
|
+
</a>
|
|
22
|
+
<h1 class="page-header-title">
|
|
23
|
+
{% if user %}Edit User{% else %}New User{% endif %}
|
|
24
|
+
</h1>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{% if error %}
|
|
29
|
+
<div class="p-4 rounded-lg bg-red-50 border border-red-200">
|
|
30
|
+
<div class="flex items-center gap-2 text-red-700 text-sm">
|
|
31
|
+
{{ icon("exclamation-triangle", size="20px") }}
|
|
32
|
+
{{ error }}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
{% endif %}
|
|
36
|
+
|
|
37
|
+
<form method="post" action="{{ admin_path | default('/admin') }}/users{% if user %}/{{ user.id }}{% else %}/create{% endif %}"
|
|
38
|
+
class="space-y-6">
|
|
39
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
40
|
+
|
|
41
|
+
<div class="fieldset">
|
|
42
|
+
<div class="fieldset__header">
|
|
43
|
+
<h3 class="fieldset__title">User Details</h3>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="fieldset__fields">
|
|
46
|
+
<div class="field-wrapper">
|
|
47
|
+
<label for="email" class="field-label">Email</label>
|
|
48
|
+
<input type="email" id="email" name="email" value="{{ user.email | default('') }}" required
|
|
49
|
+
class="form-input">
|
|
50
|
+
</div>
|
|
51
|
+
<div class="field-wrapper">
|
|
52
|
+
<label for="full_name" class="field-label">Full Name</label>
|
|
53
|
+
<input type="text" id="full_name" name="full_name" value="{{ user.full_name | default('') }}"
|
|
54
|
+
class="form-input">
|
|
55
|
+
</div>
|
|
56
|
+
<div class="field-wrapper">
|
|
57
|
+
<label for="password" class="field-label">
|
|
58
|
+
Password {% if user %}(leave blank to keep current){% endif %}
|
|
59
|
+
</label>
|
|
60
|
+
<input type="password" id="password" name="password" {% if not user %}required{% endif %}
|
|
61
|
+
class="form-input">
|
|
62
|
+
</div>
|
|
63
|
+
<div class="field-wrapper">
|
|
64
|
+
<label for="role_ids" class="field-label">Roles</label>
|
|
65
|
+
{% set selected_role_ids = [] %}
|
|
66
|
+
{% set selected_role_items = [] %}
|
|
67
|
+
{% if user and user.id %}
|
|
68
|
+
{% for r in user.roles %}
|
|
69
|
+
{% if selected_role_ids.append(r.id) %}{% endif %}
|
|
70
|
+
{% if selected_role_items.append({"id": r.id, "label": r.name | string}) %}{% endif %}
|
|
71
|
+
{% endfor %}
|
|
72
|
+
{% endif %}
|
|
73
|
+
<div x-data="multiRelation({{ selected_role_ids | tojson | forceescape }}, '{{ admin_path | default('/admin') }}/users/roles/search', {{ selected_role_items | tojson | forceescape }})">
|
|
74
|
+
<input type="hidden" name="role_ids" :value="JSON.stringify(selectedIds)">
|
|
75
|
+
<div class="flex flex-wrap gap-2" style="margin-bottom: 8px;">
|
|
76
|
+
<template x-for="(item, index) in selectedItems" :key="item.id">
|
|
77
|
+
<span class="tag-chip">
|
|
78
|
+
<span x-text="item.label"></span>
|
|
79
|
+
<button type="button" @click="remove(index)" class="tag-chip__remove">×</button>
|
|
80
|
+
</span>
|
|
81
|
+
</template>
|
|
82
|
+
</div>
|
|
83
|
+
<div style="position: relative;">
|
|
84
|
+
<input type="text" x-model="searchQuery" @input="search()" @focus="open = true" @click.outside="open = false"
|
|
85
|
+
placeholder="Search roles..."
|
|
86
|
+
class="form-input">
|
|
87
|
+
<div x-show="open && results.length > 0"
|
|
88
|
+
style="position: absolute; z-index: 50; width: 100%; margin-top: 4px; background: var(--surface-base); border: 1px solid var(--surface-border); border-radius: var(--radius-md); box-shadow: var(--shadow-md); max-height: 200px; overflow-y: auto;">
|
|
89
|
+
<template x-for="result in results" :key="result.id">
|
|
90
|
+
<div @click="add(result)"
|
|
91
|
+
style="padding: 8px 12px; cursor: pointer; font-size: 0.875rem;"
|
|
92
|
+
onmouseover="this.style.background='var(--surface-hover)'" onmouseout="this.style.background=''">
|
|
93
|
+
<span x-text="result.label"></span>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<p class="field-help" style="color: var(--text-secondary); font-size: 0.75rem; margin-top: 0.25rem;">Search and select roles. Permissions are merged from all selected roles.</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="field-wrapper">
|
|
102
|
+
<label class="field-label">Direct Permissions</label>
|
|
103
|
+
<p class="field-help" style="color: var(--text-secondary); font-size: 0.75rem; margin-bottom: 0.5rem;">
|
|
104
|
+
Override role permissions for specific tables. These are merged with role permissions using OR logic.
|
|
105
|
+
</p>
|
|
106
|
+
{% set user_perm_data = {} %}
|
|
107
|
+
{% if user and user.direct_permissions %}
|
|
108
|
+
{% for p in user.direct_permissions %}
|
|
109
|
+
{% set _ = user_perm_data.update({p.table_name: {"_label": p.table_name, "view": p.can_view, "create": p.can_create, "edit": p.can_edit, "delete": p.can_delete}}) %}
|
|
110
|
+
{% endfor %}
|
|
111
|
+
{% endif %}
|
|
112
|
+
{% set perm_search_url = admin_path | default('/admin') ~ '/tables/search' %}
|
|
113
|
+
<div x-data="permissionWidget('{{ perm_search_url }}', {{ user_perm_data | tojson | forceescape }})">
|
|
114
|
+
<input type="hidden" name="perm_data" :value="serializedPermData">
|
|
115
|
+
|
|
116
|
+
<div class="space-y-3">
|
|
117
|
+
<template x-for="(table, index) in selectedTables" :key="table.id">
|
|
118
|
+
<div class="border border-[var(--color-gray-200)] rounded-lg overflow-hidden">
|
|
119
|
+
<div class="flex items-center justify-between px-4 py-3 bg-[var(--color-gray-50)] cursor-pointer hover:bg-[var(--color-gray-100)]"
|
|
120
|
+
@click="toggleExpand(table.id)">
|
|
121
|
+
<div class="flex items-center gap-3">
|
|
122
|
+
<span class="text-sm font-medium text-[var(--color-gray-900)]" x-text="table.label"></span>
|
|
123
|
+
<span class="text-xs text-[var(--color-gray-500)]" x-text="table.id"></span>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="flex items-center gap-2">
|
|
126
|
+
<span class="text-xs px-2 py-0.5 rounded-full"
|
|
127
|
+
:class="permData[table.id]?.view || permData[table.id]?.create || permData[table.id]?.edit || permData[table.id]?.delete
|
|
128
|
+
? 'bg-green-100 text-green-700'
|
|
129
|
+
: 'bg-[var(--color-gray-100)] text-[var(--color-gray-500)]'"
|
|
130
|
+
x-text="(permData[table.id]?.view ? 'V ' : '') + (permData[table.id]?.create ? 'C ' : '') + (permData[table.id]?.edit ? 'E ' : '') + (permData[table.id]?.delete ? 'D ' : '') || 'None'">
|
|
131
|
+
</span>
|
|
132
|
+
<button type="button" @click.stop="toggleExpand(table.id)"
|
|
133
|
+
class="text-[var(--color-gray-400)] hover:text-[var(--color-gray-600)]">
|
|
134
|
+
<svg class="w-4 h-4 transition-transform" :class="expandedTable === table.id ? 'rotate-180' : ''"
|
|
135
|
+
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
136
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
137
|
+
</svg>
|
|
138
|
+
</button>
|
|
139
|
+
<button type="button" @click.stop="removeTable(index)"
|
|
140
|
+
class="text-[var(--color-gray-400)] hover:text-red-500 ml-1">
|
|
141
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
142
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
143
|
+
</svg>
|
|
144
|
+
</button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div x-show="expandedTable === table.id" x-collapse class="px-4 py-4 bg-white">
|
|
149
|
+
<div class="flex items-center justify-between mb-3">
|
|
150
|
+
<span class="text-xs font-medium text-[var(--color-gray-500)] uppercase tracking-wide">Actions</span>
|
|
151
|
+
<div class="flex gap-2">
|
|
152
|
+
<button type="button" @click="toggleAllActions(table.id, true)"
|
|
153
|
+
class="text-xs text-[var(--color-primary-600)] hover:text-[var(--color-primary-500)]">All</button>
|
|
154
|
+
<span class="text-[var(--color-gray-300)]">|</span>
|
|
155
|
+
<button type="button" @click="toggleAllActions(table.id, false)"
|
|
156
|
+
class="text-xs text-[var(--color-gray-500)] hover:text-[var(--color-gray-700)]">None</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
160
|
+
<template x-for="action in ['view', 'create', 'edit', 'delete']" :key="action">
|
|
161
|
+
<label class="flex items-center gap-2 p-2 rounded border border-[var(--color-gray-200)] cursor-pointer hover:bg-[var(--color-gray-50)]">
|
|
162
|
+
<input type="checkbox"
|
|
163
|
+
:checked="permData[table.id]?.[action]"
|
|
164
|
+
@change="permData[table.id][action] = $event.target.checked"
|
|
165
|
+
class="rounded border-[var(--color-gray-300)] text-[var(--color-primary-600)] focus:ring-[var(--color-primary-500)]">
|
|
166
|
+
<span class="text-sm capitalize" x-text="action"></span>
|
|
167
|
+
</label>
|
|
168
|
+
</template>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="mt-4" style="position: relative;">
|
|
176
|
+
<div class="flex items-center gap-2">
|
|
177
|
+
<div class="flex-1 relative">
|
|
178
|
+
<input type="text" x-model="searchQuery" @input="search()" @focus="open = true" @click.outside="open = false"
|
|
179
|
+
placeholder="Search tables to add..."
|
|
180
|
+
class="form-input w-full">
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div x-show="open && results.length > 0" x-cloak
|
|
184
|
+
style="position: absolute; z-index: 50; width: 100%; margin-top: 4px; background: var(--surface-base); border: 1px solid var(--surface-border); border-radius: var(--radius-md); box-shadow: var(--shadow-md); max-height: 200px; overflow-y: auto;">
|
|
185
|
+
<template x-for="result in results" :key="result.id">
|
|
186
|
+
<div @click="addTable(result)"
|
|
187
|
+
style="padding: 8px 12px; cursor: pointer; font-size: 0.875rem;"
|
|
188
|
+
onmouseover="this.style.background='var(--surface-hover)'" onmouseout="this.style.background=''">
|
|
189
|
+
<span x-text="result.label"></span>
|
|
190
|
+
<span class="text-xs text-[var(--color-gray-400)] ml-2" x-text="result.id"></span>
|
|
191
|
+
</div>
|
|
192
|
+
</template>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="mt-6 flex items-center gap-6">
|
|
200
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
201
|
+
<input type="checkbox" name="is_superuser" value="on"
|
|
202
|
+
{% if user and user.is_superuser %}checked{% endif %}
|
|
203
|
+
class="matrix-checkbox">
|
|
204
|
+
<span class="text-sm" style="color: var(--text-primary);">Superuser</span>
|
|
205
|
+
</label>
|
|
206
|
+
{% if user %}
|
|
207
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
208
|
+
<input type="checkbox" name="is_active" value="on"
|
|
209
|
+
{% if user.is_active %}checked{% endif %}
|
|
210
|
+
class="matrix-checkbox">
|
|
211
|
+
<span class="text-sm" style="color: var(--text-primary);">Active</span>
|
|
212
|
+
</label>
|
|
213
|
+
{% endif %}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div class="sticky bottom-0 flex items-center justify-end gap-3 p-4" style="background: var(--surface-base); border: 1px solid var(--surface-border); border-radius: var(--radius-lg);">
|
|
218
|
+
<a href="{{ admin_path | default('/admin') }}/users/"
|
|
219
|
+
class="btn btn-secondary">
|
|
220
|
+
Cancel
|
|
221
|
+
</a>
|
|
222
|
+
<button type="submit"
|
|
223
|
+
class="btn btn-primary">
|
|
224
|
+
{% if user %}Save Changes{% else %}Create User{% endif %}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
</form>
|
|
228
|
+
</div>
|
|
229
|
+
{% endblock %}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{# pages/users/list.html — admin user management list #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}Users — {{ title | default("Admin") }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block breadcrumb %}
|
|
7
|
+
{{ super() }}
|
|
8
|
+
<span class="text-[var(--color-gray-400)]">/</span>
|
|
9
|
+
<span class="text-[var(--color-gray-900)] font-medium">Users</span>
|
|
10
|
+
{% endblock %}
|
|
11
|
+
|
|
12
|
+
{% block content %}
|
|
13
|
+
<div class="space-y-6">
|
|
14
|
+
<div class="page-header">
|
|
15
|
+
<div class="page-header-left">
|
|
16
|
+
<h1 class="page-header-title">Admin Users</h1>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="page-header-right">
|
|
19
|
+
<a href="{{ admin_path | default('/admin') }}/users/create"
|
|
20
|
+
class="btn-primary px-4 py-2 rounded-lg bg-[var(--color-primary-600)] text-white text-sm font-medium hover:bg-[var(--color-primary-500)] transition-colors">
|
|
21
|
+
Add User
|
|
22
|
+
</a>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)] overflow-hidden">
|
|
27
|
+
<table class="w-full text-sm">
|
|
28
|
+
<thead>
|
|
29
|
+
<tr class="border-b border-[var(--color-gray-200)]">
|
|
30
|
+
<th class="px-6 py-3 text-left font-medium text-[var(--color-gray-500)]">Email</th>
|
|
31
|
+
<th class="px-6 py-3 text-left font-medium text-[var(--color-gray-500)]">Full Name</th>
|
|
32
|
+
<th class="px-6 py-3 text-left font-medium text-[var(--color-gray-500)]">Roles</th>
|
|
33
|
+
<th class="px-6 py-3 text-center font-medium text-[var(--color-gray-500)]">Superuser</th>
|
|
34
|
+
<th class="px-6 py-3 text-center font-medium text-[var(--color-gray-500)]">Active</th>
|
|
35
|
+
<th class="px-6 py-3 text-left font-medium text-[var(--color-gray-500)]">Last Login</th>
|
|
36
|
+
<th class="px-6 py-3 text-right font-medium text-[var(--color-gray-500)]">Actions</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody class="divide-y divide-[var(--color-gray-100)]">
|
|
40
|
+
{% for u in users %}
|
|
41
|
+
<tr class="hover:bg-[var(--color-gray-50)] transition-colors">
|
|
42
|
+
<td class="px-6 py-4 font-medium text-[var(--color-gray-900)]">{{ u.email }}</td>
|
|
43
|
+
<td class="px-6 py-4 text-[var(--color-gray-600)]">{{ u.full_name or '—' }}</td>
|
|
44
|
+
<td class="px-6 py-4">
|
|
45
|
+
<div class="flex flex-wrap gap-1">
|
|
46
|
+
{% for r in u.roles %}
|
|
47
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-[var(--color-primary-50)] text-[var(--color-primary-700)]">
|
|
48
|
+
{{ r.name }}
|
|
49
|
+
</span>
|
|
50
|
+
{% endfor %}
|
|
51
|
+
{% if not u.roles %}
|
|
52
|
+
<span class="text-[var(--color-gray-400)]">—</span>
|
|
53
|
+
{% endif %}
|
|
54
|
+
</div>
|
|
55
|
+
</td>
|
|
56
|
+
<td class="px-6 py-4 text-center">
|
|
57
|
+
{% if u.is_superuser %}
|
|
58
|
+
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-amber-100 text-amber-800">Yes</span>
|
|
59
|
+
{% else %}
|
|
60
|
+
<span class="text-[var(--color-gray-400)]">No</span>
|
|
61
|
+
{% endif %}
|
|
62
|
+
</td>
|
|
63
|
+
<td class="px-6 py-4 text-center">
|
|
64
|
+
{% if u.is_active %}
|
|
65
|
+
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-800">Active</span>
|
|
66
|
+
{% else %}
|
|
67
|
+
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-red-100 text-red-800">Inactive</span>
|
|
68
|
+
{% endif %}
|
|
69
|
+
</td>
|
|
70
|
+
<td class="px-6 py-4 text-[var(--color-gray-500)] text-sm">
|
|
71
|
+
{{ u.last_login.strftime('%Y-%m-%d %H:%M') if u.last_login else 'Never' }}
|
|
72
|
+
</td>
|
|
73
|
+
<td class="px-6 py-4 text-right">
|
|
74
|
+
<a href="{{ admin_path | default('/admin') }}/users/{{ u.id }}"
|
|
75
|
+
class="text-[var(--color-primary-600)] hover:text-[var(--color-primary-500)] text-sm font-medium mr-3">Edit</a>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
{% endfor %}
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
{% endblock %}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{# partials/command_palette.html — global search command palette #}
|
|
2
|
+
<div x-data="commandPalette()" x-show="open" x-cloak
|
|
3
|
+
@open-command-palette.window="open = true; $nextTick(() => { $refs.paletteInput.focus(); $refs.paletteInput.select(); })"
|
|
4
|
+
class="command-palette-backdrop"
|
|
5
|
+
@click="close()">
|
|
6
|
+
<div class="command-palette" @click.stop>
|
|
7
|
+
<div class="command-palette-header">
|
|
8
|
+
{{ icon("magnifying-glass", size="18px") }}
|
|
9
|
+
<input x-ref="paletteInput"
|
|
10
|
+
type="text"
|
|
11
|
+
class="command-palette-input"
|
|
12
|
+
placeholder="Search models, fields..."
|
|
13
|
+
x-model="query"
|
|
14
|
+
@input="onInput()"
|
|
15
|
+
@keydown="onKeydown($event)"
|
|
16
|
+
autocomplete="off">
|
|
17
|
+
<kbd class="command-palette-esc">Esc</kbd>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="command-palette-results" x-show="results.length > 0">
|
|
21
|
+
<template x-for="(item, index) in results" :key="index">
|
|
22
|
+
<a :href="item.url"
|
|
23
|
+
class="command-palette-result"
|
|
24
|
+
:class="{ 'command-palette-result--active': index === selectedIndex }"
|
|
25
|
+
@mouseenter="selectedIndex = index"
|
|
26
|
+
@click.prevent="navigate(item.url)">
|
|
27
|
+
<span class="command-palette-result-icon">
|
|
28
|
+
<span class="material-symbols-outlined" style="font-size: 18px;"
|
|
29
|
+
x-text="item.type === 'model' ? 'inventory_2' : 'description'"></span>
|
|
30
|
+
</span>
|
|
31
|
+
<span class="command-palette-result-text">
|
|
32
|
+
<span class="command-palette-result-label" x-text="item.label"></span>
|
|
33
|
+
<span class="command-palette-result-sublabel" x-text="item.sublabel"></span>
|
|
34
|
+
</span>
|
|
35
|
+
<span class="command-palette-result-badge"
|
|
36
|
+
:class="item.type === 'model' ? 'badge-model' : 'badge-field'"
|
|
37
|
+
x-text="item.type"></span>
|
|
38
|
+
</a>
|
|
39
|
+
</template>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="command-palette-empty" x-show="query.trim() && results.length === 0">
|
|
43
|
+
<p>No results found for "<span x-text="query"></span>"</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="command-palette-footer" x-show="results.length > 0">
|
|
47
|
+
<span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
|
|
48
|
+
<span><kbd>↵</kbd> open</span>
|
|
49
|
+
<span><kbd>esc</kbd> close</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{# partials/flash_messages.html — toast notifications #}
|
|
2
|
+
{% set messages = get_flash_messages(request) | default([]) %}
|
|
3
|
+
{% if messages %}
|
|
4
|
+
<div class="fixed top-4 right-4 z-50 space-y-2" style="display: none;" x-data="{ show: true }" x-init="show = true">
|
|
5
|
+
{% for msg in messages %}
|
|
6
|
+
<div x-data="{ show: true }"
|
|
7
|
+
x-init="setTimeout(() => show = false, 4000)"
|
|
8
|
+
x-show="show"
|
|
9
|
+
x-transition:enter="transition ease-out duration-300"
|
|
10
|
+
x-transition:enter-start="opacity-0 translate-y-2"
|
|
11
|
+
x-transition:enter-end="opacity-100 translate-y-0"
|
|
12
|
+
x-transition:leave="transition ease-in duration-200"
|
|
13
|
+
x-transition:leave-start="opacity-100 translate-y-0"
|
|
14
|
+
x-transition:leave-end="opacity-0 translate-y-2"
|
|
15
|
+
class="toast toast-{{ msg.level | default('info') }} max-w-sm w-full bg-white shadow-lg rounded-lg border p-4"
|
|
16
|
+
style="display: none;">
|
|
17
|
+
<div class="flex items-start gap-3">
|
|
18
|
+
<div class="shrink-0">
|
|
19
|
+
{% if msg.level == 'success' %}
|
|
20
|
+
{{ icon("check", size="20px", class="text-[var(--color-success)]") }}
|
|
21
|
+
{% elif msg.level == 'error' %}
|
|
22
|
+
{{ icon("x-mark", size="20px", class="text-[var(--color-danger)]") }}
|
|
23
|
+
{% elif msg.level == 'warning' %}
|
|
24
|
+
{{ icon("exclamation-triangle", size="20px", class="text-[var(--color-warning)]") }}
|
|
25
|
+
{% else %}
|
|
26
|
+
{{ icon("information-circle", size="20px", class="text-[var(--color-info)]") }}
|
|
27
|
+
{% endif %}
|
|
28
|
+
</div>
|
|
29
|
+
<div class="flex-1">
|
|
30
|
+
<p class="text-sm text-[var(--color-gray-900)]">{{ msg.text }}</p>
|
|
31
|
+
</div>
|
|
32
|
+
<button @click="show = false" class="shrink-0 text-[var(--color-gray-400)] hover:text-[var(--color-gray-600)]">
|
|
33
|
+
{{ icon("x-mark") }}
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
{% endfor %}
|
|
38
|
+
</div>
|
|
39
|
+
{% endif %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<meta charset="UTF-8">
|
|
2
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3
|
+
<meta name="csrf-token" content="{{ get_csrf_token(request) }}">
|
|
4
|
+
<title>{% block title %}Admin{% endblock %}</title>
|
|
5
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
6
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:opsz,wght@6..144,1..1000&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
8
|
+
<link rel="stylesheet" href="/static/css/tokens.css">
|
|
9
|
+
<link rel="stylesheet" href="/static/css/admin.css">
|
|
10
|
+
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
11
|
+
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
|
12
|
+
<script defer src="https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js"></script>
|
|
13
|
+
<script>
|
|
14
|
+
(function() {
|
|
15
|
+
var theme = localStorage.getItem('admin_dark_mode');
|
|
16
|
+
if (theme === 'true') {
|
|
17
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
20
|
+
</script>
|
|
21
|
+
{% block head %}{% endblock %}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<meta charset="UTF-8">
|
|
2
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3
|
+
<meta name="csrf-token" content="{{ get_csrf_token(request) }}">
|
|
4
|
+
<title>{% block title %}Admin{% endblock %}</title>
|
|
5
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
6
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:opsz,wght@6..144,1..1000&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
8
|
+
<link rel="stylesheet" href="/static/css/tokens.css">
|
|
9
|
+
<link rel="stylesheet" href="/static/css/admin.css">
|
|
10
|
+
<script>
|
|
11
|
+
(function() {
|
|
12
|
+
var theme = localStorage.getItem('admin_dark_mode');
|
|
13
|
+
if (theme === 'true') {
|
|
14
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
</script>
|
|
18
|
+
{% block head %}{% endblock %}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
{# partials/list_table.html — table wrapper fragment for HTMX requests #}
|
|
2
|
+
<div id="table-wrapper" class="table-container">
|
|
3
|
+
<input type="hidden" name="action" value="delete_selected">
|
|
4
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
5
|
+
<div class="overflow-x-auto">
|
|
6
|
+
{% set _ui = ui_config | default({}) %}
|
|
7
|
+
<table class="data-table data-table--{{ _ui.table_style | default('default') }}{% if _ui.table_row_height | default('normal') == 'compact' %} data-table--compact{% endif %}{% if _ui.table_row_height | default('normal') == 'relaxed' %} data-table--relaxed{% endif %}">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
{% if permissions.can_delete %}
|
|
11
|
+
<th class="col-checkbox">
|
|
12
|
+
<input type="checkbox" id="select-all"
|
|
13
|
+
onchange="document.querySelectorAll('input[name="ids[]"]').forEach(c => c.checked = this.checked); this.dispatchEvent(new Event('change', {bubbles: true}));">
|
|
14
|
+
</th>
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% for col in display_columns %}
|
|
17
|
+
<th class="col-header">
|
|
18
|
+
<a href="?ordering={% if ordering == col.name %}-{% endif %}{{ col.name }}"
|
|
19
|
+
class="col-header-link{% if ordering == col.name or ordering == '-' ~ col.name %} active{% endif %}">
|
|
20
|
+
{{ col.label }}
|
|
21
|
+
{% if ordering == col.name %}
|
|
22
|
+
{{ icon("chevron-up", size="16px", css_class="sort-icon sort-icon--asc") }}
|
|
23
|
+
{% elif ordering == '-' ~ col.name %}
|
|
24
|
+
{{ icon("chevron-down", size="16px", css_class="sort-icon sort-icon--desc") }}
|
|
25
|
+
{% endif %}
|
|
26
|
+
</a>
|
|
27
|
+
</th>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
<th class="col-actions">Actions</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
{% if items %}
|
|
34
|
+
{% for obj in items %}
|
|
35
|
+
<tr class="data-row">
|
|
36
|
+
{% if permissions.can_delete %}
|
|
37
|
+
<td class="col-checkbox">
|
|
38
|
+
<input type="checkbox" name="ids[]" value="{{ obj.id }}" class="row-checkbox">
|
|
39
|
+
</td>
|
|
40
|
+
{% endif %}
|
|
41
|
+
{% set _detail_url = admin_path | default('/admin') ~ '/' ~ registered.table_name ~ '/' ~ obj.id %}
|
|
42
|
+
{% set _first_col = true %}
|
|
43
|
+
{% for col in display_columns %}
|
|
44
|
+
<td class="col-cell{% if col.css_class %} {{ col.css_class }}{% endif %}">
|
|
45
|
+
{% set val = col.value(obj) %}
|
|
46
|
+
{% if _first_col %}
|
|
47
|
+
{% set _first_col = false %}
|
|
48
|
+
<a href="{{ _detail_url }}" class="text-[var(--color-primary-600)] hover:text-[var(--color-primary-700)] transition-colors">
|
|
49
|
+
{% if val is none %}
|
|
50
|
+
-
|
|
51
|
+
{% elif col.boolean and val is sameas true %}
|
|
52
|
+
<span class="toggle-switch toggle-switch--on" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
53
|
+
{% elif col.boolean and val is sameas false %}
|
|
54
|
+
<span class="toggle-switch" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
55
|
+
{% elif val is sameas true %}
|
|
56
|
+
<span class="toggle-switch toggle-switch--on" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
57
|
+
{% elif val is sameas false %}
|
|
58
|
+
<span class="toggle-switch" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
59
|
+
{% elif val is string or val is number %}
|
|
60
|
+
{% if col.icon %}<span class="material-symbols-outlined" style="font-size: 16px; vertical-align: middle; margin-right: 4px;">{{ col.icon }}</span>{% endif %}{{ val }}
|
|
61
|
+
{% elif val is iterable and val is not string %}
|
|
62
|
+
{{ model_display_name(val) }}
|
|
63
|
+
{% else %}
|
|
64
|
+
{{ val | string }}
|
|
65
|
+
{% endif %}
|
|
66
|
+
</a>
|
|
67
|
+
{% else %}
|
|
68
|
+
{% if val is none %}
|
|
69
|
+
<span class="text-muted">-</span>
|
|
70
|
+
{% elif col.boolean and val is sameas true %}
|
|
71
|
+
<span class="toggle-switch toggle-switch--on" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
72
|
+
{% elif col.boolean and val is sameas false %}
|
|
73
|
+
<span class="toggle-switch" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
74
|
+
{% elif val is sameas true %}
|
|
75
|
+
<span class="toggle-switch toggle-switch--on" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
76
|
+
{% elif val is sameas false %}
|
|
77
|
+
<span class="toggle-switch" style="cursor: default;"><span class="toggle-switch__thumb"></span></span>
|
|
78
|
+
{% elif val is string or val is number %}
|
|
79
|
+
{% if col.icon %}<span class="material-symbols-outlined" style="font-size: 16px; vertical-align: middle; margin-right: 4px;">{{ col.icon }}</span>{% endif %}{{ val }}
|
|
80
|
+
{% elif val is iterable and val is not string %}
|
|
81
|
+
{{ model_display_name(val) }}
|
|
82
|
+
{% else %}
|
|
83
|
+
{{ val | string }}
|
|
84
|
+
{% endif %}
|
|
85
|
+
{% endif %}
|
|
86
|
+
</td>
|
|
87
|
+
{% endfor %}
|
|
88
|
+
<td class="col-actions">
|
|
89
|
+
<div class="actions-dropdown" x-data="{ open: false, menuStyle: '' }"
|
|
90
|
+
@actions-close.window="open = false">
|
|
91
|
+
<button type="button" class="actions-trigger" @click.stop="
|
|
92
|
+
open = !open;
|
|
93
|
+
if (open) {
|
|
94
|
+
$dispatch('actions-close');
|
|
95
|
+
open = true;
|
|
96
|
+
$nextTick(() => {
|
|
97
|
+
const btn = $el;
|
|
98
|
+
const menu = $el.nextElementSibling;
|
|
99
|
+
const rect = btn.getBoundingClientRect();
|
|
100
|
+
const menuRect = menu.getBoundingClientRect();
|
|
101
|
+
const viewportH = window.innerHeight;
|
|
102
|
+
let top = rect.bottom + 4;
|
|
103
|
+
let left = rect.right - menuRect.width;
|
|
104
|
+
if (top + menuRect.height > viewportH) {
|
|
105
|
+
top = rect.top - menuRect.height - 4;
|
|
106
|
+
}
|
|
107
|
+
if (left < 8) left = 8;
|
|
108
|
+
menuStyle = 'position:fixed;top:' + top + 'px;left:' + left + 'px;z-index:9999;';
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
" title="Actions">
|
|
112
|
+
{{ icon("ellipsis-vertical", size="16px") }}
|
|
113
|
+
</button>
|
|
114
|
+
<div class="actions-menu" x-show="open" x-cloak
|
|
115
|
+
:style="menuStyle"
|
|
116
|
+
@click.outside="open = false"
|
|
117
|
+
x-transition>
|
|
118
|
+
{% if permissions.can_edit %}
|
|
119
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/{{ obj.id }}"
|
|
120
|
+
class="actions-menu-item"
|
|
121
|
+
@click="open = false">
|
|
122
|
+
{{ icon("pencil", size="16px") }}
|
|
123
|
+
Edit
|
|
124
|
+
</a>
|
|
125
|
+
{% endif %}
|
|
126
|
+
{% if row_actions %}
|
|
127
|
+
{% for action in row_actions %}
|
|
128
|
+
<button type="button" class="actions-menu-item"
|
|
129
|
+
hx-post="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/action/{{ action.name }}/{{ obj.id }}"
|
|
130
|
+
hx-headers='{"X-CSRF-Token": "{{ get_csrf_token(request) }}"}'
|
|
131
|
+
hx-swap="none"
|
|
132
|
+
hx-on::after-request="window.location.reload();"
|
|
133
|
+
@click="open = false">
|
|
134
|
+
{% if action.icon %}
|
|
135
|
+
{{ icon(action.icon, size="16px") }}
|
|
136
|
+
{% endif %}
|
|
137
|
+
{{ action.label | default(action.name) }}
|
|
138
|
+
</button>
|
|
139
|
+
{% endfor %}
|
|
140
|
+
{% endif %}
|
|
141
|
+
{% if permissions.can_delete %}
|
|
142
|
+
<button type="button" class="actions-menu-item actions-menu-item--danger"
|
|
143
|
+
@click="open = false; confirmAction('Delete', 'Are you sure you want to delete this {{ registered.verbose_name | lower }}?', () => { var form = document.createElement('form'); form.method = 'POST'; form.action = '{{ admin_path | default('/admin') }}/{{ registered.table_name }}/{{ obj.id }}/delete'; var csrf = document.createElement('input'); csrf.type = 'hidden'; csrf.name = 'csrf_token'; csrf.value = '{{ get_csrf_token(request) }}'; form.appendChild(csrf); document.body.appendChild(form); form.submit(); })">
|
|
144
|
+
{{ icon("trash", size="16px") }}
|
|
145
|
+
Delete
|
|
146
|
+
</button>
|
|
147
|
+
{% endif %}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</td>
|
|
151
|
+
</tr>
|
|
152
|
+
{% endfor %}
|
|
153
|
+
{% else %}
|
|
154
|
+
<tr>
|
|
155
|
+
<td colspan="{{ display_columns | length + 2 }}" class="empty-cell">
|
|
156
|
+
<div class="empty-state">
|
|
157
|
+
{{ icon("document-text", size="48px", css_class="empty-state-icon") }}
|
|
158
|
+
<p class="empty-state-text">No {{ registered.verbose_name_plural | lower }} found</p>
|
|
159
|
+
{% if permissions.can_create %}
|
|
160
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/create"
|
|
161
|
+
class="btn btn-primary btn-sm">
|
|
162
|
+
+ Create your first {{ registered.verbose_name | lower }}
|
|
163
|
+
</a>
|
|
164
|
+
{% endif %}
|
|
165
|
+
</div>
|
|
166
|
+
</td>
|
|
167
|
+
</tr>
|
|
168
|
+
{% endif %}
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{# Pagination #}
|
|
174
|
+
<div class="table-footer">
|
|
175
|
+
{% from "partials/pagination.html" import render_pagination %}
|
|
176
|
+
{{ render_pagination(page, total_pages, total, registered.admin.per_page, next_cursor|default(null), has_next|default(false), pagination_mode|default("offset")) }}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|