mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-11 16:01:20 +00:00
Merge branch '276-redesign-left-sidebar-2' into 'develop'
Resolve "Redesign left sidebar" Closes #276 See merge request bramw/baserow!167
This commit is contained in:
commit
273c76ef16
18 changed files with 475 additions and 296 deletions
changelog.md
docs/assets
web-frontend/modules
core
assets/scss
components
layouts
middleware
mixins
pages
store
database/pages
|
@ -2,6 +2,7 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
* Redesigned the left sidebar.
|
||||
* Fixed error when a very long user file name is provided when uploading.
|
||||
* Upgraded DRF Spectacular dependency to the latest version.
|
||||
* Added single select field form option validation.
|
||||
|
|
Binary file not shown.
Before ![]() (image error) Size: 134 KiB After ![]() (image error) Size: 119 KiB ![]() ![]() |
|
@ -5,9 +5,8 @@
|
|||
@import 'form';
|
||||
@import 'box';
|
||||
@import 'layout';
|
||||
@import 'menu';
|
||||
@import 'sidebar';
|
||||
@import 'tree';
|
||||
@import 'sidebar';
|
||||
@import 'header';
|
||||
@import 'scrollbar';
|
||||
@import 'modal';
|
||||
|
|
|
@ -12,42 +12,33 @@
|
|||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 52px;
|
||||
width: 240px;
|
||||
|
||||
.layout--collapsed & {
|
||||
width: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.layout__col-2 {
|
||||
position: absolute;
|
||||
z-index: $z-index-layout-col-2;
|
||||
left: 52px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 226px;
|
||||
|
||||
.layout--collapsed & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.layout__col-3 {
|
||||
position: absolute;
|
||||
z-index: $z-index-layout-col-3;
|
||||
left: 278px;
|
||||
left: 240px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
|
||||
.layout--collapsed & {
|
||||
left: 52px;
|
||||
left: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.layout__col-3-scroll {
|
||||
.layout__col-2-scroll {
|
||||
@include absolute(0);
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.layout__col-3-1 {
|
||||
.layout__col-2-1 {
|
||||
position: absolute;
|
||||
z-index: $z-index-layout-col-3-1;
|
||||
left: 0;
|
||||
|
@ -56,7 +47,7 @@
|
|||
height: 52px;
|
||||
}
|
||||
|
||||
.layout__col-3-2 {
|
||||
.layout__col-2-2 {
|
||||
position: absolute;
|
||||
z-index: $z-index-layout-col-3-2;
|
||||
left: 0;
|
||||
|
@ -64,11 +55,3 @@
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.layout__uncollapse {
|
||||
display: none;
|
||||
|
||||
.layout--collapsed & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: $color-primary-600;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.menu__items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu__item {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.menu__link {
|
||||
position: relative;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
color: $white;
|
||||
|
||||
@include center-text(32px, 16px);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.menu__link-text {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 36px;
|
||||
top: 50%;
|
||||
margin-top: -10.5px;
|
||||
background-color: $color-neutral-900;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
|
||||
@include center-text(auto, 11px, 21px);
|
||||
|
||||
a:hover & {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.menu__user-item {
|
||||
border-radius: 100%;
|
||||
background-color: $color-primary-500;
|
||||
color: $white;
|
||||
font-weight: 700;
|
||||
|
||||
@include center-text(32px, 13px);
|
||||
}
|
|
@ -1,63 +1,184 @@
|
|||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: $white;
|
||||
border-right: 1px solid $color-neutral-200;
|
||||
}
|
||||
@include absolute(0);
|
||||
|
||||
.sidebar__content-wrapper {
|
||||
overflow-y: auto;
|
||||
background-color: $color-neutral-10;
|
||||
border-right: solid 1px $color-neutral-200;
|
||||
height: 100%;
|
||||
|
||||
.layout--collapsed & {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__content {
|
||||
padding: 12px;
|
||||
.sidebar__inner {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
padding-bottom: 46px;
|
||||
|
||||
.layout--collapsed & {
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__footer {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid $color-neutral-200;
|
||||
}
|
||||
|
||||
.sidebar__collapse {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
color: $color-primary-900;
|
||||
font-weight: bold;
|
||||
|
||||
@include fixed-height(47px, 14px);
|
||||
.sidebar__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: $color-neutral-100;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.layout--collapsed & {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__title {
|
||||
font-family: $logo-font-stack;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
.sidebar__user-initials {
|
||||
flex: 0 0 36px;
|
||||
font-weight: bold;
|
||||
color: $white;
|
||||
background-color: $color-primary-500;
|
||||
border-radius: 100%;
|
||||
margin-right: 12px;
|
||||
|
||||
img {
|
||||
max-width: 104px;
|
||||
@include center-text(36px, 15px);
|
||||
|
||||
.layout--collapsed & {
|
||||
flex-basis: 32px;
|
||||
margin-right: 0;
|
||||
|
||||
@include center-text(32px, 12px);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__group-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
.sidebar__user-info {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
.layout--collapsed & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__user-info-top {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sidebar__user-name {
|
||||
@extend %ellipsis;
|
||||
|
||||
min-width: 0;
|
||||
color: $color-primary-900;
|
||||
}
|
||||
|
||||
.sidebar__user-icon {
|
||||
flex: 0 0 20px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: $color-primary-900;
|
||||
}
|
||||
|
||||
.sidebar__user-email {
|
||||
@extend %ellipsis;
|
||||
|
||||
font-size: 12px;
|
||||
color: $color-neutral-600;
|
||||
}
|
||||
|
||||
.sidebar__nav {
|
||||
padding: 0 10px;
|
||||
|
||||
.layout--collapsed & {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__new-wrapper {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.sidebar__new {
|
||||
font-size: 13px;
|
||||
color: $color-neutral-300;
|
||||
margin-left: 7px;
|
||||
margin-left: 6px;
|
||||
|
||||
&:hover {
|
||||
color: $color-neutral-500;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__foot {
|
||||
@include absolute(auto, 0, 0, 0);
|
||||
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 16px 16px 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.layout--collapsed & {
|
||||
flex-direction: column;
|
||||
height: 56px;
|
||||
padding: 0 8px 8px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__collapse-link {
|
||||
color: $color-neutral-700;
|
||||
border-radius: 3px;
|
||||
|
||||
@include center-text(20px, 12px);
|
||||
|
||||
&:hover {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
background-color: $color-neutral-100;
|
||||
}
|
||||
}
|
||||
|
||||
.layout--collapsed {
|
||||
// Some minor changes regarding the tree items within the collapsed sidebar.
|
||||
.sidebar__action {
|
||||
.tree__link {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tree__icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.sidebar__item-name {
|
||||
margin-top: -10.5px;
|
||||
background-color: $color-neutral-900;
|
||||
color: $white;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
display: none;
|
||||
|
||||
@include absolute(50%, auto, auto, 36px);
|
||||
@include center-text(auto, 11px, 21px);
|
||||
}
|
||||
|
||||
&:hover .sidebar__item-name {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar__logo {
|
||||
display: inline-block;
|
||||
order: 2;
|
||||
width: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
.tree__action {
|
||||
@extend %tree__size;
|
||||
|
||||
padding: 0 32px 0 6px;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
|
@ -45,6 +45,11 @@
|
|||
.tree__item.active &:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.tree__action--has-options,
|
||||
&.tree__action--has-right-icon {
|
||||
padding-right: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree__link {
|
||||
|
@ -58,16 +63,24 @@
|
|||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.tree__link--group {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tree__type {
|
||||
.tree__icon {
|
||||
@extend %tree__size;
|
||||
|
||||
text-align: center;
|
||||
width: $fa-fw-width;
|
||||
color: $color-neutral-300;
|
||||
color: $color-neutral-700;
|
||||
margin-right: 4px;
|
||||
font-size: 11px;
|
||||
|
||||
&.tree__icon--type {
|
||||
color: $color-neutral-300;
|
||||
}
|
||||
}
|
||||
|
||||
%tree_sub-size {
|
||||
|
@ -141,7 +154,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tree__options {
|
||||
.tree__right-icon {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
@ -149,14 +162,10 @@
|
|||
top: 0;
|
||||
text-align: center;
|
||||
width: 32px;
|
||||
color: $color-neutral-300;
|
||||
color: $color-neutral-700;
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
&:hover {
|
||||
color: $color-neutral-700;
|
||||
}
|
||||
|
||||
:hover > & {
|
||||
display: block;
|
||||
}
|
||||
|
@ -165,3 +174,13 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tree__options {
|
||||
@extend .tree__right-icon;
|
||||
|
||||
color: $color-neutral-300;
|
||||
|
||||
&:hover {
|
||||
color: $color-neutral-700;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ $color-primary-700: #0f6499 !default;
|
|||
$color-primary-800: #0a4970 !default;
|
||||
$color-primary-900: #062e47 !default;
|
||||
|
||||
$color-neutral-10: #fcfcfc !default;
|
||||
$color-neutral-50: #fafafa !default;
|
||||
$color-neutral-100: #f5f5f5 !default;
|
||||
$color-neutral-200: #d9dbde !default;
|
||||
|
|
|
@ -33,10 +33,15 @@ export default {
|
|||
* @param vertical Bottom positions the context under the target.
|
||||
* Top positions the context above the target.
|
||||
* Over-bottom positions the context over and under the target.
|
||||
* Over-top positions the context over and above the target
|
||||
* @param horizontal Left aligns the context with the left side of the target.
|
||||
* Right aligns the context with the right side of the target.
|
||||
* @param offset The distance between the target element and the context.
|
||||
* Over-top positions the context over and above the target.
|
||||
* @param horizontal `left` aligns the context with the left side of the target.
|
||||
* `right` aligns the context with the right side of the target.
|
||||
* @param verticalOffset
|
||||
* The offset indicates how many pixels the context is moved
|
||||
* top from the original calculated position.
|
||||
* @param horizontalOffset
|
||||
* The offset indicates how many pixels the context is moved
|
||||
* left from the original calculated position.
|
||||
* @param value True if context must be shown, false if not and undefine
|
||||
* will invert the current state.
|
||||
*/
|
||||
|
@ -44,7 +49,8 @@ export default {
|
|||
target,
|
||||
vertical = 'bottom',
|
||||
horizontal = 'left',
|
||||
offset = 10,
|
||||
verticalOffset = 10,
|
||||
horizontalOffset = 0,
|
||||
value
|
||||
) {
|
||||
if (value === undefined) {
|
||||
|
@ -52,7 +58,13 @@ export default {
|
|||
}
|
||||
|
||||
if (value) {
|
||||
this.show(target, vertical, horizontal, offset)
|
||||
this.show(
|
||||
target,
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
)
|
||||
} else {
|
||||
this.hide()
|
||||
}
|
||||
|
@ -61,12 +73,24 @@ export default {
|
|||
* Calculate the position, show the context menu and register a click event on the
|
||||
* body to check if the user has clicked outside the context.
|
||||
*/
|
||||
show(target, vertical, horizontal, offset) {
|
||||
show(target, vertical, horizontal, verticalOffset, horizontalOffset) {
|
||||
const isElementOrigin = isDomElement(target)
|
||||
const updatePosition = () => {
|
||||
const css = isElementOrigin
|
||||
? this.calculatePositionElement(target, vertical, horizontal, offset)
|
||||
: this.calculatePositionFixed(target, vertical, horizontal, offset)
|
||||
? this.calculatePositionElement(
|
||||
target,
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
)
|
||||
: this.calculatePositionFixed(
|
||||
target,
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
)
|
||||
|
||||
// Set the calculated positions of the context.
|
||||
for (const key in css) {
|
||||
|
@ -132,7 +156,13 @@ export default {
|
|||
* figure out the correct position, so in that case we force the element to be
|
||||
* visible.
|
||||
*/
|
||||
calculatePositionElement(target, vertical, horizontal, offset) {
|
||||
calculatePositionElement(
|
||||
target,
|
||||
vertical,
|
||||
horizontal,
|
||||
verticalOffset,
|
||||
horizontalOffset
|
||||
) {
|
||||
const visible =
|
||||
window.getComputedStyle(target).getPropertyValue('display') !== 'none'
|
||||
|
||||
|
@ -151,15 +181,29 @@ export default {
|
|||
}
|
||||
|
||||
// Calculate if top, bottom, left and right positions are possible.
|
||||
const canTop = targetRect.top - contextRect.height - offset > 0
|
||||
const canTop = targetRect.top - contextRect.height - verticalOffset > 0
|
||||
const canBottom =
|
||||
window.innerHeight - targetRect.bottom - contextRect.height - offset > 0
|
||||
const canOverTop = targetRect.bottom - contextRect.height - offset > 0
|
||||
window.innerHeight -
|
||||
targetRect.bottom -
|
||||
contextRect.height -
|
||||
verticalOffset >
|
||||
0
|
||||
const canOverTop =
|
||||
targetRect.bottom - contextRect.height - verticalOffset > 0
|
||||
const canOverBottom =
|
||||
window.innerHeight - targetRect.bottom - contextRect.height - offset > 0
|
||||
const canRight = targetRect.right - contextRect.width > 0
|
||||
window.innerHeight -
|
||||
targetRect.bottom -
|
||||
contextRect.height -
|
||||
verticalOffset >
|
||||
0
|
||||
const canRight =
|
||||
targetRect.right - contextRect.width - horizontalOffset > 0
|
||||
const canLeft =
|
||||
window.innerWidth - targetRect.left - contextRect.width > 0
|
||||
window.innerWidth -
|
||||
targetRect.left -
|
||||
contextRect.width -
|
||||
horizontalOffset >
|
||||
0
|
||||
|
||||
// If bottom, top, left or right doesn't fit, but their opposite does we switch to
|
||||
// that.
|
||||
|
@ -189,27 +233,29 @@ export default {
|
|||
|
||||
// Calculate the correct positions for horizontal and vertical values.
|
||||
if (horizontal === 'left') {
|
||||
positions.left = targetRect.left
|
||||
positions.left = targetRect.left + horizontalOffset
|
||||
}
|
||||
|
||||
if (horizontal === 'right') {
|
||||
positions.right = window.innerWidth - targetRect.right
|
||||
positions.right =
|
||||
window.innerWidth - targetRect.right - horizontalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'bottom') {
|
||||
positions.top = targetRect.bottom + offset
|
||||
positions.top = targetRect.bottom + verticalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'top') {
|
||||
positions.bottom = window.innerHeight - targetRect.top + offset
|
||||
positions.bottom = window.innerHeight - targetRect.top + verticalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'over-bottom') {
|
||||
positions.top = targetRect.top + offset
|
||||
positions.top = targetRect.top + verticalOffset
|
||||
}
|
||||
|
||||
if (vertical === 'over-top') {
|
||||
positions.bottom = window.innerHeight - targetRect.bottom + offset
|
||||
positions.bottom =
|
||||
window.innerHeight - targetRect.bottom + verticalOffset
|
||||
}
|
||||
|
||||
return positions
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="$refs.groupMembersModal.show()">
|
||||
<a @click=";[$refs.groupMembersModal.show(), hide()]">
|
||||
<i class="context__menu-icon fas fa-fw fa-users"></i>
|
||||
Members
|
||||
</a>
|
||||
|
|
|
@ -1,30 +1,179 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="hasSelectedGroup">
|
||||
<div class="sidebar__group-title">{{ selectedGroup.name }}</div>
|
||||
<ul class="tree">
|
||||
<SidebarApplication
|
||||
v-for="application in applications"
|
||||
:key="application.id"
|
||||
:application="application"
|
||||
></SidebarApplication>
|
||||
</ul>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar__inner">
|
||||
<a
|
||||
ref="createApplicationContextLink"
|
||||
class="sidebar__new"
|
||||
ref="userContextAnchor"
|
||||
class="sidebar__user"
|
||||
@click="
|
||||
$refs.createApplicationContext.toggle(
|
||||
$refs.createApplicationContextLink
|
||||
$refs.userContext.toggle(
|
||||
$refs.userContextAnchor,
|
||||
'bottom',
|
||||
'left',
|
||||
isCollapsed ? -4 : -10,
|
||||
isCollapsed ? 8 : 16
|
||||
)
|
||||
"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
Create new
|
||||
<div class="sidebar__user-initials">
|
||||
{{ name | nameAbbreviation }}
|
||||
</div>
|
||||
<div class="sidebar__user-info">
|
||||
<div class="sidebar__user-info-top">
|
||||
<div class="sidebar__user-name">{{ name }}</div>
|
||||
<div class="sidebar__user-icon">
|
||||
<i class="fas fa-caret-down"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar__user-email">{{ email }}</div>
|
||||
</div>
|
||||
</a>
|
||||
<CreateApplicationContext
|
||||
ref="createApplicationContext"
|
||||
:group="selectedGroup"
|
||||
></CreateApplicationContext>
|
||||
<Context ref="userContext">
|
||||
<div class="context__menu-title">{{ name }}</div>
|
||||
<ul class="context__menu">
|
||||
<li>
|
||||
<a
|
||||
@click="
|
||||
;[
|
||||
$refs.settingsModal.show('password'),
|
||||
$refs.userContext.hide(),
|
||||
]
|
||||
"
|
||||
>
|
||||
<i class="context__menu-icon fas fa-fw fa-cogs"></i>
|
||||
Settings
|
||||
</a>
|
||||
<SettingsModal ref="settingsModal"></SettingsModal>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="logoff()">
|
||||
<i class="context__menu-icon fas fa-fw fa-sign-out-alt"></i>
|
||||
Logoff
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Context>
|
||||
<div class="sidebar__nav">
|
||||
<ul class="tree">
|
||||
<li
|
||||
class="tree__item"
|
||||
:class="{
|
||||
active: $route.matched.some(({ name }) => name === 'dashboard'),
|
||||
}"
|
||||
>
|
||||
<div class="tree__action sidebar__action">
|
||||
<nuxt-link :to="{ name: 'dashboard' }" class="tree__link">
|
||||
<i class="tree__icon fas fa-tachometer-alt"></i>
|
||||
<span class="sidebar__item-name">Dashboard</span>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</li>
|
||||
<template v-if="hasSelectedGroup && !isCollapsed">
|
||||
<li class="tree__item margin-top-2">
|
||||
<div class="tree__action">
|
||||
<a
|
||||
ref="groupSelectToggle"
|
||||
class="tree__link tree__link--group"
|
||||
@click="
|
||||
$refs.groupSelect.toggle(
|
||||
$refs.groupSelectToggle,
|
||||
'bottom',
|
||||
'left',
|
||||
0
|
||||
)
|
||||
"
|
||||
>{{ selectedGroup.name }}</a
|
||||
>
|
||||
<GroupsContext ref="groupSelect"></GroupsContext>
|
||||
</div>
|
||||
</li>
|
||||
<li class="tree__item">
|
||||
<div class="tree__action">
|
||||
<a class="tree__link" @click="$refs.groupMembersModal.show()">
|
||||
<i class="tree__icon tree__icon--type fas fa-users"></i>
|
||||
Invite others
|
||||
</a>
|
||||
<GroupMembersModal
|
||||
ref="groupMembersModal"
|
||||
:group="selectedGroup"
|
||||
></GroupMembersModal>
|
||||
</div>
|
||||
</li>
|
||||
<ul class="tree">
|
||||
<SidebarApplication
|
||||
v-for="application in applications"
|
||||
:key="application.id"
|
||||
:application="application"
|
||||
></SidebarApplication>
|
||||
</ul>
|
||||
<li class="sidebar__new-wrapper">
|
||||
<a
|
||||
ref="createApplicationContextLink"
|
||||
class="sidebar__new"
|
||||
@click="
|
||||
$refs.createApplicationContext.toggle(
|
||||
$refs.createApplicationContextLink
|
||||
)
|
||||
"
|
||||
>
|
||||
<i class="fas fa-plus"></i>
|
||||
Create new
|
||||
</a>
|
||||
</li>
|
||||
<CreateApplicationContext
|
||||
ref="createApplicationContext"
|
||||
:group="selectedGroup"
|
||||
></CreateApplicationContext>
|
||||
</template>
|
||||
<template v-else-if="!hasSelectedGroup && !isCollapsed">
|
||||
<li v-if="groups.length === 0" class="tree_item margin-top-2">
|
||||
<p>You don’t have any groups.</p>
|
||||
</li>
|
||||
<li
|
||||
v-for="(group, index) in groups"
|
||||
:key="group.id"
|
||||
class="tree__item"
|
||||
:class="{ 'margin-top-2': index === 0 }"
|
||||
>
|
||||
<div class="tree__action tree__action--has-right-icon">
|
||||
<a
|
||||
class="tree__link tree__link--group"
|
||||
@click="$store.dispatch('group/select', group)"
|
||||
>{{ group.name }}</a
|
||||
>
|
||||
<i class="tree__right-icon fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar__new-wrapper">
|
||||
<a class="sidebar__new" @click="$refs.createGroupModal.show()">
|
||||
<i class="fas fa-plus"></i>
|
||||
Create group
|
||||
</a>
|
||||
</li>
|
||||
<CreateGroupModal ref="createGroupModal"></CreateGroupModal>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="sidebar__foot">
|
||||
<div class="sidebar__logo">
|
||||
<img
|
||||
height="14"
|
||||
src="@baserow/modules/core/static/img/logo.svg"
|
||||
alt="Baserow logo"
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
class="sidebar__collapse-link"
|
||||
@click="$store.dispatch('sidebar/toggleCollapsed')"
|
||||
>
|
||||
<i
|
||||
class="fas"
|
||||
:class="{
|
||||
'fa-angle-double-right': isCollapsed,
|
||||
'fa-angle-double-left': !isCollapsed,
|
||||
}"
|
||||
></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -32,14 +181,22 @@
|
|||
<script>
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import SettingsModal from '@baserow/modules/core/components/settings/SettingsModal'
|
||||
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
|
||||
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'
|
||||
import GroupsContext from '@baserow/modules/core/components/group/GroupsContext'
|
||||
import CreateGroupModal from '@baserow/modules/core/components/group/CreateGroupModal'
|
||||
import GroupMembersModal from '@baserow/modules/core/components/group/GroupMembersModal'
|
||||
|
||||
export default {
|
||||
name: 'Sidebar',
|
||||
components: {
|
||||
SettingsModal,
|
||||
CreateApplicationContext,
|
||||
SidebarApplication,
|
||||
GroupsContext,
|
||||
CreateGroupModal,
|
||||
GroupMembersModal,
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
|
@ -53,12 +210,21 @@ export default {
|
|||
},
|
||||
...mapState({
|
||||
allApplications: (state) => state.application.items,
|
||||
groups: (state) => state.group.items,
|
||||
selectedGroup: (state) => state.group.selected,
|
||||
}),
|
||||
...mapGetters({
|
||||
isLoading: 'application/isLoading',
|
||||
name: 'auth/getName',
|
||||
email: 'auth/getUsername',
|
||||
hasSelectedGroup: 'group/hasSelected',
|
||||
isCollapsed: 'sidebar/isCollapsed',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
logoff() {
|
||||
this.$store.dispatch('auth/logoff')
|
||||
this.$nuxt.$router.push({ name: 'login' })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
'tree__item--loading': application._.loading,
|
||||
}"
|
||||
>
|
||||
<div class="tree__action">
|
||||
<div class="tree__action tree__action--has-options">
|
||||
<a class="tree__link" @click="selectApplication(application)">
|
||||
<i
|
||||
class="tree__type fas"
|
||||
class="tree__icon tree__icon--type fas"
|
||||
:class="'fa-' + application._.type.iconClass"
|
||||
></i>
|
||||
<Editable
|
||||
|
|
|
@ -1,87 +1,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<Notifications></Notifications>
|
||||
<SettingsModal ref="settingsModal"></SettingsModal>
|
||||
<div :class="{ 'layout--collapsed': isCollapsed }" class="layout">
|
||||
<div class="layout__col-1 menu">
|
||||
<ul class="menu__items">
|
||||
<li class="menu__item">
|
||||
<nuxt-link :to="{ name: 'dashboard' }" class="menu__link">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span class="menu__link-text">Dashboard</span>
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li class="menu__item">
|
||||
<a
|
||||
ref="groupSelectToggle"
|
||||
class="menu__link"
|
||||
@click="$refs.groupSelect.toggle($refs.groupSelectToggle)"
|
||||
>
|
||||
<i class="fas fa-layer-group"></i>
|
||||
<span class="menu__link-text">Groups</span>
|
||||
</a>
|
||||
<GroupsContext ref="groupSelect"></GroupsContext>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="menu__items">
|
||||
<li class="menu__item layout__uncollapse">
|
||||
<a class="menu__link" @click="toggleCollapsed()">
|
||||
<i class="fas fa-angle-double-right"></i>
|
||||
<span class="menu__link-text">Uncollapse</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu__item">
|
||||
<a
|
||||
class="menu__link menu__user-item"
|
||||
@click="$refs.userContext.toggle($event.target)"
|
||||
>
|
||||
{{ name | nameAbbreviation }}
|
||||
<span class="menu__link-text">{{ name }}</span>
|
||||
</a>
|
||||
<Context ref="userContext">
|
||||
<div class="context__menu-title">{{ name }}</div>
|
||||
<ul class="context__menu">
|
||||
<li>
|
||||
<a
|
||||
@click="
|
||||
;[
|
||||
$refs.settingsModal.show('password'),
|
||||
$refs.userContext.hide(),
|
||||
]
|
||||
"
|
||||
>
|
||||
<i class="context__menu-icon fas fa-fw fa-cogs"></i>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a @click="logoff()">
|
||||
<i class="context__menu-icon fas fa-fw fa-sign-out-alt"></i>
|
||||
Logoff
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Context>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="layout__col-1">
|
||||
<Sidebar></Sidebar>
|
||||
</div>
|
||||
<div class="layout__col-2 sidebar">
|
||||
<div class="sidebar__content-wrapper">
|
||||
<nav class="sidebar__content">
|
||||
<div class="sidebar__title">
|
||||
<img src="@baserow/modules/core/static/img/logo.svg" alt="" />
|
||||
</div>
|
||||
<Sidebar></Sidebar>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="sidebar__footer">
|
||||
<a class="sidebar__collapse" @click="toggleCollapsed()">
|
||||
<i class="fas fa-angle-double-left"></i>
|
||||
Collapse sidebar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__col-3">
|
||||
<div class="layout__col-2">
|
||||
<nuxt />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -89,29 +13,20 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import SettingsModal from '@baserow/modules/core/components/settings/SettingsModal'
|
||||
import Notifications from '@baserow/modules/core/components/notifications/Notifications'
|
||||
import GroupsContext from '@baserow/modules/core/components/group/GroupsContext'
|
||||
import Sidebar from '@baserow/modules/core/components/sidebar/Sidebar'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsModal,
|
||||
GroupsContext,
|
||||
Notifications,
|
||||
Sidebar,
|
||||
},
|
||||
// Application pages are pages that have the edit sidebar on the left side which
|
||||
// contains the groups and applications. In order to be able to fetch them the user
|
||||
// must be authenticated. And in order to show them we must fetch all the groups and
|
||||
// applications.
|
||||
middleware: ['authenticated', 'groupsAndApplications'],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isCollapsed: 'sidebar/isCollapsed',
|
||||
name: 'auth/getName',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
|
@ -121,14 +36,5 @@ export default {
|
|||
beforeDestroy() {
|
||||
this.$realtime.disconnect()
|
||||
},
|
||||
methods: {
|
||||
logoff() {
|
||||
this.$store.dispatch('auth/logoff')
|
||||
this.$nuxt.$router.push({ name: 'login' })
|
||||
},
|
||||
...mapActions({
|
||||
toggleCollapsed: 'sidebar/toggleCollapsed',
|
||||
}),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@ export default async function GroupsAndApplications({ store, req, app }) {
|
|||
if (process.server && !req) return
|
||||
|
||||
// Get the selected group id
|
||||
const groupId = getGroupCookie(app)
|
||||
let groupId = getGroupCookie(app)
|
||||
|
||||
// If the groups haven't already been selected we will
|
||||
if (store.getters['auth/isAuthenticated']) {
|
||||
|
@ -17,6 +17,12 @@ export default async function GroupsAndApplications({ store, req, app }) {
|
|||
if (!store.getters['group/isLoaded']) {
|
||||
await store.dispatch('group/fetchAll')
|
||||
|
||||
// If the user only has one group we then that one must be selected.
|
||||
const groups = store.getters['group/getAll']
|
||||
if (store.getters['group/getAll'].length === 1) {
|
||||
groupId = groups[0].id
|
||||
}
|
||||
|
||||
// If there is a groupId cookie we will select that group.
|
||||
if (groupId) {
|
||||
try {
|
||||
|
|
|
@ -29,17 +29,9 @@ export default {
|
|||
|
||||
this.setLoading(group, false)
|
||||
},
|
||||
async selectGroup(group) {
|
||||
this.setLoading(group, true)
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('group/select', group)
|
||||
this.$emit('selected')
|
||||
} catch (error) {
|
||||
notifyIf(error, 'group')
|
||||
}
|
||||
|
||||
this.setLoading(group, false)
|
||||
selectGroup(group) {
|
||||
this.$store.dispatch('group/select', group)
|
||||
this.$emit('selected')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="layout__col-3-scroll">
|
||||
<div class="layout__col-2-scroll">
|
||||
<div
|
||||
class="alert alert--simple alert--warning alert--has-icon dashboard__alert"
|
||||
>
|
||||
|
|
|
@ -208,6 +208,9 @@ export const getters = {
|
|||
get: (state) => (id) => {
|
||||
return state.items.find((item) => item.id === id)
|
||||
},
|
||||
getAll(state) {
|
||||
return state.items
|
||||
},
|
||||
hasSelected(state) {
|
||||
return Object.prototype.hasOwnProperty.call(state.selected, 'id')
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<header class="layout__col-3-1 header">
|
||||
<header class="layout__col-2-1 header">
|
||||
<div v-show="tableLoading" class="header__loading"></div>
|
||||
<ul v-if="!tableLoading" class="header__filter">
|
||||
<li class="header__filter-item header__filter-item--grids">
|
||||
|
@ -69,7 +69,7 @@
|
|||
<li>{{ table.name }}</li>
|
||||
</ul>
|
||||
</header>
|
||||
<div class="layout__col-3-2 content">
|
||||
<div class="layout__col-2-2 content">
|
||||
<component
|
||||
:is="getViewComponent(view)"
|
||||
v-if="hasSelectedView && !tableLoading"
|
||||
|
|
Loading…
Add table
Reference in a new issue