web/satellite: fix browser hover and click behaviour
This change adds checkboxes to file items in the browser so they can be selected that way instead of the previous click behaviour, which now opens the object modal for files and open folder for folders. This change also features some styling fixes for the browser. Issue: https://github.com/storj/storj/issues/5726 Change-Id: I5b38208a8e8673d8212c749586bdb7e169c086f8
This commit is contained in:
parent
1d63395fd1
commit
085bc0c4cb
@ -95,7 +95,7 @@
|
||||
:on-close="closeBanner"
|
||||
/>
|
||||
|
||||
<v-table class="file-browser-table">
|
||||
<v-table selectable :selected="allFilesSelected" show-select class="file-browser-table" @selectAllClicked="toggleSelectAllFiles">
|
||||
<template #head>
|
||||
<file-browser-header />
|
||||
</template>
|
||||
@ -105,8 +105,9 @@
|
||||
:key="index"
|
||||
>
|
||||
<!-- using <th> to comply with common Vtable.vue-->
|
||||
<th class="hide-mobile" />
|
||||
<th
|
||||
class="align-left data"
|
||||
class="align-left"
|
||||
aria-roledescription="file-uploading"
|
||||
>
|
||||
<p class="file-name">
|
||||
@ -114,7 +115,7 @@
|
||||
<span>{{ filename(file) }}</span>
|
||||
</p>
|
||||
</th>
|
||||
<th class="align-left data" aria-roledescription="progress-bar">
|
||||
<th aria-roledescription="progress-bar">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
@ -127,7 +128,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-left data">
|
||||
<th>
|
||||
<v-button
|
||||
width="60px"
|
||||
font-size="14px"
|
||||
@ -135,16 +136,17 @@
|
||||
:on-press="() => cancelUpload(file.Key)"
|
||||
/>
|
||||
</th>
|
||||
<th />
|
||||
<th class="hide-mobile" />
|
||||
</tr>
|
||||
|
||||
<tr v-if="filesUploading.length" class="files-uploading-count">
|
||||
<th class="align-left data files-uploading-count__content" aria-roledescription="files-uploading-count">
|
||||
<th class="hide-mobile files-uploading-count__content" />
|
||||
<th class="align-left files-uploading-count__content" aria-roledescription="files-uploading-count">
|
||||
{{ formattedFilesWaitingToBeUploaded }}
|
||||
waiting to be uploaded...
|
||||
</th>
|
||||
<th class="files-uploading-count__content" />
|
||||
<th class="files-uploading-count__content" />
|
||||
<th class="hide-mobile files-uploading-count__content" />
|
||||
<th class="hide-mobile files-uploading-count__content" />
|
||||
<th class="files-uploading-count__content" />
|
||||
</tr>
|
||||
|
||||
@ -318,21 +320,42 @@ const bucketName = computed((): string => {
|
||||
return store.state.files.bucket;
|
||||
});
|
||||
|
||||
const files = computed((): BrowserFile[] => {
|
||||
/**
|
||||
* Whether all files are selected.
|
||||
* */
|
||||
const allFilesSelected = computed((): boolean => {
|
||||
if (files.value.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const shiftSelectedFiles = store.state.files.shiftSelectedFiles;
|
||||
const selectedFiles = store.state.files.selectedFiles;
|
||||
const selectedAnchorFile = store.state.files.selectedAnchorFile;
|
||||
const allSelectedFiles = [
|
||||
...selectedFiles,
|
||||
...shiftSelectedFiles,
|
||||
];
|
||||
|
||||
if (selectedAnchorFile && !allSelectedFiles.includes(selectedAnchorFile)) {
|
||||
allSelectedFiles.push(selectedAnchorFile);
|
||||
}
|
||||
return allSelectedFiles.length === files.value.length;
|
||||
});
|
||||
|
||||
const files = computed((): BrowserObject[] => {
|
||||
return store.getters['files/sortedFiles'];
|
||||
});
|
||||
|
||||
/**
|
||||
* Return an array of BrowserFile type that are files and not folders.
|
||||
*/
|
||||
const singleFiles = computed((): BrowserFile[] => {
|
||||
const singleFiles = computed((): BrowserObject[] => {
|
||||
return files.value.filter((f) => f.type === 'file');
|
||||
});
|
||||
|
||||
/**
|
||||
* Return an array of BrowserFile type that are folders and not files.
|
||||
*/
|
||||
const folders = computed((): BrowserFile[] => {
|
||||
const folders = computed((): BrowserObject[] => {
|
||||
return files.value.filter((f) => f.type === 'folder');
|
||||
});
|
||||
|
||||
@ -382,10 +405,8 @@ function closeModalDropdown(): void {
|
||||
store.dispatch('files/closeDropdown');
|
||||
}
|
||||
|
||||
if (store.state.files.selectedFile) {
|
||||
store.dispatch('files/clearAllSelectedFiles');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the folder creation modal in the store.
|
||||
@ -477,6 +498,22 @@ async function goToBuckets(): Promise<void> {
|
||||
await onRouteChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the selection of all files.
|
||||
* */
|
||||
async function toggleSelectAllFiles(): Promise<void> {
|
||||
if (files.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (allFilesSelected.value) {
|
||||
await store.dispatch('files/clearAllSelectedFiles');
|
||||
} else {
|
||||
await store.dispatch('files/clearAllSelectedFiles');
|
||||
store.commit('files/setSelectedAnchorFile', files.value[0]);
|
||||
await store.dispatch('files/updateSelectedFiles', files.value.slice(1, files.value.length));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set spinner state. If routePath is not present navigate away.
|
||||
* If there's some error then re-render the page with a call to list.
|
||||
@ -491,6 +528,9 @@ onBeforeMount(async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// clear previous file selections.
|
||||
store.dispatch('files/clearAllSelectedFiles');
|
||||
|
||||
// display the spinner while files are being fetched
|
||||
fetchingFilesSpinner.value = true;
|
||||
|
||||
@ -513,6 +553,20 @@ onBeforeMount(async () => {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.hide-mobile {
|
||||
@media screen and (max-width: 550px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
// hide size, upload date columns on mobile screens
|
||||
|
||||
:deep(.data:not(:nth-child(2))) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -111,9 +111,9 @@ function fromFilesStore(prop: string): string {
|
||||
/**
|
||||
* Check if the trashcan to delete selected files/folder should be displayed.
|
||||
*/
|
||||
const filesToDelete = computed((): string => {
|
||||
const filesToDelete = computed((): boolean => {
|
||||
return (!!store.state.files.selectedAnchorFile || (
|
||||
store.state.files.unselectedAnchorFile &&
|
||||
!!store.state.files.unselectedAnchorFile &&
|
||||
(store.state.files.selectedFiles.length > 0 ||
|
||||
store.state.files.shiftSelectedFiles.length > 0)
|
||||
));
|
||||
@ -219,7 +219,8 @@ function cancelDeleteSelection(): void {
|
||||
}
|
||||
|
||||
&__functional {
|
||||
padding: 0 10px;
|
||||
padding: 0;
|
||||
width: 50px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
|
@ -4,11 +4,13 @@
|
||||
<template>
|
||||
<table-item
|
||||
v-if="fileTypeIsFile"
|
||||
selectable
|
||||
:selected="isFileSelected"
|
||||
:on-click="selectFile"
|
||||
:on-click="openModal"
|
||||
:on-primary-click="openModal"
|
||||
:item="{'name': file.Key, 'size': size, 'date': uploadDate}"
|
||||
:item-type="fileType"
|
||||
@selectClicked="selectFile"
|
||||
>
|
||||
<th slot="options" v-click-outside="closeDropdown" class="file-entry__functional options overflow-visible" @click.stop="openDropdown">
|
||||
<div
|
||||
@ -61,10 +63,12 @@
|
||||
<table-item
|
||||
v-else-if="fileTypeIsFolder"
|
||||
:item="{'name': file.Key, 'size': '', 'date': ''}"
|
||||
selectable
|
||||
:selected="isFileSelected"
|
||||
:on-click="selectFile"
|
||||
:on-primary-click="openBucket"
|
||||
:on-click="openFolder"
|
||||
:on-primary-click="openFolder"
|
||||
item-type="folder"
|
||||
@selectClicked="selectFile"
|
||||
>
|
||||
<th slot="options" v-click-outside="closeDropdown" class="file-entry__functional options overflow-visible" @click.stop="openDropdown">
|
||||
<div
|
||||
@ -108,11 +112,11 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
import type { BrowserFile } from '@/types/browser';
|
||||
import { useNotify, useRouter, useStore } from '@/utils/hooks';
|
||||
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
|
||||
import { MODALS } from '@/utils/constants/appStatePopUps';
|
||||
import { APP_STATE_MUTATIONS } from '@/store/mutationConstants';
|
||||
import { BrowserObject } from '@/store/modules/files';
|
||||
|
||||
import TableItem from '@/components/common/TableItem.vue';
|
||||
|
||||
@ -129,7 +133,7 @@ const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
path: string,
|
||||
file: BrowserFile,
|
||||
file: BrowserObject,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['onUpdate']);
|
||||
@ -265,8 +269,9 @@ function selectFile(event: KeyboardEvent): void {
|
||||
setSelectedFile(isSelectedFile);
|
||||
}
|
||||
|
||||
async function openBucket(): Promise<void> {
|
||||
async function openFolder(): Promise<void> {
|
||||
await router.push(link.value);
|
||||
store.dispatch('files/clearAllSelectedFiles');
|
||||
emit('onUpdate');
|
||||
}
|
||||
|
||||
@ -340,11 +345,30 @@ function setSelectedFile(command: boolean): void {
|
||||
|
||||
store.dispatch('files/updateShiftSelectedFiles', []);
|
||||
} else {
|
||||
/* if it's just a file click without any modifier, then set selectedAnchorFile to the file that was clicked, set shiftSelectedFiles and selectedFiles to an empty array. */
|
||||
|
||||
/* if it's just a file click without any modifier ... */
|
||||
const newSelection = [...files];
|
||||
const fileIdx = newSelection.findIndex((file) => file === props.file);
|
||||
switch (true) {
|
||||
case fileIdx !== -1:
|
||||
// this file is already selected, deselect.
|
||||
newSelection.splice(fileIdx, 1);
|
||||
break;
|
||||
case selectedAnchorFile === props.file:
|
||||
// this file is already selected, deselect.
|
||||
store.commit('files/setSelectedAnchorFile', null);
|
||||
store.commit('files/setUnselectedAnchorFile', props.file);
|
||||
break;
|
||||
case !!selectedAnchorFile:
|
||||
// there's an anchor file, but not this file.
|
||||
// add the anchor file to the selection arr and make this file the anchor file.
|
||||
newSelection.push(selectedAnchorFile as BrowserObject);
|
||||
store.commit('files/setSelectedAnchorFile', props.file);
|
||||
break;
|
||||
default:
|
||||
store.commit('files/setSelectedAnchorFile', props.file);
|
||||
}
|
||||
store.dispatch('files/updateShiftSelectedFiles', []);
|
||||
store.dispatch('files/updateSelectedFiles', []);
|
||||
store.dispatch('files/updateSelectedFiles', newSelection);
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,10 +507,16 @@ function cancelDeletion(): void {
|
||||
.file-entry {
|
||||
|
||||
&__functional {
|
||||
padding: 0 10px;
|
||||
padding: 0;
|
||||
width: 50px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
@media screen and (max-width: 550px) {
|
||||
padding: 0 10px;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
&__dropdown {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
@ -570,7 +600,7 @@ function cancelDeletion(): void {
|
||||
@media screen and (max-width: 550px) {
|
||||
// hide size, upload date columns on mobile screens
|
||||
|
||||
:deep(.data:not(:first-of-type)) {
|
||||
:deep(.data:not(:nth-child(2))) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
<template>
|
||||
<table-item
|
||||
selectable
|
||||
select-hidden
|
||||
:on-click="openModal"
|
||||
:on-primary-click="openModal"
|
||||
:item="{'name': 'Objects locked', 'size': '', 'date': ''}"
|
||||
|
@ -3,8 +3,9 @@
|
||||
|
||||
<template>
|
||||
<table-item
|
||||
selectable
|
||||
select-hidden
|
||||
:on-click="onBack"
|
||||
:on-primary-click="onBack"
|
||||
:item="{'name': 'Back', 'size': '', 'date': ''}"
|
||||
item-type="back"
|
||||
>
|
||||
|
@ -7,8 +7,8 @@
|
||||
:class="{ 'selected': selected }"
|
||||
@click="onClick"
|
||||
>
|
||||
<th v-if="selectable" class="icon select">
|
||||
<v-table-checkbox :disabled="selectDisabled" :value="selected" @checkChange="onChange" />
|
||||
<th v-if="selectable" class="icon select" @click.stop="selectClicked">
|
||||
<v-table-checkbox v-if="!selectHidden" :disabled="selectDisabled || selectHidden" :value="selected" @selectClicked="selectClicked" />
|
||||
</th>
|
||||
<th
|
||||
v-for="(val, _, index) in item" :key="index" class="align-left data"
|
||||
@ -22,7 +22,7 @@
|
||||
<component :is="icon" />
|
||||
</div>
|
||||
<p :class="{primary: index === 0}" :title="val" @click.stop="(e) => cellContentClicked(index, e)">
|
||||
<middle-truncate v-if="(itemType.toLowerCase() === 'file')" :text="val" />
|
||||
<middle-truncate v-if="(itemType?.toLowerCase() === 'file')" :text="val" />
|
||||
<span v-else>{{ val }}</span>
|
||||
</p>
|
||||
<div v-if="showBucketGuide(index)" class="animation">
|
||||
@ -57,6 +57,7 @@ import ZipIcon from '@/../static/images/objects/zip.svg';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
selectDisabled?: boolean;
|
||||
selectHidden?: boolean;
|
||||
selected?: boolean;
|
||||
selectable?: boolean;
|
||||
showGuide?: boolean;
|
||||
@ -68,6 +69,7 @@ const props = withDefaults(defineProps<{
|
||||
onPrimaryClick?: (data?: unknown) => void;
|
||||
}>(), {
|
||||
selectDisabled: false,
|
||||
selectHidden: false,
|
||||
selected: false,
|
||||
selectable: false,
|
||||
showGuide: false,
|
||||
@ -78,7 +80,7 @@ const props = withDefaults(defineProps<{
|
||||
onPrimaryClick: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selectChange']);
|
||||
const emit = defineEmits(['selectClicked']);
|
||||
|
||||
const icons = new Map<string, VueConstructor>([
|
||||
['locked', TableLockedIcon],
|
||||
@ -97,8 +99,8 @@ const icons = new Map<string, VueConstructor>([
|
||||
|
||||
const icon = computed(() => icons.get(props.itemType.toLowerCase()));
|
||||
|
||||
function onChange(value: boolean): void {
|
||||
emit('selectChange', value);
|
||||
function selectClicked(event: Event): void {
|
||||
emit('selectClicked', event);
|
||||
}
|
||||
|
||||
function showBucketGuide(index: number): boolean {
|
||||
@ -185,15 +187,15 @@ function cellContentClicked(cellIndex: number, event: Event) {
|
||||
.primary {
|
||||
color: var(--c-blue-3);
|
||||
}
|
||||
|
||||
svg :deep(path) {
|
||||
fill: var(--c-blue-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--c-yellow-1);
|
||||
|
||||
:deep(.select) {
|
||||
background: var(--c-yellow-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,9 @@
|
||||
<table class="base-table" border="0" cellpadding="0" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="selectable" class="icon select" />
|
||||
<th v-if="selectable" class="icon select" @click.stop="() => emit('selectAllClicked')">
|
||||
<v-table-checkbox v-if="showSelect" :value="selected" @selectClicked="() => emit('selectAllClicked')" />
|
||||
</th>
|
||||
<slot name="head" />
|
||||
</tr>
|
||||
</thead>
|
||||
@ -31,6 +33,7 @@
|
||||
import { OnPageClickCallback } from '@/types/pagination';
|
||||
|
||||
import TablePagination from '@/components/common/TablePagination.vue';
|
||||
import VTableCheckbox from '@/components/common/VTableCheckbox.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
itemsLabel?: string,
|
||||
@ -39,14 +42,20 @@ const props = withDefaults(defineProps<{
|
||||
onPageClickCallback?: OnPageClickCallback,
|
||||
totalPageCount?: number,
|
||||
selectable?: boolean,
|
||||
selected?: boolean,
|
||||
showSelect?: boolean,
|
||||
}>(), {
|
||||
selectable: false,
|
||||
selected: false,
|
||||
showSelect: false,
|
||||
totalPageCount: 0,
|
||||
itemsLabel: '',
|
||||
limit: 0,
|
||||
totalItemsCount: 0,
|
||||
onPageClickCallback: () => () => Promise.resolve(),
|
||||
});
|
||||
|
||||
const emit = defineEmits(['selectAllClicked']);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -82,6 +91,10 @@ const props = withDefaults(defineProps<{
|
||||
height: 52px;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
|
||||
th.icon {
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,8 +131,9 @@ const props = withDefaults(defineProps<{
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 5%;
|
||||
width: 50px;
|
||||
overflow: visible !important;
|
||||
background: var(--c-grey-1);
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
@ -155,7 +169,7 @@ const props = withDefaults(defineProps<{
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (max-width: 550px) {
|
||||
|
||||
.select {
|
||||
display: none;
|
||||
|
@ -2,11 +2,11 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
<template>
|
||||
<label class="container" @click.stop> <!--don't propagate click event to parent <tr>-->
|
||||
<label class="container" @click.stop.prevent="selectClicked"> <!--don't propagate click event to parent <tr>-->
|
||||
<input
|
||||
id="checkbox" :disabled="disabled" :checked="value"
|
||||
class="checkmark-input"
|
||||
type="checkbox" @change="onChange"
|
||||
type="checkbox"
|
||||
>
|
||||
<span class="checkmark" />
|
||||
</label>
|
||||
@ -18,23 +18,23 @@ const props = withDefaults(defineProps<{
|
||||
disabled?: boolean,
|
||||
}>(), { value: false, disabled: false });
|
||||
|
||||
const emit = defineEmits(['checkChange']);
|
||||
const emit = defineEmits(['selectClicked']);
|
||||
|
||||
/**
|
||||
* Emits value to parent component.
|
||||
* Emits click event to parent component.
|
||||
* The parent is responsible for toggling the value prop.
|
||||
*/
|
||||
function onChange(event: { target: { checked: boolean } }): void {
|
||||
emit('checkChange', event.target.checked);
|
||||
function selectClicked(event: Event): void {
|
||||
emit('selectClicked', event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 15px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
@ -50,8 +50,6 @@ function onChange(event: { target: { checked: boolean } }): void {
|
||||
|
||||
.checkmark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border: 2px solid rgb(56 75 101 / 40%);
|
||||
|
@ -9,7 +9,7 @@
|
||||
:select-disabled="isProjectOwner"
|
||||
:selected="itemData.isSelected"
|
||||
:on-click="(_) => $emit('memberClick', itemData)"
|
||||
@selectChange="(value) => $emit('selectChange', value)"
|
||||
@selectClicked="($event) => $emit('selectClicked', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
@ -38,7 +38,7 @@
|
||||
:key="key"
|
||||
:item-data="member"
|
||||
@memberClick="onMemberCheckChange"
|
||||
@selectChange="(_) => onMemberCheckChange(member)"
|
||||
@selectClicked="(_) => onMemberCheckChange(member)"
|
||||
/>
|
||||
</template>
|
||||
</v-table>
|
||||
|
Loading…
Reference in New Issue
Block a user