mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-24 13:04:06 +00:00
Merge branch '1912-row-change-history' into 'develop'
Introduce row edit history tab Closes #1912 See merge request baserow/baserow!1594
This commit is contained in:
commit
46121e5591
21 changed files with 663 additions and 39 deletions
enterprise/web-frontend/modules/baserow_enterprise
premium/web-frontend/modules/baserow_premium
web-frontend/modules
core
assets/scss/components
components
database
|
@ -12,7 +12,6 @@ import {
|
||||||
GitLabAuthProviderType,
|
GitLabAuthProviderType,
|
||||||
OpenIdConnectAuthProviderType,
|
OpenIdConnectAuthProviderType,
|
||||||
} from '@baserow_enterprise/authProviderTypes'
|
} from '@baserow_enterprise/authProviderTypes'
|
||||||
|
|
||||||
import { TeamsWorkspaceSettingsPageType } from '@baserow_enterprise/workspaceSettingsPageTypes'
|
import { TeamsWorkspaceSettingsPageType } from '@baserow_enterprise/workspaceSettingsPageTypes'
|
||||||
import { EnterpriseMembersPagePluginType } from '@baserow_enterprise/membersPagePluginTypes'
|
import { EnterpriseMembersPagePluginType } from '@baserow_enterprise/membersPagePluginTypes'
|
||||||
import en from '@baserow_enterprise/locales/en.json'
|
import en from '@baserow_enterprise/locales/en.json'
|
||||||
|
@ -25,7 +24,6 @@ import {
|
||||||
EnterpriseWithoutSupportLicenseType,
|
EnterpriseWithoutSupportLicenseType,
|
||||||
EnterpriseLicenseType,
|
EnterpriseLicenseType,
|
||||||
} from '@baserow_enterprise/licenseTypes'
|
} from '@baserow_enterprise/licenseTypes'
|
||||||
|
|
||||||
import { EnterprisePlugin } from '@baserow_enterprise/plugins'
|
import { EnterprisePlugin } from '@baserow_enterprise/plugins'
|
||||||
|
|
||||||
export default (context) => {
|
export default (context) => {
|
||||||
|
|
|
@ -1,18 +1,7 @@
|
||||||
import RowCommentsSidebar from '@baserow_premium/components/row_comments/RowCommentsSidebar'
|
|
||||||
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
|
import { DatabaseApplicationType } from '@baserow/modules/database/applicationTypes'
|
||||||
import GridViewRowExpandButtonWithCommentCount from '@baserow_premium/components/row_comments/GridViewRowExpandButtonWithCommentCount'
|
import GridViewRowExpandButtonWithCommentCount from '@baserow_premium/components/row_comments/GridViewRowExpandButtonWithCommentCount'
|
||||||
|
|
||||||
export class PremiumDatabaseApplicationType extends DatabaseApplicationType {
|
export class PremiumDatabaseApplicationType extends DatabaseApplicationType {
|
||||||
getRowEditModalRightSidebarComponent(database, table) {
|
|
||||||
return this.app.$hasPermission(
|
|
||||||
'database.table.list_comments',
|
|
||||||
table,
|
|
||||||
database.workspace.id
|
|
||||||
)
|
|
||||||
? RowCommentsSidebar
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
getRowExpandButtonComponent() {
|
getRowExpandButtonComponent() {
|
||||||
return GridViewRowExpandButtonWithCommentCount
|
return GridViewRowExpandButtonWithCommentCount
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
"noComment": "No comments for this row yet. Use the form below to add a comment.",
|
"noComment": "No comments for this row yet. Use the form below to add a comment.",
|
||||||
"comment": "Comment",
|
"comment": "Comment",
|
||||||
"more": "More information",
|
"more": "More information",
|
||||||
"name": "Row comments"
|
"name": "Comments"
|
||||||
},
|
},
|
||||||
"rowComment": {
|
"rowComment": {
|
||||||
"you": "You",
|
"you": "You",
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { PremiumLicenseType } from '@baserow_premium/licenseTypes'
|
||||||
import { PersonalViewOwnershipType } from '@baserow_premium/viewOwnershipTypes'
|
import { PersonalViewOwnershipType } from '@baserow_premium/viewOwnershipTypes'
|
||||||
import { ViewOwnershipPermissionManagerType } from '@baserow_premium/permissionManagerTypes'
|
import { ViewOwnershipPermissionManagerType } from '@baserow_premium/permissionManagerTypes'
|
||||||
import { RowCommentMentionNotificationType } from '@baserow_premium/notificationTypes'
|
import { RowCommentMentionNotificationType } from '@baserow_premium/notificationTypes'
|
||||||
|
import { CommentsRowModalSidebarType } from '@baserow_premium/rowModalSidebarTypes'
|
||||||
|
|
||||||
export default (context) => {
|
export default (context) => {
|
||||||
const { store, app, isDev } = context
|
const { store, app, isDev } = context
|
||||||
|
@ -133,4 +134,9 @@ export default (context) => {
|
||||||
'notification',
|
'notification',
|
||||||
new RowCommentMentionNotificationType(context)
|
new RowCommentMentionNotificationType(context)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.$registry.register(
|
||||||
|
'rowModalSidebar',
|
||||||
|
new CommentsRowModalSidebarType(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { RowModalSidebarType } from '@baserow/modules/database/rowModalSidebarTypes'
|
||||||
|
import RowCommentsSidebar from '@baserow_premium/components/row_comments/RowCommentsSidebar'
|
||||||
|
import PremiumFeatures from '@baserow_premium/features'
|
||||||
|
|
||||||
|
export class CommentsRowModalSidebarType extends RowModalSidebarType {
|
||||||
|
static getType() {
|
||||||
|
return 'comments'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.app.i18n.t('rowCommentSidebar.name')
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponent() {
|
||||||
|
return RowCommentsSidebar
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeactivated(database, table) {
|
||||||
|
return !this.app.$hasPermission(
|
||||||
|
'database.table.list_comments',
|
||||||
|
table,
|
||||||
|
database.workspace.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectedByDefault(database) {
|
||||||
|
return this.app.$hasFeature(PremiumFeatures.PREMIUM, database.workspace.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -106,6 +106,9 @@
|
||||||
@import 'snapshots_modal';
|
@import 'snapshots_modal';
|
||||||
@import 'import_modal';
|
@import 'import_modal';
|
||||||
@import 'row_edit_modal';
|
@import 'row_edit_modal';
|
||||||
|
@import 'row_edit_modal_sidebar';
|
||||||
|
@import 'row_history';
|
||||||
|
@import 'row_history_entry';
|
||||||
@import 'data_table';
|
@import 'data_table';
|
||||||
@import 'auth';
|
@import 'auth';
|
||||||
@import 'expand_on_overflow_list';
|
@import 'expand_on_overflow_list';
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.row-edit-modal-sidebar {
|
||||||
|
background-color: $palette-neutral-25;
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
.row-history {
|
||||||
|
@include absolute(0, 0, 0, 0);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history__body {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
|
||||||
|
.infinite-scroll {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history__empty {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 30px 0 30px;
|
||||||
|
height: 100%;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history__empty-icon {
|
||||||
|
font-size: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history__empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 160%;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history__loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
.row-history-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__content {
|
||||||
|
padding: 10px 14px 10px 14px;
|
||||||
|
border: 1px solid #d9dbde;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__name {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 14px;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: #062e47;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row_history-entry__initials {
|
||||||
|
flex: 0 0 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $white;
|
||||||
|
background-color: $color-primary-500;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
@include center-text(24px, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__timestamp {
|
||||||
|
@extend %ellipsis;
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $color-neutral-400;
|
||||||
|
// Prevent long characters like 'g' in the time display being cut off
|
||||||
|
// partway down.
|
||||||
|
height: 14px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__field {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 10px;
|
||||||
|
color: $color-neutral-600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__field-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__diff {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 4px 4px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__diff--removed {
|
||||||
|
background-color: #f5d3ce;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-history-entry__diff--added {
|
||||||
|
background-color: #c2f0d3;
|
||||||
|
}
|
|
@ -27,6 +27,11 @@
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
padding: 0 0 0 40px;
|
padding: 0 0 0 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.tabs__header--full-width {
|
||||||
|
gap: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs__item {
|
.tabs__item {
|
||||||
|
@ -50,6 +55,11 @@
|
||||||
|
|
||||||
@include absolute(auto, 0, -1px, 0);
|
@include absolute(auto, 0, -1px, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.tabs__item--full-width {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs__link {
|
.tabs__link {
|
||||||
|
|
|
@ -7,7 +7,13 @@
|
||||||
'tabs--large': large,
|
'tabs--large': large,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ul class="tabs__header">
|
<ul
|
||||||
|
v-if="!collapseOneTab || tabs.length > 1"
|
||||||
|
class="tabs__header"
|
||||||
|
:class="{
|
||||||
|
'tabs__header--full-width': fullWidthHeader,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
v-for="(tab, index) in tabs"
|
v-for="(tab, index) in tabs"
|
||||||
:key="tab.title"
|
:key="tab.title"
|
||||||
|
@ -16,6 +22,7 @@
|
||||||
:class="{
|
:class="{
|
||||||
'tabs__item--active': isActive(index),
|
'tabs__item--active': isActive(index),
|
||||||
'tabs__item--disabled': tab.disabled,
|
'tabs__item--disabled': tab.disabled,
|
||||||
|
'tabs__item--full-width': fullWidthHeader,
|
||||||
}"
|
}"
|
||||||
@click="tab.disabled ? null : selectTab(index)"
|
@click="tab.disabled ? null : selectTab(index)"
|
||||||
>
|
>
|
||||||
|
@ -62,6 +69,16 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
fullWidthHeader: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
collapseOneTab: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -11,18 +11,6 @@ export class DatabaseApplicationType extends ApplicationType {
|
||||||
return 'database'
|
return 'database'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* By default there is no right sidebar in the row edit modal. Override this method
|
|
||||||
* and provide a Sidebar component class if you wish there to be one. This component
|
|
||||||
* will be provided two props row and table.
|
|
||||||
*
|
|
||||||
* @return The component to use as the row edit modal's right sidebar or null to not
|
|
||||||
* use one.
|
|
||||||
*/
|
|
||||||
getRowEditModalRightSidebarComponent(database, table) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The component to use as the button the user clicks to expand and view the
|
* @return The component to use as the button the user clicks to expand and view the
|
||||||
* row edit modal for a particular row. Takes a single row prop and should emit a
|
* row edit modal for a particular row. Takes a single row prop and should emit a
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
ref="modal"
|
ref="modal"
|
||||||
:full-height="!!optionalRightSideBar"
|
:full-height="hasRightSidebar"
|
||||||
:right-sidebar="!!optionalRightSideBar"
|
:right-sidebar="hasRightSidebar"
|
||||||
:content-scrollable="!!optionalRightSideBar"
|
:content-scrollable="hasRightSidebar"
|
||||||
:right-sidebar-scrollable="false"
|
:right-sidebar-scrollable="false"
|
||||||
:collapsible-right-sidebar="true"
|
:collapsible-right-sidebar="true"
|
||||||
@hidden="$emit('hidden', { row })"
|
@hidden="$emit('hidden', { row })"
|
||||||
|
@ -92,13 +92,12 @@
|
||||||
></CreateFieldContext>
|
></CreateFieldContext>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!!optionalRightSideBar" #sidebar>
|
<template #sidebar>
|
||||||
<component
|
<RowEditModalSidebar
|
||||||
:is="optionalRightSideBar"
|
|
||||||
:row="row"
|
:row="row"
|
||||||
:table="table"
|
:table="table"
|
||||||
:database="database"
|
:database="database"
|
||||||
></component>
|
></RowEditModalSidebar>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
@ -109,6 +108,7 @@ import modal from '@baserow/modules/core/mixins/modal'
|
||||||
import CreateFieldContext from '@baserow/modules/database/components/field/CreateFieldContext'
|
import CreateFieldContext from '@baserow/modules/database/components/field/CreateFieldContext'
|
||||||
import RowEditModalFieldsList from './RowEditModalFieldsList.vue'
|
import RowEditModalFieldsList from './RowEditModalFieldsList.vue'
|
||||||
import RowEditModalHiddenFieldsSection from './RowEditModalHiddenFieldsSection.vue'
|
import RowEditModalHiddenFieldsSection from './RowEditModalHiddenFieldsSection.vue'
|
||||||
|
import RowEditModalSidebar from './RowEditModalSidebar.vue'
|
||||||
import { getPrimaryOrFirstField } from '@baserow/modules/database/utils/field'
|
import { getPrimaryOrFirstField } from '@baserow/modules/database/utils/field'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -117,6 +117,7 @@ export default {
|
||||||
CreateFieldContext,
|
CreateFieldContext,
|
||||||
RowEditModalFieldsList,
|
RowEditModalFieldsList,
|
||||||
RowEditModalHiddenFieldsSection,
|
RowEditModalHiddenFieldsSection,
|
||||||
|
RowEditModalSidebar,
|
||||||
},
|
},
|
||||||
mixins: [modal],
|
mixins: [modal],
|
||||||
props: {
|
props: {
|
||||||
|
@ -165,11 +166,6 @@ export default {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
navigationLoading: 'rowModalNavigation/getLoading',
|
navigationLoading: 'rowModalNavigation/getLoading',
|
||||||
}),
|
}),
|
||||||
optionalRightSideBar() {
|
|
||||||
return this.$registry
|
|
||||||
.get('application', 'database')
|
|
||||||
.getRowEditModalRightSidebarComponent(this.database, this.table)
|
|
||||||
},
|
|
||||||
modalRow() {
|
modalRow() {
|
||||||
return this.$store.getters['rowModal/get'](this._uid)
|
return this.$store.getters['rowModal/get'](this._uid)
|
||||||
},
|
},
|
||||||
|
@ -209,6 +205,15 @@ export default {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
hasRightSidebar() {
|
||||||
|
const allSidebarTypes = this.$registry.getOrderedList('rowModalSidebar')
|
||||||
|
const activeSidebarTypes = allSidebarTypes.filter(
|
||||||
|
(type) =>
|
||||||
|
type.isDeactivated(this.database, this.table) === false &&
|
||||||
|
type.getComponent()
|
||||||
|
)
|
||||||
|
return activeSidebarTypes.length > 0
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<Tabs
|
||||||
|
:selected-index="selectedTabIndex"
|
||||||
|
:full-height="true"
|
||||||
|
:collapse-one-tab="true"
|
||||||
|
:large="true"
|
||||||
|
:full-width-header="true"
|
||||||
|
class="row-edit-modal-sidebar"
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
v-for="sidebarType in sidebarTypes"
|
||||||
|
:key="sidebarType.getType()"
|
||||||
|
:title="sidebarType.getName()"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="sidebarType.getComponent()"
|
||||||
|
:row="row"
|
||||||
|
:table="table"
|
||||||
|
:database="database"
|
||||||
|
></component>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Tabs from '@baserow/modules/core/components/Tabs.vue'
|
||||||
|
import Tab from '@baserow/modules/core/components/Tab.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RowEditModalSidebar',
|
||||||
|
components: {
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
database: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedTabIndex() {
|
||||||
|
const types = this.sidebarTypes
|
||||||
|
const index = types.findIndex((type) =>
|
||||||
|
type.isSelectedByDefault(this.database, this.table)
|
||||||
|
)
|
||||||
|
return Math.max(index, 0)
|
||||||
|
},
|
||||||
|
sidebarTypes() {
|
||||||
|
const allSidebarTypes = this.$registry.getOrderedList('rowModalSidebar')
|
||||||
|
return allSidebarTypes.filter(
|
||||||
|
(type) =>
|
||||||
|
type.isDeactivated(this.database, this.table) === false &&
|
||||||
|
type.getComponent()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div class="row-history-entry">
|
||||||
|
<div class="row-history-entry__header">
|
||||||
|
<span class="row_history-entry__initials">{{ initials }}</span>
|
||||||
|
<span class="row-history-entry__name">{{ name }}</span>
|
||||||
|
<span class="row-history-entry__timestamp" :title="timestampTooltip">{{
|
||||||
|
formattedTimestamp
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row-history-entry__content">
|
||||||
|
<template v-for="field in entryFields">
|
||||||
|
<div :key="field" class="row-history-entry__field">{{ field }}</div>
|
||||||
|
<div :key="field + 'content'" class="row-history-entry__field-content">
|
||||||
|
<div v-if="entry.before[field]">
|
||||||
|
<div
|
||||||
|
class="row-history-entry__diff row-history-entry__diff--removed"
|
||||||
|
>
|
||||||
|
{{ entry.before[field] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="entry.after[field]">
|
||||||
|
<div class="row-history-entry__diff row-history-entry__diff--added">
|
||||||
|
{{ entry.after[field] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from '@baserow/modules/core/moment'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RowHistoryEntry',
|
||||||
|
props: {
|
||||||
|
entry: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
return this.entry.user.name
|
||||||
|
},
|
||||||
|
initials() {
|
||||||
|
return this.name.slice(0, 1).toUpperCase()
|
||||||
|
},
|
||||||
|
timestampTooltip() {
|
||||||
|
return this.getLocalizedMoment(this.entry.timestamp).format('L LT')
|
||||||
|
},
|
||||||
|
formattedTimestamp() {
|
||||||
|
return this.getLocalizedMoment(this.entry.timestamp).format('LT')
|
||||||
|
},
|
||||||
|
entryFields() {
|
||||||
|
return new Set(
|
||||||
|
Object.keys(this.entry.before).concat(Object.keys(this.entry.after))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getLocalizedMoment(timestamp) {
|
||||||
|
return moment.utc(timestamp).tz(moment.tz.guess())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="!loaded && loading" class="loading-absolute-center" />
|
||||||
|
<template v-else>
|
||||||
|
<div class="row-history">
|
||||||
|
<div v-if="totalCount > 0">
|
||||||
|
<InfiniteScroll
|
||||||
|
ref="infiniteScroll"
|
||||||
|
:current-count="currentCount"
|
||||||
|
:max-count="totalCount"
|
||||||
|
:loading="loading"
|
||||||
|
:reverse="true"
|
||||||
|
:render-end="false"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<RowHistoryEntry
|
||||||
|
v-for="entry in entries"
|
||||||
|
:key="entry.id"
|
||||||
|
:entry="entry"
|
||||||
|
>
|
||||||
|
</RowHistoryEntry>
|
||||||
|
</template>
|
||||||
|
</InfiniteScroll>
|
||||||
|
</div>
|
||||||
|
<div v-else class="row-history__empty">
|
||||||
|
<i class="row-history__empty-icon fas fa-history"></i>
|
||||||
|
<div class="row-history__empty-text">
|
||||||
|
{{ $t('rowHistorySidebar.empty') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
|
import InfiniteScroll from '@baserow/modules/core/components/helpers/InfiniteScroll'
|
||||||
|
import RowHistoryEntry from '@baserow/modules/database/components/row/RowHistoryEntry.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RowHistorySidebar',
|
||||||
|
components: {
|
||||||
|
InfiniteScroll,
|
||||||
|
RowHistoryEntry,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
database: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
entries: 'rowHistory/getSortedEntries',
|
||||||
|
loading: 'rowHistory/getLoading',
|
||||||
|
loaded: 'rowHistory/getLoaded',
|
||||||
|
currentCount: 'rowHistory/getCurrentCount',
|
||||||
|
totalCount: 'rowHistory/getTotalCount',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.initialLoad()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async initialLoad() {
|
||||||
|
try {
|
||||||
|
const tableId = this.table.id
|
||||||
|
const rowId = this.row.id
|
||||||
|
await this.$store.dispatch('rowHistory/fetchInitial', {
|
||||||
|
tableId,
|
||||||
|
rowId,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
notifyIf(e, 'application')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -773,5 +773,9 @@
|
||||||
},
|
},
|
||||||
"collaboratorAddedToRowNotification": {
|
"collaboratorAddedToRowNotification": {
|
||||||
"title": "{sender} assigned you to {fieldName} in row {rowId} in {tableName}"
|
"title": "{sender} assigned you to {fieldName} in row {rowId} in {tableName}"
|
||||||
|
},
|
||||||
|
"rowHistorySidebar": {
|
||||||
|
"name": "History",
|
||||||
|
"empty": "No changes yet. You'll be able to track any changes to this row here."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,6 +103,7 @@ import formStore from '@baserow/modules/database/store/view/form'
|
||||||
import rowModal from '@baserow/modules/database/store/rowModal'
|
import rowModal from '@baserow/modules/database/store/rowModal'
|
||||||
import publicStore from '@baserow/modules/database/store/view/public'
|
import publicStore from '@baserow/modules/database/store/view/public'
|
||||||
import rowModalNavigationStore from '@baserow/modules/database/store/rowModalNavigation'
|
import rowModalNavigationStore from '@baserow/modules/database/store/rowModalNavigation'
|
||||||
|
import rowHistoryStore from '@baserow/modules/database/store/rowHistory'
|
||||||
|
|
||||||
import { registerRealtimeEvents } from '@baserow/modules/database/realtime'
|
import { registerRealtimeEvents } from '@baserow/modules/database/realtime'
|
||||||
import { CSVTableExporterType } from '@baserow/modules/database/exporterTypes'
|
import { CSVTableExporterType } from '@baserow/modules/database/exporterTypes'
|
||||||
|
@ -229,6 +230,7 @@ import { FormViewFormModeType } from '@baserow/modules/database/formViewModeType
|
||||||
import { CollaborativeViewOwnershipType } from '@baserow/modules/database/viewOwnershipTypes'
|
import { CollaborativeViewOwnershipType } from '@baserow/modules/database/viewOwnershipTypes'
|
||||||
import { DatabasePlugin } from '@baserow/modules/database/plugins'
|
import { DatabasePlugin } from '@baserow/modules/database/plugins'
|
||||||
import { CollaboratorAddedToRowNotificationType } from '@baserow/modules/database/notificationTypes'
|
import { CollaboratorAddedToRowNotificationType } from '@baserow/modules/database/notificationTypes'
|
||||||
|
import { HistoryRowModalSidebarType } from '@baserow/modules/database/rowModalSidebarTypes'
|
||||||
|
|
||||||
import en from '@baserow/modules/database/locales/en.json'
|
import en from '@baserow/modules/database/locales/en.json'
|
||||||
import fr from '@baserow/modules/database/locales/fr.json'
|
import fr from '@baserow/modules/database/locales/fr.json'
|
||||||
|
@ -258,6 +260,7 @@ export default (context) => {
|
||||||
store.registerModule('field', fieldStore)
|
store.registerModule('field', fieldStore)
|
||||||
store.registerModule('rowModal', rowModal)
|
store.registerModule('rowModal', rowModal)
|
||||||
store.registerModule('rowModalNavigation', rowModalNavigationStore)
|
store.registerModule('rowModalNavigation', rowModalNavigationStore)
|
||||||
|
store.registerModule('rowHistory', rowHistoryStore)
|
||||||
store.registerModule('page/view/grid', gridStore)
|
store.registerModule('page/view/grid', gridStore)
|
||||||
store.registerModule('page/view/gallery', galleryStore)
|
store.registerModule('page/view/gallery', galleryStore)
|
||||||
store.registerModule('page/view/form', formStore)
|
store.registerModule('page/view/form', formStore)
|
||||||
|
@ -646,5 +649,10 @@ export default (context) => {
|
||||||
new CollaboratorAddedToRowNotificationType(context)
|
new CollaboratorAddedToRowNotificationType(context)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.$registry.register(
|
||||||
|
'rowModalSidebar',
|
||||||
|
new HistoryRowModalSidebarType(context)
|
||||||
|
)
|
||||||
|
|
||||||
registerRealtimeEvents(app.$realtime)
|
registerRealtimeEvents(app.$realtime)
|
||||||
}
|
}
|
||||||
|
|
76
web-frontend/modules/database/rowModalSidebarTypes.js
Normal file
76
web-frontend/modules/database/rowModalSidebarTypes.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { Registerable } from '@baserow/modules/core/registry'
|
||||||
|
import RowHistorySidebar from '@baserow/modules/database/components/row/RowHistorySidebar.vue'
|
||||||
|
import {
|
||||||
|
featureFlagIsEnabled,
|
||||||
|
getFeatureFlags,
|
||||||
|
} from '@baserow/modules/core/utils/env'
|
||||||
|
|
||||||
|
export class RowModalSidebarType extends Registerable {
|
||||||
|
/**
|
||||||
|
* A human readable name
|
||||||
|
*/
|
||||||
|
getName() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component to render in the sidebar
|
||||||
|
*/
|
||||||
|
getComponent() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the sidebar component should be shown
|
||||||
|
*/
|
||||||
|
isDeactivated(database, table) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true, the sidebar type indicates
|
||||||
|
* that it should be focused first.
|
||||||
|
*/
|
||||||
|
isSelectedByDefault(database) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HistoryRowModalSidebarType extends RowModalSidebarType {
|
||||||
|
static getType() {
|
||||||
|
return 'history'
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.app.i18n.t('rowHistorySidebar.name')
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponent() {
|
||||||
|
return RowHistorySidebar
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeactivated(database, table) {
|
||||||
|
const featureFlags = getFeatureFlags(this.app.$config)
|
||||||
|
const featureFlagEnabled = featureFlagIsEnabled(featureFlags, 'row_history')
|
||||||
|
return !featureFlagEnabled
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// return this.app.$hasPermission(
|
||||||
|
// 'database.table.read_row_history',
|
||||||
|
// table,
|
||||||
|
// database.workspace.id
|
||||||
|
// )
|
||||||
|
}
|
||||||
|
|
||||||
|
isSelectedByDefault(database) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
}
|
47
web-frontend/modules/database/services/rowHistory.js
Normal file
47
web-frontend/modules/database/services/rowHistory.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
export default (client) => {
|
||||||
|
return {
|
||||||
|
fetchAll({ tableId, rowId }) {
|
||||||
|
// mocked for now
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
count: 2,
|
||||||
|
next: null,
|
||||||
|
previous: null,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
action_type: 'update_row',
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
name: 'John Wick',
|
||||||
|
},
|
||||||
|
timestamp: '2023-08-09T00:30:00Z',
|
||||||
|
before: {
|
||||||
|
field_1: 'a',
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
field_1: 'aa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
action_type: 'update_row',
|
||||||
|
user: {
|
||||||
|
id: 2,
|
||||||
|
name: 'Paul Smith',
|
||||||
|
},
|
||||||
|
timestamp: '2023-08-09T00:30:00Z',
|
||||||
|
before: {
|
||||||
|
field_2: 'a',
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
field_1: 'aa',
|
||||||
|
field_2: 'a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
79
web-frontend/modules/database/store/rowHistory.js
Normal file
79
web-frontend/modules/database/store/rowHistory.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import RowHistoryService from '@baserow/modules/database/services/rowHistory'
|
||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
entries: [],
|
||||||
|
loading: false,
|
||||||
|
loaded: false,
|
||||||
|
totalCount: 0,
|
||||||
|
loadedRowId: false,
|
||||||
|
loadedTableId: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
ADD_ENTRIES(state, { entries }) {
|
||||||
|
state.entries = entries
|
||||||
|
},
|
||||||
|
RESET_ENTRIES(state) {
|
||||||
|
state.entries = []
|
||||||
|
state.totalCount = 0
|
||||||
|
},
|
||||||
|
SET_LOADING(state, loading) {
|
||||||
|
state.loading = loading
|
||||||
|
},
|
||||||
|
SET_LOADED(state, loaded) {
|
||||||
|
state.loaded = loaded
|
||||||
|
},
|
||||||
|
SET_LOADED_TABLE_AND_ROW(state, { tableId, rowId }) {
|
||||||
|
state.loadedRowId = rowId
|
||||||
|
state.loadedTableId = tableId
|
||||||
|
},
|
||||||
|
SET_TOTAL_COUNT(state, totalCount) {
|
||||||
|
state.totalCount = totalCount
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
async fetchInitial({ commit }, { tableId, rowId }) {
|
||||||
|
commit('RESET_ENTRIES')
|
||||||
|
commit('SET_LOADING', true)
|
||||||
|
commit('SET_LOADED', false)
|
||||||
|
try {
|
||||||
|
const { data } = await RowHistoryService(this.$client).fetchAll(
|
||||||
|
tableId,
|
||||||
|
rowId
|
||||||
|
)
|
||||||
|
commit('ADD_ENTRIES', { entries: data.results })
|
||||||
|
commit('SET_TOTAL_COUNT', data.count)
|
||||||
|
commit('SET_LOADED_TABLE_AND_ROW', { tableId, rowId })
|
||||||
|
commit('SET_LOADED', true)
|
||||||
|
} finally {
|
||||||
|
commit('SET_LOADING', false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getSortedEntries(state) {
|
||||||
|
return state.entries
|
||||||
|
},
|
||||||
|
getCurrentCount(state) {
|
||||||
|
return state.entries.length
|
||||||
|
},
|
||||||
|
getTotalCount(state) {
|
||||||
|
return state.totalCount
|
||||||
|
},
|
||||||
|
getLoading(state) {
|
||||||
|
return state.loading
|
||||||
|
},
|
||||||
|
getLoaded(state) {
|
||||||
|
return state.loaded
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue