Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proponent File Upload #131

Merged
merged 2 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/environments/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ config:
FRONTEND_CHES_ROADMAP_BCC: NRM.PermittingAndData@gov.bc.ca
FRONTEND_CHES_SUBMISSION_CC: NRM.PermittingAndData@gov.bc.ca
FRONTEND_COMS_APIPATH: https://coms-dev.api.gov.bc.ca/api/v1
FRONTEND_COMS_BUCKETID: 1f9e1451-c130-4804-aeb0-b78b5b109c47
FRONTEND_COMS_BUCKETID: 5aa446ca-23c5-4f3b-9300-d8623bc4d101
FRONTEND_GEOCODER_APIPATH: https://geocoder.api.gov.bc.ca
FRONTEND_OIDC_AUTHORITY: https://dev.loginproxy.gov.bc.ca/auth/realms/standard
FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188
Expand Down
202 changes: 202 additions & 0 deletions frontend/src/components/file/AdvancedFileUpload.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue';

import { Button, FileUpload, ProgressBar, useToast } from '@/lib/primevue';
import DocumentCardLite from '@/components/file/DocumentCardLite.vue';
import { documentService } from '@/services';
import { useConfigStore, useSubmissionStore } from '@/store';

import type { FileUploadUploaderEvent } from 'primevue/fileupload';
import type { Ref } from 'vue';

// Props
type Props = {
activityId?: string;
accept?: string[];
reject?: string[];
disabled?: boolean;
};

const props = withDefaults(defineProps<Props>(), {
activityId: undefined,
accept: undefined,
disabled: false,
reject: undefined
});

// Store
const { getConfig } = storeToRefs(useConfigStore());
const submissionStore = useSubmissionStore();

// State
const fileInput: Ref<any> = ref(null);
const uploading: Ref<Boolean> = ref(false);

// Actions
const toast = useToast();

const onFileUploadClick = () => {
if (props.disabled) {
toast.info('Document uploading is currently disabled');
return;
}

fileInput.value.click();
};

const onFileUploadDragAndDrop = (event: FileUploadUploaderEvent) => {
if (props.disabled) {
toast.info('Document uploading is currently disabled');
return;
}

onUpload(Array.isArray(event.files) ? event.files : [event.files]);
};

const onUpload = async (files: Array<File>) => {
uploading.value = true;

await Promise.allSettled(
files.map((file: File) => {
return new Promise((resolve, reject) => {
if (props.activityId) {
documentService
.createDocument(file, props.activityId, getConfig.value.coms.bucketId)
.then((response) => {
if (response?.data) {
submissionStore.addDocument(response.data);
toast.success('Document uploaded');
}
return resolve(response);
})
.catch((e: any) => {
toast.error('Failed to upload document', e);
return reject(e);
});
} else return reject('No activityId');
});
})
);

uploading.value = false;
};

// filter documents based on accept and reject props
// if accept and reject are not provided, all documents are shown
// if accept is provided, only documents with extensions in accept are shown
// if reject is provided, only documents with extensions not in reject are shown
const filteredDocuments = computed(() => {
let documents = submissionStore.getDocuments;
return documents.filter(
(document) =>
(!props.accept && !props.reject) ||
props.accept?.some((ext) => document.filename.endsWith(ext)) ||
props.reject?.every((ext) => !document.filename.endsWith(ext)) ||
document.filename.endsWith('.pdf')
);
});
</script>

<template>
<div class="mb-3 border-dashed file-upload border-round-md w-100">
<div>
<div
v-if="uploading"
class="h-4rem align-content-center pl-2 pr-2"
>
<ProgressBar
mode="indeterminate"
class="align-self-center progress-bar"
/>
</div>
<div
v-if="!uploading"
class="hover-hand hover-shadow"
>
<FileUpload
name="fileUpload"
:multiple="true"
:custom-upload="true"
:auto="true"
:disabled="props.disabled"
@uploader="onFileUploadDragAndDrop"
>
<template #empty>
<div class="flex align-items-center justify-content-center flex-column">
<Button
aria-label="Upload"
class="justify-content-center w-full h-4rem border-none"
@click="onFileUploadClick"
>
<font-awesome-icon
class="pr-2"
icon="fa-solid fa-upload"
/>
Click or drag-and-drop
</Button>
</div>
</template>
</FileUpload>

<input
ref="fileInput"
type="file"
class="hidden"
:accept="props.accept ? props.accept.join(',') : '*'"
multiple
@change="(event: any) => onUpload(Array.from(event.target.files))"
@click="(event: any) => (event.target.value = null)"
/>
</div>
</div>
</div>

