Move Datatable and Media Manager to Composition API.
This commit is contained in:
parent
e13e6c5c0a
commit
303287a700
|
@ -1,128 +0,0 @@
|
|||
<template>
|
||||
<b-modal ref="modal" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import useVuelidate from "@vuelidate/core";
|
||||
import ModalForm from "~/components/Common/ModalForm";
|
||||
import mergeExisting from "~/functions/mergeExisting";
|
||||
|
||||
/* TODO Options API */
|
||||
|
||||
export default {
|
||||
name: 'BaseEditModal',
|
||||
components: {ModalForm}, // eslint-disable-line
|
||||
props: {
|
||||
createUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['relist'],
|
||||
setup() {
|
||||
return {v$: useVuelidate()}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
error: null,
|
||||
editUrl: null,
|
||||
form: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
langTitle () {
|
||||
return this.isEditMode
|
||||
? this.$gettext('Edit Record')
|
||||
: this.$gettext('Add Record');
|
||||
},
|
||||
isEditMode () {
|
||||
return this.editUrl !== null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetForm () {
|
||||
this.form = {};
|
||||
},
|
||||
create () {
|
||||
this.resetForm();
|
||||
this.loading = false;
|
||||
this.error = null;
|
||||
this.editUrl = null;
|
||||
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
edit (recordUrl) {
|
||||
this.resetForm();
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
this.editUrl = recordUrl;
|
||||
this.$refs.modal.show();
|
||||
|
||||
this.doLoad(recordUrl);
|
||||
},
|
||||
doLoad (recordUrl) {
|
||||
this.$wrapWithLoading(
|
||||
this.axios.get(recordUrl)
|
||||
).then((resp) => {
|
||||
this.populateForm(resp.data);
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
this.close();
|
||||
});
|
||||
},
|
||||
populateForm(data) {
|
||||
this.form = mergeExisting(this.form, data);
|
||||
},
|
||||
getSubmittableFormData() {
|
||||
return this.form;
|
||||
},
|
||||
buildSubmitRequest() {
|
||||
return {
|
||||
method: (this.isEditMode)
|
||||
? 'PUT'
|
||||
: 'POST',
|
||||
url: (this.isEditMode)
|
||||
? this.editUrl
|
||||
: this.createUrl,
|
||||
data: this.getSubmittableFormData()
|
||||
};
|
||||
},
|
||||
onSubmitSuccess() {
|
||||
this.$notifySuccess();
|
||||
this.$emit('relist');
|
||||
this.close();
|
||||
},
|
||||
onSubmitError(error) {
|
||||
this.error = error.response.data.message;
|
||||
},
|
||||
async doSubmit() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$errors.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
|
||||
this.$wrapWithLoading(
|
||||
this.axios(this.buildSubmitRequest())
|
||||
).then((resp) => {
|
||||
this.onSubmitSuccess(resp);
|
||||
}).catch((error) => {
|
||||
this.onSubmitError(error);
|
||||
});
|
||||
},
|
||||
close() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
clearContents() {
|
||||
this.v$.$reset();
|
||||
|
||||
this.loading = false;
|
||||
this.error = null;
|
||||
this.editUrl = null;
|
||||
this.resetForm();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -45,7 +45,7 @@
|
|||
<icon icon="search" />
|
||||
</div>
|
||||
<b-form-input
|
||||
v-model="filter"
|
||||
v-model="searchPhrase"
|
||||
debounce="200"
|
||||
type="search"
|
||||
class="search-field form-control"
|
||||
|
@ -74,7 +74,7 @@
|
|||
v-for="pageOption in pageOptions"
|
||||
:key="pageOption"
|
||||
:active="(pageOption === perPage)"
|
||||
@click="setPerPage(pageOption)"
|
||||
@click="settings.perPage = pageOption"
|
||||
>
|
||||
{{ getPerPageLabel(pageOption) }}
|
||||
</b-dropdown-item>
|
||||
|
@ -90,28 +90,13 @@
|
|||
<span class="caret" />
|
||||
</template>
|
||||
<b-dropdown-form class="pt-3">
|
||||
<div
|
||||
v-for="field in selectableFields"
|
||||
:key="field.key"
|
||||
class="form-group"
|
||||
>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
:id="'chk_field_' + field.key"
|
||||
v-model="field.visible"
|
||||
type="checkbox"
|
||||
class="custom-control-input"
|
||||
name="is_field_visible"
|
||||
@change="storeSettings"
|
||||
>
|
||||
<label
|
||||
class="custom-control-label"
|
||||
:for="'chk_field_'+field.key"
|
||||
>
|
||||
{{ field.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<b-form-checkbox-group
|
||||
v-model="settings.visibleFieldKeys"
|
||||
:options="selectableFields"
|
||||
value-field="key"
|
||||
text-field="label"
|
||||
stacked
|
||||
/>
|
||||
</b-dropdown-form>
|
||||
</b-dropdown>
|
||||
</b-btn-group>
|
||||
|
@ -121,10 +106,10 @@
|
|||
</div>
|
||||
<div class="datatable-main">
|
||||
<b-table
|
||||
ref="table"
|
||||
ref="$table"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:sort-by="sortBy"
|
||||
v-model:sort-desc="sortDesc"
|
||||
v-model:sort-by="settings.sortBy"
|
||||
v-model:sort-desc="settings.sortDesc"
|
||||
show-empty
|
||||
striped
|
||||
hover
|
||||
|
@ -142,11 +127,10 @@
|
|||
tbody-tr-class="align-middle"
|
||||
thead-tr-class="align-middle"
|
||||
selected-variant=""
|
||||
:filter="filter"
|
||||
:filter="searchPhrase"
|
||||
@row-selected="onRowSelected"
|
||||
@filtered="onFiltered"
|
||||
@refreshed="onRefreshed"
|
||||
@sort-changed="onSortChanged"
|
||||
>
|
||||
<template #head(selected)>
|
||||
<b-form-checkbox
|
||||
|
@ -215,310 +199,311 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from 'store';
|
||||
import {forEach, filter, map, defaultTo, includes} from 'lodash';
|
||||
<script setup>
|
||||
import {filter, map, includes, isEmpty} from 'lodash';
|
||||
import Icon from './Icon.vue';
|
||||
import {defineComponent} from "vue";
|
||||
import {computed, ref, toRef, watch} from "vue";
|
||||
import {useLocalStorage} from "@vueuse/core";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
|
||||
/* TODO Options API */
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DataTable',
|
||||
components: {Icon},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
apiUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
responsive: {
|
||||
type: [String, Boolean],
|
||||
default: true
|
||||
},
|
||||
paginated: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
pageOptions: {
|
||||
type: Array,
|
||||
default: () => [10, 25, 50, 100, 250, 500, 0]
|
||||
},
|
||||
defaultPerPage: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectFields: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
handleClientSide: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
requestConfig: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
requestProcess: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
emits: [
|
||||
'refreshed',
|
||||
'row-selected',
|
||||
'filtered'
|
||||
],
|
||||
data() {
|
||||
let allFields = [];
|
||||
forEach(this.fields, function (field) {
|
||||
allFields.push({
|
||||
...{
|
||||
label: '',
|
||||
isRowHeader: false,
|
||||
sortable: false,
|
||||
selectable: false,
|
||||
visible: true,
|
||||
formatter: null
|
||||
},
|
||||
...field
|
||||
});
|
||||
});
|
||||
apiUrl: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
responsive: {
|
||||
type: [String, Boolean],
|
||||
default: true
|
||||
},
|
||||
paginated: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
pageOptions: {
|
||||
type: Array,
|
||||
default: () => [10, 25, 50, 100, 250, 500, 0]
|
||||
},
|
||||
defaultPerPage: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectFields: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
handleClientSide: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
requestConfig: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
requestProcess: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
'refreshed',
|
||||
'row-selected',
|
||||
'filtered'
|
||||
]);
|
||||
|
||||
const selectedRows = ref([]);
|
||||
const searchPhrase = ref(null);
|
||||
const currentPage = ref(1);
|
||||
const totalRows = ref(0);
|
||||
const flushCache = ref(false);
|
||||
|
||||
watch(toRef(props, 'items'), (newVal) => {
|
||||
if (newVal !== null) {
|
||||
totalRows.value = newVal.length;
|
||||
}
|
||||
});
|
||||
|
||||
watch(filter, () => {
|
||||
currentPage.value = 1;
|
||||
});
|
||||
|
||||
const allFields = computed(() => {
|
||||
return map(props.fields, (field) => {
|
||||
return {
|
||||
allFields: allFields,
|
||||
selected: [],
|
||||
sortBy: null,
|
||||
sortDesc: false,
|
||||
storeKey: 'datatable_' + this.id + '_settings',
|
||||
filter: null,
|
||||
perPage: (this.paginated) ? this.defaultPerPage : 0,
|
||||
currentPage: 1,
|
||||
totalRows: 0,
|
||||
flushCache: false
|
||||
label: '',
|
||||
isRowHeader: false,
|
||||
sortable: false,
|
||||
selectable: false,
|
||||
visible: true,
|
||||
formatter: null,
|
||||
...field
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const selectableFields = computed(() => {
|
||||
return filter({...allFields.value}, (field) => {
|
||||
return field.selectable;
|
||||
});
|
||||
});
|
||||
|
||||
const defaultSelectableFields = computed(() => {
|
||||
return filter({...selectableFields.value}, (field) => {
|
||||
return field.visible;
|
||||
});
|
||||
});
|
||||
|
||||
const settings = useLocalStorage(
|
||||
'datatable_' + props.id + '_settings',
|
||||
{
|
||||
sortBy: null,
|
||||
sortDesc: false,
|
||||
perPage: props.defaultPerPage,
|
||||
visibleFieldKeys: map(defaultSelectableFields.value, (field) => field.key),
|
||||
},
|
||||
computed: {
|
||||
visibleFields() {
|
||||
let fields = this.allFields.slice();
|
||||
{
|
||||
mergeDefaults: true
|
||||
}
|
||||
);
|
||||
|
||||
if (this.selectable) {
|
||||
fields.unshift({
|
||||
key: 'selected',
|
||||
label: '',
|
||||
isRowHeader: false,
|
||||
sortable: false,
|
||||
selectable: false,
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
const visibleFieldKeys = computed(() => {
|
||||
if (!isEmpty(settings.value.visibleFieldKeys)) {
|
||||
return settings.value.visibleFieldKeys;
|
||||
}
|
||||
|
||||
if (!this.selectFields) {
|
||||
return fields;
|
||||
}
|
||||
return map(defaultSelectableFields.value, (field) => field.key);
|
||||
});
|
||||
|
||||
return filter(fields, (field) => {
|
||||
if (!field.selectable) {
|
||||
return true;
|
||||
}
|
||||
const perPage = computed(() => {
|
||||
return settings.value?.perPage ?? props.defaultPerPage;
|
||||
});
|
||||
|
||||
return field.visible;
|
||||
});
|
||||
},
|
||||
selectableFields() {
|
||||
return filter(this.allFields.slice(), (field) => {
|
||||
return field.selectable;
|
||||
});
|
||||
},
|
||||
showPagination() {
|
||||
return this.paginated && this.perPage !== 0;
|
||||
},
|
||||
perPageLabel() {
|
||||
return this.getPerPageLabel(this.perPage);
|
||||
},
|
||||
allSelected() {
|
||||
return ((this.selected.length === this.totalRows)
|
||||
|| (this.showPagination && this.selected.length === this.perPage));
|
||||
},
|
||||
itemProvider() {
|
||||
if (this.items !== null) {
|
||||
return this.items;
|
||||
}
|
||||
const visibleFields = computed(() => {
|
||||
let fields = allFields.value.slice();
|
||||
|
||||
return (ctx, callback) => {
|
||||
return this.loadItems(ctx, callback);
|
||||
}
|
||||
if (props.selectable) {
|
||||
fields.unshift({
|
||||
key: 'selected',
|
||||
label: '',
|
||||
isRowHeader: false,
|
||||
sortable: false,
|
||||
selectable: false,
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!props.selectFields) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
const visibleFieldsKeysValue = visibleFieldKeys.value;
|
||||
|
||||
return filter(fields, (field) => {
|
||||
if (!field.selectable) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
items(newVal) {
|
||||
if (newVal !== null) {
|
||||
this.totalRows = newVal.length;
|
||||
}
|
||||
},
|
||||
filter() {
|
||||
this.currentPage = 1;
|
||||
|
||||
return includes(visibleFieldsKeysValue, field.key);
|
||||
});
|
||||
});
|
||||
|
||||
const getPerPageLabel = (num) => {
|
||||
return (num === 0) ? 'All' : num.toString();
|
||||
};
|
||||
|
||||
const perPageLabel = computed(() => {
|
||||
return getPerPageLabel(perPage.value);
|
||||
});
|
||||
|
||||
const showPagination = computed(() => {
|
||||
return props.paginated && perPage.value !== 0;
|
||||
});
|
||||
|
||||
const allSelected = computed(() => {
|
||||
return ((selectedRows.value.length === totalRows.value)
|
||||
|| (showPagination.value && selectedRows.value.length === perPage.value));
|
||||
});
|
||||
|
||||
const {axios} = useAxios();
|
||||
|
||||
const loadItems = (ctx) => {
|
||||
let queryParams = {
|
||||
internal: true
|
||||
};
|
||||
|
||||
if (props.handleClientSide) {
|
||||
queryParams.rowCount = 0;
|
||||
} else {
|
||||
if (props.paginated) {
|
||||
queryParams.rowCount = ctx.perPage;
|
||||
queryParams.current = (ctx.perPage !== 0) ? ctx.currentPage : 1;
|
||||
} else {
|
||||
queryParams.rowCount = 0;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadStoredSettings();
|
||||
},
|
||||
methods: {
|
||||
loadStoredSettings() {
|
||||
if (store.enabled && store.get(this.storeKey) !== undefined) {
|
||||
let settings = store.get(this.storeKey);
|
||||
|
||||
this.perPage = defaultTo(settings.perPage, this.defaultPerPage);
|
||||
if (flushCache.value) {
|
||||
queryParams.flushCache = true;
|
||||
}
|
||||
|
||||
forEach(this.selectableFields, (field) => {
|
||||
field.visible = includes(settings.visibleFields, field.key);
|
||||
});
|
||||
if (typeof ctx.filter === 'string') {
|
||||
queryParams.searchPhrase = ctx.filter;
|
||||
}
|
||||
|
||||
if (settings.sortBy) {
|
||||
this.sortBy = settings.sortBy;
|
||||
this.sortDesc = settings.sortDesc;
|
||||
}
|
||||
}
|
||||
},
|
||||
storeSettings() {
|
||||
if (!store.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let settings = {
|
||||
'perPage': this.perPage,
|
||||
'sortBy': this.sortBy,
|
||||
'sortDesc': this.sortDesc,
|
||||
'visibleFields': map(this.visibleFields, 'key')
|
||||
};
|
||||
|
||||
store.set(this.storeKey, settings);
|
||||
},
|
||||
getPerPageLabel(num) {
|
||||
return (num === 0) ? 'All' : num.toString();
|
||||
},
|
||||
setPerPage(num) {
|
||||
this.perPage = num;
|
||||
this.storeSettings();
|
||||
},
|
||||
onClickRefresh(e) {
|
||||
if (e.shiftKey) {
|
||||
this.relist();
|
||||
} else {
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
onSortChanged() {
|
||||
this.$nextTick(() => {
|
||||
this.storeSettings();
|
||||
});
|
||||
},
|
||||
onRefreshed() {
|
||||
this.$emit('refreshed');
|
||||
},
|
||||
refresh() {
|
||||
this.$refs.table.refresh();
|
||||
},
|
||||
navigate() {
|
||||
this.filter = null;
|
||||
this.currentPage = 1;
|
||||
this.flushCache = true;
|
||||
this.refresh();
|
||||
},
|
||||
relist() {
|
||||
this.flushCache = true;
|
||||
this.refresh();
|
||||
},
|
||||
setFilter(newTerm) {
|
||||
this.filter = newTerm;
|
||||
},
|
||||
loadItems(ctx) {
|
||||
let queryParams = {
|
||||
internal: true
|
||||
};
|
||||
|
||||
if (this.handleClientSide) {
|
||||
queryParams.rowCount = 0;
|
||||
} else {
|
||||
if (this.paginated) {
|
||||
queryParams.rowCount = ctx.perPage;
|
||||
queryParams.current = (ctx.perPage !== 0) ? ctx.currentPage : 1;
|
||||
} else {
|
||||
queryParams.rowCount = 0;
|
||||
}
|
||||
|
||||
if (this.flushCache) {
|
||||
queryParams.flushCache = true;
|
||||
}
|
||||
|
||||
if (typeof ctx.filter === 'string') {
|
||||
queryParams.searchPhrase = ctx.filter;
|
||||
}
|
||||
|
||||
if ('' !== ctx.sortBy) {
|
||||
queryParams.sort = ctx.sortBy;
|
||||
queryParams.sortOrder = (ctx.sortDesc) ? 'DESC' : 'ASC';
|
||||
}
|
||||
}
|
||||
|
||||
let requestConfig = {params: queryParams};
|
||||
if (typeof this.requestConfig === 'function') {
|
||||
requestConfig = this.requestConfig(requestConfig);
|
||||
}
|
||||
|
||||
return this.axios.get(ctx.apiUrl, requestConfig).then((resp) => {
|
||||
this.totalRows = resp.data.total;
|
||||
|
||||
let rows = resp.data.rows;
|
||||
if (typeof this.requestProcess === 'function') {
|
||||
rows = this.requestProcess(rows);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}).catch((err) => {
|
||||
this.totalRows = 0;
|
||||
|
||||
console.error(err.response.data.message);
|
||||
return [];
|
||||
}).finally(() => {
|
||||
this.flushCache = false;
|
||||
});
|
||||
},
|
||||
onRowSelected(items) {
|
||||
this.selected = items;
|
||||
this.$emit('row-selected', items);
|
||||
},
|
||||
toggleSelected() {
|
||||
if (this.allSelected) {
|
||||
this.$refs.table.clearSelected();
|
||||
} else {
|
||||
this.$refs.table.selectAllRows();
|
||||
}
|
||||
},
|
||||
onFiltered(filter) {
|
||||
this.$emit('filtered', filter);
|
||||
if ('' !== ctx.sortBy) {
|
||||
queryParams.sort = ctx.sortBy;
|
||||
queryParams.sortOrder = (ctx.sortDesc) ? 'DESC' : 'ASC';
|
||||
}
|
||||
}
|
||||
|
||||
let requestConfig = {params: queryParams};
|
||||
if (typeof props.requestConfig === 'function') {
|
||||
requestConfig = props.requestConfig(requestConfig);
|
||||
}
|
||||
|
||||
return axios.get(ctx.apiUrl, requestConfig).then((resp) => {
|
||||
totalRows.value = resp.data.total;
|
||||
|
||||
let rows = resp.data.rows;
|
||||
if (typeof props.requestProcess === 'function') {
|
||||
rows = props.requestProcess(rows);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}).catch((err) => {
|
||||
totalRows.value = 0;
|
||||
|
||||
console.error(err.response.data.message);
|
||||
return [];
|
||||
}).finally(() => {
|
||||
flushCache.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const itemProvider = computed(() => {
|
||||
if (props.items !== null) {
|
||||
return props.items;
|
||||
}
|
||||
|
||||
return (ctx, callback) => {
|
||||
return loadItems(ctx, callback);
|
||||
}
|
||||
});
|
||||
|
||||
const $table = ref(); // Template Ref
|
||||
|
||||
const refresh = () => {
|
||||
$table.value?.refresh();
|
||||
};
|
||||
|
||||
const toggleSelected = () => {
|
||||
if (allSelected.value) {
|
||||
$table.value?.clearSelected();
|
||||
} else {
|
||||
$table.value?.selectAllRows();
|
||||
}
|
||||
};
|
||||
|
||||
const relist = () => {
|
||||
flushCache.value = true;
|
||||
refresh();
|
||||
};
|
||||
|
||||
const onClickRefresh = (e) => {
|
||||
if (e.shiftKey) {
|
||||
relist();
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const onRefreshed = () => {
|
||||
emit('refreshed');
|
||||
};
|
||||
|
||||
const navigate = () => {
|
||||
searchPhrase.value = null;
|
||||
currentPage.value = 1;
|
||||
relist();
|
||||
};
|
||||
|
||||
const setFilter = (newTerm) => {
|
||||
searchPhrase.value = newTerm;
|
||||
};
|
||||
|
||||
const onRowSelected = (items) => {
|
||||
selectedRows.value = items;
|
||||
emit('row-selected', items);
|
||||
};
|
||||
|
||||
const onFiltered = (filter) => {
|
||||
emit('filtered', filter);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
relist,
|
||||
navigate,
|
||||
setFilter
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script>
|
||||
/* TODO Options API */
|
||||
|
||||
export default {
|
||||
name: 'BFormFieldset',
|
||||
methods: {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
class="text-right text-white-50"
|
||||
>
|
||||
<stations-common-quota
|
||||
ref="quota"
|
||||
ref="$quota"
|
||||
:quota-url="quotaUrl"
|
||||
/>
|
||||
</b-col>
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
<data-table
|
||||
id="station_media"
|
||||
ref="datatable"
|
||||
ref="$datatable"
|
||||
selectable
|
||||
paginated
|
||||
select-fields
|
||||
|
@ -83,6 +83,7 @@
|
|||
:api-url="listUrl"
|
||||
:request-config="requestConfig"
|
||||
@row-selected="onRowSelected"
|
||||
@filtered="onFiltered"
|
||||
>
|
||||
<template #cell(path)="row">
|
||||
<div class="d-flex align-items-center">
|
||||
|
@ -228,13 +229,13 @@
|
|||
/>
|
||||
|
||||
<rename-modal
|
||||
ref="renameModal"
|
||||
ref="$renameModal"
|
||||
:rename-url="renameUrl"
|
||||
@relist="onTriggerRelist"
|
||||
/>
|
||||
|
||||
<edit-modal
|
||||
ref="editModal"
|
||||
ref="$editModal"
|
||||
:custom-fields="customFields"
|
||||
:playlists="playlists"
|
||||
@relist="onTriggerRelist"
|
||||
|
@ -254,223 +255,232 @@ import EditModal from './Media/EditModal';
|
|||
import StationsCommonQuota from "~/components/Stations/Common/Quota";
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import AlbumArt from '~/components/Common/AlbumArt';
|
||||
import PlayButton from "~/components/Common/PlayButton";</script>
|
||||
|
||||
<script>
|
||||
import formatFileSize from '~/functions/formatFileSize.js';
|
||||
import {forEach, map, partition} from 'lodash';
|
||||
import {DateTime} from 'luxon';
|
||||
import PlayButton from "~/components/Common/PlayButton";
|
||||
import {useTranslate} from "~/vendor/gettext";
|
||||
import {computed, nextTick, onMounted, ref} from "vue";
|
||||
import {forEach, map, partition} from "lodash";
|
||||
import {useAzuraCast} from "~/vendor/azuracast";
|
||||
import {DateTime} from "luxon";
|
||||
import {useEventListener} from "@vueuse/core";
|
||||
import formatFileSize from "../../functions/formatFileSize";
|
||||
|
||||
/* TODO Options API */
|
||||
|
||||
export default {
|
||||
props: {
|
||||
listUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
batchUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
uploadUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
listDirectoriesUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
mkdirUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
renameUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
quotaUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
initialPlaylists: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
validMimeTypes: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
stationTimeZone: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
showSftp: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
sftpUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
supportsImmediateQueue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
const props = defineProps({
|
||||
listUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
data() {
|
||||
let fields = [
|
||||
{key: 'path', isRowHeader: true, label: this.$gettext('Name'), sortable: true},
|
||||
{key: 'media.title', label: this.$gettext('Title'), sortable: true, selectable: true, visible: false},
|
||||
{
|
||||
key: 'media.artist',
|
||||
label: this.$gettext('Artist'),
|
||||
sortable: true,
|
||||
selectable: true,
|
||||
visible: false
|
||||
},
|
||||
{key: 'media.album', label: this.$gettext('Album'), sortable: true, selectable: true, visible: false},
|
||||
{key: 'media.genre', label: this.$gettext('Genre'), sortable: true, selectable: true, visible: false},
|
||||
{key: 'media.isrc', label: this.$gettext('ISRC'), sortable: true, selectable: true, visible: false},
|
||||
{key: 'media.length', label: this.$gettext('Length'), sortable: true, selectable: true, visible: true}
|
||||
];
|
||||
batchUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
uploadUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
listDirectoriesUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
mkdirUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
renameUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
quotaUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
initialPlaylists: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
validMimeTypes: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
stationTimeZone: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
showSftp: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
sftpUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
supportsImmediateQueue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
forEach(this.customFields.slice(), (field) => {
|
||||
fields.push({
|
||||
key: 'media.custom_fields[' + field.id + ']',
|
||||
label: field.name,
|
||||
sortable: true,
|
||||
selectable: true,
|
||||
visible: false
|
||||
});
|
||||
const {$gettext} = useTranslate();
|
||||
const {timeConfig} = useAzuraCast();
|
||||
|
||||
const fields = computed(() => {
|
||||
let fields = [
|
||||
{key: 'path', isRowHeader: true, label: $gettext('Name'), sortable: true},
|
||||
{key: 'media.title', label: $gettext('Title'), sortable: true, selectable: true, visible: false},
|
||||
{
|
||||
key: 'media.artist',
|
||||
label: $gettext('Artist'),
|
||||
sortable: true,
|
||||
selectable: true,
|
||||
visible: false
|
||||
},
|
||||
{key: 'media.album', label: $gettext('Album'), sortable: true, selectable: true, visible: false},
|
||||
{key: 'media.genre', label: $gettext('Genre'), sortable: true, selectable: true, visible: false},
|
||||
{key: 'media.isrc', label: $gettext('ISRC'), sortable: true, selectable: true, visible: false},
|
||||
{key: 'media.length', label: $gettext('Length'), sortable: true, selectable: true, visible: true}
|
||||
];
|
||||
|
||||
forEach({...props.customFields}, (field) => {
|
||||
fields.push({
|
||||
key: 'media.custom_fields[' + field.id + ']',
|
||||
label: field.name,
|
||||
sortable: true,
|
||||
selectable: true,
|
||||
visible: false
|
||||
});
|
||||
});
|
||||
|
||||
fields.push(
|
||||
{key: 'size', label: this.$gettext('Size'), sortable: true, selectable: true, visible: true},
|
||||
{
|
||||
key: 'timestamp',
|
||||
label: this.$gettext('Modified'),
|
||||
sortable: true,
|
||||
formatter: (value) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
fields.push(
|
||||
{key: 'size', label: $gettext('Size'), sortable: true, selectable: true, visible: true},
|
||||
{
|
||||
key: 'timestamp',
|
||||
label: $gettext('Modified'),
|
||||
sortable: true,
|
||||
formatter: (value) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const {timeConfig} = useAzuraCast();
|
||||
|
||||
return DateTime.fromSeconds(value).setZone(this.stationTimeZone).toLocaleString(
|
||||
{...DateTime.DATETIME_MED, ...timeConfig}
|
||||
);
|
||||
},
|
||||
selectable: true,
|
||||
visible: true
|
||||
return DateTime.fromSeconds(value).setZone(props.stationTimeZone).toLocaleString(
|
||||
{...DateTime.DATETIME_MED, ...timeConfig}
|
||||
);
|
||||
},
|
||||
{
|
||||
key: 'playlists',
|
||||
label: this.$gettext('Playlists'),
|
||||
sortable: false,
|
||||
selectable: true,
|
||||
visible: true
|
||||
},
|
||||
{key: 'commands', label: this.$gettext('Actions'), sortable: false, class: 'shrink'}
|
||||
);
|
||||
selectable: true,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
key: 'playlists',
|
||||
label: $gettext('Playlists'),
|
||||
sortable: false,
|
||||
selectable: true,
|
||||
visible: true
|
||||
},
|
||||
{key: 'commands', label: $gettext('Actions'), sortable: false, class: 'shrink'}
|
||||
);
|
||||
|
||||
return {
|
||||
fields: fields,
|
||||
playlists: this.initialPlaylists,
|
||||
selectedItems: {
|
||||
all: [],
|
||||
files: [],
|
||||
directories: []
|
||||
},
|
||||
currentDirectory: '',
|
||||
searchPhrase: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// Load directory from URL hash, if applicable.
|
||||
let urlHash = decodeURIComponent(window.location.hash.substring(1).replace(/\+/g, '%20'));
|
||||
return fields;
|
||||
});
|
||||
|
||||
if ('' !== urlHash) {
|
||||
if (this.isFilterString(urlHash)) {
|
||||
this.$nextTick(() => {
|
||||
this.onHashChange();
|
||||
});
|
||||
} else {
|
||||
this.currentDirectory = urlHash;
|
||||
}
|
||||
}
|
||||
const playlists = ref(props.initialPlaylists);
|
||||
const selectedItems = ref({
|
||||
all: [],
|
||||
files: [],
|
||||
directories: []
|
||||
});
|
||||
const currentDirectory = ref('');
|
||||
const searchPhrase = ref(null);
|
||||
|
||||
window.addEventListener('hashchange', this.onHashChange);
|
||||
},
|
||||
unmounted() {
|
||||
window.removeEventListener('hashchange', this.onHashChange);
|
||||
},
|
||||
methods: {
|
||||
formatFileSize(size) {
|
||||
return formatFileSize(size);
|
||||
},
|
||||
onRowSelected(items) {
|
||||
let splitItems = partition(items, 'is_dir');
|
||||
const onRowSelected = (items) => {
|
||||
let splitItems = partition(items, 'is_dir');
|
||||
|
||||
this.selectedItems = {
|
||||
all: items,
|
||||
files: map(splitItems[1], 'path'),
|
||||
directories: map(splitItems[0], 'path')
|
||||
};
|
||||
},
|
||||
onTriggerNavigate() {
|
||||
this.$refs.datatable.navigate();
|
||||
},
|
||||
onTriggerRelist() {
|
||||
this.$refs.quota.update();
|
||||
this.$refs.datatable.relist();
|
||||
},
|
||||
onAddPlaylist(row) {
|
||||
this.playlists.push(row);
|
||||
},
|
||||
onHashChange() {
|
||||
// Handle links from the sidebar for special functions.
|
||||
let urlHash = decodeURIComponent(window.location.hash.substring(1).replace(/\+/g, '%20'));
|
||||
selectedItems.value = {
|
||||
all: items,
|
||||
files: map(splitItems[1], 'path'),
|
||||
directories: map(splitItems[0], 'path')
|
||||
};
|
||||
};
|
||||
|
||||
if ('' !== urlHash && this.isFilterString(urlHash)) {
|
||||
window.location.hash = '';
|
||||
this.filter(urlHash);
|
||||
}
|
||||
},
|
||||
isFilterString(str) {
|
||||
return str.substring(0, 9) === 'playlist:' || str.substring(0, 8) === 'special:';
|
||||
},
|
||||
changeDirectory(newDir) {
|
||||
window.location.hash = newDir;
|
||||
const $datatable = ref(); // Template Ref
|
||||
|
||||
this.currentDirectory = newDir;
|
||||
this.onTriggerNavigate();
|
||||
},
|
||||
filter(newFilter) {
|
||||
this.$refs.datatable.setFilter(newFilter);
|
||||
},
|
||||
onFiltered(newFilter) {
|
||||
this.searchPhrase = newFilter;
|
||||
},
|
||||
rename(path) {
|
||||
this.$refs.renameModal.open(path);
|
||||
},
|
||||
edit(recordUrl, albumArtUrl, audioUrl, waveformUrl) {
|
||||
this.$refs.editModal.open(recordUrl, albumArtUrl, audioUrl, waveformUrl);
|
||||
},
|
||||
requestConfig(config) {
|
||||
config.params.currentDirectory = this.currentDirectory;
|
||||
return config;
|
||||
}
|
||||
const onTriggerNavigate = () => {
|
||||
$datatable.value?.navigate();
|
||||
};
|
||||
|
||||
const filter = (newFilter) => {
|
||||
$datatable.value.setFilter(newFilter);
|
||||
};
|
||||
|
||||
const $quota = ref(); // Template Ref
|
||||
|
||||
const onTriggerRelist = () => {
|
||||
$quota.value?.update();
|
||||
$datatable.value?.relist();
|
||||
};
|
||||
|
||||
const onAddPlaylist = (row) => {
|
||||
playlists.value.push(row);
|
||||
};
|
||||
|
||||
const isFilterString = (str) =>
|
||||
(str.substring(0, 9) === 'playlist:' || str.substring(0, 8) === 'special:');
|
||||
|
||||
const onHashChange = () => {
|
||||
// Handle links from the sidebar for special functions.
|
||||
let urlHash = decodeURIComponent(window.location.hash.substring(1).replace(/\+/g, '%20'));
|
||||
|
||||
if ('' !== urlHash && isFilterString(urlHash)) {
|
||||
window.location.hash = '';
|
||||
filter(urlHash);
|
||||
}
|
||||
};
|
||||
|
||||
const changeDirectory = (newDir) => {
|
||||
window.location.hash = newDir;
|
||||
currentDirectory.value = newDir;
|
||||
onTriggerNavigate();
|
||||
};
|
||||
|
||||
const onFiltered = (newFilter) => {
|
||||
searchPhrase.value = newFilter;
|
||||
};
|
||||
|
||||
const $renameModal = ref(); // Template Ref
|
||||
|
||||
const rename = (path) => {
|
||||
$renameModal.value?.open(path);
|
||||
};
|
||||
|
||||
const $editModal = ref(); // Template Ref
|
||||
|
||||
const edit = (recordUrl, albumArtUrl, audioUrl, waveformUrl) => {
|
||||
$editModal.value?.open(recordUrl, albumArtUrl, audioUrl, waveformUrl);
|
||||
};
|
||||
|
||||
const requestConfig = (config) => {
|
||||
config.params.currentDirectory = currentDirectory.value;
|
||||
return config;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Load directory from URL hash, if applicable.
|
||||
let urlHash = decodeURIComponent(window.location.hash.substring(1).replace(/\+/g, '%20'));
|
||||
|
||||
if ('' !== urlHash) {
|
||||
if (isFilterString(urlHash)) {
|
||||
nextTick(() => {
|
||||
onHashChange();
|
||||
});
|
||||
} else {
|
||||
currentDirectory.value = urlHash;
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(window, 'hashchange', onHashChange);
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue