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:
mahdahar 2026-04-28 09:01:32 +07:00
parent 6518f3a9f8
commit 9138e0286a
7 changed files with 192 additions and 97 deletions

View File

@ -278,6 +278,7 @@ $routes->group('api/gitea', function($routes) {
$routes->get('/figma', 'Figma::index'); $routes->get('/figma', 'Figma::index');
$routes->group('api/figma', function($routes) { $routes->group('api/figma', function($routes) {
$routes->get('summary', 'Api\FigmaApi::summary'); $routes->get('summary', 'Api\FigmaApi::summary');
$routes->get('users', 'Api\FigmaApi::users');
$routes->get('snapshots', 'Api\FigmaApi::snapshots'); $routes->get('snapshots', 'Api\FigmaApi::snapshots');
$routes->get('comments', 'Api\FigmaApi::comments'); $routes->get('comments', 'Api\FigmaApi::comments');
$routes->post('sync', 'Api\FigmaApi::sync'); $routes->post('sync', 'Api\FigmaApi::sync');

View File

@ -35,6 +35,12 @@ class FigmaApi extends BaseController
return null; return null;
} }
private function normalizeUsername(?string $username): ?string
{
$username = trim((string) $username);
return $username === '' ? null : mb_strtolower($username);
}
public function summary() public function summary()
{ {
if ($response = $this->ensureLoggedIn()) { if ($response = $this->ensureLoggedIn()) {
@ -63,6 +69,50 @@ class FigmaApi extends BaseController
], 200); ], 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 private function getPaginationParams(): array
{ {
$page = (int) ($this->request->getGet('page') ?? 1); $page = (int) ($this->request->getGet('page') ?? 1);
@ -86,6 +136,7 @@ class FigmaApi extends BaseController
$startDate = $this->request->getGet('start_date'); $startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date'); $endDate = $this->request->getGet('end_date');
$username = $this->normalizeUsername($this->request->getGet('username'));
[$page, $perPage] = $this->getPaginationParams(); [$page, $perPage] = $this->getPaginationParams();
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
@ -101,9 +152,13 @@ class FigmaApi extends BaseController
$baseBuilder->where('v.created_at_figma <=', $endDate . ' 23:59:59'); $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(); $total = (int) (clone $baseBuilder)->countAllResults();
$rows = $baseBuilder $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') ->orderBy('v.created_at_figma', 'DESC')
->limit($perPage, $offset) ->limit($perPage, $offset)
->get() ->get()
@ -130,6 +185,7 @@ class FigmaApi extends BaseController
$startDate = $this->request->getGet('start_date'); $startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date'); $endDate = $this->request->getGet('end_date');
$username = $this->normalizeUsername($this->request->getGet('username'));
[$page, $perPage] = $this->getPaginationParams(); [$page, $perPage] = $this->getPaginationParams();
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
@ -145,6 +201,10 @@ class FigmaApi extends BaseController
$baseBuilder->where('c.created_at_figma <=', $endDate . ' 23:59:59'); $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(); $total = (int) (clone $baseBuilder)->countAllResults();
$rows = $baseBuilder $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') ->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')

View File

@ -103,6 +103,16 @@ class CreateFigmaTables extends Migration
'constraint' => 100, 'constraint' => 100,
'null' => true, 'null' => true,
], ],
'figma_user_id' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'user_name' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'last_modified_figma' => [ 'last_modified_figma' => [
'type' => 'DATETIME', 'type' => 'DATETIME',
'null' => true, 'null' => true,
@ -123,6 +133,8 @@ class CreateFigmaTables extends Migration
$this->forge->addKey('id', true); $this->forge->addKey('id', true);
$this->forge->addUniqueKey(['file_id', 'figma_version_id']); $this->forge->addUniqueKey(['file_id', 'figma_version_id']);
$this->forge->addKey('file_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('created_at_figma');
$this->forge->addKey('last_modified_figma'); $this->forge->addKey('last_modified_figma');
$this->forge->createTable('figma_file_versions', true); $this->forge->createTable('figma_file_versions', true);

View File

@ -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');
}
}

View File

@ -184,6 +184,7 @@ class FigmaSyncService
$description = $version['description'] ?? $version['notes'] ?? $version['message'] ?? null; $description = $version['description'] ?? $version['notes'] ?? $version['message'] ?? null;
$createdAt = $this->normalizeDate($version['created_at'] ?? $version['createdAt'] ?? null); $createdAt = $this->normalizeDate($version['created_at'] ?? $version['createdAt'] ?? null);
$figmaUserId = $version['user']['id'] ?? $version['user_id'] ?? null; $figmaUserId = $version['user']['id'] ?? $version['user_id'] ?? null;
$userName = $version['user']['handle'] ?? $version['user']['name'] ?? $version['user_name'] ?? null;
$data = [ $data = [
'file_id' => $fileId, 'file_id' => $fileId,
@ -194,6 +195,7 @@ class FigmaSyncService
'name' => (string) (env('FIGMA_FILE_NAME') ?: 'Figma File'), 'name' => (string) (env('FIGMA_FILE_NAME') ?: 'Figma File'),
'editor_type' => $this->normalizeEditorType($version['editorType'] ?? null), 'editor_type' => $this->normalizeEditorType($version['editorType'] ?? null),
'figma_user_id' => is_scalar($figmaUserId) ? (string) $figmaUserId : null, 'figma_user_id' => is_scalar($figmaUserId) ? (string) $figmaUserId : null,
'user_name' => is_scalar($userName) ? (string) $userName : null,
'last_modified_figma' => $createdAt, 'last_modified_figma' => $createdAt,
'created_at_figma' => $createdAt, 'created_at_figma' => $createdAt,
]; ];

View File

@ -19,6 +19,7 @@ class FigmaFileVersionsModel extends Model
'name', 'name',
'editor_type', 'editor_type',
'figma_user_id', 'figma_user_id',
'user_name',
'last_modified_figma', 'last_modified_figma',
'created_at_figma', 'created_at_figma',
]; ];

View File

@ -15,6 +15,10 @@
</div> </div>
<div class="row g-3 mb-3"> <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"> <div class="col-md-3">
<label class="form-label">Start Date</label> <label class="form-label">Start Date</label>
<input type="date" id="filterStart" class="form-control"> <input type="date" id="filterStart" class="form-control">
@ -80,7 +84,7 @@
<th>Description</th> <th>Description</th>
<th>Version</th> <th>Version</th>
<th>Editor</th> <th>Editor</th>
<th>Figma User ID</th> <th>Username</th>
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>
@ -131,6 +135,7 @@
<?= $this->section('script') ?> <?= $this->section('script') ?>
<script> <script>
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const filterUser = document.getElementById('filterUser');
const filterStart = document.getElementById('filterStart'); const filterStart = document.getElementById('filterStart');
const filterEnd = document.getElementById('filterEnd'); const filterEnd = document.getElementById('filterEnd');
const btnApplyFilter = document.getElementById('btnApplyFilter'); const btnApplyFilter = document.getElementById('btnApplyFilter');
@ -168,6 +173,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}; };
setDefaultDateRange(); setDefaultDateRange();
await loadUsers();
await loadSummary(); await loadSummary();
await loadTables(); await loadTables();
updatePager(versionPrev, versionNext, versionPageInfo, versionsState); updatePager(versionPrev, versionNext, versionPageInfo, versionsState);
@ -267,6 +273,7 @@ document.addEventListener('DOMContentLoaded', async () => {
function getBaseParams(page, perPage) { function getBaseParams(page, perPage) {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filterUser.value) params.set('username', filterUser.value);
if (filterStart.value) params.set('start_date', filterStart.value); if (filterStart.value) params.set('start_date', filterStart.value);
if (filterEnd.value) params.set('end_date', filterEnd.value); if (filterEnd.value) params.set('end_date', filterEnd.value);
params.set('page', String(page)); params.set('page', String(page));
@ -274,7 +281,27 @@ document.addEventListener('DOMContentLoaded', async () => {
return params.toString(); 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() { async function loadSummary() {
try {
const response = await fetch(`<?= base_url('api/figma/summary') ?>`); const response = await fetch(`<?= base_url('api/figma/summary') ?>`);
const result = await response.json(); const result = await response.json();
if (!response.ok || !result.data) { if (!response.ok || !result.data) {
@ -285,6 +312,9 @@ document.addEventListener('DOMContentLoaded', async () => {
currentFileVersion.innerText = result.data.latest_version_label || result.data.file?.version || '-'; currentFileVersion.innerText = result.data.latest_version_label || result.data.file?.version || '-';
totalSnapshots.innerText = String(result.data.versions ?? 0); totalSnapshots.innerText = String(result.data.versions ?? 0);
totalComments.innerText = String(result.data.comments ?? 0); totalComments.innerText = String(result.data.comments ?? 0);
} catch (error) {
return;
}
} }
async function loadTables() { async function loadTables() {
@ -296,6 +326,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const tbody = document.querySelector('#tableVersions tbody'); const tbody = document.querySelector('#tableVersions tbody');
tbody.innerHTML = '<tr><td colspan="6">Loading...</td></tr>'; 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 response = await fetch(`<?= base_url('api/figma/snapshots') ?>?${getBaseParams(versionsState.page, versionsState.perPage)}`);
const result = await response.json(); const result = await response.json();
if (!response.ok || !Array.isArray(result.data)) { if (!response.ok || !Array.isArray(result.data)) {
@ -327,17 +358,25 @@ document.addEventListener('DOMContentLoaded', async () => {
<td>${escapeHtml(item.description || '')}</td> <td>${escapeHtml(item.description || '')}</td>
<td>${escapeHtml(item.version || item.figma_version_id || '')}</td> <td>${escapeHtml(item.version || item.figma_version_id || '')}</td>
<td>${escapeHtml(item.editor_type || '')}</td> <td>${escapeHtml(item.editor_type || '')}</td>
<td>${escapeHtml(item.figma_user_id || '')}</td> <td>${escapeHtml(item.user_name || '')}</td>
</tr>`; </tr>`;
}).join(''); }).join('');
updatePager(versionPrev, versionNext, versionPageInfo, versionsState); 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() { async function loadComments() {
const tbody = document.querySelector('#tableComments tbody'); const tbody = document.querySelector('#tableComments tbody');
tbody.innerHTML = '<tr><td colspan="4">Loading...</td></tr>'; 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 response = await fetch(`<?= base_url('api/figma/comments') ?>?${getBaseParams(commentsState.page, commentsState.perPage)}`);
const result = await response.json(); const result = await response.json();
if (!response.ok || !Array.isArray(result.data)) { if (!response.ok || !Array.isArray(result.data)) {
@ -374,6 +413,13 @@ document.addEventListener('DOMContentLoaded', async () => {
}).join(''); }).join('');
updatePager(commentPrev, commentNext, commentPageInfo, commentsState); 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> </script>