feat(migrations): add README for node-pg-migrate and enhance frontend configuration

- Created a README file for database migrations using node-pg-migrate, detailing commands, file format, best practices, and an example migration.
- Added a Vite configuration file for the frontend, including plugin setup, alias resolution, build optimizations, and responsive design adjustments for task list components.
- Updated i18n configuration to preload the 'home' namespace for improved localization.
- Enhanced task list styling with responsive design features for better mobile usability.
This commit is contained in:
chamiakJ
2025-07-23 07:46:39 +05:30
parent e3c002b088
commit a6286eb2b8
5 changed files with 169 additions and 10 deletions

View File

@@ -0,0 +1,72 @@
# Node-pg-migrate Migrations
This directory contains database migrations managed by node-pg-migrate.
## Migration Commands
- `npm run migrate:create -- migration-name` - Create a new migration file
- `npm run migrate:up` - Run all pending migrations
- `npm run migrate:down` - Rollback the last migration
- `npm run migrate:redo` - Rollback and re-run the last migration
## Migration File Format
Migrations are JavaScript files with timestamp prefixes (e.g., `20250115000000_performance-indexes.js`).
Each migration file exports two functions:
- `exports.up` - Contains the forward migration logic
- `exports.down` - Contains the rollback logic
## Best Practices
1. **Always use IF EXISTS/IF NOT EXISTS checks** to make migrations idempotent
2. **Test migrations locally** before deploying to production
3. **Include rollback logic** in the `down` function for all changes
4. **Use descriptive names** for migration files
5. **Keep migrations focused** - one logical change per migration
## Example Migration
```javascript
exports.up = pgm => {
// Create table with IF NOT EXISTS
pgm.createTable('users', {
id: 'id',
name: { type: 'varchar(100)', notNull: true },
created_at: {
type: 'timestamp',
notNull: true,
default: pgm.func('current_timestamp')
}
}, { ifNotExists: true });
// Add index with IF NOT EXISTS
pgm.createIndex('users', 'name', {
name: 'idx_users_name',
ifNotExists: true
});
};
exports.down = pgm => {
// Drop in reverse order
pgm.dropIndex('users', 'name', {
name: 'idx_users_name',
ifExists: true
});
pgm.dropTable('users', { ifExists: true });
};
```
## Migration History
The `pgmigrations` table tracks which migrations have been run. Do not modify this table manually.
## Converting from SQL Migrations
When converting SQL migrations to node-pg-migrate format:
1. Wrap SQL statements in `pgm.sql()` calls
2. Use node-pg-migrate helper methods where possible (createTable, addColumns, etc.)
3. Always include `IF EXISTS/IF NOT EXISTS` checks
4. Ensure proper rollback logic in the `down` function

View File