<div class="grid w-full">
<div
v-for="(document, index) in filteredDocuments"
:key="document.documentId"
:index="index"
class="col-4"
>
<DocumentCardLite
:document="document"
:delete-button="!props.disabled"
class="hover-hand hover-shadow mb-2"
@click="documentService.downloadDocument(document.documentId, document.filename)"
/>
</div>
</div>
</template>

<style scoped lang="scss">
:deep(.p-fileupload-buttonbar) {
display: none;
}

:deep(.p-fileupload-content) {
padding: 0;
border: none;
}

.file-input {
display: none;
}

.p-button.p-component {
background-color: transparent;
color: var(--text-color);
}

.progress-bar {
height: 0.3rem;
}

.file-upload {
width: 100%;
color: $app-out-of-focus;
&:hover {
color: $app-hover;
}
}
</style>
4 changes: 3 additions & 1 deletion frontend/src/components/file/DeleteDocument.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import type { Document } from '@/types';
// Props
type Props = {
document: Document;
disabled?: boolean;
};
const props = withDefaults(defineProps<Props>(), {});
const props = withDefaults(defineProps<Props>(), { disabled: false });

// Store
const submissionStore = useSubmissionStore();
Expand Down Expand Up @@ -43,6 +44,7 @@ const confirmDelete = (document: Document) => {
<template>
<Button
v-tooltip.bottom="'Delete document'"
:disabled="props.disabled"
class="p-button-lg p-button-text p-button-danger p-0 align-self-center"
aria-label="Delete object"
style="position: relative; top: 5; right: 0"
Expand Down
111 changes: 111 additions & 0 deletions frontend/src/components/file/DocumentCardLite.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script setup lang="ts">
import { filesize } from 'filesize';
import { ref } from 'vue';

import DeleteDocument from '@/components/file/DeleteDocument.vue';
import { Card } from '@/lib/primevue';
import { formatDateLong } from '@/utils/formatters';

import type { Ref } from 'vue';
import type { Document } from '@/types';

// Props
type Props = {
deleteButton?: boolean;
document: Document;
selectable?: boolean;
selected?: boolean;
};

const props = withDefaults(defineProps<Props>(), {
deleteButton: true,
selectable: false,
selected: false
});

// Emits
const emit = defineEmits(['document:clicked']);

// State
const isSelected: Ref<boolean> = ref(props.selected);

// Actions

function onClick() {
if (props.selectable) {
isSelected.value = !isSelected.value;
emit('document:clicked', { document: props.document, selected: isSelected.value });
}
}
</script>

<template>
<Card
class="pb-1 text-center border-round-xl"
:class="{ clicked: isSelected }"
@click="onClick"
>
<template #content>
<div class="grid">
<div
v-tooltip.bottom="`${props.document.filename} Uploaded by ${props.document.createdByFullName}`"
class="col-12 mb-0 text-left font-semibold text-overflow-ellipsis white-space-nowrap mt-2"
style="overflow: hidden"
>
<a href="#">{{ props.document.filename }}</a>
</div>
<h6 class="col-8 text-left mt-0 mb-0 pt-0 pb-0">
{{ formatDateLong(props.document.createdAt as string).split(',')[0] }},
</h6>
<h6 class="col-8 text-left mt-1 mb-0 pt-0 pb-0">
{{ formatDateLong(props.document.createdAt as string).split(',')[1] }}
</h6>
</div>
</template>
<template #footer>
<div class="flex justify-content-between ml-3 mr-3 align-items-center">
<h6 class="col-4 text-left mt-0 mb-0 pl-0 inline-block">
{{ filesize(props.document.filesize) }}
</h6>
<DeleteDocument
:document="props.document"
:disabled="!props.deleteButton"
/>
</div>
</template>
</Card>
</template>

<style scoped lang="scss">
.document-image {
max-height: 2.5rem;
position: relative;
top: 50%;
transform: translateY(-50%);
}

.clicked {
box-shadow: 0 0 11px #036;
}

:deep(.p-card-header) {
height: 5rem;
background-color: lightgray;
border-radius: 10px 10px 0 0;
}

:deep(.p-card-content) {
padding-top: 0;
padding-bottom: 0;
}

:deep(.p-card-footer) {
padding: 0;
text-align: right;
}

:deep(.p-card-body) {
sanjaytkbabu marked this conversation as resolved.
Show resolved Hide resolved
padding-bottom: 0.4em;
padding-top: 0.5em;
}
</style>
Loading
Loading