Merge branch 'fix/release-v2.1.3' of https://github.com/Worklenz/worklenz into test/invitation-process
This commit is contained in:
@@ -118,7 +118,7 @@ const ForgotPasswordPage = () => {
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder={t('emailPlaceholder')}
|
||||
placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})}
|
||||
size="large"
|
||||
style={{ borderRadius: 4 }}
|
||||
/>
|
||||
@@ -134,7 +134,7 @@ const ForgotPasswordPage = () => {
|
||||
loading={isLoading}
|
||||
style={{ borderRadius: 4 }}
|
||||
>
|
||||
{t('resetPasswordButton')}
|
||||
{t('resetPasswordButton', {defaultValue: 'Reset Password'})}
|
||||
</Button>
|
||||
<Typography.Text style={{ textAlign: 'center' }}>{t('orText')}</Typography.Text>
|
||||
<Link to="/auth/login">
|
||||
@@ -146,7 +146,7 @@ const ForgotPasswordPage = () => {
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{t('returnToLoginButton')}
|
||||
{t('returnToLoginButton', {defaultValue: 'Return to Login'})}
|
||||
</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
@@ -5,6 +5,8 @@ import { useMediaQuery } from 'react-responsive';
|
||||
import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Form, Card, Input, Flex, Button, Typography, Space, message } from 'antd/es';
|
||||
import { Rule } from 'antd/es/form';
|
||||
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
import googleIcon from '@/assets/images/google-icon.png';
|
||||
import PageHeader from '@components/AuthPageHeader';
|
||||
@@ -297,6 +299,10 @@ const SignupPage = () => {
|
||||
min: 8,
|
||||
message: t('passwordMinCharacterRequired'),
|
||||
},
|
||||
{
|
||||
max: 32,
|
||||
message: t('passwordMaxCharacterRequired'),
|
||||
},
|
||||
{
|
||||
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/,
|
||||
message: t('passwordPatternRequired'),
|
||||
@@ -304,6 +310,38 @@ const SignupPage = () => {
|
||||
],
|
||||
};
|
||||
|
||||
const passwordChecklistItems = [
|
||||
{
|
||||
key: 'minLength',
|
||||
test: (v: string) => v.length >= 8,
|
||||
label: t('passwordChecklist.minLength', { defaultValue: 'At least 8 characters' }),
|
||||
},
|
||||
{
|
||||
key: 'uppercase',
|
||||
test: (v: string) => /[A-Z]/.test(v),
|
||||
label: t('passwordChecklist.uppercase', { defaultValue: 'One uppercase letter' }),
|
||||
},
|
||||
{
|
||||
key: 'lowercase',
|
||||
test: (v: string) => /[a-z]/.test(v),
|
||||
label: t('passwordChecklist.lowercase', { defaultValue: 'One lowercase letter' }),
|
||||
},
|
||||
{
|
||||
key: 'number',
|
||||
test: (v: string) => /\d/.test(v),
|
||||
label: t('passwordChecklist.number', { defaultValue: 'One number' }),
|
||||
},
|
||||
{
|
||||
key: 'special',
|
||||
test: (v: string) => /[@$!%*?&#]/.test(v),
|
||||
label: t('passwordChecklist.special', { defaultValue: 'One special character' }),
|
||||
},
|
||||
];
|
||||
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const [passwordValue, setPasswordValue] = useState('');
|
||||
const [passwordActive, setPasswordActive] = useState(false);
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
@@ -317,7 +355,7 @@ const SignupPage = () => {
|
||||
}}
|
||||
variant="outlined"
|
||||
>
|
||||
<PageHeader description={t('headerDescription')} />
|
||||
<PageHeader description={t('headerDescription', {defaultValue: 'Sign up to get started'})} />
|
||||
<Form
|
||||
form={form}
|
||||
name="signup"
|
||||
@@ -331,35 +369,72 @@ const SignupPage = () => {
|
||||
name: urlParams.name,
|
||||
}}
|
||||
>
|
||||
<Form.Item name="name" label={t('nameLabel')} rules={formRules.name}>
|
||||
<Form.Item name="name" label={t('nameLabel', {defaultValue: 'Full Name'})} rules={formRules.name}>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder={t('namePlaceholder')}
|
||||
placeholder={t('namePlaceholder', {defaultValue: 'Enter your full name'})}
|
||||
size="large"
|
||||
style={{ borderRadius: 4 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="email" label={t('emailLabel')} rules={formRules.email as Rule[]}>
|
||||
<Form.Item name="email" label={t('emailLabel', {defaultValue: 'Email'})} rules={formRules.email as Rule[]}>
|
||||
<Input
|
||||
prefix={<MailOutlined />}
|
||||
placeholder={t('emailPlaceholder')}
|
||||
placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})}
|
||||
size="large"
|
||||
style={{ borderRadius: 4 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="password" label={t('passwordLabel')} rules={formRules.password}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={t('passwordLabel', {defaultValue: 'Password'})}
|
||||
rules={formRules.password}
|
||||
validateTrigger={['onBlur', 'onSubmit']}
|
||||
>
|
||||
<div>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder={t('strongPasswordPlaceholder')}
|
||||
placeholder={t('strongPasswordPlaceholder', {defaultValue: 'Enter a strong password'})}
|
||||
size="large"
|
||||
style={{ borderRadius: 4 }}
|
||||
value={passwordValue}
|
||||
onFocus={() => setPasswordActive(true)}
|
||||
onChange={e => {
|
||||
setPasswordValue(e.target.value);
|
||||
setPasswordActive(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!passwordValue) setPasswordActive(false);
|
||||
}}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('passwordValidationAltText')}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, marginTop: 4, marginBottom: 0, display: 'block' }}>
|
||||
{t('passwordGuideline', {
|
||||
defaultValue: 'Password must be at least 8 characters, include uppercase and lowercase letters, a number, and a special character.'
|
||||
})}
|
||||
</Typography.Text>
|
||||
{passwordActive && (
|
||||
<div style={{ marginTop: 8, marginBottom: 4 }}>
|
||||
{passwordChecklistItems.map(item => {
|
||||
const passed = item.test(passwordValue);
|
||||
// Only green if passed, otherwise neutral (never red)
|
||||
let color = passed
|
||||
? (themeMode === 'dark' ? '#52c41a' : '#389e0d')
|
||||
: (themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf');
|
||||
return (
|
||||
<Flex key={item.key} align="center" gap={8} style={{ color, fontSize: 13 }}>
|
||||
{passed ? (
|
||||
<CheckCircleTwoTone twoToneColor={themeMode === 'dark' ? '#52c41a' : '#52c41a'} />
|
||||
) : (
|
||||
<CloseCircleTwoTone twoToneColor={themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf'} />
|
||||
)}
|
||||
<span>{item.label}</span>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
@@ -416,7 +491,7 @@ const SignupPage = () => {
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Typography.Text style={{ fontSize: 14 }}>
|
||||
{t('alreadyHaveAccountText')}
|
||||
{t('alreadyHaveAccountText', {defaultValue: 'Already have an account?'})}
|
||||
</Typography.Text>
|
||||
|
||||
<Link
|
||||
@@ -4,6 +4,8 @@ import { Form, Card, Input, Flex, Button, Typography, Result } from 'antd/es';
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
|
||||
import PageHeader from '@components/AuthPageHeader';
|
||||
|
||||
@@ -36,6 +38,36 @@ const VerifyResetEmailPage = () => {
|
||||
const { t } = useTranslation('auth/verify-reset-email');
|
||||
|
||||
const isMobile = useMediaQuery({ query: '(max-width: 576px)' });
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
const [passwordValue, setPasswordValue] = useState('');
|
||||
const [passwordTouched, setPasswordTouched] = useState(false);
|
||||
const passwordChecklistItems = [
|
||||
{
|
||||
key: 'minLength',
|
||||
test: (v: string) => v.length >= 8,
|
||||
label: t('passwordChecklist.minLength', { defaultValue: 'At least 8 characters' }),
|
||||
},
|
||||
{
|
||||
key: 'uppercase',
|
||||
test: (v: string) => /[A-Z]/.test(v),
|
||||
label: t('passwordChecklist.uppercase', { defaultValue: 'One uppercase letter' }),
|
||||
},
|
||||
{
|
||||
key: 'lowercase',
|
||||
test: (v: string) => /[a-z]/.test(v),
|
||||
label: t('passwordChecklist.lowercase', { defaultValue: 'One lowercase letter' }),
|
||||
},
|
||||
{
|
||||
key: 'number',
|
||||
test: (v: string) => /\d/.test(v),
|
||||
label: t('passwordChecklist.number', { defaultValue: 'One number' }),
|
||||
},
|
||||
{
|
||||
key: 'special',
|
||||
test: (v: string) => /[@$!%*?&#]/.test(v),
|
||||
label: t('passwordChecklist.special', { defaultValue: 'One special character' }),
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
trackMixpanelEvent(evt_verify_reset_email_page_visit);
|
||||
@@ -104,12 +136,38 @@ const VerifyResetEmailPage = () => {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder={t('placeholder')}
|
||||
size="large"
|
||||
style={{ borderRadius: 4 }}
|
||||
/>
|
||||
<div>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder={t('placeholder')}
|
||||
size="large"
|
||||
style={{ borderRadius: 4 }}
|
||||
value={passwordValue}
|
||||
onChange={e => {
|
||||
setPasswordValue(e.target.value);
|
||||
if (!passwordTouched) setPasswordTouched(true);
|
||||
}}
|
||||
onBlur={() => setPasswordTouched(true)}
|
||||
/>
|
||||
<div style={{ marginTop: 8, marginBottom: 4 }}>
|
||||
{passwordChecklistItems.map(item => {
|
||||
const passed = item.test(passwordValue);
|
||||
let color = passed
|
||||
? (themeMode === 'dark' ? '#52c41a' : '#389e0d')
|
||||
: (themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf');
|
||||
return (
|
||||
<Flex key={item.key} align="center" gap={8} style={{ color, fontSize: 13 }}>
|
||||
{passed ? (
|
||||
<CheckCircleTwoTone twoToneColor={themeMode === 'dark' ? '#52c41a' : '#52c41a'} />
|
||||
) : (
|
||||
<CloseCircleTwoTone twoToneColor={themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf'} />
|
||||
)}
|
||||
<span>{item.label}</span>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
@@ -136,6 +194,8 @@ const VerifyResetEmailPage = () => {
|
||||
placeholder={t('confirmPasswordPlaceholder')}
|
||||
size="large"
|
||||
style={{ borderRadius: 4 }}
|
||||
value={form.getFieldValue('confirmPassword') || ''}
|
||||
onChange={e => form.setFieldsValue({ confirmPassword: e.target.value })}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -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