feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s
- Implemented PendingRewardDialog for handling pending reward requests. - Created RewardConfirmDialog for confirming reward redemption. - Developed TaskConfirmDialog for task confirmation with child name display. test: add unit tests for ChildView and ParentView components - Added comprehensive tests for ChildView including task triggering and SSE event handling. - Implemented tests for ParentView focusing on override modal and SSE event management. test: add ScrollingList component tests - Created tests for ScrollingList to verify item fetching, loading states, and custom item classes. - Included tests for two-step click interactions and edit button display logic. - Moved toward hashed passwords.
This commit is contained in:
205
frontend/vue-app/src/components/OverrideEditModal.vue
Normal file
205
frontend/vue-app/src/components/OverrideEditModal.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<ModalDialog v-if="isOpen" @backdrop-click="$emit('close')">
|
||||
<div class="override-edit-modal">
|
||||
<h3>Edit {{ entityName }}</h3>
|
||||
<div class="modal-body">
|
||||
<label :for="`override-input-${entityId}`">
|
||||
{{ entityType === 'task' ? 'New Points' : 'New Cost' }}:
|
||||
</label>
|
||||
<input
|
||||
:id="`override-input-${entityId}`"
|
||||
v-model.number="inputValue"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10000"
|
||||
:disabled="loading"
|
||||
@input="validateInput"
|
||||
/>
|
||||
<div v-if="errorMessage" class="error-message">{{ errorMessage }}</div>
|
||||
<div class="default-hint">Default: {{ defaultValue }}</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @click="$emit('close')" :disabled="loading" class="btn-secondary">Cancel</button>
|
||||
<button @click="save" :disabled="!isValid || loading" class="btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import ModalDialog from './shared/ModalDialog.vue'
|
||||
import { setChildOverride, parseErrorResponse } from '@/common/api'
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean
|
||||
childId: string
|
||||
entityId: string
|
||||
entityType: 'task' | 'reward'
|
||||
entityName: string
|
||||
defaultValue: number
|
||||
currentOverride?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
saved: []
|
||||
}>()
|
||||
|
||||
const inputValue = ref<number>(0)
|
||||
const errorMessage = ref<string>('')
|
||||
const isValid = ref<boolean>(true)
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
// Initialize input value when modal opens
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
inputValue.value = props.currentOverride ?? props.defaultValue
|
||||
validateInput()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function validateInput() {
|
||||
const value = inputValue.value
|
||||
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
errorMessage.value = 'Please enter a valid number'
|
||||
isValid.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (value < 0 || value > 10000) {
|
||||
errorMessage.value = 'Value must be between 0 and 10000'
|
||||
isValid.value = false
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage.value = ''
|
||||
isValid.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!isValid.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await setChildOverride(
|
||||
props.childId,
|
||||
props.entityId,
|
||||
props.entityType,
|
||||
inputValue.value,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const { msg } = parseErrorResponse(response)
|
||||
alert(`Error: ${msg}`)
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
emit('saved')
|
||||
emit('close')
|
||||
} catch (error) {
|
||||
alert(`Error: ${error}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.override-edit-modal {
|
||||
background: var(--modal-bg);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.override-edit-modal h3 {
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.modal-body label {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-body input[type='number'] {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: var(--font-size-md);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-body input[type='number']:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--error-color);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.default-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-actions button {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-size: var(--font-size-md);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.modal-actions button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--btn-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--btn-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user