web/satellite/vuetify-poc: add browser card view

This change adds an optional card view to the file browser similar to
the all projects card view.

Issue: #6427

Change-Id: I115dea7fdc2e7d0e093a00eb88e46d453c516cd9
This commit is contained in:
Wilfred Asomani 2023-10-26 20:05:30 +00:00
parent e482e1296e
commit febd2091df
12 changed files with 528 additions and 65 deletions

View File

@ -35,6 +35,7 @@ class AppState {
public isLargeUploadWarningNotificationShown = false;
public activeChangeLimit: LimitToChange = LimitToChange.Storage;
public isProjectTableViewEnabled = LocalData.getProjectTableViewEnabled();
public isBrowserCardViewEnabled = LocalData.getBrowserCardViewEnabled();
public shareModalType: ShareType = ShareType.File;
}
@ -99,6 +100,15 @@ export const useAppStore = defineStore('app', () => {
LocalData.setProjectTableViewEnabled(state.isProjectTableViewEnabled);
}
function toggleBrowserTableViewEnabled(isBrowserCardViewEnabled: boolean | null = null): void {
if (isBrowserCardViewEnabled === null) {
state.isBrowserCardViewEnabled = !state.isBrowserCardViewEnabled;
} else {
state.isBrowserCardViewEnabled = isBrowserCardViewEnabled;
}
LocalData.setBrowserCardViewEnabled(state.isBrowserCardViewEnabled);
}
function changeState(newFetchState: FetchState): void {
state.fetchState = newFetchState;
}
@ -195,6 +205,7 @@ export const useAppStore = defineStore('app', () => {
toggleActiveDropdown,
toggleSuccessfulPasswordReset,
toggleProjectTableViewEnabled,
toggleBrowserCardViewEnabled: toggleBrowserTableViewEnabled,
hasProjectTableViewConfigured,
updateActiveModal,
removeActiveModal,

View File

@ -3,6 +3,8 @@
import { Component } from 'vue';
import { BrowserObject } from '@/store/modules/objectBrowserStore';
import RedditIcon from '@poc/components/icons/share/IconReddit.vue';
import FacebookIcon from '@poc/components/icons/share/IconFacebook.vue';
import TwitterIcon from '@poc/components/icons/share/IconTwitter.vue';
@ -12,6 +14,16 @@ import TelegramIcon from '@poc/components/icons/share/IconTelegram.vue';
import WhatsAppIcon from '@poc/components/icons/share/IconWhatsApp.vue';
import EmailIcon from '@poc/components/icons/share/IconEmail.vue';
import imageIcon from '@poc/assets/icon-image-tonal.svg';
import videoIcon from '@poc/assets/icon-video-tonal.svg';
import audioIcon from '@poc/assets/icon-audio-tonal.svg';
import textIcon from '@poc/assets/icon-text-tonal.svg';
import pdfIcon from '@poc/assets/icon-pdf-tonal.svg';
import zipIcon from '@poc/assets/icon-zip-tonal.svg';
import spreadsheetIcon from '@poc/assets/icon-spreadsheet-tonal.svg';
import folderIcon from '@poc/assets/icon-folder-tonal.svg';
import fileIcon from '@poc/assets/icon-file-tonal.svg';
export enum ShareOptions {
Reddit = 'Reddit',
Facebook = 'Facebook',
@ -96,3 +108,30 @@ export const EXTENSION_PREVIEW_TYPES = new Map<string[], PreviewType>([
[['m4a', 'mp3', 'wav', 'ogg'], PreviewType.Audio],
[['pdf'], PreviewType.PDF],
]);
export type BrowserObjectTypeInfo = {
title: string;
icon: string;
};
/**
* Contains extra information to aid in the display, filtering, and sorting of browser objects.
*/
export type BrowserObjectWrapper = {
browserObject: BrowserObject;
typeInfo: BrowserObjectTypeInfo;
lowerName: string;
ext: string;
};
export const EXTENSION_INFOS: Map<string[], BrowserObjectTypeInfo> = new Map([
[['jpg', 'jpeg', 'png', 'gif', 'svg'], { title: 'Image', icon: imageIcon }],
[['mp4', 'mkv', 'mov'], { title: 'Video', icon: videoIcon }],
[['mp3', 'aac', 'wav', 'm4a'], { title: 'Audio', icon: audioIcon }],
[['txt', 'docx', 'doc', 'pages'], { title: 'Text', icon: textIcon }],
[['pdf'], { title: 'PDF', icon: pdfIcon }],
[['zip'], { title: 'ZIP', icon: zipIcon }],
[['xls', 'numbers', 'csv', 'xlsx', 'tsv'], { title: 'Spreadsheet', icon: spreadsheetIcon }],
]);
export const FOLDER_INFO: BrowserObjectTypeInfo = { title: 'Folder', icon: folderIcon };
export const FILE_INFO: BrowserObjectTypeInfo = { title: 'File', icon: fileIcon };

View File

@ -15,6 +15,7 @@ export class LocalData {
private static sessionExpirationDate = 'sessionExpirationDate';
private static projectLimitBannerHidden = 'projectLimitBannerHidden';
private static projectTableViewEnabled = 'projectTableViewEnabled';
private static browserCardViewEnabled = 'browserCardViewEnabled';
public static getSelectedProjectId(): string | null {
return localStorage.getItem(LocalData.selectedProjectId);
@ -125,6 +126,21 @@ export class LocalData {
localStorage.setItem(LocalData.projectTableViewEnabled, enabled.toString());
}
/*
* Whether the file browser should use the card view.
*/
public static getBrowserCardViewEnabled(): boolean {
const value = localStorage.getItem(LocalData.browserCardViewEnabled);
return value === 'true';
}
/*
* Set whether the file browser should use the card view.
*/
public static setBrowserCardViewEnabled(enabled: boolean): void {
localStorage.setItem(LocalData.browserCardViewEnabled, enabled.toString());
}
/*
* Whether a user defined setting has been made for the projects table
* */

View File

@ -0,0 +1,203 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-card v-if="!allFiles.length" title="No objects uploaded" cols="12" sm="6" md="4" lg="3" variant="flat" :border="true" rounded="xlg">
<v-card-text>
<v-divider class="mt-1 mb-4" />
<v-btn color="primary" size="small" class="mr-2" @click="emit('uploadClick')">Upload</v-btn>
<v-btn size="small" class="mr-2" @click="emit('newFolderClick')">New Folder</v-btn>
</v-card-text>
</v-card>
<v-row v-else>
<v-col v-for="item in allFiles" :key="item.browserObject.Key" cols="12" sm="6" md="4" lg="3">
<file-card
:item="item"
class="h-100"
@preview-click="onFileClick(item.browserObject)"
@delete-file-click="onDeleteFileClick(item.browserObject)"
@share-click="onShareClick(item.browserObject)"
/>
</v-col>
</v-row>
<file-preview-dialog v-model="previewDialog" />
<delete-file-dialog
v-if="fileToDelete"
v-model="isDeleteFileDialogShown"
:file="fileToDelete"
@content-removed="fileToDelete = null"
/>
<share-dialog
v-model="isShareDialogShown"
:bucket-name="bucketName"
:file="fileToShare || undefined"
@content-removed="fileToShare = null"
/>
<browser-snackbar-component v-model="isObjectsUploadModal" @file-click="onFileClick" />
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { VBtn, VCard, VCardText, VCol, VDivider, VRow } from 'vuetify/components';
import {
BrowserObject,
useObjectBrowserStore,
} from '@/store/modules/objectBrowserStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { LocalData } from '@/utils/localData';
import { useAppStore } from '@/store/modules/appStore';
import { BrowserObjectTypeInfo, BrowserObjectWrapper, EXTENSION_INFOS, FILE_INFO, FOLDER_INFO } from '@/types/browser';
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 FileCard from '@poc/components/FileCard.vue';
const props = defineProps<{
forceEmpty?: boolean;
}>();
const emit = defineEmits<{
uploadClick: [];
newFolderClick: [];
}>();
const config = useConfigStore();
const obStore = useObjectBrowserStore();
const projectsStore = useProjectsStore();
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
const notify = useNotify();
const router = useRouter();
const isFetching = ref<boolean>(false);
const search = ref<string>('');
const selected = ref([]);
const previewDialog = ref<boolean>(false);
const fileToDelete = ref<BrowserObject | null>(null);
const isDeleteFileDialogShown = ref<boolean>(false);
const fileToShare = ref<BrowserObject | null>(null);
const isShareDialogShown = ref<boolean>(false);
const isFileGuideShown = ref<boolean>(false);
/**
* Returns the name of the selected bucket.
*/
const bucketName = computed<string>(() => bucketsStore.state.fileComponentBucketName);
/**
* Returns the current path within the selected bucket.
*/
const filePath = computed<string>(() => bucketsStore.state.fileComponentPath);
/**
* Indicates whether objects upload modal should be shown.
*/
const isObjectsUploadModal = computed<boolean>(() => appStore.state.isUploadingModal);
/**
* Returns every file under the current path.
*/
const allFiles = computed<BrowserObjectWrapper[]>(() => {
if (props.forceEmpty) return [];
const objects = obStore.state.files;
return objects.map<BrowserObjectWrapper>(file => {
const lowerName = file.Key.toLowerCase();
const dotIdx = lowerName.lastIndexOf('.');
const ext = dotIdx === -1 ? '' : file.Key.slice(dotIdx + 1);
return {
browserObject: file,
typeInfo: getFileTypeInfo(ext, file.type),
lowerName,
ext,
};
});
});
/**
* Returns the title and icon representing a file's type.
*/
function getFileTypeInfo(ext: string, type: BrowserObject['type']): BrowserObjectTypeInfo {
if (!type) return FILE_INFO;
if (type === 'folder') return FOLDER_INFO;
ext = ext.toLowerCase();
for (const [exts, info] of EXTENSION_INFOS.entries()) {
if (exts.indexOf(ext) !== -1) return info;
}
return FILE_INFO;
}
/**
* Handles file click.
*/
function onFileClick(file: BrowserObject): void {
if (!file.type) return;
if (file.type === 'folder') {
const uriParts = [file.Key];
if (filePath.value) {
uriParts.unshift(...filePath.value.split('/'));
}
const pathAndKey = uriParts.map(part => encodeURIComponent(part)).join('/');
router.push(`/projects/${projectsStore.state.selectedProject.urlId}/buckets/${bucketName.value}/${pathAndKey}`);
return;
}
obStore.setObjectPathForModal(file.path + file.Key);
previewDialog.value = true;
isFileGuideShown.value = false;
LocalData.setFileGuideHidden();
}
/**
* Fetches all files in the current directory.
*/
async function fetchFiles(): Promise<void> {
if (isFetching.value || props.forceEmpty) return;
isFetching.value = true;
try {
const path = filePath.value ? filePath.value + '/' : '';
await obStore.list(path);
selected.value = [];
} catch (err) {
err.message = `Error fetching files. ${err.message}`;
notify.notifyError(err, AnalyticsErrorEventSource.FILE_BROWSER_LIST_CALL);
}
isFetching.value = false;
}
/**
* Handles delete button click event for files.
*/
function onDeleteFileClick(file: BrowserObject): void {
fileToDelete.value = file;
isDeleteFileDialogShown.value = true;
}
/**
* Handles Share button click event.
*/
function onShareClick(file: BrowserObject): void {
fileToShare.value = file;
isShareDialogShown.value = true;
}
watch(filePath, fetchFiles, { immediate: true });
watch(() => props.forceEmpty, v => !v && fetchFiles());
</script>

View File

@ -131,6 +131,7 @@ import { useConfigStore } from '@/store/modules/configStore';
import { tableSizeOptions } from '@/types/common';
import { LocalData } from '@/utils/localData';
import { useAppStore } from '@/store/modules/appStore';
import { BrowserObjectTypeInfo, BrowserObjectWrapper, EXTENSION_INFOS, FILE_INFO, FOLDER_INFO } from '@/types/browser';
import BrowserRowActions from '@poc/components/BrowserRowActions.vue';
import FilePreviewDialog from '@poc/components/dialogs/FilePreviewDialog.vue';
@ -138,16 +139,6 @@ 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';
import imageIcon from '@poc/assets/icon-image-tonal.svg';
import videoIcon from '@poc/assets/icon-video-tonal.svg';
import audioIcon from '@poc/assets/icon-audio-tonal.svg';
import textIcon from '@poc/assets/icon-text-tonal.svg';
import zipIcon from '@poc/assets/icon-zip-tonal.svg';
import spreadsheetIcon from '@poc/assets/icon-spreadsheet-tonal.svg';
import fileIcon from '@poc/assets/icon-file-tonal.svg';
type SortKey = 'name' | 'type' | 'size' | 'date';
type TableOptions = {
@ -159,21 +150,6 @@ type TableOptions = {
}[];
};
type BrowserObjectTypeInfo = {
title: string;
icon: string;
};
/**
* Contains extra information to aid in the display, filtering, and sorting of browser objects.
*/
type BrowserObjectWrapper = {
browserObject: BrowserObject;
typeInfo: BrowserObjectTypeInfo;
lowerName: string;
ext: string;
};
type ItemSlotProps = { item: { raw: BrowserObjectWrapper } };
const props = defineProps<{
@ -216,18 +192,6 @@ const headers = [
];
const collator = new Intl.Collator('en', { sensitivity: 'case' });
const extensionInfos: Map<string[], BrowserObjectTypeInfo> = new Map([
[['jpg', 'jpeg', 'png', 'gif', 'svg'], { title: 'Image', icon: imageIcon }],
[['mp4', 'mkv', 'mov'], { title: 'Video', icon: videoIcon }],
[['mp3', 'aac', 'wav', 'm4a'], { title: 'Audio', icon: audioIcon }],
[['txt', 'docx', 'doc', 'pages'], { title: 'Text', icon: textIcon }],
[['pdf'], { title: 'PDF', icon: pdfIcon }],
[['zip'], { title: 'ZIP', icon: zipIcon }],
[['xls', 'numbers', 'csv', 'xlsx', 'tsv'], { title: 'Spreadsheet', icon: spreadsheetIcon }],
]);
const folderInfo: BrowserObjectTypeInfo = { title: 'Folder', icon: folderIcon };
const fileInfo: BrowserObjectTypeInfo = { title: 'File', icon: fileIcon };
/**
* Returns the name of the selected bucket.
*/
@ -267,7 +231,7 @@ const allFiles = computed<BrowserObjectWrapper[]>(() => {
const objects = isPaginationEnabled.value ? obStore.displayedObjects : obStore.state.files;
return objects.map<BrowserObjectWrapper>(file => {
const lowerName = file.Key.toLowerCase();
const dotIdx = lowerName.indexOf('.');
const dotIdx = lowerName.lastIndexOf('.');
const ext = dotIdx === -1 ? '' : file.Key.slice(dotIdx + 1);
return {
browserObject: file,
@ -390,14 +354,14 @@ function getFormattedSize(file: BrowserObject): string {
* Returns the title and icon representing a file's type.
*/
function getFileTypeInfo(ext: string, type: BrowserObject['type']): BrowserObjectTypeInfo {
if (!type) return fileInfo;
if (type === 'folder') return folderInfo;
if (!type) return FILE_INFO;
if (type === 'folder') return FOLDER_INFO;
ext = ext.toLowerCase();
for (const [exts, info] of extensionInfos.entries()) {
for (const [exts, info] of EXTENSION_INFOS.entries()) {
if (exts.indexOf(ext) !== -1) return info;
}
return fileInfo;
return FILE_INFO;
}
/**

View File

@ -0,0 +1,126 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<v-card variant="flat" :border="true" rounded="xlg">
<div class="h-100 d-flex flex-column justify-space-between">
<v-container v-if="isLoading" class="fill-height flex-column justify-center align-center mt-n16">
<v-progress-circular indeterminate />
</v-container>
<div
v-else
class="d-flex flex-column justify-center align-center file-icon-container"
>
<img
:src="item.typeInfo.icon"
:alt="item.typeInfo.title + 'icon'"
:aria-roledescription="item.typeInfo.title + 'icon'"
height="100"
>
</div>
<v-card-item>
<v-card-title>
<a class="link" @click="emit('previewClick', item.browserObject)">
{{ item.browserObject.Key }}
</a>
</v-card-title>
<v-card-subtitle>
{{ getFormattedSize(item.browserObject) }}
</v-card-subtitle>
<v-card-subtitle>
{{ getFormattedDate(item.browserObject) }}
</v-card-subtitle>
</v-card-item>
<v-card-text class="flex-grow-0">
<v-divider class="mt-1 mb-4" />
<browser-row-actions
:file="item.browserObject"
@preview-click="emit('previewClick', item.browserObject)"
@delete-file-click="emit('deleteFileClick', item.browserObject)"
@share-click="emit('shareClick', item.browserObject)"
/>
</v-card-text>
</div>
</v-card>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
import {
VCard,
VCardItem,
VCardSubtitle,
VCardText,
VCardTitle,
VContainer,
VDivider,
VProgressCircular,
} from 'vuetify/components';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useNotify } from '@/utils/hooks';
import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { Size } from '@/utils/bytesSize';
import { useLoading } from '@/composables/useLoading';
import BrowserRowActions from '@poc/components/BrowserRowActions.vue';
type BrowserObjectTypeInfo = {
title: string;
icon: string;
};
type BrowserObjectWrapper = {
browserObject: BrowserObject;
typeInfo: BrowserObjectTypeInfo;
lowerName: string;
ext: string;
};
const analyticsStore = useAnalyticsStore();
const projectsStore = useProjectsStore();
const bucketsStore = useBucketsStore();
const obStore = useObjectBrowserStore();
const router = useRouter();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const props = defineProps<{
item: BrowserObjectWrapper,
}>();
const emit = defineEmits<{
previewClick: [BrowserObject];
deleteFileClick: [BrowserObject];
shareClick: [BrowserObject];
}>();
/**
* Returns the string form of the file's size.
*/
function getFormattedSize(file: BrowserObject): string {
if (file.type === 'folder') return '---';
const size = new Size(file.Size);
return `${size.formattedBytes} ${size.label}`;
}
/**
* Returns the string form of the file's last modified date.
*/
function getFormattedDate(file: BrowserObject): string {
if (file.type === 'folder') return '---';
const date = file.LastModified;
return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
</script>
<style scoped lang="scss">
.file-icon-container {
height: 200px;
}
</style>

View File

@ -45,6 +45,7 @@
density="compact"
:items="pageSizes"
variant="outlined"
hide-details
@update:model-value="sizeChanged"
/>
</v-col>

View File

@ -3,8 +3,7 @@
<template>
<v-dialog
v-model="dialog"
activator="parent"
v-model="model"
width="auto"
min-width="400px"
transition="fade-transition"
@ -26,7 +25,7 @@
size="small"
color="default"
:disabled="isLoading"
@click="dialog = false"
@click="model = false"
/>
</template>
</v-card-item>
@ -62,7 +61,7 @@
color="default"
block
:disabled="isLoading"
@click="dialog = false"
@click="model = false"
>
Cancel
</v-btn>
@ -114,7 +113,19 @@ const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const dialog = ref<boolean>(false);
const props = defineProps<{
modelValue: boolean,
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
}>();
const model = computed<boolean>({
get: () => props.modelValue,
set: value => emit('update:modelValue', value),
});
const formValid = ref<boolean>(false);
const folder = ref<string>('');
const innerContent = ref<Component | null>(null);
@ -141,7 +152,7 @@ function createFolder(): void {
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.CREATE_FOLDER_MODAL);
}
dialog.value = false;
model.value = false;
});
}

View File

@ -0,0 +1,19 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.99902" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="6.99902" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
</svg>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
size: number | string;
}>(), {
size: 24,
});
</script>

View File

@ -0,0 +1,21 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 8C9 8.55228 8.55228 9 8 9V9C7.44772 9 7 8.55228 7 8V8C7 7.44772 7.44772 7 8 7V7C8.55228 7 9 7.44772 9 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C9 12.5523 8.55228 13 8 13V13C7.44772 13 7 12.5523 7 12V12C7 11.4477 7.44772 11 8 11V11C8.55228 11 9 11.4477 9 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 16C9 16.5523 8.55228 17 8 17V17C7.44772 17 7 16.5523 7 16V16C7 15.4477 7.44772 15 8 15V15C8.55228 15 9 15.4477 9 16V16Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 8C18 8.55228 17.5523 9 17 9H11C10.4477 9 10 8.55228 10 8V8C10 7.44772 10.4477 7 11 7H17C17.5523 7 18 7.44772 18 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 12C18 12.5523 17.5523 13 17 13H11C10.4477 13 10 12.5523 10 12V12C10 11.4477 10.4477 11 11 11H17C17.5523 11 18 11.4477 18 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 16C18 16.5523 17.5523 17 17 17H11C10.4477 17 10 16.5523 10 16V16C10 15.4477 10.4477 15 11 15H17C17.5523 15 18 15.4477 18 16V16Z" fill="currentColor" />
</svg>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
size: number | string;
}>(), {
size: 24,
});
</script>

View File

@ -11,7 +11,7 @@
<browser-breadcrumbs-component />
<v-col>
<v-row class="mt-2 mb-4">
<v-row align="center" class="mt-2 mb-4">
<v-menu v-model="menu" location="bottom" transition="scale-transition" offset="5">
<template #activator="{ props }">
<v-btn
@ -70,17 +70,56 @@
color="default"
class="mx-4"
:disabled="!isInitialized"
@click="isNewFolderDialogOpen = true"
>
<icon-folder />
New Folder
<browser-new-folder-dialog />
</v-btn>
<template v-if="isCardViewEnabled">
<v-spacer v-if="smAndUp" />
<v-col class="pa-0" :class="{ 'pt-2': !smAndUp }" cols="auto">
<v-btn-toggle
mandatory
border
inset
density="comfortable"
class="pa-1"
>
<v-btn
size="small"
rounded="xl"
active-class="active"
:active="isCardView"
aria-label="Toggle Cards View"
@click="isCardView = true"
>
<icon-card-view />
Cards
</v-btn>
<v-btn
size="small"
rounded="xl"
active-class="active"
:active="!isCardView"
aria-label="Toggle Table View"
@click="isCardView = false"
>
<icon-table-view />
Table
</v-btn>
</v-btn-toggle>
</v-col>
</template>
</v-row>
</v-col>
<browser-table-component :loading="isFetching" :force-empty="!isInitialized" />
<browser-card-view-component v-if="isCardView" :force-empty="!isInitialized" @new-folder-click="isNewFolderDialogOpen = true" @upload-click="menu = true" />
<browser-table-component v-else :loading="isFetching" :force-empty="!isInitialized" />
</v-container>
<browser-new-folder-dialog v-model="isNewFolderDialogOpen" />
<enter-bucket-passphrase-dialog v-model="isBucketPassphraseDialogOpen" @passphrase-entered="initObjectStore" />
</template>
@ -96,8 +135,11 @@ import {
VList,
VListItem,
VListItemTitle,
VSpacer,
VDivider,
VBtnToggle,
} from 'vuetify/components';
import { useDisplay } from 'vuetify';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
@ -107,6 +149,7 @@ import { useNotify } from '@/utils/hooks';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useAppStore } from '@/store/modules/appStore';
import { useConfigStore } from '@/store/modules/configStore';
import PageTitleComponent from '@poc/components/PageTitleComponent.vue';
import BrowserBreadcrumbsComponent from '@poc/components/BrowserBreadcrumbsComponent.vue';
@ -117,16 +160,21 @@ import IconFolder from '@poc/components/icons/IconFolder.vue';
import IconFile from '@poc/components/icons/IconFile.vue';
import EnterBucketPassphraseDialog from '@poc/components/dialogs/EnterBucketPassphraseDialog.vue';
import DropzoneDialog from '@poc/components/dialogs/DropzoneDialog.vue';
import BrowserCardViewComponent from '@poc/components/BrowserCardViewComponent.vue';
import IconTableView from '@poc/components/icons/IconTableView.vue';
import IconCardView from '@poc/components/icons/IconCardView.vue';
const bucketsStore = useBucketsStore();
const obStore = useObjectBrowserStore();
const projectsStore = useProjectsStore();
const analyticsStore = useAnalyticsStore();
const config = useConfigStore();
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
const notify = useNotify();
const { smAndUp } = useDisplay();
const folderInput = ref<HTMLInputElement>();
const fileInput = ref<HTMLInputElement>();
@ -136,6 +184,7 @@ const isInitialized = ref<boolean>(false);
const isDragging = ref<boolean>(false);
const snackbar = ref<boolean>(false);
const isBucketPassphraseDialogOpen = ref<boolean>(false);
const isNewFolderDialogOpen = ref<boolean>(false);
/**
* Returns the name of the selected bucket.
@ -157,6 +206,19 @@ const projectId = computed<string>(() => projectsStore.state.selectedProject.id)
*/
const isPromptForPassphrase = computed<boolean>(() => bucketsStore.state.promptForPassphrase);
/**
* Returns total object count from store.
*/
const isCardViewEnabled = computed<boolean>(() => config.state.config.objectBrowserCardViewEnabled);
/**
* Returns whether to use the card view.
*/
const isCardView = computed<boolean>({
get: () => isCardViewEnabled.value && appStore.state.isBrowserCardViewEnabled,
set: value => appStore.toggleBrowserCardViewEnabled(value),
});
/**
* Open the operating system's file system for file upload.
*/

View File

@ -43,12 +43,7 @@
aria-label="Toggle Cards View"
@click="isTableView = false"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.99902" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="6.99902" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="6.99951" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
<rect x="12.999" y="13.0005" width="4.0003" height="4.0003" rx="1" fill="currentColor" />
</svg>
<icon-card-view />
Cards
</v-btn>
<v-btn
@ -59,14 +54,7 @@
aria-label="Toggle Table View"
@click="isTableView = true"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 8C9 8.55228 8.55228 9 8 9V9C7.44772 9 7 8.55228 7 8V8C7 7.44772 7.44772 7 8 7V7C8.55228 7 9 7.44772 9 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C9 12.5523 8.55228 13 8 13V13C7.44772 13 7 12.5523 7 12V12C7 11.4477 7.44772 11 8 11V11C8.55228 11 9 11.4477 9 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 16C9 16.5523 8.55228 17 8 17V17C7.44772 17 7 16.5523 7 16V16C7 15.4477 7.44772 15 8 15V15C8.55228 15 9 15.4477 9 16V16Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 8C18 8.55228 17.5523 9 17 9H11C10.4477 9 10 8.55228 10 8V8C10 7.44772 10.4477 7 11 7H17C17.5523 7 18 7.44772 18 8V8Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 12C18 12.5523 17.5523 13 17 13H11C10.4477 13 10 12.5523 10 12V12C10 11.4477 10.4477 11 11 11H17C17.5523 11 18 11.4477 18 12V12Z" fill="currentColor" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 16C18 16.5523 17.5523 17 17 17H11C10.4477 17 10 16.5523 10 16V16C10 15.4477 10.4477 15 11 15H17C17.5523 15 18 15.4477 18 16V16Z" fill="currentColor" />
</svg>
<icon-table-view />
Table
</v-btn>
</v-btn-toggle>
@ -130,6 +118,8 @@ import ProjectsTableComponent from '@poc/components/ProjectsTableComponent.vue';
import JoinProjectDialog from '@poc/components/dialogs/JoinProjectDialog.vue';
import CreateProjectDialog from '@poc/components/dialogs/CreateProjectDialog.vue';
import AddTeamMemberDialog from '@poc/components/dialogs/AddTeamMemberDialog.vue';
import IconCardView from '@poc/components/icons/IconCardView.vue';
import IconTableView from '@poc/components/icons/IconTableView.vue';
const appStore = useAppStore();
const projectsStore = useProjectsStore();