mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-20 22:01:45 +00:00
chore: apply prettier on the entire project
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// FE-COMP-SCOPE-001 to FE-COMP-SCOPE-009
|
||||
import { render, screen, waitFor } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from '../../../tests/helpers/render';
|
||||
import { resetAllStores } from '../../../tests/helpers/store';
|
||||
import ScopeGroupPicker from './ScopeGroupPicker';
|
||||
|
||||
@@ -34,9 +34,7 @@ describe('ScopeGroupPicker', () => {
|
||||
// First collect all scopes by clicking Select All and capturing the callback
|
||||
const user = userEvent.setup();
|
||||
const captured: string[][] = [];
|
||||
const { rerender } = render(
|
||||
<ScopeGroupPicker selected={[]} onChange={s => captured.push(s)} />
|
||||
);
|
||||
const { rerender } = render(<ScopeGroupPicker selected={[]} onChange={(s) => captured.push(s)} />);
|
||||
await user.click(screen.getByRole('button', { name: /select all/i }));
|
||||
const allScopes = captured[0];
|
||||
|
||||
@@ -50,9 +48,7 @@ describe('ScopeGroupPicker', () => {
|
||||
const captured: string[][] = [];
|
||||
|
||||
// Get all scopes first
|
||||
const { rerender } = render(
|
||||
<ScopeGroupPicker selected={[]} onChange={s => captured.push(s)} />
|
||||
);
|
||||
const { rerender } = render(<ScopeGroupPicker selected={[]} onChange={(s) => captured.push(s)} />);
|
||||
await user.click(screen.getByRole('button', { name: /select all/i }));
|
||||
const allScopes = captured[0];
|
||||
|
||||
@@ -67,10 +63,12 @@ describe('ScopeGroupPicker', () => {
|
||||
render(<ScopeGroupPicker selected={[]} onChange={vi.fn()} />);
|
||||
|
||||
// Groups are collapsed by default — checkboxes for individual scopes not visible
|
||||
const groupToggles = screen.getAllByRole('button').filter(b =>
|
||||
!b.textContent?.toLowerCase().includes('select all') &&
|
||||
!b.textContent?.toLowerCase().includes('deselect all')
|
||||
);
|
||||
const groupToggles = screen
|
||||
.getAllByRole('button')
|
||||
.filter(
|
||||
(b) =>
|
||||
!b.textContent?.toLowerCase().includes('select all') && !b.textContent?.toLowerCase().includes('deselect all')
|
||||
);
|
||||
// Click the first group expand button
|
||||
await user.click(groupToggles[0]);
|
||||
// Individual scope checkboxes should now appear (more than just group-level ones)
|
||||
@@ -96,10 +94,12 @@ describe('ScopeGroupPicker', () => {
|
||||
render(<ScopeGroupPicker selected={[]} onChange={onChange} />);
|
||||
|
||||
// Expand first group
|
||||
const groupToggles = screen.getAllByRole('button').filter(b =>
|
||||
!b.textContent?.toLowerCase().includes('select all') &&
|
||||
!b.textContent?.toLowerCase().includes('deselect all')
|
||||
);
|
||||
const groupToggles = screen
|
||||
.getAllByRole('button')
|
||||
.filter(
|
||||
(b) =>
|
||||
!b.textContent?.toLowerCase().includes('select all') && !b.textContent?.toLowerCase().includes('deselect all')
|
||||
);
|
||||
await user.click(groupToggles[0]);
|
||||
|
||||
// There are now individual scope checkboxes — click the second one (first is group-level)
|
||||
|
||||
@@ -1,64 +1,78 @@
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { getScopesByGroup } from '../../api/oauthScopes'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { getScopesByGroup } from '../../api/oauthScopes';
|
||||
import { useTranslation } from '../../i18n';
|
||||
|
||||
interface Props {
|
||||
selected: string[]
|
||||
onChange: (scopes: string[]) => void
|
||||
selected: string[];
|
||||
onChange: (scopes: string[]) => void;
|
||||
}
|
||||
|
||||
export default function ScopeGroupPicker({ selected, onChange }: Props): React.ReactElement {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState<Record<string, boolean>>({})
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState<Record<string, boolean>>({});
|
||||
|
||||
const scopesByGroup = getScopesByGroup(t)
|
||||
const allScopeKeys = Object.values(scopesByGroup).flat().map(s => s.scope)
|
||||
const allSelected = allScopeKeys.every(s => selected.includes(s))
|
||||
const scopesByGroup = getScopesByGroup(t);
|
||||
const allScopeKeys = Object.values(scopesByGroup)
|
||||
.flat()
|
||||
.map((s) => s.scope);
|
||||
const allSelected = allScopeKeys.every((s) => selected.includes(s));
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-end mb-2">
|
||||
<div className="mb-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(allSelected ? [] : allScopeKeys)}
|
||||
className="text-xs px-2 py-0.5 rounded border transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
|
||||
style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}>
|
||||
className="rounded border px-2 py-0.5 text-xs transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
|
||||
style={{ borderColor: 'var(--border-primary)', color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{allSelected ? t('settings.oauth.modal.deselectAll') : t('settings.oauth.modal.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1 max-h-96 overflow-y-auto pr-1">
|
||||
<div className="max-h-96 space-y-1 overflow-y-auto pr-1">
|
||||
{Object.entries(scopesByGroup).map(([group, groupScopes]) => {
|
||||
const groupScopeKeys = groupScopes.map(s => s.scope)
|
||||
const allGroupSelected = groupScopeKeys.every(s => selected.includes(s))
|
||||
const someGroupSelected = groupScopeKeys.some(s => selected.includes(s))
|
||||
const groupScopeKeys = groupScopes.map((s) => s.scope);
|
||||
const allGroupSelected = groupScopeKeys.every((s) => selected.includes(s));
|
||||
const someGroupSelected = groupScopeKeys.some((s) => selected.includes(s));
|
||||
return (
|
||||
<div key={group} className="rounded-lg border overflow-hidden" style={{ borderColor: 'var(--border-primary)' }}>
|
||||
<div
|
||||
key={group}
|
||||
className="overflow-hidden rounded-lg border"
|
||||
style={{ borderColor: 'var(--border-primary)' }}
|
||||
>
|
||||
<div className="flex items-center gap-1 px-3 py-2" style={{ background: 'var(--bg-secondary)' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(prev => ({ ...prev, [group]: !prev[group] }))}
|
||||
className="flex items-center gap-1 flex-1 text-xs font-semibold hover:opacity-70 transition-opacity text-left"
|
||||
style={{ color: 'var(--text-secondary)' }}>
|
||||
{open[group]
|
||||
? <ChevronDown className="w-3 h-3 flex-shrink-0" />
|
||||
: <ChevronRight className="w-3 h-3 flex-shrink-0" />}
|
||||
onClick={() => setOpen((prev) => ({ ...prev, [group]: !prev[group] }))}
|
||||
className="flex flex-1 items-center gap-1 text-left text-xs font-semibold transition-opacity hover:opacity-70"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
>
|
||||
{open[group] ? (
|
||||
<ChevronDown className="h-3 w-3 flex-shrink-0" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
{group}
|
||||
{someGroupSelected && (
|
||||
<span className="ml-1.5 text-xs font-normal" style={{ color: 'var(--text-tertiary)' }}>
|
||||
({groupScopeKeys.filter(s => selected.includes(s)).length}/{groupScopeKeys.length})
|
||||
({groupScopeKeys.filter((s) => selected.includes(s)).length}/{groupScopeKeys.length})
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allGroupSelected}
|
||||
ref={el => { if (el) el.indeterminate = someGroupSelected && !allGroupSelected }}
|
||||
onChange={e => onChange(
|
||||
e.target.checked
|
||||
? [...new Set([...selected, ...groupScopeKeys])]
|
||||
: selected.filter(s => !groupScopeKeys.includes(s))
|
||||
)}
|
||||
ref={(el) => {
|
||||
if (el) el.indeterminate = someGroupSelected && !allGroupSelected;
|
||||
}}
|
||||
onChange={(e) =>
|
||||
onChange(
|
||||
e.target.checked
|
||||
? [...new Set([...selected, ...groupScopeKeys])]
|
||||
: selected.filter((s) => !groupScopeKeys.includes(s))
|
||||
)
|
||||
}
|
||||
className="rounded"
|
||||
title={allGroupSelected ? `Deselect all ${group}` : `Select all ${group}`}
|
||||
/>
|
||||
@@ -68,29 +82,32 @@ export default function ScopeGroupPicker({ selected, onChange }: Props): React.R
|
||||
{groupScopes.map(({ scope, label, description }) => (
|
||||
<label
|
||||
key={scope}
|
||||
className="flex items-start gap-2.5 px-3 py-2 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
|
||||
className="flex cursor-pointer items-start gap-2.5 px-3 py-2 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(scope)}
|
||||
onChange={e => onChange(
|
||||
e.target.checked
|
||||
? [...selected, scope]
|
||||
: selected.filter(s => s !== scope)
|
||||
)}
|
||||
className="mt-0.5 rounded flex-shrink-0"
|
||||
onChange={(e) =>
|
||||
onChange(e.target.checked ? [...selected, scope] : selected.filter((s) => s !== scope))
|
||||
}
|
||||
className="mt-0.5 flex-shrink-0 rounded"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs font-medium" style={{ color: 'var(--text-primary)' }}>{label}</p>
|
||||
<p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>{description}</p>
|
||||
<p className="text-xs font-medium" style={{ color: 'var(--text-primary)' }}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: 'var(--text-tertiary)' }}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user