#137 Deployment: **Upgrade to Liquidsoap 2.4**: code has been adapted to work with liquidsoap 2.4 Co-authored-by: bkfox <thomas bkfox net> Reviewed-on: #138
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<component :is="tag" @click.capture.stop="call" type="button" :class="buttonClass">
|
||||
<component :is="tag" @click.capture.stop="call" type="button" :class="[buttonClass, this.promise && 'blink' || '']">
|
||||
<span v-if="promise && runIcon">
|
||||
<i :class="runIcon"></i>
|
||||
</span>
|
||||
|
@ -94,9 +94,13 @@ export default {
|
||||
this.inputValue = value
|
||||
},
|
||||
|
||||
inputValue(value) {
|
||||
if(value != this.inputValue && value != this.modelValue)
|
||||
inputValue(value, old) {
|
||||
if(value != old && value != this.modelValue) {
|
||||
this.$emit('update:modelValue', value)
|
||||
this.$emit('change', {target: this.$refs.input})
|
||||
}
|
||||
if(this.selectedLabel != value)
|
||||
this.selectedIndex = -1
|
||||
},
|
||||
},
|
||||
|
||||
@ -176,8 +180,11 @@ export default {
|
||||
},
|
||||
|
||||
onBlur(event) {
|
||||
var index = event.relatedTarget && event.relatedTarget.dataset.autocompleteIndex;
|
||||
if(index !== undefined)
|
||||
if(!this.items.length)
|
||||
return
|
||||
|
||||
var index = event.relatedTarget && Math.parseInt(event.relatedTarget.dataset.autocompleteIndex);
|
||||
if(index !== undefined && index !== null)
|
||||
this.select(index, false, false)
|
||||
this.cursor = -1;
|
||||
},
|
||||
|
@ -29,7 +29,7 @@
|
||||
import {getCsrf} from "../model"
|
||||
|
||||
export default {
|
||||
emit: ["fileChange", "load"],
|
||||
emit: ["fileChange", "load", "abort", "error"],
|
||||
|
||||
props: {
|
||||
url: { type: String },
|
||||
@ -71,9 +71,9 @@ export default {
|
||||
const req = new XMLHttpRequest()
|
||||
req.open("POST", this.url)
|
||||
req.upload.addEventListener("progress", (e) => this.onUploadProgress(e))
|
||||
req.addEventListener("load", (e) => this.onUploadDone(e))
|
||||
req.addEventListener("abort", (e) => this.onUploadDone(e))
|
||||
req.addEventListener("error", (e) => this.onUploadDone(e))
|
||||
req.addEventListener("load", (e) => this.onUploadDone(e, 'load'))
|
||||
req.addEventListener("abort", (e) => this.onUploadDone(e, 'abort'))
|
||||
req.addEventListener("error", (e) => this.onUploadDone(e, 'error'))
|
||||
|
||||
const formData = new FormData(this.$refs.form);
|
||||
formData.append('csrfmiddlewaretoken', getCsrf())
|
||||
@ -87,8 +87,8 @@ export default {
|
||||
this.total = event.total
|
||||
},
|
||||
|
||||
onUploadDone(event) {
|
||||
this.$emit("load", event)
|
||||
onUploadDone(event, eventName) {
|
||||
this.$emit(eventName, event)
|
||||
this._resetUpload(this.STATE.DEFAULT, true)
|
||||
},
|
||||
|
||||
|
195
assets/src/components/AFormSet.vue
Normal file
195
assets/src/components/AFormSet.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div>
|
||||
<input type="hidden" :name="_prefix + 'TOTAL_FORMS'" :value="items.length || 0"/>
|
||||
<template v-for="(value,name) in formData.management" v-bind:key="name">
|
||||
<input type="hidden" :name="_prefix + name.toUpperCase()"
|
||||
:value="value"/>
|
||||
</template>
|
||||
|
||||
<a-rows ref="rows" :set="set"
|
||||
:columns="visibleFields" :columnsOrderable="columnsOrderable"
|
||||
:orderable="orderable" @move="moveItem" @colmove="onColumnMove"
|
||||
@cell="e => $emit('cell', e)">
|
||||
|
||||
<template #header-head>
|
||||
<template v-if="orderable">
|
||||
<th style="max-width:2em" :title="orderField.label"
|
||||
:aria-label="orderField.label"
|
||||
:aria-description="orderField.help || ''">
|
||||
<span class="icon">
|
||||
<i class="fa fa-arrow-down-1-9"></i>
|
||||
</span>
|
||||
</th>
|
||||
<slot name="rows-header-head"></slot>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #row-head="data">
|
||||
<input v-if="orderable" type="hidden"
|
||||
:name="_prefix + data.row + '-' + orderBy"
|
||||
:value="data.row"/>
|
||||
<input type="hidden" :name="_prefix + data.row + '-id'"
|
||||
:value="data.item ? data.item.id : ''"/>
|
||||
|
||||
<template v-for="field of hiddenFields" v-bind:key="field.name">
|
||||
<input type="hidden"
|
||||
v-if="!(field.name in ['id', orderBy])"
|
||||
:name="_prefix + data.row + '-' + field.name"
|
||||
:value="field.value in [null, undefined] ? data.item.data[name] : field.value"/>
|
||||
</template>
|
||||
|
||||
<slot name="row-head" v-bind="data">
|
||||
<td v-if="orderable">{{ data.row+1 }}</td>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template v-for="(field,slot) of fieldSlots" v-bind:key="field.name"
|
||||
v-slot:[slot]="data">
|
||||
<slot :name="slot" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<slot :name="'control-' + field.name" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name"/>
|
||||
</div>
|
||||
<p v-for="[error,index] in data.item.error(field.name)" class="help is-danger" v-bind:key="index">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<template #row-tail="data">
|
||||
<slot v-if="$slots['row-tail']" name="row-tail" v-bind="data"/>
|
||||
<td class="align-right pr-0">
|
||||
<button type="button" class="button square"
|
||||
@click.stop="removeItem(data.row, data.item)"
|
||||
:title="labels.remove_item"
|
||||
:aria-label="labels.remove_item">
|
||||
<span class="icon"><i class="fa fa-trash" /></span>
|
||||
</button>
|
||||
</td>
|
||||
</template>
|
||||
</a-rows>
|
||||
<div class="a-formset-footer flex-row">
|
||||
<div class="flex-grow-1 flex-row">
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
<div class="flex-grow-1 align-right">
|
||||
<button type="button" class="button square is-warning p-2"
|
||||
@click="reset()"
|
||||
:title="labels.discard_changes"
|
||||
:aria-label="labels.discard_changes"
|
||||
>
|
||||
<span class="icon"><i class="fa fa-rotate" /></span>
|
||||
</button>
|
||||
<button type="button" class="button square is-primary p-2"
|
||||
@click="onActionAdd"
|
||||
:title="labels.add_item"
|
||||
:aria-label="labels.add_item"
|
||||
>
|
||||
<span class="icon">
|
||||
<i class="fa fa-plus"/></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {cloneDeep} from 'lodash'
|
||||
import Model, {Set} from '../model'
|
||||
|
||||
import ARows from './ARows'
|
||||
|
||||
export default {
|
||||
emit: ['cell', 'move', 'colmove', 'load'],
|
||||
components: {ARows},
|
||||
|
||||
props: {
|
||||
labels: Object,
|
||||
|
||||
//! If provided call this function instead of adding an item to rows on "+" button click.
|
||||
actionAdd: Function,
|
||||
|
||||
//! If True, columns can be reordered
|
||||
columnsOrderable: Boolean,
|
||||
//! Field name used for ordering
|
||||
orderBy: String,
|
||||
|
||||
//! Formset data as returned by get_formset_data
|
||||
formData: Object,
|
||||
//! Model class used for item's set
|
||||
model: {type: Function, default: Model},
|
||||
//! initial data set load at mount
|
||||
initials: Array,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
set: new Set(Model),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// ---- fields
|
||||
_prefix() { return this.formData.prefix ? this.formData.prefix + '-' : '' },
|
||||
fields() { return this.formData.fields },
|
||||
orderField() { return this.orderBy && this.fields.find(f => f.name == this.orderBy) },
|
||||
orderable() { return !!this.orderField },
|
||||
|
||||
hiddenFields() { return this.fields.filter(f => f.hidden && !(this.orderable && f == this.orderField)) },
|
||||
visibleFields() { return this.fields.filter(f => !f.hidden) },
|
||||
|
||||
fieldSlots() { return this.visibleFields.reduce(
|
||||
(slots, f) => ({...slots, ['row-' + f.name]: f}),
|
||||
{}
|
||||
)},
|
||||
|
||||
items() { return this.set.items },
|
||||
rows() { return this.$refs.rows },
|
||||
},
|
||||
|
||||
methods: {
|
||||
onCellEvent(event) { this.$emit('cell', event) },
|
||||
onColumnMove(event) { this.$emit('colmove', event) },
|
||||
onActionAdd() {
|
||||
if(this.actionAdd)
|
||||
return this.actionAdd(this)
|
||||
this.set.push()
|
||||
},
|
||||
|
||||
moveItem(event) {
|
||||
const {from, to} = event
|
||||
const set_ = event.set || this.set
|
||||
set_.move(from, to);
|
||||
this.$emit('move', {...event, seŧ: set_})
|
||||
},
|
||||
|
||||
removeItem(row) {
|
||||
const item = this.items[row]
|
||||
if(item.id) {
|
||||
// TODO
|
||||
}
|
||||
else {
|
||||
this.items.splice(row,1)
|
||||
}
|
||||
},
|
||||
|
||||
//! Load items into set
|
||||
load(items=[], reset=false) {
|
||||
if(reset)
|
||||
this.set.items = []
|
||||
for(var item of items)
|
||||
this.set.push(cloneDeep(item))
|
||||
this.$emit('load', items)
|
||||
},
|
||||
|
||||
//! Reset forms to initials
|
||||
reset() {
|
||||
this.load(this.initials || [], true)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -6,6 +6,7 @@
|
||||
<div class="modal-card-title">
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</div>
|
||||
<slot name="bar"></slot>
|
||||
<button type="button" class="delete square" aria-label="close" @click="close">
|
||||
<span class="icon">
|
||||
<i class="fa fa-close"></i>
|
||||
|
@ -27,12 +27,15 @@ import {isReactive, toRefs} from 'vue'
|
||||
import Model from '../model'
|
||||
|
||||
export default {
|
||||
emit: ['move', 'cell'],
|
||||
emits: ['move', 'cell'],
|
||||
|
||||
props: {
|
||||
//! Item to display in row
|
||||
item: Object,
|
||||
item: {type: Object, default: () => ({})},
|
||||
//! Columns to display, as items' attributes
|
||||
//! - name: field name / item attribute value
|
||||
//! - label: display label
|
||||
//! - help: help text
|
||||
columns: Array,
|
||||
//! Default cell's info
|
||||
cell: {type: Object, default() { return {row: 0}}},
|
||||
|
@ -1,34 +1,38 @@
|
||||
<template>
|
||||
<table class="table is-stripped is-fullwidth">
|
||||
<thead>
|
||||
<a-row :item="labels" :columns="columns" :orderable="orderable"
|
||||
@move="$emit('colmove', $event)">
|
||||
<a-row :columns="columnNames"
|
||||
:orderable="columnsOrderable" cellTag="th"
|
||||
@move="moveColumn">
|
||||
<template v-if="$slots['header-head']" v-slot:head="data">
|
||||
<slot name="header-head" v-bind="data"/>
|
||||
</template>
|
||||
<template v-if="$slots['header-tail']" v-slot:tail="data">
|
||||
<slot name="header-tail" v-bind="data"/>
|
||||
</template>
|
||||
<template v-for="column of columns" v-bind:key="column.name"
|
||||
v-slot:[column.name]="data">
|
||||
<slot :name="'header-' + column.name" v-bind="data">
|
||||
{{ column.label }}
|
||||
<span v-if="column.help" class="icon small"
|
||||
:title="column.help">
|
||||
<i class="fa fa-circle-question"/>
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
</a-row>
|
||||
</thead>
|
||||
<tbody>
|
||||
<slot name="head"/>
|
||||
<template v-for="(item,row) in items" :key="row">
|
||||
<!-- data-index comes from AList component drag & drop -->
|
||||
<a-row :item="item" :cell="{row}" :columns="columns" :data-index="row"
|
||||
<a-row :item="item" :cell="{row}" :columns="columnNames" :data-index="row"
|
||||
:data-row="row"
|
||||
:draggable="orderable"
|
||||
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
|
||||
@cell="onCellEvent(row, $event)">
|
||||
<template v-for="[name,slot] of rowSlots" :key="slot" v-slot:[slot]="data">
|
||||
<template v-if="slot == 'head' || slot == 'tail'">
|
||||
<slot :name="name" v-bind="data"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<slot :name="name" v-bind="data"/>
|
||||
</div>
|
||||
</template>
|
||||
<slot :name="name" v-bind="data"/>
|
||||
</template>
|
||||
</a-row>
|
||||
</template>
|
||||
@ -43,28 +47,38 @@ import ARow from './ARow.vue'
|
||||
const Component = {
|
||||
extends: AList,
|
||||
components: { ARow },
|
||||
emit: ['cell', 'colmove'],
|
||||
//! Event:
|
||||
//! - cell(event): an event occured inside cell
|
||||
//! - colmove({from,to}), colmove(): columns moved
|
||||
emits: ['cell', 'colmove'],
|
||||
|
||||
props: {
|
||||
...AList.props,
|
||||
//! Ordered list of columns, as objects with:
|
||||
//! - name: item attribute value
|
||||
//! - label: display label
|
||||
//! - help: help text
|
||||
//! - hidden: if true, field is hidden
|
||||
columns: Array,
|
||||
labels: Object,
|
||||
//! If True, columns are orderable
|
||||
columnsOrderable: Boolean,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
...super.data,
|
||||
// TODO: add observer
|
||||
columns_: [...this.columns],
|
||||
extraItem: new this.set.model(),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
rowCells() {
|
||||
const cells = []
|
||||
for(var row in this.items)
|
||||
cells.push({row})
|
||||
},
|
||||
|
||||
columnNames() { return this.columns_.map(c => c.name) },
|
||||
columnLabels() { return this.columns_.reduce(
|
||||
(labels, c) => ({...labels, [c.name]: c.label}),
|
||||
{}
|
||||
)},
|
||||
rowSlots() {
|
||||
return Object.keys(this.$slots).filter(x => x.startsWith('row-'))
|
||||
.map(x => [x, x.slice(4)])
|
||||
@ -72,6 +86,25 @@ const Component = {
|
||||
},
|
||||
|
||||
methods: {
|
||||
// TODO: use in tracklist
|
||||
sortColumns(names) {
|
||||
const ordered = names.map(n => this.columns_.find(c => c.name == n)).filter(c => !!c);
|
||||
const remaining = this.columns_.filter(c => names.indexOf(c.name) == -1)
|
||||
this.columns_ = [...ordered, ...remaining]
|
||||
this.$emit('colmove')
|
||||
},
|
||||
|
||||
/**
|
||||
* Move column using provided event object (as `{from, to}`)
|
||||
*/
|
||||
moveColumn(event) {
|
||||
const {from, to} = event
|
||||
const value = this.columns_[from]
|
||||
this.columns_.splice(from, 1)
|
||||
this.columns_.splice(to, 0, value)
|
||||
this.$emit('colmove', event)
|
||||
},
|
||||
|
||||
/**
|
||||
* React on 'cell' event, re-emitting it with additional values:
|
||||
* - `set`: data set
|
||||
|
@ -1,63 +1,99 @@
|
||||
<template>
|
||||
<div class="a-select-file">
|
||||
<div ref="list" :class="['a-select-file-list', listClass]">
|
||||
<!-- upload -->
|
||||
<form ref="uploadForm" class="flex-column" v-if="state == STATE.DEFAULT">
|
||||
<div class="field flex-grow-1">
|
||||
<label class="label">{{ uploadLabel }}</label>
|
||||
<input type="file" ref="uploadFile" :name="uploadFieldName" @change="onSubmit"/>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<slot name="upload-form"></slot>
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex-column" v-else>
|
||||
<slot name="upload-preview" :upload="upload"></slot>
|
||||
<div class="flex-row">
|
||||
<progress :max="upload.total" :value="upload.loaded"/>
|
||||
<button type="button" class="button small square ml-2" @click="uploadAbort">
|
||||
<span class="icon small">
|
||||
<i class="fa fa-close"></i>
|
||||
</span>
|
||||
</button>
|
||||
<a-modal ref="modal" :title="title">
|
||||
<template #bar>
|
||||
<button type="button" class="button small mr-3" v-if="panel == LIST"
|
||||
@click="showPanel(UPLOAD)">
|
||||
<span class="icon">
|
||||
<i class="fa fa-upload"></i>
|
||||
</span>
|
||||
<span>{{ labels.upload }}</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="button small mr-3" v-else
|
||||
@click="showPanel(LIST)">
|
||||
<span class="icon">
|
||||
<i class="fa fa-list"></i>
|
||||
</span>
|
||||
<span>{{ labels.list }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<template #default>
|
||||
<a-file-upload ref="upload" v-if="panel == UPLOAD"
|
||||
:url="uploadUrl"
|
||||
:label="uploadLabel" :field-name="uploadFieldName"
|
||||
@load="uploadDone">
|
||||
<template #form="data">
|
||||
<slot name="upload-form" v-bind="data"></slot>
|
||||
</template>
|
||||
<template #preview="data">
|
||||
<slot name="upload-preview" v-bind="data"></slot>
|
||||
</template>
|
||||
</a-file-upload>
|
||||
<div class="a-select-file" v-else>
|
||||
<div ref="list"
|
||||
:class="['a-select-file-list', listClass]">
|
||||
<!-- tiles -->
|
||||
<div v-if="prevUrl">
|
||||
<a href="#" @click="load(prevUrl)">
|
||||
{{ labels.show_previous }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<template v-for="item in items" v-bind:key="item.id">
|
||||
<div :class="['file-preview', this.item && item.id == this.item.id && 'active']" @click="select(item)">
|
||||
<slot :item="item" :load="load" :lastUrl="lastUrl"></slot>
|
||||
<a-action-button v-if="deleteUrl"
|
||||
class="has-text-danger small float-right"
|
||||
icon="fa fa-trash"
|
||||
:confirm="labels.confirm_delete"
|
||||
method="DELETE"
|
||||
:url="deleteUrl.replace('123', item.id)"
|
||||
@done="load(lastUrl)">
|
||||
</a-action-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="nextUrl">
|
||||
<a href="#" @click="load(nextUrl)">
|
||||
{{ labels.show_next }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tiles -->
|
||||
<div v-if="prevUrl">
|
||||
<a href="#" @click="load(prevUrl)">
|
||||
{{ prevLabel }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<template v-for="item in items" v-bind:key="item.id">
|
||||
<div :class="['file-preview', this.item && item.id == this.item.id && 'active']" @click="select(item)">
|
||||
<slot :item="item" :load="load" :lastUrl="lastUrl"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="nextUrl">
|
||||
<a href="#" @click="load(nextUrl)">
|
||||
{{ nextLabel }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-select-footer">
|
||||
<slot name="footer" :item="item" :items="items"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<slot name="footer" :item="item">
|
||||
<span class="mr-3" v-if="item">{{ item.name }}</span>
|
||||
</slot>
|
||||
<button type="button" v-if="panel == LIST" class="button align-right"
|
||||
@click="selected">
|
||||
{{ labels.select_file }}
|
||||
</button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
<script>
|
||||
import {getCsrf} from "../model"
|
||||
import AModal from "./AModal"
|
||||
import AActionButton from "./AActionButton"
|
||||
import AFileUpload from "./AFileUpload"
|
||||
|
||||
export default {
|
||||
emit: ["select"],
|
||||
|
||||
components: {AActionButton, AFileUpload, AModal},
|
||||
|
||||
props: {
|
||||
name: { type: String },
|
||||
title: { type: String },
|
||||
labels: Object,
|
||||
listClass: {type: String, default: ""},
|
||||
prevLabel: { type: String, default: "Prev" },
|
||||
nextLabel: { type: String, default: "Next" },
|
||||
|
||||
// List url
|
||||
listUrl: { type: String },
|
||||
|
||||
// URL to delete an item, where "123" is replaced by
|
||||
// the item id.
|
||||
deleteUrl: {type: String },
|
||||
|
||||
uploadUrl: { type: String },
|
||||
uploadFieldName: { type: String, default: "file" },
|
||||
uploadLabel: { type: String, default: "Upload a file" },
|
||||
@ -65,91 +101,63 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
STATE: {
|
||||
DEFAULT: 0,
|
||||
UPLOADING: 1,
|
||||
},
|
||||
|
||||
state: 0,
|
||||
LIST: 0,
|
||||
UPLOAD: 1,
|
||||
|
||||
panel: 0,
|
||||
item: null,
|
||||
items: [],
|
||||
nextUrl: "",
|
||||
prevUrl: "",
|
||||
lastUrl: "",
|
||||
|
||||
upload: {},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.modal.open()
|
||||
},
|
||||
|
||||
close() {
|
||||
this.$refs.modal.close()
|
||||
},
|
||||
|
||||
showPanel(panel) {
|
||||
this.panel = panel
|
||||
},
|
||||
|
||||
load(url) {
|
||||
fetch(url || this.listUrl).then(
|
||||
return fetch(url || this.listUrl).then(
|
||||
response => response.ok ? response.json() : Promise.reject(response)
|
||||
).then(data => {
|
||||
this.lastUrl = url
|
||||
this.nextUrl = data.next
|
||||
this.prevUrl = data.previous
|
||||
this.items = data.results
|
||||
this.showPanel(this.LIST)
|
||||
|
||||
this.$forceUpdate()
|
||||
this.$refs.list.scroll(0, 0)
|
||||
return this.items
|
||||
})
|
||||
},
|
||||
|
||||
//! Select an item
|
||||
select(item) {
|
||||
this.item = item;
|
||||
},
|
||||
|
||||
// ---- upload
|
||||
uploadAbort() {
|
||||
this.upload.request && this.upload.request.abort()
|
||||
//! User click on select button (confirm selection)
|
||||
selected() {
|
||||
this.$emit("select", this.item)
|
||||
this.close()
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
const [file] = this.$refs.uploadFile.files
|
||||
if(!file)
|
||||
return
|
||||
this._setUploadFile(file)
|
||||
|
||||
const req = new XMLHttpRequest()
|
||||
req.open("POST", this.uploadUrl || this.listUrl)
|
||||
req.upload.addEventListener("progress", (e) => this.onUploadProgress(e))
|
||||
req.addEventListener("load", (e) => this.onUploadDone(e, true))
|
||||
req.addEventListener("abort", (e) => this.onUploadDone(e))
|
||||
req.addEventListener("error", (e) => this.onUploadDone(e))
|
||||
|
||||
const formData = new FormData(this.$refs.uploadForm);
|
||||
formData.append('csrfmiddlewaretoken', getCsrf())
|
||||
req.send(formData)
|
||||
|
||||
this._resetUpload(this.STATE.UPLOADING, false, req)
|
||||
uploadDone(reload=false) {
|
||||
reload && this.load().then(items => {
|
||||
this.item = items[0]
|
||||
})
|
||||
},
|
||||
|
||||
onUploadProgress(event) {
|
||||
this.upload.loaded = event.loaded
|
||||
this.upload.total = event.total
|
||||
},
|
||||
|
||||
onUploadDone(reload=false) {
|
||||
this._resetUpload(this.STATE.DEFAULT, true)
|
||||
reload && this.load()
|
||||
},
|
||||
|
||||
_setUploadFile(file) {
|
||||
this.upload.file = file
|
||||
this.upload.fileURL = file && URL.createObjectURL(file)
|
||||
},
|
||||
|
||||
_resetUpload(state, resetFile=false, request=null) {
|
||||
this.state = state
|
||||
this.upload.loaded = 0
|
||||
this.upload.total = 0
|
||||
this.upload.request = request
|
||||
if(resetFile)
|
||||
this.upload.file = null
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -1,155 +1,83 @@
|
||||
<template>
|
||||
<div class="a-playlist-editor">
|
||||
<a-modal ref="modal" :title="labels && labels.add_sound">
|
||||
<template #default>
|
||||
<a-file-upload ref="file-upload" :url="soundUploadUrl" :label="labels.select_file" submitLabel="" @load="uploadDone"
|
||||
>
|
||||
<template #preview="{upload}">
|
||||
<slot name="upload-preview" :upload="upload"></slot>
|
||||
</template>
|
||||
<template #form>
|
||||
<slot name="upload-form"></slot>
|
||||
</template>
|
||||
</a-file-upload>
|
||||
<a-select-file ref="select-file"
|
||||
:title="labels && labels.add_sound"
|
||||
:labels="labels"
|
||||
:list-url="soundListUrl"
|
||||
:deleteUrl="soundDeleteUrl"
|
||||
:uploadUrl="soundUploadUrl"
|
||||
:uploadLabel="labels.select_file"
|
||||
@select="selected"
|
||||
>
|
||||
<template #upload-preview="{upload}">
|
||||
<slot name="upload-preview" :upload="upload"></slot>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" class="button"
|
||||
@click.stop="$refs['file-upload'].submit()">
|
||||
<span class="icon">
|
||||
<i class="fa fa-upload"></i>
|
||||
</span>
|
||||
<span>{{ labels.submit }}</span>
|
||||
</button>
|
||||
<template #upload-form>
|
||||
<slot name="upload-form"></slot>
|
||||
</template>
|
||||
</a-modal>
|
||||
<template #default="{item}">
|
||||
<audio controls :src="item.url"></audio>
|
||||
<label class="label small flex-grow-1">{{ item.name }}</label>
|
||||
</template>
|
||||
</a-select-file>
|
||||
|
||||
<slot name="top" :set="set" :items="set.items"></slot>
|
||||
<a-rows :set="set" :columns="allColumns"
|
||||
:labels="allColumnsLabels" :allow-create="true" :orderable="true"
|
||||
@move="listItemMove">
|
||||
<a-form-set ref="formset" :form-data="formData" :labels="labels"
|
||||
:initials="initData.items"
|
||||
order-by="position"
|
||||
:action-add="actionAdd">
|
||||
<template v-for="[name,slot] of rowsSlots" :key="slot"
|
||||
v-slot:[slot]="data">
|
||||
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
|
||||
</template>
|
||||
</a-rows>
|
||||
|
||||
<div class="flex-row">
|
||||
<div class="flex-grow-1 flex-row">
|
||||
</div>
|
||||
<div class="flex-grow-1 align-right">
|
||||
<button type="button" class="button square is-warning p-2"
|
||||
@click="loadData({items: this.initData.items},true)"
|
||||
:title="labels.discard_changes"
|
||||
:aria-label="labels.discard_changes"
|
||||
>
|
||||
<span class="icon"><i class="fa fa-rotate" /></span>
|
||||
</button>
|
||||
<button type="button" class="button square is-primary p-2"
|
||||
@click="$refs.modal.open()"
|
||||
:title="labels.add_sound"
|
||||
:aria-label="labels.add_sound"
|
||||
>
|
||||
<span class="icon">
|
||||
<i class="fa fa-plus"/></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template #row-sound="{item,inputName}">
|
||||
<label>{{ item.data.name }}</label><br>
|
||||
<audio controls :src="item.data.url"/>
|
||||
<input type="hidden" :name="inputName" :value="item.data.sound"/>
|
||||
</template>
|
||||
</a-form-set>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
|
||||
import {cloneDeep} from 'lodash'
|
||||
import Model, {Set} from '../model'
|
||||
|
||||
// import AActionButton from './AActionButton'
|
||||
import ARows from './ARows'
|
||||
import AModal from "./AModal"
|
||||
import AFileUpload from "./AFileUpload"
|
||||
import AFormSet from './AFormSet'
|
||||
import ASelectFile from "./ASelectFile"
|
||||
|
||||
export default {
|
||||
components: {ARows, AModal, AFileUpload},
|
||||
components: {AFormSet, ASelectFile},
|
||||
|
||||
props: {
|
||||
initData: Object,
|
||||
dataPrefix: String,
|
||||
formData: Object,
|
||||
labels: Object,
|
||||
settingsUrl: String,
|
||||
// initial datas
|
||||
initData: Object,
|
||||
|
||||
soundListUrl: String,
|
||||
soundUploadUrl: String,
|
||||
player: Object,
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => ['name', "type", 'is_public', 'is_downloadable']
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
set: new Set(Model),
|
||||
}
|
||||
soundDeleteUrl: String,
|
||||
},
|
||||
|
||||
computed: {
|
||||
player_() {
|
||||
return this.player || window.aircox.player
|
||||
},
|
||||
|
||||
allColumns() {
|
||||
return [...this.columns, "delete"]
|
||||
},
|
||||
|
||||
allColumnsLabels() {
|
||||
return {...this.labels, ...this.initData.fields}
|
||||
},
|
||||
|
||||
items() {
|
||||
return this.set.items
|
||||
},
|
||||
|
||||
rowsSlots() {
|
||||
return Object.keys(this.$slots)
|
||||
.filter(x => x.startsWith('row-') || x.startsWith('rows-'))
|
||||
.filter(x => x.startsWith('row-') || x.startsWith('rows-') || x.startsWith('control-'))
|
||||
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
listItemMove({from, to, set}) {
|
||||
set.move(from, to);
|
||||
actionAdd() {
|
||||
this.$refs['select-file'].open()
|
||||
},
|
||||
|
||||
/**
|
||||
* Load initial data
|
||||
*/
|
||||
loadData({items=[] /*, settings=null*/}, reset=false) {
|
||||
if(reset) {
|
||||
this.set.items = []
|
||||
selected(item) {
|
||||
const data = {
|
||||
"sound": item.id,
|
||||
"name": item.name,
|
||||
"url": item.url,
|
||||
"broadcast": item.broadcast,
|
||||
}
|
||||
for(var index in items)
|
||||
this.set.push(cloneDeep(items[index]))
|
||||
// if(settings)
|
||||
// this.settingsSaved(settings)
|
||||
this.$refs.formset.set.push(data)
|
||||
},
|
||||
|
||||
uploadDone(event) {
|
||||
const req = event.target
|
||||
if(req.status == 201) {
|
||||
const item = JSON.parse(req.response)
|
||||
this.set.push(item)
|
||||
this.$refs.modal.close()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
initData(val) {
|
||||
this.loadData(val)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initData && this.loadData(this.initData)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -12,7 +12,7 @@
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</span>
|
||||
<span>Texte</span>
|
||||
<span>{{ labels.text }}</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
@ -21,13 +21,21 @@
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-list"></i>
|
||||
</span>
|
||||
<span>Liste</span>
|
||||
<span>{{ labels.list }}</span>
|
||||
</button>
|
||||
</p>
|
||||
<p class="control ml-3">
|
||||
<button type="button" class="button is-info square"
|
||||
:title="labels.settings"
|
||||
@click="$refs.settings.open()">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-cog"></i>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="top" :set="set" :columns="columns" :items="items"/>
|
||||
<section v-show="page == Page.Text" class="panel">
|
||||
<textarea ref="textarea" class="is-fullwidth is-size-6" rows="20"
|
||||
@change="updateList"
|
||||
@ -35,61 +43,20 @@
|
||||
|
||||
</section>
|
||||
<section v-show="page == Page.List" class="panel">
|
||||
<a-rows :set="set" :columns="columns" :labels="initData.fields"
|
||||
:orderable="true" @move="listItemMove" @colmove="columnMove"
|
||||
@cell="onCellEvent">
|
||||
<a-form-set ref="formset"
|
||||
:form-data="formData" :initials="initData.items"
|
||||
:columnsOrderable="true" :labels="labels"
|
||||
order-by="position"
|
||||
@load="updateInput" @colmove="onColumnMove" @move="updateInput"
|
||||
@cell="onCellEvent">
|
||||
<template v-for="[name,slot] of rowsSlots" :key="slot"
|
||||
v-slot:[slot]="data">
|
||||
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:row-tail="data">
|
||||
<slot v-if="$slots['row-tail']" :name="row-tail" v-bind="data"/>
|
||||
<td class="align-right pr-0">
|
||||
<button type="button" class="button square"
|
||||
@click.stop="items.splice(data.row,1)"
|
||||
:title="labels.remove_item"
|
||||
:aria-label="labels.remove_item">
|
||||
<span class="icon"><i class="fa fa-trash" /></span>
|
||||
</button>
|
||||
</td>
|
||||
</template>
|
||||
</a-rows>
|
||||
</a-form-set>
|
||||
</section>
|
||||
|
||||
<div class="flex-row">
|
||||
<div class="flex-grow-1 flex-row">
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<button type="button" class="button is-info"
|
||||
@click="$refs.settings.open()">
|
||||
<span class="icon is-small">
|
||||
<i class="fa fa-cog"></i>
|
||||
</span>
|
||||
<span>Options</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 align-right">
|
||||
<button type="button" class="button square is-warning p-2"
|
||||
@click="loadData({items: this.initData.items},true)"
|
||||
:title="labels.discard_changes"
|
||||
:aria-label="labels.discard_changes"
|
||||
>
|
||||
<span class="icon"><i class="fa fa-rotate" /></span>
|
||||
</button>
|
||||
<button type="button" class="button square is-primary p-2" v-if="page == Page.List"
|
||||
@click="this.set.push(new this.set.model())"
|
||||
:title="labels.add_item"
|
||||
:aria-label="labels.add_item"
|
||||
>
|
||||
<span class="icon"><i class="fa fa-plus"/></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-modal ref="settings" title="Options">
|
||||
<a-modal ref="settings" :title="labels.settings">
|
||||
<template #default>
|
||||
<div class="field">
|
||||
<label class="label" style="vertical-align: middle">
|
||||
@ -97,12 +64,14 @@
|
||||
</label>
|
||||
<table class="table is-bordered"
|
||||
style="vertical-align: middle">
|
||||
<tr>
|
||||
<a-row :columns="columns" :item="initData.fields"
|
||||
@move="formatMove" :orderable="true">
|
||||
<tr v-if="$refs.formset">
|
||||
<a-row :columns="$refs.formset.rows.columnNames"
|
||||
:item="$refs.formset.rows.columnLabels"
|
||||
@move="$refs.formset.rows.moveColumn"
|
||||
>
|
||||
<template v-slot:cell-after="{cell}">
|
||||
<td style="cursor:pointer;" v-if="cell.col < columns.length-1">
|
||||
<span class="icon" @click="formatMove({from: cell.col, to: cell.col+1})"
|
||||
<td style="cursor:pointer;" v-if="cell.col < $refs.formset.rows.columns_.length-1">
|
||||
<span class="icon" @click="$refs.formset.rows.moveColumn({from: cell.col, to: cell.col+1})"
|
||||
><i class="fa fa-left-right"/>
|
||||
</span>
|
||||
</td>
|
||||
@ -143,16 +112,14 @@
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
<slot name="bottom" :set="set" :columns="columns" :items="items"/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
|
||||
import Model, {Set} from '../model'
|
||||
|
||||
import AActionButton from './AActionButton'
|
||||
import AFormSet from './AFormSet'
|
||||
import ARow from './ARow'
|
||||
import ARows from './ARows'
|
||||
import AModal from "./AModal"
|
||||
|
||||
/// Page display
|
||||
@ -161,12 +128,14 @@ export const Page = {
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { AActionButton, ARow, ARows, AModal },
|
||||
components: { AActionButton, AFormSet, ARow, AModal },
|
||||
props: {
|
||||
formData: Object,
|
||||
labels: Object,
|
||||
|
||||
///! initial data as: {items: [], fields: {column_name: label, settings: {}}
|
||||
initData: Object,
|
||||
dataPrefix: String,
|
||||
labels: Object,
|
||||
settingsUrl: String,
|
||||
defaultColumns: {
|
||||
type: Array,
|
||||
@ -175,13 +144,12 @@ export default {
|
||||
|
||||
data() {
|
||||
const settings = {
|
||||
tracklist_editor_columns: this.defaultColumns,
|
||||
// tracklist_editor_columns: this.columns,
|
||||
tracklist_editor_sep: ' -- ',
|
||||
}
|
||||
return {
|
||||
Page: Page,
|
||||
page: Page.Text,
|
||||
set: new Set(Model),
|
||||
extraData: {},
|
||||
settings,
|
||||
savedSettings: cloneDeep(settings),
|
||||
@ -189,6 +157,9 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
rows() { return this.$refs.formset && this.$refs.formset.rows },
|
||||
columns() { return this.rows && this.rows.columns_ || [] },
|
||||
|
||||
settingsChanged() {
|
||||
var k = Object.keys(this.savedSettings)
|
||||
.findIndex(k => !isEqual(this.settings[k], this.savedSettings[k]))
|
||||
@ -204,25 +175,9 @@ export default {
|
||||
get() { return this.settings.tracklist_editor_sep }
|
||||
},
|
||||
|
||||
columns: {
|
||||
set(value) {
|
||||
var cols = value.filter(x => x in this.defaultColumns)
|
||||
var left = this.defaultColumns.filter(x => !(x in cols))
|
||||
value = cols.concat(left)
|
||||
this.settings.tracklist_editor_columns = value
|
||||
},
|
||||
get() {
|
||||
return this.settings.tracklist_editor_columns
|
||||
}
|
||||
},
|
||||
|
||||
items() {
|
||||
return this.set.items
|
||||
},
|
||||
|
||||
rowsSlots() {
|
||||
return Object.keys(this.$slots)
|
||||
.filter(x => x.startsWith('row-') || x.startsWith('rows-'))
|
||||
.filter(x => x.startsWith('row-') || x.startsWith('rows-') || x.startsWith('control-'))
|
||||
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
|
||||
},
|
||||
},
|
||||
@ -235,35 +190,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
formatMove({from, to}) {
|
||||
const value = this.columns[from]
|
||||
this.settings.tracklist_editor_columns.splice(from, 1)
|
||||
this.settings.tracklist_editor_columns.splice(to, 0, value)
|
||||
if(this.page == Page.Text)
|
||||
this.updateList()
|
||||
else
|
||||
onColumnMove() {
|
||||
this.settings.tracklist_editor_columns = this.$refs.formset.rows.columnNames
|
||||
if(this.page == this.Page.List)
|
||||
this.updateInput()
|
||||
},
|
||||
|
||||
columnMove({from, to}) {
|
||||
const value = this.columns[from]
|
||||
this.columns.splice(from, 1)
|
||||
this.columns.splice(to, 0, value)
|
||||
this.updateInput()
|
||||
},
|
||||
|
||||
listItemMove({from, to, set}) {
|
||||
set.move(from, to);
|
||||
this.updateInput()
|
||||
else
|
||||
this.updateList()
|
||||
},
|
||||
|
||||
updateList() {
|
||||
const items = this.toList(this.$refs.textarea.value)
|
||||
this.set.reset(items)
|
||||
this.$refs.formset.set.reset(items)
|
||||
},
|
||||
|
||||
updateInput() {
|
||||
const input = this.toText(this.items)
|
||||
const input = this.toText(this.$refs.formset.items)
|
||||
this.$refs.textarea.value = input
|
||||
},
|
||||
|
||||
@ -271,6 +212,7 @@ export default {
|
||||
* From input and separator, return list of items.
|
||||
*/
|
||||
toList(input) {
|
||||
const columns = this.$refs.formset.rows.columns_
|
||||
var lines = input.split('\n')
|
||||
var items = []
|
||||
|
||||
@ -281,11 +223,11 @@ export default {
|
||||
|
||||
var lineBits = line.split(this.separator)
|
||||
var item = {}
|
||||
for(var col in this.columns) {
|
||||
for(var col in columns) {
|
||||
if(col >= lineBits.length)
|
||||
break
|
||||
const attr = this.columns[col]
|
||||
item[attr] = lineBits[col].trim()
|
||||
const column = columns[col]
|
||||
item[column.name] = lineBits[col].trim()
|
||||
}
|
||||
item && items.push(item)
|
||||
}
|
||||
@ -296,14 +238,15 @@ export default {
|
||||
* From items and separator return a string
|
||||
*/
|
||||
toText(items) {
|
||||
const columns = this.$refs.formset.rows.columns_
|
||||
const sep = ` ${this.separator.trim()} `
|
||||
const lines = []
|
||||
for(let item of items) {
|
||||
if(!item)
|
||||
continue
|
||||
var line = []
|
||||
for(var col of this.columns)
|
||||
line.push(item.data[col] || '')
|
||||
for(var col of columns)
|
||||
line.push(item.data[col.name] || '')
|
||||
line = dropRightWhile(line, x => !x || !('' + x).trim())
|
||||
line = line.join(sep).trimRight()
|
||||
lines.push(line)
|
||||
@ -331,31 +274,15 @@ export default {
|
||||
this.$refs.settings.close()
|
||||
this.savedSettings = cloneDeep(this.settings)
|
||||
},
|
||||
|
||||
/**
|
||||
* Load initial data
|
||||
*/
|
||||
loadData({items=[], settings=null}, reset=false) {
|
||||
if(reset) {
|
||||
this.set.items = []
|
||||
}
|
||||
for(var index in items)
|
||||
this.set.push(cloneDeep(items[index]))
|
||||
if(settings)
|
||||
this.settingsSaved(settings)
|
||||
this.updateInput()
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
initData(val) {
|
||||
this.loadData(val)
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.initData && this.loadData(this.initData)
|
||||
this.page = this.items.length ? Page.List : Page.Text
|
||||
const settings = this.initData && this.initData.settings
|
||||
if(settings) {
|
||||
this.settingsSaved(settings)
|
||||
this.rows.sortColumns(settings.tracklist_editor_columns)
|
||||
}
|
||||
this.page = this.initData.items.length ? Page.List : Page.Text
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import AActionButton from './AActionButton'
|
||||
import AActionButton from './AActionButton.vue'
|
||||
import AAutocomplete from './AAutocomplete'
|
||||
import ACarousel from './ACarousel'
|
||||
import ADropdown from "./ADropdown"
|
||||
@ -16,6 +16,8 @@ import AFileUpload from "./AFileUpload"
|
||||
import ASelectFile from "./ASelectFile"
|
||||
import AStatistics from './AStatistics'
|
||||
import AStreamer from './AStreamer'
|
||||
|
||||
import AFormSet from './AFormSet'
|
||||
import ATrackListEditor from './ATrackListEditor'
|
||||
import ASoundListEditor from './ASoundListEditor'
|
||||
|
||||
@ -37,5 +39,6 @@ export const admin = {
|
||||
|
||||
export const dashboard = {
|
||||
...base,
|
||||
AActionButton, AFileUpload, ASelectFile, AModal, ATrackListEditor, ASoundListEditor
|
||||
AActionButton, AFileUpload, ASelectFile, AModal,
|
||||
AFormSet, ATrackListEditor, ASoundListEditor
|
||||
}
|
||||
|
@ -17,13 +17,12 @@ const DashboardApp = {
|
||||
methods: {
|
||||
...App.methods,
|
||||
|
||||
fileSelected(select, cover, input, modal) {
|
||||
console.log("file!")
|
||||
fileSelected(select, input, preview) {
|
||||
const item = this.$refs[select].item
|
||||
if(item) {
|
||||
this.$refs[cover].src = item.file
|
||||
this.$refs[input].value = item.id
|
||||
modal && this.$refs[modal].close()
|
||||
if(preview)
|
||||
preview.src = item.file
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -2,9 +2,6 @@
|
||||
* This module includes code available for both the public website and
|
||||
* administration interface)
|
||||
*/
|
||||
//-- vendor
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
|
||||
|
||||
//-- aircox
|
||||
import App, {PlayerApp} from './app'
|
||||
|
@ -2,8 +2,11 @@ import Model from './model';
|
||||
|
||||
|
||||
export default class Sound extends Model {
|
||||
constructor({sound={}, ...data}={}, options={}) {
|
||||
// flatten EpisodeSound and sound data
|
||||
super({...sound, ...data}, options)
|
||||
}
|
||||
|
||||
get name() { return this.data.name }
|
||||
get src() { return this.data.url }
|
||||
|
||||
static getId(data) { return data.pk }
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
@use "./vars";
|
||||
@use "./components";
|
||||
|
||||
@import "~bulma/sass/utilities/_all.sass";
|
||||
@import "~bulma/sass/elements/button";
|
||||
@import "~bulma/sass/components/navbar";
|
||||
@import "bulma/sass/utilities/_all.sass";
|
||||
@import "bulma/sass/elements/button";
|
||||
@import "bulma/sass/components/navbar";
|
||||
|
||||
|
||||
// enforce button usage inside custom application
|
||||
|
@ -1,4 +1,5 @@
|
||||
@import 'v-calendar/style.css';
|
||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
|
||||
// ---- bulma
|
||||
$body-color: #000;
|
||||
@ -6,29 +7,29 @@ $title-color: #000;
|
||||
$modal-content-width: 80%;
|
||||
|
||||
|
||||
@import "~bulma/sass/utilities/_all.sass";
|
||||
@import "bulma/sass/utilities/_all.sass";
|
||||
|
||||
|
||||
@import "~bulma/sass/base/_all";
|
||||
@import "~bulma/sass/components/dropdown";
|
||||
// @import "~bulma/sass/components/card";
|
||||
@import "~bulma/sass/components/media";
|
||||
@import "~bulma/sass/components/message";
|
||||
@import "~bulma/sass/components/modal";
|
||||
//@import "~bulma/sass/components/pagination";
|
||||
@import "bulma/sass/base/_all";
|
||||
@import "bulma/sass/components/dropdown";
|
||||
// @import "bulma/sass/components/card";
|
||||
@import "bulma/sass/components/media";
|
||||
@import "bulma/sass/components/message";
|
||||
@import "bulma/sass/components/modal";
|
||||
//@import "bulma/sass/components/pagination";
|
||||
|
||||
@import "~bulma/sass/form/_all";
|
||||
@import "~bulma/sass/grid/_all";
|
||||
@import "~bulma/sass/helpers/_all";
|
||||
@import "~bulma/sass/layout/_all";
|
||||
@import "~bulma/sass/elements/box";
|
||||
// @import "~bulma/sass/elements/button";
|
||||
@import "~bulma/sass/elements/container";
|
||||
// @import "~bulma/sass/elements/content";
|
||||
@import "~bulma/sass/elements/icon";
|
||||
// @import "~bulma/sass/elements/image";
|
||||
// @import "~bulma/sass/elements/notification";
|
||||
// @import "~bulma/sass/elements/progress";
|
||||
@import "~bulma/sass/elements/table";
|
||||
@import "~bulma/sass/elements/tag";
|
||||
//@import "~bulma/sass/elements/title";
|
||||
@import "bulma/sass/form/_all";
|
||||
@import "bulma/sass/grid/_all";
|
||||
@import "bulma/sass/helpers/_all";
|
||||
@import "bulma/sass/layout/_all";
|
||||
@import "bulma/sass/elements/box";
|
||||
// @import "bulma/sass/elements/button";
|
||||
@import "bulma/sass/elements/container";
|
||||
// @import "bulma/sass/elements/content";
|
||||
@import "bulma/sass/elements/icon";
|
||||
// @import "bulma/sass/elements/image";
|
||||
// @import "bulma/sass/elements/notification";
|
||||
// @import "bulma/sass/elements/progress";
|
||||
@import "bulma/sass/elements/table";
|
||||
@import "bulma/sass/elements/tag";
|
||||
//@import "bulma/sass/elements/title";
|
||||
|
Reference in New Issue
Block a user