@@ -10,6 +10,7 @@ i18n
.init({ .init({
fallbackLng: 'en', fallbackLng: 'en',
defaultNS: 'common', defaultNS: 'common',
ns: ['common', 'home'], // Preload home namespace
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,

View File

@@ -6,3 +6,81 @@
.ant-table-row:hover .row-action-button { .ant-table-row:hover .row-action-button {
opacity: 1; opacity: 1;
} }
/* Responsive styles for task list */
@media (max-width: 768px) {
.task-list-card .ant-card-head {
flex-direction: column;
gap: 12px;
}
.task-list-card .ant-card-head-title {
flex: 1;
width: 100%;
}
.task-list-card .ant-card-extra {
width: 100%;
justify-content: space-between;
}
.task-list-mobile-header {
flex-direction: column;
gap: 8px;
align-items: stretch !important;
}
.task-list-mobile-controls {
flex-direction: column;
gap: 8px;
align-items: stretch !important;
}
.task-list-mobile-select {
width: 100% !important;
}
.task-list-mobile-segmented {
width: 100% !important;
}
}
@media (max-width: 576px) {
.task-list-card .ant-table {
font-size: 12px;
}
.task-list-card .ant-table-thead > tr > th {
padding: 8px 4px;
font-size: 12px;
}
.task-list-card .ant-table-tbody > tr > td {
padding: 8px 4px;
}
.row-action-button {
opacity: 1; /* Always show on mobile */
}
/* Hide project column on very small screens */
.task-list-card .ant-table-thead > tr > th:nth-child(2),
.task-list-card .ant-table-tbody > tr > td:nth-child(2) {
display: none;
}
}
/* Table responsive container */
.task-list-card .ant-table-container {
overflow-x: auto;
}
@media (max-width: 768px) {
.task-list-card .ant-table-wrapper {
overflow-x: auto;
}
.task-list-card .ant-table {
min-width: 600px;
}
}

View File

@@ -15,6 +15,7 @@ import {
} from 'antd'; } from 'antd';
import React, { useState, useMemo, useCallback, useEffect } from 'react'; import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import ListView from './list-view'; import ListView from './list-view';
import CalendarView from './calendar-view'; import CalendarView from './calendar-view';
@@ -61,21 +62,22 @@ const TasksList: React.FC = React.memo(() => {
refetchOnFocus: false, refetchOnFocus: false,
}); });
const { t } = useTranslation('home'); const { t, ready } = useTranslation('home');
const { model } = useAppSelector(state => state.homePageReducer); const { model } = useAppSelector(state => state.homePageReducer);
const isMobile = useMediaQuery({ maxWidth: 768 });
const taskModes = useMemo( const taskModes = useMemo(
() => [ () => [
{ {
value: 0, value: 0,
label: t('home:tasks.assignedToMe'), label: ready ? t('tasks.assignedToMe') : 'Assigned to me',
}, },
{ {
value: 1, value: 1,
label: t('home:tasks.assignedByMe'), label: ready ? t('tasks.assignedByMe') : 'Assigned by me',
}, },
], ],
[t] [t, ready]
); );
const handleSegmentChange = (value: 'List' | 'Calendar') => { const handleSegmentChange = (value: 'List' | 'Calendar') => {
@@ -123,7 +125,7 @@ const TasksList: React.FC = React.memo(() => {
<span>{t('tasks.name')}</span> <span>{t('tasks.name')}</span>
</Flex> </Flex>
), ),
width: '40%', width: isMobile ? '50%' : '40%',
render: (_, record) => ( render: (_, record) => (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Tooltip title={record.name}> <Tooltip title={record.name}>
@@ -155,7 +157,7 @@ const TasksList: React.FC = React.memo(() => {
{ {
key: 'project', key: 'project',
title: t('tasks.project'), title: t('tasks.project'),
width: '25%', width: isMobile ? '30%' : '25%',
render: (_, record) => { render: (_, record) => {
return ( return (
<Tooltip title={record.project_name}> <Tooltip title={record.project_name}>
@@ -185,7 +187,7 @@ const TasksList: React.FC = React.memo(() => {
render: (_, record) => <HomeTasksDatePicker record={record} />, render: (_, record) => <HomeTasksDatePicker record={record} />,
}, },
], ],
[t, data?.body?.total, currentPage, pageSize, handlePageChange] [t, data?.body?.total, currentPage, pageSize, handlePageChange, isMobile]
); );
const handleTaskModeChange = (value: number) => { const handleTaskModeChange = (value: number) => {
@@ -210,23 +212,27 @@ const TasksList: React.FC = React.memo(() => {
); );
}, [dispatch]); }, [dispatch]);
return ( return (
<Card <Card
className="task-list-card"
title={ title={
<Flex gap={8} align="center"> <Flex gap={8} align="center" className="task-list-mobile-header">
<Typography.Title level={5} style={{ margin: 0 }}> <Typography.Title level={5} style={{ margin: 0 }}>
{t('tasks.tasks')} {t('tasks.tasks')}
</Typography.Title> </Typography.Title>
<Select <Select
defaultValue={taskModes[0].label} value={homeTasksConfig.tasks_group_by || 0}
options={taskModes} options={taskModes}
onChange={value => handleTaskModeChange(+value)} onChange={value => handleTaskModeChange(+value)}
fieldNames={{ label: 'label', value: 'value' }} fieldNames={{ label: 'label', value: 'value' }}
className="task-list-mobile-select"
style={{ minWidth: 160 }}
/> />
</Flex> </Flex>
} }
extra={ extra={
<Flex gap={8} align="center"> <Flex gap={8} align="center" className="task-list-mobile-controls">
<Tooltip title={t('tasks.refresh')} trigger={'hover'}> <Tooltip title={t('tasks.refresh')} trigger={'hover'}>
<Button <Button
shape="circle" shape="circle"
@@ -241,6 +247,7 @@ const TasksList: React.FC = React.memo(() => {
]} ]}
defaultValue="List" defaultValue="List"
onChange={handleSegmentChange} onChange={handleSegmentChange}
className="task-list-mobile-segmented"
/> />
</Flex> </Flex>
} }
@@ -283,6 +290,7 @@ const TasksList: React.FC = React.memo(() => {
rowClassName={() => 'custom-row-height'} rowClassName={() => 'custom-row-height'}
loading={homeTasksFetching && skipAutoRefetch} loading={homeTasksFetching && skipAutoRefetch}
pagination={false} pagination={false}
scroll={{ x: 'max-content' }}
/> />
<div <div