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