Init Repo
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Upload Limit Section -->
|
||||
<GlobalUploadLimitEditor />
|
||||
|
||||
<!-- All Settings Management -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Settings class="h-5 w-5" />
|
||||
All Global Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage all system-wide configuration settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-4">
|
||||
<!-- Add New Setting -->
|
||||
<div class="border rounded-lg p-4 space-y-3">
|
||||
<h4 class="font-medium">Add New Setting</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Label for="new-key">Setting Key</Label>
|
||||
<Input
|
||||
id="new-key"
|
||||
v-model="newSetting.setting_key"
|
||||
placeholder="e.g., max_concurrent_uploads"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="new-value">Value</Label>
|
||||
<Input
|
||||
id="new-value"
|
||||
v-model="newSetting.setting_value"
|
||||
placeholder="Setting value"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="new-description">Description</Label>
|
||||
<Input
|
||||
id="new-description"
|
||||
v-model="newSetting.description"
|
||||
placeholder="Optional description"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
@click="createNewSetting"
|
||||
:disabled="loading || !canCreateSetting"
|
||||
size="sm"
|
||||
>
|
||||
<Loader2 v-if="loading" class="h-4 w-4 animate-spin mr-2" />
|
||||
<Plus class="h-4 w-4 mr-2" />
|
||||
Add Setting
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Settings List -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-medium">Current Settings</h4>
|
||||
<Button
|
||||
@click="refreshSettings"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="loading"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && allSettings.length === 0" class="text-center py-8">
|
||||
<Loader2 class="h-6 w-6 animate-spin mx-auto mb-2" />
|
||||
<p class="text-sm text-muted-foreground">Loading settings...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="allSettings.length === 0" class="text-center py-8">
|
||||
<Settings class="h-12 w-12 mx-auto mb-2 text-muted-foreground" />
|
||||
<p class="text-sm text-muted-foreground">No settings found</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="setting in allSettings"
|
||||
:key="setting.id"
|
||||
class="border rounded-lg p-3"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1 space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="text-sm font-mono bg-muted px-2 py-1 rounded">
|
||||
{{ setting.setting_key }}
|
||||
</code>
|
||||
<Badge v-if="setting.setting_key === 'global_upload_limit_mb'" variant="secondary">
|
||||
System
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-sm">{{ setting.setting_value }}</p>
|
||||
<p v-if="setting.description" class="text-xs text-muted-foreground">
|
||||
{{ setting.description }}
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Updated: {{ formatDate(setting.updated_at) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
@click="editSetting(setting)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="loading"
|
||||
>
|
||||
<Edit class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
@click="deleteSetting(setting.setting_key)"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="loading || setting.setting_key === 'global_upload_limit_mb'"
|
||||
class="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mt-4 text-sm text-destructive">
|
||||
{{ error }}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Edit Setting Dialog -->
|
||||
<Dialog v-model:open="editDialogOpen">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Setting</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the value and description for this setting
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label>Setting Key</Label>
|
||||
<Input :value="editingSetting?.setting_key" disabled />
|
||||
</div>
|
||||
<div>
|
||||
<Label for="edit-value">Value</Label>
|
||||
<Input
|
||||
id="edit-value"
|
||||
v-model="editForm.setting_value"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="edit-description">Description</Label>
|
||||
<Input
|
||||
id="edit-description"
|
||||
v-model="editForm.description"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="editDialogOpen = false" :disabled="loading">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button @click="updateSetting" :disabled="loading">
|
||||
<Loader2 v-if="loading" class="h-4 w-4 animate-spin mr-2" />
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Settings, Plus, Edit, Trash2, RefreshCw, Loader2 } from 'lucide-vue-next'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useToast } from '@/components/ui/toast/use-toast'
|
||||
import type { GlobalSetting } from '@/services/settings'
|
||||
import GlobalUploadLimitEditor from './GlobalUploadLimitEditor.vue'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const { toast } = useToast()
|
||||
|
||||
const newSetting = ref({
|
||||
setting_key: '',
|
||||
setting_value: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const editDialogOpen = ref(false)
|
||||
const editingSetting = ref<GlobalSetting | null>(null)
|
||||
const editForm = ref({
|
||||
setting_value: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const loading = computed(() => settingsStore.loading)
|
||||
const allSettings = computed(() => settingsStore.allSettings)
|
||||
const error = computed(() => settingsStore.error)
|
||||
|
||||
const canCreateSetting = computed(() =>
|
||||
newSetting.value.setting_key.trim() && newSetting.value.setting_value.trim()
|
||||
)
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
async function createNewSetting() {
|
||||
if (!canCreateSetting.value) return
|
||||
|
||||
try {
|
||||
await settingsStore.createSetting(newSetting.value)
|
||||
|
||||
toast({
|
||||
title: 'Setting created',
|
||||
description: `Setting "${newSetting.value.setting_key}" has been created`
|
||||
})
|
||||
|
||||
// Reset form
|
||||
newSetting.value = {
|
||||
setting_key: '',
|
||||
setting_value: '',
|
||||
description: ''
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to create setting',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function editSetting(setting: GlobalSetting) {
|
||||
editingSetting.value = setting
|
||||
editForm.value = {
|
||||
setting_value: setting.setting_value,
|
||||
description: setting.description || ''
|
||||
}
|
||||
editDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function updateSetting() {
|
||||
if (!editingSetting.value) return
|
||||
|
||||
try {
|
||||
await settingsStore.updateSetting(editingSetting.value.setting_key, editForm.value)
|
||||
|
||||
toast({
|
||||
title: 'Setting updated',
|
||||
description: `Setting "${editingSetting.value.setting_key}" has been updated`
|
||||
})
|
||||
|
||||
editDialogOpen.value = false
|
||||
editingSetting.value = null
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to update setting',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSetting(settingKey: string) {
|
||||
if (settingKey === 'global_upload_limit_mb') {
|
||||
toast({
|
||||
title: 'Cannot delete',
|
||||
description: 'The global upload limit setting cannot be deleted',
|
||||
variant: 'destructive'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to delete the setting "${settingKey}"?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await settingsStore.deleteSetting(settingKey)
|
||||
|
||||
toast({
|
||||
title: 'Setting deleted',
|
||||
description: `Setting "${settingKey}" has been deleted`
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to delete setting',
|
||||
variant: 'destructive'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSettings() {
|
||||
await settingsStore.fetchAllSettings()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
settingsStore.fetchAllSettings()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user