This commit is contained in:
2023-04-21 15:46:12 +10:00
parent 3dfe96fa89
commit 84bfd3cda2
7 changed files with 276 additions and 113 deletions

View File

@@ -9,24 +9,66 @@
<label class="control-label" v-bind="{ for: id }">{{
label
}}</label>
<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 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
"
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 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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,115 +1,153 @@
<template>
<SMPage :page-error="pageError" permission="admin/media">
<SMRow>
<SMFormCard>
<h1>{{ page_title }}</h1>
<SMForm
:model-value="form"
:loading_message="formLoadingMessage"
@submit="handleSubmit">
<SMRow>
<SMColumn>
<SMInput control="file" type="file" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput
contorl="url"
type="link"
label="URL"
:href="formData.url.value" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput
v-model="computedFileSize"
type="static"
label="File Size" />
</SMColumn>
<SMColumn>
<SMInput
v-model="formData.mime.value"
type="static"
label="File Mime" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput
v-model="formData.permission.value"
label="Permission"
:error="formData.permission.error"
@blur="fieldValidate(formData.permission)" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMButton
type="danger"
label="Delete"
@click="handleDelete" />
</SMColumn>
<SMColumn class="justify-content-end">
<SMButton type="submit" label="Save" />
</SMColumn>
</SMRow>
</SMForm>
</SMFormCard>
</SMRow>
<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" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput control="title" />
</SMColumn>
<SMColumn>
<SMInput control="permission" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput
v-model="computedFileSize"
type="static"
label="File Size" />
</SMColumn>
<SMColumn>
<SMInput
v-model="fileData.mime_type"
type="static"
label="File Mime Type" />
</SMColumn>
</SMRow>
<SMRow>
<SMColumn>
<SMInput
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" />
<SMButton type="submit" label="Save" />
</SMRow>
</SMForm>
</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;
}
}
};