updates
This commit is contained in:
@@ -114,7 +114,7 @@
|
||||
}
|
||||
|
||||
.flex-row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
flex-direction: row-reverse !important;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
props.size,
|
||||
{ 'button-block': block },
|
||||
{ 'button-dropdown': dropdown },
|
||||
{ 'button-loading': loading },
|
||||
]"
|
||||
ref="buttonRef"
|
||||
:style="{ minWidth: minWidth }"
|
||||
@@ -146,8 +147,10 @@ if (props.form !== undefined) {
|
||||
watch(
|
||||
() => props.form.loading(),
|
||||
(newValue) => {
|
||||
loading.value = newValue;
|
||||
disabled.value = newValue;
|
||||
if (buttonType === "submit") {
|
||||
loading.value = newValue;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -265,7 +268,7 @@ const handleClickItem = (item: string) => {
|
||||
|
||||
&:disabled,
|
||||
&.primary:disabled {
|
||||
background-color: var(--base-color-dark);
|
||||
background-color: var(--base-color-dark) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
58
resources/js/components/SMImage.vue
Normal file
58
resources/js/components/SMImage.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="image">
|
||||
<SMLoading v-if="imgLoaded == false && imgError == false" />
|
||||
<img
|
||||
v-if="imgError == false"
|
||||
:src="src"
|
||||
@load="imgLoaded = true"
|
||||
@error="imgError = true" />
|
||||
<div v-if="imgError == true" class="image-error">
|
||||
<ion-icon name="alert-circle-outline"></ion-icon>
|
||||
<p>Error loading image</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import SMLoading from "./SMLoading.vue";
|
||||
|
||||
defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const imgLoaded = ref(false);
|
||||
const imgError = ref(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image {
|
||||
display: flex;
|
||||
flex-basis: 300px;
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
ion-icon {
|
||||
font-size: 300%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="loading-container">
|
||||
<SMLoadingIcon v-bind="{ large: props.large }" />
|
||||
<div :class="['loading-background', { overlay: props.overlay }]">
|
||||
<div :class="{ 'loading-box': props.overlay }">
|
||||
<SMLoadingIcon v-bind="{ large: props.large }" />
|
||||
<p v-if="props.text" class="loading-text">{{ props.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -13,14 +16,54 @@ const props = defineProps({
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.loading-container {
|
||||
.loading-background {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
background-color: #fff;
|
||||
padding: 48px 48px 16px 48px;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--base-shadow);
|
||||
|
||||
.loading-text {
|
||||
font-size: 150%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,17 +17,11 @@
|
||||
:data-title="header['text']"
|
||||
:key="`item-row-${index}-${header['value']}`">
|
||||
<template v-if="slots[`item-${header['value']}`]">
|
||||
<slot
|
||||
:name="`item-${header['value']}`"
|
||||
v-bind="item as any">
|
||||
<slot :name="`item-${header['value']}`" v-bind="item">
|
||||
</slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
header["value"]
|
||||
.split(".")
|
||||
.reduce((item, key) => item[key], item)
|
||||
}}
|
||||
{{ getItemValue(item, header["value"]) }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -57,6 +51,22 @@ const slots = useSlots();
|
||||
const handleRowClick = (item) => {
|
||||
emits("rowClick", item);
|
||||
};
|
||||
|
||||
const getItemValue = (data: unknown, key: string): string => {
|
||||
if (typeof data === "object" && data !== null) {
|
||||
return key.split(".").reduce((item, key) => item[key], data);
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const hasClassLong = (text: unknown): boolean => {
|
||||
if (typeof text == "string") {
|
||||
return text.length >= 35;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -81,6 +91,10 @@ const handleRowClick = (item) => {
|
||||
td {
|
||||
font-size: 85%;
|
||||
background-color: #fff;
|
||||
|
||||
&.long {
|
||||
font-size: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
@@ -127,16 +141,19 @@ const handleRowClick = (item) => {
|
||||
border: none;
|
||||
border-bottom: 1px solid #eee;
|
||||
position: relative;
|
||||
padding: 8px 12px 8px 50%;
|
||||
padding: 8px 12px 8px 40%;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 12px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 45%;
|
||||
width: 35%;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -122,11 +122,11 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
&.success .sm-toast-inner {
|
||||
&.success .toast-inner {
|
||||
border-left-color: var(--success-color);
|
||||
}
|
||||
|
||||
&.danger .sm-toast-inner {
|
||||
&.danger .toast-inner {
|
||||
border-left-color: var(--danger-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<SMFormCard>
|
||||
<h1>{{ props.title }}</h1>
|
||||
<h3>{{ props.title }}</h3>
|
||||
<p v-html="computedSanitizedText"></p>
|
||||
<SMFormFooter>
|
||||
<template #left>
|
||||
|
||||
@@ -71,6 +71,8 @@ export const api = {
|
||||
options.headers["Authorization"] = `Bearer ${userStore.token}`;
|
||||
}
|
||||
|
||||
options.method = options.method.toUpperCase() || "GET";
|
||||
|
||||
if (options.body && typeof options.body === "object") {
|
||||
if (options.body instanceof FormData) {
|
||||
if (
|
||||
@@ -82,6 +84,11 @@ export const api = {
|
||||
// remove the "Content-Type" key from the headers object
|
||||
delete options.headers["Content-Type"];
|
||||
}
|
||||
|
||||
if (options.method != "POST") {
|
||||
options.body.append("_method", options.method);
|
||||
options.method = "POST";
|
||||
}
|
||||
} else if (
|
||||
options.body instanceof Blob ||
|
||||
options.body instanceof ArrayBuffer
|
||||
@@ -94,7 +101,9 @@ export const api = {
|
||||
}
|
||||
|
||||
if (
|
||||
(options.method.toUpperCase() || "GET") == "POST" &&
|
||||
(options.method == "POST" ||
|
||||
options.method == "PUT" ||
|
||||
options.method == "PATCH") &&
|
||||
options.progress
|
||||
) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
@@ -167,7 +167,7 @@ type FormControlSetValidation = (
|
||||
type FormControlIsValid = () => boolean;
|
||||
|
||||
export interface FormControlObject {
|
||||
value: string;
|
||||
value: unknown;
|
||||
validate: () => Promise<ValidationResult>;
|
||||
validation: FormControlValidation;
|
||||
clearValidations: FormControlClearValidations;
|
||||
|
||||
@@ -3,19 +3,23 @@ import { extractFileNameFromUrl } from "./url";
|
||||
/**
|
||||
* Tests if an object or string is empty.
|
||||
*
|
||||
* @param {object|string} objOrString The object or string.
|
||||
* @param {unknown} value The object or string.
|
||||
* @returns {boolean} If the object or string is empty.
|
||||
*/
|
||||
export const isEmpty = (objOrString: unknown): boolean => {
|
||||
if (objOrString == null) {
|
||||
return true;
|
||||
} else if (typeof objOrString === "string") {
|
||||
return objOrString.length == 0;
|
||||
export const isEmpty = (value: unknown): boolean => {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length === 0;
|
||||
} else if (
|
||||
typeof objOrString == "object" &&
|
||||
Object.keys(objOrString).length === 0
|
||||
value instanceof File ||
|
||||
value instanceof Blob ||
|
||||
value instanceof Map ||
|
||||
value instanceof Set
|
||||
) {
|
||||
return true;
|
||||
return value.size === 0;
|
||||
} else if (value instanceof FormData) {
|
||||
return [...value.entries()].length === 0;
|
||||
} else if (typeof value === "object") {
|
||||
return !value || Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { bytesReadable } from "../helpers/types";
|
||||
import { SMDate } from "./datetime";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
|
||||
export interface ValidationObject {
|
||||
validate: (value: any) => Promise<ValidationResult>;
|
||||
validate: (value: unknown) => Promise<ValidationResult>;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
@@ -744,9 +745,9 @@ export function Required(
|
||||
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: string): Promise<ValidationResult> {
|
||||
validate: function (value: unknown): Promise<ValidationResult> {
|
||||
return Promise.resolve({
|
||||
valid: value.length > 0,
|
||||
valid: !isEmpty(value),
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
@@ -831,8 +832,11 @@ export function FileSize(
|
||||
return {
|
||||
...options,
|
||||
validate: function (value: File): Promise<ValidationResult> {
|
||||
const isValid =
|
||||
value instanceof File ? value.size < options.size : true;
|
||||
|
||||
return Promise.resolve({
|
||||
valid: value.size < options.size,
|
||||
valid: isValid,
|
||||
invalidMessages: [
|
||||
typeof this.invalidMessage === "string"
|
||||
? this.invalidMessage
|
||||
|
||||
@@ -40,7 +40,10 @@
|
||||
<div
|
||||
class="thumbnail"
|
||||
:style="{
|
||||
backgroundImage: `url('${event.hero.url}')`,
|
||||
backgroundImage: `url('${mediaGetVariantUrl(
|
||||
event.hero,
|
||||
'medium'
|
||||
)}')`,
|
||||
}">
|
||||
<div :class="['banner', event['bannerType']]">
|
||||
{{ event["banner"] }}
|
||||
@@ -87,6 +90,7 @@ import SMToolbar from "../components/SMToolbar.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Event, EventCollection } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import SMMastHead from "../components/SMMastHead.vue";
|
||||
import SMContainer from "../components/SMContainer.vue";
|
||||
import SMNoItems from "../components/SMNoItems.vue";
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
<SMContainer class="flex-grow-1">
|
||||
<SMLoading v-if="pageLoading" large />
|
||||
<SMForm v-else :model-value="form" @submit="handleSubmit">
|
||||
<SMRow>
|
||||
<SMColumn class="media-container">
|
||||
<!-- <div class="media-container"> -->
|
||||
<SMImage :src="imageUrl" />
|
||||
<!-- </div> -->
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="file" type="file" />
|
||||
@@ -61,12 +68,15 @@
|
||||
<SMInput type="textarea" control="description" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow class="px-2 justify-content-space-between">
|
||||
<SMRow
|
||||
class="px-2 flex-row-reverse justify-content-space-between">
|
||||
<SMButton type="submit" label="Save" :form="form" />
|
||||
<SMButton
|
||||
:form="form"
|
||||
v-if="route.params.id"
|
||||
type="danger"
|
||||
label="Delete"
|
||||
@click="handleDelete" />
|
||||
<SMButton type="submit" label="Save" />
|
||||
</SMRow>
|
||||
</SMForm>
|
||||
</SMContainer>
|
||||
@@ -74,13 +84,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { api } from "../../helpers/api";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { And, FileSize, Required } from "../../helpers/validate";
|
||||
import { Media, MediaResponse } from "../../helpers/api.types";
|
||||
import { MediaResponse } from "../../helpers/api.types";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
@@ -89,6 +99,9 @@ import SMInput from "../../components/SMInput.vue";
|
||||
import SMMastHead from "../../components/SMMastHead.vue";
|
||||
import SMLoading from "../../components/SMLoading.vue";
|
||||
import { toTitleCase } from "../../helpers/string";
|
||||
import { useToastStore } from "../../store/ToastStore";
|
||||
import SMColumn from "../../components/SMColumn.vue";
|
||||
import SMImage from "../../components/SMImage.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -116,6 +129,8 @@ const fileData = reactive({
|
||||
user: {},
|
||||
});
|
||||
|
||||
const imageUrl = ref("");
|
||||
|
||||
const handleLoad = async () => {
|
||||
if (route.params.id) {
|
||||
try {
|
||||
@@ -142,6 +157,8 @@ const handleLoad = async () => {
|
||||
: toTitleCase(data.medium.status);
|
||||
|
||||
fileData.dimensions = data.medium.dimensions;
|
||||
|
||||
imageUrl.value = fileData.url;
|
||||
} catch (err) {
|
||||
pageError.value = err.status;
|
||||
}
|
||||
@@ -152,51 +169,74 @@ const handleLoad = async () => {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
let res = null;
|
||||
// let data = {
|
||||
// title: formData.title.value,
|
||||
// slug: formData.slug.value,
|
||||
// user_id: formData.user_id.value,
|
||||
// content: formData.content.value
|
||||
// }
|
||||
form.loading(true);
|
||||
let submitData = new FormData();
|
||||
|
||||
// if(route.params.id) {
|
||||
// res = await axios.put(`posts/${route.params.id}`, data);
|
||||
// } else {
|
||||
// res = await axios.post(`posts`, data);
|
||||
// }
|
||||
|
||||
let submitFormData = new FormData();
|
||||
if (form.file.value instanceof File) {
|
||||
submitFormData.append("file", form.file.value);
|
||||
// add file if there is one
|
||||
if (form.controls.file.value instanceof File) {
|
||||
submitData.append("file", form.controls.file.value);
|
||||
}
|
||||
|
||||
submitFormData.append("permission", form.permission.value);
|
||||
submitData.append("title", form.controls.title.value as string);
|
||||
submitData.append(
|
||||
"permission",
|
||||
form.controls.permission.value as string
|
||||
);
|
||||
submitData.append(
|
||||
"description",
|
||||
form.controls.description.value as string
|
||||
);
|
||||
|
||||
await api.post({
|
||||
url: "/media",
|
||||
body: submitFormData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
progress: (progressEvent) =>
|
||||
(formLoadingMessage.value = `Uploading Files ${Math.floor(
|
||||
(progressEvent.loaded / progressEvent.total) * 100
|
||||
)}%`),
|
||||
if (route.params.id) {
|
||||
await api.put({
|
||||
url: "/media/{id}",
|
||||
params: {
|
||||
id: route.params.id,
|
||||
},
|
||||
body: submitData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await api.post({
|
||||
url: "/media",
|
||||
body: submitData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
// progress: (progressEvent) =>
|
||||
// (formLoadingMessage.value = `Uploading Files ${Math.floor(
|
||||
// (progressEvent.loaded / progressEvent.total) * 100
|
||||
// )}%`),
|
||||
});
|
||||
}
|
||||
|
||||
useToastStore().addToast({
|
||||
title: route.params.id ? "Media Updated" : "Media Created",
|
||||
content: route.params.id
|
||||
? "The media item has been updated."
|
||||
: "The media item been created.",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
form.message("Your details have been updated", "success");
|
||||
} catch (err) {
|
||||
form.apiErrors(err);
|
||||
router.push({ name: "dashboard-media-list" });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
useToastStore().addToast({
|
||||
title: "Server error",
|
||||
content: "An error occurred saving the media.",
|
||||
type: "danger",
|
||||
});
|
||||
} finally {
|
||||
form.loading(false);
|
||||
}
|
||||
|
||||
form.loading(false);
|
||||
};
|
||||
|
||||
const handleDelete = async (item: Media) => {
|
||||
const handleDelete = async () => {
|
||||
let result = await openDialog(DialogConfirm, {
|
||||
title: "Delete File?",
|
||||
text: `Are you sure you want to delete the file <strong>${item.title}</strong>?`,
|
||||
text: `Are you sure you want to delete the file <strong>${form.controls.title.value}</strong>?`,
|
||||
cancel: {
|
||||
type: "secondary",
|
||||
label: "Cancel",
|
||||
@@ -209,7 +249,7 @@ const handleDelete = async (item: Media) => {
|
||||
|
||||
if (result) {
|
||||
try {
|
||||
await api.delete(`media/${item.id}`);
|
||||
await api.delete(`media/${route.params.id}`);
|
||||
router.push({ name: "media" });
|
||||
} catch (error) {
|
||||
pageError.value = error.status;
|
||||
@@ -223,3 +263,14 @@ const computedFileSize = computed(() => {
|
||||
|
||||
handleLoad();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page-dashboard-media-edit {
|
||||
.media-container {
|
||||
max-height: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
<template #item-size="item">
|
||||
{{ bytesReadable(item.size) }}
|
||||
</template>
|
||||
<template #item-title="item"
|
||||
>{{ item.title }}<br /><span class="small"
|
||||
>({{ item.name }})</span
|
||||
></template
|
||||
>
|
||||
<template #item-actions="item">
|
||||
<SMButton
|
||||
label="Edit"
|
||||
@@ -86,7 +91,7 @@ const itemsPerPage = 25;
|
||||
const itemsPage = ref(parseInt((route.query.page as string) || "1"));
|
||||
|
||||
const headers = [
|
||||
{ text: "Name", value: "title", sortable: true },
|
||||
{ text: "Title (Name)", value: "title", sortable: true },
|
||||
{ text: "Size", value: "size", sortable: true },
|
||||
{ text: "Uploaded By", value: "user.display_name", sortable: true },
|
||||
{ text: "Actions", value: "actions" },
|
||||
@@ -257,7 +262,8 @@ handleLoad();
|
||||
<style lang="scss">
|
||||
.page-dashboard-media-list {
|
||||
.table tr {
|
||||
td:first-of-type {
|
||||
td:first-of-type,
|
||||
td:nth-of-type(2) {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
|
||||
@@ -241,7 +241,6 @@ const handleSubmit = async () => {
|
||||
|
||||
router.push({ name: "dashboard-post-list" });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
form.apiErrors(error);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user