Folder tree dynamic navigator

This commit is contained in:
Čarodej
2022-01-21 16:46:17 +01:00
parent 6cb2a1bb9a
commit e2cfdd5345
11 changed files with 68 additions and 1332 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,31 @@
<template> <template>
<transition name="folder"> <div>
<div class="folder-item-wrapper"> <div
<div @click="goToFolder"
@click="goToFolder" class="flex items-center py-2 rounded-lg border-2 border-transparent border-dashed cursor-pointer"
class="folder-item text-theme dark-text-theme flex" :class="{'border-theme': area, 'pointer-events-none opacity-50': disabledFolder || disabled && draggedItem.length > 0 }"
:class="{'is-selected': isSelected, 'is-dragenter': area, 'is-inactive': disabledFolder || disabled && draggedItem.length > 0 }" :style="indent"
:style="indent" @dragover.prevent="dragEnter"
@dragover.prevent="dragEnter" @dragleave="dragLeave"
@dragleave="dragLeave" @drop="dragFinish()"
@drop="dragFinish()" >
> <div @click.stop.prevent="showTree" class="p-2 -ml-2 -my-2 cursor-pointer">
<chevron-right-icon <chevron-right-icon
@click.stop.prevent="showTree"
size="17" size="17"
class="icon-arrow" class="vue-feather"
:class="{'is-opened': isVisible, 'is-visible': nodes.folders.length !== 0}" :class="{'transform rotate-90': isVisible, 'opacity-0': nodes.folders.length === 0}"
/> />
<folder-icon size="17" class="icon text-theme dark-text-theme" /> </div>
<span class="label">{{ nodes.name }}</span> <folder-icon size="17" class="mr-2.5 vue-feather" :class="{'text-theme': isSelected}" />
</div> <b
<TreeMenuNavigator :disabled="disableChildren" :depth="depth + 1" v-if="isVisible" :nodes="item" v-for="item in nodes.folders" :key="item.id" /> class="font-bold text-sm max-w-1 overflow-hidden overflow-ellipsis whitespace-nowrap"
</div> :class="{'text-theme': isSelected}"
</transition> >
{{ nodes.name }}
</b>
</div>
<TreeMenuNavigator :disabled="disableChildren" :depth="depth + 1" v-if="isVisible" :nodes="item" v-for="item in nodes.folders" :key="item.id" />
</div>
</template> </template>
<script> <script>
@@ -44,13 +48,14 @@
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'clipboard' 'clipboard',
]), ]),
isSelected() { isSelected() {
return this.$route.params.id === this.nodes.id return this.$route.params.id === this.nodes.id
}, },
disabledFolder() { disabledFolder() {
let disableFolder = false let disableFolder = false
if (this.draggedItem.length > 0) { if (this.draggedItem.length > 0) {
this.draggedItem.forEach(item => { this.draggedItem.forEach(item => {
@@ -75,9 +80,9 @@
}, },
indent() { indent() {
let offset = window.innerWidth <= 1024 ? 17 : 22; let offset = window.innerWidth <= 1024 ? 14 : 18;
let value = this.depth === 0 ? offset : offset + (this.depth * 20); let value = this.depth === 0 ? offset : offset + (this.depth * 18);
return {paddingLeft: value + 'px'} return {paddingLeft: value + 'px'}
}, },
@@ -92,11 +97,7 @@
}, },
methods: { methods: {
goToFolder() { goToFolder() {
if (this.$router.currentRoute.name === 'Public') { this.$goToFileView(this.nodes.id)
this.$router.push({name: 'Public', params: {id: this.nodes.id}})
} else {
this.$router.push({name: 'Files', params: {id: this.nodes.id}})
}
}, },
dragFinish() { dragFinish() {
// Move no selected item // Move no selected item
@@ -144,100 +145,3 @@
} }
} }
</script> </script>
<style lang="scss" scoped>
@import '/resources/sass/vuefilemanager/_variables';
@import '/resources/sass/vuefilemanager/_mixins';
.is-inactive {
opacity: 0.5;
pointer-events: none;
}
.is-dragenter {
border-radius: 8px;
}
.folder-item {
padding: 8px 0;
@include transition(150ms);
cursor: pointer;
position: relative;
white-space: nowrap;
width: 100%;
border: 2px dashed transparent;
.icon {
line-height: 0;
width: 15px;
margin-right: 9px;
vertical-align: middle;
margin-top: -1px;
path, line, polyline, rect, circle {
@include transition(150ms);
}
}
.icon-arrow {
@include transition(300ms);
margin-right: 4px;
vertical-align: middle;
opacity: 0;
&.is-visible {
opacity: 1;
}
&.is-opened {
@include transform(rotate(90deg));
}
}
.label {
@include transition(150ms);
@include font-size(13);
font-weight: 700;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
color: $text;
max-width: 130px;
}
&:hover,
&.is-selected {
.icon {
path, line, polyline, rect, circle {
color: inherit !important;;
}
}
.label {
color: inherit !important;
}
}
}
@media only screen and (max-width: 1024px) {
.folder-item {
padding: 8px 0;
}
}
// Dark mode
.dark {
.folder-item {
.label {
color: $dark_mode_text_primary;
}
}
}
</style>

View File

@@ -1,100 +0,0 @@
<template>
<div class="upgrade-banner">
<div class="header-title">
<hard-drive-icon size="19" class="icon"></hard-drive-icon>
<span class="title">{{ storage.used }}% From {{ storage.capacity_formatted }}</span>
</div>
<div class="content">
<p v-if="storage.used > 95" class="reach-capacity">{{ $t('upgrade_banner.title') }}</p>
<p v-else class="reach-capacity">{{ $t('upgrade_banner.description') }}</p>
</div>
<div v-if="config.app_allowed_payments" class="footer">
<router-link :to="{name: 'UpgradePlan'}" class="button">
{{ $t('upgrade_banner.button') }}
</router-link>
</div>
</div>
</template>
<script>
import ButtonBase from '/resources/js/components/FilesView/ButtonBase'
import { HardDriveIcon } from 'vue-feather-icons'
import { mapGetters } from 'vuex'
export default {
name: 'UpgradeSidebarBanner',
components: {
HardDriveIcon,
ButtonBase,
},
computed: {
...mapGetters(['config']),
storage() {
return this.$store.getters.user.relationships.storage.data.attributes
}
}
}
</script>
<style lang="scss" scoped>
@import '/resources/sass/vuefilemanager/_variables';
@import '/resources/sass/vuefilemanager/_mixins';
.upgrade-banner {
background: rgba($danger, 0.1);
padding: 10px;
border-radius: 6px;
margin: 0 16px;
}
.header-title {
margin-bottom: 12px;
display: flex;
align-items: center;
.icon {
margin-right: 10px;
line, path {
stroke: $danger;
}
}
.title {
@include font-size(13);
font-weight: 800;
color: $danger;
}
}
.content {
margin-bottom: 12px;
p {
@include font-size(12);
color: $danger;
font-weight: 700;
}
}
.button {
background: $danger;
border-radius: 50px;
padding: 6px 0;
width: 100%;
color: white;
text-align: center;
@include font-size(12);
font-weight: 700;
display: block;
box-shadow: 0 4px 10px rgba($danger, 0.35);
}
@media only screen and (max-width: 1024px) {
}
.dark {
}
</style>

View File

@@ -60,7 +60,7 @@
<!--Transaction detail--> <!--Transaction detail-->
<tr v-if="row.data.attributes.metadata && showedTransactionDetailById === row.data.id"> <tr v-if="row.data.attributes.metadata && showedTransactionDetailById === row.data.id">
<td colspan="7" class="dark:bg-2x-dark-foreground bg-light-background bg-opacity-50 rounded-lg overflow-hidden py-2 px-4"> <td colspan="7" class="rounded-lg overflow-hidden py-2">
<div class="flex items-center justify-between py-2 border-b dark:border-opacity-5 border-light border-dashed" v-for="(usage, i) in row.data.attributes.metadata" :key="i"> <div class="flex items-center justify-between py-2 border-b dark:border-opacity-5 border-light border-dashed" v-for="(usage, i) in row.data.attributes.metadata" :key="i">
<div class="w-2/4 leading-none"> <div class="w-2/4 leading-none">
<b class="text-sm font-bold leading-none"> <b class="text-sm font-bold leading-none">

View File

@@ -7,19 +7,19 @@
> >
<div <div
v-if="isVisible" v-if="isVisible"
class="absolute transform -translate-y-full -translate-x-1/2 -top-4 ml-1.5 bg-gray-800 rounded-lg shadow-lg py-2 px-3 z-10" class="absolute transform -translate-y-full -translate-x-1/2 -top-4 ml-1.5 dark:bg-white bg-gray-800 rounded-lg shadow-lg py-2 px-3 z-10"
> >
<b class="text-white text-xs whitespace-nowrap block mb-2"> <b class="dark:text-gray-800 text-white text-xs whitespace-nowrap block mb-2">
{{ bar.created_at }} {{ bar.created_at }}
</b> </b>
<div class="flex items-center pb-1"> <div class="flex items-center pb-1">
<span class="w-3 h-3 block bg-theme mr-2 rounded"></span> <span class="w-3 h-3 block bg-theme mr-2 rounded"></span>
<b class="text-white text-xs whitespace-nowrap"> <b class="dark:text-gray-800 text-white text-xs whitespace-nowrap">
{{ bar.amount }} {{ bar.amount }}
</b> </b>
</div> </div>
<div class="w-5 overflow-hidden inline-block absolute -bottom-2.5 left-0 right-0 mx-auto"> <div class="w-5 overflow-hidden inline-block absolute -bottom-2.5 left-0 right-0 mx-auto">
<div class="h-3 w-3 bg-gray-800 -rotate-45 transform origin-top-left"></div> <div class="h-3 w-3 dark:bg-white bg-gray-800 -rotate-45 transform origin-top-left"></div>
</div> </div>
</div> </div>
<span class="bg-theme w-full h-full block rounded-lg"></span> <span class="bg-theme w-full h-full block rounded-lg"></span>

View File

@@ -23,7 +23,6 @@
<script> <script>
import {FolderIcon, HomeIcon, LinkIcon, Trash2Icon, UploadCloudIcon, UserCheckIcon, UsersIcon, XIcon} from "vue-feather-icons"; import {FolderIcon, HomeIcon, LinkIcon, Trash2Icon, UploadCloudIcon, UserCheckIcon, UsersIcon, XIcon} from "vue-feather-icons";
import UpgradeSidebarBanner from '/resources/js/components/Others/UpgradeSidebarBanner'
import TreeMenuNavigator from '/resources/js/components/Others/TreeMenuNavigator' import TreeMenuNavigator from '/resources/js/components/Others/TreeMenuNavigator'
import ContentSidebar from '/resources/js/components/Sidebar/ContentSidebar' import ContentSidebar from '/resources/js/components/Sidebar/ContentSidebar'
import ContentGroup from '/resources/js/components/Sidebar/ContentGroup' import ContentGroup from '/resources/js/components/Sidebar/ContentGroup'
@@ -33,7 +32,6 @@
export default { export default {
name: "NavigationSharePanel", name: "NavigationSharePanel",
components: { components: {
UpgradeSidebarBanner,
TreeMenuNavigator, TreeMenuNavigator,
ContentSidebar, ContentSidebar,
ContentGroup, ContentGroup,

View File

@@ -4,11 +4,6 @@
<chevrons-left-icon size="18"/> <chevrons-left-icon size="18"/>
</div> </div>
<!--Empty storage warning-->
<!-- <ContentGroup v-if="user && config.storageLimit && storage.used > 95">
<UpgradeSidebarBanner/>
</ContentGroup>-->
<!--Locations--> <!--Locations-->
<ContentGroup :title="$t('sidebar.locations_title')"> <ContentGroup :title="$t('sidebar.locations_title')">
<div class="menu-list-wrapper vertical"> <div class="menu-list-wrapper vertical">
@@ -74,7 +69,7 @@
<span v-if="tree.length === 0" class="empty-note navigator"> <span v-if="tree.length === 0" class="empty-note navigator">
{{ $t('sidebar.folders_empty') }} {{ $t('sidebar.folders_empty') }}
</span> </span>
<TreeMenuNavigator class="folder-tree" :depth="0" :nodes="folder" v-for="folder in tree" :key="folder.id"/> <TreeMenuNavigator v-if="navigation" class="folder-tree" :depth="0" :nodes="folder" v-for="folder in tree" :key="folder.id"/>
</ContentGroup> </ContentGroup>
<!--Favourites--> <!--Favourites-->
@@ -101,7 +96,6 @@
<script> <script>
import { ChevronsLeftIcon, FolderIcon, HomeIcon, LinkIcon, Trash2Icon, UploadCloudIcon, UserCheckIcon, UsersIcon, XIcon} from "vue-feather-icons"; import { ChevronsLeftIcon, FolderIcon, HomeIcon, LinkIcon, Trash2Icon, UploadCloudIcon, UserCheckIcon, UsersIcon, XIcon} from "vue-feather-icons";
import UpgradeSidebarBanner from '/resources/js/components/Others/UpgradeSidebarBanner'
import TreeMenuNavigator from '/resources/js/components/Others/TreeMenuNavigator' import TreeMenuNavigator from '/resources/js/components/Others/TreeMenuNavigator'
import ContentSidebar from '/resources/js/components/Sidebar/ContentSidebar' import ContentSidebar from '/resources/js/components/Sidebar/ContentSidebar'
import ContentGroup from '/resources/js/components/Sidebar/ContentGroup' import ContentGroup from '/resources/js/components/Sidebar/ContentGroup'
@@ -111,7 +105,6 @@
export default { export default {
name: "PanelNavigationFiles", name: "PanelNavigationFiles",
components: { components: {
UpgradeSidebarBanner,
TreeMenuNavigator, TreeMenuNavigator,
ContentSidebar, ContentSidebar,
ContentGroup, ContentGroup,
@@ -128,6 +121,7 @@ export default {
computed: { computed: {
...mapGetters([ ...mapGetters([
'isVisibleNavigationBars', 'isVisibleNavigationBars',
'navigation',
'clipboard', 'clipboard',
'config', 'config',
'user', 'user',
@@ -139,7 +133,15 @@ export default {
return this.$store.getters.user.data.attributes.storage return this.$store.getters.user.data.attributes.storage
}, },
tree() { tree() {
return this.user.data.attributes.folders return {
'RecentUploads': this.navigation[0].folders,
'MySharedItems': this.navigation[0].folders,
'Trash': this.navigation[0].folders,
'Public': this.navigation[0].folders,
'Files': this.navigation[0].folders,
'TeamFolders': this.navigation[1].folders,
'SharedWithMe': this.navigation[2].folders,
}[this.$route.name]
}, },
}, },
data() { data() {
@@ -197,6 +199,8 @@ export default {
created() { created() {
// Listen for dragstart folder items // Listen for dragstart folder items
events.$on('dragstart', item => this.draggedItem = item) events.$on('dragstart', item => this.draggedItem = item)
this.$store.dispatch('getFolderTree')
} }
} }
</script> </script>

View File

@@ -121,19 +121,6 @@ class User extends Authenticatable implements MustVerifyEmail
->sum('filesize'); ->sum('filesize');
} }
/**
* Get user full folder tree
*/
public function getFolderTreeAttribute(): Collection
{
return Folder::with(['folders.shared', 'shared:token,id,item_id,permission,is_protected,expire_in'])
->where('parent_id')
->where('team_folder', false)
->where('user_id', $this->id)
->sortable()
->get();
}
public function settings(): HasOne public function settings(): HasOne
{ {
return $this->hasOne(UserSetting::class); return $this->hasOne(UserSetting::class);

View File

@@ -36,7 +36,6 @@ class UserResource extends JsonResource
'role' => $this->role, 'role' => $this->role,
'two_factor_authentication' => $this->two_factor_secret ? true : false, 'two_factor_authentication' => $this->two_factor_secret ? true : false,
'socialite_account' => $this->password ? false : true, 'socialite_account' => $this->password ? false : true,
'folders' => $this->folder_tree,
'storage' => $this->storage, 'storage' => $this->storage,
'created_at' => format_date($this->created_at, '%d. %b. %Y'), 'created_at' => format_date($this->created_at, '%d. %b. %Y'),
'updated_at' => format_date($this->updated_at, '%d. %B. %Y'), 'updated_at' => format_date($this->updated_at, '%d. %B. %Y'),

View File

@@ -134,13 +134,13 @@ class UserStorageResource extends JsonResource
->get(); ->get();
$upload = $trafficRecords->map(fn($record) => [ $upload = $trafficRecords->map(fn($record) => [
'created_at' => format_date($record->created_at), 'created_at' => format_date($record->created_at, '%d. %B'),
'percentage' => $uploadMax !== 0 ? round(($record->upload / $uploadMax) * 100, 2) : 0, 'percentage' => $uploadMax !== 0 ? round(($record->upload / $uploadMax) * 100, 2) : 0,
'amount' => Metric::bytes($record->upload)->format(), 'amount' => Metric::bytes($record->upload)->format(),
]); ]);
$download = $trafficRecords->map(fn($record) => [ $download = $trafficRecords->map(fn($record) => [
'created_at' => format_date($record->created_at), 'created_at' => format_date($record->created_at, '%d. %B'),
'percentage' => $downloadMax !== 0 ? round(($record->download / $downloadMax) * 100, 2) : 0, 'percentage' => $downloadMax !== 0 ? round(($record->download / $downloadMax) * 100, 2) : 0,
'amount' => Metric::bytes($record->download)->format(), 'amount' => Metric::bytes($record->download)->format(),
]); ]);

View File

@@ -161,7 +161,6 @@ class UserAccountTest extends TestCase
'role' => $user->role, 'role' => $user->role,
'socialite_account' => false, 'socialite_account' => false,
'two_factor_authentication' => false, 'two_factor_authentication' => false,
'folders' => [],
'storage' => [ 'storage' => [
'used' => 0, 'used' => 0,
'used_formatted' => '0%', 'used_formatted' => '0%',