formset component
This commit is contained in:
		
							
								
								
									
										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="onMoveColumn"
 | 
			
		||||
                @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) },
 | 
			
		||||
            onMoveColumn(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>
 | 
			
		||||
@ -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,32 @@ import ARow from './ARow.vue'
 | 
			
		||||
const Component = {
 | 
			
		||||
    extends: AList,
 | 
			
		||||
    components: { ARow },
 | 
			
		||||
    emit: ['cell', 'colmove'],
 | 
			
		||||
    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_.map(c => c.label) },
 | 
			
		||||
        rowSlots() {
 | 
			
		||||
            return Object.keys(this.$slots).filter(x => x.startsWith('row-'))
 | 
			
		||||
                                           .map(x => [x, x.slice(4)])
 | 
			
		||||
@ -72,6 +80,17 @@ const Component = {
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    methods: {
 | 
			
		||||
        /**
 | 
			
		||||
         * 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
 | 
			
		||||
 | 
			
		||||
@ -21,141 +21,63 @@
 | 
			
		||||
            </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>
 | 
			
		||||
 | 
			
		||||
            <template #row-sound="{item}">
 | 
			
		||||
            <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-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['select-file'].open()"
 | 
			
		||||
                        :title="labels.add_sound"
 | 
			
		||||
                        :aria-label="labels.add_sound"
 | 
			
		||||
                        >
 | 
			
		||||
                    <span class="icon">
 | 
			
		||||
                        <i class="fa fa-plus"/></span>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        </a-form-set>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
// import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
 | 
			
		||||
import {cloneDeep} from 'lodash'
 | 
			
		||||
import Model, {Set} from '../model'
 | 
			
		||||
 | 
			
		||||
import ARows from './ARows'
 | 
			
		||||
//import AFileUpload from "./AFileUpload"
 | 
			
		||||
import AFormSet from './AFormSet'
 | 
			
		||||
import ASelectFile from "./ASelectFile"
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {ARows, ASelectFile},
 | 
			
		||||
    components: {AFormSet, ASelectFile},
 | 
			
		||||
 | 
			
		||||
    props: {
 | 
			
		||||
        // default values of items
 | 
			
		||||
        itemDefaults: Object,
 | 
			
		||||
        formData: Object,
 | 
			
		||||
        labels: Object,
 | 
			
		||||
        // initial datas
 | 
			
		||||
        initData: Object,
 | 
			
		||||
        labels: Object,
 | 
			
		||||
 | 
			
		||||
        soundListUrl: String,
 | 
			
		||||
        soundUploadUrl: String,
 | 
			
		||||
        soundDeleteUrl: String,
 | 
			
		||||
 | 
			
		||||
        columns: {
 | 
			
		||||
            type: Array,
 | 
			
		||||
            default: () => ['name', "broadcast"]
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            set: new Set(Model),
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    computed: {
 | 
			
		||||
        player_() {
 | 
			
		||||
            return this.player || window.aircox.player
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        allColumns() {
 | 
			
		||||
            return ["sound", ...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 = []
 | 
			
		||||
            }
 | 
			
		||||
            for(var index in items)
 | 
			
		||||
                this.set.push(cloneDeep(items[index]))
 | 
			
		||||
            // if(settings)
 | 
			
		||||
            //     this.settingsSaved(settings)
 | 
			
		||||
         },
 | 
			
		||||
 | 
			
		||||
         selected(item) {
 | 
			
		||||
        selected(item) {
 | 
			
		||||
            const data = {
 | 
			
		||||
                ...this.itemDefaults,
 | 
			
		||||
                "sound": item.id,
 | 
			
		||||
                "name": item.name,
 | 
			
		||||
                "url": item.url,
 | 
			
		||||
                "broadcast": item.broadcast,
 | 
			
		||||
            }
 | 
			
		||||
            this.set.push(data)
 | 
			
		||||
            this.$refs.formset.set.push(data)
 | 
			
		||||
         },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    watch: {
 | 
			
		||||
        initData(val) {
 | 
			
		||||
            this.loadData(val)
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.initData && this.loadData(this.initData)
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,6 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <slot name="top" :set="set" :columns="allColumns" :items="items"/>
 | 
			
		||||
        <section v-show="page == Page.Text" class="panel">
 | 
			
		||||
            <textarea ref="textarea" class="is-fullwidth is-size-6" rows="20"
 | 
			
		||||
                @change="updateList"
 | 
			
		||||
@ -35,60 +34,33 @@
 | 
			
		||||
 | 
			
		||||
        </section>
 | 
			
		||||
        <section v-show="page == Page.List" class="panel">
 | 
			
		||||
            <a-rows :set="set" :columns="allColumns" :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="updateInput" @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 #footer>
 | 
			
		||||
                    <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>
 | 
			
		||||
                </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">
 | 
			
		||||
            <template #default>
 | 
			
		||||
                <div class="field">
 | 
			
		||||
@ -97,12 +69,14 @@
 | 
			
		||||
                    </label>
 | 
			
		||||
                    <table class="table is-bordered"
 | 
			
		||||
                            style="vertical-align: middle">
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <a-row :columns="allColumns" :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 < allColumns.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 +117,14 @@
 | 
			
		||||
                </div>
 | 
			
		||||
            </template>
 | 
			
		||||
        </a-modal>
 | 
			
		||||
        <slot name="bottom" :set="set" :columns="allColumns" :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 +133,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 +149,12 @@ export default {
 | 
			
		||||
 | 
			
		||||
    data() {
 | 
			
		||||
        const settings = {
 | 
			
		||||
            tracklist_editor_columns: this.columns,
 | 
			
		||||
            // 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 +162,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 +180,9 @@ export default {
 | 
			
		||||
            get() { return this.settings.tracklist_editor_sep }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        allColumns: {
 | 
			
		||||
            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])
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
@ -236,34 +196,30 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        formatMove({from, to}) {
 | 
			
		||||
            const value = this.allColumns[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
 | 
			
		||||
                this.updateInput()
 | 
			
		||||
            this.$refs.formset.rows.columnMove({from, to})
 | 
			
		||||
            /*this.settings.tracklist_editor_columns.splice(from, 1)
 | 
			
		||||
            this.settings.tracklist_editor_columns.splice(to, 0, value)*/
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        columnMove({from, to}) {
 | 
			
		||||
            const value = this.allColumns[from]
 | 
			
		||||
            this.allColumns.splice(from, 1)
 | 
			
		||||
            this.allColumns.splice(to, 0, value)
 | 
			
		||||
            this.updateInput()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        listItemMove({from, to, set}) {
 | 
			
		||||
        moveItem({from, to, set}) {
 | 
			
		||||
            set.move(from, to);
 | 
			
		||||
            this.updateInput()
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        onColumnMove() {
 | 
			
		||||
            if(this.page == this.Page.List)
 | 
			
		||||
                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 +227,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 +238,11 @@ export default {
 | 
			
		||||
 | 
			
		||||
                var lineBits = line.split(this.separator)
 | 
			
		||||
                var item = {}
 | 
			
		||||
                for(var col in this.allColumns) {
 | 
			
		||||
                for(var col in columns) {
 | 
			
		||||
                    if(col >= lineBits.length)
 | 
			
		||||
                        break
 | 
			
		||||
                    const attr = this.allColumns[col]
 | 
			
		||||
                    item[attr] = lineBits[col].trim()
 | 
			
		||||
                    const column = columns[col]
 | 
			
		||||
                    item[column.name] = lineBits[col].trim()
 | 
			
		||||
                }
 | 
			
		||||
                item && items.push(item)
 | 
			
		||||
            }
 | 
			
		||||
@ -296,14 +253,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.allColumns)
 | 
			
		||||
                    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)
 | 
			
		||||
@ -335,27 +293,17 @@ export default {
 | 
			
		||||
        /**
 | 
			
		||||
         * 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]))
 | 
			
		||||
        /*loadData({items=[], settings=null}, reset=false) {
 | 
			
		||||
            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
 | 
			
		||||
        //this.initData && this.initData.settings &&
 | 
			
		||||
        //this.initData && this.loadData(this.initData)
 | 
			
		||||
        this.page = this.initData.items.length ? Page.List : Page.Text
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user