mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-12 16:28:06 +00:00
Merge branch '2090_field_description' into 'develop'
#2090 field description Closes #2090 See merge request baserow/baserow!2392
This commit is contained in:
commit
3db2391fa7
30 changed files with 709 additions and 104 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database
api
ws/public
changelog/entries/unreleased/feature
enterprise/web-frontend/modules/baserow_enterprise/components
premium/backend/tests/baserow_premium_tests/api/views/views
web-frontend
modules
core
assets/scss/components
components
directives
mixins
utils
database
test/unit/database
__snapshots__
components/view/grid/__snapshots__
|
@ -34,10 +34,25 @@ class FieldSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Field
|
||||
fields = ("id", "table_id", "name", "order", "type", "primary", "read_only")
|
||||
fields = (
|
||||
"id",
|
||||
"table_id",
|
||||
"name",
|
||||
"order",
|
||||
"type",
|
||||
"primary",
|
||||
"read_only",
|
||||
"description",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"table_id": {"read_only": True},
|
||||
"description": {
|
||||
"required": False,
|
||||
"default": None,
|
||||
"allow_null": True,
|
||||
"allow_blank": True,
|
||||
},
|
||||
}
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
|
@ -84,7 +99,15 @@ class CreateFieldSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Field
|
||||
fields = ("name", "type")
|
||||
fields = ("name", "type", "description")
|
||||
extra_kwargs = {
|
||||
"description": {
|
||||
"required": False,
|
||||
"default": None,
|
||||
"allow_null": True,
|
||||
"allow_blank": True,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UpdateFieldSerializer(serializers.ModelSerializer):
|
||||
|
@ -94,9 +117,15 @@ class UpdateFieldSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = Field
|
||||
fields = ("name", "type")
|
||||
fields = ("name", "type", "description")
|
||||
extra_kwargs = {
|
||||
"name": {"required": False},
|
||||
"description": {
|
||||
"required": False,
|
||||
"default": None,
|
||||
"allow_null": True,
|
||||
"allow_blank": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -291,6 +291,7 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
return_updated_fields=False,
|
||||
primary_key=None,
|
||||
skip_search_updates=False,
|
||||
description: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Union[Field, Tuple[Field, List[Field]]]:
|
||||
"""
|
||||
|
@ -366,6 +367,7 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
primary=primary,
|
||||
pk=primary_key,
|
||||
tsvector_column_created=table.tsvectors_are_supported,
|
||||
description=description,
|
||||
**field_values,
|
||||
)
|
||||
|
||||
|
@ -510,7 +512,7 @@ class FieldHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
dependants_broken_due_to_type_change = []
|
||||
to_field_type = from_field_type
|
||||
|
||||
allowed_fields = ["name"] + to_field_type.allowed_fields
|
||||
allowed_fields = ["name", "description"] + to_field_type.allowed_fields
|
||||
field_values = extract_allowed(kwargs, allowed_fields)
|
||||
|
||||
self._validate_name_and_optionally_rename_if_collision(
|
||||
|
|
|
@ -122,6 +122,9 @@ class Field(
|
|||
"search release which haven't been lazily migrated yet. Or for "
|
||||
"users who have turned off full text search entirely.",
|
||||
)
|
||||
description = models.TextField(
|
||||
help_text="Field description", default=None, null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = (
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 4.1.13 on 2024-05-13 12:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('database', '0157_alter_airtableimportjob_database')
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="field",
|
||||
name="description",
|
||||
field=models.TextField(
|
||||
default=None, help_text="Field description", null=True
|
||||
),
|
||||
),
|
||||
]
|
|
@ -209,7 +209,31 @@ def test_create_field(api_client, data_fixture):
|
|||
|
||||
response = api_client.post(
|
||||
reverse("api:database:fields:list", kwargs={"table_id": table.id}),
|
||||
{"name": "Test 1", "type": "text", "text_default": "default!"},
|
||||
{"name": "Test no description", "type": "text", "text_default": "default!"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json["type"] == "text"
|
||||
|
||||
text = TextField.objects.filter()[0]
|
||||
assert response_json["id"] == text.id
|
||||
assert response_json["name"] == "Test no description"
|
||||
assert response_json["order"] == text.order
|
||||
assert response_json["text_default"] == "default!"
|
||||
assert response_json["description"] is None
|
||||
text.delete()
|
||||
|
||||
description_txt_a = "This is a description"
|
||||
response = api_client.post(
|
||||
reverse("api:database:fields:list", kwargs={"table_id": table.id}),
|
||||
{
|
||||
"name": "Test 1",
|
||||
"type": "text",
|
||||
"text_default": "default!",
|
||||
"description": description_txt_a,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||
)
|
||||
|
@ -222,6 +246,7 @@ def test_create_field(api_client, data_fixture):
|
|||
assert response_json["name"] == text.name
|
||||
assert response_json["order"] == text.order
|
||||
assert response_json["text_default"] == "default!"
|
||||
assert response_json["description"] == description_txt_a
|
||||
|
||||
# Test authentication with token
|
||||
response = api_client.post(
|
||||
|
@ -310,6 +335,7 @@ def test_get_field(api_client, data_fixture):
|
|||
assert response_json["name"] == text.name
|
||||
assert response_json["table_id"] == text.table_id
|
||||
assert not response_json["text_default"]
|
||||
assert response_json["description"] is None
|
||||
|
||||
response = api_client.delete(
|
||||
reverse(
|
||||
|
@ -380,7 +406,7 @@ def test_update_field(api_client, data_fixture):
|
|||
url = reverse("api:database:fields:item", kwargs={"field_id": text.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{"name": "Test 1", "text_default": "Something"},
|
||||
{"name": "Test 1", "text_default": "Something", "description": "a description"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -389,6 +415,7 @@ def test_update_field(api_client, data_fixture):
|
|||
assert response_json["id"] == text.id
|
||||
assert response_json["name"] == "Test 1"
|
||||
assert response_json["text_default"] == "Something"
|
||||
assert response_json["description"] == "a description"
|
||||
|
||||
text.refresh_from_db()
|
||||
assert text.name == "Test 1"
|
||||
|
@ -397,7 +424,12 @@ def test_update_field(api_client, data_fixture):
|
|||
url = reverse("api:database:fields:item", kwargs={"field_id": text.id})
|
||||
response = api_client.patch(
|
||||
url,
|
||||
{"name": "Test 1", "type": "text", "text_default": "Something"},
|
||||
{
|
||||
"name": "Test 1",
|
||||
"type": "text",
|
||||
"text_default": "Something",
|
||||
"description": None,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -405,6 +437,7 @@ def test_update_field(api_client, data_fixture):
|
|||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json["name"] == "Test 1"
|
||||
assert response_json["type"] == "text"
|
||||
assert response_json["description"] is None
|
||||
|
||||
url = reverse("api:database:fields:item", kwargs={"field_id": text.id})
|
||||
response = api_client.patch(
|
||||
|
|
|
@ -816,6 +816,7 @@ def test_get_public_gallery_view(api_client, data_fixture):
|
|||
"text_default": "",
|
||||
"type": "text",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
}
|
||||
],
|
||||
"view": {
|
||||
|
|
|
@ -3240,6 +3240,7 @@ def test_get_public_grid_view(api_client, data_fixture):
|
|||
"text_default": "",
|
||||
"type": "text",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
}
|
||||
],
|
||||
"view": {
|
||||
|
|
|
@ -222,6 +222,7 @@ def test_when_field_unhidden_in_public_view_force_refresh_sent(
|
|||
"primary": False,
|
||||
"text_default": "",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
}
|
||||
],
|
||||
"view": view_serialized["view"],
|
||||
|
@ -298,6 +299,7 @@ def test_when_only_field_options_updated_in_public_grid_view_force_refresh_sent(
|
|||
"primary": False,
|
||||
"text_default": "",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
}
|
||||
],
|
||||
"view": view_serialized["view"],
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Support for rich text field description",
|
||||
"issue_number": 2090,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-04-29"
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
<HelpIcon
|
||||
v-if="roleUidSelected === 'ADMIN'"
|
||||
:tooltip="$t('membersRoleField.adminHelpText')"
|
||||
is-warning
|
||||
icon="warning-triangle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -966,6 +966,7 @@ def test_get_public_calendar_view_with_single_select_and_cover(
|
|||
"date_include_time": False,
|
||||
"date_show_tzinfo": False,
|
||||
"date_time_format": "24",
|
||||
"description": None,
|
||||
},
|
||||
{
|
||||
"id": public_field.id,
|
||||
|
@ -976,6 +977,7 @@ def test_get_public_calendar_view_with_single_select_and_cover(
|
|||
"text_default": "",
|
||||
"type": "text",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
},
|
||||
],
|
||||
"view": {
|
||||
|
|
|
@ -1566,6 +1566,7 @@ def test_get_public_kanban_without_with_single_select_and_cover(
|
|||
"text_default": "",
|
||||
"type": "text",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
},
|
||||
],
|
||||
"view": {
|
||||
|
@ -1646,6 +1647,7 @@ def test_get_public_kanban_view_with_single_select_and_cover(
|
|||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"type": "single_select",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
},
|
||||
{
|
||||
"id": cover_field.id,
|
||||
|
@ -1655,6 +1657,7 @@ def test_get_public_kanban_view_with_single_select_and_cover(
|
|||
"table_id": PUBLIC_PLACEHOLDER_ENTITY_ID,
|
||||
"type": "file",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
},
|
||||
{
|
||||
"id": public_field.id,
|
||||
|
@ -1665,6 +1668,7 @@ def test_get_public_kanban_view_with_single_select_and_cover(
|
|||
"text_default": "",
|
||||
"type": "text",
|
||||
"read_only": False,
|
||||
"description": None,
|
||||
},
|
||||
],
|
||||
"view": {
|
||||
|
|
|
@ -175,6 +175,14 @@
|
|||
&--multiple-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&--align-left {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
&--align-right {
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
.context__menu-active-icon {
|
||||
|
|
|
@ -329,3 +329,20 @@ span[data-type='mention'] {
|
|||
background-color: $palette-neutral-100;
|
||||
}
|
||||
}
|
||||
|
||||
.rich-text-editor--fixed-size {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
border: 1px solid $palette-neutral-400;
|
||||
padding: 6px 12px;
|
||||
outline: none;
|
||||
line-height: 100%;
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
border-radius: 4px;
|
||||
|
||||
@include elevation($elevation-low);
|
||||
}
|
||||
|
|
|
@ -45,4 +45,35 @@
|
|||
|
||||
@include fixed-height(26px, 12px);
|
||||
@include rounded($rounded);
|
||||
|
||||
&--expandable {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
max-height: 320px;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
max-width: 320px;
|
||||
text-wrap: wrap;
|
||||
|
||||
@include rounded($rounded);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
border-radius: 10px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background: $color-neutral-500;
|
||||
background-clip: content-box;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -615,11 +615,20 @@
|
|||
|
||||
.grid-view__description-options {
|
||||
margin-left: auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.grid-view__description-icon-trigger {
|
||||
color: $color-neutral-600;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
vertical-align: center;
|
||||
|
||||
&:hover {
|
||||
color: $color-neutral-900;
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
<template>
|
||||
<i v-tooltip="tooltip" class="help-icon" :class="iconClass"> </i>
|
||||
<i
|
||||
v-tooltip:[tooltipOptions]="tooltipText"
|
||||
class="help-icon"
|
||||
:class="iconClass"
|
||||
>
|
||||
</i>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Markdown from 'markdown-it'
|
||||
|
||||
const TooltipContentPlain = 'plain'
|
||||
const TooltipContentMarkdown = 'markdown'
|
||||
|
||||
export default {
|
||||
name: 'HelpIcon',
|
||||
props: {
|
||||
|
@ -11,17 +21,81 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
isWarning: {
|
||||
type: Boolean,
|
||||
/**
|
||||
* How many seconds the tooltip should be displayed after mouse moves out of icon/contents?
|
||||
* */
|
||||
tooltipDuration: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: false,
|
||||
default: 0,
|
||||
},
|
||||
/**
|
||||
* Hints on tooltip content type.
|
||||
* Possible values are: `plain` | `markdown`
|
||||
* */
|
||||
tooltipContentType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: TooltipContentPlain,
|
||||
validators: (contentType) => {
|
||||
return [TooltipContentMarkdown, TooltipContentPlain].includes(
|
||||
contentType
|
||||
)
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Iconoir icon name without iconoir- prefix.
|
||||
* */
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'chat-bubble-question',
|
||||
},
|
||||
/**
|
||||
* Additional css classes for tooltip icon
|
||||
* */
|
||||
tooltipClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* Additional css classes for tooltip content container
|
||||
* */
|
||||
tooltipContentClasses: {
|
||||
type: String,
|
||||
requred: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tooltipOptions() {
|
||||
return {
|
||||
duration: this.tooltipDuration,
|
||||
contentIsHtml: this.tooltipContentIsHtml(),
|
||||
contentClasses: this.tooltipContentClasses,
|
||||
}
|
||||
},
|
||||
tooltipText() {
|
||||
if (this.tooltipContentType === TooltipContentMarkdown) {
|
||||
const md = new Markdown()
|
||||
return md.render(this.tooltip)
|
||||
}
|
||||
return this.tooltip
|
||||
},
|
||||
iconClass() {
|
||||
return this.isWarning
|
||||
? 'iconoir-warning-triangle'
|
||||
: 'iconoir-chat-bubble-question'
|
||||
const clsNames = this.tooltipClasses ? [this.tooltipClasses] : []
|
||||
if (this.icon) {
|
||||
clsNames.push(`iconoir-${this.icon}`)
|
||||
}
|
||||
return clsNames.join(' ')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tooltipContentIsHtml() {
|
||||
// tooltip directive doesn't need to know anything about markdown conversion,
|
||||
// just whether the content is html or not
|
||||
return this.tooltipContentType !== 'plain'
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,13 +1,70 @@
|
|||
import _ from 'lodash'
|
||||
import { onClickOutside } from '@baserow/modules/core/utils/dom'
|
||||
|
||||
/**
|
||||
* helper function to extract options from element
|
||||
*
|
||||
* If binding provides .arg, the .arg property should be an object:
|
||||
*
|
||||
* {duration: Number, contentIsHtml: Bool, contentClasses: String}
|
||||
*
|
||||
* @param el
|
||||
* @param binding directive binding value
|
||||
* @returns {{duration: number, contentClasses: (*|null), value, contentType: (*|string)}}
|
||||
*/
|
||||
const getOptions = (el, binding) => {
|
||||
// defaults
|
||||
const tooltipOptions = {
|
||||
duration: 0,
|
||||
contentIsHtml: false,
|
||||
value: null,
|
||||
contentClasses: '',
|
||||
}
|
||||
|
||||
if (binding.arg && _.isObject(binding.arg)) {
|
||||
_.merge(tooltipOptions, binding.arg)
|
||||
}
|
||||
tooltipOptions.value = binding.value
|
||||
return tooltipOptions
|
||||
}
|
||||
|
||||
// make sure only one tooltip is visible
|
||||
let currentTooltip = null
|
||||
const switchToTooltip = (el) => {
|
||||
if (currentTooltip != null) {
|
||||
currentTooltip.tooltipClose()
|
||||
}
|
||||
currentTooltip = el
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a very simple and fast tooltip directive. It will add the binding value as
|
||||
* tooltip content. The tooltip only shows if there is a value.
|
||||
*
|
||||
* tooltip directive can be customized with arg, where arg is an object:
|
||||
* {
|
||||
* // duration number of seconds the tooltip should be visible after pointer
|
||||
* // leaves i icon or tooltip content
|
||||
* duration: Number,
|
||||
* // contentIsHtml informs the value is html and should not be escaped (sets .innerHtml directly)
|
||||
* contentIsHtml: Bool,
|
||||
* // contentClasses is a string with additional css classes for tooltip content container
|
||||
* contentClasses: String
|
||||
* }
|
||||
*
|
||||
* <i v-tooltip:[configObject]="someValue"/>
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* If there is a value and the tooltip has not yet been initialized we can add the
|
||||
* mouse events to show and hide the tooltip.
|
||||
*
|
||||
*/
|
||||
initialize(el, value) {
|
||||
|
||||
initialize(el, binding) {
|
||||
el.tooltipOptions = getOptions(el, binding)
|
||||
el.onClickOutsideCallback = null
|
||||
|
||||
el.updatePositionEvent = () => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const position = el.getAttribute('tooltip-position') || 'bottom'
|
||||
|
@ -22,7 +79,15 @@ export default {
|
|||
const width = rect.right - rect.left
|
||||
el.tooltipElement.style.left = rect.left + width / 2 + 'px'
|
||||
}
|
||||
el.removeTimeout = () => {
|
||||
if (el.tooltipTimeout) {
|
||||
clearTimeout(el.tooltipTimeout)
|
||||
}
|
||||
el.tooltipTimeout = null
|
||||
}
|
||||
|
||||
el.tooltipMouseEnterEvent = () => {
|
||||
switchToTooltip(el)
|
||||
const position = el.getAttribute('tooltip-position') || 'bottom'
|
||||
const hide = el.getAttribute('hide-tooltip')
|
||||
|
||||
|
@ -30,34 +95,92 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
if (el.tooltipElement) {
|
||||
this.terminate(el)
|
||||
if (!el.tooltipElement) {
|
||||
el.tooltipElement = document.createElement('div')
|
||||
|
||||
const classes = ['tooltip', 'tooltip--body', 'tooltip--center']
|
||||
if (position === 'top') {
|
||||
classes.push('tooltip--top')
|
||||
}
|
||||
|
||||
el.tooltipElement.className = classes.join(' ')
|
||||
document.body.insertBefore(el.tooltipElement, document.body.firstChild)
|
||||
|
||||
el.tooltipContentElement = document.createElement('div')
|
||||
|
||||
el.tooltipElement.appendChild(el.tooltipContentElement)
|
||||
}
|
||||
|
||||
el.tooltipElement = document.createElement('div')
|
||||
|
||||
const classes = ['tooltip', 'tooltip--body', 'tooltip--center']
|
||||
if (position === 'top') {
|
||||
classes.push('tooltip--top')
|
||||
if (el.tooltipOptions.contentIsHtml) {
|
||||
el.tooltipContentElement.innerHTML = el.tooltipOptions.value
|
||||
} else {
|
||||
el.tooltipContentElement.textContent = el.tooltipOptions.value
|
||||
}
|
||||
|
||||
el.tooltipElement.className = classes.join(' ')
|
||||
document.body.insertBefore(el.tooltipElement, document.body.firstChild)
|
||||
|
||||
el.tooltipContentElement = document.createElement('div')
|
||||
el.tooltipContentElement.className = 'tooltip__content'
|
||||
el.tooltipContentElement.textContent = value
|
||||
el.tooltipElement.appendChild(el.tooltipContentElement)
|
||||
// additional css classes for content container
|
||||
const contentClass = ['tooltip__content']
|
||||
if (el.tooltipOptions.contentClasses) {
|
||||
contentClass.push(el.tooltipOptions.contentClasses)
|
||||
}
|
||||
el.tooltipContentElement.className = contentClass.join(' ')
|
||||
|
||||
el.updatePositionEvent()
|
||||
// we just entered, so we don't want any previously set timeout to close
|
||||
// the tooltip content
|
||||
el.removeTimeout()
|
||||
|
||||
// make tooltip content preserved if pointer hovers
|
||||
el.tooltipContentElement.addEventListener('mouseenter', el.removeTimeout)
|
||||
el.tooltipContentElement.addEventListener(
|
||||
'mouseleave',
|
||||
el.tooltipMoveLeaveEvent
|
||||
)
|
||||
|
||||
// When the user scrolls or resizes the window it could be possible that the
|
||||
// element where the tooltip is anchored to has moved, so then the position
|
||||
// needs to be updated. We only want to do this when the tooltip is visible.
|
||||
window.addEventListener('scroll', el.updatePositionEvent, true)
|
||||
window.addEventListener('resize', el.updatePositionEvent)
|
||||
|
||||
// refresh the callback - old callback should be removed because old instance
|
||||
// is may be longer present, and window object may still have click event handlers
|
||||
// that check if the pointer is in or out of tooltip content element (which itself
|
||||
// is long time gone)
|
||||
el.removeTooltipOutsideClickCallback()
|
||||
el.onClickOutsideCallback = onClickOutside(
|
||||
el.tooltipContentElement,
|
||||
el.tooltipClose
|
||||
)
|
||||
}
|
||||
/**
|
||||
* queue a close tooltip action.
|
||||
*
|
||||
* This can be called multiple times. Each call will postpone tooltipClose() call.
|
||||
* This way user can hover in and hover out several times and the tooltip still be
|
||||
* visible, if duration is > 0.
|
||||
*/
|
||||
el.tooltipMoveLeaveEvent = () => {
|
||||
// we should remove any pending timeout before setting new one, because timeout
|
||||
// should be counted from the last mouse leave event.
|
||||
el.removeTimeout()
|
||||
el.tooltipTimeout = setTimeout(
|
||||
el.tooltipClose,
|
||||
// timeout from caller is in seconds. remember to convert to mseconds
|
||||
el.tooltipOptions.duration * 1000
|
||||
)
|
||||
}
|
||||
el.removeTooltipOutsideClickCallback = () => {
|
||||
if (el.onClickOutsideCallback) {
|
||||
el.onClickOutsideCallback()
|
||||
el.onClickOutsideCallback = null
|
||||
}
|
||||
}
|
||||
/**
|
||||
* actually closing the tooltip here
|
||||
*/
|
||||
el.tooltipClose = () => {
|
||||
// cleanup actions: remove window handlers set with onClickOutside()
|
||||
el.removeTooltipOutsideClickCallback()
|
||||
|
||||
if (el.tooltipElement) {
|
||||
el.tooltipElement.parentNode.removeChild(el.tooltipElement)
|
||||
el.tooltipElement = null
|
||||
|
@ -66,10 +189,13 @@ export default {
|
|||
|
||||
window.removeEventListener('scroll', el.updatePositionEvent, true)
|
||||
window.removeEventListener('resize', el.updatePositionEvent)
|
||||
el.removeTimeout()
|
||||
}
|
||||
// those event listeners should be bind all the time to the el element
|
||||
el.addEventListener('mouseenter', el.tooltipMouseEnterEvent)
|
||||
el.addEventListener('mouseleave', el.tooltipMoveLeaveEvent)
|
||||
},
|
||||
|
||||
/**
|
||||
* If there isn't a value or if the directive is unbinded the tooltipElement can
|
||||
* be destroyed if it wasn't already and all the events can be removed.
|
||||
|
@ -94,9 +220,9 @@ export default {
|
|||
const { value } = binding
|
||||
|
||||
if (!!value && el.tooltipElement) {
|
||||
el.tooltipContentElement.textContent = value
|
||||
el.tooltipOptions = getOptions(el, binding)
|
||||
} else if (!!value && el.tooltipElement === null) {
|
||||
binding.def.initialize(el, value)
|
||||
binding.def.initialize(el, binding)
|
||||
} else if (!value) {
|
||||
binding.def.terminate(el)
|
||||
}
|
||||
|
|
|
@ -109,7 +109,10 @@ export default {
|
|||
* Returns true if the field value has no errors
|
||||
*/
|
||||
fieldHasErrors(fieldName) {
|
||||
return this.$v.values[fieldName].$error
|
||||
// a field can be without any validators
|
||||
return this.$v.values[fieldName]
|
||||
? this.$v.values[fieldName].$error
|
||||
: false
|
||||
},
|
||||
/**
|
||||
* Returns true is everything is valid.
|
||||
|
|
|
@ -56,6 +56,14 @@ export const findScrollableParent = (element) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects clicks outside el element and call callback
|
||||
*
|
||||
* Returns a callback to unregister click handlers after successful outside click
|
||||
* @param el
|
||||
* @param callback
|
||||
* @returns {(function(): void)|*}
|
||||
*/
|
||||
export const onClickOutside = (el, callback) => {
|
||||
const insideEvent = new Set()
|
||||
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
<APIDocsParameter name="read_only" :optional="false" type="boolean">
|
||||
{{ $t('apiDocsTableListFields.readOnly') }}
|
||||
</APIDocsParameter>
|
||||
<APIDocsParameter name="description" :optional="false" type="string">
|
||||
{{ $t('apiDocsTableListFields.descriptionField') }}
|
||||
</APIDocsParameter>
|
||||
</ul>
|
||||
<p class="api-docs__content">
|
||||
{{ $t('apiDocsTableListFields.extraProps') }}
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
class="field-form-context"
|
||||
:overflow-scroll="true"
|
||||
:max-height-if-outside-viewport="true"
|
||||
@shown="$emit('shown', $event)"
|
||||
@shown="
|
||||
onShow()
|
||||
$emit('shown', $event)
|
||||
"
|
||||
>
|
||||
<FieldForm
|
||||
ref="form"
|
||||
|
@ -16,7 +19,20 @@
|
|||
@submitted="submit"
|
||||
@keydown-enter="$refs.submitButton.focus()"
|
||||
>
|
||||
<div class="context__form-actions">
|
||||
<div
|
||||
class="context__form-actions context__form-actions--multiple-actions"
|
||||
>
|
||||
<span class="context__form-actions--alight-left">
|
||||
<ButtonText
|
||||
v-if="!showDescription"
|
||||
ref="showDescription"
|
||||
icon="iconoir iconoir-plus"
|
||||
type="secondary"
|
||||
@click="showDescriptionField"
|
||||
>
|
||||
{{ $t('fieldForm.addDescription') }}
|
||||
</ButtonText>
|
||||
</span>
|
||||
<Button
|
||||
ref="submitButton"
|
||||
type="primary"
|
||||
|
@ -66,6 +82,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showDescription: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -111,6 +128,18 @@ export default {
|
|||
showFieldTypesDropdown(target) {
|
||||
this.$refs.form.showFieldTypesDropdown(target)
|
||||
},
|
||||
showDescriptionField(evt) {
|
||||
this.hideDescriptionLink()
|
||||
this.$refs.form.showDescriptionField()
|
||||
evt.stopPropagation()
|
||||
evt.preventDefault()
|
||||
},
|
||||
hideDescriptionLink() {
|
||||
this.showDescription = true
|
||||
},
|
||||
onShow() {
|
||||
this.showDescription = this.$refs.form.isDescriptionFieldNotEmpty()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -108,6 +108,29 @@
|
|||
@suggested-field-name="handleSuggestedFieldName($event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<FormElement
|
||||
v-if="showDescription"
|
||||
:error="fieldHasErrors('description')"
|
||||
class="control"
|
||||
>
|
||||
<label class="control__label control__label--small">{{
|
||||
$t('fieldForm.description')
|
||||
}}</label>
|
||||
<div class="control__elements">
|
||||
<RichTextEditor
|
||||
ref="description"
|
||||
:value="editorValue"
|
||||
class="field-form__editor rich-text-editor rich-text-editor--fixed-size"
|
||||
:editable="true"
|
||||
:enter-stop-edit="false"
|
||||
:thin-scrollbar="true"
|
||||
:enable-rich-text-formatting="false"
|
||||
:placeholder="$t('fieldForm.description')"
|
||||
@blur="onDescriptionBlur"
|
||||
/>
|
||||
</div>
|
||||
</FormElement>
|
||||
<slot v-if="!selectedFieldIsDeactivated"></slot>
|
||||
</form>
|
||||
</template>
|
||||
|
@ -117,6 +140,7 @@ import { mapGetters } from 'vuex'
|
|||
import { required, maxLength } from 'vuelidate/lib/validators'
|
||||
|
||||
import { getNextAvailableNameInSequence } from '@baserow/modules/core/utils/string'
|
||||
import RichTextEditor from '@baserow/modules/core/components/editor/RichTextEditor.vue'
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
import {
|
||||
RESERVED_BASEROW_FIELD_NAMES,
|
||||
|
@ -126,6 +150,7 @@ import {
|
|||
// @TODO focus form on open
|
||||
export default {
|
||||
name: 'FieldForm',
|
||||
components: { RichTextEditor },
|
||||
mixins: [form],
|
||||
props: {
|
||||
table: {
|
||||
|
@ -158,13 +183,15 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
allowedValues: ['name', 'type'],
|
||||
allowedValues: ['name', 'type', 'description'],
|
||||
values: {
|
||||
name: '',
|
||||
type: this.forcedType || '',
|
||||
description: null,
|
||||
},
|
||||
isPrefilledWithSuggestedFieldName: false,
|
||||
oldValueType: null,
|
||||
showDescription: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -190,6 +217,11 @@ export default {
|
|||
return false
|
||||
}
|
||||
},
|
||||
editorValue() {
|
||||
// temp fix to have proper line breaks
|
||||
// this will not be needed when RTE will be in minimal mode
|
||||
return (this.values.description || '').replaceAll('\n', '<br/>')
|
||||
},
|
||||
...mapGetters({
|
||||
fields: 'field/getAll',
|
||||
}),
|
||||
|
@ -277,6 +309,29 @@ export default {
|
|||
this.$refs[`deactivatedClickModal-${fieldType.type}`][0].show()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* This sets the showDescription flag to display description text editor, even
|
||||
* if values.description is empty.
|
||||
*
|
||||
* Used by parent components.
|
||||
*/
|
||||
showDescriptionField() {
|
||||
this.showDescription = true
|
||||
},
|
||||
/**
|
||||
* Helper method to get information if description is not empty.
|
||||
* Used by parent components
|
||||
*/
|
||||
isDescriptionFieldNotEmpty() {
|
||||
this.showDescription = !!this.values.description
|
||||
return this.showDescription
|
||||
},
|
||||
onDescriptionBlur() {
|
||||
// Handle blur event on field description text editor.
|
||||
// A bit hacky way to get current state of description editor once the
|
||||
// edition finished.
|
||||
this.values.description = this.$refs.description.editor.getText()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
class="field-form-context"
|
||||
:overflow-scroll="true"
|
||||
:max-height-if-outside-viewport="true"
|
||||
@shown="onShow"
|
||||
>
|
||||
<FieldForm
|
||||
ref="form"
|
||||
|
@ -14,17 +15,31 @@
|
|||
:all-fields-in-table="allFieldsInTable"
|
||||
:database="database"
|
||||
@submitted="submit"
|
||||
@description-shown="hideDescriptionLink"
|
||||
>
|
||||
<div
|
||||
class="context__form-actions context__form-actions--multiple-actions"
|
||||
>
|
||||
<a @click="cancel">
|
||||
{{ $t('action.cancel') }}
|
||||
</a>
|
||||
<span class="context__form-actions--alight-left">
|
||||
<ButtonText
|
||||
v-if="!showDescription"
|
||||
ref="showDescription"
|
||||
icon="iconoir iconoir-plus"
|
||||
type="secondary"
|
||||
@click="showDescriptionField"
|
||||
>
|
||||
{{ $t('fieldForm.addDescription') }}
|
||||
</ButtonText>
|
||||
</span>
|
||||
|
||||
<Button :loading="loading" :disabled="loading || fieldTypeDisabled">
|
||||
{{ $t('action.save') }}
|
||||
</Button>
|
||||
<span class="context__form-actions--align-right">
|
||||
<span class="margin-right-2">
|
||||
<a class="form-action" @click="cancel">{{ $t('action.cancel') }}</a>
|
||||
</span>
|
||||
<Button :loading="loading" :disabled="loading || fieldTypeDisabled">
|
||||
{{ $t('action.save') }}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</FieldForm>
|
||||
</Context>
|
||||
|
@ -64,6 +79,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showDescription: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -87,6 +103,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.showDescription = false
|
||||
this.$nextTick(() => {
|
||||
this.$refs.form && this.$refs.form.reset()
|
||||
})
|
||||
|
@ -131,6 +148,19 @@ export default {
|
|||
this.reset()
|
||||
this.hide()
|
||||
},
|
||||
onShow() {
|
||||
this.showDescription = this.$refs.form.isDescriptionFieldNotEmpty()
|
||||
},
|
||||
|
||||
showDescriptionField(evt) {
|
||||
this.hideDescriptionLink()
|
||||
this.$refs.form.showDescriptionField()
|
||||
evt.stopPropagation()
|
||||
evt.preventDefault()
|
||||
},
|
||||
hideDescriptionLink() {
|
||||
this.showDescription = true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -7,6 +7,15 @@
|
|||
></a>
|
||||
<i class="control__label-icon" :class="field._.type.iconClass"></i>
|
||||
{{ field.name }}
|
||||
<span v-if="field.description" class="margin-left-1">
|
||||
<HelpIcon
|
||||
:tooltip="descriptionText"
|
||||
:tooltip-duration="3"
|
||||
:tooltip-content-type="'html'"
|
||||
:tooltip-content-classes="'tooltip__content--expandable'"
|
||||
:icon="'info-empty'"
|
||||
/>
|
||||
</span>
|
||||
<i
|
||||
v-if="!readOnly && canModifyFields"
|
||||
ref="contextLink"
|
||||
|
@ -112,6 +121,11 @@ export default {
|
|||
default: () => true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
descriptionText() {
|
||||
return (this.field.description || '').replaceAll('\n', '<br/>')
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getFieldComponent(type) {
|
||||
return this.$registry
|
||||
|
|
|
@ -28,15 +28,25 @@
|
|||
<div v-if="field.error" class="grid-view__description-icon-error">
|
||||
<i v-tooltip="field.error" class="iconoir-warning-triangle"></i>
|
||||
</div>
|
||||
<a
|
||||
v-if="!readOnly && showFieldContext"
|
||||
ref="contextLink"
|
||||
class="grid-view__description-options"
|
||||
@click="$refs.context.toggle($refs.contextLink, 'bottom', 'right', 0)"
|
||||
@mousedown.stop
|
||||
>
|
||||
<i class="iconoir-nav-arrow-down"></i>
|
||||
</a>
|
||||
<span class="grid-view__description-options">
|
||||
<HelpIcon
|
||||
v-if="field.description"
|
||||
:tooltip="descriptionText"
|
||||
:tooltip-content-type="'html'"
|
||||
:tooltip-content-classes="'tooltip__content--expandable'"
|
||||
:icon="'info-empty'"
|
||||
/>
|
||||
|
||||
<a
|
||||
v-if="!readOnly && showFieldContext"
|
||||
ref="contextLink"
|
||||
class="grid-view__description-icon-trigger"
|
||||
@click="$refs.context.toggle($refs.contextLink, 'bottom', 'right', 0)"
|
||||
@mousedown.stop
|
||||
>
|
||||
<i class="iconoir-nav-arrow-down"></i>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<FieldContext
|
||||
v-if="!readOnly"
|
||||
|
@ -303,6 +313,9 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
descriptionText() {
|
||||
return (this.field.description || '').replaceAll('\n', '<br/>')
|
||||
},
|
||||
width() {
|
||||
return this.getFieldWidth(this.field.id)
|
||||
},
|
||||
|
|
|
@ -568,7 +568,7 @@ export class FieldType extends Registerable {
|
|||
* @returns a sample for this field.
|
||||
*/
|
||||
getDocsFieldResponseExample(
|
||||
{ id, table_id: tableId, name, order, type, primary },
|
||||
{ id, table_id: tableId, name, order, type, primary, description },
|
||||
readOnly
|
||||
) {
|
||||
return {
|
||||
|
@ -579,6 +579,7 @@ export class FieldType extends Registerable {
|
|||
type,
|
||||
primary,
|
||||
read_only: readOnly,
|
||||
description: description || 'A sample description',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -170,7 +170,8 @@
|
|||
"primary": "Indicates if the field is a primary field. If `true` the field cannot be deleted and the value should represent the whole row.",
|
||||
"type": "Type defined for this field.",
|
||||
"extraProps": "Some extra properties are not described here because they are type specific.",
|
||||
"readOnly": "Indicates whether the field is a read only field. If true, it's not possible to update the cell value."
|
||||
"readOnly": "Indicates whether the field is a read only field. If true, it's not possible to update the cell value.",
|
||||
"descriptionField": "Field description"
|
||||
},
|
||||
"apiDocsTableDeleteRow": {
|
||||
"description": "Deletes an existing {name} row.",
|
||||
|
@ -280,9 +281,11 @@
|
|||
},
|
||||
"fieldForm": {
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"fieldAlreadyExists": "A field with this name already exists.",
|
||||
"nameNotAllowed": "This field name is not allowed.",
|
||||
"nameTooLong": "This field name is too long."
|
||||
"nameTooLong": "This field name is too long.",
|
||||
"addDescription": "Add description"
|
||||
},
|
||||
"fieldSelectThroughFieldSubForm": {
|
||||
"noTable": "You need at least one link to table field to create this field.",
|
||||
|
|
|
@ -393,7 +393,13 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
|
|||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
|
@ -427,7 +433,13 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
|
|||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
|
@ -461,7 +473,13 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
|
|||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
|
@ -495,7 +513,13 @@ exports[`Public View Page Tests Can see a publicly shared grid view 1`] = `
|
|||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
|
||||
|
|
|
@ -208,13 +208,19 @@ exports[`GridView component with decoration Default component with first_cell de
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -247,13 +253,19 @@ exports[`GridView component with decoration Default component with first_cell de
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -286,13 +298,19 @@ exports[`GridView component with decoration Default component with first_cell de
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -642,13 +660,19 @@ exports[`GridView component with decoration Default component with row wrapper d
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -681,13 +705,19 @@ exports[`GridView component with decoration Default component with row wrapper d
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -720,13 +750,19 @@ exports[`GridView component with decoration Default component with row wrapper d
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -1079,13 +1115,19 @@ exports[`GridView component with decoration Default component with unavailable d
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -1118,13 +1160,19 @@ exports[`GridView component with decoration Default component with unavailable d
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
@ -1157,13 +1205,19 @@ exports[`GridView component with decoration Default component with unavailable d
|
|||
|
||||
<!---->
|
||||
|
||||
<a
|
||||
<span
|
||||
class="grid-view__description-options"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
<!---->
|
||||
|
||||
<a
|
||||
class="grid-view__description-icon-trigger"
|
||||
>
|
||||
<i
|
||||
class="iconoir-nav-arrow-down"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
<div
|
||||
|
|
Loading…
Add table
Reference in a new issue