V13-FE-006: consolidate approved UI and contract hardening
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export function useKbxDirtyState() {
|
||||
const revision = ref(0)
|
||||
const savedRevision = ref(0)
|
||||
const dirty = computed(() => revision.value !== savedRevision.value)
|
||||
|
||||
function touch() { revision.value++ }
|
||||
function markSaved() { savedRevision.value = revision.value }
|
||||
function reset() { revision.value = 0; savedRevision.value = 0 }
|
||||
|
||||
return { dirty, touch, markSaved, reset }
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { onBeforeUnmount, ref } from 'vue'
|
||||
import type { KbxImportProgressEvent } from '@kbx/contracts'
|
||||
|
||||
export interface KbxImportProgressConnection {
|
||||
start(): Promise<void>
|
||||
stop(): Promise<void>
|
||||
subscribe(sessionId: string): Promise<void>
|
||||
unsubscribe(sessionId: string): Promise<void>
|
||||
onProgress(callback: (event: KbxImportProgressEvent) => void): void
|
||||
}
|
||||
|
||||
/**
|
||||
* SignalR is intentionally hidden behind this adapter.
|
||||
* The app shell can provide a @microsoft/signalr implementation without
|
||||
* coupling KbxExcelImport or a vertical slice to HubConnection directly.
|
||||
*/
|
||||
export function useKbxImportProgress(connection: KbxImportProgressConnection | null) {
|
||||
const connected = ref(false)
|
||||
const lastEvent = ref<KbxImportProgressEvent | null>(null)
|
||||
let activeSessionId: string | null = null
|
||||
|
||||
if (connection) {
|
||||
connection.onProgress(event => {
|
||||
if (!activeSessionId || event.sessionId === activeSessionId)
|
||||
lastEvent.value = event
|
||||
})
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (!connection || connected.value) return
|
||||
await connection.start()
|
||||
connected.value = true
|
||||
}
|
||||
|
||||
async function watch(sessionId: string) {
|
||||
if (!connection) return
|
||||
await connect()
|
||||
if (activeSessionId && activeSessionId !== sessionId)
|
||||
await connection.unsubscribe(activeSessionId)
|
||||
activeSessionId = sessionId
|
||||
await connection.subscribe(sessionId)
|
||||
}
|
||||
|
||||
async function dispose() {
|
||||
if (!connection) return
|
||||
if (activeSessionId) await connection.unsubscribe(activeSessionId)
|
||||
if (connected.value) await connection.stop()
|
||||
connected.value = false
|
||||
activeSessionId = null
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => { void dispose() })
|
||||
|
||||
return { connected, lastEvent, watch, dispose }
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import type { KbxShortcutDefinition, KbxShortcutScope } from '@kbx/contracts'
|
||||
import { registerKbxShortcuts } from '../keyboard/manager'
|
||||
|
||||
export interface KbxPageShortcut {
|
||||
key:string
|
||||
enabled?:()=>boolean
|
||||
execute:()=>void|Promise<void>
|
||||
}
|
||||
|
||||
export function useKbxShortcuts(shortcuts:KbxShortcutDefinition[]){
|
||||
let unregister:(()=>void)|undefined
|
||||
onMounted(()=>{unregister=registerKbxShortcuts(shortcuts)})
|
||||
onBeforeUnmount(()=>unregister?.())
|
||||
}
|
||||
|
||||
export function useKbxPageShortcuts(shortcuts:KbxPageShortcut[]){
|
||||
useKbxShortcuts(shortcuts.map(x=>({...x,scope:'page' as KbxShortcutScope})))
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import type { KbxProblem, KbxValidationError } from '@kbx/contracts'
|
||||
|
||||
export function useKbxValidation() {
|
||||
const errors = ref<KbxValidationError[]>([])
|
||||
|
||||
const fieldErrors = computed(() => {
|
||||
const result = new Map<string, string>()
|
||||
for (const error of errors.value) {
|
||||
if (error.field && !error.rowKey && !result.has(error.field)) result.set(error.field, error.message)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
function fieldError(field: string) {
|
||||
return fieldErrors.value.get(field)
|
||||
}
|
||||
|
||||
function setErrors(next: KbxValidationError[]) {
|
||||
errors.value = next
|
||||
}
|
||||
|
||||
function setRowFieldError(rowKey: string, field: string, error?: Omit<KbxValidationError, 'rowKey' | 'field'>) {
|
||||
errors.value = errors.value.filter(x => !(x.rowKey === rowKey && x.field === field))
|
||||
if (error) errors.value.push({ rowKey, field, ...error })
|
||||
}
|
||||
|
||||
function applyProblem(problem: KbxProblem) {
|
||||
errors.value = problem.type === 'validation' ? problem.errors : []
|
||||
}
|
||||
|
||||
function clear() {
|
||||
errors.value = []
|
||||
}
|
||||
|
||||
return { errors, fieldError, setErrors, setRowFieldError, applyProblem, clear }
|
||||
}
|
||||
Reference in New Issue
Block a user