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

🌈 5️⃣ - Row coloring v5 - Add color to Gallery and Kanban view types

This commit is contained in:
Jrmi 2022-05-17 06:49:29 +00:00
parent df48bf7986
commit 224d5dfee3
30 changed files with 2234 additions and 1102 deletions

View file

@ -290,6 +290,7 @@ class GalleryViewType(ViewType):
api_exceptions_map = {
FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE,
}
can_decorate = True
def get_api_urls(self):
from baserow.contrib.database.api.views.gallery import urls as api_urls

View file

@ -2,6 +2,8 @@
## Unreleased
* Added row coloring for Kanban and Gallery views
## Released (2022-10-05 1.10.0)
* Added batch create/update/delete rows endpoints. These endpoints make it possible to

View file

@ -52,6 +52,7 @@ class KanbanViewType(ViewType):
),
FieldNotInTable: ERROR_FIELD_NOT_IN_TABLE,
}
can_decorate = True
def get_api_urls(self):
from baserow_premium.api.views.kanban import urls as api_urls

View file

@ -1,8 +1,10 @@
.left-border-decorator {
width: 6px;
height: 24px;
margin: 0 4px;
align-self: stretch;
flex-shrink: 0;
flex-grow: 0;
border-radius: 6px;
margin: 4px;
@include add-shadow-on-same-background();
}

View file

