Archived exercises hidden from selects. Password fields Show Password toggle

This commit is contained in:
AG
2025-12-18 21:11:40 +02:00
parent b6cb3059af
commit b32f47c2b5
9 changed files with 103 additions and 32 deletions

Binary file not shown.

View File

@@ -7,7 +7,8 @@ export class ExerciseController {
static async getAllExercises(req: any, res: Response) {
try {
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);
} catch (error) {
logger.error('Error in getAllExercises', { error });

View File

@@ -1,13 +1,18 @@
import prisma from '../lib/prisma';
export class ExerciseService {
static async getAllExercises(userId: string) {
static async getAllExercises(userId: string, includeArchived: boolean = false) {
const exercises = await prisma.exercise.findMany({
where: {
AND: [
{
OR: [
{ userId: null }, // System default
{ userId } // User custom
]
},
includeArchived ? {} : { isArchived: false }
]
}
});
return exercises;

View File

@@ -1,5 +1,5 @@
import React, { useId } from 'react';
import { X } from 'lucide-react';
import { X, Eye, EyeOff } from 'lucide-react';
interface FilledInputProps {
label: string;
@@ -18,15 +18,19 @@ interface FilledInputProps {
rightElement?: React.ReactNode;
multiline?: boolean;
rows?: number;
showPasswordToggle?: boolean;
}
const FilledInput: React.FC<FilledInputProps> = ({
label, value, onChange, onClear, onFocus, onBlur, type = "number", icon,
autoFocus, step, inputMode, autocapitalize, autoComplete, rightElement,
multiline = false, rows = 3
multiline = false, rows = 3, showPasswordToggle = false
}) => {
const id = useId();
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement>(null);
const [showPassword, setShowPassword] = React.useState(false);
const actualType = type === 'password' && showPassword ? 'text' : type;
const handleClear = () => {
const syntheticEvent = {
@@ -47,11 +51,11 @@ const FilledInput: React.FC<FilledInputProps> = ({
<input
ref={inputRef as React.RefObject<HTMLInputElement>}
id={id}
type={type}
type={actualType}
step={step}
inputMode={inputMode || (type === 'number' ? 'decimal' : 'text')}
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=" "
value={value}
onChange={onChange}
@@ -80,12 +84,24 @@ const FilledInput: React.FC<FilledInputProps> = ({
type="button"
onClick={handleClear}
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}
>
<X size={16} />
</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 && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">

View File

@@ -66,6 +66,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
type="password"
showPasswordToggle
/>
<button
onClick={handleChangePassword}
@@ -119,6 +120,7 @@ const Login: React.FC<LoginProps> = ({ onLogin, language, onLanguageChange }) =>
onChange={(e) => setPassword(e.target.value)}
type="password"
icon={<Lock size={16} />}
showPasswordToggle
/>
</div>

View File

@@ -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 { getExercises, saveExercise } from '../services/storage';
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 FilledInput from './FilledInput';
import { t } from '../services/i18n';
@@ -115,7 +116,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
};
const refreshExercises = async () => {
const exercises = await getExercises(user.id);
const exercises = await getExercises(user.id, true);
setExercises(exercises);
};
@@ -478,15 +479,17 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
{/* Change Password */}
<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>
<div className="flex gap-2">
<input
type="password"
placeholder={t('change_pass_new', lang)}
<div className="flex gap-2 items-end">
<div className="flex-1">
<FilledInput
label={t('change_pass_new', lang)}
value={newPassword}
onChange={(e) => 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"
onChange={(e: any) => setNewPassword(e.target.value)}
type="password"
showPasswordToggle
/>
<Button onClick={handleChangePassword} size="sm" variant="secondary">OK</Button>
</div>
<Button onClick={handleChangePassword} className="mb-0.5">OK</Button>
</div>
{passMsg && <p className="text-xs text-primary mt-2">{passMsg}</p>}
</Card>
@@ -533,6 +536,16 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
value={newUserPass}
onChange={(e) => setNewUserPass(e.target.value)}
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>
{t('create_btn', lang)}
@@ -601,20 +614,19 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
</div>
{u.role !== 'ADMIN' && (
<div className="flex gap-2 items-center">
<div className="flex-1 flex items-center bg-surface-container rounded px-2 border border-outline-variant/20">
<KeyRound size={12} className="text-on-surface-variant mr-2" />
<input
type="text"
placeholder={t('change_pass_new', lang)}
className="bg-transparent text-xs py-2 w-full focus:outline-none text-on-surface"
<div className="flex gap-2 items-end">
<div className="flex-1">
<FilledInput
label={t('change_pass_new', lang)}
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>
<Button
onClick={() => handleAdminResetPass(u.id)}
size="sm"
className="mb-0.5"
variant="secondary"
>
{t('reset_pass', lang)}

View File

@@ -7,9 +7,9 @@ interface ApiResponse<T> {
error?: string;
}
export const getExercises = async (userId: string): Promise<ExerciseDef[]> => {
export const getExercises = async (userId: string, includeArchived: boolean = false): Promise<ExerciseDef[]> => {
try {
const res = await api.get<ApiResponse<ExerciseDef[]>>('/exercises');
const res = await api.get<ApiResponse<ExerciseDef[]>>(`/exercises${includeArchived ? '?includeArchived=true' : ''}`);
return res.data || [];
} catch {
return [];

9
src/utils/password.ts Normal file
View 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;
}

View File

@@ -485,6 +485,26 @@ test.describe('II. Workout Management', () => {
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 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 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 }) => {