bug fixes

This commit is contained in:
2023-04-23 13:56:27 +10:00
parent 95aadd45ee
commit 77aa622610
9 changed files with 418 additions and 320 deletions

View File

@@ -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());
} }
/** /**

View File

@@ -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.
*/ */

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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}`;
} }

View File

@@ -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",
});
} }
}; };

View File

@@ -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>

View File

@@ -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?",

View 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">