feat(figma): add DB-backed dashboard filters and paginated APIs
- add Figma API endpoints for summary, users, snapshots, comments, and admin sync - support date and user filters plus pagination for snapshots and comments - expand sync service and schema to store Figma user ids - refresh dashboard UI with summary cards, filters, pagination, and sync action - fold figma_user_id into base migration and remove extra migration
This commit is contained in:
parent
6518f3a9f8
commit
9138e0286a
@ -278,6 +278,7 @@ $routes->group('api/gitea', function($routes) {
|
||||
$routes->get('/figma', 'Figma::index');
|
||||
$routes->group('api/figma', function($routes) {
|
||||
$routes->get('summary', 'Api\FigmaApi::summary');
|
||||
$routes->get('users', 'Api\FigmaApi::users');
|
||||
$routes->get('snapshots', 'Api\FigmaApi::snapshots');
|
||||
$routes->get('comments', 'Api\FigmaApi::comments');
|
||||
$routes->post('sync', 'Api\FigmaApi::sync');
|
||||
|
||||
@ -35,6 +35,12 @@ class FigmaApi extends BaseController
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizeUsername(?string $username): ?string
|
||||
{
|
||||
$username = trim((string) $username);
|
||||
return $username === '' ? null : mb_strtolower($username);
|
||||
}
|
||||
|
||||
public function summary()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
@ -63,6 +69,50 @@ class FigmaApi extends BaseController
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
if ($response = $this->ensureLoggedIn()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$versionUsers = $db->table('figma_file_versions')
|
||||
->select('user_name')
|
||||
->where('user_name IS NOT NULL', null, false)
|
||||
->where('user_name !=', '')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$commentUsers = $db->table('figma_comments')
|
||||
->select('user_name')
|
||||
->where('user_name IS NOT NULL', null, false)
|
||||
->where('user_name !=', '')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$users = [];
|
||||
foreach (array_merge($versionUsers, $commentUsers) as $row) {
|
||||
$name = trim((string) ($row['user_name'] ?? ''));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = mb_strtolower($name);
|
||||
if (!isset($users[$key])) {
|
||||
$users[$key] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$rows = array_values($users);
|
||||
sort($rows, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Users fetched',
|
||||
'data' => $rows,
|
||||
], 200);
|
||||
}
|
||||
|
||||
private function getPaginationParams(): array
|
||||
{
|
||||
$page = (int) ($this->request->getGet('page') ?? 1);
|
||||
@ -86,6 +136,7 @@ class FigmaApi extends BaseController
|
||||
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$username = $this->normalizeUsername($this->request->getGet('username'));
|
||||
[$page, $perPage] = $this->getPaginationParams();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
@ -101,9 +152,13 @@ class FigmaApi extends BaseController
|
||||
$baseBuilder->where('v.created_at_figma <=', $endDate . ' 23:59:59');
|
||||
}
|
||||
|
||||
if (!empty($username)) {
|
||||
$baseBuilder->where('LOWER(TRIM(v.user_name)) = ' . $db->escape($username), null, false);
|
||||
}
|
||||
|
||||
$total = (int) (clone $baseBuilder)->countAllResults();
|
||||
$rows = $baseBuilder
|
||||
->select('v.id, v.figma_version_id, v.version, v.label, v.description, v.name, v.editor_type, v.figma_user_id, v.last_modified_figma, v.created_at_figma, f.file_key, f.last_synced_at')
|
||||
->select('v.id, v.figma_version_id, v.version, v.label, v.description, v.name, v.editor_type, v.figma_user_id, v.user_name, v.last_modified_figma, v.created_at_figma, f.file_key, f.last_synced_at')
|
||||
->orderBy('v.created_at_figma', 'DESC')
|
||||
->limit($perPage, $offset)
|
||||
->get()
|
||||
@ -130,6 +185,7 @@ class FigmaApi extends BaseController
|
||||
|
||||
$startDate = $this->request->getGet('start_date');
|
||||
$endDate = $this->request->getGet('end_date');
|
||||
$username = $this->normalizeUsername($this->request->getGet('username'));
|
||||
[$page, $perPage] = $this->getPaginationParams();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
@ -145,6 +201,10 @@ class FigmaApi extends BaseController
|
||||
$baseBuilder->where('c.created_at_figma <=', $endDate . ' 23:59:59');
|
||||
}
|
||||
|
||||
if (!empty($username)) {
|
||||
$baseBuilder->where('LOWER(TRIM(c.user_name)) = ' . $db->escape($username), null, false);
|
||||
}
|
||||
|
||||
$total = (int) (clone $baseBuilder)->countAllResults();
|
||||
$rows = $baseBuilder
|
||||
->select('c.id, c.figma_comment_id, c.user_name, c.message, c.is_resolved, c.resolved_at, c.created_at_figma, c.client_meta_json, f.file_key')
|
||||
|
||||
@ -103,6 +103,16 @@ class CreateFigmaTables extends Migration
|
||||
'constraint' => 100,
|
||||
'null' => true,
|
||||
],
|
||||
'figma_user_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'user_name' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
'last_modified_figma' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
@ -123,6 +133,8 @@ class CreateFigmaTables extends Migration
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey(['file_id', 'figma_version_id']);
|
||||
$this->forge->addKey('file_id');
|
||||
$this->forge->addKey('figma_user_id');
|
||||
$this->forge->addKey('user_name');
|
||||
$this->forge->addKey('created_at_figma');
|
||||
$this->forge->addKey('last_modified_figma');
|
||||
$this->forge->createTable('figma_file_versions', true);
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddFigmaUserIdToFileVersions extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addColumn('figma_file_versions', [
|
||||
'figma_user_id' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->db->query('CREATE INDEX idx_figma_file_versions_figma_user_id ON figma_file_versions(figma_user_id)');
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->db->query('DROP INDEX idx_figma_file_versions_figma_user_id ON figma_file_versions');
|
||||
$this->forge->dropColumn('figma_file_versions', 'figma_user_id');
|
||||
}
|
||||
}
|
||||
@ -184,6 +184,7 @@ class FigmaSyncService
|
||||
$description = $version['description'] ?? $version['notes'] ?? $version['message'] ?? null;
|
||||
$createdAt = $this->normalizeDate($version['created_at'] ?? $version['createdAt'] ?? null);
|
||||
$figmaUserId = $version['user']['id'] ?? $version['user_id'] ?? null;
|
||||
$userName = $version['user']['handle'] ?? $version['user']['name'] ?? $version['user_name'] ?? null;
|
||||
|
||||
$data = [
|
||||
'file_id' => $fileId,
|
||||
@ -194,6 +195,7 @@ class FigmaSyncService
|
||||
'name' => (string) (env('FIGMA_FILE_NAME') ?: 'Figma File'),
|
||||
'editor_type' => $this->normalizeEditorType($version['editorType'] ?? null),
|
||||
'figma_user_id' => is_scalar($figmaUserId) ? (string) $figmaUserId : null,
|
||||
'user_name' => is_scalar($userName) ? (string) $userName : null,
|
||||
'last_modified_figma' => $createdAt,
|
||||
'created_at_figma' => $createdAt,
|
||||
];
|
||||
|
||||
@ -19,6 +19,7 @@ class FigmaFileVersionsModel extends Model
|
||||
'name',
|
||||
'editor_type',
|
||||
'figma_user_id',
|
||||
'user_name',
|
||||
'last_modified_figma',
|
||||
'created_at_figma',
|
||||
];
|
||||
|
||||
@ -15,6 +15,10 @@
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">User</label>
|
||||
<select id="filterUser" class="form-select"><option value="">All User</option></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Start Date</label>
|
||||
<input type="date" id="filterStart" class="form-control">
|
||||
@ -80,7 +84,7 @@
|
||||
<th>Description</th>
|
||||
<th>Version</th>
|
||||
<th>Editor</th>
|
||||
<th>Figma User ID</th>
|
||||
<th>Username</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@ -131,6 +135,7 @@
|
||||
<?= $this->section('script') ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const filterUser = document.getElementById('filterUser');
|
||||
const filterStart = document.getElementById('filterStart');
|
||||
const filterEnd = document.getElementById('filterEnd');
|
||||
const btnApplyFilter = document.getElementById('btnApplyFilter');
|
||||
@ -168,6 +173,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
};
|
||||
|
||||
setDefaultDateRange();
|
||||
await loadUsers();
|
||||
await loadSummary();
|
||||
await loadTables();
|
||||
updatePager(versionPrev, versionNext, versionPageInfo, versionsState);
|
||||
@ -267,6 +273,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
function getBaseParams(page, perPage) {
|
||||
const params = new URLSearchParams();
|
||||
if (filterUser.value) params.set('username', filterUser.value);
|
||||
if (filterStart.value) params.set('start_date', filterStart.value);
|
||||
if (filterEnd.value) params.set('end_date', filterEnd.value);
|
||||
params.set('page', String(page));
|
||||
@ -274,7 +281,27 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const response = await fetch(`<?= base_url('api/figma/users') ?>`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.data.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
option.value = item;
|
||||
option.textContent = item;
|
||||
filterUser.appendChild(option);
|
||||
});
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSummary() {
|
||||
try {
|
||||
const response = await fetch(`<?= base_url('api/figma/summary') ?>`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !result.data) {
|
||||
@ -285,6 +312,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentFileVersion.innerText = result.data.latest_version_label || result.data.file?.version || '-';
|
||||
totalSnapshots.innerText = String(result.data.versions ?? 0);
|
||||
totalComments.innerText = String(result.data.comments ?? 0);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTables() {
|
||||
@ -296,6 +326,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const tbody = document.querySelector('#tableVersions tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="6">Loading...</td></tr>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`<?= base_url('api/figma/snapshots') ?>?${getBaseParams(versionsState.page, versionsState.perPage)}`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
@ -327,17 +358,25 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
<td>${escapeHtml(item.description || '')}</td>
|
||||
<td>${escapeHtml(item.version || item.figma_version_id || '')}</td>
|
||||
<td>${escapeHtml(item.editor_type || '')}</td>
|
||||
<td>${escapeHtml(item.figma_user_id || '')}</td>
|
||||
<td>${escapeHtml(item.user_name || '')}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
updatePager(versionPrev, versionNext, versionPageInfo, versionsState);
|
||||
} catch (error) {
|
||||
versionsState.total = 0;
|
||||
versionsState.totalPages = 0;
|
||||
totalVersionRows.innerText = '0';
|
||||
tbody.innerHTML = '<tr><td colspan="6">Failed loading snapshots</td></tr>';
|
||||
updatePager(versionPrev, versionNext, versionPageInfo, versionsState);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadComments() {
|
||||
const tbody = document.querySelector('#tableComments tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="4">Loading...</td></tr>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`<?= base_url('api/figma/comments') ?>?${getBaseParams(commentsState.page, commentsState.perPage)}`);
|
||||
const result = await response.json();
|
||||
if (!response.ok || !Array.isArray(result.data)) {
|
||||
@ -374,6 +413,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
}).join('');
|
||||
|
||||
updatePager(commentPrev, commentNext, commentPageInfo, commentsState);
|
||||
} catch (error) {
|
||||
commentsState.total = 0;
|
||||
commentsState.totalPages = 0;
|
||||
totalCommentRows.innerText = '0';
|
||||
tbody.innerHTML = '<tr><td colspan="4">Failed loading comments</td></tr>';
|
||||
updatePager(commentPrev, commentNext, commentPageInfo, commentsState);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user