330 lines
9.2 KiB
Vue
330 lines
9.2 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
import { useServicesStore } from '@ChronoManager/stores/servicesStore'
|
|
import { CollectionObject } from '@ChronoManager/models/collection'
|
|
import { ServiceObject } from '@ChronoManager/models/service'
|
|
|
|
// Store
|
|
const servicesStore = useServicesStore()
|
|
|
|
// Props
|
|
const props = defineProps<{
|
|
modelValue: boolean
|
|
collection: CollectionObject | null
|
|
mode: 'create' | 'edit'
|
|
type?: 'calendar' | 'tasklist'
|
|
}>()
|
|
|
|
// Emits
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean]
|
|
'save': [collection: CollectionObject, service: ServiceObject]
|
|
'delete': [collection: CollectionObject]
|
|
}>()
|
|
|
|
// State
|
|
const services = ref<ServiceObject[]>([])
|
|
const editingCollection = ref<CollectionObject | null>(null)
|
|
const editingCollectionService = ref<ServiceObject | null>(null)
|
|
const editingCollectionValidated = ref(false)
|
|
const colorMenuOpen = ref(false)
|
|
const colorOptions = [
|
|
'#FF6B6B', '#FF8E4D', '#FFB347', '#FFD166', '#E5FF7F', '#B5E86D',
|
|
'#7CD992', '#46CBB7', '#2ECCCB', '#27B1E6', '#2196F3', '#2E6FE3',
|
|
'#3A4CC9', '#5A3FC0', '#7F3FBF', '#B339AE', '#DE3A9B', '#FF6FB7'
|
|
]
|
|
|
|
const COLORS_PER_ROW = 6
|
|
const colorRows = computed(() => {
|
|
const rows: string[][] = []
|
|
for (let i = 0; i < colorOptions.length; i += COLORS_PER_ROW) {
|
|
rows.push(colorOptions.slice(i, i + COLORS_PER_ROW))
|
|
}
|
|
return rows
|
|
})
|
|
|
|
// Computed
|
|
const dialogOpen = computed({
|
|
get: () => props.modelValue,
|
|
set: (value: boolean) => emit('update:modelValue', value)
|
|
})
|
|
|
|
const dialogTitle = computed(() => {
|
|
const itemType = props.type === 'tasklist' ? 'Task List' : 'Calendar'
|
|
return props.mode === 'create' ? `Create New ${itemType}` : `Edit ${itemType}`
|
|
})
|
|
|
|
const dialogIcon = computed(() => {
|
|
return props.type === 'tasklist' ? 'mdi-checkbox-marked-outline' : 'mdi-calendar'
|
|
})
|
|
|
|
// Functions
|
|
const onOpen = async () => {
|
|
if (services.value.length === 0) {
|
|
services.value = Object.values(await servicesStore.list())
|
|
}
|
|
|
|
if (!props.collection) {
|
|
return
|
|
}
|
|
|
|
// Clone the collection to avoid mutating the original
|
|
editingCollection.value = props.collection.clone()
|
|
|
|
if (props.mode === 'edit') {
|
|
// Edit mode - find the service
|
|
editingCollectionService.value = services.value.find(s =>
|
|
s.provider === props.collection!.provider && s.identifier === props.collection!.service
|
|
) || null
|
|
} else {
|
|
// Create mode - use first service that can create
|
|
editingCollectionService.value = services.value.filter(s => s.capabilities?.CollectionCreate)[0] || null
|
|
}
|
|
}
|
|
|
|
const onSave = () => {
|
|
if (!editingCollection.value || !editingCollectionService.value) {
|
|
return
|
|
}
|
|
emit('save', editingCollection.value, editingCollectionService.value)
|
|
dialogOpen.value = false
|
|
}
|
|
|
|
const onDelete = () => {
|
|
if (!editingCollection.value) {
|
|
return
|
|
}
|
|
emit('delete', editingCollection.value)
|
|
dialogOpen.value = false
|
|
}
|
|
|
|
const onColorSelect = (color: string | null, closeMenu = true) => {
|
|
if (!editingCollection.value) {
|
|
return
|
|
}
|
|
editingCollection.value.properties.color = color
|
|
if (closeMenu) {
|
|
colorMenuOpen.value = false
|
|
}
|
|
}
|
|
|
|
const onCustomColorChange = (color: string) => {
|
|
if (!color) {
|
|
return
|
|
}
|
|
onColorSelect(color, false)
|
|
}
|
|
|
|
// Watch for dialog opening
|
|
watch(() => props.modelValue, async (newValue) => {
|
|
if (newValue) {
|
|
await onOpen()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<v-dialog
|
|
v-model="dialogOpen"
|
|
max-width="500"
|
|
persistent
|
|
>
|
|
<v-card>
|
|
<v-card-title>
|
|
<v-icon :icon="dialogIcon" class="mr-2" />
|
|
{{ dialogTitle }}
|
|
</v-card-title>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-text v-if="editingCollection" class="pt-4">
|
|
<v-form v-model="editingCollectionValidated" ref="collectionEditor">
|
|
<v-combobox v-show="mode === 'create' && services.length > 1"
|
|
v-model="editingCollectionService"
|
|
label="Service"
|
|
:items="services.filter(s => s.capabilities?.CollectionCreate)"
|
|
item-title="label"
|
|
item-value="identifier"
|
|
required
|
|
:rules="[(v: ServiceObject) => !!v || 'Service is required']"
|
|
/>
|
|
|
|
<div v-if="mode === 'edit'" class="mb-4"><strong>Service:</strong> {{ editingCollection.service }}</div>
|
|
|
|
<v-text-field
|
|
v-model="editingCollection.properties.label"
|
|
label="Label"
|
|
required
|
|
:rules="[(v: string) => !!v || 'Label is required']"
|
|
>
|
|
<template #append-inner>
|
|
<v-menu
|
|
v-model="colorMenuOpen"
|
|
:close-on-content-click="false"
|
|
location="bottom end"
|
|
offset="8"
|
|
>
|
|
<template #activator="{ props: menuProps }">
|
|
<v-btn
|
|
v-bind="menuProps"
|
|
icon
|
|
variant="text"
|
|
size="small"
|
|
:style="{ color: editingCollection.properties.color || 'var(--v-theme-on-surface)' }"
|
|
aria-label="Select color"
|
|
title="Select color"
|
|
>
|
|
<v-icon icon="mdi-palette" />
|
|
</v-btn>
|
|
</template>
|
|
|
|
<v-card class="pa-2 color-menu" elevation="4">
|
|
<div class="color-menu-header">
|
|
<v-btn
|
|
variant="text"
|
|
size="small"
|
|
class="color-menu-header__close"
|
|
aria-label="Close"
|
|
title="Close"
|
|
@click="colorMenuOpen = false"
|
|
>
|
|
<v-icon icon="mdi-close" />
|
|
</v-btn>
|
|
</div>
|
|
<div class="color-menu-body__presets">
|
|
<div
|
|
v-for="(rowColors, rowIndex) in colorRows"
|
|
:key="`color-row-${rowIndex}`"
|
|
class="color-menu-body__row"
|
|
>
|
|
<v-btn
|
|
v-for="color in rowColors"
|
|
:key="color"
|
|
variant="flat"
|
|
size="small"
|
|
class="color-menu-body__presets--swatch"
|
|
:class="{ 'color-menu-body__presets--swatch--active': editingCollection.properties.color === color }"
|
|
:style="{ backgroundColor: color }"
|
|
@click="onColorSelect(color)">
|
|
<v-icon
|
|
v-if="editingCollection.properties.color === color"
|
|
icon="mdi-check"
|
|
size="x-small"
|
|
color="white"
|
|
/>
|
|
</v-btn>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-menu-body__picker">
|
|
<v-color-picker
|
|
v-model="editingCollection.properties.color"
|
|
mode="hex"
|
|
hide-canvas
|
|
width="100%"
|
|
elevation="0"
|
|
@update:model-value="onCustomColorChange"
|
|
/>
|
|
</div>
|
|
</v-card>
|
|
</v-menu>
|
|
</template>
|
|
</v-text-field>
|
|
|
|
<v-textarea
|
|
v-model="editingCollection.properties.description"
|
|
label="Description"
|
|
rows="2"
|
|
/>
|
|
|
|
<v-row>
|
|
<v-col v-if="mode === 'edit'" cols="6">
|
|
<v-switch
|
|
v-model="editingCollection.properties.visibility"
|
|
label="Enabled"
|
|
color="primary"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
</v-card-text>
|
|
|
|
<v-divider />
|
|
|
|
<v-card-actions class="justify-space-between align-center">
|
|
<div>
|
|
<v-btn
|
|
v-if="mode === 'edit' && editingCollectionService?.capabilities?.CollectionDestroy"
|
|
color="error"
|
|
variant="text"
|
|
@click="onDelete"
|
|
>
|
|
Delete
|
|
</v-btn>
|
|
</div>
|
|
|
|
<div class="d-flex align-center">
|
|
<v-btn
|
|
variant="text"
|
|
@click="dialogOpen = false"
|
|
>
|
|
Cancel
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
variant="flat"
|
|
:disabled="!editingCollectionValidated"
|
|
@click="onSave"
|
|
>
|
|
{{ mode === 'create' ? 'Create' : 'Save' }}
|
|
</v-btn>
|
|
</div>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.color-menu {
|
|
max-width: 320px;
|
|
}
|
|
|
|
.color-menu-header {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
width: 100%;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.color-menu-header__close {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.color-menu-body__presets {
|
|
width: 100%;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.color-menu-body__row {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.color-menu-body__picker {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.color-menu-body__presets--swatch {
|
|
min-width: 32px !important;
|
|
height: 32px !important;
|
|
padding: 0 !important;
|
|
border-radius: 4px !important;
|
|
position: relative;
|
|
}
|
|
|
|
.color-menu-body__presets--swatch--active {
|
|
outline: 2px solid rgba(var(--v-theme-on-surface), 0.5);
|
|
outline-offset: 2px;
|
|
}
|
|
</style>
|