updates
This commit is contained in:
@@ -9,24 +9,66 @@
|
|||||||
<label class="control-label" v-bind="{ for: id }">{{
|
<label class="control-label" v-bind="{ for: id }">{{
|
||||||
label
|
label
|
||||||
}}</label>
|
}}</label>
|
||||||
<ion-icon
|
<template v-if="props.type == 'static'">
|
||||||
class="invalid-icon"
|
<div class="static-input-control" v-bind="{ id: id }">
|
||||||
name="alert-circle-outline"></ion-icon>
|
{{ value }}
|
||||||
<ion-icon
|
</div>
|
||||||
v-if="props.showClear && value?.length > 0 && !feedbackInvalid"
|
</template>
|
||||||
class="clear-icon"
|
<template v-else-if="props.type == 'file'">
|
||||||
name="close-outline"
|
<input
|
||||||
@click.stop="handleClear"></ion-icon>
|
:id="id"
|
||||||
<input
|
type="file"
|
||||||
:type="props.type"
|
class="file-input-control"
|
||||||
class="input-control"
|
:accept="props.accept"
|
||||||
:disabled="disabled"
|
@change="handleChange" />
|
||||||
v-bind="{ id: id, autofocus: props.autofocus }"
|
<div class="file-input-control-value">
|
||||||
v-model="value"
|
{{ value?.name ? value.name : value }}
|
||||||
@focus="handleFocus"
|
</div>
|
||||||
@blur="handleBlur"
|
<label
|
||||||
@input="handleInput"
|
class="button primary file-input-control-button"
|
||||||
@keyup="handleKeyup" />
|
:for="id"
|
||||||
|
>Select file</label
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="props.type == 'textarea'">
|
||||||
|
<ion-icon
|
||||||
|
class="invalid-icon"
|
||||||
|
name="alert-circle-outline"></ion-icon>
|
||||||
|
<textarea
|
||||||
|
:type="props.type"
|
||||||
|
class="input-control"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="{ id: id, autofocus: props.autofocus }"
|
||||||
|
v-model="value"
|
||||||
|
rows="5"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@input="handleInput"
|
||||||
|
@keyup="handleKeyup"></textarea>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ion-icon
|
||||||
|
class="invalid-icon"
|
||||||
|
name="alert-circle-outline"></ion-icon>
|
||||||
|
<ion-icon
|
||||||
|
v-if="
|
||||||
|
props.showClear && value?.length > 0 && !feedbackInvalid
|
||||||
|
"
|
||||||
|
class="clear-icon"
|
||||||
|
name="close-outline"
|
||||||
|
@click.stop="handleClear"></ion-icon>
|
||||||
|
|
||||||
|
<input
|
||||||
|
:type="props.type"
|
||||||
|
class="input-control"
|
||||||
|
:disabled="disabled"
|
||||||
|
v-bind="{ id: id, autofocus: props.autofocus }"
|
||||||
|
v-model="value"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
@input="handleInput"
|
||||||
|
@keyup="handleKeyup" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="slots.append" class="input-control-append">
|
<div v-if="slots.append" class="input-control-append">
|
||||||
<slot name="append"></slot>
|
<slot name="append"></slot>
|
||||||
@@ -37,7 +79,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, watch, ref, useSlots } from "vue";
|
import { inject, watch, ref, useSlots } from "vue";
|
||||||
import { isEmpty } from "../helpers/utils";
|
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||||
import { toTitleCase } from "../helpers/string";
|
import { toTitleCase } from "../helpers/string";
|
||||||
import SMControl from "./SMControl.vue";
|
import SMControl from "./SMControl.vue";
|
||||||
|
|
||||||
@@ -97,6 +139,11 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
accept: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
@@ -131,7 +178,7 @@ const id = ref(
|
|||||||
? props.id
|
? props.id
|
||||||
: typeof props.control == "string"
|
: typeof props.control == "string"
|
||||||
? props.control
|
? props.control
|
||||||
: ""
|
: generateRandomElementId()
|
||||||
);
|
);
|
||||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||||
const active = ref(value.value?.length ?? 0 > 0);
|
const active = ref(value.value?.length ?? 0 > 0);
|
||||||
@@ -141,10 +188,22 @@ const disabled = ref(props.disabled);
|
|||||||
watch(
|
watch(
|
||||||
() => value.value,
|
() => value.value,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
active.value = newValue.length > 0 || focused.value == true;
|
active.value =
|
||||||
|
newValue.length > 0 ||
|
||||||
|
newValue instanceof File ||
|
||||||
|
focused.value == true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (props.modelValue != undefined) {
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(newValue) => {
|
||||||
|
value.value = newValue;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.feedbackInvalid,
|
() => props.feedbackInvalid,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@@ -215,7 +274,13 @@ const handleKeyup = (event: Event) => {
|
|||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
value.value = "";
|
value.value = "";
|
||||||
emits("update:modelValue", "");
|
emits("update:modelValue", "");
|
||||||
// emits("change");
|
};
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
if (control) {
|
||||||
|
control.value = event.target.files[0];
|
||||||
|
feedbackInvalid.value = "";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -326,6 +391,40 @@ const handleClear = () => {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.static-input-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 22px 16px 8px 16px;
|
||||||
|
border: 1px solid var(--base-color-darker);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--base-color);
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-control {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0.1px;
|
||||||
|
height: 0.1px;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-control-value {
|
||||||
|
width: 100%;
|
||||||
|
padding: 22px 16px 8px 16px;
|
||||||
|
border: 1px solid var(--base-color-darker);
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
background-color: var(--base-color);
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-control-button {
|
||||||
|
border-width: 1px 1px 1px 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--base-color-darker);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 15px 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ defineProps({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin: 8px auto;
|
margin: 8px auto;
|
||||||
align-items: top;
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,12 +30,14 @@ export interface Media {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
name: string;
|
name: string;
|
||||||
mime: string;
|
mime_type: string;
|
||||||
permission: Array<string>;
|
permission: string;
|
||||||
size: number;
|
size: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
storage: string;
|
||||||
url: string;
|
url: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
dimensions: string;
|
||||||
variants: { [key: string]: string };
|
variants: { [key: string]: string };
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@@ -95,3 +95,12 @@ export const updateRouterParams = (router: Router, params: Params): void => {
|
|||||||
|
|
||||||
router.push({ query });
|
router.push({ query });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const extractFileNameFromUrl = (url: string): string => {
|
||||||
|
const matches = url.match(/\/([^/]+\.[^/]+)$/);
|
||||||
|
if (!matches) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const fileName = matches[1];
|
||||||
|
return fileName;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { extractFileNameFromUrl } from "./url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests if an object or string is empty.
|
* Tests if an object or string is empty.
|
||||||
*
|
*
|
||||||
@@ -55,7 +57,7 @@ export const getFileIconImagePath = (fileName: string): string => {
|
|||||||
* @returns {string} The url to the file preview icon.
|
* @returns {string} The url to the file preview icon.
|
||||||
*/
|
*/
|
||||||
export const getFilePreview = (url: string): string => {
|
export const getFilePreview = (url: string): string => {
|
||||||
const ext = getFileExtension(fileName);
|
const ext = getFileExtension(extractFileNameFromUrl(url));
|
||||||
if (ext.length > 0) {
|
if (ext.length > 0) {
|
||||||
if (/(gif|jpe?g|png)/i.test(ext)) {
|
if (/(gif|jpe?g|png)/i.test(ext)) {
|
||||||
return `${url}?size=thumb`;
|
return `${url}?size=thumb`;
|
||||||
@@ -80,3 +82,19 @@ export const clamp = (n: number, min: number, max: number): number => {
|
|||||||
if (n > max) return max;
|
if (n > max) return max;
|
||||||
return n;
|
return n;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random element ID.
|
||||||
|
*
|
||||||
|
* @param {string} prefix Any prefix to add to the ID.
|
||||||
|
* @returns {string} A random string non-existent in the document.
|
||||||
|
*/
|
||||||
|
export const generateRandomElementId = (prefix: string = ""): string => {
|
||||||
|
let randomId = "";
|
||||||
|
|
||||||
|
do {
|
||||||
|
randomId = prefix + Math.random().toString(36).substring(2, 9);
|
||||||
|
} while (document.getElementById(randomId));
|
||||||
|
|
||||||
|
return randomId;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { bytesReadable } from "../helpers/types";
|
|||||||
import { SMDate } from "./datetime";
|
import { SMDate } from "./datetime";
|
||||||
|
|
||||||
export interface ValidationObject {
|
export interface ValidationObject {
|
||||||
validate: (value: string) => Promise<ValidationResult>;
|
validate: (value: any) => Promise<ValidationResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationResult {
|
export interface ValidationResult {
|
||||||
@@ -818,7 +818,7 @@ const defaultValidationFileSizeOptions: ValidationFileSizeOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate field is in a valid Email format
|
* Validate file is equal or less than size.
|
||||||
*
|
*
|
||||||
* @param options options data
|
* @param options options data
|
||||||
* @returns ValidationEmailObject
|
* @returns ValidationEmailObject
|
||||||
|
|||||||
@@ -1,115 +1,153 @@
|
|||||||
<template>
|
<template>
|
||||||
<SMPage :page-error="pageError" permission="admin/media">
|
<SMPage :page-error="pageError" permission="admin/media">
|
||||||
<SMRow>
|
<SMMastHead
|
||||||
<SMFormCard>
|
:title="pageHeading"
|
||||||
<h1>{{ page_title }}</h1>
|
:back-link="{ name: 'dashboard-media-list' }"
|
||||||
<SMForm
|
back-title="Back to Media" />
|
||||||
:model-value="form"
|
<SMContainer class="flex-grow-1">
|
||||||
:loading_message="formLoadingMessage"
|
<SMLoading v-if="pageLoading" large />
|
||||||
@submit="handleSubmit">
|
<SMForm v-else :model-value="form" @submit="handleSubmit">
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput control="file" type="file" />
|
<SMInput control="file" type="file" />
|
||||||
</SMColumn>
|
</SMColumn>
|
||||||
</SMRow>
|
</SMRow>
|
||||||
<SMRow>
|
<SMRow>
|
||||||
<SMColumn>
|
<SMColumn>
|
||||||
<SMInput
|
<SMInput control="title" />
|
||||||
contorl="url"
|
</SMColumn>
|
||||||
type="link"
|
<SMColumn>
|
||||||
label="URL"
|
<SMInput control="permission" />
|
||||||
:href="formData.url.value" />
|
</SMColumn>
|
||||||
</SMColumn>
|
</SMRow>
|
||||||
</SMRow>
|
<SMRow>
|
||||||
<SMRow>
|
<SMColumn>
|
||||||
<SMColumn>
|
<SMInput
|
||||||
<SMInput
|
v-model="computedFileSize"
|
||||||
v-model="computedFileSize"
|
type="static"
|
||||||
type="static"
|
label="File Size" />
|
||||||
label="File Size" />
|
</SMColumn>
|
||||||
</SMColumn>
|
<SMColumn>
|
||||||
<SMColumn>
|
<SMInput
|
||||||
<SMInput
|
v-model="fileData.mime_type"
|
||||||
v-model="formData.mime.value"
|
type="static"
|
||||||
type="static"
|
label="File Mime Type" />
|
||||||
label="File Mime" />
|
</SMColumn>
|
||||||
</SMColumn>
|
</SMRow>
|
||||||
</SMRow>
|
<SMRow>
|
||||||
<SMRow>
|
<SMColumn>
|
||||||
<SMColumn>
|
<SMInput
|
||||||
<SMInput
|
v-model="fileData.status"
|
||||||
v-model="formData.permission.value"
|
type="static"
|
||||||
label="Permission"
|
label="Status" />
|
||||||
:error="formData.permission.error"
|
</SMColumn>
|
||||||
@blur="fieldValidate(formData.permission)" />
|
<SMColumn>
|
||||||
</SMColumn>
|
<SMInput
|
||||||
</SMRow>
|
v-model="fileData.dimensions"
|
||||||
<SMRow>
|
type="static"
|
||||||
<SMColumn>
|
label="Dimensions" />
|
||||||
<SMButton
|
</SMColumn>
|
||||||
type="danger"
|
</SMRow>
|
||||||
label="Delete"
|
<SMRow>
|
||||||
@click="handleDelete" />
|
<SMColumn>
|
||||||
</SMColumn>
|
<SMInput
|
||||||
<SMColumn class="justify-content-end">
|
v-model="fileData.url"
|
||||||
<SMButton type="submit" label="Save" />
|
type="static"
|
||||||
</SMColumn>
|
label="URL" />
|
||||||
</SMRow>
|
</SMColumn>
|
||||||
</SMForm>
|
</SMRow>
|
||||||
</SMFormCard>
|
<SMRow>
|
||||||
</SMRow>
|
<SMColumn>
|
||||||
|
<SMInput type="textarea" control="description" />
|
||||||
|
</SMColumn>
|
||||||
|
</SMRow>
|
||||||
|
<SMRow class="px-2 justify-content-space-between">
|
||||||
|
<SMButton
|
||||||
|
type="danger"
|
||||||
|
label="Delete"
|
||||||
|
@click="handleDelete" />
|
||||||
|
<SMButton type="submit" label="Save" />
|
||||||
|
</SMRow>
|
||||||
|
</SMForm>
|
||||||
|
</SMContainer>
|
||||||
</SMPage>
|
</SMPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref } from "vue";
|
import { computed, reactive, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import SMButton from "../../components/SMButton.vue";
|
|
||||||
import SMFormCard from "../../components/SMFormCard.vue";
|
|
||||||
import SMForm from "../../components/SMForm.vue";
|
|
||||||
import SMInput from "../../components/SMInput.vue";
|
|
||||||
|
|
||||||
import { api } from "../../helpers/api";
|
import { api } from "../../helpers/api";
|
||||||
import { Form, FormControl } from "../../helpers/form";
|
import { Form, FormControl } from "../../helpers/form";
|
||||||
import { bytesReadable } from "../../helpers/types";
|
import { bytesReadable } from "../../helpers/types";
|
||||||
import { And, FileSize, Required } from "../../helpers/validate";
|
import { And, FileSize, Required } from "../../helpers/validate";
|
||||||
|
import { Media, MediaResponse } from "../../helpers/api.types";
|
||||||
const router = useRouter();
|
import { openDialog } from "../../components/SMDialog";
|
||||||
const pageError = ref(200);
|
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||||
const formLoadingMessage = ref("");
|
import SMButton from "../../components/SMButton.vue";
|
||||||
|
import SMForm from "../../components/SMForm.vue";
|
||||||
|
import SMInput from "../../components/SMInput.vue";
|
||||||
|
import SMMastHead from "../../components/SMMastHead.vue";
|
||||||
|
import SMLoading from "../../components/SMLoading.vue";
|
||||||
|
import { toTitleCase } from "../../helpers/string";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const page_title = route.params.id ? "Edit Media" : "Upload Media";
|
const router = useRouter();
|
||||||
|
|
||||||
let form = reactive(
|
const pageError = ref(200);
|
||||||
|
const pageLoading = ref(true);
|
||||||
|
const pageHeading = route.params.id ? "Edit Media" : "Upload Media";
|
||||||
|
|
||||||
|
const form = reactive(
|
||||||
Form({
|
Form({
|
||||||
file: FormControl("", And([Required(), FileSize(5242880)])),
|
file: FormControl("", And([Required(), FileSize({ size: 5242880 })])),
|
||||||
|
title: FormControl(),
|
||||||
|
description: FormControl(),
|
||||||
permission: FormControl(),
|
permission: FormControl(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const fileData = reactive({
|
const fileData = reactive({
|
||||||
url: "",
|
url: "",
|
||||||
mime: "",
|
mime_type: "",
|
||||||
size: 0,
|
size: 0,
|
||||||
|
storage: "",
|
||||||
|
status: "",
|
||||||
|
dimensions: "",
|
||||||
|
user: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLoad = async () => {
|
const handleLoad = async () => {
|
||||||
if (route.params.id) {
|
if (route.params.id) {
|
||||||
try {
|
try {
|
||||||
let res = await api.get(`media/${route.params.id}`);
|
let result = await api.get({
|
||||||
|
url: "/media/{id}",
|
||||||
|
params: {
|
||||||
|
id: route.params.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
form.file.value = res.data.media.name;
|
const data = result.data as MediaResponse;
|
||||||
form.permission.value = res.data.media.permission;
|
|
||||||
fileData.url = res.data.media.url;
|
form.controls.file.value = data.medium.name;
|
||||||
fileData.mime = res.data.media.mime;
|
form.controls.title.value = data.medium.title;
|
||||||
fileData.size = res.data.media.size;
|
form.controls.description.value = data.medium.description;
|
||||||
|
form.controls.permission.value = data.medium.permission;
|
||||||
|
fileData.url = data.medium.url;
|
||||||
|
fileData.mime_type = data.medium.mime_type;
|
||||||
|
fileData.size = data.medium.size;
|
||||||
|
fileData.storage = data.medium.storage;
|
||||||
|
fileData.status =
|
||||||
|
data.medium.status == ""
|
||||||
|
? "OK"
|
||||||
|
: toTitleCase(data.medium.status);
|
||||||
|
|
||||||
|
fileData.dimensions = data.medium.dimensions;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
form.apiErrors(err);
|
pageError.value = err.status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form.loading(false);
|
pageLoading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@@ -155,7 +193,7 @@ const handleSubmit = async () => {
|
|||||||
form.loading(false);
|
form.loading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async (item: Media) => {
|
||||||
let result = await openDialog(DialogConfirm, {
|
let result = await openDialog(DialogConfirm, {
|
||||||
title: "Delete File?",
|
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>${item.title}</strong>?`,
|
||||||
@@ -173,11 +211,8 @@ const handleDelete = async () => {
|
|||||||
try {
|
try {
|
||||||
await api.delete(`media/${item.id}`);
|
await api.delete(`media/${item.id}`);
|
||||||
router.push({ name: "media" });
|
router.push({ name: "media" });
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
alert(
|
pageError.value = error.status;
|
||||||
err.response?.data?.message ||
|
|
||||||
"An unexpected server error occurred"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user