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) {
|
||||
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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
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();
|
||||
|
||||
// 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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user