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:
parent
00ae305039
commit
04e1a69c03
55 changed files with 1651 additions and 1751 deletions
changelog/entries/unreleased/feature
enterprise/web-frontend/modules/baserow_enterprise/components
premium/web-frontend
web-frontend
locales
modules
builder
core
adminTypes.jsapplicationTypes.js
assets
icons
scss
components
Modal.vue
jobTypes.jssidebar
Sidebar.vueSidebarAdmin.vueSidebarApplication.vueSidebarApplicationPendingJob.vueSidebarFoot.vueSidebarMenu.vueSidebarUserContext.vueSidebarWithWorkspace.vueSidebarWithoutWorkspace.vue
template
workspace
layouts
locales
plugin.jsplugins.jsstore
database
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Redesigned the left sidebar.",
|
||||
"issue_number": null,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-07-28"
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.instance-wide-license {
|
||||
@include absolute(4px, 4px, auto, auto);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
.premium-top-sidebar {
|
||||
position: relative;
|
||||
|
||||
.layout--collapsed & {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@
|
|||
ref="license"
|
||||
v-model="values.license"
|
||||
:error="fieldHasErrors('license')"
|
||||
rows="6"
|
||||
:rows="6"
|
||||
@blur="$v.values.license.$touch()"
|
||||
/>
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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() {
|
|
@ -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.",
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
|
@ -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.",
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
},
|
||||
"sidebarComponentBuilder": {
|
||||
"settings": "Settings",
|
||||
"createPage": "Create page"
|
||||
"createPage": "New page"
|
||||
},
|
||||
"builderSettingsModal": {
|
||||
"title": "Application"
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
4
web-frontend/modules/core/assets/icons/application.svg
Normal file
4
web-frontend/modules/core/assets/icons/application.svg
Normal 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 |
5
web-frontend/modules/core/assets/icons/groups.svg
Normal file
5
web-frontend/modules/core/assets/icons/groups.svg
Normal 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 |
|
@ -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 |
|
@ -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 {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.badge {
|
||||
background: $palette-blue-500;
|
||||
display: inline-flex;
|
||||
padding: 6px 10px;
|
||||
align-items: center;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -127,7 +127,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar__collapse {
|
||||
.modal__collapse {
|
||||
@extend %modal-close;
|
||||
|
||||
margin-right: 4px;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
<a
|
||||
v-if="collapsibleRightSidebar"
|
||||
class="sidebar__collapse"
|
||||
class="modal__collapse"
|
||||
@click="collapseSidebar"
|
||||
>
|
||||
<i
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 === ''"> </template>
|
||||
<Editable
|
||||
|
|
|
@ -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 }} %
|
||||
|
|
40
web-frontend/modules/core/components/sidebar/SidebarFoot.vue
Normal file
40
web-frontend/modules/core/components/sidebar/SidebarFoot.vue
Normal 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>
|
189
web-frontend/modules/core/components/sidebar/SidebarMenu.vue
Normal file
189
web-frontend/modules/core/components/sidebar/SidebarMenu.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -23,15 +23,21 @@
|
|||
},
|
||||
"sidebar": {
|
||||
"createWorkspace": "Create workspace",
|
||||
"addNewWorkspace": "Add new workspace",
|
||||
"inviteOthers": "Invite others",
|
||||
"members": "Members",
|
||||
"logoff": "Logoff",
|
||||
"logoff": "Log out",
|
||||
"errorNoWorkspace": "You don’t 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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
},
|
||||
"sidebar": {
|
||||
"viewAPI": "View API Docs",
|
||||
"createTable": "Create table"
|
||||
"createTable": "New table"
|
||||
},
|
||||
"sidebarItem": {
|
||||
"exportTable": "Export table",
|
||||
|
|
Loading…
Add table
Reference in a new issue