fix: Check uploaded file size in widget and dashboard (#1975)
* File size check logic in widget * set maxium file size * update locale texts * file size check in dashboard * dynamincally set file size limit error message * code climate review fixes * add alert mixin * move attahcment bus event to constants * Move file size check logic to helper * add specs for file size limit check helper * changes as per review
This commit is contained in:
parent
45e43b0b89
commit
ce4ce3c86c
12 changed files with 143 additions and 56 deletions
|
@ -11,9 +11,7 @@
|
||||||
class="image-thumb"
|
class="image-thumb"
|
||||||
:src="attachment.thumb"
|
:src="attachment.thumb"
|
||||||
/>
|
/>
|
||||||
<span v-else class="attachment-thumb">
|
<span v-else class="attachment-thumb"> 📄 </span>
|
||||||
📄
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name-wrap">
|
<div class="file-name-wrap">
|
||||||
<span class="item">
|
<span class="item">
|
||||||
|
@ -37,8 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { formatBytes } from 'dashboard/helper/files';
|
import { formatBytes } from 'shared/helpers/FileHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
attachments: {
|
attachments: {
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||||
import CannedResponse from './CannedResponse';
|
import CannedResponse from './CannedResponse';
|
||||||
|
@ -81,6 +82,9 @@ import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel
|
||||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
||||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||||
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
|
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isEscape,
|
isEscape,
|
||||||
isEnter,
|
isEnter,
|
||||||
|
@ -100,7 +104,7 @@ export default {
|
||||||
ReplyBottomPanel,
|
ReplyBottomPanel,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, inboxMixin, uiSettingsMixin],
|
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
inReplyTo: {
|
inReplyTo: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
|
@ -351,21 +355,28 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFileUpload(file) {
|
onFileUpload(file) {
|
||||||
this.attachedFiles = [];
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reader = new FileReader();
|
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||||
reader.readAsDataURL(file.file);
|
this.attachedFiles = [];
|
||||||
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.readAsDataURL(file.file);
|
||||||
this.attachedFiles.push({
|
reader.onloadend = () => {
|
||||||
currentChatId: this.currentChat.id,
|
this.attachedFiles.push({
|
||||||
resource: file,
|
currentChatId: this.currentChat.id,
|
||||||
isPrivate: this.isPrivate,
|
resource: file,
|
||||||
thumb: reader.result,
|
isPrivate: this.isPrivate,
|
||||||
});
|
thumb: reader.result,
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.showAlert(
|
||||||
|
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
||||||
|
MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeAttachment(itemIndex) {
|
removeAttachment(itemIndex) {
|
||||||
this.attachedFiles = this.attachedFiles.filter(
|
this.attachedFiles = this.attachedFiles.filter(
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
export const formatBytes = (bytes, decimals = 2) => {
|
|
||||||
if (bytes === 0) return '0 Bytes';
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
|
||||||
};
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { formatBytes } from '../files';
|
|
||||||
|
|
||||||
describe('#File Helpers', () => {
|
|
||||||
describe('formatBytes', () => {
|
|
||||||
it('should return zero bytes if 0 is passed', () => {
|
|
||||||
expect(formatBytes(0)).toBe('0 Bytes');
|
|
||||||
});
|
|
||||||
it('should return in bytes if 1000 is passed', () => {
|
|
||||||
expect(formatBytes(1000)).toBe('1000 Bytes');
|
|
||||||
});
|
|
||||||
it('should return in KB if 100000 is passed', () => {
|
|
||||||
expect(formatBytes(10000)).toBe('9.77 KB');
|
|
||||||
});
|
|
||||||
it('should return in MB if 10000000 is passed', () => {
|
|
||||||
expect(formatBytes(10000000)).toBe('9.54 MB');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -58,6 +58,7 @@
|
||||||
"CHANGE_STATUS": "Conversation status changed",
|
"CHANGE_STATUS": "Conversation status changed",
|
||||||
"CHANGE_AGENT": "Conversation Assignee changed",
|
"CHANGE_AGENT": "Conversation Assignee changed",
|
||||||
"CHANGE_TEAM": "Conversation team changed",
|
"CHANGE_TEAM": "Conversation team changed",
|
||||||
|
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
||||||
"SENT_BY": "Sent by:",
|
"SENT_BY": "Sent by:",
|
||||||
"ASSIGNMENT": {
|
"ASSIGNMENT": {
|
||||||
"SELECT_AGENT": "Select Agent",
|
"SELECT_AGENT": "Select Agent",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export const BUS_EVENTS = {
|
export const BUS_EVENTS = {
|
||||||
SET_REFERRER_HOST: 'SET_REFERRER_HOST',
|
SET_REFERRER_HOST: 'SET_REFERRER_HOST',
|
||||||
SET_TWEET_REPLY: 'SET_TWEET_REPLY',
|
SET_TWEET_REPLY: 'SET_TWEET_REPLY',
|
||||||
|
ATTACHMENT_SIZE_CHECK_ERROR: 'ATTACHMENT_SIZE_CHECK_ERROR',
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,3 +10,5 @@ export const MESSAGE_TYPE = {
|
||||||
ACTIVITY: 2,
|
ACTIVITY: 2,
|
||||||
TEMPLATE: 3,
|
TEMPLATE: 3,
|
||||||
};
|
};
|
||||||
|
// Size in mega bytes
|
||||||
|
export const MAXIMUM_FILE_UPLOAD_SIZE = 40;
|
||||||
|
|
25
app/javascript/shared/helpers/FileHelper.js
Normal file
25
app/javascript/shared/helpers/FileHelper.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
export const formatBytes = (bytes, decimals = 2) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fileSizeInMegaBytes = bytes => {
|
||||||
|
if (bytes === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const sizeInMB = (bytes / (1024 * 1024)).toFixed(2);
|
||||||
|
return sizeInMB;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkFileSizeLimit = (file, maximumUploadLimit) => {
|
||||||
|
const fileSize = file?.file?.size;
|
||||||
|
const fileSizeInMB = fileSizeInMegaBytes(fileSize);
|
||||||
|
return fileSizeInMB <= maximumUploadLimit;
|
||||||
|
};
|
38
app/javascript/shared/helpers/specs/FileHelper.spec.js
Normal file
38
app/javascript/shared/helpers/specs/FileHelper.spec.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import {
|
||||||
|
formatBytes,
|
||||||
|
fileSizeInMegaBytes,
|
||||||
|
checkFileSizeLimit,
|
||||||
|
} from '../FileHelper';
|
||||||
|
|
||||||
|
describe('#File Helpers', () => {
|
||||||
|
describe('formatBytes', () => {
|
||||||
|
it('should return zero bytes if 0 is passed', () => {
|
||||||
|
expect(formatBytes(0)).toBe('0 Bytes');
|
||||||
|
});
|
||||||
|
it('should return in bytes if 1000 is passed', () => {
|
||||||
|
expect(formatBytes(1000)).toBe('1000 Bytes');
|
||||||
|
});
|
||||||
|
it('should return in KB if 100000 is passed', () => {
|
||||||
|
expect(formatBytes(10000)).toBe('9.77 KB');
|
||||||
|
});
|
||||||
|
it('should return in MB if 10000000 is passed', () => {
|
||||||
|
expect(formatBytes(10000000)).toBe('9.54 MB');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('fileSizeInMegaBytes', () => {
|
||||||
|
it('should return zero if 0 is passed', () => {
|
||||||
|
expect(fileSizeInMegaBytes(0)).toBe(0);
|
||||||
|
});
|
||||||
|
it('should return 19.07 if 20000000 is passed', () => {
|
||||||
|
expect(fileSizeInMegaBytes(20000000)).toBe('19.07');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('checkFileSizeLimit', () => {
|
||||||
|
it('should return false if file with size 62208194 and file size limit 40 are passed', () => {
|
||||||
|
expect(checkFileSizeLimit({ file: { size: 62208194 } }, 40)).toBe(false);
|
||||||
|
});
|
||||||
|
it('should return true if file with size 62208194 and file size limit 40 are passed', () => {
|
||||||
|
expect(checkFileSizeLimit({ file: { size: 199154 } }, 40)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,7 +4,7 @@
|
||||||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
|
||||||
@input-file="onFileUpload"
|
@input-file="onFileUpload"
|
||||||
>
|
>
|
||||||
<span class="attachment-button ">
|
<span class="attachment-button">
|
||||||
<i v-if="!isUploading.image" class="ion-android-attach" />
|
<i v-if="!isUploading.image" class="ion-android-attach" />
|
||||||
<spinner v-if="isUploading" size="small" />
|
<spinner v-if="isUploading" size="small" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -14,6 +14,9 @@
|
||||||
<script>
|
<script>
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
|
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { FileUpload, Spinner },
|
components: { FileUpload, Spinner },
|
||||||
|
@ -31,14 +34,21 @@ export default {
|
||||||
return fileType.includes('image') ? 'image' : 'file';
|
return fileType.includes('image') ? 'image' : 'file';
|
||||||
},
|
},
|
||||||
async onFileUpload(file) {
|
async onFileUpload(file) {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.isUploading = true;
|
this.isUploading = true;
|
||||||
try {
|
try {
|
||||||
const thumbUrl = window.URL.createObjectURL(file.file);
|
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||||
await this.onAttach({
|
const thumbUrl = window.URL.createObjectURL(file.file);
|
||||||
fileType: this.getFileType(file.type),
|
await this.onAttach({
|
||||||
file: file.file,
|
fileType: this.getFileType(file.type),
|
||||||
thumbUrl,
|
file: file.file,
|
||||||
});
|
thumbUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
window.bus.$emit(BUS_EVENTS.ATTACHMENT_SIZE_CHECK_ERROR);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error
|
// Error
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,5 +48,6 @@
|
||||||
"ERROR": "Message too short"
|
"ERROR": "Message too short"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit"
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,15 @@
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="showAttachmentError" class="banner">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
$t('FILE_SIZE_LIMIT', {
|
||||||
|
MAXIMUM_FILE_UPLOAD_SIZE: fileUploadSizeLimit,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="flex flex-1 overflow-auto">
|
<div class="flex flex-1 overflow-auto">
|
||||||
<conversation-wrap
|
<conversation-wrap
|
||||||
v-if="currentView === 'messageView'"
|
v-if="currentView === 'messageView'"
|
||||||
|
@ -80,6 +89,8 @@ import configMixin from '../mixins/configMixin';
|
||||||
import TeamAvailability from 'widget/components/TeamAvailability';
|
import TeamAvailability from 'widget/components/TeamAvailability';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import PreChatForm from '../components/PreChat/Form';
|
import PreChatForm from '../components/PreChat/Form';
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
|
@ -105,7 +116,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return { isOnCollapsedView: false };
|
return { isOnCollapsedView: false, showAttachmentError: false };
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
@ -132,6 +143,9 @@ export default {
|
||||||
isOpen() {
|
isOpen() {
|
||||||
return this.conversationAttributes.status === 'open';
|
return this.conversationAttributes.status === 'open';
|
||||||
},
|
},
|
||||||
|
fileUploadSizeLimit() {
|
||||||
|
return MAXIMUM_FILE_UPLOAD_SIZE;
|
||||||
|
},
|
||||||
showInputTextArea() {
|
showInputTextArea() {
|
||||||
if (this.hideInputForBotConversations) {
|
if (this.hideInputForBotConversations) {
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
|
@ -154,6 +168,14 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
bus.$on(BUS_EVENTS.ATTACHMENT_SIZE_CHECK_ERROR, () => {
|
||||||
|
this.showAttachmentError = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showAttachmentError = false;
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startConversation() {
|
startConversation() {
|
||||||
this.isOnCollapsedView = !this.isOnCollapsedView;
|
this.isOnCollapsedView = !this.isOnCollapsedView;
|
||||||
|
@ -221,5 +243,13 @@ export default {
|
||||||
.input-wrap {
|
.input-wrap {
|
||||||
padding: 0 $space-normal;
|
padding: 0 $space-normal;
|
||||||
}
|
}
|
||||||
|
.banner {
|
||||||
|
background: $color-error;
|
||||||
|
color: $color-white;
|
||||||
|
font-size: $font-size-default;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
padding: $space-slab;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue