Archived exercises hidden from selects. Password fields Show Password toggle
This commit is contained in:
Binary file not shown.
@@ -7,7 +7,8 @@ export class ExerciseController {
|
|||||||
static async getAllExercises(req: any, res: Response) {
|
static async getAllExercises(req: any, res: Response) {
|
||||||
try {
|
try {
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
const exercises = await ExerciseService.getAllExercises(userId);
|
const includeArchived = req.query.includeArchived === 'true';
|
||||||
|
const exercises = await ExerciseService.getAllExercises(userId, includeArchived);
|
||||||
return sendSuccess(res, exercises);
|
return sendSuccess(res, exercises);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in getAllExercises', { error });
|
logger.error('Error in getAllExercises', { error });
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import prisma from '../lib/prisma';
|
import prisma from '../lib/prisma';
|
||||||
|
|
||||||
export class ExerciseService {
|
export class ExerciseService {
|
||||||
static async getAllExercises(userId: string) {
|
static async getAllExercises(userId: string, includeArchived: boolean = false) {
|
||||||
const exercises = await prisma.exercise.findMany({
|
const exercises = await prisma.exercise.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
AND: [
|
||||||
{ userId: null }, // System default
|
{
|
||||||
{ userId } // User custom
|
OR: [
|
||||||
|
{ userId: null }, // System default
|
||||||
|
{ userId } // User custom
|
||||||
|
]
|
||||||
|
},
|
||||||
|
includeArchived ? {} : { isArchived: false }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useId } from 'react';
|
import React, { useId } from 'react';
|
||||||
import { X } from 'lucide-react';
|
import { X, Eye, EyeOff } from 'lucide-react';
|
||||||
|
|
||||||
interface FilledInputProps {
|
interface FilledInputProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -18,15 +18,19 @@ interface FilledInputProps {
|
|||||||
rightElement?: React.ReactNode;
|
rightElement?: React.ReactNode;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
|
showPasswordToggle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilledInput: React.FC<FilledInputProps> = ({
|
const FilledInput: React.FC<FilledInputProps> = ({
|
||||||
label, value, onChange, onClear, onFocus, onBlur, type = "number", icon,
|
label, value, onChange, onClear, onFocus, onBlur, type = "number", icon,
|
||||||
autoFocus, step, inputMode, autocapitalize, autoComplete, rightElement,
|
autoFocus, step, inputMode, autocapitalize, autoComplete, rightElement,
|
||||||
multiline = false, rows = 3
|
multiline = false, rows = 3, showPasswordToggle = false
|
||||||
}) => {
|
}) => {
|
||||||
const id = useId();
|
const id = useId();
|
||||||
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
||||||
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
|
|
||||||
|
const actualType = type === 'password' && showPassword ? 'text' : type;
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
const syntheticEvent = {
|
const syntheticEvent = {
|
||||||
@@ -47,11 +51,11 @@ const FilledInput: React.FC<FilledInputProps> = ({
|
|||||||
<input
|
<input
|
||||||
ref={inputRef as React.RefObject<HTMLInputElement>}
|
ref={inputRef as React.RefObject<HTMLInputElement>}
|
||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={actualType}
|
||||||
step={step}
|
step={step}
|
||||||
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
|
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
className={`w-full h-[56px] pt-5 pb-1 pl-4 bg-transparent text-body-lg text-on-surface focus:outline-none placeholder-transparent ${rightElement ? 'pr-20' : 'pr-10'}`}
|
className={`w-full h-[56px] pt-5 pb-1 pl-4 bg-transparent text-body-lg text-on-surface focus:outline-none placeholder-transparent ${rightElement ? 'pr-20' : (showPasswordToggle && type === 'password' ? 'pr-20' : 'pr-10')}`}
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
@@ -80,12 +84,24 @@ const FilledInput: React.FC<FilledInputProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
aria-label="Clear input"
|
aria-label="Clear input"
|
||||||
className={`absolute top-1/2 -translate-y-1/2 p-2 text-on-surface-variant hover:text-on-surface rounded-full transition-opacity ${rightElement ? 'right-12' : 'right-2'}`}
|
className={`absolute top-1/2 -translate-y-1/2 p-2 text-on-surface-variant hover:text-on-surface rounded-full transition-opacity ${(rightElement || (showPasswordToggle && type === 'password')) ? 'right-12' : 'right-2'}`}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPasswordToggle && type === 'password' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
aria-label="Toggle visibility"
|
||||||
|
className="absolute top-1/2 -translate-y-1/2 right-2 p-2 text-on-surface-variant hover:text-on-surface rounded-full transition-opacity"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
rightElement && (
|
rightElement && (
|
||||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
|||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
type="password"
|
type="password"
|
||||||
|
showPasswordToggle
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleChangePassword}
|
onClick={handleChangePassword}
|
||||||
@@ -119,6 +120,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
type="password"
|
type="password"
|
||||||
icon={<Lock size={16} />}
|
icon={<Lock size={16} />}
|
||||||
|
showPasswordToggle
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { User, Language, ExerciseDef, ExerciseType, BodyWeightRecord } from '../
|
|||||||
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth';
|
import { createUser, changePassword, updateUserProfile, getCurrentUserProfile, getUsers, deleteUser, toggleBlockUser, adminResetPassword, getMe } from '../services/auth';
|
||||||
import { getExercises, saveExercise } from '../services/storage';
|
import { getExercises, saveExercise } from '../services/storage';
|
||||||
import { getWeightHistory, logWeight } from '../services/weight';
|
import { getWeightHistory, logWeight } from '../services/weight';
|
||||||
import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, Plus, RefreshCcw } from 'lucide-react';
|
import { generatePassword } from '../utils/password';
|
||||||
|
import { User as UserIcon, LogOut, Save, Shield, UserPlus, Lock, Calendar, Ruler, Scale, PersonStanding, Globe, ChevronDown, ChevronUp, Trash2, Ban, KeyRound, Dumbbell, Archive, ArchiveRestore, Pencil, Plus, RefreshCcw, Sparkles } from 'lucide-react';
|
||||||
import ExerciseModal from './ExerciseModal';
|
import ExerciseModal from './ExerciseModal';
|
||||||
import FilledInput from './FilledInput';
|
import FilledInput from './FilledInput';
|
||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
@@ -115,7 +116,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
};
|
};
|
||||||
|
|
||||||
const refreshExercises = async () => {
|
const refreshExercises = async () => {
|
||||||
const exercises = await getExercises(user.id);
|
const exercises = await getExercises(user.id, true);
|
||||||
|
|
||||||
setExercises(exercises);
|
setExercises(exercises);
|
||||||
};
|
};
|
||||||
@@ -478,15 +479,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
{/* Change Password */}
|
{/* Change Password */}
|
||||||
<Card>
|
<Card>
|
||||||
<h3 className="text-sm font-bold text-primary mb-4 flex items-center gap-2"><Lock size={14} /> {t('change_pass_btn', lang)}</h3>
|
<h3 className="text-sm font-bold text-primary mb-4 flex items-center gap-2"><Lock size={14} /> {t('change_pass_btn', lang)}</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 items-end">
|
||||||
<input
|
<div className="flex-1">
|
||||||
type="password"
|
<FilledInput
|
||||||
placeholder={t('change_pass_new', lang)}
|
label={t('change_pass_new', lang)}
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
onChange={(e: any) => setNewPassword(e.target.value)}
|
||||||
className="flex-1 bg-surface-container-high border-b border-outline-variant px-3 py-2 text-on-surface focus:outline-none rounded-t-lg"
|
type="password"
|
||||||
/>
|
showPasswordToggle
|
||||||
<Button onClick={handleChangePassword} size="sm" variant="secondary">OK</Button>
|
/>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleChangePassword} className="mb-0.5">OK</Button>
|
||||||
</div>
|
</div>
|
||||||
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
|
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -533,6 +536,16 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
value={newUserPass}
|
value={newUserPass}
|
||||||
onChange={(e) => setNewUserPass(e.target.value)}
|
onChange={(e) => setNewUserPass(e.target.value)}
|
||||||
type="text"
|
type="text"
|
||||||
|
rightElement={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNewUserPass(generatePassword(8))}
|
||||||
|
className="p-2 text-primary hover:bg-primary/10 rounded-full transition-colors"
|
||||||
|
title="Generate"
|
||||||
|
>
|
||||||
|
<Sparkles size={20} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleCreateUser} fullWidth>
|
<Button onClick={handleCreateUser} fullWidth>
|
||||||
{t('create_btn', lang)}
|
{t('create_btn', lang)}
|
||||||
@@ -601,20 +614,19 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{u.role !== 'ADMIN' && (
|
{u.role !== 'ADMIN' && (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-end">
|
||||||
<div className="flex-1 flex items-center bg-surface-container rounded px-2 border border-outline-variant/20">
|
<div className="flex-1">
|
||||||
<KeyRound size={12} className="text-on-surface-variant mr-2" />
|
<FilledInput
|
||||||
<input
|
label={t('change_pass_new', lang)}
|
||||||
type="text"
|
|
||||||
placeholder={t('change_pass_new', lang)}
|
|
||||||
className="bg-transparent text-xs py-2 w-full focus:outline-none text-on-surface"
|
|
||||||
value={adminPassResetInput[u.id] || ''}
|
value={adminPassResetInput[u.id] || ''}
|
||||||
onChange={(e) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })}
|
onChange={(e: any) => setAdminPassResetInput({ ...adminPassResetInput, [u.id]: e.target.value })}
|
||||||
|
type="password"
|
||||||
|
showPasswordToggle
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAdminResetPass(u.id)}
|
onClick={() => handleAdminResetPass(u.id)}
|
||||||
size="sm"
|
className="mb-0.5"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{t('reset_pass', lang)}
|
{t('reset_pass', lang)}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ interface ApiResponse<T> {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getExercises = async (userId: string): Promise<ExerciseDef[]> => {
|
export const getExercises = async (userId: string, includeArchived: boolean = false): Promise<ExerciseDef[]> => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<ApiResponse<ExerciseDef[]>>('/exercises');
|
const res = await api.get<ApiResponse<ExerciseDef[]>>(`/exercises${includeArchived ? '?includeArchived=true' : ''}`);
|
||||||
return res.data || [];
|
return res.data || [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
9
src/utils/password.ts
Normal file
9
src/utils/password.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
export function generatePassword(length = 8): string {
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+";
|
||||||
|
let password = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
}
|
||||||
@@ -485,6 +485,26 @@ test.describe('II. Workout Management', () => {
|
|||||||
|
|
||||||
await expect(page.getByText('Archive Me')).not.toBeVisible();
|
await expect(page.getByText('Archive Me')).not.toBeVisible();
|
||||||
|
|
||||||
|
// VERIFY: Should not appear in Plans Add Exercise selector
|
||||||
|
await page.getByRole('button', { name: 'Plans' }).first().click();
|
||||||
|
await page.getByRole('button', { name: 'Create Plan' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Manually' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Add Exercise' }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'Archive Me' })).not.toBeVisible();
|
||||||
|
// Close sidesheet - use more robust selector and wait for stability
|
||||||
|
const closeBtn = page.getByLabel('Close');
|
||||||
|
await expect(closeBtn).toBeVisible();
|
||||||
|
await closeBtn.click();
|
||||||
|
|
||||||
|
// VERIFY: Should not appear in Tracker/Quick Log suggestions
|
||||||
|
await page.getByRole('button', { name: 'Tracker' }).first().click();
|
||||||
|
await page.getByRole('button', { name: 'Quick Log' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Select Exercise' }).fill('Archive');
|
||||||
|
await expect(page.getByRole('button', { name: 'Archive Me' })).not.toBeVisible();
|
||||||
|
|
||||||
|
// Go back to Profile and unarchive
|
||||||
|
await page.getByRole('button', { name: 'Profile' }).first().click();
|
||||||
|
await page.locator('button:has-text("Manage Exercises")').click();
|
||||||
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').check();
|
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').check();
|
||||||
await expect(page.getByText('Archive Me')).toBeVisible();
|
await expect(page.getByText('Archive Me')).toBeVisible();
|
||||||
|
|
||||||
@@ -496,6 +516,12 @@ test.describe('II. Workout Management', () => {
|
|||||||
|
|
||||||
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').uncheck();
|
await page.locator('div').filter({ hasText: /Show Archived/i }).last().locator('input[type="checkbox"]').uncheck();
|
||||||
await expect(page.getByText('Archive Me')).toBeVisible();
|
await expect(page.getByText('Archive Me')).toBeVisible();
|
||||||
|
|
||||||
|
// VERIFY: Should appear again in Tracker/Quick Log suggestions
|
||||||
|
await page.getByRole('button', { name: 'Tracker' }).first().click();
|
||||||
|
await page.getByRole('button', { name: 'Quick Log' }).click();
|
||||||
|
await page.getByRole('textbox', { name: 'Select Exercise' }).fill('Archive');
|
||||||
|
await expect(page.getByRole('button', { name: 'Archive Me' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('2.10 B. Exercise Library - Filter by Name', async ({ page, createUniqueUser, request }) => {
|
test('2.10 B. Exercise Library - Filter by Name', async ({ page, createUniqueUser, request }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user