diff --git a/worklenz-backend/database/pg-migrations/README.md b/worklenz-backend/database/pg-migrations/README.md new file mode 100644 index 00000000..ee063447 --- /dev/null +++ b/worklenz-backend/database/pg-migrations/README.md @@ -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 \ No newline at end of file diff --git a/worklenz-frontend/src/i18n.ts b/worklenz-frontend/src/i18n.ts index c7337343..f70b2f5c 100644 --- a/worklenz-frontend/src/i18n.ts +++ b/worklenz-frontend/src/i18n.ts @@ -10,6 +10,7 @@ i18n .init({ fallbackLng: 'en', defaultNS: 'common', + ns: ['common', 'home'], // Preload home namespace interpolation: { escapeValue: false, diff --git a/worklenz-frontend/src/pages/home/task-list/tasks-list.css b/worklenz-frontend/src/pages/home/task-list/tasks-list.css index 698cfcdc..bb9c82ce 100644 --- a/worklenz-frontend/src/pages/home/task-list/tasks-list.css +++ b/worklenz-frontend/src/pages/home/task-list/tasks-list.css @@ -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; + } +} diff --git a/worklenz-frontend/src/pages/home/task-list/tasks-list.tsx b/worklenz-frontend/src/pages/home/task-list/tasks-list.tsx index 01e7706c..17ba17dd 100644 --- a/worklenz-frontend/src/pages/home/task-list/tasks-list.tsx +++ b/worklenz-frontend/src/pages/home/task-list/tasks-list.tsx @@ -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(() => { {t('tasks.name')} ), - width: '40%', + width: isMobile ? '50%' : '40%', render: (_, record) => (
@@ -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 ( @@ -185,7 +187,7 @@ const TasksList: React.FC = React.memo(() => { render: (_, 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 ( + {t('tasks.tasks')}