web/satellite/vuetify-poc: object browser improvements

- Object preview can be opened through the object row actions menu.
- Previewed objects no longer shift vertically when transitioning.
- Previewed objects no longer display the next object's preview when
  transitioning.
- The download notification text has been changed from "Success" to
  "Download Started".
- Clicking object browser breadcrumbs no longer redirects to the
  all projects dashboard.
- Upload progress snackbar:
  - The expanded and collapsed widths are now the same.
  - Clicking an item opens the object preview for it.
  - The "Uploading" tooltip position has been moved to the left so that
    it doesn't block the cancel button.

Resolves #6379

Change-Id: Ic1f5cc7948ffa62dc0bce488b61f6d5e121c77b9
This commit is contained in:
Jeremy Wharton 2023-10-05 00:43:52 -05:00 committed by Storj Robot
parent 99ba88ae2f
commit b6b9cccb72
19 changed files with 283 additions and 310 deletions

View File

@ -118,7 +118,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, h, ref } from 'vue';
import { useRouter } from 'vue-router';
import prettyBytes from 'pretty-bytes';
@ -425,13 +425,12 @@ function openDropdown(): void {
async function download(): Promise<void> {
try {
await obStore.download(props.file);
const message = `
<p class="message-title">Downloading...</p>
<p class="message-info">
Keep this download link private.<br>If you want to share, use the Share option.
</p>
`;
notify.success('', message);
notify.success(() => [
h('p', { class: 'message-title' }, 'Downloading...'),
h('p', { class: 'message-info' }, [
'Keep this download link private.', h('br'), 'If you want to share, use the Share option.',
]),
]);
} catch (error) {
notify.error('Can not download your file', AnalyticsErrorEventSource.FILE_BROWSER_ENTRY);
}

View File

@ -112,7 +112,7 @@
</template>
<script setup lang="ts">
import { Component, computed, onBeforeMount, onMounted, ref, Teleport, watch } from 'vue';
import { Component, computed, h, onBeforeMount, onMounted, ref, Teleport, watch } from 'vue';
import { useRoute } from 'vue-router';
import prettyBytes from 'pretty-bytes';
@ -324,13 +324,12 @@ async function onDelete(): Promise<void> {
async function download(): Promise<void> {
try {
await obStore.download(file.value);
const message = `
<p class="message-title">Downloading...</p>
<p class="message-info">
Keep this download link private.<br>If you want to share, use the Share option.
</p>
`;
notify.success('', message);
notify.success(() => [
h('p', { class: 'message-title' }, 'Downloading...'),
h('p', { class: 'message-info' }, [
'Keep this download link private.', h('br'), 'If you want to share, use the Share option.',
]),
]);
} catch (error) {
notify.error('Can not download your file', AnalyticsErrorEventSource.OBJECT_DETAILS_MODAL);
}

View File

@ -114,7 +114,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { computed, h, onBeforeMount, ref, watch } from 'vue';
import prettyBytes from 'pretty-bytes';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
@ -273,9 +273,15 @@ async function fetchPreviewAndMapUrl(): Promise<void> {
* Download the current opened file.
*/
async function download(): Promise<void> {
if (!file.value) return;
try {
await obStore.download(file.value);
notify.warning('Do not share download link with other people. If you want to share this data better use "Share" option.');
notify.success(() => [
h('p', { class: 'message-title' }, 'Downloading...'),
h('p', { class: 'message-info' }, [
'Keep this download link private.', h('br'), 'If you want to share, use the Share option.',
]),
]);
} catch (error) {
notify.error('Can not download your file', AnalyticsErrorEventSource.OBJECT_DETAILS_MODAL);
}

View File

@ -105,7 +105,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue';
import { computed, h, onBeforeMount, ref } from 'vue';
import ArrowDownIcon from '../../../static/images/common/dropIcon.svg';
@ -298,10 +298,10 @@ async function sendRequest(): Promise<void> {
limit = limit * Number(Memory.TB);
}
await projectsStore.requestLimitIncrease(activeLimit.value, limit);
notify.success('', `
<span class="message-title">Your request for limits increase has been submitted.</span>
<span class="message-info">Limit increases may take up to 3 business days to be reflected in your limits.</span>
`);
notify.success(() => [
h('span', { class: 'message-title' }, 'Your request for limits increase has been submitted.\xa0'),
h('span', { class: 'message-info' }, 'Limit increases may take up to 3 business days to be reflected in your limits.'),
]);
closeModal();
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.REQUEST_PROJECT_LIMIT_MODAL);

View File

@ -2,15 +2,21 @@
// See LICENSE for copying information.
<template>
<div :style="notification.style" class="notification-wrap" :class="{ active: isClassActive }" @mouseover="onMouseOver" @mouseleave="onMouseLeave">
<div
:style="{ backgroundColor: notification.backgroundColor }"
class="notification-wrap"
:class="{ active: isClassActive }"
@mouseover="onMouseOver"
@mouseleave="onMouseLeave"
>
<div class="notification-wrap__content-area">
<div class="notification-wrap__content-area__image">
<component :is="notification.icon" />
</div>
<div class="notification-wrap__content-area__message-area">
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="notification.messageNode" v-html="notification.messageNode" />
<p v-else class="notification-wrap__content-area__message">{{ notification.message }}</p>
<p ref="messageArea" class="notification-wrap__content-area__message">
<component :is="notification.messageNode" />
</p>
<p v-if="isTimeoutMentioned && notOnSettingsPage" class="notification-wrap__content-area__account-msg">
To change this go to your
@ -41,7 +47,7 @@
import { computed, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { DelayedNotification } from '@/types/DelayedNotification';
import { DelayedNotification, NotificationType } from '@/types/DelayedNotification';
import { useNotificationsStore } from '@/store/modules/notificationsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { RouteConfig } from '@/types/router';
@ -55,10 +61,13 @@ const route = useRoute();
const props = withDefaults(defineProps<{
notification: DelayedNotification;
}>(), {
notification: () => new DelayedNotification(() => { return; }, '', ''),
notification: () => new DelayedNotification(() => {}, NotificationType.Info, ''),
});
const isClassActive = ref<boolean>(false);
const isTimeoutMentioned = ref<boolean>(false);
const isSupportLinkMentioned = ref<boolean>(false);
const messageArea = ref<HTMLParagraphElement | null>(null);
/**
* Returns the correct settings route based on if we're on all projects dashboard.
@ -89,22 +98,6 @@ const notOnSettingsPage = computed((): boolean => {
&& route.name !== RouteConfig.Settings2.name;
});
/**
* Indicates if session timeout is mentioned in message.
* Temporal solution, can be changed later.
*/
const isTimeoutMentioned = computed((): boolean => {
return props.notification.message.toLowerCase().includes('session timeout');
});
/**
* Indicates if support word is mentioned in message.
* Temporal solution, can be changed later.
*/
const isSupportLinkMentioned = computed((): boolean => {
return props.notification.message.toLowerCase().includes('support');
});
/**
* Forces notification deletion.
*/
@ -130,6 +123,10 @@ function onMouseLeave(): void {
* Uses for class change for animation.
*/
onMounted((): void => {
const msg = messageArea.value?.innerText.toLowerCase() || '';
isSupportLinkMentioned.value = msg.includes('support');
isTimeoutMentioned.value = msg.includes('session timeout');
setTimeout(() => {
isClassActive.value = true;
}, 100);

View File

@ -1,10 +1,10 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
import { reactive } from 'vue';
import { VNode, reactive } from 'vue';
import { defineStore } from 'pinia';
import { DelayedNotification, NOTIFICATION_TYPES } from '@/types/DelayedNotification';
import { DelayedNotification, NotificationMessage, NotificationType } from '@/types/DelayedNotification';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
@ -13,11 +13,6 @@ export class NotificationsState {
public analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
}
interface ErrorPayload {
message: string,
source: AnalyticsErrorEventSource | null,
}
export const useNotificationsStore = defineStore('notifications', () => {
const state = reactive<NotificationsState>(new NotificationsState());
@ -51,47 +46,46 @@ export const useNotificationsStore = defineStore('notifications', () => {
}
}
function notifySuccess(message: string, messageNode?: string): void {
function notifySuccess(message: NotificationMessage, title?: string): void {
const notification = new DelayedNotification(
() => deleteNotification(notification.id),
NOTIFICATION_TYPES.SUCCESS,
NotificationType.Success,
message,
messageNode,
title,
);
addNotification(notification);
}
function notifyInfo(message: string): void {
function notifyInfo(message: NotificationMessage): void {
const notification = new DelayedNotification(
() => deleteNotification(notification.id),
NOTIFICATION_TYPES.NOTIFICATION,
NotificationType.Info,
message,
);
addNotification(notification);
}
function notifyWarning(message: string): void {
function notifyWarning(message: NotificationMessage): void {
const notification = new DelayedNotification(
() => deleteNotification(notification.id),
NOTIFICATION_TYPES.WARNING,
NotificationType.Warning,
message,
);
addNotification(notification);
}
function notifyError(payload: ErrorPayload, messageNode?: string): void {
if (payload.source) {
state.analytics.errorEventTriggered(payload.source);
function notifyError(message: NotificationMessage, source?: AnalyticsErrorEventSource): void {
if (source) {
state.analytics.errorEventTriggered(source);
}
const notification = new DelayedNotification(
() => deleteNotification(notification.id),
NOTIFICATION_TYPES.ERROR,
payload.message,
messageNode,
NotificationType.Error,
message,
);
addNotification(notification);

View File

@ -537,7 +537,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
if (config.state.config.newUploadModalEnabled) {
if (state.uploading.some(f => f.Key === key && f.status === UploadingStatus.InProgress)) {
notifyError({ message: `${key} is already uploading`, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
notifyError(`${key} is already uploading`, AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR);
return;
}
@ -558,6 +558,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Body: body,
status: UploadingStatus.Failed,
failedMessage: FailedUploadMessage.TooBig,
type: 'file',
});
return;
@ -585,10 +586,10 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
const item = state.uploading.find(f => f.Key === key);
if (!item) {
upload.off('httpUploadProgress', progressListener);
notifyError({
message: `Error updating progress. No file found with key '${key}'`,
source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR,
});
notifyError(
`Error updating progress. No file found with key '${key}'`,
AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR,
);
return;
}
@ -607,6 +608,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Size: 0,
LastModified: new Date(),
status: UploadingStatus.InProgress,
type: 'file',
});
state.uploadChain = state.uploadChain.then(async () => {
@ -664,9 +666,9 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
const limitExceededError = 'storage limit exceeded';
if (error.message.includes(limitExceededError)) {
notifyError({ message: `Error: ${limitExceededError}`, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
notifyError(`Error: ${limitExceededError}`, AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR);
} else {
notifyError({ message: error.message, source: AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR });
notifyError(error.message, AnalyticsErrorEventSource.OBJECT_UPLOAD_ERROR);
}
}

View File

@ -1,6 +1,8 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { VNode, createTextVNode } from 'vue';
import { getId } from '@/utils/idGenerator';
import SuccessIcon from '@/../static/images/notifications/success.svg';
@ -8,18 +10,36 @@ import NotificationIcon from '@/../static/images/notifications/notification.svg'
import ErrorIcon from '@/../static/images/notifications/error.svg';
import WarningIcon from '@/../static/images/notifications/warning.svg';
export const NOTIFICATION_TYPES = {
SUCCESS: 'SUCCESS',
NOTIFICATION: 'NOTIFICATION',
ERROR: 'ERROR',
WARNING: 'WARNING',
export enum NotificationType {
Success = 'Success',
Info = 'Info',
Error = 'Error',
Warning = 'Warning',
}
type RenderFunction = () => (string | VNode | (string | VNode)[]);
export type NotificationMessage = string | RenderFunction;
const StyleInfo: Record<NotificationType, { icon: string; backgroundColor: string }> = {
[NotificationType.Success]: {
backgroundColor: '#DBF1D3',
icon: SuccessIcon,
},
[NotificationType.Error]: {
backgroundColor: '#FFD4D2',
icon: ErrorIcon,
},
[NotificationType.Warning]: {
backgroundColor: '#FCF8E3',
icon: WarningIcon,
},
[NotificationType.Info]: {
backgroundColor: '#D0E3FE',
icon: NotificationIcon,
},
};
export class DelayedNotification {
private readonly successColor: string = '#DBF1D3';
private readonly errorColor: string = '#FFD4D2';
private readonly infoColor: string = '#D0E3FE';
private readonly warningColor: string = '#FCF8E3';
public readonly id: string;
private readonly callback: () => void;
@ -27,43 +47,23 @@ export class DelayedNotification {
private startTime: number;
private remainingTime: number;
public readonly type: string;
public readonly message: string;
public readonly messageNode: string | undefined;
public readonly style: { backgroundColor: string };
public readonly type: NotificationType;
public readonly title: string | undefined;
public readonly messageNode: RenderFunction;
public readonly backgroundColor: string;
public readonly icon: string;
constructor(callback: () => void, type: string, message: string, messageNode?: string) {
constructor(callback: () => void, type: NotificationType, message: NotificationMessage, title?: string) {
this.callback = callback;
this.type = type;
this.message = message;
this.messageNode = messageNode;
this.title = title;
this.messageNode = typeof message === 'string' ? () => createTextVNode(message) : message;
this.id = getId();
this.remainingTime = 3000;
this.start();
// Switch for choosing notification style depends on notification type
switch (this.type) {
case NOTIFICATION_TYPES.SUCCESS:
this.style = { backgroundColor: this.successColor };
this.icon = SuccessIcon;
break;
case NOTIFICATION_TYPES.ERROR:
this.style = { backgroundColor: this.errorColor };
this.icon = ErrorIcon;
break;
case NOTIFICATION_TYPES.WARNING:
this.style = { backgroundColor: this.warningColor };
this.icon = WarningIcon;
break;
default:
this.style = { backgroundColor: this.infoColor };
this.icon = NotificationIcon;
break;
}
this.backgroundColor = StyleInfo[type].backgroundColor;
this.icon = StyleInfo[type].icon;
}
public pause(): void {

View File

@ -1,9 +1,12 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { VNode, h } from 'vue';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotificationsStore } from '@/store/modules/notificationsStore';
import { APIError } from '@/utils/error';
import { NotificationMessage } from '@/types/DelayedNotification';
/**
* Exposes UI notifications functionality.
@ -11,41 +14,36 @@ import { APIError } from '@/utils/error';
export class Notificator {
public constructor() {}
public success(message: string, messageNode?: string): void {
public success(message: NotificationMessage, title?: string): void {
const notificationsStore = useNotificationsStore();
notificationsStore.notifySuccess(message, messageNode);
notificationsStore.notifySuccess(message, title);
}
public notifyError(error: Error, source: AnalyticsErrorEventSource | null): void {
public notifyError(error: Error, source?: AnalyticsErrorEventSource): void {
const notificationsStore = useNotificationsStore();
if (error instanceof APIError) {
let template = `
<p class="message-title">${error.message}</p>
`;
if (error.requestID) {
template = `
${template}
<p class="message-footer text-caption">Request ID: ${error.requestID}</p>
`;
}
notificationsStore.notifyError({ message: '', source }, template);
return;
let msg: NotificationMessage = error.message;
if (error instanceof APIError && error.requestID) {
msg = () => [
h('p', { class: 'message-title' }, error.message),
h('p', { class: 'message-footer' }, `Request ID: ${error.requestID}`),
];
}
notificationsStore.notifyError({ message: error.message, source });
notificationsStore.notifyError(msg, source);
}
public error(message: string, source: AnalyticsErrorEventSource | null): void {
public error(message: NotificationMessage, source?: AnalyticsErrorEventSource): void {
const notificationsStore = useNotificationsStore();
notificationsStore.notifyError({ message, source });
notificationsStore.notifyError(message, source);
}
public notify(message: string): void {
public notify(message: NotificationMessage): void {
const notificationsStore = useNotificationsStore();
notificationsStore.notifyInfo(message);
}
public warning(message: string): void {
public warning(message: NotificationMessage): void {
const notificationsStore = useNotificationsStore();
notificationsStore.notifyWarning(message);
}

View File

@ -19,11 +19,6 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
const projectsStore = useProjectsStore();
const bucketsStore = useBucketsStore();
/**
* Returns ID of selected project from store.
*/
const projectId = computed<string>(() => projectsStore.state.selectedProject.id);
/**
* Returns the name of the selected bucket.
*/
@ -43,7 +38,7 @@ type BreadcrumbItem = {
* Returns breadcrumb items corresponding to parts in the file browser path.
*/
const items = computed<BreadcrumbItem[]>(() => {
const bucketsURL = `/projects/${projectId.value}/buckets`;
const bucketsURL = `/projects/${projectsStore.state.selectedProject.urlId}/buckets`;
const pathParts = [bucketName.value];
if (filePath.value) pathParts.push(...filePath.value.split('/'));

View File

@ -30,7 +30,7 @@
<v-menu activator="parent">
<v-list class="pa-2">
<template v-if="file.type !== 'folder'">
<v-list-item density="comfortable" link rounded="lg">
<v-list-item density="comfortable" link rounded="lg" @click="emit('previewClick')">
<template #prepend>
<icon-preview />
</template>
@ -59,7 +59,7 @@
</v-list-item>
</template>
<v-list-item density="comfortable" link rounded="lg" @click="() => emit('shareClick')">
<v-list-item density="comfortable" link rounded="lg" @click="emit('shareClick')">
<template #prepend>
<icon-share bold />
</template>
@ -85,7 +85,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ref, h } from 'vue';
import {
VMenu,
VList,
@ -119,14 +119,13 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
previewClick: [];
deleteFileClick: [];
shareClick: [];
}>();
const isDownloading = ref<boolean>(false);
const filePath = computed<string>(() => bucketsStore.state.fileComponentPath);
async function onDownloadClick(): Promise<void> {
if (isDownloading.value) {
return;
@ -135,7 +134,10 @@ async function onDownloadClick(): Promise<void> {
isDownloading.value = true;
try {
await obStore.download(props.file);
notify.success('', `<p>Keep this download link private.<br>If you want to share, use the Share option.</p>`);
notify.success(
() => ['Keep this download link private.', h('br'), 'If you want to share, use the Share option.'],
'Download Started',
);
} catch (error) {
error.message = `Error downloading file. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.FILE_BROWSER_ENTRY);

View File

@ -9,11 +9,12 @@
elevation="24"
rounded="lg"
class="upload-snackbar"
:max-width="xs ? '400px' : ''"
width="100%"
max-width="400px"
>
<v-row>
<v-col>
<v-expansion-panels theme="dark">
<v-expansion-panels theme="dark" @update:model-value="v => isExpanded = v != undefined">
<v-expansion-panel
color="default"
rounded="lg"
@ -49,14 +50,16 @@
</v-row>
</v-expansion-panel-text>
<v-divider />
<v-expansion-panel-text class="uploading-content">
<UploadItem
v-for="item in uploading"
:key="item.Key"
:item="item"
/>
<v-expansion-panel-text />
</v-expansion-panel-text>
<v-expand-transition>
<div v-show="isExpanded" class="uploading-content">
<UploadItem
v-for="item in uploading"
:key="item.Key"
:item="item"
@click="item.status === UploadingStatus.Finished && emit('fileClick', item)"
/>
</div>
</v-expand-transition>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
@ -78,10 +81,10 @@ import {
VTooltip,
VIcon,
VDivider,
VExpandTransition,
} from 'vuetify/components';
import { useDisplay } from 'vuetify';
import { UploadingBrowserObject, UploadingStatus, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { BrowserObject, UploadingBrowserObject, UploadingStatus, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { Duration } from '@/utils/time';
import { useNotify } from '@/utils/hooks';
@ -93,8 +96,11 @@ const remainingTimeString = ref<string>('');
const interval = ref<NodeJS.Timer>();
const notify = useNotify();
const startDate = ref<number>(Date.now());
const isExpanded = ref<boolean>(false);
const { xs } = useDisplay();
const emit = defineEmits<{
'fileClick': [file: BrowserObject],
}>();
/**
* Returns header's status label.

View File

@ -81,8 +81,9 @@
<template #item.actions="{ item }: ItemSlotProps">
<browser-row-actions
:file="item.raw.browserObject"
@delete-file-click="() => onDeleteFileClick(item.raw.browserObject)"
@share-click="() => onShareClick(item.raw.browserObject)"
@preview-click="onFileClick(item.raw.browserObject)"
@delete-file-click="onDeleteFileClick(item.raw.browserObject)"
@share-click="onShareClick(item.raw.browserObject)"
/>
</template>
</v-data-table-row>
@ -104,6 +105,7 @@
:file="fileToShare || undefined"
@content-removed="fileToShare = null"
/>
<browser-snackbar-component v-model="isObjectsUploadModal" @file-click="onFileClick" />
</template>
<script setup lang="ts">
@ -127,11 +129,13 @@ import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { tableSizeOptions } from '@/types/common';
import { LocalData } from '@/utils/localData';
import { useAppStore } from '@/store/modules/appStore';
import BrowserRowActions from '@poc/components/BrowserRowActions.vue';
import FilePreviewDialog from '@poc/components/dialogs/FilePreviewDialog.vue';
import DeleteFileDialog from '@poc/components/dialogs/DeleteFileDialog.vue';
import ShareDialog from '@poc/components/dialogs/ShareDialog.vue';
import BrowserSnackbarComponent from '@poc/components/BrowserSnackbarComponent.vue';
import folderIcon from '@poc/assets/icon-folder-tonal.svg';
import pdfIcon from '@poc/assets/icon-pdf-tonal.svg';
@ -180,6 +184,7 @@ const config = useConfigStore();
const obStore = useObjectBrowserStore();
const projectsStore = useProjectsStore();
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const notify = useNotify();
const router = useRouter();
@ -246,6 +251,11 @@ const totalObjectCount = computed<number>(() => obStore.state.totalObjectCount);
*/
const cursor = computed<ObjectBrowserCursor>(() => obStore.state.cursor);
/**
* Indicates whether objects upload modal should be shown.
*/
const isObjectsUploadModal = computed<boolean>(() => appStore.state.isUploadingModal);
/**
* Returns every file under the current path.
*/
@ -403,7 +413,7 @@ function onFileClick(file: BrowserObject): void {
return;
}
obStore.setObjectPathForModal(obStore.state.path + file.Key);
obStore.setObjectPathForModal(file.path + file.Key);
previewDialog.value = true;
isFileGuideShown.value = false;
LocalData.setFileGuideHidden();

View File

@ -2,12 +2,9 @@
// See LICENSE for copying information.
<template>
<v-row class="pt-2" justify="space-between">
<v-col cols="9">
<p class="text-truncate">{{ item.Key }}</p>
</v-col>
<v-col cols="auto">
<v-tooltip :text="uploadStatus">
<v-list-item :title="item.Key" class="px-6" height="54" :link="props.item.status === UploadingStatus.Finished">
<template #append>
<v-tooltip :text="uploadStatus" location="left">
<template #activator="{ props: activatorProps }">
<v-progress-circular
v-if="props.item.status === UploadingStatus.InProgress"
@ -36,13 +33,13 @@
/>
</template>
</v-tooltip>
</v-col>
</v-row>
</template>
</v-list-item>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { VCol, VIcon, VProgressCircular, VRow, VTooltip } from 'vuetify/components';
import { VListItem, VIcon, VProgressCircular, VTooltip } from 'vuetify/components';
import {
UploadingBrowserObject,

View File

@ -4,92 +4,94 @@
<template>
<v-dialog v-model="model" transition="fade-transition" class="preview-dialog" fullscreen theme="dark" persistent no-click-animation>
<v-card class="preview-card">
<v-carousel v-model="constCarouselIndex" hide-delimiters show-arrows="hover" height="100vh">
<template #prev>
<v-btn
v-if="files.length > 1"
color="default"
class="rounded-circle"
icon
@click="onPrevious"
>
<v-icon icon="mdi-chevron-left" size="x-large" />
<v-toolbar
color="rgba(0, 0, 0, 0.3)"
theme="dark"
>
<v-toolbar-title>
{{ fileName }}
</v-toolbar-title>
<template #append>
<v-btn :loading="isDownloading" icon size="small" color="white" @click="download">
<img src="@poc/assets/icon-download.svg" width="22" alt="Download">
<v-tooltip
activator="parent"
location="bottom"
>
Download
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white" @click="isShareDialogShown = true">
<icon-share size="22" />
<v-tooltip
activator="parent"
location="bottom"
>
Share
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white" @click="isGeographicDistributionDialogShown = true">
<icon-distribution size="22" />
<v-tooltip
activator="parent"
location="bottom"
>
Geographic Distribution
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white">
<img src="@poc/assets/icon-more.svg" width="22" alt="More">
<v-tooltip
activator="parent"
location="bottom"
>
More
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white" @click="model = false">
<img src="@poc/assets/icon-close.svg" width="18" alt="Close">
<v-tooltip
activator="parent"
location="bottom"
>
Close
</v-tooltip>
</v-btn>
</template>
<template #next>
<v-btn
v-if="files.length > 1"
color="default"
class="rounded-circle"
icon
@click="onNext"
>
<v-icon icon="mdi-chevron-right" size="x-large" />
</v-btn>
</template>
<v-toolbar
color="rgba(0, 0, 0, 0.3)"
theme="dark"
>
<v-toolbar-title>
{{ fileName }}
</v-toolbar-title>
<template #append>
<v-btn :loading="isDownloading" icon size="small" color="white" @click="download">
<img src="@poc/assets/icon-download.svg" width="22" alt="Download">
<v-tooltip
activator="parent"
location="bottom"
>
Download
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white" @click="isShareDialogShown = true">
<icon-share size="22" />
<v-tooltip
activator="parent"
location="bottom"
>
Share
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white" @click="isGeographicDistributionDialogShown = true">
<icon-distribution size="22" />
<v-tooltip
activator="parent"
location="bottom"
>
Geographic Distribution
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white">
<img src="@poc/assets/icon-more.svg" width="22" alt="More">
<v-tooltip
activator="parent"
location="bottom"
>
More
</v-tooltip>
</v-btn>
<v-btn icon size="small" color="white" @click="model = false">
<img src="@poc/assets/icon-close.svg" width="18" alt="Close">
<v-tooltip
activator="parent"
location="bottom"
>
Close
</v-tooltip>
</v-toolbar>
<div class="flex-grow-1">
<v-carousel v-model="constCarouselIndex" hide-delimiters show-arrows="hover" class="h-100">
<template #prev>
<v-btn
v-if="files.length > 1"
color="default"
class="rounded-circle"
icon
@click="onPrevious"
>
<v-icon icon="mdi-chevron-left" size="x-large" />
</v-btn>
</template>
<template #next>
<v-btn
v-if="files.length > 1"
color="default"
class="rounded-circle"
icon
@click="onNext"
>
<v-icon icon="mdi-chevron-right" size="x-large" />
</v-btn>
</template>
</v-toolbar>
<v-carousel-item v-for="(file, i) in files" :key="file.Key">
<!-- v-carousel will mount all items at the same time -->
<!-- so :active will tell file-preview-item if it is the current. -->
<!-- If it is, it'll load the preview. -->
<file-preview-item :active="i === fileIndex" :file="file" @download="download" />
</v-carousel-item>
</v-carousel>
<v-carousel-item v-for="(file, i) in files" :key="file.Key">
<!-- v-carousel will mount all items at the same time -->
<!-- so :active will tell file-preview-item if it is the current. -->
<!-- If it is, it'll load the preview. -->
<file-preview-item :active="i === fileIndex" :file="file" @download="download" />
</v-carousel-item>
</v-carousel>
</div>
</v-card>
</v-dialog>
@ -98,7 +100,7 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { computed, h, ref, watch } from 'vue';
import {
VBtn,
VCard,
@ -205,7 +207,10 @@ async function download(): Promise<void> {
isDownloading.value = true;
try {
await obStore.download(currentFile.value);
notify.success('', `<p>Keep this download link private.<br>If you want to share, use the Share option.</p>`);
notify.success(
() => ['Keep this download link private.', h('br'), 'If you want to share, use the Share option.'],
'Download Started',
);
} catch (error) {
error.message = `Error downloading file. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.OBJECT_DETAILS_MODAL);

View File

@ -101,25 +101,18 @@ const bucket = computed((): string => {
return bucketsStore.state.fileComponentBucketName;
});
/**
* Retrieve the current filepath.
*/
const filePath = computed((): string => {
return obStore.state.objectPathForModal;
});
/**
* Retrieve the encoded filepath.
*/
const encodedFilePath = computed((): string => {
return encodeURIComponent(`${bucket.value}/${filePath.value.trim()}`);
return encodeURIComponent(`${bucket.value}/${props.file.path}${props.file.Key}`);
});
/**
* Get the extension of the current file.
*/
const extension = computed((): string | undefined => {
return filePath.value.split('.').pop();
return props.file.Key.split('.').pop();
});
/**
@ -175,7 +168,7 @@ async function fetchPreviewAndMapUrl(): Promise<void> {
let url = '';
try {
url = await generateObjectPreviewAndMapURL(bucketsStore.state.fileComponentBucketName, filePath.value);
url = await generateObjectPreviewAndMapURL(bucketsStore.state.fileComponentBucketName, props.file.path + props.file.Key);
} catch (error) {
error.message = `Unable to get file preview and map URL. ${error.message}`;
notify.notifyError(error, AnalyticsErrorEventSource.GALLERY_VIEW);

View File

@ -14,9 +14,8 @@
:key="item.id"
closable
variant="elevated"
:title="title(item.type)"
:text="item.messageNode ? '' : item.message"
:type="getType(item.type)"
:title="item.title || item.type"
:type="item.type.toLowerCase() as 'error' | 'success' | 'warning' | 'info'"
rounded="lg"
class="my-2"
border
@ -25,8 +24,7 @@
@click:close="() => onCloseClick(item.id)"
>
<template #default>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="item.messageNode" v-html="item.messageNode" />
<component :is="item.messageNode" />
</template>
</v-alert>
</v-snackbar>
@ -37,7 +35,7 @@ import { computed } from 'vue';
import { VAlert, VSnackbar } from 'vuetify/components';
import { useNotificationsStore } from '@/store/modules/notificationsStore';
import { DelayedNotification, NOTIFICATION_TYPES } from '@/types/DelayedNotification';
import { DelayedNotification } from '@/types/DelayedNotification';
const notificationsStore = useNotificationsStore();
@ -55,34 +53,6 @@ const notifications = computed((): DelayedNotification[] => {
return notificationsStore.state.notificationQueue as DelayedNotification[];
});
/**
* Returns notification title based on type.
* @param itemType
*/
function title(itemType: string): string {
const type = getType(itemType);
const [firstLetter, ...rest] = type;
return `${firstLetter.toUpperCase()}${rest.join('')}`;
}
/**
* Returns notification type.
* @param itemType
*/
function getType(itemType: string): string {
switch (itemType) {
case NOTIFICATION_TYPES.SUCCESS:
return 'success';
case NOTIFICATION_TYPES.ERROR:
return 'error';
case NOTIFICATION_TYPES.WARNING:
return 'warning';
default:
return 'info';
}
}
/**
* Forces notification to stay on page on mouse over it.
*/

View File

@ -287,6 +287,7 @@ import { useAppStore } from '@poc/store/appStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { RouteName } from '@poc/router';
import IconProject from '@poc/components/icons/IconProject.vue';
import IconSettings from '@poc/components/icons/IconSettings.vue';
@ -377,13 +378,21 @@ function compareProjects(a: Project, b: Project): number {
*/
async function onProjectSelected(project: Project): Promise<void> {
analyticsStore.eventTriggered(AnalyticsEvent.NAVIGATE_PROJECTS);
await router.push({
name: route.name || undefined,
params: {
...route.params,
id: project.urlId,
},
});
if (route.name === RouteName.Bucket) {
await router.push({
name: RouteName.Buckets,
params: { id: project.urlId },
});
} else {
await router.push({
name: route.name || undefined,
params: {
...route.params,
id: project.urlId,
},
});
}
bucketsStore.clearS3Data();
}

View File

@ -20,7 +20,6 @@
:disabled="!isInitialized"
v-bind="props"
>
<browser-snackbar-component v-model="isObjectsUploadModal" />
<IconUpload class="mr-2" />
Upload
</v-btn>
@ -111,7 +110,6 @@ import { useAppStore } from '@/store/modules/appStore';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import BrowserBreadcrumbsComponent from '@poc/components/BrowserBreadcrumbsComponent.vue';
import BrowserSnackbarComponent from '@poc/components/BrowserSnackbarComponent.vue';
import BrowserTableComponent from '@poc/components/BrowserTableComponent.vue';
import BrowserNewFolderDialog from '@poc/components/dialogs/BrowserNewFolderDialog.vue';
import IconUpload from '@poc/components/icons/IconUpload.vue';
@ -159,13 +157,6 @@ const projectId = computed<string>(() => projectsStore.state.selectedProject.id)
*/
const isPromptForPassphrase = computed<boolean>(() => bucketsStore.state.promptForPassphrase);
/**
* Indicates whether objects upload modal should be shown.
*/
const isObjectsUploadModal = computed((): boolean => {
return appStore.state.isUploadingModal;
});
/**
* Open the operating system's file system for file upload.
*/