@budibase/frontend-core 2.6.22 → 2.6.24-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/package.json +4 -4
  2. package/src/api/app.js +6 -0
  3. package/src/api/automations.js +2 -2
  4. package/src/api/datasources.js +11 -0
  5. package/src/api/groups.js +17 -0
  6. package/src/api/index.js +10 -0
  7. package/src/api/tables.js +4 -2
  8. package/src/components/Testimonial.svelte +67 -0
  9. package/src/components/TestimonialPage.svelte +2 -61
  10. package/src/components/UserAvatar.svelte +58 -0
  11. package/src/components/grid/cells/BooleanCell.svelte +3 -0
  12. package/src/components/grid/cells/DataCell.svelte +3 -0
  13. package/src/components/grid/cells/DateCell.svelte +46 -4
  14. package/src/components/grid/cells/GridCell.svelte +36 -16
  15. package/src/components/grid/cells/GutterCell.svelte +2 -10
  16. package/src/components/grid/cells/HeaderCell.svelte +5 -1
  17. package/src/components/grid/layout/Grid.svelte +22 -5
  18. package/src/components/grid/layout/GridBody.svelte +5 -1
  19. package/src/components/grid/layout/GridRow.svelte +3 -2
  20. package/src/components/grid/layout/HeaderRow.svelte +1 -1
  21. package/src/components/grid/layout/KeyboardShortcut.svelte +1 -1
  22. package/src/components/grid/layout/NewRow.svelte +3 -3
  23. package/src/components/grid/layout/StickyColumn.svelte +2 -1
  24. package/src/components/grid/layout/UserAvatars.svelte +14 -4
  25. package/src/components/grid/lib/constants.js +1 -1
  26. package/src/components/grid/lib/websocket.js +40 -36
  27. package/src/components/grid/overlays/KeyboardManager.svelte +3 -4
  28. package/src/components/grid/overlays/MenuOverlay.svelte +34 -4
  29. package/src/components/grid/stores/columns.js +2 -2
  30. package/src/components/grid/stores/reorder.js +77 -9
  31. package/src/components/grid/stores/rows.js +27 -19
  32. package/src/components/grid/stores/ui.js +20 -1
  33. package/src/components/grid/stores/users.js +18 -63
  34. package/src/components/index.js +2 -0
  35. package/src/constants.js +1 -0
  36. package/src/fetch/DataFetch.js +23 -1
  37. package/src/fetch/GroupUserFetch.js +51 -0
  38. package/src/fetch/fetchData.js +2 -0
  39. package/src/utils/index.js +1 -0
  40. package/src/utils/websocket.js +60 -0
  41. package/src/components/grid/controls/BetaButton.svelte +0 -46
  42. package/src/components/grid/layout/Avatar.svelte +0 -24
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "2.6.22",
3
+ "version": "2.6.24-alpha.0",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
7
7
  "svelte": "src/index.js",
8
8
  "dependencies": {
9
- "@budibase/bbui": "^2.6.22",
10
- "@budibase/shared-core": "^2.6.22",
9
+ "@budibase/bbui": "2.6.24-alpha.0",
10
+ "@budibase/shared-core": "2.6.24-alpha.0",
11
11
  "dayjs": "^1.11.7",
12
12
  "lodash": "^4.17.21",
13
13
  "socket.io-client": "^4.6.1",
14
14
  "svelte": "^3.46.2"
15
15
  },
16
- "gitHead": "6937bcd24a7338ccb9e392f5cb4131a5be4b4b24"
16
+ "gitHead": "71a9558d12c55e7d9ef87b82995c62497662acf8"
17
17
  }
package/src/api/app.js CHANGED
@@ -152,4 +152,10 @@ export const buildAppEndpoints = API => ({
152
152
  url: `/api/${appId}/components/definitions`,
153
153
  })
154
154
  },
155
+
156
+ addSampleData: async appId => {
157
+ return await API.post({
158
+ url: `/api/applications/${appId}/sample`,
159
+ })
160
+ },
155
161
  })
