feat: implement single-page module with data services, transformers, and UI components

This commit is contained in:
Firman Ramdhani
2026-07-03 11:53:48 +07:00
parent 299ddaad27
commit 0e9188aeae
14 changed files with 308 additions and 5 deletions
@@ -1,9 +1,6 @@
import { Table, Box, Title, Paper } from '@repo/ui/components'; import { Table, Box, Title, Paper } from '@repo/ui/components';
import { PageActions } from '@repo/ui/components'; import { PageActions } from '@repo/ui/components';
import { import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations';
useEnterpriseModuleTranslationContext,
useEnterpriseModuleNavigationContext,
} from '@repo/ui/foundations';
import { FullPageEntity } from '../../domain/entities'; import { FullPageEntity } from '../../domain/entities';
import { Edit2, Eye, Copy, Trash2 } from 'lucide-react'; import { Edit2, Eye, Copy, Trash2 } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -22,7 +19,7 @@ const MOCK_DATA: FullPageEntity[] = [
]; ];
export default function FullPagePageIndex() { export default function FullPagePageIndex() {
// Translation is scoped to ['full-page', 'common'] — no prefix needed for module keys // Translation is scoped to ['FULL_PAGE', 'common'] — no prefix needed for module keys
const { t } = useEnterpriseModuleTranslationContext(); const { t } = useEnterpriseModuleTranslationContext();
const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext(); const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext();
@@ -0,0 +1,25 @@
import { BaseRemoteDataServices } from '@repo/core-api/data-services';
import { SinglePageDTO, SinglePageEntity } from '../domain/entities';
/**
* Full Page Remote Data Services
*
* Provides core data services for the single-page module by extending the base remote data services.
* While this class automatically handles standard CRUD operations and data transformations out-of-the-box,
* implementers can freely extend it by adding custom methods to support domain-specific API endpoints
* or complex business logic as needed.
*
* @example
* ```ts
* export class SinglePageRemoteDataServices extends BaseRemoteDataServices<SinglePageEntity, SinglePageDTO> {
* // Example of adding a custom method tailored to specific module needs
* public async getDashboardMetrics(status: string): Promise<MetricsPayload> {
* const response = await this.httpClient.get(`${this.apiUrl}/metrics`, {
* params: { status }
* });
* return response.data;
* }
* }
* ```
*/
export class SinglePageRemoteDataServices extends BaseRemoteDataServices<SinglePageEntity, SinglePageDTO> {}
@@ -0,0 +1 @@
export * from './single-page.constants';
@@ -0,0 +1,22 @@
import { ModuleConfigEntity } from '@repo/ui/foundations';
/**
* Core configuration and constants for the Full Page module.
* Used across Domain, Data, and Presentation layers.
*/
export const SinglePageModuleConfig: ModuleConfigEntity = {
/** Unique identifier for permissions, caching, and i18n */
moduleKey: 'EXAMPLE_SINGLE_PAGE',
/** Translation namespace — must match the namespace used in registerModuleNamespace() */
translationNamespace: 'single-page',
/** Base API endpoint for remote data services */
apiUrl: '/single-page',
/** Base Web Router URL for UI navigation */
webUrl: 'apps/example/single-page',
/** Architectural category of the module, used for rendering and routing logic */
moduleCategory: 'SINGLE_PAGE',
} as const;
@@ -0,0 +1 @@
export * from './single-page.entity';
@@ -0,0 +1,26 @@
import { BaseEntity } from '@repo/core-api/data-services';
/**
* Represents a single-page entity in the frontend domain model.
*
* This entity is derived from the API's `SinglePageDTO` via the
* {@link SinglePageTransformer}, which handles field name mapping
* and computed field derivation.
*
*/
export interface SinglePageEntity extends BaseEntity {
status?: string;
name?: string;
code?: string;
description?: string;
}
/**
* Represents the raw data structure returned by the API for a single-page resource.
*
* This DTO uses snake_case field names matching the backend's JSON serialization.
* It is transformed into a {@link SinglePageEntity} by the {@link SinglePageTransformer}.
*/
export interface SinglePageDTO extends SinglePageEntity {
[key: string]: any;
}
@@ -0,0 +1,30 @@
/**
* Full Page Factory
*
* This module acts as the dependency injection and configuration center for the single-page feature.
* It pre-configures and exports singleton instances of the data transformer and data service.
* By centralizing the instantiation here, it ensures that all UI components and hooks
* within the module share the same API client, configuration, and transformation logic.
*/
import { apiClient } from '../../../../../../core/lib/api-client';
import { SinglePageRemoteDataServices } from '../../data/single-page.remote.service';
import { SinglePageModuleConfig } from '../constants/single-page.constants';
import { SinglePageRemoteDataTransformer } from '../transformers/single-page.remote.transformer';
/**
* Singleton instance of the SinglePageRemoteDataTransformer.
* Exported for potential direct usage if manual data mapping is required outside the standard API flow.
*/
export const singlePageDataTransformer = new SinglePageRemoteDataTransformer();
/**
* Pre-configured singleton instance of the SinglePageRemoteDataServices.
* Ready to be consumed by UI components, state managers, or module providers.
* It is fully wired with the HTTP client and automatically handles data mapping via the injected transformer.
*/
export const singlePageDataService = new SinglePageRemoteDataServices(apiClient, {
apiUrl: SinglePageModuleConfig.apiUrl,
moduleKey: SinglePageModuleConfig.moduleKey,
transformer: singlePageDataTransformer,
});
@@ -0,0 +1,45 @@
import { BaseDataTransformer } from '@repo/core-api/data-services';
import { SinglePageEntity, SinglePageDTO } from '../entities';
/**
* Full Page Remote Data Transformer
*
* Responsible for transforming data between the raw API data transfer objects (DTOs)
* and the frontend domain entities for the single-page module. By extending the base transformer,
* it ensures strict type safety and decouples data parsing logic from the API service layer.
*
* Implementers must define the core mapping rules (`transformToEntity`, `transformToDTO`)
* and can freely add custom transformation methods for specific API responses.
*
* @example
* ```ts
* export class SinglePageRemoteDataTransformer extends BaseDataTransformer<SinglePageEntity, SinglePageDTO> {
* // Map snake_case API payload to camelCase frontend entity
* public transformToEntity(dto: SinglePageDTO): SinglePageEntity {
* return {
* id: dto.id,
* documentNumber: dto.document_number,
* createdAt: new Date(dto.created_at),
* // ... other property mappings
* };
* }
*
* // Map camelCase frontend entity back to snake_case API payload
* public transformToDTO(entity: SinglePageEntity): SinglePageDTO {
* return {
* id: entity.id,
* document_number: entity.documentNumber,
* // ... other property mappings
* };
* }
*
* // Example of adding a custom transformation method for a specific feature
* public transformMetrics(rawData: any): MetricsEntity {
* return {
* totalActive: rawData.total_active_count ?? 0,
* };
* }
* }
* ```
*/
export class SinglePageRemoteDataTransformer extends BaseDataTransformer<SinglePageEntity, SinglePageDTO> {}
@@ -0,0 +1,37 @@
import { lazy } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
import { registerModuleNamespace } from '@repo/core-i18n';
import { SinglePageModuleConfig } from '../../domain/constants';
import { singlePageDataService } from '../../domain/factories';
import { SinglePageEntity } from '../../domain/entities';
import singlePageId from '../locales/id/single-page.json';
import singlePageEn from '../locales/en/single-page.json';
const IndexPage = lazy(() => import('../pages/single-page.page.index'));
// ---------------------------------------------------------------------------
// Namespace Registration (Module Scope)
// ---------------------------------------------------------------------------
// Called once at import time — safe, idempotent, outside React render cycle.
// The namespace 'single-page' must match config.translationNamespace.
registerModuleNamespace(SinglePageModuleConfig.translationNamespace, {
id: singlePageId,
en: singlePageEn,
});
// ---------------------------------------------------------------------------
// Module Factory
// ---------------------------------------------------------------------------
export default function SinglePageModule() {
return (
<EnterpriseModuleProvider<SinglePageEntity> config={SinglePageModuleConfig} dataServices={singlePageDataService}>
<Routes>
<Route path="/index" element={<IndexPage />} />
<Route path="/" element={<Navigate to={`${SinglePageModuleConfig.webUrl}/index`} replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes>
</EnterpriseModuleProvider>
);
}
@@ -0,0 +1,9 @@
{
"title": "Single Page Management",
"fields": {
"status": "Status",
"name": "Name",
"code": "Code",
"description": "Description"
}
}
@@ -0,0 +1,9 @@
{
"title": "Manajemen Halaman Tunggal",
"fields": {
"status": "Status",
"name": "Nama",
"code": "Kode",
"description": "Deskripsi"
}
}
@@ -0,0 +1,99 @@
import { Table, Box, Title, Paper } from '@repo/ui/components';
import { PageActions } from '@repo/ui/components';
import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations';
import { SinglePageEntity } from '../../domain/entities';
import { Edit2, Eye, Copy, Trash2 } from 'lucide-react';
import { useCallback } from 'react';
// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext
const MOCK_DATA: SinglePageEntity[] = [
{ id: '1', name: 'Dashboard Widget', code: 'WID-001', status: 'ACTIVE', description: 'Main dashboard widget' },
{ id: '2', name: 'Report Generator', code: 'REP-002', status: 'INACTIVE', description: 'Generates monthly reports' },
{
id: '3',
name: 'User Management',
code: 'USR-003',
status: 'ACTIVE',
description: 'Manages user roles and permissions',
},
];
export default function SinglePagePageIndex() {
// Translation is scoped to ['SINGLE_PAGE', 'common'] — no prefix needed for module keys
const { t } = useEnterpriseModuleTranslationContext();
const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext();
const getRowActions = useCallback(
(row: SinglePageEntity) => [
{
key: 'view',
label: t('common:view'),
icon: <Eye size={14} />,
onClick: () => navigateToDetail(row.id as string),
},
{
key: 'edit',
label: t('common:edit'),
icon: <Edit2 size={14} />,
onClick: () => navigateToEdit(row.id as string),
},
{
key: 'duplicate',
label: t('common:duplicate'),
icon: <Copy size={14} />,
onClick: () => navigateToDuplicate(row.id as string),
},
{
type: 'divider' as const,
key: 'div-1',
},
{
key: 'delete',
label: t('common:delete'),
icon: <Trash2 size={14} />,
intent: 'destructive' as const,
onClick: () => {
// Mock delete action
alert(`Delete ${row.name}`);
},
},
],
[t, navigateToDetail, navigateToEdit, navigateToDuplicate],
);
const rows = MOCK_DATA.map((item) => (
<Table.Tr key={item.id}>
<Table.Td>{item.code}</Table.Td>
<Table.Td>{item.name}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>{item.description}</Table.Td>
<Table.Td>
<PageActions actions={getRowActions(item)} />
</Table.Td>
</Table.Tr>
));
return (
<Box p="md">
<Title order={2} mb="md">
{t('title')}
</Title>
<Paper withBorder shadow="sm" radius="md">
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>{t('fields.code')}</Table.Th>
<Table.Th>{t('fields.name')}</Table.Th>
<Table.Th>{t('fields.status')}</Table.Th>
<Table.Th>{t('fields.description')}</Table.Th>
<Table.Th w={200}>{t('common:edit')}</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</Paper>
</Box>
);
}
+2
View File
@@ -3,12 +3,14 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import ModuleLayout from './layouts/module.layout'; import ModuleLayout from './layouts/module.layout';
const FullPageModule = lazy(() => import('./example/full-page/presentation/factory')); const FullPageModule = lazy(() => import('./example/full-page/presentation/factory'));
const SinglePageModule = lazy(() => import('./example/single-page/presentation/factory'));
export default function AppModule() { export default function AppModule() {
return ( return (
<ModuleLayout> <ModuleLayout>
<Routes> <Routes>
<Route path="/full-page/*" element={<FullPageModule />} /> <Route path="/full-page/*" element={<FullPageModule />} />
<Route path="/single-page/*" element={<SinglePageModule />} />
<Route path="/" element={<Navigate to="/app/full-page" replace={true} />} /> <Route path="/" element={<Navigate to="/app/full-page" replace={true} />} />
<Route path="*" element={<Navigate to={'/404'} replace={true} />} /> <Route path="*" element={<Navigate to={'/404'} replace={true} />} />
</Routes> </Routes>