bug fixes
This commit is contained in:
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder;
|
|||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class Conductor
|
class Conductor
|
||||||
@@ -552,19 +553,35 @@ class Conductor
|
|||||||
} else {
|
} else {
|
||||||
$limitFields = array_map('strtolower', $limitFields);
|
$limitFields = array_map('strtolower', $limitFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
$tokens = preg_split('/([()]|,OR,|,AND,|,)/', $filterString, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE));
|
$tokens = preg_split('/([()]|,OR,|,AND,|,)/', $filterString, -1, (PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE));
|
||||||
$glued = [];
|
$glued = [];
|
||||||
$glueToken = '';
|
$glueToken = '';
|
||||||
foreach ($tokens as $item) {
|
foreach ($tokens as $item) {
|
||||||
if ($glueToken === '') {
|
if ($glueToken === '') {
|
||||||
if (preg_match('/(?<!\\\\)[\'"]/', $item, $matches, PREG_OFFSET_CAPTURE) === 1) {
|
$amount = preg_match_all('/(?<!\\\\)[\'"]/', $item, $matches, PREG_OFFSET_CAPTURE);
|
||||||
$glueToken = $matches[0][0];
|
if ($amount > 0) {
|
||||||
$item = substr($item, 0, $matches[0][1]) . substr($item, ($matches[0][1] + 1));
|
$glueToken = $matches[0][0][0];
|
||||||
$item = str_replace("\\$glueToken", $glueToken, $item);
|
if ($amount === 1) {
|
||||||
}
|
$item = substr($item, 0, $matches[0][1]) . substr($item, ($matches[0][1] + 1));
|
||||||
|
$item = str_replace("\\$glueToken", $glueToken, $item);
|
||||||
$glued[] = $item;
|
$glued[] = $item;
|
||||||
|
} else {
|
||||||
|
$lastPos = 0;
|
||||||
|
$newStr = '';
|
||||||
|
foreach ($matches[0] as $pos) {
|
||||||
|
$matchLen = strlen($glueToken);
|
||||||
|
$startPos = ($pos[1] - $lastPos);
|
||||||
|
$newStr .= substr($item, $lastPos, $startPos);
|
||||||
|
$lastPos = ($pos[1] + $matchLen);
|
||||||
|
}
|
||||||
|
$newStr .= substr($item, $lastPos);
|
||||||
|
$newStr = str_replace("\\$glueToken", $glueToken, $newStr);
|
||||||
|
$glued[] = $newStr;
|
||||||
|
$glueToken = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$glued[] = $item;
|
||||||
|
}//end if
|
||||||
} else {
|
} else {
|
||||||
// search for ending glue token
|
// search for ending glue token
|
||||||
if (preg_match('/(?<!\\\\)' . $glueToken . '/', $item, $matches, PREG_OFFSET_CAPTURE) === 1) {
|
if (preg_match('/(?<!\\\\)' . $glueToken . '/', $item, $matches, PREG_OFFSET_CAPTURE) === 1) {
|
||||||
@@ -575,7 +592,7 @@ class Conductor
|
|||||||
$item = str_replace("\\$glueToken", $glueToken, $item);
|
$item = str_replace("\\$glueToken", $glueToken, $item);
|
||||||
|
|
||||||
$glued[(count($glued) - 1)] .= $item;
|
$glued[(count($glued) - 1)] .= $item;
|
||||||
}
|
}//end if
|
||||||
}//end foreach
|
}//end foreach
|
||||||
$tokens = $glued;
|
$tokens = $glued;
|
||||||
|
|
||||||
@@ -671,7 +688,10 @@ class Conductor
|
|||||||
return $index;
|
return $index;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Log::info(print_r($tokens, true));
|
||||||
$parseTokens($tokens, 0, 0);
|
$parseTokens($tokens, 0, 0);
|
||||||
|
|
||||||
|
// Log::info($this->query->toSql());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class EventConductor extends Conductor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The included fields
|
* The included fields
|
||||||
*
|
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $includes = ['attachments'];
|
protected $includes = ['attachments'];
|
||||||
@@ -101,7 +100,7 @@ class EventConductor extends Conductor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Include Attachments Field.
|
* Include Attachments Field.
|
||||||
*
|
*
|
||||||
* @param Model $model Them model.
|
* @param Model $model Them model.
|
||||||
* @return mixed The model result.
|
* @return mixed The model result.
|
||||||
*/
|
*/
|
||||||
@@ -114,7 +113,7 @@ class EventConductor extends Conductor
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the Hero field.
|
* Transform the Hero field.
|
||||||
*
|
*
|
||||||
* @param mixed $value The current value.
|
* @param mixed $value The current value.
|
||||||
* @return array The new value.
|
* @return array The new value.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -48,6 +48,19 @@
|
|||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@keyup="handleKeyup"></textarea>
|
@keyup="handleKeyup"></textarea>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="props.type == 'select'">
|
||||||
|
<ion-icon
|
||||||
|
class="select-dropdown-icon"
|
||||||
|
name="caret-down-outline" />
|
||||||
|
<select class="select-input-control">
|
||||||
|
<option
|
||||||
|
v-for="option in Object.entries(props.options)"
|
||||||
|
:key="option[0]"
|
||||||
|
:value="option[0]">
|
||||||
|
{{ option[1] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
class="invalid-icon"
|
class="invalid-icon"
|
||||||
@@ -146,6 +159,11 @@ const props = defineProps({
|
|||||||
default: "",
|
default: "",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
@@ -445,6 +463,24 @@ const handleChange = (event) => {
|
|||||||
padding: 15px 30px;
|
padding: 15px 30px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-dropdown-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 110%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-input-control {
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
padding: 22px 16px 8px 16px;
|
||||||
|
border: 1px solid var(--base-color-darker);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--base-color-light);
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,10 +502,4 @@ const handleChange = (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.control-group.control-type-input {
|
|
||||||
// width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface Event {
|
|||||||
price: string;
|
price: string;
|
||||||
ages: string;
|
ages: string;
|
||||||
attachments: Array<Media>;
|
attachments: Array<Media>;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventResponse {
|
export interface EventResponse {
|
||||||
|
|||||||
@@ -5,17 +5,20 @@
|
|||||||
<SMInput
|
<SMInput
|
||||||
v-model="filterKeywords"
|
v-model="filterKeywords"
|
||||||
label="Keywords"
|
label="Keywords"
|
||||||
@blur="handleFilter" />
|
@blur="handleFilter"
|
||||||
|
@keyup.enter="handleFilter" />
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="filterLocation"
|
v-model="filterLocation"
|
||||||
label="Location"
|
label="Location"
|
||||||
@blur="handleFilter" />
|
@blur="handleFilter"
|
||||||
|
@keyup.enter="handleFilter" />
|
||||||
<SMInput
|
<SMInput
|
||||||
v-model="filterDateRange"
|
v-model="filterDateRange"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
label="Date Range"
|
label="Date Range"
|
||||||
:feedback-invalid="dateRangeError"
|
:feedback-invalid="dateRangeError"
|
||||||
@blur="handleFilter" />
|
@blur="handleFilter"
|
||||||
|
@keyup.enter="handleFilter" />
|
||||||
</SMToolbar>
|
</SMToolbar>
|
||||||
<SMPagination
|
<SMPagination
|
||||||
v-if="postsTotal > postsPerPage"
|
v-if="postsTotal > postsPerPage"
|
||||||
@@ -125,13 +128,16 @@ const handleLoad = async () => {
|
|||||||
(title:""cats, dogs", mice",OR,content:"\"cats, dogs\", mice")
|
(title:""cats, dogs", mice",OR,content:"\"cats, dogs\", mice")
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
query["filter"] = [];
|
||||||
if (filterKeywords.value && filterKeywords.value.length > 0) {
|
if (filterKeywords.value && filterKeywords.value.length > 0) {
|
||||||
let value = filterKeywords.value.replace(/"/g, '\\"');
|
let value = filterKeywords.value.replace(/"/g, '\\"');
|
||||||
|
|
||||||
query["filter"] = `(title:"${value}",OR,content:"${value}")`;
|
query["filter"].push(`(title:"${value}",OR,content:"${value}")`);
|
||||||
}
|
}
|
||||||
if (filterLocation.value && filterLocation.value.length > 0) {
|
if (filterLocation.value && filterLocation.value.length > 0) {
|
||||||
query["location"] = filterLocation.value;
|
let value = filterLocation.value.replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
query["filter"].push(`(location:"${value}",OR,address:"${value}")`);
|
||||||
}
|
}
|
||||||
if (filterDateRange.value && filterDateRange.value.length > 0) {
|
if (filterDateRange.value && filterDateRange.value.length > 0) {
|
||||||
let error = false;
|
let error = false;
|
||||||
@@ -167,6 +173,12 @@ const handleLoad = async () => {
|
|||||||
formMessage.value = "";
|
formMessage.value = "";
|
||||||
events = [];
|
events = [];
|
||||||
|
|
||||||
|
if (query["filter"].length > 0) {
|
||||||
|
query["filter"] = query["filter"].join(",AND,");
|
||||||
|
} else {
|
||||||
|
delete query["filter"];
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(query).length == 0) {
|
if (Object.keys(query).length == 0) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const startingDate = new Date(now.setDate(now.getDate() - 14));
|
const startingDate = new Date(now.setDate(now.getDate() - 14));
|
||||||
@@ -181,6 +193,8 @@ const handleLoad = async () => {
|
|||||||
query["limit"] = postsPerPage;
|
query["limit"] = postsPerPage;
|
||||||
query["page"] = postsPage.value;
|
query["page"] = postsPage.value;
|
||||||
|
|
||||||
|
console.log(query);
|
||||||
|
|
||||||
let result = await api.get({
|
let result = await api.get({
|
||||||
url: "/events",
|
url: "/events",
|
||||||
params: query,
|
params: query,
|
||||||
@@ -330,6 +344,10 @@ const computedAges = (ages: string): string => {
|
|||||||
const trimmed = ages.trim();
|
const trimmed = ages.trim();
|
||||||
const regex = /^(\d+)(\s*\+?\s*|\s*-\s*\d+\s*)?$/;
|
const regex = /^(\d+)(\s*\+?\s*|\s*-\s*\d+\s*)?$/;
|
||||||
|
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return "All ages";
|
||||||
|
}
|
||||||
|
|
||||||
if (regex.test(trimmed)) {
|
if (regex.test(trimmed)) {
|
||||||
return `Ages ${trimmed}`;
|
return `Ages ${trimmed}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMPage
|
<SMPage :page-error="pageError" permission="admin/events">
|
||||||
:page-error="pageError"
|
<SMMastHead
|
||||||
permission="admin/events"
|
:title="pageHeading"
|
||||||
class="sm-page-event-edit">
|
:back-link="{ name: 'dashboard-event-list' }"
|
||||||
<template #container>
|
back-title="Back to Events" />
|
||||||
<h1>{{ page_title }}</h1>
|
<SMContainer class="flex-grow-1">
|
||||||
|
<SMLoading v-if="pageLoading" large />
|
||||||
<SMForm
|
<SMForm
|
||||||
|
v-else
|
||||||
:model-value="form"
|
:model-value="form"
|
||||||
@submit="handleSubmit"
|
@submit="handleSubmit"
|
||||||
@failed-validation="handleFailValidation">
|
@failed-validation="handleFailValidation">
|
||||||
@@ -122,7 +124,7 @@
|
|||||||
</SMFormFooter>
|
</SMFormFooter>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
</SMForm>
|
</SMForm>
|
||||||
</template>
|
</SMContainer>
|
||||||
</SMPage>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -149,11 +151,16 @@ import SMInputAttachments from "../../components/SMInputAttachments.vue";
|
|||||||
import SMForm from "../../components/SMForm.vue";
|
import SMForm from "../../components/SMForm.vue";
|
||||||
import { EventResponse } from "../../helpers/api.types";
|
import { EventResponse } from "../../helpers/api.types";
|
||||||
import { useToastStore } from "../../store/ToastStore";
|
import { useToastStore } from "../../store/ToastStore";
|
||||||
|
import SMMastHead from "../../components/SMMastHead.vue";
|
||||||
|
import SMLoading from "../../components/SMLoading.vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const page_title = route.params.id ? "Edit Event" : "Create New Event";
|
|
||||||
const pageError = ref(200);
|
const pageError = ref(200);
|
||||||
|
const pageLoading = ref(true);
|
||||||
|
const pageHeading = route.params.id ? "Edit Event" : "Create Event";
|
||||||
|
|
||||||
const attachments = ref([]);
|
const attachments = ref([]);
|
||||||
|
|
||||||
const address_data = computed(() => {
|
const address_data = computed(() => {
|
||||||
@@ -259,7 +266,7 @@ let form = reactive(
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
try {
|
try {
|
||||||
form.loading(true);
|
pageLoading.value = true;
|
||||||
|
|
||||||
const result = await api.get({
|
const result = await api.get({
|
||||||
url: "/events/{id}",
|
url: "/events/{id}",
|
||||||
@@ -308,7 +315,7 @@ const loadData = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
pageError.value = err.response.status;
|
pageError.value = err.response.status;
|
||||||
} finally {
|
} finally {
|
||||||
form.loading(false);
|
pageLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -380,8 +387,11 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
router.push({ name: "dashboard-event-list" });
|
router.push({ name: "dashboard-event-list" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleFailValidation();
|
useToastStore().addToast({
|
||||||
form.apiErrors(error);
|
title: "Server error",
|
||||||
|
content: "An error occurred saving the media.",
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,145 +1,156 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMPage permission="admin/events">
|
<SMPage permission="admin/media">
|
||||||
<template #container>
|
<SMMastHead
|
||||||
<SMHeading heading="Events" />
|
title="Events"
|
||||||
<SMMessage
|
:back-link="{ name: 'dashboard' }"
|
||||||
v-if="formMessage.message"
|
back-title="Return to Dashboard" />
|
||||||
:icon="formMessage.icon"
|
<SMContainer class="flex-grow-1">
|
||||||
:type="formMessage.type"
|
|
||||||
:message="formMessage.message" />
|
|
||||||
<SMToolbar>
|
<SMToolbar>
|
||||||
<template #left>
|
<SMButton
|
||||||
<SMButton
|
:to="{ name: 'workshops' }"
|
||||||
type="primary"
|
type="primary"
|
||||||
label="Create Event"
|
label="Create Event"
|
||||||
:small="true"
|
@click="handleCreate" />
|
||||||
@click="handleCreate" />
|
<SMInput
|
||||||
</template>
|
v-model="itemSearch"
|
||||||
<template #right>
|
label="Search"
|
||||||
<SMInput
|
class="toolbar-search"
|
||||||
v-model="search"
|
@keyup.enter="handleSearch">
|
||||||
label="Search"
|
<template #append>
|
||||||
:small="true"
|
<SMButton
|
||||||
style="max-width: 250px" />
|
type="primary"
|
||||||
</template>
|
label="Search"
|
||||||
|
icon="search-outline"
|
||||||
|
@click="handleSearch" />
|
||||||
|
</template>
|
||||||
|
</SMInput>
|
||||||
</SMToolbar>
|
</SMToolbar>
|
||||||
|
<SMLoading large v-if="itemsLoading" />
|
||||||
<EasyDataTable
|
<template v-else>
|
||||||
v-model:server-options="serverOptions"
|
<SMPagination
|
||||||
:server-items-length="serverItemsLength"
|
v-if="items.length < itemsTotal"
|
||||||
:loading="formLoading"
|
v-model="itemsPage"
|
||||||
:headers="headers"
|
:total="itemsTotal"
|
||||||
:items="items"
|
:per-page="itemsPerPage" />
|
||||||
:search-value="search">
|
<SMNoItems v-if="items.length == 0" text="No Media Found" />
|
||||||
<template #loading>
|
<SMTable
|
||||||
<SMLoadingIcon />
|
v-else
|
||||||
</template>
|
:headers="headers"
|
||||||
<template #item-title="item">
|
:items="items"
|
||||||
<router-link
|
@row-click="handleEdit">
|
||||||
:to="{
|
<template #item-location="item"
|
||||||
name: 'dashboard-event-edit',
|
>{{ parseEventLocation(item) }}
|
||||||
params: { id: item.id },
|
</template>
|
||||||
}"
|
<template #item-actions="item">
|
||||||
>{{ item.title }}</router-link
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template #item-actions="item">
|
|
||||||
<div class="action-wrapper">
|
|
||||||
<SMButton
|
<SMButton
|
||||||
label="Edit"
|
label="Edit"
|
||||||
:dropdown="{
|
:dropdown="{
|
||||||
duplicate: 'Duplicate',
|
duplicate: 'Duplicate',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
}"
|
}"
|
||||||
@click="handleClick(item, $event)"></SMButton>
|
size="medium"
|
||||||
</div>
|
@click="
|
||||||
</template>
|
handleActionButton(item, $event)
|
||||||
</EasyDataTable>
|
"></SMButton>
|
||||||
</template>
|
</template>
|
||||||
|
</SMTable>
|
||||||
|
</template>
|
||||||
|
</SMContainer>
|
||||||
</SMPage>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import EasyDataTable from "vue3-easy-data-table";
|
|
||||||
import { openDialog } from "../../components/SMDialog";
|
import { openDialog } from "../../components/SMDialog";
|
||||||
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
|
||||||
import SMButton from "../../components/SMButton.vue";
|
|
||||||
import SMHeading from "../../components/SMHeading.vue";
|
|
||||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
|
||||||
import SMMessage from "../../components/SMMessage.vue";
|
|
||||||
import SMToolbar from "../../components/SMToolbar.vue";
|
|
||||||
import SMInput from "../../components/SMInput.vue";
|
|
||||||
import { api } from "../../helpers/api";
|
import { api } from "../../helpers/api";
|
||||||
|
import { EventCollection, Event } from "../../helpers/api.types";
|
||||||
import { SMDate } from "../../helpers/datetime";
|
import { SMDate } from "../../helpers/datetime";
|
||||||
import { debounce } from "../../helpers/debounce";
|
import { bytesReadable } from "../../helpers/types";
|
||||||
import { EventCollection, EventResponse } from "../../helpers/api.types";
|
import { updateRouterParams } from "../../helpers/url";
|
||||||
import { useToastStore } from "../../store/ToastStore";
|
import { useToastStore } from "../../store/ToastStore";
|
||||||
|
import SMButton from "../../components/SMButton.vue";
|
||||||
|
import SMDialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||||
|
import SMInput from "../../components/SMInput.vue";
|
||||||
|
import SMLoading from "../../components/SMLoading.vue";
|
||||||
|
import SMMastHead from "../../components/SMMastHead.vue";
|
||||||
|
import SMNoItems from "../../components/SMNoItems.vue";
|
||||||
|
import SMPagination from "../../components/SMPagination.vue";
|
||||||
|
import SMTable from "../../components/SMTable.vue";
|
||||||
|
import SMToolbar from "../../components/SMToolbar.vue";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const search = ref("");
|
const toastStore = useToastStore();
|
||||||
|
|
||||||
|
const items = ref([]);
|
||||||
|
const itemsLoading = ref(true);
|
||||||
|
const itemSearch = ref((route.query.search as string) || "");
|
||||||
|
const itemsTotal = ref(0);
|
||||||
|
const itemsPerPage = 25;
|
||||||
|
const itemsPage = ref(parseInt((route.query.page as string) || "1"));
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
{ text: "Title", value: "title", sortable: true },
|
{ text: "Title", value: "title", sortable: true },
|
||||||
{ text: "Starts", value: "start_at_formatted", sortable: true },
|
{ text: "Starts", value: "start_at", sortable: true },
|
||||||
{ text: "Created", value: "created_at_formatted", sortable: true },
|
{ text: "Location", value: "location", sortable: true },
|
||||||
{ text: "Updated", value: "updated_at_formatted", sortable: true },
|
|
||||||
{ text: "Actions", value: "actions" },
|
{ text: "Actions", value: "actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = ref([]);
|
/**
|
||||||
const formMessage = reactive({
|
* Watch if page number changes.
|
||||||
icon: "",
|
*/
|
||||||
type: "",
|
watch(itemsPage, () => {
|
||||||
message: "",
|
handleLoad();
|
||||||
});
|
});
|
||||||
|
|
||||||
const formLoading = ref(false);
|
/**
|
||||||
const serverItemsLength = ref(0);
|
* Handle searching for item.
|
||||||
const serverOptions = ref({
|
*/
|
||||||
page: 1,
|
const handleSearch = () => {
|
||||||
rowsPerPage: 25,
|
itemsPage.value = 1;
|
||||||
sortBy: "start_at",
|
handleLoad();
|
||||||
sortType: "desc",
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const handleClick = (item, extra: string): void => {
|
/**
|
||||||
if (extra.length == 0) {
|
* Handle user selecting option in action button.
|
||||||
|
*
|
||||||
|
* @param {Event} item The event item.
|
||||||
|
* @param option
|
||||||
|
*/
|
||||||
|
const handleActionButton = (item: Event, option: string): void => {
|
||||||
|
if (option.length == 0) {
|
||||||
handleEdit(item);
|
handleEdit(item);
|
||||||
} else if (extra.toLowerCase() == "duplicate") {
|
} else if (option.toLowerCase() == "duplicate") {
|
||||||
handleDuplicate(item);
|
handleDuplicate(item);
|
||||||
} else if (extra.toLowerCase() == "delete") {
|
} else if (option.toLowerCase() == "delete") {
|
||||||
handleDelete(item);
|
handleDelete(item);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadFromServer = async () => {
|
/**
|
||||||
formMessage.icon = "";
|
* Handle loading the page and list
|
||||||
formMessage.type = "error";
|
*/
|
||||||
formMessage.message = "";
|
const handleLoad = async () => {
|
||||||
formLoading.value = true;
|
itemsLoading.value = true;
|
||||||
|
items.value = [];
|
||||||
|
itemsTotal.value = 0;
|
||||||
|
|
||||||
|
updateRouterParams(router, {
|
||||||
|
search: itemSearch.value,
|
||||||
|
page: itemsPage.value == 1 ? "" : itemsPage.value.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let params = {};
|
let params = {
|
||||||
if (serverOptions.value.sortBy) {
|
page: itemsPage.value,
|
||||||
params["sort"] = serverOptions.value.sortBy.replace(
|
limit: itemsPerPage,
|
||||||
"_formatted",
|
};
|
||||||
""
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
serverOptions.value.sortType &&
|
|
||||||
serverOptions.value.sortType === "desc"
|
|
||||||
) {
|
|
||||||
params["sort"] = "-" + params["sort"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params["page"] = serverOptions.value.page;
|
if (itemSearch.value.length > 0) {
|
||||||
params["limit"] = serverOptions.value.rowsPerPage;
|
params[
|
||||||
|
"filter"
|
||||||
if (search.value.length > 0) {
|
] = `title:${itemSearch.value},OR,name:${itemSearch.value},OR,description:${itemSearch.value}`;
|
||||||
params["title"] = search.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = await api.get({
|
let result = await api.get({
|
||||||
@@ -148,144 +159,89 @@ const loadFromServer = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = result.data as EventCollection;
|
const data = result.data as EventCollection;
|
||||||
|
data.events.forEach(async (row) => {
|
||||||
if (!data.events) {
|
|
||||||
throw new Error("The server is currently not available");
|
|
||||||
}
|
|
||||||
|
|
||||||
items.value = data.events;
|
|
||||||
|
|
||||||
items.value.forEach((row) => {
|
|
||||||
if (row.start_at !== "undefined") {
|
if (row.start_at !== "undefined") {
|
||||||
row.start_at_formatted = new SMDate(row.start_at, {
|
row.start_at = new SMDate(row.start_at, {
|
||||||
|
format: "ymd",
|
||||||
|
utc: true,
|
||||||
|
}).relative();
|
||||||
|
}
|
||||||
|
if (row.end_at !== "undefined") {
|
||||||
|
row.end_at = new SMDate(row.end_at, {
|
||||||
|
format: "ymd",
|
||||||
|
utc: true,
|
||||||
|
}).relative();
|
||||||
|
}
|
||||||
|
if (row.publish_at !== "undefined") {
|
||||||
|
row.publish_at = new SMDate(row.publish_at, {
|
||||||
format: "ymd",
|
format: "ymd",
|
||||||
utc: true,
|
utc: true,
|
||||||
}).relative();
|
}).relative();
|
||||||
}
|
}
|
||||||
if (row.created_at !== "undefined") {
|
if (row.created_at !== "undefined") {
|
||||||
row.created_at_formatted = new SMDate(row.created_at, {
|
row.created_at = new SMDate(row.created_at, {
|
||||||
format: "ymd",
|
format: "ymd",
|
||||||
utc: true,
|
utc: true,
|
||||||
}).relative();
|
}).relative();
|
||||||
}
|
}
|
||||||
if (row.updated_at !== "undefined") {
|
if (row.updated_at !== "undefined") {
|
||||||
row.updated_at_formatted = new SMDate(row.updated_at, {
|
row.updated_at = new SMDate(row.updated_at, {
|
||||||
format: "ymd",
|
format: "ymd",
|
||||||
utc: true,
|
utc: true,
|
||||||
}).relative();
|
}).relative();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items.value.push(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
serverItemsLength.value = data.total;
|
itemsTotal.value = data.total;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
// restParseErrors(formData, [formMessage, "message"], err);
|
if (error.status != 404) {
|
||||||
|
toastStore.addToast({
|
||||||
|
title: "Server Error",
|
||||||
|
content:
|
||||||
|
"An error occurred retrieving the list from the server.",
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
itemsLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
formLoading.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadFromServer();
|
/**
|
||||||
|
* Handle creating new event.
|
||||||
watch(
|
*/
|
||||||
serverOptions,
|
const handleCreate = (): void => {
|
||||||
() => {
|
|
||||||
loadFromServer();
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const debouncedFilter = debounce(loadFromServer, 1000);
|
|
||||||
watch(search, () => {
|
|
||||||
debouncedFilter();
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCreate = () => {
|
|
||||||
router.push({ name: "dashboard-event-create" });
|
router.push({ name: "dashboard-event-create" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item) => {
|
/**
|
||||||
|
* Handle duplicating an event.
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
const handleDuplicate = (item: Event): void => {
|
||||||
|
alert("not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User requests to edit the item
|
||||||
|
*
|
||||||
|
* @param {Event} item The event item.
|
||||||
|
*/
|
||||||
|
const handleEdit = (item: Event) => {
|
||||||
router.push({ name: "dashboard-event-edit", params: { id: item.id } });
|
router.push({ name: "dashboard-event-edit", params: { id: item.id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicate = async (item) => {
|
/**
|
||||||
const duplicateItem = { ...item };
|
* Request to delete an event item from the server.
|
||||||
|
*
|
||||||
try {
|
* @param {Event} item The event object to delete.
|
||||||
let tries = 1;
|
*/
|
||||||
let number = 2;
|
const handleDelete = async (item: Event) => {
|
||||||
|
|
||||||
let originalTitle = duplicateItem.title;
|
|
||||||
|
|
||||||
const titleMatch = originalTitle.match(/[- ](\d+)$/);
|
|
||||||
if (titleMatch !== null) {
|
|
||||||
number = parseInt(titleMatch[1], 10);
|
|
||||||
|
|
||||||
originalTitle = originalTitle.replace(
|
|
||||||
new RegExp(`[- ]${number}$`),
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete duplicateItem.key;
|
|
||||||
delete duplicateItem.id;
|
|
||||||
delete duplicateItem.created_at;
|
|
||||||
delete duplicateItem.updated_at;
|
|
||||||
|
|
||||||
while (tries < 25) {
|
|
||||||
const title = `${originalTitle} ${number}`;
|
|
||||||
try {
|
|
||||||
await api.get({
|
|
||||||
url: `/events/?title==${title}`,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (err.status === 404) {
|
|
||||||
duplicateItem.title = `${originalTitle} ${number}`;
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
useToastStore().addToast({
|
|
||||||
title: "Server error",
|
|
||||||
content: "The event could not be duplicated.",
|
|
||||||
type: "danger",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
++tries;
|
|
||||||
++number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await api.post({
|
|
||||||
url: "/events",
|
|
||||||
body: duplicateItem,
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = result.data as EventResponse;
|
|
||||||
|
|
||||||
loadFromServer();
|
|
||||||
|
|
||||||
useToastStore().addToast({
|
|
||||||
title: "Event duplicated",
|
|
||||||
content: "The event was duplicated successfully.",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
name: "dashboard-event-edit",
|
|
||||||
params: { id: data.event.id },
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
useToastStore().addToast({
|
|
||||||
title: "Server error",
|
|
||||||
content: "The event could not be duplicated.",
|
|
||||||
type: "danger",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (item) => {
|
|
||||||
let result = await openDialog(SMDialogConfirm, {
|
let result = await openDialog(SMDialogConfirm, {
|
||||||
title: "Delete User?",
|
title: "Delete File?",
|
||||||
text: `Are you sure you want to delete the event <strong>${item.title}</strong>?`,
|
text: `Are you sure you want to delete the event <strong>${item.title}</strong>?`,
|
||||||
cancel: {
|
cancel: {
|
||||||
type: "secondary",
|
type: "secondary",
|
||||||
@@ -293,30 +249,77 @@ const handleDelete = async (item) => {
|
|||||||
},
|
},
|
||||||
confirm: {
|
confirm: {
|
||||||
type: "danger",
|
type: "danger",
|
||||||
label: "Delete Post",
|
label: "Delete File",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
try {
|
try {
|
||||||
await api.delete({
|
await api.delete({
|
||||||
url: `/events/{id}`,
|
url: "/events/{id}",
|
||||||
params: {
|
params: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
loadFromServer();
|
|
||||||
|
|
||||||
useToastStore().addToast({
|
toastStore.addToast({
|
||||||
title: "Post deleted",
|
title: "Event Deleted",
|
||||||
content: "The post has been deleted successfully.",
|
content: `The event ${item.title} has been deleted.`,
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
} catch (err) {
|
handleLoad();
|
||||||
formMessage.message = err.response?.data?.message;
|
} catch (error) {
|
||||||
|
toastStore.addToast({
|
||||||
|
title: "Error Deleting Event",
|
||||||
|
content:
|
||||||
|
error.data?.message ||
|
||||||
|
"An unexpected server error occurred",
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Event location for humans.
|
||||||
|
*
|
||||||
|
* @param {Event} item The event object to delete.
|
||||||
|
* @returns {string} human readable location.
|
||||||
|
*/
|
||||||
|
const parseEventLocation = (item: Event) => {
|
||||||
|
if (item.location == "online") {
|
||||||
|
return "Online";
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.address;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleLoad();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss">
|
||||||
|
.page-dashboard-event-list {
|
||||||
|
.toolbar-search {
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tr {
|
||||||
|
td:first-of-type,
|
||||||
|
td:nth-of-type(2) {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:not(:first-of-type) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 768px) {
|
||||||
|
.page-dashboard-event-list {
|
||||||
|
.toolbar-search {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
back-title="Back to Media" />
|
back-title="Back to Media" />
|
||||||
<SMContainer class="flex-grow-1">
|
<SMContainer class="flex-grow-1">
|
||||||
<SMLoading v-if="pageLoading" large />
|
<SMLoading v-if="pageLoading" large />
|
||||||
<SMForm v-else :model-value="form" @submit="handleSubmit">
|
<SMForm
|
||||||
|
v-else
|
||||||
|
:model-value="form"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@failed-validation="handleFailValidation">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn class="media-container">
|
<SMColumn class="media-container">
|
||||||
<!-- <div class="media-container"> -->
|
<!-- <div class="media-container"> -->
|
||||||
@@ -226,7 +230,6 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
router.push({ name: "dashboard-media-list" });
|
router.push({ name: "dashboard-media-list" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
useToastStore().addToast({
|
useToastStore().addToast({
|
||||||
title: "Server error",
|
title: "Server error",
|
||||||
content: "An error occurred saving the media.",
|
content: "An error occurred saving the media.",
|
||||||
@@ -237,6 +240,15 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFailValidation = () => {
|
||||||
|
useToastStore().addToast({
|
||||||
|
title: "Save Error",
|
||||||
|
content:
|
||||||
|
"There are some errors in the form. Fix these before continuing.",
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
let result = await openDialog(DialogConfirm, {
|
let result = await openDialog(DialogConfirm, {
|
||||||
title: "Delete File?",
|
title: "Delete File?",
|
||||||
|
|||||||
@@ -1,62 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMMastHead
|
<SMPage permission="admin/media">
|
||||||
title="Media"
|
<SMMastHead
|
||||||
:back-link="{ name: 'dashboard' }"
|
title="Media"
|
||||||
back-title="Return to Dashboard" />
|
:back-link="{ name: 'dashboard' }"
|
||||||
<SMContainer class="flex-grow-1">
|
back-title="Return to Dashboard" />
|
||||||
<SMToolbar>
|
<SMContainer class="flex-grow-1">
|
||||||
<SMButton
|
<SMToolbar>
|
||||||
:to="{ name: 'workshops' }"
|
<SMButton
|
||||||
type="primary"
|
:to="{ name: 'workshops' }"
|
||||||
label="Upload Media" />
|
type="primary"
|
||||||
<SMInput
|
label="Upload Media" />
|
||||||
v-model="itemSearch"
|
<SMInput
|
||||||
label="Search"
|
v-model="itemSearch"
|
||||||
class="toolbar-search"
|
label="Search"
|
||||||
@keyup.enter="handleSearch">
|
class="toolbar-search"
|
||||||
<template #append>
|
@keyup.enter="handleSearch">
|
||||||
<SMButton
|
<template #append>
|
||||||
type="primary"
|
<SMButton
|
||||||
label="Search"
|
type="primary"
|
||||||
icon="search-outline"
|
label="Search"
|
||||||
@click="handleSearch" />
|
icon="search-outline"
|
||||||
</template>
|
@click="handleSearch" />
|
||||||
</SMInput>
|
</template>
|
||||||
</SMToolbar>
|
</SMInput>
|
||||||
<SMLoading large v-if="itemsLoading" />
|
</SMToolbar>
|
||||||
<template v-else>
|
<SMLoading large v-if="itemsLoading" />
|
||||||
<SMPagination
|
<template v-else>
|
||||||
v-if="items.length < itemsTotal"
|
<SMPagination
|
||||||
v-model="itemsPage"
|
v-if="items.length < itemsTotal"
|
||||||
:total="itemsTotal"
|
v-model="itemsPage"
|
||||||
:per-page="itemsPerPage" />
|
:total="itemsTotal"
|
||||||
<SMNoItems v-if="items.length == 0" text="No Media Found" />
|
:per-page="itemsPerPage" />
|
||||||
<SMTable
|
<SMNoItems v-if="items.length == 0" text="No Media Found" />
|
||||||
v-else
|
<SMTable
|
||||||
:headers="headers"
|
v-else
|
||||||
:items="items"
|
:headers="headers"
|
||||||
@row-click="handleEdit">
|
:items="items"
|
||||||
<template #item-size="item">
|
@row-click="handleEdit">
|
||||||
{{ bytesReadable(item.size) }}
|
<template #item-size="item">
|
||||||
</template>
|
{{ bytesReadable(item.size) }}
|
||||||
<template #item-title="item"
|
</template>
|
||||||
>{{ item.title }}<br /><span class="small"
|
<template #item-title="item"
|
||||||
>({{ item.name }})</span
|
>{{ item.title }}<br /><span class="small"
|
||||||
></template
|
>({{ item.name }})</span
|
||||||
>
|
></template
|
||||||
<template #item-actions="item">
|
>
|
||||||
<SMButton
|
<template #item-actions="item">
|
||||||
label="Edit"
|
<SMButton
|
||||||
:dropdown="{
|
label="Edit"
|
||||||
download: 'Download',
|
:dropdown="{
|
||||||
delete: 'Delete',
|
download: 'Download',
|
||||||
}"
|
delete: 'Delete',
|
||||||
size="medium"
|
}"
|
||||||
@click="handleActionButton(item, $event)"></SMButton>
|
size="medium"
|
||||||
</template>
|
@click="
|
||||||
</SMTable>
|
handleActionButton(item, $event)
|
||||||
</template>
|
"></SMButton>
|
||||||
</SMContainer>
|
</template>
|
||||||
|
</SMTable>
|
||||||
|
</template>
|
||||||
|
</SMContainer>
|
||||||
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
Reference in New Issue
Block a user