1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 06:15:36 +00:00

New left sidebar

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

View file

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

View file

@ -1,46 +1,48 @@
<template>
<li
<nuxt-link
v-if="hasPermission"
class="tree__item"
:class="{
'tree__item--loading': loading,
'tree__action--deactivated': deactivated,
active: $route.matched.some(({ name }) => name === 'workspace-audit-log'),
v-slot="{ href, navigate, isExactActive }"
:to="{
name: 'workspace-audit-log',
params: {
workspaceId: workspace.id,
},
}"
>
<div class="tree__action">
<a
v-if="deactivated"
href="#"
class="tree__link"
@click.prevent="$refs.enterpriseModal.show()"
>
<i class="tree__icon tree__icon--type 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>
<span class="tree__link-text">{{
$t('auditLogSidebarWorkspace.title')
}}</span>
</nuxt-link>
</div>
<EnterpriseModal
ref="enterpriseModal"
:workspace="workspace"
:name="$t('auditLogSidebarWorkspace.title')"
></EnterpriseModal>
</li>
<li
class="tree__item"
:class="{
'tree__item--loading': loading,
'tree__action--deactivated': deactivated,
active: isExactActive,
}"
>
<div class="tree__action">
<a
v-if="deactivated"
href="#"
class="tree__link"
@click.prevent="$refs.enterpriseModal.show()"
>
<i class="tree__icon iconoir-lock"></i>
<span class="tree__link-text">{{
$t('auditLogSidebarWorkspace.title')
}}</span>
</a>
<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>
</a>
</div>
<EnterpriseModal
ref="enterpriseModal"
:workspace="workspace"
:name="$t('auditLogSidebarWorkspace.title')"
></EnterpriseModal>
</li>
</nuxt-link>
</template>
<script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,200 +1,200 @@
{
"premium": {
"user": {
"isStaff": "Is staff",
"isWorkspaceAdmin": "Is workspace admin",
"active": "Active",
"deactivated": "Deactivated"
},
"adminType": {
"dashboard": "Dashboard",
"users": "Users",
"workspaces": "Workspaces",
"licenses": "Licenses"
},
"viewType": {
"kanban": "Kanban",
"calendar": "Calendar"
},
"exporterType": {
"json": "Export to JSON",
"xml": "Export to XML"
},
"deactivated": "Available in premium version"
"premium": {
"user": {
"isStaff": "Is staff",
"isWorkspaceAdmin": "Is workspace admin",
"active": "Active",
"deactivated": "Deactivated"
},
"premiumModal": {
"title": "Upgrade to use the {name}",
"description": "Your account doesnt have access to the premium features. Upgrade to the premium version to begin using {name}. You can upgrade your account by getting a license. Click on the button below to view the pricing.",
"viewPricing": "View pricing"
"adminType": {
"dashboard": "Dashboard",
"users": "Users",
"workspaces": "Workspaces",
"licenses": "Manage licenses"
},
"rowCommentSidebar": {
"onlyPremium": "Row comments are available in the premium version.",
"readOnlyNoComment": "No comments for this row.",
"noComment": "No comments for this row yet. Use the form below to add a comment.",
"comment": "Comment",
"more": "More information",
"name": "Comments"
"viewType": {
"kanban": "Kanban",
"calendar": "Calendar"
},
"rowComment": {
"you": "You",
"anonymous": "Anonymous",
"creating": "creating",
"updating": "updating",
"deleting": "deleting",
"created" : "created",
"edited": "edited",
"commentTrashed": "This comment has been deleted.",
"errorUserNotCommentAuthorTitle": "Cannot update or delete.",
"errorUserNotCommentAuthor": "Only the author of the comment can update or delete it.",
"errorInvalidCommentMentionTitle": "Invalid mention",
"errorInvalidCommentMention": "The mentioned user does not exist."
"exporterType": {
"json": "Export to JSON",
"xml": "Export to XML"
},
"rowCommentContext": {
"edit": "Edit comment",
"delete": "Delete comment"
},
"rowCommentMentionNotification": {
"title": "{sender} mentioned you in row {row} in {table}",
"deletedUser": "A deleted user"
},
"rowCommentNotification": {
"title": "{sender} posted a comment in row {row} in {table}",
"deletedUser": "A deleted user"
},
"trashType": {
"row_comment": "row comment"
},
"registerLicenseModal": {
"titleRegisterLicense": "Register a license",
"registerLicense": "Register license",
"viewPricing": "View pricing",
"licenseDescription": "A license can only be obtained on baserow.io. If you have already purchased a license, it will be delivered to you by email and you can get in from the overview in your account. Copy and paste the license key in the box below and click on the button to register the license key to this instance. Its not possible to use the same key on two different installations. {pricingLink} if you dont have a key yet.",
"licenseError": {
"invalidTitle": "Invalid",
"invalid": "The provided license is invalid.",
"unsupportedTitle": "Unsupported",
"unsupported": "The provided license is incompatible with your Baserow version. Please update to the latest version and try again.",
"expiredTitle": "Expired",
"expired": "The provided license has expired.",
"duplicateTitle": "Duplicate",
"duplicate": "The provided license is already registered to this instance.",
"instanceMismatchTitle": "Instance mismatch",
"instanceMismatch": "The provided license does not belong to this instance."
}
},
"disconnectLicenseModal": {
"disconnectLicense": "Disconnect license",
"disconnectDescription": "Are you sure that you want to disconnect this license? If you disconnect this license while it's active, the related users wont have access to the plan anymore. It will effectively remove the license. Please contact our support team at {contact} if you want to use this license in another self hosted instance."
},
"registerLicenseForm": {
"licenseKey": "License key"
},
"workspacesAdminTable": {
"allWorkspaces": "All workspaces",
"id": "ID",
"name": "Name",
"members": "Members",
"applications": "Applications",
"created": "Created",
"rowCount": "Row count",
"freeUsers": "Free users",
"seatsTaken": "Seats taken",
"storageUsage": "Storage Used (MB)",
"usageHelpText": "Calculated nightly when the track workspace usage setting is enabled"
},
"editWorkspaceContext": {
"delete": "Permanently delete"
},
"deleteWorkspaceModal": {
"title": "Delete {name}",
"confirmation": "Are you sure you want to delete the workspace: {name}?",
"comment": "The workspace will be permanently deleted, including the related applications. It is not possible to undo this action.",
"delete": "Delete workspace {name}"
},
"activeUsers": {
"newUsers": "New users",
"activeUsers": "Active users"
},
"usersAdminTable": {
"allUsers": "All users",
"username": "Username",
"name": "Name",
"workspaces": "Workspaces",
"lastLogin": "Last login",
"dateJoined": "Signed up",
"active": "Active"
},
"editUserContext": {
"changePassword": "Change password",
"delete": "Permanently delete",
"impersonate": "Impersonate"
},
"changePasswordForm": {
"newPassword": "New password",
"repeatPassword": "Repeat password",
"changePassword": "Change password",
"error": {
"doesntMatch": "This field must match your password field."
}
},
"userForm": {
"fullName": "Full name",
"email": "Email",
"isActive": "Is active",
"warning": {
"changeEmail": "Changing this users email address means when they sign in they must use the new email address to do so. This must be communicated with that user.",
"inactiveUser": "When a user is marked as inactive they are prevented from signing in.",
"userStaff": "Making the user staff gives them admin access to all users, all workspaces and the ability to revoke your own staff permissions."
},
"error": {
"invalidName": "Please enter a valid full name, it must be longer than 2 letters and less than 150.",
"invalidEmail": "Please enter a valid e-mail address."
}
},
"changeUserPasswordModal": {
"changePassword": "Change password for {username}"
},
"deleteUserModal": {
"title": "Delete {username}",
"confirmation": "Are you sure you want to delete the user: {name}?",
"comment1": "The user account will be deleted, however the workspaces that user is a member of will continue existing. The users workspace will not be deleted, even if this user is the last user in the workspace. Deleting the last user in a workspace prevents anyone being able to access that workspace.",
"comment2": "After deleting a user it is possible for a new user to sign up again using the deleted users email address. To ensure they cannot sign up again instead deactivate the user and do not delete them.",
"delete": "Delete user {username}"
},
"editUserModal": {
"delete": "Delete user",
"edit": "Edit { username }"
},
"tableJSONExporter": {
"encoding": "Encoding"
},
"tableXMLExporter": {
"encoding": "Encoding"
},
"kanbanViewStackContext": {
"createCard": "Create card",
"editStack": "Edit stack",
"deleteStack": "Delete stack",
"delete": "Delete {name}",
"deleteDescription": "Are you sure that you want to delete stack {name}? Deleting the stack results in deleting the select option of the single select field, which might result into data loss because row values are going to be set to empty."
},
"kanbanViewHeader": {
"stackBy": "Stack by",
"stackedBy": "Stacked by {fieldName}",
"customizeCards": "Customize cards"
},
"kanbanViewOptionForm": {
"selectOption": "Select option"
},
"kanbanViewStakedBy": {
"title": "Group field",
"chooseField": "Which single select field should the cards be stacked by?"
},
"kanbanViewStack": {
"uncategorized": "Uncategorized",
"tryAgain": "Try again",
"new": "New"
"deactivated": "Available in premium version"
},
"premiumModal": {
"title": "Upgrade to use the {name}",
"description": "Your account doesnt have access to the premium features. Upgrade to the premium version to begin using {name}. You can upgrade your account by getting a license. Click on the button below to view the pricing.",
"viewPricing": "View pricing"
},
"rowCommentSidebar": {
"onlyPremium": "Row comments are available in the premium version.",
"readOnlyNoComment": "No comments for this row.",
"noComment": "No comments for this row yet. Use the form below to add a comment.",
"comment": "Comment",
"more": "More information",
"name": "Comments"
},
"rowComment": {
"you": "You",
"anonymous": "Anonymous",
"creating": "creating",
"updating": "updating",
"deleting": "deleting",
"created": "created",
"edited": "edited",
"commentTrashed": "This comment has been deleted.",
"errorUserNotCommentAuthorTitle": "Cannot update or delete.",
"errorUserNotCommentAuthor": "Only the author of the comment can update or delete it.",
"errorInvalidCommentMentionTitle": "Invalid mention",
"errorInvalidCommentMention": "The mentioned user does not exist."
},
"rowCommentContext": {
"edit": "Edit comment",
"delete": "Delete comment"
},
"rowCommentMentionNotification": {
"title": "{sender} mentioned you in row {row} in {table}",
"deletedUser": "A deleted user"
},
"rowCommentNotification": {
"title": "{sender} posted a comment in row {row} in {table}",
"deletedUser": "A deleted user"
},
"trashType": {
"row_comment": "row comment"
},
"registerLicenseModal": {
"titleRegisterLicense": "Register a license",
"registerLicense": "Register license",
"viewPricing": "View pricing",
"licenseDescription": "A license can only be obtained on baserow.io. If you have already purchased a license, it will be delivered to you by email and you can get in from the overview in your account. Copy and paste the license key in the box below and click on the button to register the license key to this instance. Its not possible to use the same key on two different installations. {pricingLink} if you dont have a key yet.",
"licenseError": {
"invalidTitle": "Invalid",
"invalid": "The provided license is invalid.",
"unsupportedTitle": "Unsupported",
"unsupported": "The provided license is incompatible with your Baserow version. Please update to the latest version and try again.",
"expiredTitle": "Expired",
"expired": "The provided license has expired.",
"duplicateTitle": "Duplicate",
"duplicate": "The provided license is already registered to this instance.",
"instanceMismatchTitle": "Instance mismatch",
"instanceMismatch": "The provided license does not belong to this instance."
}
},
"disconnectLicenseModal": {
"disconnectLicense": "Disconnect license",
"disconnectDescription": "Are you sure that you want to disconnect this license? If you disconnect this license while it's active, the related users wont have access to the plan anymore. It will effectively remove the license. Please contact our support team at {contact} if you want to use this license in another self hosted instance."
},
"registerLicenseForm": {
"licenseKey": "License key"
},
"workspacesAdminTable": {
"allWorkspaces": "All workspaces",
"id": "ID",
"name": "Name",
"members": "Members",
"applications": "Applications",
"created": "Created",
"rowCount": "Row count",
"freeUsers": "Free users",
"seatsTaken": "Seats taken",
"storageUsage": "Storage Used (MB)",
"usageHelpText": "Calculated nightly when the track workspace usage setting is enabled"
},
"editWorkspaceContext": {
"delete": "Permanently delete"
},
"deleteWorkspaceModal": {
"title": "Delete {name}",
"confirmation": "Are you sure you want to delete the workspace: {name}?",
"comment": "The workspace will be permanently deleted, including the related applications. It is not possible to undo this action.",
"delete": "Delete workspace {name}"
},
"activeUsers": {
"newUsers": "New users",
"activeUsers": "Active users"
},
"usersAdminTable": {
"allUsers": "All users",
"username": "Username",
"name": "Name",
"workspaces": "Workspaces",
"lastLogin": "Last login",
"dateJoined": "Signed up",
"active": "Active"
},
"editUserContext": {
"changePassword": "Change password",
"delete": "Permanently delete",
"impersonate": "Impersonate"
},
"changePasswordForm": {
"newPassword": "New password",
"repeatPassword": "Repeat password",
"changePassword": "Change password",
"error": {
"doesntMatch": "This field must match your password field."
}
},
"userForm": {
"fullName": "Full name",
"email": "Email",
"isActive": "Is active",
"warning": {
"changeEmail": "Changing this users email address means when they sign in they must use the new email address to do so. This must be communicated with that user.",
"inactiveUser": "When a user is marked as inactive they are prevented from signing in.",
"userStaff": "Making the user staff gives them admin access to all users, all workspaces and the ability to revoke your own staff permissions."
},
"error": {
"invalidName": "Please enter a valid full name, it must be longer than 2 letters and less than 150.",
"invalidEmail": "Please enter a valid e-mail address."
}
},
"changeUserPasswordModal": {
"changePassword": "Change password for {username}"
},
"deleteUserModal": {
"title": "Delete {username}",
"confirmation": "Are you sure you want to delete the user: {name}?",
"comment1": "The user account will be deleted, however the workspaces that user is a member of will continue existing. The users workspace will not be deleted, even if this user is the last user in the workspace. Deleting the last user in a workspace prevents anyone being able to access that workspace.",
"comment2": "After deleting a user it is possible for a new user to sign up again using the deleted users email address. To ensure they cannot sign up again instead deactivate the user and do not delete them.",
"delete": "Delete user {username}"
},
"editUserModal": {
"delete": "Delete user",
"edit": "Edit { username }"
},
"tableJSONExporter": {
"encoding": "Encoding"
},
"tableXMLExporter": {
"encoding": "Encoding"
},
"kanbanViewStackContext": {
"createCard": "Create card",
"editStack": "Edit stack",
"deleteStack": "Delete stack",
"delete": "Delete {name}",
"deleteDescription": "Are you sure that you want to delete stack {name}? Deleting the stack results in deleting the select option of the single select field, which might result into data loss because row values are going to be set to empty."
},
"kanbanViewHeader": {
"stackBy": "Stack by",
"stackedBy": "Stacked by {fieldName}",
"customizeCards": "Customize cards"
},
"kanbanViewOptionForm": {
"selectOption": "Select option"
},
"kanbanViewStakedBy": {
"title": "Group field",
"chooseField": "Which single select field should the cards be stacked by?"
},
"kanbanViewStack": {
"uncategorized": "Uncategorized",
"tryAgain": "Try again",
"new": "New"
},
"calendarViewType": {
"sharedViewText": "Allow anyone to see the data in this view or sync events to your external calendar.",
"sharedViewEnableSyncToExternalCalendar": "Sync to an external calendar",
@ -202,195 +202,195 @@
"sharedViewDescription": "Paste this link into your calendar application",
"sharedViewTitle": "Sync to an external calendar"
},
"calendarViewHeader": {
"displayBy": "Display by",
"displayedBy": "Displayed by “{fieldName}” field",
"labels": "Labels"
},
"calendarDateSelector": {
"today": "Today"
},
"calendarMonthDay": {
"hiddenRowsCount": "0 | 1 more... | {hiddenRowsCount} more..."
},
"calendarMonthDayExpanded": {
"tryAgain": "try again"
},
"dashboard": {
"title": "Dashboard",
"totals": "Totals",
"totalUsers": "Total users",
"totalWorkspaces": "Total workspaces",
"totalApplications": "Total applications",
"newUsers": "New users",
"newUsers24h": "New users last 24 hours",
"newUsers7days": "New users last 7 days",
"newUsers30days": "New users last 30 days",
"activeUsers": "Active users",
"activeUsers24h": "Active users last 24 hours",
"activeUsers7days": "Active users last 7 days",
"activeUsers30days": "Active users last 30 days",
"viewAll": "View all"
},
"premiumFeatures": {
"rowComments": "Row comments",
"kanbanView": "Kanban view",
"calendarView": "Calendar view",
"exports": "JSON and XML export",
"admin": "Admin functionality",
"rowColoring": "Row coloring",
"surveyForm": "Survey form",
"publicLogoRemoval": "Public logo removal",
"personalViews": "Personal views",
"aiFeatures": "AI field"
},
"licenses": {
"titleNoLicenses": "No licenses found",
"titleLicenses": "Licenses",
"noLicensesDescription": "Your Baserow instance doesnt have any licenses registered. A premium license gives you immediate access to all of the additional features. If you already have a license, you can register it here. Alternatively you can get one by clicking on the button below.",
"getLicense": "Get a license",
"baserowInstanceId": "Your Baserow instance ID is:",
"registerLicense": "Register license",
"licenceId": "License ID",
"premium": "Premium",
"expired": "Expired",
"validity": "Valid from {start} through {end}",
"seats": "seats"
},
"license": {
"title": "{name} plan",
"users": "Users",
"description": "Choose the users that can use the {product_code} plan. This license allows you to grant {product_code} to a maximum of {seats} users.",
"seatLeft": "You have no seats left|You have one seat left|You have {count} seats left",
"fillSeats": "Fill seats with users that are not on the plan",
"removeAll": "Remove all users",
"licenseId": "License ID",
"addUser": "Add a user",
"plan": "Plan",
"premium": "premium",
"expired": "Expired",
"validFrom": "Valid from",
"validThrough": "Valid through",
"validThroughDescription": "After your license has expired, you and any assigned users will no longer be able to use the extra functionality granted by the license.",
"lastCheck": "Last check",
"lastCheckDescription": "The license is periodically checked for changes. If you for example renewed or upgraded your license, the changes become active after the check.",
"checkNow": "check now",
"seats": "Seats (amount of users)",
"licensedTo": "Licensed to",
"applications": "Applications / databases",
"unlimited": "Unlimited",
"rowUsage": "Row usage",
"storeUsage": "Storage usage",
"disconnectLicense": "Disconnect license",
"disconnectDescription": "If you disconnect this license while it's active, the related users wont have access to the plan anymore. It will effectively remove the license. Please contact our support team at {contact} if you want to use this license in another self hosted instance.",
"moreSeatsNeededTitle": "More Seats Needed",
"contactSalesMoreSeats": "Contact Sales for more seats",
"automaticSeatsProgressBarStatus": "{seats_taken} / {seats} paid seats and {free_users_count} free seats in use",
"premiumFeatureName": "Premium",
"enterpriseFeatureName": "Enterprise",
"supportFeatureName": "Support"
},
"viewDecoratorType": {
"leftBorderColor": "Left border",
"leftBorderColorDescription": "Color the left border of a row",
"backgroundColor": "Background color",
"backgroundColorDescription": "Color the background of a row",
"onlyForPremium": "Available in the premium version",
"onlyOneDecoratorPerView": "You can only have one decoration of that type for a view"
},
"decoratorValueProviderType": {
"singleSelectColor": "Single select",
"singleSelectColorDescription": "Color rows the same as a single select value",
"conditionalColor": "Conditions",
"conditionalColorDescription": "Color rows when they match the conditions"
},
"singleSelectColorValueProviderForm": {
"chooseAColor": "Which single select field should the row be colored by?"
},
"conditionalColorValueProviderForm": {
"addCondition": "Add condition",
"addConditionGroup": "Add condition group",
"colorAlwaysApplyTitle": "This color applies by default.",
"colorAlwaysApply": "You can add conditions by clicking on the \"Add condition\" button.",
"addColor": "Add color",
"deleteColor": "Delete color",
"title": "Colors"
},
"redirectToBaserowModal": {
"title": "Redirecting to https://baserow.io",
"content": "You are leaving your self hosted instance and you are being redirected to https://baserow.io."
},
"premiumTopSidebar": {
"impersonateDescription": "This account is impersonated. A page refresh will stop it.",
"impersonateStop": "Stop",
"premium": "Premium",
"premiumDescription": "Your account has access to the premium features globally"
},
"formViewModeType": {
"survey": "Survey",
"surveyDescription": "One field is visible at the same time.",
"onlyForPremium": "Available in the premium version"
},
"formViewModePreviewSurvey": {
"orderFields": "Order fields",
"deactivated": "Survey form is available in the premium version.",
"more": "More information"
},
"shareLinkOptions": {
"baserowLogo": {
"label": "Hide Baserow logo on shared view",
"premiumModalName": "public logo removal"
}
},
"viewsContext": {
"personal": "Personal"
},
"dateFieldSelectForm": {
"dateField": "Which field would you like to use for this view?",
"noCompatibleDateFields": "There are no date fields in this table, please create one first."
},
"selectDateFieldModal": {
"chooseDateField": "Choose date field",
"save": "Save"
},
"RowEditModalCommentNotificationMode": {
"modeMentionsTitle": "Only mentions",
"modeMentionsDesc": "You will only receive notifications when you are mentioned.",
"modeAllTitle": "All comments",
"modeAllDesc": "You will receive notifications for all comments in this row."
},
"premiumFieldType": {
"ai": "AI prompt",
"aiDescription": "A read-only field that holds AI generated text based on the field prompt."
},
"functionalGridViewFieldAI": {
"generate": "Generate"
},
"gridViewFieldAI": {
"generate": "Generate",
"regenerate": "Regenerate"
},
"fieldAISubForm": {
"prompt": "Prompt",
"promptPlaceholder": "What is Baserow?",
"premiumFeature": "The AI field is a premium feature",
"emptyFileField": "None",
"fileFieldHelp": "The first compatible file in the field will be used as the knowledge base for the prompt. The file has to be a text file with the supported file extension like .txt, .md, .pdf, .docx."
},
"rowEditFieldAI": {
"generate": "Generate",
"createRowBefore": "The AI value can be generated after the row has been created."
},
"aiFormulaModal": {
"title": "Generate formula using AI",
"description": "Note that the generated formulas might not always work as expected. We're constructing a prompt for the model, and we load the output in the formula input. It works best with a high parameter model like gpt-4-turbo-preview.",
"label": "Prompt",
"labelDescription": "Describe the formula you would like to generate",
"generate": "Generate",
"noModels": "Your Baserow instance and workspace doesn't have any AI models configured. Click on the three dots next to your workspace, then on settings to configure them."
},
"formulaFieldAI": {
"generateWithAI": "Generate using AI",
"featureName": "Generate formula using AI"
"calendarViewHeader": {
"displayBy": "Display by",
"displayedBy": "Displayed by “{fieldName}” field",
"labels": "Labels"
},
"calendarDateSelector": {
"today": "Today"
},
"calendarMonthDay": {
"hiddenRowsCount": "0 | 1 more... | {hiddenRowsCount} more..."
},
"calendarMonthDayExpanded": {
"tryAgain": "try again"
},
"dashboard": {
"title": "Dashboard",
"totals": "Totals",
"totalUsers": "Total users",
"totalWorkspaces": "Total workspaces",
"totalApplications": "Total applications",
"newUsers": "New users",
"newUsers24h": "New users last 24 hours",
"newUsers7days": "New users last 7 days",
"newUsers30days": "New users last 30 days",
"activeUsers": "Active users",
"activeUsers24h": "Active users last 24 hours",
"activeUsers7days": "Active users last 7 days",
"activeUsers30days": "Active users last 30 days",
"viewAll": "View all"
},
"premiumFeatures": {
"rowComments": "Row comments",
"kanbanView": "Kanban view",
"calendarView": "Calendar view",
"exports": "JSON and XML export",
"admin": "Admin functionality",
"rowColoring": "Row coloring",
"surveyForm": "Survey form",
"publicLogoRemoval": "Public logo removal",
"personalViews": "Personal views",
"aiFeatures": "AI field"
},
"licenses": {
"titleNoLicenses": "No licenses found",
"titleLicenses": "Licenses",
"noLicensesDescription": "Your Baserow instance doesnt have any licenses registered. A premium license gives you immediate access to all of the additional features. If you already have a license, you can register it here. Alternatively you can get one by clicking on the button below.",
"getLicense": "Get a license",
"baserowInstanceId": "Your Baserow instance ID is:",
"registerLicense": "Register license",
"licenceId": "License ID",
"premium": "Premium",
"expired": "Expired",
"validity": "Valid from {start} through {end}",
"seats": "seats"
},
"license": {
"title": "{name} plan",
"users": "Users",
"description": "Choose the users that can use the {product_code} plan. This license allows you to grant {product_code} to a maximum of {seats} users.",
"seatLeft": "You have no seats left|You have one seat left|You have {count} seats left",
"fillSeats": "Fill seats with users that are not on the plan",
"removeAll": "Remove all users",
"licenseId": "License ID",
"addUser": "Add a user",
"plan": "Plan",
"premium": "premium",
"expired": "Expired",
"validFrom": "Valid from",
"validThrough": "Valid through",
"validThroughDescription": "After your license has expired, you and any assigned users will no longer be able to use the extra functionality granted by the license.",
"lastCheck": "Last check",
"lastCheckDescription": "The license is periodically checked for changes. If you for example renewed or upgraded your license, the changes become active after the check.",
"checkNow": "check now",
"seats": "Seats (amount of users)",
"licensedTo": "Licensed to",
"applications": "Applications / databases",
"unlimited": "Unlimited",
"rowUsage": "Row usage",
"storeUsage": "Storage usage",
"disconnectLicense": "Disconnect license",
"disconnectDescription": "If you disconnect this license while it's active, the related users wont have access to the plan anymore. It will effectively remove the license. Please contact our support team at {contact} if you want to use this license in another self hosted instance.",
"moreSeatsNeededTitle": "More Seats Needed",
"contactSalesMoreSeats": "Contact Sales for more seats",
"automaticSeatsProgressBarStatus": "{seats_taken} / {seats} paid seats and {free_users_count} free seats in use",
"premiumFeatureName": "Premium",
"enterpriseFeatureName": "Enterprise",
"supportFeatureName": "Support"
},
"viewDecoratorType": {
"leftBorderColor": "Left border",
"leftBorderColorDescription": "Color the left border of a row",
"backgroundColor": "Background color",
"backgroundColorDescription": "Color the background of a row",
"onlyForPremium": "Available in the premium version",
"onlyOneDecoratorPerView": "You can only have one decoration of that type for a view"
},
"decoratorValueProviderType": {
"singleSelectColor": "Single select",
"singleSelectColorDescription": "Color rows the same as a single select value",
"conditionalColor": "Conditions",
"conditionalColorDescription": "Color rows when they match the conditions"
},
"singleSelectColorValueProviderForm": {
"chooseAColor": "Which single select field should the row be colored by?"
},
"conditionalColorValueProviderForm": {
"addCondition": "Add condition",
"addConditionGroup": "Add condition group",
"colorAlwaysApplyTitle": "This color applies by default.",
"colorAlwaysApply": "You can add conditions by clicking on the \"Add condition\" button.",
"addColor": "Add color",
"deleteColor": "Delete color",
"title": "Colors"
},
"redirectToBaserowModal": {
"title": "Redirecting to https://baserow.io",
"content": "You are leaving your self hosted instance and you are being redirected to https://baserow.io."
},
"premiumTopSidebar": {
"impersonateDescription": "This account is impersonated. A page refresh will stop it.",
"impersonateStop": "Stop",
"premium": "Premium",
"premiumDescription": "Your account has access to the premium features globally"
},
"formViewModeType": {
"survey": "Survey",
"surveyDescription": "One field is visible at the same time.",
"onlyForPremium": "Available in the premium version"
},
"formViewModePreviewSurvey": {
"orderFields": "Order fields",
"deactivated": "Survey form is available in the premium version.",
"more": "More information"
},
"shareLinkOptions": {
"baserowLogo": {
"label": "Hide Baserow logo on shared view",
"premiumModalName": "public logo removal"
}
},
"viewsContext": {
"personal": "Personal"
},
"dateFieldSelectForm": {
"dateField": "Which field would you like to use for this view?",
"noCompatibleDateFields": "There are no date fields in this table, please create one first."
},
"selectDateFieldModal": {
"chooseDateField": "Choose date field",
"save": "Save"
},
"RowEditModalCommentNotificationMode": {
"modeMentionsTitle": "Only mentions",
"modeMentionsDesc": "You will only receive notifications when you are mentioned.",
"modeAllTitle": "All comments",
"modeAllDesc": "You will receive notifications for all comments in this row."
},
"premiumFieldType": {
"ai": "AI prompt",
"aiDescription": "A read-only field that holds AI generated text based on the field prompt."
},
"functionalGridViewFieldAI": {
"generate": "Generate"
},
"gridViewFieldAI": {
"generate": "Generate",
"regenerate": "Regenerate"
},
"fieldAISubForm": {
"prompt": "Prompt",
"promptPlaceholder": "What is Baserow?",
"premiumFeature": "The AI field is a premium feature",
"emptyFileField": "None",
"fileFieldHelp": "The first compatible file in the field will be used as the knowledge base for the prompt. The file has to be a text file with the supported file extension like .txt, .md, .pdf, .docx."
},
"rowEditFieldAI": {
"generate": "Generate",
"createRowBefore": "The AI value can be generated after the row has been created."
},
"aiFormulaModal": {
"title": "Generate formula using AI",
"description": "Note that the generated formulas might not always work as expected. We're constructing a prompt for the model, and we load the output in the formula input. It works best with a high parameter model like gpt-4-turbo-preview.",
"label": "Prompt",
"labelDescription": "Describe the formula you would like to generate",
"generate": "Generate",
"noModels": "Your Baserow instance and workspace doesn't have any AI models configured. Click on the three dots next to your workspace, then on settings to configure them."
},
"formulaFieldAI": {
"generateWithAI": "Generate using AI",
"featureName": "Generate formula using AI"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

After

(image error) Size: 425 B

View file

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

After

(image error) Size: 903 B

View file

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

After

(image error) Size: 348 B

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,456 +1,99 @@
<template>
<div class="sidebar">
<div class="sidebar__inner">
<component
:is="component"
v-for="(component, index) in sidebarTopComponents"
:key="index"
></component>
<component
:is="component"
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>
<span class="sidebar__workspaces-selector-selected-workspace">{{
selectedWorkspace.name || name
}}</span>
<span
v-if="unreadNotificationsInOtherWorkspaces"
class="sidebar__unread-notifications-icon"
></span>
<i
class="sidebar__workspaces-selector-icon baserow-icon-up-down-arrows"
></i>
</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>
</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>
<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>
</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>
<SidebarWithWorkspace
v-if="hasSelectedWorkspace"
:applications="applications"
:selected-workspace="selectedWorkspace"
></SidebarWithWorkspace>
<SidebarWithoutWorkspace
v-if="!hasSelectedWorkspace"
:workspaces="workspaces"
@selected-workspace="$emit('selected-workspace', $event)"
></SidebarWithoutWorkspace>
</template>
<SidebarFoot></SidebarFoot>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
import SettingsModal from '@baserow/modules/core/components/settings/SettingsModal'
import SidebarAdminItem from '@baserow/modules/core/components/sidebar/SidebarAdminItem'
import SidebarApplication from '@baserow/modules/core/components/sidebar/SidebarApplication'
import CreateApplicationContext from '@baserow/modules/core/components/application/CreateApplicationContext'
import WorkspacesContext from '@baserow/modules/core/components/workspace/WorkspacesContext'
import WorkspaceContext from '@baserow/modules/core/components/workspace/WorkspaceContext'
import CreateWorkspaceModal from '@baserow/modules/core/components/workspace/CreateWorkspaceModal'
import TrashModal from '@baserow/modules/core/components/trash/TrashModal'
import editWorkspace from '@baserow/modules/core/mixins/editWorkspace'
import undoRedo from '@baserow/modules/core/mixins/undoRedo'
import ExternalLinkBaserowLogo from '@baserow/modules/core/components/ExternalLinkBaserowLogo'
import WorkspaceMemberInviteModal from '@baserow/modules/core/components/workspace/WorkspaceMemberInviteModal'
import { logoutAndRedirectToLogin } from '@baserow/modules/core/utils/auth'
import NotificationPanel from '@baserow/modules/core/components/NotificationPanel'
import BadgeCounter from '@baserow/modules/core/components/BadgeCounter'
import SidebarUserContext from '@baserow/modules/core/components/sidebar/SidebarUserContext'
import SidebarWithWorkspace from '@baserow/modules/core/components/sidebar/SidebarWithWorkspace'
import SidebarWithoutWorkspace from '@baserow/modules/core/components/sidebar/SidebarWithoutWorkspace'
import SidebarAdmin from '@baserow/modules/core/components/sidebar/SidebarAdmin'
import SidebarFoot from '@baserow/modules/core/components/sidebar/SidebarFoot'
import SidebarMenu from '@baserow/modules/core/components/sidebar/SidebarMenu'
import SidebarAdminItem from './SidebarAdminItem.vue'
export default {
name: 'Sidebar',
components: {
ExternalLinkBaserowLogo,
SettingsModal,
CreateApplicationContext,
SidebarAdminItem,
SidebarApplication,
WorkspacesContext,
WorkspaceContext,
CreateWorkspaceModal,
TrashModal,
WorkspaceMemberInviteModal,
NotificationPanel,
BadgeCounter,
SidebarAdmin,
SidebarWithoutWorkspace,
SidebarWithWorkspace,
SidebarUserContext,
SidebarMenu,
SidebarFoot,
},
mixins: [editWorkspace, undoRedo],
props: {
applications: {
type: Array,
@ -467,144 +110,43 @@ export default {
},
data() {
return {
logoffLoading: false,
showAdmin: false,
}
},
computed: {
/**
* Because all the applications that belong to the user are in the store we will
* filter on the selected workspace here.
*/
orderedApplicationsInSelectedWorkspace() {
return this.applications
.filter(
(application) =>
application.workspace.id === this.selectedWorkspace.id
)
.sort((a, b) => a.order - b.order)
SidebarAdminItem() {
return SidebarAdminItem
},
adminTypes() {
return this.$registry.getAll('admin')
},
sortedAdminTypes() {
return Object.values(this.adminTypes)
.slice()
.sort((a, b) => a.getOrder() - b.getOrder())
},
sidebarTopComponents() {
impersonateComponent() {
return Object.values(this.$registry.getAll('plugin'))
.map((plugin) => plugin.getSidebarTopComponent())
.map((plugin) => plugin.getImpersonateComponent())
.filter((component) => component !== null)
},
sidebarMainMenuComponents() {
return Object.values(this.$registry.getAll('plugin'))
.map((plugin) => plugin.getSidebarMainMenuComponent())
.filter((component) => component !== null)
},
sidebarWorkspaceComponents() {
return Object.values(this.$registry.getAll('plugin'))
.flatMap((plugin) =>
plugin.getSidebarWorkspaceComponents(this.selectedWorkspace)
)
.filter((component) => component !== null)
},
pendingJobs() {
return this.$store.getters['job/getAll'].filter((job) =>
this.$registry
.get('job', job.type)
.isJobPartOfWorkspace(job, this.selectedWorkspace)
)
},
/**
* Indicates whether the current user is visiting an admin page.
*/
isAdminPage() {
return Object.values(this.adminTypes).some((adminType) => {
return this.$route.matched.some(
({ name }) => name === adminType.routeName
)
})
},
avatarSize() {
return this.isCollapsed ? 'large' : 'x-large'
},
hasSelectedWorkspace() {
return Object.prototype.hasOwnProperty.call(this.selectedWorkspace, 'id')
},
...mapGetters({
isStaff: 'auth/isStaff',
name: 'auth/getName',
email: 'auth/getUsername',
isCollapsed: 'sidebar/isCollapsed',
unreadNotificationCount: 'notification/getUnreadCount',
unreadNotificationsInOtherWorkspaces:
'notification/anyOtherWorkspaceWithUnread',
}),
},
created() {
// Checks whether the rendered page is an admin page. If so, switch the left sidebar
// navigation to the admin.
// this.showAdmin = Object.values(this.$registry.getAll('admin')).some(
// (adminType) => {
// return this.$route.matched.some(
// ({ name }) => name === adminType.routeName
// )
// }
// )
},
methods: {
hasUnreadNotifications(workspaceId) {
return this.$store.getters['notification/workspaceHasUnread'](workspaceId)
},
getApplicationComponent(application) {
return this.$registry
.get('application', application.type)
.getSidebarComponent()
},
getPendingJobComponent(job) {
return this.$registry.get('job', job.type).getSidebarComponent()
},
logoff() {
this.logoffLoading = true
logoutAndRedirectToLogin(
this.$nuxt.$router,
this.$store,
false,
false,
true
)
},
/**
* Called when the user clicks on the admin menu. Because there isn't an
* admin page it will navigate to the route of the first registered admin
* type.
*/
admin() {
// If the user is already on an admin page we don't have to do anything because
// the link is disabled.
if (this.isAdminPage) {
return
}
// We only want to autoselect the first active admin type because the other ones
// can't be selected.
const activated = this.sortedAdminTypes.filter((adminType) => {
return !this.$registry.get('admin', adminType.type).isDeactivated()
})
if (activated.length > 0) {
this.$nuxt.$router.push({ name: activated[0].routeName })
}
},
async orderApplications(order, oldOrder) {
try {
await this.$store.dispatch('application/order', {
workspace: this.selectedWorkspace,
order,
oldOrder,
})
} catch (error) {
notifyIf(error, 'application')
}
},
handleInvite(event) {
if (this.$route.name !== 'settings-invites') {
this.$router.push({
name: 'settings-invites',
params: {
workspaceId: this.selectedWorkspace.id,
},
})
}
setShowAdmin(value) {
this.showAdmin = value
this.$forceUpdate()
},
},
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,35 +1,42 @@
<template>
<div class="sidebar">
<div v-show="!collapsed" class="sidebar__nav">
<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"
:key="application.id"
:application="application"
:page="page"
@selected="selectedApplication"
@selected-page="$emit('selected-page', $event)"
></component>
</ul>
</div>
<div class="sidebar__foot">
<div class="sidebar__logo">
<Logo height="14" alt="Baserow logo" />
<div
v-show="!collapsed"
class="sidebar__section sidebar__section--scrollable"
>
<div class="sidebar__section-scrollable">
<div class="sidebar__section-scrollable-inner">
<ul class="tree">
<component
:is="getApplicationComponent(application)"
v-for="application in sortedApplications"
:key="application.id"
:application="application"
:page="page"
@selected="selectedApplication"
@selected-page="$emit('selected-page', $event)"
></component>
</ul>
</div>
</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>
<a class="sidebar__collapse-link" @click="$emit('collapse-toggled')">
<i
:class="{
'iconoir-fast-arrow-right': collapsed,
'iconoir-fast-arrow-left': !collapsed,
}"
></i>
</a>
</div>
<a class="sidebar__collapse-link" @click="$emit('collapse-toggled')">
<i
:class="{
'iconoir-fast-arrow-right': collapsed,
'iconoir-fast-arrow-left': !collapsed,
}"
></i>
</a>
</div>
</div>
</template>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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