@@ -4,10 +4,10 @@ export const buildAutomationEndpoints = API => ({
4
4
  * @param automationId the ID of the automation to trigger
5
5
  * @param fields the fields to trigger the automation with
6
6
  */
7
- triggerAutomation: async ({ automationId, fields }) => {
7
+ triggerAutomation: async ({ automationId, fields, timeout }) => {
8
8
  return await API.post({
9
9
  url: `/api/automations/${automationId}/trigger`,
10
- body: { fields },
10
+ body: { fields, timeout },
11
11
  })
12
12
  },
13
13
 
@@ -58,4 +58,15 @@ export const buildDatasourceEndpoints = API => ({
58
58
  url: `/api/datasources/${datasourceId}/${datasourceRev}`,
59
59
  })
60
60
  },
61
+
62
+ /**
63
+ * Validate a datasource configuration
64
+ * @param datasource the datasource configuration to validate
65
+ */
66
+ validateDatasource: async datasource => {
67
+ return await API.post({
68
+ url: `/api/datasources/verify`,
69
+ body: { datasource },
70
+ })
71
+ },
61
72
  })
package/src/api/groups.js CHANGED
@@ -52,6 +52,23 @@ export const buildGroupsEndpoints = API => {
52
52
  })
53
53
  },
54
54
 
