Got lyrics editing working for PATCH
This commit is contained in:
1 parent
4754d61279
commit
522e062a36
3 files changed
+93
-17
No files matched your search
@@ -1,8 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Option } from "~/ui/SearchSelect.vue"
|
import { type Option } from "~/ui/SearchSelect.vue"
|
||||||
|
|
||||||
type InputType = "text" | "file" | "search-select"
|
|
||||||
|
|
||||||
interface BaseField<T> {
|
interface BaseField<T> {
|
||||||
key: keyof T
|
key: keyof T
|
||||||
label?: string
|
label?: string
|
||||||
@@ -12,6 +10,11 @@ interface TextField<T> extends BaseField<T> {
|
|||||||
type?: "text"
|
type?: "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TextAreaField<T> extends BaseField<T> {
|
||||||
|
type?: "textarea"
|
||||||
|
rows: number
|
||||||
|
}
|
||||||
|
|
||||||
interface FileField<T> extends BaseField<T> {
|
interface FileField<T> extends BaseField<T> {
|
||||||
type: "file"
|
type: "file"
|
||||||
}
|
}
|
||||||
@@ -26,11 +29,17 @@ interface SearchSelectArrayField<T> extends BaseField<T> {
|
|||||||
fetchOptions: (q?: string) => Promise<Option[]>
|
fetchOptions: (q?: string) => Promise<Option[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CheckboxField<T> extends BaseField<T> {
|
||||||
|
type?: "checkbox"
|
||||||
|
}
|
||||||
|
|
||||||
export type Field<T = any> =
|
export type Field<T = any> =
|
||||||
| TextField<T>
|
| TextField<T>
|
||||||
|
| TextAreaField<T>
|
||||||
| FileField<T>
|
| FileField<T>
|
||||||
| SearchSelectField<T>
|
| SearchSelectField<T>
|
||||||
| SearchSelectArrayField<T>
|
| SearchSelectArrayField<T>
|
||||||
|
| CheckboxField<T>
|
||||||
|
|
||||||
interface ModalFormProps<T> {
|
interface ModalFormProps<T> {
|
||||||
item?: T
|
item?: T
|
||||||
@@ -44,27 +53,60 @@ const open = defineModel("open", { required: true })
|
|||||||
|
|
||||||
let localItem = reactive<any>({})
|
let localItem = reactive<any>({})
|
||||||
|
|
||||||
|
const getNestedValue = (obj: any, path: string) =>
|
||||||
|
path.split(".").reduce((acc, key) => acc?.[key], obj)
|
||||||
|
|
||||||
|
const setNestedValue = (obj: any, path: string, value: any) => {
|
||||||
|
const keys = path.split(".")
|
||||||
|
keys.reduce((acc, key, i) => {
|
||||||
|
if (i === keys.length - 1) acc[key] = value
|
||||||
|
else acc[key] ??= {}
|
||||||
|
return acc[key]
|
||||||
|
}, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expandDotKeys = (flat: Record<string, any>) => {
|
||||||
|
const result: any = {}
|
||||||
|
for (const [key, value] of Object.entries(flat)) {
|
||||||
|
setNestedValue(result, key, toRaw(value))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const expanded = expandDotKeys(localItem)
|
||||||
|
|
||||||
|
Object.keys(expanded).forEach((k) => {
|
||||||
|
if (expanded[k] == undefined || expanded[k] == null) delete expanded[k]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (await props.onSave(expanded)) open.value = false
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.item,
|
() => props.item,
|
||||||
(item) => {
|
(item) => {
|
||||||
Object.keys(localItem).forEach((k) => delete localItem[k])
|
Object.keys(localItem).forEach((k) => delete localItem[k])
|
||||||
|
|
||||||
if (!item) return
|
if (item)
|
||||||
|
for (const [key, value] of Object.entries(item))
|
||||||
for (const [key, value] of Object.entries(item)) {
|
|
||||||
localItem[key] = value
|
localItem[key] = value
|
||||||
}
|
|
||||||
|
|
||||||
for (const field of props.fields) {
|
for (const field of props.fields) {
|
||||||
if (field.type == "file") localItem[field.key as string] = null
|
const key = field.key as string
|
||||||
|
if (field.type === "file") {
|
||||||
|
localItem[key] = null
|
||||||
|
} else if (field.type === "checkbox") {
|
||||||
|
localItem[key] = item
|
||||||
|
? (getNestedValue(item, key) ?? false)
|
||||||
|
: false
|
||||||
|
} else {
|
||||||
|
localItem[key] = item ? getNestedValue(item, key) : undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
if (await props.onSave(localItem)) open.value = false
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -84,12 +126,19 @@ const save = async () => {
|
|||||||
:predefined="props.item?.[field.key]"
|
:predefined="props.item?.[field.key]"
|
||||||
:multiple="field.type == 'search-select-array'"
|
:multiple="field.type == 'search-select-array'"
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
v-else-if="field.type == 'checkbox'"
|
||||||
|
:label="field.label || field.key"
|
||||||
|
@change="(v) => (localItem[field.key] = v)"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-else
|
v-else
|
||||||
:type="field.type"
|
:type="field.type"
|
||||||
:key="field.key"
|
:key="field.key"
|
||||||
:label="field.label || field.key"
|
:label="field.label || field.key"
|
||||||
v-model:value="localItem[field.key]"
|
v-model:value="localItem[field.key]"
|
||||||
|
:textarea="field.type == 'textarea'"
|
||||||
|
:rows="field.type == 'textarea' ? field.rows : 0"
|
||||||
full
|
full
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ const modalFields: Field[] = [
|
|||||||
type: "search-select",
|
type: "search-select",
|
||||||
fetchOptions: albumFetchOptions,
|
fetchOptions: albumFetchOptions,
|
||||||
},
|
},
|
||||||
|
{ key: "lyrics.text", label: "Lyrics", type: "textarea", rows: 6 },
|
||||||
|
{ key: "lyrics.synced", label: "Synced lyrics", type: "checkbox" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const showModal = ref<boolean>(false)
|
const showModal = ref<boolean>(false)
|
||||||
@@ -73,7 +75,10 @@ const save = async (value: any) => {
|
|||||||
try {
|
try {
|
||||||
await cfetch("/admin/track", {
|
await cfetch("/admin/track", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
data: value,
|
data: {
|
||||||
|
...value,
|
||||||
|
lyrics: JSON.stringify(value.lyrics),
|
||||||
|
},
|
||||||
params: { id: value._id },
|
params: { id: value._id },
|
||||||
forceFormData: true,
|
forceFormData: true,
|
||||||
})
|
})
|
||||||
@@ -88,7 +93,10 @@ const save = async (value: any) => {
|
|||||||
try {
|
try {
|
||||||
await cfetch("/admin/track", {
|
await cfetch("/admin/track", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: value,
|
data: {
|
||||||
|
...value,
|
||||||
|
lyrics: JSON.stringify(value.lyrics),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.show("Song added successfully", "success")
|
toast.show("Song added successfully", "success")
|
||||||
@@ -137,8 +145,6 @@ const show = (id?: string) => {
|
|||||||
|
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(data, () => console.log(data))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
+23
-2
@@ -11,6 +11,8 @@ interface InputProps {
|
|||||||
required?: boolean
|
required?: boolean
|
||||||
autocomplete?: AutocompleteType
|
autocomplete?: AutocompleteType
|
||||||
label?: string
|
label?: string
|
||||||
|
textarea?: boolean
|
||||||
|
rows?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = defineModel<string | File | File[] | null>("value")
|
const value = defineModel<string | File | File[] | null>("value")
|
||||||
@@ -63,7 +65,7 @@ const onFileChange = (e: Event) => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-if="props.type != 'file'"
|
v-if="props.type != 'file' && props.type != 'textarea'"
|
||||||
:placeholder="props.placeholder"
|
:placeholder="props.placeholder"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
@input="(e) => emit('input', e.target?.value)"
|
@input="(e) => emit('input', e.target?.value)"
|
||||||
@@ -76,6 +78,20 @@ const onFileChange = (e: Event) => {
|
|||||||
:autocomplete="props.autocomplete"
|
:autocomplete="props.autocomplete"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-else-if="props.textarea"
|
||||||
|
:placeholder="props.placeholder"
|
||||||
|
v-model="value"
|
||||||
|
@input="(e) => emit('input', e.target?.value)"
|
||||||
|
@focus="emit('focus')"
|
||||||
|
@blur="emit('blur')"
|
||||||
|
:name="props.name"
|
||||||
|
:id="props.name"
|
||||||
|
:required="props.required"
|
||||||
|
:autocomplete="props.autocomplete"
|
||||||
|
:rows="props.rows"
|
||||||
|
/>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
@focus="emit('focus')"
|
@focus="emit('focus')"
|
||||||
@@ -120,7 +136,8 @@ const onFileChange = (e: Event) => {
|
|||||||
outline: 2px solid $color-accent;
|
outline: 2px solid $color-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
& input {
|
& input,
|
||||||
|
& textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
@@ -132,6 +149,10 @@ const onFileChange = (e: Event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
& input:focus {
|
& input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user