updates
This commit is contained in:
@@ -9,14 +9,55 @@
|
||||
<label class="control-label" v-bind="{ for: id }">{{
|
||||
label
|
||||
}}</label>
|
||||
<template v-if="props.type == 'static'">
|
||||
<div class="static-input-control" v-bind="{ id: id }">
|
||||
{{ value }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="props.type == 'file'">
|
||||
<input
|
||||
:id="id"
|
||||
type="file"
|
||||
class="file-input-control"
|
||||
:accept="props.accept"
|
||||
@change="handleChange" />
|
||||
<div class="file-input-control-value">
|
||||
{{ value?.name ? value.name : value }}
|
||||
</div>
|
||||
<label
|
||||
class="button primary file-input-control-button"
|
||||
: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"
|
||||
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"
|
||||
@@ -27,6 +68,7 @@
|
||||
@blur="handleBlur"
|
||||
@input="handleInput"
|
||||
@keyup="handleKeyup" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="slots.append" class="input-control-append">
|
||||
<slot name="append"></slot>
|
||||
@@ -37,7 +79,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, watch, ref, useSlots } from "vue";
|
||||
import { isEmpty } from "../helpers/utils";
|
||||
import { isEmpty, generateRandomElementId } from "../helpers/utils";
|
||||
import { toTitleCase } from "../helpers/string";
|
||||
import SMControl from "./SMControl.vue";
|
||||
|
||||
@@ -97,6 +139,11 @@ const props = defineProps({
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
@@ -131,7 +178,7 @@ const id = ref(
|
||||
? props.id
|
||||
: typeof props.control == "string"
|
||||
? props.control
|
||||
: ""
|
||||
: generateRandomElementId()
|
||||
);
|
||||
const feedbackInvalid = ref(props.feedbackInvalid);
|
||||
const active = ref(value.value?.length ?? 0 > 0);
|
||||
@@ -141,10 +188,22 @@ const disabled = ref(props.disabled);
|
||||
watch(
|
||||
() => value.value,
|
||||
(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(
|
||||
() => props.feedbackInvalid,
|
||||
(newValue) => {
|
||||
@@ -215,7 +274,13 @@ const handleKeyup = (event: Event) => {
|
||||
const handleClear = () => {
|
||||
value.value = "";
|
||||
emits("update:modelValue", "");
|
||||
// emits("change");
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
if (control) {
|
||||
control.value = event.target.files[0];
|
||||
feedbackInvalid.value = "";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -326,6 +391,40 @@ const handleClear = () => {
|
||||
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;
|
||||
flex-direction: row;
|
||||
margin: 8px auto;
|
||||
align-items: top;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
@@ -30,12 +30,14 @@ export interface Media {
|
||||
user_id: string;
|
||||
title: string;
|
||||
name: string;
|
||||
mime: string;
|
||||
permission: Array<string>;
|
||||
mime_type: string;
|
||||
permission: string;
|
||||
size: number;
|
||||
status: string;
|
||||
storage: string;
|
||||
url: string;
|
||||
description: string;
|
||||
dimensions: string;
|
||||
variants: { [key: string]: string };
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
@@ -95,3 +95,12 @@ export const updateRouterParams = (router: Router, params: Params): void => {
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -55,7 +57,7 @@ export const getFileIconImagePath = (fileName: string): string => {
|
||||
* @returns {string} The url to the file preview icon.
|
||||
*/
|
||||
export const getFilePreview = (url: string): string => {
|
||||
const ext = getFileExtension(fileName);
|
||||
const ext = getFileExtension(extractFileNameFromUrl(url));
|
||||
if (ext.length > 0) {
|
||||
if (/(gif|jpe?g|png)/i.test(ext)) {
|
||||
return `${url}?size=thumb`;
|
||||
@@ -80,3 +82,19 @@ export const clamp = (n: number, min: number, max: number): number => {
|
||||
if (n > max) return max;
|
||||
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";
|
||||
|
||||
export interface ValidationObject {
|
||||
validate: (value: string) => Promise<ValidationResult>;
|
||||
validate: (value: any) => Promise<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
|
||||
* @returns ValidationEmailObject
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<SMPage :page-error="pageError" permission="admin/media">
|
||||
<SMRow>
|
||||
<SMFormCard>
|
||||
<h1>{{ page_title }}</h1>
|
||||
<SMForm
|
||||
:model-value="form"
|
||||
:loading_message="formLoadingMessage"
|
||||
@submit="handleSubmit">
|
||||
<SMMastHead
|
||||
:title="pageHeading"
|
||||
:back-link="{ name: 'dashboard-media-list' }"
|
||||
back-title="Back to Media" />
|
||||
<SMContainer class="flex-grow-1">
|
||||
<SMLoading v-if="pageLoading" large />
|
||||
<SMForm v-else :model-value="form" @submit="handleSubmit">
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput control="file" type="file" />
|
||||
@@ -14,11 +14,10 @@
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
contorl="url"
|
||||
type="link"
|
||||
label="URL"
|
||||
:href="formData.url.value" />
|
||||
<SMInput control="title" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput control="permission" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
@@ -30,86 +29,125 @@
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.mime.value"
|
||||
v-model="fileData.mime_type"
|
||||
type="static"
|
||||
label="File Mime" />
|
||||
label="File Mime Type" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="formData.permission.value"
|
||||
label="Permission"
|
||||
:error="formData.permission.error"
|
||||
@blur="fieldValidate(formData.permission)" />
|
||||
v-model="fileData.status"
|
||||
type="static"
|
||||
label="Status" />
|
||||
</SMColumn>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="fileData.dimensions"
|
||||
type="static"
|
||||
label="Dimensions" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMColumn>
|
||||
<SMInput
|
||||
v-model="fileData.url"
|
||||
type="static"
|
||||
label="URL" />
|
||||
</SMColumn>
|
||||
</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" />
|
||||
</SMColumn>
|
||||
<SMColumn class="justify-content-end">
|
||||
<SMButton type="submit" label="Save" />
|
||||
</SMColumn>
|
||||
</SMRow>
|
||||
</SMForm>
|
||||
</SMFormCard>
|
||||
</SMRow>
|
||||
</SMContainer>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "vue";
|
||||
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 { Form, FormControl } from "../../helpers/form";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { And, FileSize, Required } from "../../helpers/validate";
|
||||
|
||||
const router = useRouter();
|
||||
const pageError = ref(200);
|
||||
const formLoadingMessage = ref("");
|
||||
import { Media, MediaResponse } from "../../helpers/api.types";
|
||||
import { openDialog } from "../../components/SMDialog";
|
||||
import DialogConfirm from "../../components/dialogs/SMDialogConfirm.vue";
|
||||
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 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({
|
||||
file: FormControl("", And([Required(), FileSize(5242880)])),
|
||||
file: FormControl("", And([Required(), FileSize({ size: 5242880 })])),
|
||||
title: FormControl(),
|
||||
description: FormControl(),
|
||||
permission: FormControl(),
|
||||
})
|
||||
);
|
||||
|
||||
const fileData = reactive({
|
||||
url: "",
|
||||
mime: "",
|
||||
mime_type: "",
|
||||
size: 0,
|
||||
storage: "",
|
||||
status: "",
|
||||
dimensions: "",
|
||||
user: {},
|
||||
});
|
||||
|
||||
const handleLoad = async () => {
|
||||
if (route.params.id) {
|
||||
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;
|
||||
form.permission.value = res.data.media.permission;
|
||||
fileData.url = res.data.media.url;
|
||||
fileData.mime = res.data.media.mime;
|
||||
fileData.size = res.data.media.size;
|
||||
const data = result.data as MediaResponse;
|
||||
|
||||
form.controls.file.value = data.medium.name;
|
||||
form.controls.title.value = data.medium.title;
|
||||
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) {
|
||||
form.apiErrors(err);
|
||||
pageError.value = err.status;
|
||||
}
|
||||
}
|
||||
|
||||
form.loading(false);
|
||||
pageLoading.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -155,7 +193,7 @@ const handleSubmit = async () => {
|
||||
form.loading(false);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const handleDelete = async (item: Media) => {
|
||||
let result = await openDialog(DialogConfirm, {
|
||||
title: "Delete File?",
|
||||
text: `Are you sure you want to delete the file <strong>${item.title}</strong>?`,
|
||||
@@ -173,11 +211,8 @@ const handleDelete = async () => {
|
||||
try {
|
||||
await api.delete(`media/${item.id}`);
|
||||
router.push({ name: "media" });
|
||||
} catch (err) {
|
||||
alert(
|
||||
err.response?.data?.message ||
|
||||
"An unexpected server error occurred"
|
||||
);
|
||||
} catch (error) {
|
||||
pageError.value = error.status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user