1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-13 08:41:46 +00:00

New left sidebar

This commit is contained in:
Bram Wiepjes 2024-07-31 13:41:49 +00:00
parent 00ae305039
commit 04e1a69c03
55 changed files with 1651 additions and 1751 deletions

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Redesigned the left sidebar.",
"issue_number": null,
"bullet_points": [],
"created_at": "2024-07-28"
}

View file

@ -1,11 +1,20 @@
<template>
<li
<nuxt-link
v-if="hasPermission"
v-slot="{ href, navigate, isExactActive }"
:to="{
name: 'workspace-audit-log',
params: {
workspaceId: workspace.id,
},
}"
>
<li
class="tree__item"
:class="{
'tree__item--loading': loading,
'tree__action--deactivated': deactivated,
active: $route.matched.some(({ name }) => name === 'workspace-audit-log'),
active: isExactActive,
}"
>
<div class="tree__action">
@ -15,25 +24,17 @@
class="tree__link"
@click.prevent="$refs.enterpriseModal.show()"
>
<i class="tree__icon tree__icon--type iconoir-lock"></i>
<i class="tree__icon iconoir-lock"></i>
<span class="tree__link-text">{{
$t('auditLogSidebarWorkspace.title')
}}</span>
</a>
<nuxt-link
v-else
:event="!hasPermission ? null : 'click'"
class="tree__link"
:to="{
name: 'workspace-audit-log',
params: { workspaceId: workspace.id },
}"
>
<i class="tree__icon tree__icon--type baserow-icon-history"></i>
<a v-else :href="href" class="tree__link" @click="navigate">
<i class="tree__icon baserow-icon-history"></i>
<span class="tree__link-text">{{
$t('auditLogSidebarWorkspace.title')
}}</span>
</nuxt-link>
</a>
</div>
<EnterpriseModal
ref="enterpriseModal"
@ -41,6 +42,7 @@
:name="$t('auditLogSidebarWorkspace.title')"
></EnterpriseModal>
</li>
</nuxt-link>
</template>
<script>

View file

@ -13,13 +13,13 @@
class="tree__link"
@click.prevent="$refs.enterpriseModal.show()"
>
<i class="tree__icon tree__icon--type iconoir-lock"></i>
<i class="tree__icon iconoir-lock"></i>
<span class="tree__link-text">{{
$t('chatwootSupportSidebarWorkspace.directSupport')
}}</span>
</a>
<a v-else class="tree__link" @click="open">
<i class="tree__icon tree__icon--type iconoir-chat-bubble-question"></i>
<i class="tree__icon iconoir-chat-bubble-question"></i>
<span class="tree__link-text">{{
$t('chatwootSupportSidebarWorkspace.directSupport')
}}</span>

View file

