475 lines
12 KiB
Vue
475 lines
12 KiB
Vue
<template>
|
|
<v-dialog
|
|
:model-value="modelValue"
|
|
@update:model-value="$emit('update:model-value', $event)"
|
|
max-width="700"
|
|
persistent
|
|
scrollable
|
|
>
|
|
<v-card>
|
|
<v-card-title class="d-flex align-center pa-6 bg-primary">
|
|
<v-icon class="mr-3 text-white">mdi-database-cog</v-icon>
|
|
<h2 class="text-h5 text-white font-weight-bold flex-grow-1">
|
|
NocoDB Configuration
|
|
</h2>
|
|
<v-btn
|
|
icon
|
|
variant="text"
|
|
color="white"
|
|
@click="closeDialog"
|
|
>
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
</v-card-title>
|
|
|
|
<v-card-text class="pa-6">
|
|
<v-alert
|
|
type="info"
|
|
variant="tonal"
|
|
class="mb-4"
|
|
>
|
|
<template #title>Admin Only Configuration</template>
|
|
Configure the NocoDB database connection for the Member Management system.
|
|
These settings will override environment variables when set.
|
|
</v-alert>
|
|
|
|
<v-form ref="formRef" v-model="formValid">
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<h3 class="text-h6 mb-4 text-primary">Database Connection</h3>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="form.url"
|
|
label="NocoDB URL"
|
|
variant="outlined"
|
|
:rules="[rules.required, rules.url]"
|
|
required
|
|
placeholder="https://database.monacousa.org"
|
|
:error="hasFieldError('url')"
|
|
:error-messages="getFieldError('url')"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="form.apiKey"
|
|
label="API Token"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
:type="showApiKey ? 'text' : 'password'"
|
|
:append-inner-icon="showApiKey ? 'mdi-eye' : 'mdi-eye-off'"
|
|
@click:append-inner="showApiKey = !showApiKey"
|
|
placeholder="Enter your NocoDB API token"
|
|
:error="hasFieldError('apiKey')"
|
|
:error-messages="getFieldError('apiKey')"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="form.baseId"
|
|
label="Base ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
placeholder="your-base-id"
|
|
:error="hasFieldError('baseId')"
|
|
:error-messages="getFieldError('baseId')"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<h3 class="text-h6 mb-4 text-primary">Table Configuration</h3>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="form.tables.members"
|
|
label="Members Table ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
placeholder="members-table-id"
|
|
:error="hasFieldError('tables.members')"
|
|
:error-messages="getFieldError('tables.members')"
|
|
/>
|
|
<div class="text-caption text-medium-emphasis mt-1">
|
|
Configure the table ID for the Members functionality
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="form.tables.events"
|
|
label="Events Table ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
placeholder="events-table-id"
|
|
:error="hasFieldError('tables.events')"
|
|
:error-messages="getFieldError('tables.events')"
|
|
/>
|
|
<div class="text-caption text-medium-emphasis mt-1">
|
|
Configure the table ID for the Events functionality
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-text-field
|
|
v-model="form.tables.rsvps"
|
|
label="RSVPs Table ID"
|
|
variant="outlined"
|
|
:rules="[rules.required]"
|
|
required
|
|
placeholder="rsvps-table-id"
|
|
:error="hasFieldError('tables.rsvps')"
|
|
:error-messages="getFieldError('tables.rsvps')"
|
|
/>
|
|
<div class="text-caption text-medium-emphasis mt-1">
|
|
Configure the table ID for the Event RSVPs functionality
|
|
</div>
|
|
</v-col>
|
|
|
|
<v-col cols="12">
|
|
<v-divider class="my-2" />
|
|
</v-col>
|
|
|
|
<!-- Connection Status -->
|
|
<v-col cols="12" md="6">
|
|
<v-btn
|
|
@click="testConnection"
|
|
:loading="testLoading"
|
|
:disabled="!formValid || loading"
|
|
color="info"
|
|
variant="outlined"
|
|
block
|
|
>
|
|
<v-icon start>mdi-database-check</v-icon>
|
|
Test Connection
|
|
</v-btn>
|
|
</v-col>
|
|
|
|
<v-col cols="12" md="6">
|
|
<div class="d-flex align-center h-100">
|
|
<v-chip
|
|
v-if="connectionStatus"
|
|
:color="connectionStatus.success ? 'success' : 'error'"
|
|
variant="flat"
|
|
size="small"
|
|
>
|
|
<v-icon start size="14">
|
|
{{ connectionStatus.success ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
|
</v-icon>
|
|
{{ connectionStatus.message }}
|
|
</v-chip>
|
|
<span v-else class="text-caption text-medium-emphasis">
|
|
Connection not tested
|
|
</span>
|
|
</div>
|
|
</v-col>
|
|
|
|
<!-- Display errors -->
|
|
<v-col cols="12" v-if="hasGeneralError">
|
|
<v-alert
|
|
type="error"
|
|
variant="tonal"
|
|
closable
|
|
@click:close="clearGeneralError"
|
|
>
|
|
{{ getGeneralError }}
|
|
</v-alert>
|
|
</v-col>
|
|
|
|
<!-- Display success -->
|
|
<v-col cols="12" v-if="showSuccessMessage">
|
|
<v-alert
|
|
type="success"
|
|
variant="tonal"
|
|
closable
|
|
@click:close="showSuccessMessage = false"
|
|
>
|
|
NocoDB configuration saved successfully!
|
|
</v-alert>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
</v-card-text>
|
|
|
|
<v-card-actions class="pa-6 pt-0">
|
|
<v-spacer />
|
|
<v-btn
|
|
variant="text"
|
|
@click="closeDialog"
|
|
:disabled="loading"
|
|
>
|
|
Cancel
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
@click="saveSettings"
|
|
:loading="loading"
|
|
:disabled="!formValid"
|
|
>
|
|
<v-icon start>mdi-content-save</v-icon>
|
|
Save Configuration
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { NocoDBSettings } from '~/utils/types';
|
|
|
|
interface Props {
|
|
modelValue: boolean;
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'update:model-value', value: boolean): void;
|
|
(e: 'settings-saved'): void;
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
const emit = defineEmits<Emits>();
|
|
|
|
// Form state
|
|
const formRef = ref();
|
|
const formValid = ref(false);
|
|
const loading = ref(false);
|
|
const testLoading = ref(false);
|
|
const showApiKey = ref(false);
|
|
const showSuccessMessage = ref(false);
|
|
|
|
// Form data
|
|
const form = ref<NocoDBSettings>({
|
|
url: 'https://database.monacousa.org',
|
|
apiKey: '',
|
|
baseId: '',
|
|
tables: {
|
|
members: '',
|
|
events: '',
|
|
rsvps: ''
|
|
}
|
|
});
|
|
|
|
// Error handling
|
|
const fieldErrors = ref<Record<string, string>>({});
|
|
const connectionStatus = ref<{ success: boolean; message: string } | null>(null);
|
|
|
|
// Validation rules
|
|
const rules = {
|
|
required: (value: string) => {
|
|
return !!value?.trim() || 'This field is required';
|
|
},
|
|
url: (value: string) => {
|
|
if (!value) return true; // Let required rule handle empty values
|
|
const pattern = /^https?:\/\/.+/;
|
|
return pattern.test(value) || 'Please enter a valid URL';
|
|
}
|
|
};
|
|
|
|
// Error handling methods
|
|
const hasFieldError = (fieldName: string) => {
|
|
return !!fieldErrors.value[fieldName];
|
|
};
|
|
|
|
const getFieldError = (fieldName: string) => {
|
|
return fieldErrors.value[fieldName] || '';
|
|
};
|
|
|
|
const hasGeneralError = computed(() => {
|
|
return !!fieldErrors.value.general;
|
|
});
|
|
|
|
const getGeneralError = computed(() => {
|
|
return fieldErrors.value.general || '';
|
|
});
|
|
|
|
const clearFieldErrors = () => {
|
|
fieldErrors.value = {};
|
|
};
|
|
|
|
const clearGeneralError = () => {
|
|
delete fieldErrors.value.general;
|
|
};
|
|
|
|
// Load current settings
|
|
const loadSettings = async () => {
|
|
try {
|
|
const response = await $fetch<{ success: boolean; data?: NocoDBSettings }>('/api/admin/nocodb-config');
|
|
|
|
if (response.success && response.data) {
|
|
form.value = { ...response.data };
|
|
// Ensure tables object exists with all required fields
|
|
if (!form.value.tables) {
|
|
form.value.tables = {
|
|
members: '',
|
|
events: '',
|
|
rsvps: ''
|
|
};
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Failed to load NocoDB settings:', error);
|
|
// Use defaults if loading fails
|
|
}
|
|
};
|
|
|
|
// Test connection
|
|
const testConnection = async () => {
|
|
if (!formRef.value) return;
|
|
|
|
const isValid = await formRef.value.validate();
|
|
if (!isValid.valid) {
|
|
return;
|
|
}
|
|
|
|
testLoading.value = true;
|
|
connectionStatus.value = null;
|
|
|
|
try {
|
|
const response = await $fetch<{ success: boolean; message: string }>('/api/admin/nocodb-test', {
|
|
method: 'POST',
|
|
body: form.value
|
|
});
|
|
|
|
connectionStatus.value = {
|
|
success: response.success,
|
|
message: response.message || (response.success ? 'Connection successful' : 'Connection failed')
|
|
};
|
|
} catch (error: any) {
|
|
connectionStatus.value = {
|
|
success: false,
|
|
message: error.message || 'Connection test failed'
|
|
};
|
|
} finally {
|
|
testLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Save settings
|
|
const saveSettings = async () => {
|
|
if (!formRef.value) return;
|
|
|
|
const isValid = await formRef.value.validate();
|
|
if (!isValid.valid) {
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
clearFieldErrors();
|
|
|
|
try {
|
|
const response = await $fetch<{ success: boolean; message?: string }>('/api/admin/nocodb-config', {
|
|
method: 'POST',
|
|
body: form.value
|
|
});
|
|
|
|
if (response.success) {
|
|
showSuccessMessage.value = true;
|
|
emit('settings-saved');
|
|
|
|
// Auto-close after a delay
|
|
setTimeout(() => {
|
|
closeDialog();
|
|
}, 2000);
|
|
} else {
|
|
throw new Error(response.message || 'Failed to save settings');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error saving NocoDB settings:', error);
|
|
|
|
if (error.data?.fieldErrors) {
|
|
fieldErrors.value = error.data.fieldErrors;
|
|
} else {
|
|
fieldErrors.value.general = error.message || 'Failed to save NocoDB configuration. Please try again.';
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Dialog management
|
|
const closeDialog = () => {
|
|
emit('update:model-value', false);
|
|
};
|
|
|
|
const resetForm = () => {
|
|
form.value = {
|
|
url: 'https://database.monacousa.org',
|
|
apiKey: '',
|
|
baseId: '',
|
|
tables: {
|
|
members: '',
|
|
events: '',
|
|
rsvps: ''
|
|
}
|
|
};
|
|
clearFieldErrors();
|
|
connectionStatus.value = null;
|
|
showSuccessMessage.value = false;
|
|
|
|
nextTick(() => {
|
|
formRef.value?.resetValidation();
|
|
});
|
|
};
|
|
|
|
// Watch for dialog open
|
|
watch(() => props.modelValue, async (newValue) => {
|
|
if (newValue) {
|
|
resetForm();
|
|
await loadSettings();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.bg-primary {
|
|
background: linear-gradient(135deg, #a31515 0%, #d32f2f 100%) !important;
|
|
}
|
|
|
|
.text-primary {
|
|
color: #a31515 !important;
|
|
}
|
|
|
|
.v-card {
|
|
border-radius: 12px !important;
|
|
}
|
|
|
|
/* Form section styling */
|
|
.v-card-text .v-row .v-col h3 {
|
|
border-bottom: 2px solid rgba(var(--v-theme-primary), 0.12);
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
/* Connection status styling */
|
|
.h-100 {
|
|
height: 100%;
|
|
}
|
|
|
|
/* Password field styling */
|
|
.v-text-field :deep(.v-input__append-inner) {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Responsive adjustments */
|
|
@media (max-width: 600px) {
|
|
.v-card-title {
|
|
padding: 16px !important;
|
|
}
|
|
|
|
.v-card-text {
|
|
padding: 16px !important;
|
|
}
|
|
|
|
.v-card-actions {
|
|
padding: 16px !important;
|
|
padding-top: 0 !important;
|
|
}
|
|
}
|
|
</style>
|