55
+ /**
56
+ * Gets a group users by the group id
57
+ */
58
+ getGroupUsers: async ({ id, bookmark, emailSearch }) => {
59
+ let url = `/api/global/groups/${id}/users?`
60
+ if (bookmark) {
61
+ url += `bookmark=${bookmark}&`
62
+ }
63
+ if (emailSearch) {
64
+ url += `emailSearch=${emailSearch}&`
65
+ }
66
+
67
+ return await API.get({
68
+ url,
69
+ })
70
+ },
71
+
55
72
  /**
56
73
  * Adds users to a group
57
74
  * @param groupId The group to update
package/src/api/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Helpers } from "@budibase/bbui"
1
2
  import { ApiVersion } from "../constants"
2
3
  import { buildAnalyticsEndpoints } from "./analytics"
3
4
  import { buildAppEndpoints } from "./app"
@@ -30,6 +31,14 @@ import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
30
31
  import { buildEventEndpoints } from "./events"
31
32
  import { buildAuditLogsEndpoints } from "./auditLogs"
32
33
 
34
+ /**
35
+ * Random identifier to uniquely identify a session in a tab. This is
36
+ * used to determine the originator of calls to the API, which is in
37
+ * turn used to determine who caused a websocket message to be sent, so
38
+ * that we can ignore events caused by ourselves.
39
+ */
40
+ export const APISessionID = Helpers.uuid()
41
+
33
42
  const defaultAPIClientConfig = {
34
43
  /**
35
44
  * Certain definitions can't change at runtime for client apps, such as the
@@ -116,6 +125,7 @@ export const createAPIClient = config => {
116
125
 
117
126
  // Build headers
118
127
  let headers = { Accept: "application/json" }
128
+ headers["x-budibase-session-id"] = APISessionID
119
129
  if (!external) {
120
130
  headers["x-budibase-api-version"] = ApiVersion
121
131
  }
package/src/api/tables.js CHANGED
@@ -62,13 +62,15 @@ export const buildTableEndpoints = API => ({
62
62
  /**
63
63
  * Imports data into an existing table
64
64
  * @param tableId the table ID to import to
65
- * @param data the data import object
65
+ * @param rows the data import object
66
+ * @param identifierFields column names to be used as keys for overwriting existing rows
66
67
  */
67
- importTableData: async ({ tableId, rows }) => {
68
+ importTableData: async ({ tableId, rows, identifierFields }) => {
68
69
  return await API.post({
69
70
  url: `/api/tables/${tableId}/import`,
70
71
  body: {
71
72
  rows,
73
+ identifierFields,
72
74
  },
73
75
  })
74
76
  },
@@ -0,0 +1,67 @@
1
+ <script>
2
+ import { Layout } from "@budibase/bbui"
3
+ import Bulgaria from "../../assets/bulgaria.png"
4
+ import Covanta from "../../assets/covanta.png"
5
+ import Schnellecke from "../../assets/schnellecke.png"
6
+
7
+ const testimonials = [
8
+ {
9
+ text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
10
+ name: "Charles Link",
11
+ role: "Senior Director, Data and Analytics",
12
+ image: Covanta,
13
+ imageSize: 105,
14
+ },
15
+ {
16
+ text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
17
+ name: "Bozhidar Bozhanov",
18
+ role: "Government of Bulgaria",
19
+ image: Bulgaria,
20
+ imageSize: 49,
21
+ },
22
+ {
23
+ text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and it’s now used daily for internal development for those apps that you know you need but don’t feel value in losing days of development to reinvent the wheel.",
24
+ name: "Davide Lenzarini",
25
+ role: "IT manager",
26
+ image: Schnellecke,
27
+ imageSize: 141,
28
+ },
29
+ ]
30
+ const testimonial = testimonials[Math.floor(Math.random() * 3)]
31
+ </script>
32
+
33
+ <div class="testimonial">
34
+ <Layout noPadding gap="S">
35
+ <img
36
+ width={testimonial.imageSize}
37
+ alt="a-happy-budibase-user"
38
+ src={testimonial.image}
39
+ />
40
+ <div class="text">
41
+ "{testimonial.text}"
42
+ </div>
43
+ <div class="author">
44
+ <div class="name">{testimonial.name}</div>
45
+ <div class="company">{testimonial.role}</div>
46
+ </div>
47
+ </Layout>
48
+ </div>
49
+
50
+ <style>
51
+ .testimonial {
52
+ width: 380px;
53
+ padding: 40px;
54
+ }
55
+ .text {
56
+ font-size: var(--font-size-l);
57
+ font-style: italic;
58
+ }
59
+ .name {
60
+ font-weight: bold;
61
+ color: var(--spectrum-global-color-gray-900);
62
+ font-size: var(--font-size-l);
63
+ }
64
+ .company {
65
+ color: var(--spectrum-global-color-gray-700);
66
+ }
67
+ </style>
@@ -1,58 +1,15 @@
1
1
  <script>
2
2
  import SplitPage from "./SplitPage.svelte"
3
- import { Layout } from "@budibase/bbui"
4
- import Bulgaria from "../../assets/bulgaria.png"
5
- import Covanta from "../../assets/covanta.png"
6
- import Schnellecke from "../../assets/schnellecke.png"
3
+ import Testimonial from "./Testimonial.svelte"
7
4
 
8
5
  export let enabled = true
9
-
10
- const testimonials = [
11
- {
12
- text: "Budibase was the only solution that checked all the boxes for Covanta. Covanta expects to realize $3.2MM in savings due to the elimination of redundant data entry.",
13
- name: "Charles Link",
14
- role: "Senior Director, Data and Analytics",
15
- image: Covanta,
16
- imageSize: 105,
17
- },
18
- {
19
- text: "Budibase was mission-critical for us and went a long way in preventing what could have become a humanitarian crisis here in Bulgaria.",
20
- name: "Bozhidar Bozhanov",
21
- role: "Government of Bulgaria",
22
- image: Bulgaria,
23
- imageSize: 49,
24
- },
25
- {
26
- text: "Centralization of authentication, quick turnaround time for requests, integration with different database systems has given it the edge and it’s now used daily for internal development for those apps that you know you need but don’t feel value in losing days of development to reinvent the wheel.",
27
- name: "Davide Lenzarini",
28
- role: "IT manager",
29
- image: Schnellecke,
30
- imageSize: 141,
31
- },
32
- ]
33
- const testimonial = testimonials[Math.floor(Math.random() * 3)]
34
6
  </script>
35
7
 
36
8
  <SplitPage>
37
9
  <slot />
38
10
  <div class:wrapper={enabled} slot="right">
39
11
  {#if enabled}
40
- <div class="testimonial">
41
- <Layout noPadding gap="S">
42
- <img
43
- width={testimonial.imageSize}
44
- alt="a-happy-budibase-user"
45
- src={testimonial.image}
46
- />
47
- <div class="text">
48
- "{testimonial.text}"
49
- </div>
50
- <div class="author">
51
- <div class="name">{testimonial.name}</div>
52
- <div class="company">{testimonial.role}</div>
53
- </div>
54
- </Layout>
55
- </div>
12
+ <Testimonial />
56
13
  {/if}
57
14
  </div>
58
15
  </SplitPage>
@@ -64,20 +21,4 @@
64
21
  display: grid;
65
22
  place-items: center;
66
23
  }
67
- .testimonial {
68
- width: 380px;
69
- padding: 40px;
70
- }
71
- .text {
72
- font-size: var(--font-size-l);
73
- font-style: italic;
74
- }
75
- .name {
76
- font-weight: bold;
77
- color: var(--spectrum-global-color-gray-900);
78
- font-size: var(--font-size-l);
79
- }
80
- .company {
81
- color: var(--spectrum-global-color-gray-700);
82
- }
83
24
  </style>
@@ -0,0 +1,58 @@
1
+ <script>
2
+ import { Avatar, Tooltip } from "@budibase/bbui"
3
+ import { helpers } from "@budibase/shared-core"
4
+
5
+ export let user
6
+ export let size
7
+ export let tooltipDirection = "top"
8
+ export let showTooltip = true
9
+
10
+ $: tooltipStyle = getTooltipStyle(tooltipDirection)
11
+
12
+ const getTooltipStyle = direction => {
13
+ if (!direction) {
14
+ return ""
15
+ }
16
+ if (direction === "top") {
17
+ return "transform: translateX(-50%) translateY(-100%);"
18
+ } else if (direction === "bottom") {
19
+ return "transform: translateX(-50%) translateY(100%);"
20
+ }
21
+ }
22
+ </script>
23
+
24
+ {#if user}
25
+ <div class="user-avatar">
26
+ <Avatar
27
+ {size}
28
+ initials={helpers.getUserInitials(user)}
29
+ color={helpers.getUserColor(user)}
30
+ />
31
+ {#if showTooltip}
32
+ <div class="tooltip" style={tooltipStyle}>
33
+ <Tooltip
34
+ direction={tooltipDirection}
35
+ textWrapping
36
+ text={user.email}
37
+ size="S"
38
+ />
39
+ </div>
40
+ {/if}
41
+ </div>
42
+ {/if}
43
+
44
+ <style>
45
+ .user-avatar {
46
+ position: relative;
47
+ }
48
+ .tooltip {
49
+ display: none;
50
+ position: absolute;
51
+ top: 0;
52
+ left: 50%;
53
+ white-space: nowrap;
54
+ }
55
+ .user-avatar:hover .tooltip {
56
+ display: block;
57
+ }
58
+ </style>
@@ -37,6 +37,9 @@
37
37
  .boolean-cell {
38
38
  padding: 2px var(--cell-padding);
39
39
  pointer-events: none;
40
+ flex: 1 1 auto;
41
+ display: flex;
42
+ justify-content: center;
40
43
  }
41
44
  .boolean-cell.editable {
42
45
  pointer-events: all;
@@ -11,6 +11,7 @@
11
11
  export let selected
12
12
  export let rowFocused
13
13
  export let rowIdx
14
+ export let topRow = false
14
15
  export let focused
15
16
  export let selectedUser
16
17
  export let column
@@ -32,6 +33,7 @@
32
33
  $: readonly =
33
34
  column.schema.autocolumn ||
34
35
  column.schema.disabled ||
36
+ column.schema.type === "formula" ||
35
37
  (!$config.allowEditRows && row._id)
36
38
 
37
39
  // Register this cell API if the row is focused
@@ -67,6 +69,7 @@
67
69
  {highlighted}
68
70
  {selected}
69
71
  {rowIdx}
72
+ {topRow}
70
73
  {focused}
71
74
  {selectedUser}
72
75
  {readonly}
@@ -1,17 +1,22 @@
1
1
  <script>
2
2
  import dayjs from "dayjs"
3
3
  import { CoreDatePicker, Icon } from "@budibase/bbui"
4
+ import { onMount } from "svelte"
4
5
 
5
6
  export let value
6
7
  export let schema
7
8
  export let onChange
8
9
  export let focused = false
9
10
  export let readonly = false
11
+ export let api
10
12
 
11
- // adding the 0- will turn a string like 00:00:00 into a valid ISO
13
+ let flatpickr
14
+ let isOpen
15
+
16
+ // Adding the 0- will turn a string like 00:00:00 into a valid ISO
12
17
  // date, but will make actual ISO dates invalid
13
- $: time = new Date(`0-${value}`)
14
- $: timeOnly = !isNaN(time) || schema?.timeOnly
18
+ $: isTimeValue = !isNaN(new Date(`0-${value}`))
19
+ $: timeOnly = isTimeValue || schema?.timeOnly
15
20
  $: dateOnly = schema?.dateOnly
16
21
  $: format = timeOnly
17
22
  ? "HH:mm:ss"
@@ -19,12 +24,45 @@
19
24
  ? "MMM D YYYY"
20
25
  : "MMM D YYYY, HH:mm"
21
26
  $: editable = focused && !readonly
27
+ $: displayValue = getDisplayValue(value, format, timeOnly, isTimeValue)
28
+
29
+ const getDisplayValue = (value, format, timeOnly, isTimeValue) => {
30
+ if (!value) {
31
+ return ""
32
+ }
33
+ // Parse full date strings
34
+ if (!timeOnly || !isTimeValue) {
35
+ return dayjs(value).format(format)
36
+ }
37
+ // Otherwise must be a time string
38
+ return dayjs(`0-${value}`).format(format)
39
+ }
40
+
41
+ // Ensure we close flatpickr when unselected
42
+ $: {
43
+ if (!focused) {
44
+ flatpickr?.close()
45
+ }
46
+ }
47
+
48
+ const onKeyDown = () => {
49
+ return isOpen
50
+ }
51
+
52
+ onMount(() => {
53
+ api = {
54
+ onKeyDown,
55
+ focus: () => flatpickr?.open(),
56
+ blur: () => flatpickr?.close(),
57
+ isActive: () => isOpen,
58
+ }
59
+ })
22
60
  </script>
23
61
 
24
62
  <div class="container">
25
63
  <div class="value">
26
64
  {#if value}
27
- {dayjs(timeOnly ? time : value).format(format)}
65
+ {displayValue}
28
66
  {/if}
29
67
  </div>
30
68
  {#if editable}
@@ -42,6 +80,10 @@
42
80
  {timeOnly}
43
81
  time24hr
44
82
  ignoreTimezones={schema.ignoreTimezones}
83
+ bind:flatpickr
84
+ on:open={() => (isOpen = true)}
85
+ on:close={() => (isOpen = false)}
86
+ useKeyboardShortcuts={false}
45
87
  />
46
88
  </div>
47
89
  {/if}
@@ -6,6 +6,7 @@
6
6
  export let selectedUser = null
7
7
  export let error = null
8
8
  export let rowIdx
9
+ export let topRow = false
9
10
  export let defaultHeight = false
10
11
  export let center = false
11
12
  export let readonly = false
@@ -15,7 +16,7 @@
15
16
  const getStyle = (width, selectedUser) => {
16
17
  let style = `flex: 0 0 ${width}px;`
17
18
  if (selectedUser) {
18
- style += `--cell-color:${selectedUser.color};`
19
+ style += `--user-color:${selectedUser.color};`
19
20
  }
20
21
  return style
21
22
  }
@@ -31,13 +32,14 @@
31
32
  class:readonly
32
33
  class:default-height={defaultHeight}
33
34
  class:selected-other={selectedUser != null}
35
+ class:alt={rowIdx % 2 === 1}
36
+ class:top={topRow}
34
37
  on:focus
35
38
  on:mousedown
36
39
  on:mouseup
37
40
  on:click
38
41
  on:contextmenu
39
42
  {style}
40
- data-row={rowIdx}
41
43
  >
42
44
  {#if error}
43
45
  <div class="label">
@@ -70,6 +72,9 @@
70
72
  width: 0;
71
73
  --cell-color: transparent;
72
74
  }
75
+ .cell.alt {
76
+ --cell-background: var(--cell-background-alt);
77
+ }
73
78
  .cell.default-height {
74
79
  height: var(--default-row-height);
75
80
  }
@@ -94,14 +99,15 @@
94
99
  }
95
100
 
96
101
  /* Cell border for cells with labels */
97
- .cell.error:after,
98
- .cell.selected-other:not(.focused):after {
102
+ .cell.error:after {
99
103
  border-radius: 0 2px 2px 2px;
100
104
  }
101
- .cell[data-row="0"].error:after,
102
- .cell[data-row="0"].selected-other:not(.focused):after {
105
+ .cell.top.error:after {
103
106
  border-radius: 2px 2px 2px 0;
104
107
  }
108
+ .cell.selected-other:not(.focused):after {
109
+ border-radius: 2px;
110
+ }
105
111
 
106
112
  /* Cell z-index */
107
113
  .cell.error,
@@ -111,21 +117,30 @@
111
117
  .cell.focused {
112
118
  z-index: 2;
113
119
  }
120
+ .cell.selected-other:hover {
121
+ z-index: 2;
122
+ }
123
+ .cell:not(.focused) {
124
+ user-select: none;
125
+ }
126
+ .cell:hover {
127
+ cursor: default;
128
+ }
129
+
130
+ /* Cell color overrides */
131
+ .cell.selected-other {
132
+ --cell-color: var(--user-color);
133
+ }
114
134
  .cell.focused {
115
135
  --cell-color: var(--spectrum-global-color-blue-400);
116
136
  }
117
137
  .cell.error {
118
138
  --cell-color: var(--spectrum-global-color-red-500);
119
139
  }
120
- .cell.readonly {
140
+ .cell.focused.readonly {
121
141
  --cell-color: var(--spectrum-global-color-gray-600);
122
142
  }
123
- .cell:not(.focused) {
124
- user-select: none;
125
- }
126
- .cell:hover {
127
- cursor: default;
128
- }
143
+
129
144
  .cell.highlighted:not(.focused),
130
145
  .cell.focused.readonly {
131
146
  --cell-background: var(--cell-background-hover);
@@ -141,7 +156,7 @@
141
156
  left: 0;
142
157
  padding: 1px 4px 3px 4px;
143
158
  margin: 0 0 -2px 0;
144
- background: var(--user-color);
159
+ background: var(--cell-color);
145
160
  border-radius: 2px;
146
161
  display: block;
147
162
  color: white;
@@ -152,14 +167,19 @@
152
167
  overflow: hidden;
153
168
  user-select: none;
154
169
  }
155
- .cell[data-row="0"] .label {
170
+ .cell.top .label {
156
171
  bottom: auto;
157
172
  top: 100%;
158
- border-radius: 0 2px 2px 2px;
159
173
  padding: 2px 4px 2px 4px;
160
174
  margin: -2px 0 0 0;
161
175
  }
162
176
  .error .label {
163
177
  background: var(--spectrum-global-color-red-500);
164
178
  }
179
+ .selected-other:not(.error) .label {
180
+ display: none;
181
+ }
182
+ .selected-other:not(.error):hover .label {
183
+ display: block;
184
+ }
165
185
  </style>
@@ -21,16 +21,7 @@
21
21
  svelteDispatch("select")
22
22
  const id = row?._id
23
23
  if (id) {
24
- selectedRows.update(state => {
25
- let newState = {
26
- ...state,
27
- [id]: !state[id],
28
- }
29
- if (!newState[id]) {
30
- delete newState[id]
31
- }
32
- return newState
33
- })
24
+ selectedRows.actions.toggleRow(id)
34
25
  }
35
26
  }
36
27
 
@@ -47,6 +38,7 @@
47
38
  highlighted={rowFocused || rowHovered}
48
39
  selected={rowSelected}
49
40
  {defaultHeight}
41
+ rowIdx={row?.__idx}
50
42
  >
51
43
  <div class="gutter">
52
44
  {#if $$slots.default}
@@ -196,7 +196,11 @@
196
196
  <MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
197
197
  Move right
198
198
  </MenuItem>
199
- <MenuItem icon="VisibilityOff" on:click={hideColumn}>Hide column</MenuItem>
199
+ <MenuItem
200
+ disabled={idx === "sticky"}
201
+ icon="VisibilityOff"
202
+ on:click={hideColumn}>Hide column</MenuItem
203
+ >
200
204
  </Menu>
201
205
  </Popover>
202
206