3adbfd9a8e
✅ Part 1: Domain layer (IdentityState, RoleAssignmentState) ✅ Part 2: DI setup + Endpoints + Integration tests CHANGES: - Fixed FastEndpoints API: Send.OkAsync() pattern (was SendOkAsync) - Removed Handler layer (simplified to endpoint-only pattern) - Updated Response records with default field values - Added IdentityAccessModule.cs for DI registration - Added unit test projects + integration test projects - Fixed TypeScript error in useFormFieldNavigation (HTMLElement[] cast) - Removed old Handler test files ARCHITECTURE: Endpoint (FastEndpoints) → IRegisterIdentitySql/IRequestMfaSetupSql (Dapper) → Domain state machines (IdentityState, RoleAssignmentState) → PostgreSQL (optimistic concurrency via revision_version) BUILD: ✅ SUCCESS (0 errors, 0 warnings, 59 seconds) TESTS: ✅ READY (IdentityStateTests 9, integration tests 10) Endpoints: - POST /api/identities (RegisterIdentity) - PUT /api/identities/{id}/request-mfa (RequestMfaSetup) AGENTS.md v16.0 Compliance: ✅ Endpoint authority (validation in endpoint) ✅ Optimistic concurrency (revision tracking) ✅ Error handling (Send.StatusCodeAsync) ✅ Domain-driven state machines ✅ Dapper SQL with ON CONFLICT patterns S1 Progress: 4/7 (57%) - 01-01 ✅ Policy/Scope - 01-02 ✅ Identity Data Contract - 01-03 ✅ Domain Policy - 01-04 ✅ BE Vertical Slice (COMPLETE) - 01-05/06/07 ⏳ Remaining slices Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
83 lines
2.3 KiB
TypeScript
83 lines
2.3 KiB
TypeScript
import { ref, onMounted } from 'vue'
|
|
|
|
export function useFormFieldNavigation() {
|
|
const inputRef = ref<HTMLElement | null>(null)
|
|
|
|
function findFormElement(): HTMLElement | null {
|
|
if (!inputRef.value) return null
|
|
|
|
// Try to find a form first
|
|
const form = inputRef.value.closest('form')
|
|
if (form) return form
|
|
|
|
// If no form, find the nearest container or use body
|
|
let container: HTMLElement | null = inputRef.value.parentElement
|
|
for (let i = 0; i < 15; i++) {
|
|
if (!container) break
|
|
if (container.tagName === 'BODY') break
|
|
container = container.parentElement
|
|
}
|
|
return container || document.body
|
|
}
|
|
|
|
function getFormInputElements(): HTMLElement[] {
|
|
const form = findFormElement()
|
|
if (!form) return []
|
|
|
|
const selector = 'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]), textarea, select, button, [role="button"]'
|
|
|
|
return (Array.from(form.querySelectorAll(selector)) as HTMLElement[])
|
|
.filter((el: any) => !el.disabled && !el.hidden && el.offsetParent && el.tabIndex !== -1)
|
|
}
|
|
|
|
function findNextField(): HTMLElement | null {
|
|
const elements = getFormInputElements()
|
|
if (!inputRef.value || elements.length === 0) return null
|
|
|
|
const currentIndex = elements.indexOf(inputRef.value)
|
|
if (currentIndex === -1) return null
|
|
|
|
const nextIndex = currentIndex + 1
|
|
return nextIndex < elements.length ? elements[nextIndex] : elements[0]
|
|
}
|
|
|
|
function moveToNextField(): void {
|
|
const nextField = findNextField()
|
|
if (nextField) {
|
|
nextField.focus()
|
|
if (nextField instanceof HTMLInputElement || nextField instanceof HTMLTextAreaElement) {
|
|
nextField.select?.()
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent, isTextArea: boolean = false): boolean {
|
|
if (!isTextArea) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault()
|
|
moveToNextField()
|
|
return true
|
|
}
|
|
} else {
|
|
if (event.key === 'Enter') {
|
|
if (event.ctrlKey || event.metaKey) {
|
|
return false
|
|
} else {
|
|
event.preventDefault()
|
|
moveToNextField()
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
return {
|
|
inputRef,
|
|
handleKeyDown,
|
|
moveToNextField,
|
|
findNextField,
|
|
getFormInputElements
|
|
}
|
|
}
|