@ -18,7 +18,7 @@ export class DashboardType extends PremiumAdminType {
}
getIconClass() {
return 'iconoir-candlestick-chart'
return 'iconoir-home-simple'
}
getName() {
@ -49,6 +49,11 @@ export class UsersAdminType extends PremiumAdminType {
return i18n.t('premium.adminType.users')
}
getCategory() {
const { i18n } = this.app
return i18n.t('sidebar.people')
}
getRouteName() {
return 'admin-users'
}
@ -64,7 +69,7 @@ export class WorkspacesAdminType extends PremiumAdminType {
}
getIconClass() {
return 'iconoir-book-stack'
return 'baserow-icon-groups'
}
getName() {
@ -72,6 +77,11 @@ export class WorkspacesAdminType extends PremiumAdminType {
return i18n.t('premium.adminType.workspaces')
}
getCategory() {
const { i18n } = this.app
return i18n.t('sidebar.people')
}
getRouteName() {
return 'admin-workspaces'
}
@ -95,6 +105,11 @@ export class LicensesAdminType extends AdminType {
return i18n.t('premium.adminType.licenses')
}
getCategory() {
const { i18n } = this.app
return i18n.t('sidebar.licenses')
}
getRouteName() {
return 'admin-licenses'
}

View file

@ -11,8 +11,6 @@
@import 'impersonate_warning';
@import 'views/conditional_color_value_provider_form';
@import 'redirect-modal';
@import 'top_sidebar';
@import 'instance_wide_license';
@import 'form_view_survey';
@import 'views/calendar/all';
@import 'active_users';

View file

@ -1,3 +0,0 @@
.instance-wide-license {
@include absolute(4px, 4px, auto, auto);
}

View file

@ -1,7 +0,0 @@
.premium-top-sidebar {
position: relative;
.layout--collapsed & {
display: none;
}
}

View file

@ -10,7 +10,7 @@
ref="license"
v-model="values.license"
:error="fieldHasErrors('license')"
rows="6"
:rows="6"
@blur="$v.values.license.$touch()"
/>

View file

@ -0,0 +1,21 @@
<template>
<Badge
v-if="highestLicenseType && highestLicenseType.showInTopSidebarWhenActive()"
v-tooltip="highestLicenseType.getTopSidebarTooltip()"
:color="highestLicenseType.getLicenseBadgeColor()"
bold
>
{{ highestLicenseType.getName() }}</Badge
>
</template>
<script>
export default {
name: 'HighestLicenseTypeBadge',
computed: {
highestLicenseType() {
return this.$highestLicenseType()
},
},
}
</script>

View file

@ -1,5 +1,5 @@
<template>
<div class="premium-top-sidebar">
<div class="sidebar__impersonate">
<div v-if="impersonating" class="impersonate-warning">
{{ $t('premiumTopSidebar.impersonateDescription') }}
<div>
@ -16,18 +16,6 @@
>
</div>
</div>
<Badge
v-if="
highestLicenseType && highestLicenseType.showInTopSidebarWhenActive()
"
v-tooltip="highestLicenseType.getTopSidebarTooltip()"
:color="highestLicenseType.getLicenseBadgeColor()"
class="instance-wide-license"
bold
>
{{ highestLicenseType.getName() }}</Badge
>
</div>
</template>
@ -35,7 +23,7 @@
import { mapGetters } from 'vuex'
export default {
name: 'PremiumTopSidebar',
name: 'Impersonate',
data() {
return {
loading: false,
@ -45,9 +33,6 @@ export default {
...mapGetters({
impersonating: 'impersonating/getImpersonating',
}),
highestLicenseType() {
return this.$highestLicenseType()
},
},
methods: {
resolveAdminUsersHref() {

View file

@ -10,7 +10,7 @@
"dashboard": "Dashboard",
"users": "Users",
"workspaces": "Workspaces",
"licenses": "Licenses"
"licenses": "Manage licenses"
},
"viewType": {
"kanban": "Kanban",
@ -41,7 +41,7 @@
"creating": "creating",
"updating": "updating",
"deleting": "deleting",
"created" : "created",
"created": "created",
"edited": "edited",
"commentTrashed": "This comment has been deleted.",
"errorUserNotCommentAuthorTitle": "Cannot update or delete.",

View file

@ -1,5 +1,6 @@
import { BaserowPlugin } from '@baserow/modules/core/plugins'
import PremiumTopSidebar from '@baserow_premium/components/sidebar/PremiumTopSidebar'
import Impersonate from '@baserow_premium/components/sidebar/Impersonate'
import HighestLicenseTypeBadge from '@baserow_premium/components/sidebar/HighestLicenseTypeBadge'
import BaserowLogoShareLinkOption from '@baserow_premium/components/views/BaserowLogoShareLinkOption'
export class PremiumPlugin extends BaserowPlugin {
@ -7,8 +8,12 @@ export class PremiumPlugin extends BaserowPlugin {
return 'premium'
}
getSidebarTopComponent() {
return PremiumTopSidebar
getImpersonateComponent() {
return Impersonate
}
getHighestLicenseTypeBadge() {
return HighestLicenseTypeBadge
}
getAdditionalShareLinkOptions() {

View file

@ -1,8 +0,0 @@
/**
* Various helper functions which interact with premium baserow components.
*/
export class PremiumUIHelpers {
static sidebarShowsPremiumEnabled(sidebarComponent) {
return sidebarComponent.find('.instance-wide-license').exists()
}
}

View file

@ -1,192 +0,0 @@
import Sidebar from '@baserow/modules/core/components/sidebar/Sidebar'
import { PremiumTestApp } from '@baserow_premium_test/helpers/premiumTestApp'
import { PremiumUIHelpers } from '@baserow_premium_test/helpers/premiumUIHelpers'
import { UIHelpers } from '@baserow/test/helpers/testApp'
describe('Sidebar Premium Features Snapshot tests', () => {
let testApp = null
beforeEach(() => {
testApp = new PremiumTestApp()
testApp.createTestUserInAuthStore()
})
afterEach(() => {
testApp.afterEach()
})
test(
'When user does not have global premium enabled the sidebar does not show a' +
' premium badge',
async () => {
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(
PremiumUIHelpers.sidebarShowsPremiumEnabled(sidebarComponent)
).toBe(false)
}
)
test(
'When user does have global premium enabled the sidebar does show a premium' +
' badge',
async () => {
testApp.giveCurrentUserGlobalPremiumFeatures()
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(
PremiumUIHelpers.sidebarShowsPremiumEnabled(sidebarComponent)
).toBe(true)
}
)
test('A realtime update to global premium is reflected in the badge', async () => {
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(PremiumUIHelpers.sidebarShowsPremiumEnabled(sidebarComponent)).toBe(
false
)
testApp.giveCurrentUserGlobalPremiumFeatures()
await sidebarComponent.vm.$nextTick()
expect(PremiumUIHelpers.sidebarShowsPremiumEnabled(sidebarComponent)).toBe(
true
)
})
test('When user is staff without global premium they dont see a premium badge', async () => {
testApp.updateCurrentUserToBecomeStaffMember()
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(PremiumUIHelpers.sidebarShowsPremiumEnabled(sidebarComponent)).toBe(
false
)
})
test('When user is staff with global premium they see a premium badge', async () => {
testApp.giveCurrentUserGlobalPremiumFeatures()
testApp.updateCurrentUserToBecomeStaffMember()
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(PremiumUIHelpers.sidebarShowsPremiumEnabled(sidebarComponent)).toBe(
true
)
})
test('A non staff user cannot see admin settings links', async () => {
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(UIHelpers.getSidebarItemNames(sidebarComponent)).not.toContain(
'sidebar.admin'
)
})
test('A non staff user with workspace premium cannot see admin settings links', async () => {
testApp.giveCurrentUserPremiumFeatureForSpecificWorkspaceOnly(1)
testApp
.getStore()
.dispatch('workspace/forceCreate', { id: 1, name: 'testWorkspace' })
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(UIHelpers.getSidebarItemNames(sidebarComponent)).not.toContain(
'sidebar.admin'
)
})
test('A staff user can see admin settings links', async () => {
testApp.updateCurrentUserToBecomeStaffMember()
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
expect(UIHelpers.getSidebarItemNames(sidebarComponent)).toContain(
'sidebar.admin'
)
})
test('A staff user without global prem sees premium admin options locked', async () => {
testApp.updateCurrentUserToBecomeStaffMember()
testApp.setRouteToBe('admin-dashboard')
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
await UIHelpers.selectSidebarItem(sidebarComponent, 'sidebar.admin')
expect(UIHelpers.getDisabledSidebarItemNames(sidebarComponent)).toEqual(
expect.arrayContaining([
'premium.adminType.dashboard',
'premium.adminType.users',
'premium.adminType.workspaces',
])
)
})
test('A staff user with global prem sees premium admin options available', async () => {
const openedPage = 'sidebar.admin'
testApp.updateCurrentUserToBecomeStaffMember()
testApp.giveCurrentUserGlobalPremiumFeatures()
testApp.setRouteToBe('admin-dashboard')
const sidebarComponent = await testApp.mount(Sidebar, {
propsData: {
applications: [],
workspaces: [],
selectedWorkspace: {},
},
})
await UIHelpers.selectSidebarItem(sidebarComponent, openedPage)
expect(
UIHelpers.getDisabledSidebarItemNames(sidebarComponent)
).toStrictEqual([])
const sidebarItemNames = UIHelpers.getSidebarItemNames(sidebarComponent)
expect(sidebarItemNames).toEqual(
expect.arrayContaining([
openedPage,
'premium.adminType.dashboard',
'premium.adminType.users',
'premium.adminType.workspaces',
])
)
})
})

View file

@ -19,7 +19,7 @@
"signIn": "Sign in",
"login": "Login",
"logout": "Logout",
"createNew": "Create new",
"createNew": "Add new...",
"create": "Create",
"edit": "Edit",
"change": "Change",
@ -48,9 +48,11 @@
},
"applicationType": {
"database": "Database",
"databases": "Databases",
"databaseDefaultName": "Untitled Database",
"databaseDesc": "Create an organized collection of structured data.",
"builder": "Application",
"builders": "Applications",
"builderDefaultName": "Untitled Application",
"builderDesc": "Easily build websites, web apps and portals without code.",
"cantSelectTableTitle": "Couldn't select the database.",

View file

@ -12,7 +12,7 @@ export class BuilderApplicationType extends ApplicationType {
}
getIconClass() {
return 'iconoir-apple-imac-2021'
return 'baserow-icon-application'
}
getName() {
@ -20,6 +20,11 @@ export class BuilderApplicationType extends ApplicationType {
return i18n.t('applicationType.builder')
}
getNamePlural() {
const { i18n } = this.app
return i18n.t('applicationType.builders')
}
getDescription() {
const { i18n } = this.app
return i18n.t('applicationType.builderDesc')

View file

@ -8,10 +8,7 @@
>
<div class="tree__action">
<a class="tree__link" @click="$emit('selected', application)">
<i
class="tree__icon tree__icon--type"
:class="application._.type.iconClass"
></i>
<i class="tree__icon" :class="application._.type.iconClass"></i>
<span class="tree__link-text">{{ application.name }}</span>
</a>
</div>

View file

@ -14,8 +14,6 @@
v-sortable="{
id: page.id,
update: orderPages,
marginLeft: 34,
marginRight: 10,
marginTop: -1.5,
enabled: $hasPermission(
'builder.order_pages',
@ -27,15 +25,6 @@
:page="page"
></SidebarItemBuilder>
</ul>
<ul v-if="pendingJobs.length" class="tree__subs">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs"
:key="job.id"
:job="job"
>
</component>
</ul>
<a
v-if="
$hasPermission(
@ -47,7 +36,7 @@
class="tree__sub-add"
@click="$refs.createPageModal.show()"
>
<i class="iconoir-plus"></i>
<i class="tree__sub-add-icon iconoir-plus"></i>
{{ $t('sidebarComponentBuilder.createPage') }}
</a>
<CreatePageModal
@ -62,7 +51,6 @@
<script>
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
import BuilderSettingsModal from '@baserow/modules/builder/components/settings/BuilderSettingsModal'
import { mapGetters } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import SidebarItemBuilder from '@baserow/modules/builder/components/sidebar/SidebarItemBuilder'
@ -73,7 +61,6 @@ export default {
components: {
CreatePageModal,
SidebarItemBuilder,
BuilderSettingsModal,
SidebarApplication,
},
props: {
@ -89,20 +76,12 @@ export default {
computed: {
...mapGetters({
isAppSelected: 'application/isSelected',
allJobs: 'job/getAll',
}),
orderedPages() {
return this.application.pages
.map((page) => page)
.sort((a, b) => a.order - b.order)
},
pendingJobs() {
return this.allJobs.filter((job) =>
this.$registry
.get('job', job.type)
.isJobPartOfApplication(job, this.application)
)
},
},
methods: {
selected(application) {
@ -123,9 +102,6 @@ export default {
notifyIf(error, 'page')
}
},
getPendingJobComponent(job) {
return this.$registry.get('job', job.type).getSidebarComponent()
},
},
}
</script>

View file

@ -16,7 +16,7 @@
},
"sidebarComponentBuilder": {
"settings": "Settings",
"createPage": "Create page"
"createPage": "New page"
},
"builderSettingsModal": {
"title": "Application"

View file

@ -24,6 +24,15 @@ export class AdminType extends Registerable {
return null
}
/**
* A human readable name of the admin type category. This admin type is grouped by
* the category in the left sidebar.
*/
getCategory() {
const { i18n } = this.app
return i18n.t('sidebar.general')
}
/**
* The order value used to sort admin types in the sidebar menu.
*/

View file

@ -23,6 +23,10 @@ export class ApplicationType extends Registerable {
return null
}
getNamePlural() {
return null
}
/**
* Small description of the application type, shown in the create new application
* context menu.

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 17.5V6.5C3.5 5.39543 4.39543 4.5 5.5 4.5H18.5C19.6046 4.5 20.5 5.39543 20.5 6.5V17.5C20.5 18.6046 19.6046 19.5 18.5 19.5H5.5C4.39543 19.5 3.5 18.6046 3.5 17.5Z" stroke="black" stroke-width="1.4"/>
<path d="M3.5 8.5L20.5 8.5" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

(image error) Size: 425 B

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 10.35C13.8075 10.35 15.2727 8.87254 15.2727 7.05C15.2727 5.22746 13.8075 3.75 12 3.75C10.1925 3.75 8.72729 5.22746 8.72729 7.05C8.72729 8.87254 10.1925 10.35 12 10.35Z" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.27273 20.25C8.0802 20.25 9.54545 18.7725 9.54545 16.95C9.54545 15.1275 8.0802 13.65 6.27273 13.65C4.46525 13.65 3 15.1275 3 16.95C3 18.7725 4.46525 20.25 6.27273 20.25Z" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.7273 20.25C19.5348 20.25 21 18.7725 21 16.95C21 15.1275 19.5348 13.65 17.7273 13.65C15.9198 13.65 14.4546 15.1275 14.4546 16.95C14.4546 18.7725 15.9198 20.25 17.7273 20.25Z" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

(image error) Size: 903 B

View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5 8.75L12 5.25L8.5 8.75" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.5 15.25L12 18.75L8.5 15.25" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

(image error) Size: 348 B

View file

@ -1,5 +1,5 @@
%badge-cyan {
background: $palette-cyan-50 !important;
background: $palette-cyan-50;
color: $palette-cyan-800;
.badge__indicator {
@ -8,7 +8,7 @@
}
%badge-green {
background: $palette-green-50 !important;
background: $palette-green-50;
color: $palette-green-800;
.badge__indicator {
@ -17,7 +17,7 @@
}
%badge-purple {
background: $palette-purple-50 !important;
background: $palette-purple-50;
color: $palette-purple-800;
.badge__indicator {
@ -26,7 +26,7 @@
}
%badge-red {
background: $palette-red-50 !important;
background: $palette-red-50;
color: $palette-red-800;
.badge__indicator {
@ -35,7 +35,7 @@
}
%badge-magenta {
background: $palette-magenta-50 !important;
background: $palette-magenta-50;
color: $palette-magenta-800;
.badge__indicator {
@ -44,7 +44,7 @@
}
%badge-yellow {
background: $palette-yellow-50 !important;
background: $palette-yellow-50;
color: $palette-yellow-800;
.badge__indicator {
@ -53,7 +53,7 @@
}
%badge-neutral {
background: $palette-neutral-50 !important;
background: $palette-neutral-50;
color: $palette-neutral-800;
.badge__indicator {

View file

@ -1,5 +1,4 @@
.badge {
background: $palette-blue-500;
display: inline-flex;
padding: 6px 10px;
align-items: center;

View file

@ -84,7 +84,7 @@
padding: 8px 10px;
user-select: none;
@include rounded($rounded);
@include rounded($rounded-md);
@include flex-align-items(6px);
&.disabled {
@ -98,7 +98,7 @@
}
&:hover {
background-color: rgba($palette-neutral-1300, 0.04);
background-color: $palette-neutral-100;
text-decoration: none;
&.disabled {

View file

@ -127,7 +127,7 @@
}
}
.sidebar__collapse {
.modal__collapse {
@extend %modal-close;
margin-right: 4px;

View file

@ -1,161 +1,170 @@
.sidebar {
@include absolute(0);
overflow-y: auto;
background-color: $color-neutral-10;
background-color: $color-neutral-50;
border-right: solid 1px $color-neutral-200;
height: 100%;
display: flex;
flex-direction: column;
}
.layout--collapsed & {
overflow: visible;
.sidebar__section {
padding: 8px;
flex: 0 0;
&:not(:last-child) {
border-bottom: solid 1px $palette-neutral-200;
}
&.sidebar__section--scrollable {
flex: 1 1;
min-height: 0;
padding: 0;
display: flex;
flex-direction: column;
}
&.sidebar__section--bottom {
margin-top: auto;
}
}
.sidebar__inner {
position: relative;
min-height: 100%;
padding-bottom: 46px;
.sidebar__section-scrollable {
overflow-y: auto;
min-height: 0;
}
.layout--collapsed & {
padding-bottom: 56px;
.sidebar__section-scrollable-inner {
padding: 8px;
}
.sidebar__workspaces-selector {
@include flex-align-items(10px);
border-bottom: 1px solid $palette-neutral-200;
padding: 0 16px;
height: 51px;
color: $palette-neutral-1200;
font-weight: 500;
&:hover {
text-decoration: none;
}
}
.sidebar__workspaces-selector-selected-workspace {
@extend %ellipsis;
flex: 1;
}
.sidebar__workspaces-selector-icon {
font-size: 18px;
color: $palette-neutral-600;
}
.sidebar__user {
display: flex;
align-items: center;
width: 100%;
padding: 16px;
margin-bottom: 4px;
gap: 12px;
&:hover {
background-color: $color-neutral-100;
text-decoration: none;
}
.layout--collapsed & {
padding: 8px;
}
}
.sidebar__user-info {
width: 100%;
min-width: 0;
.layout--collapsed & {
display: none;
}
}
.sidebar__user-info-top {
@include flex-align-items(4px);
justify-items: center;
margin-bottom: 4px;
}
.sidebar__user-name {
@extend %ellipsis;
min-width: 0;
line-height: 22px;
color: $color-neutral-900;
}
.sidebar__user-icon {
color: $color-neutral-900;
border-top: solid 1px $palette-neutral-200;
background: $palette-neutral-25;
padding-bottom: 8px;
border-bottom-left-radius: $rounded-md;
border-bottom-right-radius: $rounded-md;
}
.sidebar__user-email {
@extend %ellipsis;
font-size: 12px;
line-height: 1.25;
color: $color-neutral-600;
font-weight: 500;
font-size: 11px;
color: $palette-neutral-900;
padding-left: 8px;
}
.sidebar__nav {
padding: 0 10px;
.sidebar__user-info {
@include flex-align-items;
.layout--collapsed & {
padding: 0 8px;
padding: 8px 8px 0;
gap: 18px;
}
.sidebar__user-license {
margin-left: auto;
margin-right: 8px;
&.badge.badge--neutral {
background-color: $palette-neutral-200;
}
}
.sidebar__new-wrapper {
margin-top: 12px;
padding: 14px;
}
.sidebar__new-wrapper--separator {
border-top: solid 1px $palette-neutral-200;
}
.sidebar__new {
font-size: 13px;
color: $color-neutral-400;
padding-left: 6px;
font-weight: 500;
line-height: 17px;
color: $palette-neutral-900;
@include flex-align-items(4px);
&:hover {
color: $color-neutral-500;
color: $palette-neutral-1200;
text-decoration: none;
}
}
.sidebar__new-icon {
font-size: 18px;
font-size: 16px;
color: $palette-neutral-600;
.sidebar__new:hover & {
color: $palette-neutral-1200;
}
}
.sidebar__unread-notifications-icon {
width: 8px;
height: 8px;
border-radius: 100%;
background-color: $color-primary-500;
margin-left: 4px;
}
.sidebar__impersonate {
position: relative;
}
.sidebar__foot {
@include absolute(auto, 0, 0, 0);
height: 31px;
padding: 8px;
display: flex;
width: 100%;
padding: 0 16px 16px;
align-items: center;
justify-content: space-between;
.layout--collapsed & {
flex-direction: column;
height: 56px;
padding: 0 8px 8px;
}
// Is needed temporarily while the undo redo functionality is behind the
// `undo` feature flag.
&.sidebar__foot--with-undo-redo {
.layout--collapsed & {
height: 114px;
}
&.sidebar__foot--collapsed {
padding-left: 0;
padding-right: 0;
justify-content: center;
}
}
.sidebar__foot-links {
display: flex;
.layout--collapsed & {
flex-direction: column;
}
gap: 16px;
}
.sidebar__foot-link {
position: relative;
color: $color-neutral-700;
color: $palette-neutral-700;
@include center-text(20px, 12px);
@include rounded($rounded);
@include center-text(16px, 16px);
&:hover {
display: inline-block;
text-decoration: none;
background-color: $color-neutral-100;
}
&:not(:first-child) {
margin-left: 6px;
.layout--collapsed & {
margin-left: auto;
margin-top: 8px;
}
color: $palette-neutral-1200;
}
&.sidebar__foot-link--loading {
@ -182,54 +191,89 @@
font-size: 18px;
}
.layout--collapsed {
// Some minor changes regarding the tree items within the collapsed sidebar.
.tree .sidebar__tree {
padding-left: 0;
}
.sidebar__action {
.tree__link {
.sidebar__workspace-active-icon {
text-align: center;
}
.tree__icon {
margin-right: 0;
}
.sidebar__item-name {
background-color: $color-neutral-900;
color: $white;
padding: 0 4px;
white-space: nowrap;
font-weight: 400;
display: none;
flex: 1;
@include absolute(6px, auto, auto, 36px);
@include center-text(auto, 11px, 21px);
@include rounded($rounded);
}
&:hover .sidebar__item-name {
display: block;
}
}
.sidebar__logo {
display: inline-block;
order: 2;
width: 18px;
overflow: hidden;
}
}
.sidebar__unread-notifications-icon {
color: $palette-neutral-1200;
position: absolute;
top: 12px;
right: 32px;
width: 8px;
height: 8px;
border-radius: 100%;
background-color: $color-primary-500;
top: 50%;
right: 5px;
font-size: 14px;
transform: translateY(-50%);
}
.dashboard__user-workspaces.select__items {
padding: 6px;
.select__item {
&.active:not(.select__item--loading) {
background-color: transparent;
}
}
}
.dashboard__user-workspace-avatar {
margin-right: 4px;
}
.sidebar__back-icon {
font-size: 14px;
color: $palette-neutral-700;
}
.sidebar__head {
@include flex-align-items(15px);
border-bottom: 1px solid $palette-neutral-200;
padding: 0 21px;
height: 51px;
color: $palette-neutral-1200;
font-weight: 500;
}
.sidebar__back {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
color: $palette-neutral-1200;
@include rounded($rounded-md);
&:hover {
background-color: rgba($palette-neutral-1300, 0.04);
}
}
.sidebar__title {
font-size: 13px;
font-weight: 500;
color: $palette-neutral-1200;
}
.sidebar__logo {
.sidebar__foot--collapsed & {
display: none;
}
}
.sidebar__collapse-link {
font-size: 14px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
color: $palette-neutral-1200;
@include rounded($rounded-md);
&:hover {
background-color: rgba($palette-neutral-1300, 0.04);
}
}
.sidebar__item-count {
margin-left: auto;
color: $palette-neutral-700;
}

View file

@ -2,7 +2,11 @@
position: relative;
list-style: none;
padding: 0;
margin: 0 0 12px;
margin: 0;
&:not(:last-child) {
margin-bottom: 12px;
}
.tree__item & {
padding-left: 8px;
@ -10,27 +14,23 @@
}
}
.tree:not(:last-child) {
margin-bottom: 3px;
}
.tree__item {
@extend %first-last-no-margin;
position: relative;
margin: 4px 0;
margin: 2px 0;
@include rounded($rounded);
@include rounded($rounded-md);
&.active {
background-color: $color-primary-100;
background-color: rgba($palette-neutral-1300, 0.04);
}
&.tree__item--loading::after {
content: ' ';
@include loading(14px);
@include absolute(9px, 9px, auto, auto);
@include absolute(8px, 9px, auto, auto);
}
}
@ -41,7 +41,7 @@
.tree__action {
@extend %tree-size;
padding: 0 6px;
padding: 0 8px;
@include rounded($rounded);
@ -62,7 +62,7 @@
}
&:not(.tree__action--disabled):hover {
background-color: $color-neutral-100;
background-color: rgba($palette-neutral-1300, 0.04);
}
.tree__item.active &:hover {
@ -74,22 +74,17 @@
&.tree__action--has-right-icon {
padding-right: 32px;
}
&.tree__action--has-notification {
padding-right: 48px;
}
}
.tree__link {
@extend %tree-size;
color: $color-neutral-900;
font-size: 14px;
color: $palette-neutral-900;
@include flex-align-items(4px);
@include flex-align-items(8px);
.tree__action--deactivated & {
color: $color-neutral-500;
color: $palette-neutral-500;
}
&:hover {
@ -119,6 +114,12 @@
@extend %ellipsis;
min-width: 0;
color: $palette-neutral-900;
font-weight: 500;
.active & {
color: $palette-neutral-1200;
}
}
.tree__progress-percentage {
@ -132,16 +133,17 @@
@extend %tree-size;
text-align: center;
color: $color-neutral-900;
color: $palette-neutral-700;
font-size: 16px;
&.tree__icon--type {
color: $color-neutral-500;
.active & {
color: $palette-neutral-1200;
}
}
%tree-sub-size {
line-height: 28px;
height: 28px;
line-height: 32px;
height: 32px;
}
.tree__subs {
@ -155,29 +157,33 @@
@extend %tree-sub-size;
position: relative;
padding: 0 34px;
margin: 2px 0 0 21px;
@include rounded($rounded-md);
&:hover,
&.active {
background-color: rgba($palette-neutral-1300, 0.04);
}
&::before,
&::after {
content: '';
position: absolute;
left: 12px;
left: -8px;
}
&::before {
top: 0;
height: 28px;
border-left: 1px solid $color-neutral-200;
height: 32px;
border-right: 1px solid $palette-neutral-200;
}
&::after {
top: 14px;
width: 12px;
border-bottom: 1px solid $color-neutral-200;
}
&:not(:last-child) {
margin-bottom: 2px;
&:last-child::before {
height: 15px;
&::before {
height: 34px;
}
}
}
@ -193,18 +199,18 @@
@extend %tree-sub-size;
@extend %ellipsis;
color: $color-neutral-900;
color: $palette-neutral-900;
display: block;
font-weight: 500;
padding: 0 32px 0 16px;
&:hover {
cursor: pointer;
text-decoration: none;
color: $color-primary-500;
}
.active > & {
font-weight: 600;
color: $color-primary-600;
color: $palette-neutral-1200;
}
&--empty::before {
@ -237,13 +243,24 @@
.tree__sub-add {
display: inline-block;
margin: 0 0 10px 10px;
margin: 10px 0 10px 6px;
line-height: 17px;
font-size: 12px;
color: $color-neutral-400;
font-weight: 500;
color: $palette-neutral-900;
&:hover {
text-decoration: none;
color: $color-neutral-500;
color: $palette-neutral-1200;
}
}
.tree__sub-add-icon {
font-size: 16px;
color: $palette-neutral-600;
.tree__sub-add:hover & {
color: $palette-neutral-1200;
}
}
@ -287,3 +304,10 @@
top: 8px;
right: 8px;
}
.tree__heading {
font-size: 11px;
font-weight: 500;
color: $palette-neutral-900;
margin: 8px;
}

View file

@ -76,4 +76,5 @@ $baserow-icons: 'circle-empty', 'circle-checked', 'check-square', 'formula',
'file-audio', 'file-video', 'file-code', 'tablet', 'form', 'file-excel',
'kanban', 'file-word', 'file-archive', 'gallery', 'file-powerpoint',
'calendar', 'smile', 'smartphone', 'plus', 'heading-1', 'heading-2',
'heading-3', 'paragraph', 'ordered-list', 'enlarge', 'share', 'settings';
'heading-3', 'paragraph', 'ordered-list', 'enlarge', 'share', 'settings',
'up-down-arrows', 'application', 'groups';

View file

@ -45,7 +45,7 @@
<a
v-if="collapsibleRightSidebar"
class="sidebar__collapse"
class="modal__collapse"
@click="collapseSidebar"
>
<i

View file

@ -1,456 +1,99 @@
<template>
<div class="sidebar">
<div class="sidebar__inner">
<component
:is="component"
v-for="(component, index) in sidebarTopComponents"
v-for="(component, index) in impersonateComponent"
:key="index"
></component>
<template v-if="showAdmin">
<div class="sidebar__head">
<a href="#" class="sidebar__back" @click="setShowAdmin(false)">
<i class="sidebar__back-icon iconoir-nav-arrow-left"></i>
</a>
<div class="sidebar__title">
{{ $t('sidebar.adminSettings') }}
</div>
</div>
<SidebarAdmin></SidebarAdmin>
</template>
<template v-if="!showAdmin">
<a
ref="userContextAnchor"
class="sidebar__user"
ref="workspaceContextAnchor"
class="sidebar__workspaces-selector"
@click="
$refs.userContext.toggle(
$refs.userContextAnchor,
$refs.workspacesContext.toggle(
$refs.workspaceContextAnchor,
'bottom',
'left',
isCollapsed ? -4 : -10,
isCollapsed ? 8 : 16
8,
16
)
"
>
<Avatar
rounded
:initials="name | nameAbbreviation"
:size="avatarSize"
:initials="selectedWorkspace.name || name | nameAbbreviation"
></Avatar>
<div class="sidebar__user-info">
<div class="sidebar__user-info-top">
<div class="sidebar__user-name">{{ name }}</div>
<i class="sidebar__user-icon iconoir-nav-arrow-down"></i>
</div>
<div class="sidebar__user-email">{{ email }}</div>
</div>
</a>
<Context
ref="userContext"
:overflow-scroll="true"
:max-height-if-outside-viewport="true"
>
<div class="context__menu-title">{{ name }}</div>
<ul class="context__menu">
<li class="context__menu-item">
<a
class="context__menu-item-link"
@click=";[$refs.settingsModal.show(), $refs.userContext.hide()]"
>
<i class="context__menu-item-icon iconoir-settings"></i>
{{ $t('sidebar.settings') }}
</a>
<SettingsModal ref="settingsModal"></SettingsModal>
</li>
<li class="context__menu-item">
<a
class="context__menu-item-link"
:class="{ 'context__menu-item-link--loading': logoffLoading }"
@click="logoff()"
>
<i class="context__menu-item-icon iconoir-log-out"></i>
{{ $t('sidebar.logoff') }}
</a>
</li>
</ul>
</Context>
<div class="sidebar__nav">
<ul class="tree">
<component
:is="component"
v-for="(component, index) in sidebarMainMenuComponents"
:key="index"
></component>
<li class="tree__item">
<div class="tree__action sidebar__action">
<a class="tree__link" @click="$refs.trashModal.show()">
<i class="tree__icon iconoir-bin"></i>
<span class="tree__link-text">
<span class="sidebar__item-name">{{
$t('sidebar.trash')
<span class="sidebar__workspaces-selector-selected-workspace">{{
selectedWorkspace.name || name
}}</span>
</span>
</a>
<TrashModal ref="trashModal"></TrashModal>
</div>
</li>
<li v-if="isStaff" class="tree__item">
<div
class="tree__action sidebar__action"
:class="{ 'tree__action--disabled': isAdminPage }"
>
<a class="tree__link" @click.prevent="admin()">
<i class="tree__icon iconoir-settings"></i>
<span class="tree__link-text">
<span class="sidebar__item-name">{{
$t('sidebar.admin')
}}</span>
</span>
</a>
</div>
<ul v-show="isAdminPage" class="tree sidebar__tree">
<SidebarAdminItem
v-for="adminType in sortedAdminTypes"
:key="adminType.type"
:admin-type="adminType"
>
</SidebarAdminItem>
</ul>
</li>
<template v-if="hasSelectedWorkspace && !isCollapsed">
<li class="tree__item margin-top-2">
<div
:title="selectedWorkspace.name"
class="tree__action tree__action--has-options"
:class="{
'tree__action--has-notification':
unreadNotificationsInOtherWorkspaces,
}"
>
<a
ref="workspaceSelectToggle"
class="tree__link tree__link--group"
@click="
$refs.workspaceSelect.toggle(
$refs.workspaceSelectToggle,
'bottom',
'left',
0
)
"
>
<span class="tree__link-text">
<Editable
ref="rename"
:value="selectedWorkspace.name"
@change="renameWorkspace(selectedWorkspace, $event)"
></Editable>
</span>
</a>
<span
v-if="unreadNotificationsInOtherWorkspaces"
class="sidebar__unread-notifications-icon"
></span>
<i
class="sidebar__workspaces-selector-icon baserow-icon-up-down-arrows"
></i>
</a>
<SidebarUserContext
ref="workspacesContext"
:workspaces="workspaces"
:selected-workspace="selectedWorkspace"
@selected-workspace="$emit('selected-workspace', $event)"
@toggle-admin="setShowAdmin($event)"
></SidebarUserContext>
<a
ref="contextLink"
class="tree__options"
@click="
$refs.context.toggle(
$refs.contextLink,
'bottom',
'right',
0
)
"
>
<i class="baserow-icon-more-vertical"></i>
</a>
<SidebarMenu
v-if="hasSelectedWorkspace"
:selected-workspace="selectedWorkspace"
></SidebarMenu>
<WorkspacesContext ref="workspaceSelect"></WorkspacesContext>
<WorkspaceContext
ref="context"
:workspace="selectedWorkspace"
@rename="enableRename()"
></WorkspaceContext>
</div>
</li>
<nuxt-link
v-slot="{ href, navigate, isExactActive }"
:to="{
name: 'workspace',
params: {
workspaceId: selectedWorkspace.id,
},
}"
>
<li
data-highlight="members"
class="tree__item"
:class="{ active: isExactActive }"
>
<div class="tree__action">
<a :href="href" class="tree__link" @click="navigate">
<i
class="tree__icon tree__icon--type iconoir-home-alt-slim-horiz"
></i>
<span class="tree__link-text">{{
$t('sidebar.home')
}}</span>
</a>
</div>
</li>
</nuxt-link>
<li class="tree__item">
<div class="tree__action tree__action--has-counter">
<a
class="tree__link"
@click="$refs.notificationPanel.toggle($event.currentTarget)"
>
<i class="tree__icon tree__icon--type iconoir-bell"></i>
<span class="tree__link-text">{{
$t('sidebar.notifications')
}}</span>
</a>
<BadgeCounter
v-show="unreadNotificationCount"
class="tree__counter"
:count="unreadNotificationCount"
:limit="10"
>
</BadgeCounter>
</div>
<NotificationPanel ref="notificationPanel" />
</li>
<li
v-if="
$hasPermission(
'workspace.create_invitation',
selectedWorkspace,
selectedWorkspace.id
)
"
class="tree__item"
>
<div class="tree__action">
<a class="tree__link" @click="$refs.inviteModal.show()">
<i class="tree__icon tree__icon--type iconoir-add-user"></i>
<span class="tree__link-text">{{
$t('sidebar.inviteOthers')
}}</span>
</a>
</div>
<WorkspaceMemberInviteModal
ref="inviteModal"
:workspace="selectedWorkspace"
@invite-submitted="handleInvite"
/>
</li>
<nuxt-link
v-if="
$hasPermission(
'workspace.list_workspace_users',
selectedWorkspace,
selectedWorkspace.id
)
"
v-slot="{ href, navigate, isExactActive }"
:to="{
name: 'settings-members',
params: {
workspaceId: selectedWorkspace.id,
},
}"
>
<li
data-highlight="members"
class="tree__item"
:class="{ active: isExactActive }"
>
<div class="tree__action">
<a :href="href" class="tree__link" @click="navigate">
<i
class="tree__icon tree__icon--type iconoir-community"
></i>
<span class="tree__link-text">{{
$t('sidebar.members')
}}</span>
</a>
</div>
</li>
</nuxt-link>
<component
:is="component"
v-for="(component, index) in sidebarWorkspaceComponents"
:key="'sidebarWorkspaceComponents' + index"
:workspace="selectedWorkspace"
></component>
<ul class="tree" data-highlight="applications">
<component
:is="getApplicationComponent(application)"
v-for="application in orderedApplicationsInSelectedWorkspace"
:key="application.id"
v-sortable="{
id: application.id,
update: orderApplications,
handle: '[data-sortable-handle]',
marginTop: -1.5,
enabled: $hasPermission(
'workspace.order_applications',
selectedWorkspace,
selectedWorkspace.id
),
}"
:application="application"
:workspace="selectedWorkspace"
></component>
</ul>
<ul v-if="pendingJobs.length" class="tree">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs"
:key="job.id"
:job="job"
>
</component>
</ul>
<li class="sidebar__new-wrapper">
<a
v-if="
$hasPermission(
'workspace.create_application',
selectedWorkspace,
selectedWorkspace.id
)
"
ref="createApplicationContextLink"
class="sidebar__new"
@click="
$refs.createApplicationContext.toggle(
$refs.createApplicationContextLink
)
"
>
<i class="sidebar__new-icon iconoir-plus"></i>
{{ $t('action.createNew') }}
</a>
</li>
<CreateApplicationContext
ref="createApplicationContext"
:workspace="selectedWorkspace"
></CreateApplicationContext>
<SidebarWithWorkspace
v-if="hasSelectedWorkspace"
:applications="applications"
:selected-workspace="selectedWorkspace"
></SidebarWithWorkspace>
<SidebarWithoutWorkspace
v-if="!hasSelectedWorkspace"
:workspaces="workspaces"
@selected-workspace="$emit('selected-workspace', $event)"
></SidebarWithoutWorkspace>
</template>
<template v-else-if="!hasSelectedWorkspace && !isCollapsed">
<li v-if="workspaces.length === 0" class="tree_item margin-top-2">
<p>{{ $t('sidebar.errorNoWorkspace') }}</p>
</li>
<li
v-for="(workspace, index) in workspaces"
:key="workspace.id"
class="tree__item"
:class="{
'margin-top-2': index === 0,
'tree__item--loading': workspace._.additionalLoading,
}"
>
<div
class="tree__action tree__action--has-right-icon tree__action--has-notification"
>
<a
class="tree__link tree__link--group"
@click="$emit('selected-workspace', workspace)"
><span class="tree__link-text">{{ workspace.name }}</span></a
>
<span
v-if="hasUnreadNotifications(workspace.id)"
class="sidebar__unread-notifications-icon"
></span>
<span class="tree__right-icon">
<i class="iconoir-arrow-right"></i>
</span>
</div>
</li>
<li class="sidebar__new-wrapper">
<a
v-if="$hasPermission('create_workspace')"
class="sidebar__new"
@click="$refs.createWorkspaceModal.show()"
>
<i class="iconoir-plus"></i>
{{ $t('sidebar.createWorkspace') }}
</a>
</li>
<CreateWorkspaceModal
ref="createWorkspaceModal"
></CreateWorkspaceModal>
</template>
</ul>
</div>
<div class="sidebar__foot sidebar__foot--with-undo-redo">
<div class="sidebar__logo">
<ExternalLinkBaserowLogo />
</div>
<div class="sidebar__foot-links">
<a
class="sidebar__foot-link"
:class="{
'sidebar__foot-link--loading': undoLoading,
}"
@click="undo(false)"
>
<i class="sidebar__foot-link-icon iconoir-undo"></i>
</a>
<a
class="sidebar__foot-link"
:class="{
'sidebar__foot-link--loading': redoLoading,
}"
@click="redo(false)"
>
<i class="sidebar__foot-link-icon iconoir-redo"></i>
</a>
<a
class="sidebar__foot-link"
@click="$store.dispatch('sidebar/toggleCollapsed')"
>
<i
class="sidebar__foot-link-icon"
:class="{
'iconoir-fast-arrow-right': isCollapsed,
'iconoir-fast-arrow-left': !isCollapsed,
}"
></i>
</a>
</div>
</div>
</div>
<SidebarFoot></SidebarFoot>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import SettingsModal from '@baserow/modules/core/components/settings/SettingsModal'
import SidebarAdminItem from '@baserow/modules/core/components/sidebar/SidebarAdminItem'
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'
import WorkspacesContext from '@baserow/modules/core/components/workspace/WorkspacesContext'
import WorkspaceContext from '@baserow/modules/core/components/workspace/WorkspaceContext'
import CreateWorkspaceModal from '@baserow/modules/core/components/workspace/CreateWorkspaceModal'
import TrashModal from '@baserow/modules/core/components/trash/TrashModal'
import editWorkspace from '@baserow/modules/core/mixins/editWorkspace'
import undoRedo from '@baserow/modules/core/mixins/undoRedo'
import ExternalLinkBaserowLogo from '@baserow/modules/core/components/ExternalLinkBaserowLogo'
import WorkspaceMemberInviteModal from '@baserow/modules/core/components/workspace/WorkspaceMemberInviteModal'
import { logoutAndRedirectToLogin } from '@baserow/modules/core/utils/auth'
import NotificationPanel from '@baserow/modules/core/components/NotificationPanel'
import BadgeCounter from '@baserow/modules/core/components/BadgeCounter'
import SidebarUserContext from '@baserow/modules/core/components/sidebar/SidebarUserContext'
import SidebarWithWorkspace from '@baserow/modules/core/components/sidebar/SidebarWithWorkspace'
import SidebarWithoutWorkspace from '@baserow/modules/core/components/sidebar/SidebarWithoutWorkspace'
import SidebarAdmin from '@baserow/modules/core/components/sidebar/SidebarAdmin'
import SidebarFoot from '@baserow/modules/core/components/sidebar/SidebarFoot'
import SidebarMenu from '@baserow/modules/core/components/sidebar/SidebarMenu'
import SidebarAdminItem from './SidebarAdminItem.vue'
export default {
name: 'Sidebar',
components: {
ExternalLinkBaserowLogo,
SettingsModal,
CreateApplicationContext,
SidebarAdminItem,
SidebarApplication,
WorkspacesContext,
WorkspaceContext,
CreateWorkspaceModal,
TrashModal,
WorkspaceMemberInviteModal,
NotificationPanel,
BadgeCounter,
SidebarAdmin,
SidebarWithoutWorkspace,
SidebarWithWorkspace,
SidebarUserContext,
SidebarMenu,
SidebarFoot,
},
mixins: [editWorkspace, undoRedo],
props: {
applications: {
type: Array,
@ -467,144 +110,43 @@ export default {
},
data() {
return {
logoffLoading: false,
showAdmin: false,
}
},
computed: {
/**
* Because all the applications that belong to the user are in the store we will
* filter on the selected workspace here.
*/
orderedApplicationsInSelectedWorkspace() {
return this.applications
.filter(
(application) =>
application.workspace.id === this.selectedWorkspace.id
)
.sort((a, b) => a.order - b.order)
SidebarAdminItem() {
return SidebarAdminItem
},
adminTypes() {
return this.$registry.getAll('admin')
},
sortedAdminTypes() {
return Object.values(this.adminTypes)
.slice()
.sort((a, b) => a.getOrder() - b.getOrder())
},
sidebarTopComponents() {
impersonateComponent() {
return Object.values(this.$registry.getAll('plugin'))
.map((plugin) => plugin.getSidebarTopComponent())
.map((plugin) => plugin.getImpersonateComponent())
.filter((component) => component !== null)
},
sidebarMainMenuComponents() {
return Object.values(this.$registry.getAll('plugin'))
.map((plugin) => plugin.getSidebarMainMenuComponent())
.filter((component) => component !== null)
},
sidebarWorkspaceComponents() {
return Object.values(this.$registry.getAll('plugin'))
.flatMap((plugin) =>
plugin.getSidebarWorkspaceComponents(this.selectedWorkspace)
)
.filter((component) => component !== null)
},
pendingJobs() {
return this.$store.getters['job/getAll'].filter((job) =>
this.$registry
.get('job', job.type)
.isJobPartOfWorkspace(job, this.selectedWorkspace)
)
},
/**
* Indicates whether the current user is visiting an admin page.
*/
isAdminPage() {
return Object.values(this.adminTypes).some((adminType) => {
return this.$route.matched.some(
({ name }) => name === adminType.routeName
)
})
},
avatarSize() {
return this.isCollapsed ? 'large' : 'x-large'
},
hasSelectedWorkspace() {
return Object.prototype.hasOwnProperty.call(this.selectedWorkspace, 'id')
},
...mapGetters({
isStaff: 'auth/isStaff',
name: 'auth/getName',
email: 'auth/getUsername',
isCollapsed: 'sidebar/isCollapsed',
unreadNotificationCount: 'notification/getUnreadCount',
unreadNotificationsInOtherWorkspaces:
'notification/anyOtherWorkspaceWithUnread',
}),
},
created() {
// Checks whether the rendered page is an admin page. If so, switch the left sidebar
// navigation to the admin.
// this.showAdmin = Object.values(this.$registry.getAll('admin')).some(
// (adminType) => {
// return this.$route.matched.some(
// ({ name }) => name === adminType.routeName
// )
// }
// )
},
methods: {
hasUnreadNotifications(workspaceId) {
return this.$store.getters['notification/workspaceHasUnread'](workspaceId)
},
getApplicationComponent(application) {
return this.$registry
.get('application', application.type)
.getSidebarComponent()
},
getPendingJobComponent(job) {
return this.$registry.get('job', job.type).getSidebarComponent()
},
logoff() {
this.logoffLoading = true
logoutAndRedirectToLogin(
this.$nuxt.$router,
this.$store,
false,
false,
true
)
},
/**
* Called when the user clicks on the admin menu. Because there isn't an
* admin page it will navigate to the route of the first registered admin
* type.
*/
admin() {
// If the user is already on an admin page we don't have to do anything because
// the link is disabled.
if (this.isAdminPage) {
return
}
// We only want to autoselect the first active admin type because the other ones
// can't be selected.
const activated = this.sortedAdminTypes.filter((adminType) => {
return !this.$registry.get('admin', adminType.type).isDeactivated()
})
if (activated.length > 0) {
this.$nuxt.$router.push({ name: activated[0].routeName })
}
},
async orderApplications(order, oldOrder) {
try {
await this.$store.dispatch('application/order', {
workspace: this.selectedWorkspace,
order,
oldOrder,
})
} catch (error) {
notifyIf(error, 'application')
}
},
handleInvite(event) {
if (this.$route.name !== 'settings-invites') {
this.$router.push({
name: 'settings-invites',
params: {
workspaceId: this.selectedWorkspace.id,
},
})
}
setShowAdmin(value) {
this.showAdmin = value
this.$forceUpdate()
},
},
}

View file

@ -0,0 +1,57 @@
<template>
<div class="sidebar__section sidebar__section--scrollable">
<div class="sidebar__section-scrollable">
<div class="sidebar__section-scrollable-inner">
<ul class="tree">
<template v-for="(category, index) in groupedSortedAdminTypes">
<li
:key="category.name"
class="tree__heading"
:class="{ 'margin-top-2': index > 0 }"
>
{{ category.name }}
</li>
<SidebarAdminItem
v-for="adminType in category.items"
:key="adminType.type"
:admin-type="adminType"
>
</SidebarAdminItem>
</template>
</ul>
</div>
</div>
</div>
</template>
<script>
import SidebarAdminItem from '@baserow/modules/core/components/sidebar/SidebarAdminItem.vue'
export default {
name: 'SidebarAdmin',
components: { SidebarAdminItem },
computed: {
adminTypes() {
return this.$registry.getAll('admin')
},
sortedAdminTypes() {
return Object.values(this.adminTypes)
.slice()
.sort((a, b) => a.getOrder() - b.getOrder())
},
groupedSortedAdminTypes() {
const categories = []
this.sortedAdminTypes.forEach((adminType) => {
const categoryName = adminType.getCategory()
let category = categories.find((c) => c.name === categoryName)
if (!category) {
category = { name: categoryName, items: [] }
categories.push(category)
}
category.items.push(adminType)
})
return categories
},
},
}
</script>

View file

@ -2,7 +2,6 @@
<li
class="tree__item"
:class="{
active: application._.selected,
'tree__item--loading': application._.loading,
}"
>
@ -13,10 +12,7 @@
:title="application.name"
@click="$emit('selected', application)"
>
<i
class="tree__icon tree__icon--type"
:class="application._.type.iconClass"
></i>
<i class="tree__icon" :class="application._.type.iconClass"></i>
<span class="tree__link-text">
<template v-if="application.name === ''">&nbsp;</template>
<Editable

View file

@ -2,7 +2,7 @@
<li class="tree__item tree__item--loading">
<div class="tree__action tree__action--disabled">
<a class="tree__link">
<i class="tree__icon tree__icon--type" :class="jobIconClass"></i>
<i class="tree__icon" :class="jobIconClass"></i>
<span class="tree__link-text">{{ jobSidebarText }}</span>
<div class="tree__progress-percentage">
{{ job.progress_percentage }} %

View file

@ -0,0 +1,40 @@
<template>
<div class="sidebar__section sidebar__section--bottom">
<div class="sidebar__foot">
<div class="sidebar__logo">
<ExternalLinkBaserowLogo />
</div>
<div class="sidebar__foot-links">
<a
class="sidebar__foot-link"
:class="{
'sidebar__foot-link--loading': undoLoading,
}"
@click="undo(false)"
>
<i class="sidebar__foot-link-icon iconoir-undo"></i>
</a>
<a
class="sidebar__foot-link"
:class="{
'sidebar__foot-link--loading': redoLoading,
}"
@click="redo(false)"
>
<i class="sidebar__foot-link-icon iconoir-redo"></i>
</a>
</div>
</div>
</div>
</template>
<script>
import undoRedo from '@baserow/modules/core/mixins/undoRedo'
import ExternalLinkBaserowLogo from '@baserow/modules/core/components/ExternalLinkBaserowLogo'
export default {
name: 'SidebarFoot',
components: { ExternalLinkBaserowLogo },
mixins: [undoRedo],
}
</script>

View file

@ -0,0 +1,189 @@
<template>
<div class="sidebar__section">
<ul class="tree">
<nuxt-link
v-slot="{ href, navigate, isExactActive }"
:to="{
name: 'workspace',
params: {
workspaceId: selectedWorkspace.id,
},
}"
>
<li
class="tree__item"
:class="{
active: isExactActive,
}"
>
<div class="tree__action sidebar__action">
<a :href="href" class="tree__link" @click="navigate">
<i class="tree__icon iconoir-home-simple"></i>
<span class="tree__link-text">
<span class="sidebar__item-name">{{ $t('sidebar.home') }}</span>
</span>
</a>
</div>
</li>
</nuxt-link>
<li class="tree__item">
<div class="tree__action tree__action--has-counter">
<a
class="tree__link"
@click="$refs.notificationPanel.toggle($event.currentTarget)"
>
<i class="tree__icon tree__icon--type iconoir-bell"></i>
<span class="tree__link-text">{{
$t('sidebar.notifications')
}}</span>
</a>
<BadgeCounter
v-show="unreadNotificationCount"
class="tree__counter"
:count="unreadNotificationCount"
:limit="10"
>
</BadgeCounter>
</div>
<NotificationPanel ref="notificationPanel" />
</li>
<nuxt-link
v-if="
$hasPermission(
'workspace.list_workspace_users',
selectedWorkspace,
selectedWorkspace.id
)
"
v-slot="{ href, navigate, isExactActive }"
:to="{
name: 'settings-members',
params: {
workspaceId: selectedWorkspace.id,
},
}"
>
<li
class="tree__item"
:class="{
active: isExactActive,
}"
data-highlight="members"
>
<div class="tree__action sidebar__action">
<a :href="href" class="tree__link" @click="navigate">
<i class="tree__icon iconoir-group"></i>
<span class="tree__link-text">
<span class="sidebar__item-name">{{
$t('sidebar.members')
}}</span>
</span>
<span
v-if="selectedWorkspace.users.length"
class="sidebar__item-count"
>
{{ selectedWorkspace.users.length }}</span
>
</a>
</div>
</li>
</nuxt-link>
<li
v-if="
$hasPermission(
'workspace.create_invitation',
selectedWorkspace,
selectedWorkspace.id
)
"
class="tree__item"
>
<div class="tree__action sidebar__action">
<a class="tree__link" @click="$refs.inviteModal.show()">
<i class="tree__icon iconoir-add-user"></i>
<span class="tree__link-text">
<span class="sidebar__item-name">{{
$t('sidebar.inviteOthers')
}}</span>
</span>
</a>
</div>
<WorkspaceMemberInviteModal
ref="inviteModal"
:workspace="selectedWorkspace"
@invite-submitted="handleInvite"
/>
</li>
<component
:is="component"
v-for="(component, index) in sidebarWorkspaceComponents"
:key="'sidebarWorkspaceComponents' + index"
:workspace="selectedWorkspace"
></component>
<li class="tree__item">
<div class="tree__action sidebar__action">
<a class="tree__link" @click="$refs.trashModal.show()">
<i class="tree__icon iconoir-bin"></i>
<span class="tree__link-text">
<span class="sidebar__item-name">{{ $t('sidebar.trash') }}</span>
</span>
</a>
<TrashModal ref="trashModal"></TrashModal>
</div>
</li>
</ul>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import TrashModal from '@baserow/modules/core/components/trash/TrashModal'
import NotificationPanel from '@baserow/modules/core/components/NotificationPanel'
import WorkspaceMemberInviteModal from '@baserow/modules/core/components/workspace/WorkspaceMemberInviteModal'
import BadgeCounter from '@baserow/modules/core/components/BadgeCounter'
export default {
name: 'SidebarMenu',
components: {
TrashModal,
NotificationPanel,
WorkspaceMemberInviteModal,
BadgeCounter,
},
props: {
selectedWorkspace: {
type: Object,
required: true,
},
},
computed: {
sidebarWorkspaceComponents() {
return Object.values(this.$registry.getAll('plugin'))
.flatMap((plugin) =>
plugin.getSidebarWorkspaceComponents(this.selectedWorkspace)
)
.filter((component) => component !== null)
},
...mapGetters({
unreadNotificationCount: 'notification/getUnreadCount',
}),
},
methods: {
handleInvite(event) {
if (this.$route.name !== 'settings-invites') {
this.$router.push({
name: 'settings-invites',
params: {
workspaceId: this.selectedWorkspace.id,
},
})
}
},
},
}
</script>

View file

@ -0,0 +1,177 @@
<template>
<Context :max-height-if-outside-viewport="true" class="select">
<ul
ref="dropdown"
v-auto-overflow-scroll
class="select__items select__items--no-max-height dashboard__user-workspaces"
>
<li
v-for="workspace in workspaces"
:key="workspace.id"
class="select__item"
:class="{
'select__item--loading': workspace._.loading,
active: workspace.id === selectedWorkspace.id,
}"
>
<i
v-if="workspace.id === selectedWorkspace.id"
class="sidebar__workspace-active-icon iconoir-check"
></i>
<a
class="select__item-link"
@click=";[$emit('selected-workspace', workspace), hide()]"
>
<div class="select__item-name">
<Avatar
class="dashboard__user-workspace-avatar"
:initials="workspace.name | nameAbbreviation"
></Avatar>
{{ workspace.name }}
<span
v-if="hasUnreadNotifications(workspace.id)"
class="sidebar__unread-notifications-icon"
></span>
</div>
</a>
</li>
<li class="select__item">
<a class="select__item-link" @click="$refs.createWorkspaceModal.show()">
<div class="select__item-name">
<ButtonIcon
tag="a"
size="small"
type="secondary"
icon="iconoir-plus"
style="pointer-events: none"
/>
<span>{{ $t('sidebar.addNewWorkspace') }}</span>
</div>
</a>
</li>
</ul>
<div class="sidebar__user">
<div class="sidebar__user-info">
<span class="sidebar__user-email">
{{ email }}
</span>
<component
:is="component"
v-for="(component, index) in highestLicenceTypeBadge"
:key="index"
class="sidebar__user-license"
></component>
</div>
<ul class="context__menu margin-bottom-0">
<li v-if="isStaff" class="context__menu-item">
<a class="context__menu-item-link" @click="admin">
<i class="context__menu-item-icon iconoir-settings"></i>
{{ $t('sidebar.adminTools') }}
</a>
</li>
<li class="context__menu-item">
<a
class="context__menu-item-link"
@click="$refs.settingsModal.show()"
>
<i class="context__menu-item-icon iconoir-user-circle"></i>
{{ $t('sidebar.settings') }}
</a>
<SettingsModal ref="settingsModal"></SettingsModal>
</li>
<li
class="context__menu-item context__menu-item--with-separator margin-bottom-0"
>
<a
class="context__menu-item-link"
:class="{ 'context__menu-item-link--loading': logoffLoading }"
@click="logoff()"
>
<i class="context__menu-item-icon iconoir-log-out"></i>
{{ $t('sidebar.logoff') }}
</a>
</li>
</ul>
</div>
<CreateWorkspaceModal ref="createWorkspaceModal"></CreateWorkspaceModal>
</Context>
</template>
<script>
import { mapGetters } from 'vuex'
import { logoutAndRedirectToLogin } from '@baserow/modules/core/utils/auth'
import context from '@baserow/modules/core/mixins/context'
import SettingsModal from '@baserow/modules/core/components/settings/SettingsModal'
import CreateWorkspaceModal from '@baserow/modules/core/components/workspace/CreateWorkspaceModal'
export default {
name: 'SidebarUserContext',
components: { SettingsModal, CreateWorkspaceModal },
mixins: [context],
props: {
workspaces: {
type: Array,
required: true,
},
selectedWorkspace: {
type: Object,
required: true,
},
},
data() {
return {
logoffLoading: false,
}
},
computed: {
highestLicenceTypeBadge() {
return Object.values(this.$registry.getAll('plugin'))
.map((plugin) => plugin.getHighestLicenseTypeBadge())
.filter((component) => component !== null)
},
adminTypes() {
return this.$registry.getAll('admin')
},
sortedAdminTypes() {
return Object.values(this.adminTypes)
.slice()
.sort((a, b) => a.getOrder() - b.getOrder())
},
...mapGetters({
isStaff: 'auth/isStaff',
name: 'auth/getName',
email: 'auth/getUsername',
}),
},
methods: {
logoff() {
this.logoffLoading = true
logoutAndRedirectToLogin(
this.$nuxt.$router,
this.$store,
false,
false,
true
)
},
async admin() {
this.$emit('toggle-admin', true)
this.hide()
const activatedAdminTypes = this.sortedAdminTypes.filter(
(adminType) => !adminType.isDeactivated()
)
try {
await this.$router.push({ name: activatedAdminTypes[0].routeName })
} catch {}
},
hasUnreadNotifications(workspaceId) {
return this.$store.getters['notification/workspaceHasUnread'](workspaceId)
},
},
}
</script>

View file

@ -0,0 +1,183 @@
<template>
<div class="sidebar__section sidebar__section--scrollable">
<div v-if="applicationsCount" class="sidebar__section-scrollable">
<div
class="sidebar__section-scrollable-inner"
data-highlight="applications"
>
<ul v-if="pendingJobs[null].length" class="tree">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs[null]"
:key="job.id"
:job="job"
>
</component>
</ul>
<ul class="tree">
<div
v-for="applicationGroup in groupedApplicationsForSelectedWorkspace"
:key="applicationGroup.type"
>
<template v-if="applicationGroup.applications.length > 0">
<div class="tree__heading">
{{ applicationGroup.name }}
</div>
<ul
class="tree"
:class="{
'margin-bottom-0': pendingJobs[applicationGroup.type].length,
}"
data-highlight="applications"
>
<component
:is="getApplicationComponent(application)"
v-for="application in applicationGroup.applications"
:key="application.id"
v-sortable="{
id: application.id,
update: orderApplications,
handle: '[data-sortable-handle]',
marginTop: -1.5,
enabled: $hasPermission(
'workspace.order_applications',
selectedWorkspace,
selectedWorkspace.id
),
}"
:application="application"
:pending-jobs="pendingJobs[application.type]"
:workspace="selectedWorkspace"
></component>
</ul>
<ul v-if="pendingJobs[applicationGroup.type].length" class="tree">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs[applicationGroup.type]"
:key="job.id"
:job="job"
>
</component>
</ul>
</template>
</div>
</ul>
</div>
</div>
<div
v-if="
$hasPermission(
'workspace.create_application',
selectedWorkspace,
selectedWorkspace.id
)
"
class="sidebar__new-wrapper"
:class="{ 'sidebar__new-wrapper--separator': applicationsCount > 0 }"
>
<a
ref="createApplicationContextLink"
class="sidebar__new"
@click="
$refs.createApplicationContext.toggle(
$refs.createApplicationContextLink
)
"
>
<i class="sidebar__new-icon iconoir-plus"></i>
{{ $t('action.createNew') }}
</a>
</div>
<CreateApplicationContext
ref="createApplicationContext"
:workspace="selectedWorkspace"
></CreateApplicationContext>
</div>
</template>
<script>
import { notifyIf } from '@baserow/modules/core/utils/error'
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'
export default {
name: 'SidebarWithWorkspace',
components: { CreateApplicationContext },
props: {
applications: {
type: Array,
required: true,
},
selectedWorkspace: {
type: Object,
required: true,
},
},
computed: {
/**
* Because all the applications that belong to the user are in the store we will
* filter on the selected workspace here.
*/
groupedApplicationsForSelectedWorkspace() {
const applicationTypes = Object.values(
this.$registry.getAll('application')
).map((applicationType) => {
return {
name: applicationType.getNamePlural(),
type: applicationType.getType(),
applications: this.applications
.filter((application) => {
return (
application.workspace.id === this.selectedWorkspace.id &&
application.type === applicationType.getType()
)
})
.sort((a, b) => a.order - b.order),
}
})
return applicationTypes
},
applicationsCount() {
return this.groupedApplicationsForSelectedWorkspace.reduce(
(acc, group) => acc + group.applications.length,
0
)
},
pendingJobs() {
const grouped = { null: [] }
Object.values(this.$registry.getAll('application')).forEach(
(applicationType) => {
grouped[applicationType.getType()] = []
}
)
this.$store.getters['job/getAll'].forEach((job) => {
const jobType = this.$registry.get('job', job.type)
if (jobType.isJobPartOfWorkspace(job, this.selectedWorkspace)) {
grouped[jobType.getSidebarApplicationTypeLocation(job)].push(job)
}
})
return grouped
},
},
methods: {
getApplicationComponent(application) {
return this.$registry
.get('application', application.type)
.getSidebarComponent()
},
getPendingJobComponent(job) {
return this.$registry.get('job', job.type).getSidebarComponent()
},
async orderApplications(order, oldOrder) {
try {
await this.$store.dispatch('application/order', {
workspace: this.selectedWorkspace,
order,
oldOrder,
})
} catch (error) {
notifyIf(error, 'application')
}
},
},
}
</script>

View file

@ -0,0 +1,45 @@
<template>
<div class="sidebar__section sidebar__section--scrollable">
<div class="sidebar__section-scrollable">
<div class="sidebar__section-scrollable-inner">
<p
v-if="workspaces.length === 0"
class="margin-left-1 margin-right-1 margin-top-1 margin-bottom-1"
>
{{ $t('sidebar.errorNoWorkspace') }}
</p>
</div>
</div>
<div class="sidebar__new-wrapper sidebar__new-wrapper--separator">
<a
v-if="$hasPermission('create_workspace')"
class="sidebar__new"
@click="$refs.createWorkspaceModal.show()"
>
<i class="sidebar__new-icon iconoir-plus"></i>
{{ $t('sidebar.createWorkspace') }}
</a>
</div>
<CreateWorkspaceModal ref="createWorkspaceModal"></CreateWorkspaceModal>
</div>
</template>
<script>
import CreateWorkspaceModal from '@baserow/modules/core/components/workspace/CreateWorkspaceModal'
export default {
name: 'SidebarWithoutWorkspace',
components: { CreateWorkspaceModal },
props: {
workspaces: {
type: Array,
required: true,
},
},
methods: {
hasUnreadNotifications(workspaceId) {
return this.$store.getters['notification/workspaceHasUnread'](workspaceId)
},
},
}
</script>

View file

@ -1,12 +1,12 @@
<template>
<div class="sidebar">
<div v-show="!collapsed" class="sidebar__nav">
<div
v-show="!collapsed"
class="sidebar__section sidebar__section--scrollable"
>
<div class="sidebar__section-scrollable">
<div class="sidebar__section-scrollable-inner">
<ul class="tree">
<li class="tree__item margin-top-2">
<div class="tree__link tree__link--group">
<span class="tree__link-text">{{ template.name }}</span>
</div>
</li>
<component
:is="getApplicationComponent(application)"
v-for="application in sortedApplications"
@ -18,7 +18,13 @@
></component>
</ul>
</div>
<div class="sidebar__foot">
</div>
</div>
<div class="sidebar__section sidebar__section--bottom">
<div
class="sidebar__foot"
:class="{ 'sidebar__foot--collapsed': collapsed }"
>
<div class="sidebar__logo">
<Logo height="14" alt="Baserow logo" />
</div>
@ -32,6 +38,7 @@
</a>
</div>
</div>
</div>
</template>
<script>

View file

@ -8,10 +8,7 @@
>
<div class="tree__action">
<a class="tree__link">
<i
class="tree__icon tree__icon--type"
:class="application._.type.iconClass"
></i>
<i class="tree__icon" :class="application._.type.iconClass"></i>
<span class="tree__link-text">{{ application.name }}</span>
</a>
</div>

View file

@ -1,118 +0,0 @@
<template>
<Context
ref="workspacesContext"
class="select"
:max-height-if-outside-viewport="true"
>
<div class="select__search">
<i class="select__search-icon iconoir-search"></i>
<input
v-model="query"
type="text"
class="select__search-input"
:placeholder="$t('workspacesContext.search')"
/>
</div>
<div v-if="isLoading" class="context--loading">
<div class="loading"></div>
</div>
<ul
v-if="!isLoading && isLoaded && workspaces.length > 0"
v-auto-overflow-scroll
class="select__items select__items--no-max-height"
>
<WorkspacesContextItem
v-for="workspace in searchAndSort(workspaces)"
:key="workspace.id"
v-sortable="{ id: workspace.id, update: order, marginTop: -1.5 }"
:workspace="workspace"
@selected="hide"
></WorkspacesContextItem>
</ul>
<div
v-if="!isLoading && isLoaded && workspaces.length == 0"
class="context__description"
>
{{ $t('workspacesContext.noResults') }}
</div>
<div class="select__footer">
<a
v-if="$hasPermission('create_workspace')"
class="select__footer-button"
@click="$refs.createWorkspaceModal.show()"
>
<i class="iconoir-plus"></i>
{{ $t('workspacesContext.createWorkspace') }}
</a>
</div>
<CreateWorkspaceModal
ref="createWorkspaceModal"
@created="hide"
></CreateWorkspaceModal>
</Context>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
import CreateWorkspaceModal from '@baserow/modules/core/components/workspace/CreateWorkspaceModal'
import WorkspacesContextItem from '@baserow/modules/core/components/workspace/WorkspacesContextItem'
import context from '@baserow/modules/core/mixins/context'
import { notifyIf } from '@baserow/modules/core/utils/error'
import { escapeRegExp } from '@baserow/modules/core/utils/string'
export default {
name: 'WorkspacesContext',
components: {
CreateWorkspaceModal,
WorkspacesContextItem,
},
mixins: [context],
data() {
return {
query: '',
}
},
computed: {
...mapState({
workspaces: (state) => state.workspace.items,
}),
...mapGetters({
isLoading: 'workspace/isLoading',
isLoaded: 'workspace/isLoaded',
}),
},
methods: {
/**
* When the workspaces context select is opened for the first time we must make
* sure that all the workspaces are already loaded or going to be loaded.
*/
toggle(...args) {
this.$store.dispatch('workspace/loadAll')
this.getRootContext().toggle(...args)
},
searchAndSort(workspaces) {
const query = this.query
return workspaces
.filter(function (workspace) {
const regex = new RegExp('(' + escapeRegExp(query) + ')', 'i')
return workspace.name.match(regex)
})
.sort((a, b) => {
return a.order - b.order
})
},
async order(order, oldOrder) {
try {
await this.$store.dispatch('workspace/order', {
order,
oldOrder,
})
} catch (error) {
notifyIf(error, 'workspace')
}
},
},
}
</script>

View file

@ -1,68 +0,0 @@
<template>
<li
class="select__item"
:class="{
active: workspace._.selected,
'select__item--loading':
workspace._.loading || workspace._.additionalLoading,
'select__item--has-notification': hasUnreadNotifications,
}"
>
<a class="select__item-link" @click="selectWorkspace(workspace)">
<div :title="workspace.name" class="select__item-name">
<span class="select__item-name-text">
<Editable
ref="rename"
:value="workspace.name"
@change="renameWorkspace(workspace, $event)"
></Editable>
</span>
<span
v-if="hasUnreadNotifications"
class="sidebar__unread-notifications-icon"
></span>
</div>
</a>
<i
v-if="workspace._.selected"
class="select__item-active-icon iconoir-check"
></i>
<a
ref="contextLink"
class="select__item-options"
@click="$refs.context.toggle($refs.contextLink, 'bottom', 'right', 0)"
@mousedown.stop
>
<i class="baserow-icon-more-vertical"></i>
</a>
<WorkspaceContext
ref="context"
:workspace="workspace"
@rename="enableRename()"
></WorkspaceContext>
</li>
</template>
<script>
import WorkspaceContext from '@baserow/modules/core/components/workspace/WorkspaceContext'
import editWorkspace from '@baserow/modules/core/mixins/editWorkspace'
export default {
name: 'WorkspacesContextItem',
components: { WorkspaceContext },
mixins: [editWorkspace],
props: {
workspace: {
type: Object,
required: true,
},
},
computed: {
hasUnreadNotifications() {
return this.$store.getters['notification/workspaceHasUnread'](
this.workspace.id
)
},
},
}
</script>

View file

@ -41,6 +41,16 @@ export class JobType extends Registerable {
return null
}
/**
* The left sidebar groups the applications. This method can optionally return an
* application type. If that's the case, then the job animation will be put in
* that application group. If nothing is provided, then the job will be put outside
* of the applications.
*/
getSidebarApplicationTypeLocation(job) {
return null
}
constructor(...args) {
super(...args)
this.type = this.getType()
@ -125,6 +135,10 @@ export class DuplicateApplicationJobType extends JobType {
return SidebarApplicationPendingJob
}
getSidebarApplicationTypeLocation(job) {
return job.original_application.type
}
isJobPartOfWorkspace(job, workspace) {
return job.original_application.workspace.id === workspace.id
}

View file

@ -1,13 +1,12 @@
<template>
<div>
<Toasts></Toasts>
<div :class="{ 'layout--collapsed': isCollapsed }" class="layout">
<div class="layout">
<div class="layout__col-1">
<Sidebar
:workspaces="workspaces"
:selected-workspace="selectedWorkspace"
:applications="applications"
:user="user"
@selected-workspace="selectedWorkspaceEvent"
></Sidebar>
</div>
@ -59,8 +58,6 @@ export default {
}),
...mapGetters({
applications: 'application/getAll',
isCollapsed: 'sidebar/isCollapsed',
user: 'auth/getUserObject',
}),
},
created() {

View file

@ -23,15 +23,21 @@
},
"sidebar": {
"createWorkspace": "Create workspace",
"addNewWorkspace": "Add new workspace",
"inviteOthers": "Invite others",
"members": "Members",
"logoff": "Logoff",
"logoff": "Log out",
"errorNoWorkspace": "You dont have any workspaces.",
"admin": "Admin",
"adminTools": "Admin tools",
"home": "Home",
"dashboard": "Dashboard",
"trash": "Trash",
"settings": "Settings",
"notifications": "Notifications"
"settings": "My settings",
"notifications": "Notifications",
"adminSettings": "Admin settings",
"general": "General",
"people": "People",
"licenses": "Licenses"
},
"accountForm": {
"nameLabel": "Your name",
@ -44,7 +50,7 @@
"submitButton": "Update account"
},
"settingsModal": {
"title": "Settings"
"title": "My settings"
},
"notificationPanel": {
"title": "Notifications",
@ -157,11 +163,6 @@
"errorTooLongMessage": "Messages are limited to {amount} characters.",
"additionalRoles": "Additional roles"
},
"workspacesContext": {
"search": "Search workspaces",
"noResults": "No results found",
"createWorkspace": "Create workspace"
},
"workspaceContext": {
"renameWorkspace": "Rename workspace",
"settings": "Settings",

View file

@ -60,7 +60,6 @@ import authStore from '@baserow/modules/core/store/auth'
import workspaceStore from '@baserow/modules/core/store/workspace'
import jobStore from '@baserow/modules/core/store/job'
import toastStore from '@baserow/modules/core/store/toast'
import sidebarStore from '@baserow/modules/core/store/sidebar'
import undoRedoStore from '@baserow/modules/core/store/undoRedo'
import integrationStore from '@baserow/modules/core/store/integration'
import userSourceStore from '@baserow/modules/core/store/userSource'
@ -179,7 +178,6 @@ export default (context, inject) => {
store.registerModule('job', jobStore)
store.registerModule('workspace', workspaceStore)
store.registerModule('toast', toastStore)
store.registerModule('sidebar', sidebarStore)
store.registerModule('undoRedo', undoRedoStore)
store.registerModule('integration', integrationStore)
store.registerModule('userSource', userSourceStore)

View file

@ -21,14 +21,14 @@ export class BaserowPlugin extends Registerable {
* Every registered plugin can have a component that's rendered at the top of the
* left sidebar.
*/
getSidebarTopComponent() {
getImpersonateComponent() {
return null
}
/*
* Every registered plugin can display an item in the main sidebar menu.
/**
* Every registered plugin can have a component displaying a badge with the highest license type
*/
getSidebarMainMenuComponent() {
getHighestLicenseTypeBadge() {
return null
}

View file

@ -1,32 +0,0 @@
export const state = () => ({
collapsed: false,
})
export const mutations = {
SET_COLLAPSED(state, collapsed) {
state.collapsed = collapsed
},
}
export const actions = {
toggleCollapsed({ commit, getters }, value) {
if (value === undefined) {
value = !getters.isCollapsed
}
commit('SET_COLLAPSED', value)
},
}
export const getters = {
isCollapsed(state) {
return !!state.collapsed
},
}
export default {
namespaced: true,
state,
getters,
actions,
mutations,
}

View file

@ -30,6 +30,11 @@ export class DatabaseApplicationType extends ApplicationType {
return i18n.t('applicationType.database')
}
getNamePlural() {
const { i18n } = this.app
return i18n.t('applicationType.databases')
}
getDescription() {
const { i18n } = this.app
return i18n.t('applicationType.databaseDesc')

View file

@ -28,8 +28,6 @@
v-sortable="{
id: table.id,
update: orderTables,
marginLeft: 34,
marginRight: 10,
marginTop: -1.5,
enabled: $hasPermission(
'database.order_tables',
@ -41,15 +39,6 @@
:table="table"
></SidebarItem>
</ul>
<ul v-if="pendingJobs.length" class="tree__subs">
<component
:is="getPendingJobComponent(job)"
v-for="job in pendingJobs"
:key="job.id"
:job="job"
>
</component>
</ul>
<a
v-if="
$hasPermission(
@ -61,7 +50,7 @@
class="tree__sub-add"
@click="$refs.importFileModal.show()"
>
<i class="iconoir-plus"></i>
<i class="tree__sub-add-icon iconoir-plus"></i>
{{ $t('sidebar.createTable') }}
</a>
<ImportFileModal ref="importFileModal" :database="application" />
@ -73,7 +62,6 @@
import { mapGetters } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import SidebarItem from '@baserow/modules/database/components/sidebar/SidebarItem'
import SidebarItemPendingJob from '@baserow/modules/core/components/sidebar/SidebarItemPendingJob'
import ImportFileModal from '@baserow/modules/database/components/table/ImportFileModal'
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
@ -82,7 +70,6 @@ export default {
components: {
SidebarApplication,
SidebarItem,
SidebarItemPendingJob,
ImportFileModal,
},
props: {
@ -101,13 +88,6 @@ export default {
.map((table) => table)
.sort((a, b) => a.order - b.order)
},
pendingJobs() {
return this.$store.getters['job/getAll'].filter((job) =>
this.$registry
.get('job', job.type)
.isJobPartOfApplication(job, this.application)
)
},
...mapGetters({ isAppSelected: 'application/isSelected' }),
},
methods: {
@ -129,9 +109,6 @@ export default {
notifyIf(error, 'table')
}
},
getPendingJobComponent(job) {
return this.$registry.get('job', job.type).getSidebarComponent()
},
},
}
</script>

View file

@ -2,16 +2,12 @@
<li
class="tree__item"
:class="{
active: application._.selected,
'tree__item--loading': application._.loading,
}"
>
<div class="tree__action">
<a class="tree__link" @click="$emit('selected', application)">
<i
class="tree__icon tree__icon--type"
:class="application._.type.iconClass"
></i>
<i class="tree__icon" :class="application._.type.iconClass"></i>
<span class="tree__link-text">{{ application.name }}</span>
</a>
</div>

View file

@ -63,7 +63,7 @@
},
"sidebar": {
"viewAPI": "View API Docs",
"createTable": "Create table"
"createTable": "New table"
},
"sidebarItem": {
"exportTable": "Export table",