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:
parent
e482e1296e
commit
febd2091df
@ -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,
|
||||
|
@ -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 };
|
||||
|
@ -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
|
||||
* */
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
126
web/satellite/vuetify-poc/src/components/FileCard.vue
Normal file
126
web/satellite/vuetify-poc/src/components/FileCard.vue
Normal 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>
|
@ -45,6 +45,7 @@
|
||||
density="compact"
|
||||
:items="pageSizes"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
@update:model-value="sizeChanged"
|
||||
/>
|
||||
</v-col>
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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>
|
@ -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.
|
||||
*/
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user