@ -76,6 +76,7 @@
v-show="slot.position != -1"
:key="'card-' + slot.id"
:fields="cardFields"
:decorations-by-place="decorationsByPlace"
:row="slot.row"
:cover-image-field="coverImageField"
:style="{
@ -138,11 +139,12 @@ import InfiniteScroll from '@baserow/modules/core/components/helpers/InfiniteScr
import { populateRow } from '@baserow_premium/store/view/kanban'
import KanbanViewStackContext from '@baserow_premium/components/views/kanban/KanbanViewStackContext'
import { getCardHeight } from '@baserow/modules/database/utils/card'
import viewDecoration from '@baserow/modules/database/mixins/viewDecoration'
export default {
name: 'KanbanViewStack',
components: { InfiniteScroll, RowCard, KanbanViewStackContext },
mixins: [kanbanViewHelper],
mixins: [kanbanViewHelper, viewDecoration],
props: {
option: {
validator: (prop) => typeof prop === 'object' || prop === null,

View file

@ -107,7 +107,11 @@ export class ConditionalColorValueProviderType extends DecoratorValueProviderTyp
getValue({ options, fields, row }) {
const { $registry } = this.app
for (const { color, filters, operator } of options.colors) {
if (matchSearchFilters($registry, operator, filters, fields, row)) {
if (
row.id !== -1 &&
row.id !== undefined &&
matchSearchFilters($registry, operator, filters, fields, row)
) {
return color
}
}

View file

@ -1,6 +1,13 @@
import { ViewDecoratorType } from '@baserow/modules/database/viewDecorators'
import { PremiumPlugin } from '@baserow_premium/plugins'
import {
GridViewType,
GalleryViewType,
} from '@baserow/modules/database/viewTypes'
import { KanbanViewType } from './viewTypes'
import leftBorderDecoratorImage from '@baserow_premium/assets/images/leftBorderDecorator.svg'
import backgroundDecoratorImage from '@baserow_premium/assets/images/backgroundDecorator.svg'
@ -71,7 +78,13 @@ export class LeftBorderColorViewDecoratorType extends ViewDecoratorType {
isCompatible(view) {
const { store } = this.app
return ['grid'].includes(view.type) && !store.getters['view/grid/isPublic']
return (
[
GridViewType.getType(),
GalleryViewType.getType(),
KanbanViewType.getType(),
].includes(view.type) && !store.getters['view/grid/isPublic']
)
}
}
@ -114,7 +127,13 @@ export class BackgroundColorViewDecoratorType extends ViewDecoratorType {
isCompatible(view) {
const { store } = this.app
return ['grid'].includes(view.type) && !store.getters['view/grid/isPublic']
return (
[
GridViewType.getType(),
GalleryViewType.getType(),
KanbanViewType.getType(),
].includes(view.type) && !store.getters['view/grid/isPublic']
)
}
getDeactivatedText() {

View file

@ -68,8 +68,13 @@
background-position: center center;
}
.card__content {
display: flex;
}
.card__fields {
padding: 16px 0;
overflow: auto;
}
.card__field {

View file

@ -6,4 +6,5 @@
@extend %ellipsis;
@include select-option-style(inline-block);
@include add-shadow-on-same-background();
}

View file

@ -6,41 +6,65 @@
@mousemove="$emit('mousemove', $event)"
@mouseenter="$emit('mouseenter', $event)"
>
<div v-if="coverImageField !== null" class="card__cover">
<div
v-if="coverImageUrl !== null"
class="card__cover-image"
:style="{
'background-image': 'url(' + coverImageUrl + ')',
}"
></div>
</div>
<div class="card__fields">
<div v-for="field in fields" :key="field.id" class="card__field">
<div class="card__field-name">{{ field.name }}</div>
<div class="card__field-value">
<component
:is="getCardComponent(field)"
v-if="!loading"
:field="field"
:value="row['field_' + field.id]"
/>
<RecursiveWrapper
:components="
wrapperDecorations.map((comp) => ({
...comp,
props: comp.propsFn(row),
}))
"
>
<div v-if="coverImageField !== null" class="card__cover">
<div
v-if="coverImageUrl !== null"
class="card__cover-image"
:style="{
'background-image': 'url(' + coverImageUrl + ')',
}"
></div>
</div>
<div class="card__content">
<component
:is="dec.component"
v-for="dec in firstCellDecorations"
:key="dec.decoration.id"
v-bind="dec.propsFn(row)"
/>
<div class="card__fields">
<div v-for="field in fields" :key="field.id" class="card__field">
<div class="card__field-name">{{ field.name }}</div>
<div class="card__field-value">
<component
:is="getCardComponent(field)"
v-if="!loading"
:field="field"
:value="row['field_' + field.id]"
/>
</div>
</div>
</div>
</div>
</div>
</RecursiveWrapper>
</div>
</template>
<script>
import { FileFieldType } from '@baserow/modules/database/fieldTypes'
import RecursiveWrapper from '@baserow/modules/database/components/RecursiveWrapper'
export default {
name: 'RowCard',
components: { RecursiveWrapper },
props: {
fields: {
type: Array,
required: true,
},
decorationsByPlace: {
type: Object,
required: false,
default: () => {},
},
row: {
type: Object,
required: false,
@ -80,6 +104,12 @@ export default {
return image.thumbnails.card_cover.url
},
firstCellDecorations() {
return this.decorationsByPlace?.first_cell || []
},
wrapperDecorations() {
return this.decorationsByPlace?.wrapper || []
},
},
methods: {
getCardComponent(field) {

View file

@ -17,11 +17,11 @@
<ViewDecoratorItem :decorator-type="dec.decoratorType" />
</div>
<div
v-show="dec.decoration.value_provider_type"
v-show="dec.valueProviderType"
class="decorator-context__decorator-header-select"
>
<Picker
v-if="dec.decoration.value_provider_type"
v-if="dec.valueProviderType"
:icon="dec.valueProviderType.getIconClass()"
:name="dec.valueProviderType.getName()"
@select="selectValueProvider(dec.decoration, $event)"
@ -104,6 +104,7 @@
<script>
import context from '@baserow/modules/core/mixins/context'
import viewDecoration from '@baserow/modules/database/mixins/viewDecoration'
import ViewDecoratorList from '@baserow/modules/database/components/view/ViewDecoratorList'
import ViewDecoratorItem from '@baserow/modules/database/components/view/ViewDecoratorItem'
import SelectViewDecoratorContext from '@baserow/modules/database/components/view/SelectViewDecoratorContext'
@ -120,7 +121,7 @@ export default {
SelectViewDecoratorContext,
DecoratorValueProviderList,
},
mixins: [context],
mixins: [context, viewDecoration],
props: {
primary: {
type: Object,
@ -143,42 +144,6 @@ export default {
required: true,
},
},
computed: {
allFields() {
return [this.primary, ...this.fields]
},
augmentedDecorations() {
return this.view.decorations.map((decoration) => {
const deco = { decoration }
deco.decoratorType = this.$registry.get(
'viewDecorator',
decoration.type
)
if (decoration.value_provider_type) {
deco.valueProviderType = this.$registry.get(
'decoratorValueProvider',
decoration.value_provider_type
)
}
deco.availableValueProviderTypes = this.$registry
.getOrderedList('decoratorValueProvider')
.filter((valueProviderType) =>
valueProviderType.isCompatible(deco.decoratorType)
)
deco.isDeactivated = deco.decoratorType.isDeactivated({
view: this.view,
})
return deco
})
},
activeDecorations() {
return this.augmentedDecorations.filter((deco) => !deco.isDeactivated)
},
},
methods: {
async removeDecoration(decoration) {
try {
@ -202,7 +167,7 @@ export default {
value_provider_type: valueProviderType.getType(),
value_provider_conf: valueProviderType.getDefaultConfiguration({
view: this.view,
fields: this.allFields,
fields: this.allTableFields,
}),
},
decoration,

View file

@ -33,6 +33,7 @@
:row="slot.item || {}"
:loading="slot.item === null"
:cover-image-field="coverImageField"
:decorations-by-place="decorationsByPlace"
class="gallery-view__card"
:style="{
width: cardWidth + 'px',
@ -93,11 +94,12 @@ import RowCreateModal from '@baserow/modules/database/components/row/RowCreateMo
import RowEditModal from '@baserow/modules/database/components/row/RowEditModal'
import bufferedRowsDragAndDrop from '@baserow/modules/database/mixins/bufferedRowsDragAndDrop'
import viewHelpers from '@baserow/modules/database/mixins/viewHelpers'
import viewDecoration from '@baserow/modules/database/mixins/viewDecoration'
export default {
name: 'GalleryView',
components: { RowCard, RowCreateModal, RowEditModal },
mixins: [viewHelpers, bufferedRowsDragAndDrop],
mixins: [viewHelpers, bufferedRowsDragAndDrop, viewDecoration],
props: {
primary: {
type: Object,

View file

@ -16,7 +16,7 @@
ref="left"
class="grid-view__left"
:fields="leftFields"
:all-table-fields="allTableFields"
:decorations-by-place="decorationsByPlace"
:table="table"
:view="view"
:include-field-width-handles="false"
@ -66,7 +66,7 @@
ref="right"
class="grid-view__right"
:fields="visibleFields"
:all-table-fields="allTableFields"
:decorations-by-place="decorationsByPlace"
:table="table"
:view="view"
:include-add-field="true"
@ -188,6 +188,7 @@ import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
import { maxPossibleOrderValue } from '@baserow/modules/database/viewTypes'
import viewHelpers from '@baserow/modules/database/mixins/viewHelpers'
import { isElement } from '@baserow/modules/core/utils/dom'
import viewDecoration from '@baserow/modules/database/mixins/viewDecoration'
export default {
name: 'GridView',
@ -197,7 +198,7 @@ export default {
GridViewRowDragging,
RowEditModal,
},
mixins: [viewHelpers, gridViewHelpers],
mixins: [viewHelpers, gridViewHelpers, viewDecoration],
props: {
primary: {
type: Object,
@ -280,9 +281,6 @@ export default {
leftWidth() {
return this.leftFieldsWidth + this.gridViewRowDetailsWidth
},
allTableFields() {
return [this.primary, ...this.fields]
},
},
watch: {
fieldOptions: {

View file

@ -128,10 +128,9 @@ export default {
type: Array,
required: true,
},
decorations: {
type: Array,
required: false,
default: () => [],
decorationsByPlace: {
type: Object,
required: true,
},
allFields: {
type: Array,
@ -212,10 +211,10 @@ export default {
return fields
},
firstCellDecorations() {
return this.decorations.filter(({ place }) => place === 'first_cell')
return this.decorationsByPlace?.first_cell || []
},
wrapperDecorations() {
return this.decorations.filter(({ place }) => place === 'wrapper')
return this.decorationsByPlace?.wrapper || []
},
},
methods: {

View file

@ -13,7 +13,7 @@
:all-fields="allFields"
:field-widths="fieldWidths"
:include-row-details="includeRowDetails"
:decorations="augmentedDecorations"
:decorations-by-place="decorationsByPlace"
:read-only="readOnly"
:can-drag="view.sortings.length === 0"
:store-prefix="storePrefix"
@ -41,8 +41,8 @@ export default {
type: Array,
required: true,
},
allTableFields: {
type: Array,
decorationsByPlace: {
type: Object,
required: true,
},
leftOffset: {
@ -72,42 +72,6 @@ export default {
})
return fieldWidths
},
augmentedDecorations() {
return this.view.decorations
.filter(({ value_provider_type: valPro }) => valPro)
.map((decoration) => {
const deco = { decoration }
deco.decorationType = this.$registry.get(
'viewDecorator',
decoration.type
)
deco.component = deco.decorationType.getComponent()
deco.place = deco.decorationType.getPlace()
deco.valueProviderType = this.$registry.get(
'decoratorValueProvider',
decoration.value_provider_type
)
deco.propsFn = (row) => {
return {
value: deco.valueProviderType.getValue({
row,
fields: this.allTableFields,
options: decoration.value_provider_conf,
}),
}
}
return deco
})
.filter(
({ decorationType }) =>
!decorationType.isDeactivated({ view: this.view })
)
},
},
beforeCreate() {
this.$options.computed = {

View file

@ -52,7 +52,7 @@
:view="view"
:fields="fieldsToRender"
:all-fields="fields"
:all-table-fields="allTableFields"
:decorations-by-place="decorationsByPlace"
:left-offset="fieldsLeftOffset"
:include-row-details="includeRowDetails"
:read-only="readOnly"
@ -127,8 +127,8 @@ export default {
type: Array,
required: true,
},
allTableFields: {
type: Array,
decorationsByPlace: {
type: Object,
required: true,
},
table: {

View file

@ -0,0 +1,75 @@
/**
* A mixin that can be used in combination with the view filter input components. If
* contains the expected props and it has a computed property that finds the field
* object related to filter field id.
*/
export default {
props: {
view: {
type: Object,
required: false,
default: undefined,
},
fields: {
type: Array,
required: true,
},
primary: {
type: Object,
required: true,
},
},
computed: {
allTableFields() {
return [this.primary, ...this.fields]
},
activeDecorations() {
return this.view.decorations
.map((decoration) => {
const deco = { decoration }
deco.decoratorType = this.$registry.get(
'viewDecorator',
decoration.type
)
deco.component = deco.decoratorType.getComponent()
deco.place = deco.decoratorType.getPlace()
if (decoration.value_provider_type) {
deco.valueProviderType = this.$registry.get(
'decoratorValueProvider',
decoration.value_provider_type
)
deco.propsFn = (row) => {
return {
value: deco.valueProviderType.getValue({
row,
fields: this.allTableFields,
options: decoration.value_provider_conf,
}),
}
}
}
return deco
})
.filter(
({ decoratorType }) =>
!decoratorType.isDeactivated({ view: this.view })
)
},
decorationsByPlace() {
return this.activeDecorations
.filter(({ valueProviderType }) => valueProviderType)
.reduce((prev, deco) => {
if (deco.valueProviderType) {
const decType = deco.decoratorType.getPlace()
prev[decType] = [...(prev[decType] || []), deco]
}
return prev
}, {})
},
},
}

49
web-frontend/test/fixtures/gallery.js vendored Normal file
View file

@ -0,0 +1,49 @@
export function createGalleryView(
mock,
application,
table,
{
viewType = 'gallery',
viewId = 1,
filters = [],
sortings = [],
decorations = [],
}
) {
const tableId = table.id
const galleryView = {
id: viewId,
table_id: tableId,
name: `mock_view_${viewId}`,
order: 0,
type: viewType,
table: {
id: tableId,
name: table.name,
order: 0,
database_id: application.id,
},
filter_type: 'AND',
filters_disabled: false,
filters,
sortings,
decorations,
}
mock.onGet(`/database/views/table/${tableId}/`).reply(200, [galleryView])
return galleryView
}
export function createGalleryRows(mock, view, fields, rows = []) {
const fieldOptions = {}
for (let i = 1; i < fields.length; i++) {
fieldOptions[i] = {
hidden: false,
order: i,
}
}
mock.onGet(`/database/views/gallery/${view.id}/`).reply(200, {
count: rows.length,
results: rows,
field_options: fieldOptions,
})
}

View file

@ -1,4 +1,4 @@
export function createRows(mock, gridView, fields, rows = []) {
export function createGridRows(mock, gridView, fields, rows = []) {
const fieldOptions = {}
for (let i = 1; i < fields.length; i++) {
fieldOptions[i] = {

View file

@ -7,8 +7,12 @@ import {
import { createFields } from '@baserow/test/fixtures/fields'
import {
createPublicGridViewRows,
createRows,
createGridRows,
} from '@baserow/test/fixtures/grid'
import {
createGalleryRows,
createGalleryView,
} from '@baserow/test/fixtures/gallery'
/**
* MockServer is responsible for being the single place where we mock out calls to the
@ -39,12 +43,13 @@ export class MockServer {
createGridView(
application,
table,
{ filters = [], sortings = [], decorations = [] }
{ filters = [], sortings = [], decorations = [], ...rest }
) {
return createGridView(this.mock, application, table, {
filters,
sortings,
decorations,
...rest,
})
}
@ -56,12 +61,29 @@ export class MockServer {
return createPublicGridViewRows(this.mock, viewSlug, fields, rows)
}
createGalleryView(
application,
table,
{ filters = [], sortings = [], decorations = [], ...rest }
) {
return createGalleryView(this.mock, application, table, {
filters,
sortings,
decorations,
...rest,
})
}
createFields(application, table, fields) {
return createFields(this.mock, application, table, fields)
}
createRows(gridView, fields, rows) {
return createRows(this.mock, gridView, fields, rows)
createGridRows(gridView, fields, rows) {
return createGridRows(this.mock, gridView, fields, rows)
}
createGalleryRows(gridView, fields, rows) {
return createGalleryRows(this.mock, gridView, fields, rows)
}
nextSearchForTermWillReturn(searchTerm, gridView, results) {

View file

@ -43,7 +43,7 @@ exports[`GridViewRows component with decoration Default component 1`] = `
</div>
`;
exports[`GridViewRows component with decoration Should show can add decorator tooltip 1`] = `
exports[`GridViewRows component with decoration Should show cant add decorator tooltip 1`] = `
<body>
<div
class="tooltip tooltip--body tooltip--center"
@ -95,303 +95,6 @@ exports[`GridViewRows component with decoration Should show can add decorator to
</div>
</div>
</div>
<div
class="tooltip tooltip--body tooltip--center"
style="top: 4px; left: 0px;"
>
<div
class="tooltip__content"
>
unavailability reason
</div>
</div>
<div
class="context"
>
<div
class="decorator-list"
>
<div
class="decorator-list__item decorator-list__item--disabled"
>
<div
class="decorator-item"
>
<img
class="decorator-item__image"
src="fake/url.png"
/>
<div
class="decorator-item__content"
>
<div
class="decorator-item__title"
>
Fake decorator
</div>
<div
class="decorator-item__description"
>
Fake decorator description
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="context"
>
<div
class="decorator-context"
>
<div
class="decorator-context__list"
>
<div
class="decorator-context__decorator"
>
<div
class="decorator-context__decorator-header"
>
<div
class="decorator-context__decorator-header-info"
>
<div
class="decorator-item"
>
<img
class="decorator-item__image"
src="fake/url.png"
/>
<div
class="decorator-item__content"
>
<div
class="decorator-item__title"
>
Fake decorator
</div>
<div
class="decorator-item__description"
>
Fake decorator description
</div>
</div>
</div>
</div>
<div
class="decorator-context__decorator-header-select"
>
<div
class="dropdown"
>
<a
class="dropdown__selected"
>
<i
class="dropdown__selected-icon fas fa-filter"
/>
Fake value provider
<i
class="dropdown__toggle-icon fas fa-caret-down"
/>
</a>
</div>
</div>
<div
class="decorator-context__decorator-header-trash"
>
<a
class="decorator-context__decorator-header-trash-link"
>
<i
class="fa fa-trash"
/>
</a>
</div>
</div>
<div>
fake_value_provider_form
</div>
</div>
<div
class="decorator-context__decorator"
>
<div
class="decorator-context__decorator-header"
>
<div
class="decorator-context__decorator-header-info"
>
<div
class="decorator-item"
>
<img
class="decorator-item__image"
src="fake/url.png"
/>
<div
class="decorator-item__content"
>
<div
class="decorator-item__title"
>
Fake decorator
</div>
<div
class="decorator-item__description"
>
Fake decorator description
</div>
</div>
</div>
</div>
<div
class="decorator-context__decorator-header-select"
style="display: none;"
>
<!---->
</div>
<div
class="decorator-context__decorator-header-trash"
>
<a
class="decorator-context__decorator-header-trash-link"
>
<i
class="fa fa-trash"
/>
</a>
</div>
</div>
<div
class="value-provider-list value-provider-list--row"
>
<div
class="value-provider-list__item"
>
<div
class="value-provider-item"
>
<div
class="value-provider-item__title"
>
<i
class="fa fa-fw fa-filter value-provider-item__icon"
/>
Fake value provider
</div>
<div
class="value-provider-item__description"
>
Fake value provider description.
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="decorator-context__footer"
>
<a
class="decorator-context__add"
>
<i
class="fas fa-plus"
/>
viewDecoratorContext.addDecorator
</a>
</div>
</div>
</div>
<div
class="context visibility-hidden"
>
<!---->
</div>
<div
class="context picker__context visibility-hidden"
>
<!---->
</div>
<div
class="context"
>
<div
class="decorator-list"
>
<div
class="decorator-list__item"
>
<div
class="decorator-item"
>
<img
class="decorator-item__image"
src="fake/url.png"
/>
<div
class="decorator-item__content"
>
<div
class="decorator-item__title"
>
Fake decorator
</div>
<div
class="decorator-item__description"
>
Fake decorator description
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
@ -447,253 +150,6 @@ exports[`GridViewRows component with decoration Should show unavailable decorato
</div>
</div>
</div>
<div
class="context"
>
<div
class="decorator-context"
>
<div
class="decorator-context__list"
>
<div
class="decorator-context__decorator"
>
<div
class="decorator-context__decorator-header"
>
<div
class="decorator-context__decorator-header-info"
>
<div
class="decorator-item"
>
<img
class="decorator-item__image"
src="fake/url.png"
/>
<div
class="decorator-item__content"
>
<div
class="decorator-item__title"
>
Fake decorator
</div>
<div
class="decorator-item__description"
>
Fake decorator description
</div>
</div>
</div>
</div>
<div
class="decorator-context__decorator-header-select"
>
<div
class="dropdown"
>
<a
class="dropdown__selected"
>
<i
class="dropdown__selected-icon fas fa-filter"
/>
Fake value provider
<i
class="dropdown__toggle-icon fas fa-caret-down"
/>
</a>
</div>
</div>
<div
class="decorator-context__decorator-header-trash"
>
<a
class="decorator-context__decorator-header-trash-link"
>
<i
class="fa fa-trash"
/>
</a>
</div>
</div>
<div>
fake_value_provider_form
</div>
</div>
<div
class="decorator-context__decorator"
>
<div
class="decorator-context__decorator-header"
>
<div
class="decorator-context__decorator-header-info"
>
<div
class="decorator-item"
>
<img
class="decorator-item__image"
src="fake/url.png"
/>
<div
class="decorator-item__content"
>
<div
class="decorator-item__title"
>
Fake decorator
</div>
<div
class="decorator-item__description"
>
Fake decorator description
</div>
</div>
</div>
</div>
<div
class="decorator-context__decorator-header-select"
style="display: none;"
>
<!---->
</div>
<div
class="decorator-context__decorator-header-trash"
>
<a
class="decorator-context__decorator-header-trash-link"
>
<i
class="fa fa-trash"
/>
</a>
</div>
</div>
<div
class="value-provider-list value-provider-list--row"
>
<div
class="value-provider-list__item"
>
<div
class="value-provider-item"
>
<div
class="value-provider-item__title"
>
<i
class="fa fa-fw fa-filter value-provider-item__icon"
/>
Fake value provider
</div>
<div
class="value-provider-item__description"
>
Fake value provider description.
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="decorator-context__footer"
>
<a
class="decorator-context__add"
>
<i
class="fas fa-plus"
/>
viewDecoratorContext.addDecorator
</a>
</div>
</div>
</div>
<div
class="context visibility-hidden"
>
<!---->
</div>
<div
class="context picker__context visibility-hidden"
>
<!---->
</div>
<div
class="context"
>
<div
class="decorator-list"
>
<div
class="decorator-list__item"
>
<div
class="decorator-item"
>
<img
class="decorator-item__image"
src="fake/url.png"
/>
<div
class="decorator-item__content"
>
<div
class="decorator-item__title"
>
Fake decorator
</div>
<div
class="decorator-item__description"
>
Fake decorator description
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;

View file

@ -0,0 +1,251 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GalleryView component with decoration Default component with first_cell decoration 1`] = `
<div
class="gallery-view"
>
<a
class="gallery-view__add"
>
<i
class="fas fa-plus"
/>
</a>
<div
class="gallery-view__scroll"
>
<div
class="gallery-view__cards"
style="height: 410px;"
>
<div
class="card gallery-view__card"
style="width: -60px; transform: translateX(30px) translateY(30px);"
>
<!---->
<div
class="card__content"
>
<div>
fake_decoration: fake_value
</div>
<div>
fake_decoration: fake_value
</div>
<div
class="card__fields"
>
<div
class="card__field"
>
<div
class="card__field-name"
>
Name
</div>
<div
class="card__field-value"
>
<div
class="card-text"
>
first
</div>
</div>
</div>
<div
class="card__field"
>
<div
class="card__field-name"
>
Surname
</div>
<div
class="card__field-value"
>
<div
class="card-text"
>
Bram
</div>
</div>
</div>
<div
class="card__field"
>
<div
class="card__field-name"
>
Address
</div>
<div
class="card__field-value"
>
<div
class="card-text"
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`GalleryView component with decoration Default component with row wrapper decoration 1`] = `
<div
class="gallery-view"
>
<a
class="gallery-view__add"
>
<i
class="fas fa-plus"
/>
</a>
<div
class="gallery-view__scroll"
>
<div
class="gallery-view__cards"
style="height: 410px;"
>
<div
class="card gallery-view__card"
style="width: -60px; transform: translateX(30px) translateY(30px);"
>
<div
class="test-wrapper"
/>
</div>
</div>
</div>
</div>
`;
exports[`GalleryView component with decoration Default component with unavailable decoration 1`] = `
<div
class="gallery-view"
>
<a
class="gallery-view__add"
>
<i
class="fas fa-plus"
/>
</a>
<div
class="gallery-view__scroll"
>
<div
class="gallery-view__cards"
style="height: 410px;"
>
<div
class="card gallery-view__card"
style="width: -60px; transform: translateX(30px) translateY(30px);"
>
<!---->
<div
class="card__content"
>
<div
class="card__fields"
>
<div
class="card__field"
>
<div
class="card__field-name"
>
Name
</div>
<div
class="card__field-value"
>
<div
class="card-text"
>
first
</div>
</div>
</div>
<div
class="card__field"
>
<div
class="card__field-name"
>
Surname
</div>
<div
class="card__field-value"
>
<div
class="card-text"
>
Bram
</div>
</div>
</div>
<div
class="card__field"
>
<div
class="card__field-name"
>
Address
</div>
<div
class="card__field-value"
>
<div
class="card-text"
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,252 @@
import { TestApp } from '@baserow/test/helpers/testApp'
import GalleryView from '@baserow/modules/database/components/view/gallery/GalleryView'
import { DecoratorValueProviderType } from '@baserow/modules/database/decoratorValueProviders'
import { ViewDecoratorType } from '@baserow/modules/database/viewDecorators'
export class FakeDecoratorType extends ViewDecoratorType {
static getType() {
return 'fake_decorator'
}
getPlace() {
return 'first_cell'
}
isDeactivated({ view }) {
return false
}
getComponent() {
const component = {
functional: true,
render(h, ctx) {
return h('div', `fake_decoration: ${ctx.props.value}`)
},
}
return component
}
}
export class FakeValueProviderType extends DecoratorValueProviderType {
static getType() {
return 'fake_value_provider_type'
}
getValue({ options, fields, row }) {
return 'fake_value'
}
getFormComponent() {
const component = {
functional: true,
render(h) {
return h('div', 'fake_value_provider_form')
},
}
return component
}
}
const fieldData = [
{
id: 1,
name: 'Name',
order: 0,
type: 'text',
primary: true,
text_default: '',
},
{
id: 2,
name: 'Surname',
type: 'text',
text_default: '',
primary: false,
},
{
id: 3,
name: 'Address',
type: 'text',
text_default: '',
primary: false,
},
]
const rows = [
{
id: 2,
order: '2.00000000000000000000',
field_1: 'first',
field_2: 'Bram',
},
{
id: 4,
order: '2.50000000000000000000',
field_1: 'second',
field_2: 'foo',
field_3: 'bar',
},
]
describe('GalleryView component with decoration', () => {
let testApp = null
let mockServer = null
let store = null
beforeAll(() => {
testApp = new TestApp()
store = testApp.store
mockServer = testApp.mockServer
})
afterEach((done) => {
testApp.afterEach().then(done)
// Clean up potentially registered stuff
try {
store.$registry.unregister('viewDecorator', 'fake_decorator')
} catch {}
try {
store.$registry.unregister(
'decoratorValueProvider',
'fake_value_provider_type'
)
} catch {}
})
const mountComponent = (props, slots = {}) => {
return testApp.mount(GalleryView, { propsData: props, slots })
}
const populateStore = async (decorations) => {
const table = mockServer.createTable()
const { application } = await mockServer.createAppAndGroup(table)
const view = mockServer.createGalleryView(application, table, {
decorations,
})
mockServer.createFields(application, table, fieldData)
await store.dispatch('field/fetchAll', table)
const primary = store.getters['field/getPrimary']
const fields = store.getters['field/getAll']
mockServer.createGalleryRows(view, fields, rows)
await store.dispatch('page/view/gallery/fetchInitial', {
viewId: 1,
fields,
primary,
})
await store.dispatch('view/fetchAll', { id: 1 })
return { table, primary, fields, view }
}
test('Default component with first_cell decoration', async () => {
const { table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
value_provider_conf: {},
},
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
value_provider_conf: {},
},
])
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
store.$registry.register('viewDecorator', fakeDecorator)
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
table,
view,
primary,
fields,
readOnly: false,
storePrefix: 'page/',
})
expect(wrapper1.element).toMatchSnapshot()
})
test('Default component with row wrapper decoration', async () => {
const { table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
value_provider_conf: {},
},
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
value_provider_conf: {},
},
])
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
fakeDecorator.getPlace = () => 'wrapper'
fakeDecorator.getComponent = () => {
const component = {
functional: true,
render(h, ctx) {
return h(
'div',
{ class: { 'test-wrapper': true } },
ctx.slots().default
)
},
}
return component
}
store.$registry.register('viewDecorator', fakeDecorator)
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
table,
view,
primary,
fields,
readOnly: false,
storePrefix: 'page/',
})
expect(wrapper1.element).toMatchSnapshot()
})
test('Default component with unavailable decoration', async () => {
const { table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
value_provider_conf: {},
},
])
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
fakeDecorator.isDeactivated = () => true
store.$registry.register('viewDecorator', fakeDecorator)
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
table,
view,
primary,
fields,
readOnly: false,
storePrefix: 'page/',
})
expect(wrapper1.element).toMatchSnapshot()
})
})

View file

@ -1,303 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GridViewRows component with decoration Default component with firs_cell decoration 1`] = `
<div
class="grid-view__rows"
style="transform: translateY(0px) translateX(0px);"
>
<div
class="grid-view__row"
>
<!---->
<div
class="grid-view__column"
style="width: 70px;"
>
<div
class="grid-view__row-info"
>
<div
class="grid-view__row-count"
title="2"
>
2
</div>
<div
class="grid-view__row-drag"
/>
<a
class="grid-view__row-more"
>
<i
class="fas fa-expand"
/>
</a>
<div>
fake_decoration: fake_value
</div>
<div>
fake_decoration: fake_value
</div>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
Bram
</div>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
</div>
</div>
</div>
</div>
<div
class="grid-view__row"
>
<!---->
<div
class="grid-view__column"
style="width: 70px;"
>
<div
class="grid-view__row-info"
>
<div
class="grid-view__row-count"
title="4"
>
4
</div>
<div
class="grid-view__row-drag"
/>
<a
class="grid-view__row-more"
>
<i
class="fas fa-expand"
/>
</a>
<div>
fake_decoration: fake_value
</div>
<div>
fake_decoration: fake_value
</div>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
foo
</div>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
bar
</div>
</div>
</div>
</div>
</div>
`;
exports[`GridViewRows component with decoration Default component with row wrapper decoration 1`] = `
<div
class="grid-view__rows"
style="transform: translateY(0px) translateX(0px);"
>
<div
class="testWrapper"
/>
<div
class="testWrapper"
/>
</div>
`;
exports[`GridViewRows component with decoration Default component with unavailable decoration 1`] = `
<div
class="grid-view__rows"
style="transform: translateY(0px) translateX(0px);"
>
<div
class="grid-view__row"
>
<!---->
<div
class="grid-view__column"
style="width: 70px;"
>
<div
class="grid-view__row-info"
>
<div
class="grid-view__row-count"
title="2"
>
2
</div>
<div
class="grid-view__row-drag"
/>
<a
class="grid-view__row-more"
>
<i
class="fas fa-expand"
/>
</a>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
Bram
</div>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
</div>
</div>
</div>
</div>
<div
class="grid-view__row"
>
<!---->
<div
class="grid-view__column"
style="width: 70px;"
>
<div
class="grid-view__row-info"
>
<div
class="grid-view__row-count"
title="4"
>
4
</div>
<div
class="grid-view__row-drag"
/>
<a
class="grid-view__row-more"
>
<i
class="fas fa-expand"
/>
</a>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
foo
</div>
</div>
</div>
<div
class="grid-view__column"
style="width: 200px;"
>
<div
class="grid-view__cell"
>
<div
class="grid-field-text"
>
bar
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -1,5 +1,5 @@
import { TestApp } from '@baserow/test/helpers/testApp'
import GridViewRows from '@baserow/modules/database/components/view/grid/GridViewRows'
import GridView from '@baserow/modules/database/components/view/grid/GridView'
import { DecoratorValueProviderType } from '@baserow/modules/database/decoratorValueProviders'
import { ViewDecoratorType } from '@baserow/modules/database/viewDecorators'
@ -49,7 +49,48 @@ export class FakeValueProviderType extends DecoratorValueProviderType {
}
}
describe('GridViewRows component with decoration', () => {
const fieldData = [
{
id: 1,
name: 'Name',
order: 0,
type: 'text',
primary: true,
text_default: '',
},
{
id: 2,
name: 'Surname',
type: 'text',
text_default: '',
primary: false,
},
{
id: 3,
name: 'Address',
type: 'text',
text_default: '',
primary: false,
},
]
const rows = [
{
id: 2,
order: '2.00000000000000000000',
field_1: 'first',
field_2: 'Bram',
},
{
id: 4,
order: '2.50000000000000000000',
field_1: 'second',
field_2: 'foo',
field_3: 'bar',
},
]
describe('GridView component with decoration', () => {
let testApp = null
let mockServer = null
let store = null
@ -75,69 +116,32 @@ describe('GridViewRows component with decoration', () => {
})
const mountComponent = (props, slots = {}) => {
return testApp.mount(GridViewRows, { propsData: props, slots })
return testApp.mount(GridView, { propsData: props, slots })
}
const primary = {
id: 1,
name: 'Name',
order: 0,
type: 'text',
primary: true,
text_default: '',
_: {
loading: false,
},
}
const fieldData = [
{
id: 2,
name: 'Surname',
type: 'text',
text_default: '',
primary: false,
_: {
loading: false,
},
},
{
id: 3,
name: 'Address',
type: 'text',
text_default: '',
primary: false,
_: {
loading: false,
},
},
]
const rows = [
{ id: 2, order: '2.00000000000000000000', field_2: 'Bram' },
{ id: 4, order: '2.50000000000000000000', field_2: 'foo', field_3: 'bar' },
]
const populateStore = async (decorations) => {
const table = mockServer.createTable()
const { application } = await mockServer.createAppAndGroup(table)
const view = mockServer.createGridView(application, table, {
decorations,
})
const fields = mockServer.createFields(application, table, fieldData)
mockServer.createFields(application, table, fieldData)
await store.dispatch('field/fetchAll', table)
const primary = store.getters['field/getPrimary']
const fields = store.getters['field/getAll']
mockServer.createRows(view, fields, rows)
mockServer.createGridRows(view, fields, rows)
await store.dispatch('page/view/grid/fetchInitial', {
gridId: 1,
fields,
primary,
})
await store.dispatch('view/fetchAll', { id: 1 })
return { table, fields, view }
return { table, primary, fields, view }
}
test('Default component with firs_cell decoration', async () => {
const { fields, view } = await populateStore([
test('Default component with first_cell decoration', async () => {
const { table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -157,12 +161,11 @@ describe('GridViewRows component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
table,
view,
primary,
fields,
allFields: fields,
leftOffset: 0,
readOnly: false,
includeRowDetails: true,
storePrefix: 'page/',
})
@ -170,7 +173,7 @@ describe('GridViewRows component with decoration', () => {
})
test('Default component with row wrapper decoration', async () => {
const { fields, view } = await populateStore([
const { table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -192,7 +195,11 @@ describe('GridViewRows component with decoration', () => {
functional: true,
render(h, ctx) {
return h('div', { class: { testWrapper: true } }, ctx.slots().default)
return h(
'div',
{ class: { 'test-wrapper': true } },
ctx.slots().default
)
},
}
return component
@ -202,12 +209,11 @@ describe('GridViewRows component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
table,
view,
primary,
fields,
allFields: fields,
leftOffset: 0,
readOnly: false,
includeRowDetails: false,
storePrefix: 'page/',
})
@ -215,7 +221,7 @@ describe('GridViewRows component with decoration', () => {
})
test('Default component with unavailable decoration', async () => {
const { fields, view } = await populateStore([
const { table, primary, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
@ -232,12 +238,11 @@ describe('GridViewRows component with decoration', () => {
store.$registry.register('decoratorValueProvider', fakeValueProvider)
const wrapper1 = await mountComponent({
table,
view,
primary,
fields,
allFields: fields,
leftOffset: 0,
readOnly: false,
includeRowDetails: true,
storePrefix: 'page/',
})

View file

@ -67,7 +67,7 @@ describe('GridViewRows component', () => {
const view = mockServer.createGridView(application, table, {})
const fields = mockServer.createFields(application, table, fieldData)
mockServer.createRows(view, fields, rows)
mockServer.createGridRows(view, fields, rows)
await store.dispatch('page/view/grid/fetchInitial', {
gridId: 1,
fields,

View file

@ -32,6 +32,17 @@ export class FakeDecoratorType extends ViewDecoratorType {
return 'unavailability reason'
}
getComponent() {
const component = {
functional: true,
render(h, ctx) {
return h('div', `fake_decoration: ${ctx.props.value}`)
},
}
return component
}
canAdd({ view }) {
return [true]
}
@ -70,19 +81,51 @@ export class FakeValueProviderType extends DecoratorValueProviderType {
}
}
const fieldData = [
{
id: 1,
name: 'Name',
order: 0,
type: 'text',
primary: true,
text_default: '',
},
{
id: 2,
name: 'Surname',
type: 'text',
text_default: '',
primary: false,
},
{
id: 3,
name: 'Status',
type: 'single_select',
text_default: '',
primary: false,
},
]
const rows = [
{ id: 2, order: '2.00000000000000000000', field_2: 'Bram' },
{ id: 4, order: '2.50000000000000000000', field_2: 'foo', field_3: 'bar' },
]
describe('GridViewRows component with decoration', () => {
let testApp = null
let mockServer = null
let store = null
let wrapperToDestroy = null
beforeAll(() => {
testApp = new TestApp()
store = testApp.store
mockServer = testApp.mockServer
store.$registry.registerNamespace('viewDecorator')
store.$registry.registerNamespace('decoratorValueProvider')
})
afterEach((done) => {
testApp.afterEach().then(done)
// Clean up potentially registered stuff
try {
store.$registry.unregister('viewDecorator', 'fake_decorator')
@ -93,64 +136,34 @@ describe('GridViewRows component with decoration', () => {
'fake_value_provider_type'
)
} catch {}
if (wrapperToDestroy) {
wrapperToDestroy.destroy()
wrapperToDestroy = null
}
testApp.afterEach().then(done)
})
const primary = {
id: 1,
name: 'Name',
order: 0,
type: 'text',
primary: true,
text_default: '',
_: {
loading: false,
},
}
const fieldData = [
{
id: 2,
name: 'Surname',
type: 'text',
text_default: '',
primary: false,
_: {
loading: false,
},
},
{
id: 3,
name: 'Status',
type: 'select',
text_default: '',
primary: false,
_: {
loading: false,
},
},
]
const rows = [
{ id: 2, order: '2.00000000000000000000', field_2: 'Bram' },
{ id: 4, order: '2.50000000000000000000', field_2: 'foo', field_3: 'bar' },
]
const populateStore = async (decorations) => {
const populateStore = async ({ viewId = 1, decorations } = {}) => {
const table = mockServer.createTable()
const { application } = await mockServer.createAppAndGroup(table)
const view = mockServer.createGridView(application, table, {
decorations,
viewId,
})
const fields = mockServer.createFields(application, table, fieldData)
mockServer.createRows(view, fields, rows)
mockServer.createFields(application, table, fieldData)
await store.dispatch('field/fetchAll', table)
const primary = store.getters['field/getPrimary']
const fields = store.getters['field/getAll']
mockServer.createGridRows(view, fields, rows)
await store.dispatch('page/view/grid/fetchInitial', {
gridId: 1,
gridId: view.id,
fields,
primary,
})
await store.dispatch('view/fetchAll', { id: 1 })
return { table, fields, view }
await store.dispatch('view/fetchAll', { id: table.id })
return { table, primary, fields, view }
}
const mountComponent = async (props) => {
@ -160,11 +173,15 @@ describe('GridViewRows component with decoration', () => {
await wrapper.vm.show(document.body)
await wrapper.vm.$nextTick()
// Allow to clean the DOM after the test
wrapperToDestroy = wrapper
return wrapper
}
test('Default component', async () => {
const { table, fields, view } = await populateStore()
const { table, primary, fields, view } = await populateStore()
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
@ -184,20 +201,22 @@ describe('GridViewRows component with decoration', () => {
})
test('View with decoration configured', async () => {
const { table, fields, view } = await populateStore([
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
value_provider_conf: {},
_: { loading: false },
},
{
type: 'fake_decorator',
value_provider_type: '',
value_provider_conf: {},
_: { loading: false },
},
])
const { table, primary, fields, view } = await populateStore({
decorations: [
{
type: 'fake_decorator',
value_provider_type: 'fake_value_provider_type',
value_provider_conf: {},
_: { loading: false },
},
{
type: 'fake_decorator',
value_provider_type: '',
value_provider_conf: {},
_: { loading: false },
},
],
})
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
@ -217,7 +236,7 @@ describe('GridViewRows component with decoration', () => {
})
test('Should show unavailable decorator tooltip', async () => {
const { table, fields, view } = await populateStore()
const { table, primary, fields, view } = await populateStore()
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })
@ -242,8 +261,8 @@ describe('GridViewRows component with decoration', () => {
expect(document.body).toMatchSnapshot()
})
test('Should show can add decorator tooltip', async () => {
const { table, fields, view } = await populateStore()
test('Should show cant add decorator tooltip', async () => {
const { table, primary, fields, view } = await populateStore()
const fakeDecorator = new FakeDecoratorType({ app: testApp })
const fakeValueProvider = new FakeValueProviderType({ app: testApp })

View file

@ -144,7 +144,7 @@ describe('Table Component Tests', () => {
},
])
mockServer.createRows(gridView, fields, [
mockServer.createGridRows(gridView, fields, [
{
id: 1,
order: 0,

View file

@ -54,7 +54,7 @@ describe('View Filter Tests', () => {
})
const fields = mockServer.createFields(application, table, [field])
mockServer.createRows(gridView, fields, [row])
mockServer.createGridRows(gridView, fields, [row])
await store.dispatch('page/view/grid/fetchInitial', {
gridId: 1,
fields: [field],