commit
ff53acfb9f
|
@ -21,6 +21,10 @@ export const DEFINITIONS: MigrationDefinition[] = [
|
|||
type: MigrationType.APP,
|
||||
name: MigrationName.EVENT_APP_BACKFILL,
|
||||
},
|
||||
{
|
||||
type: MigrationType.APP,
|
||||
name: MigrationName.TABLE_SETTINGS_LINKS_TO_ACTIONS,
|
||||
},
|
||||
{
|
||||
type: MigrationType.GLOBAL,
|
||||
name: MigrationName.EVENT_GLOBAL_BACKFILL,
|
||||
|
|
|
@ -1,18 +1,53 @@
|
|||
export default function clickOutside(element, callbackFunction) {
|
||||
function onClick(event) {
|
||||
if (!element.contains(event.target)) {
|
||||
callbackFunction(event)
|
||||
const ignoredClasses = [".flatpickr-calendar", ".modal-container"]
|
||||
let clickHandlers = []
|
||||
|
||||
/**
|
||||
* Handle a body click event
|
||||
*/
|
||||
const handleClick = event => {
|
||||
// Ignore click if needed
|
||||
for (let className of ignoredClasses) {
|
||||
if (event.target.closest(className)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
document.body.addEventListener("click", onClick, true)
|
||||
// Process handlers
|
||||
clickHandlers.forEach(handler => {
|
||||
if (!handler.element.contains(event.target)) {
|
||||
handler.callback?.(event)
|
||||
}
|
||||
})
|
||||
}
|
||||
document.documentElement.addEventListener("click", handleClick, true)
|
||||
|
||||
/**
|
||||
* Adds or updates a click handler
|
||||
*/
|
||||
const updateHandler = (id, element, callback) => {
|
||||
let existingHandler = clickHandlers.find(x => x.id === id)
|
||||
if (!existingHandler) {
|
||||
clickHandlers.push({ id, element, callback })
|
||||
} else {
|
||||
existingHandler.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a click handler
|
||||
*/
|
||||
const removeHandler = id => {
|
||||
clickHandlers = clickHandlers.filter(x => x.id !== id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Svelte action to apply a click outside handler for a certain element
|
||||
*/
|
||||
export default (element, callback) => {
|
||||
const id = Math.random()
|
||||
updateHandler(id, element, callback)
|
||||
return {
|
||||
update(newCallbackFunction) {
|
||||
callbackFunction = newCallbackFunction
|
||||
},
|
||||
destroy() {
|
||||
document.body.removeEventListener("click", onClick, true)
|
||||
},
|
||||
update: newCallback => updateHandler(id, element, newCallback),
|
||||
destroy: () => removeHandler(id),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,19 @@
|
|||
</script>
|
||||
|
||||
<div class="property-group-container">
|
||||
{#if name}
|
||||
<div class="property-group-name" on:click={onHeaderClick}>
|
||||
<div class="name">{name}</div>
|
||||
{#if collapsible}
|
||||
<Icon size="S" name={show ? "Remove" : "Add"} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="property-panel" class:show={show || !collapsible}>
|
||||
{/if}
|
||||
<div
|
||||
class="property-panel"
|
||||
class:show={show || !collapsible}
|
||||
class:no-title={!name}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -72,6 +78,9 @@
|
|||
padding: var(--spacing-s) var(--spacing-xl) var(--spacing-xl)
|
||||
var(--spacing-xl);
|
||||
}
|
||||
.property-panel.no-title {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.show {
|
||||
display: flex;
|
||||
|
|
|
@ -23,6 +23,15 @@
|
|||
let open = false
|
||||
let flatpickr, flatpickrOptions
|
||||
|
||||
// Another classic flatpickr issue. Errors were randomly being thrown due to
|
||||
// flatpickr internal code. Making sure that "destroy" is a valid function
|
||||
// fixes it. The sooner we remove flatpickr the better.
|
||||
$: {
|
||||
if (flatpickr && !flatpickr.destroy) {
|
||||
flatpickr.destroy = () => {}
|
||||
}
|
||||
}
|
||||
|
||||
const resolveTimeStamp = timestamp => {
|
||||
let maskedDate = new Date(`0-${timestamp}`)
|
||||
|
||||
|
@ -252,6 +261,7 @@
|
|||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
max-height: 100%;
|
||||
}
|
||||
:global(.flatpickr-calendar) {
|
||||
font-family: "Source Sans Pro", sans-serif;
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
transition: color var(--spectrum-global-animation-duration-100, 130ms);
|
||||
}
|
||||
svg.hoverable:hover {
|
||||
color: var(--spectrum-alias-icon-color-selected-hover);
|
||||
color: var(--spectrum-alias-icon-color-selected-hover) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -275,6 +275,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#key fields?.length}
|
||||
<div
|
||||
class="wrapper"
|
||||
class:wrapper--quiet={quiet}
|
||||
|
@ -313,8 +314,8 @@
|
|||
class:noBorderHeader={!showHeaderBorder}
|
||||
class:spectrum-Table-headCell--alignCenter={schema[field]
|
||||
.align === "Center"}
|
||||
class:spectrum-Table-headCell--alignRight={schema[field].align ===
|
||||
"Right"}
|
||||
class:spectrum-Table-headCell--alignRight={schema[field]
|
||||
.align === "Right"}
|
||||
class:is-sortable={schema[field].sortable !== false}
|
||||
class:is-sorted-desc={sortColumn === field &&
|
||||
sortOrder === "Descending"}
|
||||
|
@ -426,6 +427,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
/* Wrapper */
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const Events = {
|
||||
COMPONENT_CREATED: "component:created",
|
||||
COMPONENT_UPDATED: "component:updated",
|
||||
APP_VIEW_PUBLISHED: "app:view_published",
|
||||
}
|
||||
|
||||
|
|
|
@ -182,13 +182,21 @@ export const makeComponentUnique = component => {
|
|||
return
|
||||
}
|
||||
|
||||
// Replace component ID
|
||||
// Generate a full set of component ID replacements in this tree
|
||||
const idReplacements = []
|
||||
const generateIdReplacements = (component, replacements) => {
|
||||
const oldId = component._id
|
||||
const newId = Helpers.uuid()
|
||||
let definition = JSON.stringify(component)
|
||||
replacements.push([oldId, newId])
|
||||
component._children?.forEach(x => generateIdReplacements(x, replacements))
|
||||
}
|
||||
generateIdReplacements(component, idReplacements)
|
||||
|
||||
// Replace all instances of this ID in HBS bindings
|
||||
let definition = JSON.stringify(component)
|
||||
idReplacements.forEach(([oldId, newId]) => {
|
||||
definition = definition.replace(new RegExp(oldId, "g"), newId)
|
||||
})
|
||||
|
||||
// Replace all instances of this ID in JS bindings
|
||||
const bindings = findHBSBlocks(definition)
|
||||
|
@ -201,7 +209,9 @@ export const makeComponentUnique = component => {
|
|||
let js = decodeJSBinding(sanitizedBinding)
|
||||
if (js != null) {
|
||||
// Replace ID inside JS binding
|
||||
idReplacements.forEach(([oldId, newId]) => {
|
||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||
})
|
||||
|
||||
// Create new valid JS binding
|
||||
let newBinding = encodeJSBinding(js)
|
||||
|
@ -218,9 +228,5 @@ export const makeComponentUnique = component => {
|
|||
})
|
||||
|
||||
// Recurse on all children
|
||||
component = JSON.parse(definition)
|
||||
return {
|
||||
...component,
|
||||
_children: component._children?.map(makeComponentUnique),
|
||||
}
|
||||
return JSON.parse(definition)
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
messagePassing: false,
|
||||
continueIfAction: false,
|
||||
showNotificationAction: false,
|
||||
sidePanel: false,
|
||||
},
|
||||
errors: [],
|
||||
hasAppPackage: false,
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import newRowScreen from "./newRowScreen"
|
||||
import rowDetailScreen from "./rowDetailScreen"
|
||||
import rowListScreen from "./rowListScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
|
||||
const allTemplates = tables => [
|
||||
...newRowScreen(tables),
|
||||
...rowDetailScreen(tables),
|
||||
...rowListScreen(tables),
|
||||
]
|
||||
const allTemplates = tables => [...rowListScreen(tables)]
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
const createTemplateOverride = (frontendState, template) => () => {
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
import sanitizeUrl from "./utils/sanitizeUrl"
|
||||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
import { makeBreadcrumbContainer } from "./utils/commonComponents"
|
||||
import { getSchemaForDatasource } from "../../dataBinding"
|
||||
|
||||
export default function (tables) {
|
||||
return tables.map(table => {
|
||||
return {
|
||||
name: `${table.name} - New`,
|
||||
create: () => createScreen(table),
|
||||
id: NEW_ROW_TEMPLATE,
|
||||
table: table._id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const newRowUrl = table => sanitizeUrl(`/${table.name}/new/row`)
|
||||
export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
|
||||
|
||||
const rowListUrl = table => sanitizeUrl(`/${table.name}`)
|
||||
|
||||
const getFields = schema => {
|
||||
let columns = []
|
||||
Object.entries(schema || {}).forEach(([field, fieldSchema]) => {
|
||||
if (!field || !fieldSchema) {
|
||||
return
|
||||
}
|
||||
if (!fieldSchema?.autocolumn) {
|
||||
columns.push(field)
|
||||
}
|
||||
})
|
||||
return columns
|
||||
}
|
||||
|
||||
const generateFormBlock = table => {
|
||||
const datasource = { type: "table", tableId: table._id }
|
||||
const { schema } = getSchemaForDatasource(null, datasource, {
|
||||
formSchema: true,
|
||||
})
|
||||
const formBlock = new Component("@budibase/standard-components/formblock")
|
||||
formBlock
|
||||
.customProps({
|
||||
title: "New row",
|
||||
actionType: "Create",
|
||||
actionUrl: rowListUrl(table),
|
||||
showDeleteButton: false,
|
||||
showSaveButton: true,
|
||||
fields: getFields(schema),
|
||||
dataSource: {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
labelPosition: "left",
|
||||
size: "spectrum--medium",
|
||||
})
|
||||
.instanceName(`${table.name} - Form block`)
|
||||
return formBlock
|
||||
}
|
||||
|
||||
const createScreen = table => {
|
||||
const formBlock = generateFormBlock(table)
|
||||
const screen = new Screen()
|
||||
.instanceName(`${table.name} - New`)
|
||||
.route(newRowUrl(table))
|
||||
|
||||
return screen
|
||||
.addChild(makeBreadcrumbContainer(table.name, "New row"))
|
||||
.addChild(formBlock)
|
||||
.json()
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import sanitizeUrl from "./utils/sanitizeUrl"
|
||||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
import { makeBreadcrumbContainer } from "./utils/commonComponents"
|
||||
import { getSchemaForDatasource } from "../../dataBinding"
|
||||
|
||||
export default function (tables) {
|
||||
return tables.map(table => {
|
||||
return {
|
||||
name: `${table.name} - Detail`,
|
||||
create: () => createScreen(table),
|
||||
id: ROW_DETAIL_TEMPLATE,
|
||||
table: table._id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
|
||||
export const rowDetailUrl = table => sanitizeUrl(`/${table.name}/:id`)
|
||||
|
||||
const rowListUrl = table => sanitizeUrl(`/${table.name}`)
|
||||
|
||||
const getFields = schema => {
|
||||
let columns = []
|
||||
Object.entries(schema || {}).forEach(([field, fieldSchema]) => {
|
||||
if (!field || !fieldSchema) {
|
||||
return
|
||||
}
|
||||
if (!fieldSchema?.autocolumn) {
|
||||
columns.push(field)
|
||||
}
|
||||
})
|
||||
return columns
|
||||
}
|
||||
|
||||
const generateFormBlock = table => {
|
||||
const datasource = { type: "table", tableId: table._id }
|
||||
const { schema } = getSchemaForDatasource(null, datasource, {
|
||||
formSchema: true,
|
||||
})
|
||||
|
||||
const formBlock = new Component("@budibase/standard-components/formblock")
|
||||
formBlock
|
||||
.customProps({
|
||||
title: "Edit row",
|
||||
actionType: "Update",
|
||||
actionUrl: rowListUrl(table),
|
||||
showDeleteButton: true,
|
||||
showSaveButton: true,
|
||||
fields: getFields(schema),
|
||||
dataSource: {
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
labelPosition: "left",
|
||||
size: "spectrum--medium",
|
||||
})
|
||||
.instanceName(`${table.name} - Form block`)
|
||||
return formBlock
|
||||
}
|
||||
|
||||
const createScreen = table => {
|
||||
return new Screen()
|
||||
.instanceName(`${table.name} - Detail`)
|
||||
.route(rowDetailUrl(table))
|
||||
.addChild(makeBreadcrumbContainer(table.name, "Edit row"))
|
||||
.addChild(generateFormBlock(table))
|
||||
.json()
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import sanitizeUrl from "./utils/sanitizeUrl"
|
||||
import { newRowUrl } from "./newRowScreen"
|
||||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
|
||||
|
@ -21,12 +20,6 @@ const generateTableBlock = table => {
|
|||
const tableBlock = new Component("@budibase/standard-components/tableblock")
|
||||
tableBlock
|
||||
.customProps({
|
||||
linkRows: true,
|
||||
linkURL: `${rowListUrl(table)}/:id`,
|
||||
showAutoColumns: false,
|
||||
showTitleButton: true,
|
||||
titleButtonText: "Create new",
|
||||
titleButtonURL: newRowUrl(table),
|
||||
title: table.name,
|
||||
dataSource: {
|
||||
label: table.name,
|
||||
|
@ -34,9 +27,14 @@ const generateTableBlock = table => {
|
|||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
sortOrder: "Ascending",
|
||||
size: "spectrum--medium",
|
||||
paginate: true,
|
||||
rowCount: 8,
|
||||
clickBehaviour: "details",
|
||||
showTitleButton: true,
|
||||
titleButtonText: "Create row",
|
||||
titleButtonClickBehaviour: "new",
|
||||
})
|
||||
.instanceName(`${table.name} - Table block`)
|
||||
return tableBlock
|
||||
|
|
|
@ -1,137 +1,6 @@
|
|||
import { Component } from "./Component"
|
||||
import { rowListUrl } from "../rowListScreen"
|
||||
import { getSchemaForDatasource } from "../../../dataBinding"
|
||||
|
||||
export function spectrumColor(number) {
|
||||
// Acorn throws a parsing error in this file if the word g-l-o-b-a-l is found
|
||||
// (without dashes - I can't even type it in a comment).
|
||||
// God knows why. It seems to think optional chaining further down the
|
||||
// file is invalid if the word g-l-o-b-a-l is found - hence the reason this
|
||||
// statement is split into parts.
|
||||
return "var(--spectrum-glo" + `bal-color-gray-${number})`
|
||||
}
|
||||
|
||||
export function makeLinkComponent(tableName) {
|
||||
return new Component("@budibase/standard-components/link")
|
||||
.text(tableName)
|
||||
.customProps({
|
||||
url: `/${tableName.toLowerCase()}`,
|
||||
openInNewTab: false,
|
||||
color: spectrumColor(700),
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
}
|
||||
|
||||
export function makeMainForm() {
|
||||
return new Component("@budibase/standard-components/form")
|
||||
.normalStyle({
|
||||
width: "600px",
|
||||
})
|
||||
.instanceName("Form")
|
||||
}
|
||||
|
||||
export function makeBreadcrumbContainer(tableName, text) {
|
||||
const link = makeLinkComponent(tableName).instanceName("Back Link")
|
||||
|
||||
const arrowText = new Component("@budibase/standard-components/text")
|
||||
.type("none")
|
||||
.normalStyle({
|
||||
"margin-right": "4px",
|
||||
"margin-left": "4px",
|
||||
})
|
||||
.text(">")
|
||||
.instanceName("Arrow")
|
||||
.customProps({
|
||||
color: spectrumColor(700),
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
const identifierText = new Component("@budibase/standard-components/text")
|
||||
.text(text)
|
||||
.instanceName("Identifier")
|
||||
.customProps({
|
||||
color: spectrumColor(700),
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
return new Component("@budibase/standard-components/container")
|
||||
.customProps({
|
||||
gap: "N",
|
||||
direction: "row",
|
||||
hAlign: "left",
|
||||
vAlign: "middle",
|
||||
size: "shrink",
|
||||
})
|
||||
.normalStyle({
|
||||
width: "600px",
|
||||
"margin-right": "auto",
|
||||
"margin-left": "auto",
|
||||
})
|
||||
.instanceName("Breadcrumbs")
|
||||
.addChild(link)
|
||||
.addChild(arrowText)
|
||||
.addChild(identifierText)
|
||||
}
|
||||
|
||||
export function makeSaveButton(table, formId) {
|
||||
return new Component("@budibase/standard-components/button")
|
||||
.text("Save")
|
||||
.customProps({
|
||||
type: "primary",
|
||||
size: "M",
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Validate Form",
|
||||
parameters: {
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
providerId: formId,
|
||||
tableId: table._id,
|
||||
},
|
||||
"##eventHandlerType": "Save Row",
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
url: rowListUrl(table),
|
||||
},
|
||||
"##eventHandlerType": "Navigate To",
|
||||
},
|
||||
],
|
||||
})
|
||||
.instanceName("Save Button")
|
||||
}
|
||||
|
||||
export function makeTitleContainer(title) {
|
||||
const heading = new Component("@budibase/standard-components/heading")
|
||||
.instanceName("Title")
|
||||
.text(title)
|
||||
.customProps({
|
||||
size: "M",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
return new Component("@budibase/standard-components/container")
|
||||
.normalStyle({
|
||||
"margin-top": "32px",
|
||||
"margin-bottom": "32px",
|
||||
})
|
||||
.customProps({
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "middle",
|
||||
size: "shrink",
|
||||
gap: "M",
|
||||
})
|
||||
.instanceName("Title Container")
|
||||
.addChild(heading)
|
||||
}
|
||||
|
||||
const fieldTypeToComponentMap = {
|
||||
string: "stringfield",
|
||||
number: "numberfield",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Checkbox, Select, Stepper } from "@budibase/bbui"
|
||||
import { Checkbox, Select, RadioGroup, Stepper } from "@budibase/bbui"
|
||||
import DataSourceSelect from "./controls/DataSourceSelect.svelte"
|
||||
import S3DataSourceSelect from "./controls/S3DataSourceSelect.svelte"
|
||||
import DataProviderSelect from "./controls/DataProviderSelect.svelte"
|
||||
|
@ -25,6 +25,7 @@ import BarButtonList from "./controls/BarButtonList.svelte"
|
|||
const componentMap = {
|
||||
text: DrawerBindableCombobox,
|
||||
select: Select,
|
||||
radio: RadioGroup,
|
||||
dataSource: DataSourceSelect,
|
||||
"dataSource/s3": S3DataSourceSelect,
|
||||
dataProvider: DataProviderSelect,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<div class="root">This action doesn't require any settings.</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { selectedScreen } from "builderStore"
|
||||
import { findAllMatchingComponents } from "builderStore/componentUtils"
|
||||
|
||||
export let parameters
|
||||
|
||||
$: sidePanelOptions = getSidePanelOptions($selectedScreen)
|
||||
|
||||
const getSidePanelOptions = screen => {
|
||||
const sidePanelComponents = findAllMatchingComponents(
|
||||
screen.props,
|
||||
component => component._component.endsWith("/sidepanel")
|
||||
)
|
||||
return sidePanelComponents.map(sidePanel => ({
|
||||
label: sidePanel._instanceName,
|
||||
value: sidePanel._id,
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label small>Side Panel</Label>
|
||||
<Select bind:value={parameters.id} options={sidePanelOptions} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
column-gap: var(--spacing-l);
|
||||
row-gap: var(--spacing-s);
|
||||
grid-template-columns: 60px 1fr;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -16,3 +16,5 @@ export { default as ExportData } from "./ExportData.svelte"
|
|||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
||||
export { default as ShowNotification } from "./ShowNotification.svelte"
|
||||
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
||||
export { default as CloseSidePanel } from "./CloseSidePanel.svelte"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as ActionComponents from "./actions"
|
||||
import { get } from "svelte/store"
|
||||
import { store } from "builderStore"
|
||||
// @ts-ignore
|
||||
import ActionDefinitions from "./manifest.json"
|
||||
|
||||
// Defines which actions are available to configure in the front end.
|
||||
|
|
|
@ -116,6 +116,18 @@
|
|||
"type": "application",
|
||||
"component": "ShowNotification",
|
||||
"dependsOnFeature": "showNotificationAction"
|
||||
},
|
||||
{
|
||||
"name": "Open Side Panel",
|
||||
"type": "application",
|
||||
"component": "OpenSidePanel",
|
||||
"dependsOnFeature": "sidePanel"
|
||||
},
|
||||
{
|
||||
"name": "Close Side Panel",
|
||||
"type": "application",
|
||||
"component": "CloseSidePanel",
|
||||
"dependsOnFeature": "sidePanel"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import { DetailSummary, Icon } from "@budibase/bbui"
|
||||
|
||||
export let componentDefinition
|
||||
</script>
|
||||
|
||||
<DetailSummary collapsible={false}>
|
||||
<div class="info">
|
||||
<div class="title">
|
||||
<Icon name="HelpOutline" />
|
||||
{componentDefinition.name}
|
||||
</div>
|
||||
{componentDefinition.info}
|
||||
</div>
|
||||
</DetailSummary>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--spacing-m);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-s);
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.info {
|
||||
padding: var(--spacing-m) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
||||
background-color: var(--background-alt);
|
||||
border-radius: var(--border-radius-s);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
|
@ -5,6 +5,7 @@
|
|||
import DesignSection from "./DesignSection.svelte"
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
||||
import ComponentInfoSection from "./ComponentInfoSection.svelte"
|
||||
import {
|
||||
getBindableProperties,
|
||||
getComponentBindableProperties,
|
||||
|
@ -29,6 +30,9 @@
|
|||
{#if $selectedComponent}
|
||||
{#key $selectedComponent._id}
|
||||
<Panel {title} icon={componentDefinition?.icon} borderLeft>
|
||||
{#if componentDefinition.info}
|
||||
<ComponentInfoSection {componentDefinition} />
|
||||
{/if}
|
||||
<ComponentSettingsSection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
|
||||
import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte"
|
||||
import { getComponentForSetting } from "components/design/settings/componentSettings"
|
||||
import analytics, { Events } from "analytics"
|
||||
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
|
@ -36,15 +37,26 @@
|
|||
section.settings.forEach(setting => {
|
||||
setting.visible = canRenderControl(instance, setting, isScreen)
|
||||
})
|
||||
section.visible = section.settings.some(setting => setting.visible)
|
||||
section.visible =
|
||||
section.name === "General" ||
|
||||
section.settings.some(setting => setting.visible)
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
const updateSetting = async (key, value) => {
|
||||
const updateSetting = async (setting, value) => {
|
||||
try {
|
||||
await store.actions.components.updateSetting(key, value)
|
||||
await store.actions.components.updateSetting(setting.key, value)
|
||||
|
||||
// Send event if required
|
||||
if (setting.sendEvents) {
|
||||
analytics.captureEvent(Events.COMPONENT_UPDATED, {
|
||||
name: componentInstance._component,
|
||||
setting: setting.key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error updating component prop")
|
||||
}
|
||||
|
@ -111,7 +123,7 @@
|
|||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateSetting("_instanceName", val)}
|
||||
onChange={val => updateSetting({ key: "_instanceName" }, val)}
|
||||
/>
|
||||
{/if}
|
||||
{#each section.settings as setting (setting.key)}
|
||||
|
@ -124,7 +136,7 @@
|
|||
value={componentInstance[setting.key]}
|
||||
defaultValue={setting.defaultValue}
|
||||
nested={setting.nested}
|
||||
onChange={val => updateSetting(setting.key, val)}
|
||||
onChange={val => updateSetting(setting, val)}
|
||||
highlighted={$store.highlightedSettingKey === setting.key}
|
||||
info={setting.info}
|
||||
props={{
|
||||
|
@ -148,9 +160,11 @@
|
|||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if idx === 0 && componentDefinition?.block}
|
||||
<EjectBlockButton />
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if componentDefinition?.block}
|
||||
<DetailSummary name="Eject" collapsible={false}>
|
||||
<EjectBlockButton />
|
||||
</DetailSummary>
|
||||
{/if}
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"children": [
|
||||
"container",
|
||||
"section",
|
||||
"grid"
|
||||
"grid",
|
||||
"sidepanel"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
<span data-cy="data-source-modal">
|
||||
<ModalContent
|
||||
title="Create CRUD Screens"
|
||||
title="Autogenerated screens"
|
||||
confirmText="Confirm"
|
||||
cancelText="Back"
|
||||
onConfirm={confirmDatasourceSelection}
|
||||
|
@ -77,7 +77,7 @@
|
|||
size="L"
|
||||
>
|
||||
<Body size="S">
|
||||
Select which datasource you would like to use to create your screens
|
||||
Select which datasources you would like to use to create your screens
|
||||
</Body>
|
||||
<Layout noPadding gap="S">
|
||||
{#each filteredSources as datasource}
|
||||
|
|
|
@ -40,9 +40,9 @@
|
|||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={"Create CRUD Screens"}
|
||||
confirmText={"Done"}
|
||||
cancelText={"Back"}
|
||||
title="Autogenerated screens"
|
||||
confirmText="Done"
|
||||
cancelText="Back"
|
||||
{onConfirm}
|
||||
{onCancel}
|
||||
disabled={!!error}
|
||||
|
|
|
@ -127,9 +127,6 @@
|
|||
|
||||
// Handler for Datasource Screen Creation
|
||||
const completeDatasourceScreenCreation = async () => {
|
||||
// // Handle template selection
|
||||
if (selectedTemplates?.length > 1) {
|
||||
// Autoscreens, so create immediately
|
||||
const screens = selectedTemplates.map(template => {
|
||||
let screenTemplate = template.create()
|
||||
screenTemplate.datasource = template.datasource
|
||||
|
@ -138,7 +135,6 @@
|
|||
})
|
||||
await createScreens({ screens, screenAccessRole })
|
||||
}
|
||||
}
|
||||
|
||||
const confirmScreenBlank = async ({ screenUrl }) => {
|
||||
blankScreenUrl = screenUrl
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
<Layout>
|
||||
<Layout noPadding justifyItems="center">
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<Heading>Sign in to {company}</Heading>
|
||||
<Heading textAlign="center">Sign in to {company}</Heading>
|
||||
</Layout>
|
||||
{#if loaded}
|
||||
<GoogleButton />
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"messagePassing": true,
|
||||
"rowSelection": true,
|
||||
"continueIfAction": true,
|
||||
"showNotificationAction": true
|
||||
"showNotificationAction": true,
|
||||
"sidePanel": true
|
||||
},
|
||||
"layout": {
|
||||
"name": "Layout",
|
||||
|
@ -3669,7 +3670,7 @@
|
|||
"Ascending",
|
||||
"Descending"
|
||||
],
|
||||
"defaultValue": "Descending"
|
||||
"defaultValue": "Ascending"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
|
@ -3736,12 +3737,6 @@
|
|||
"key": "dataProvider",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Row count",
|
||||
"key": "rowCount",
|
||||
"defaultValue": 8
|
||||
},
|
||||
{
|
||||
"type": "columns",
|
||||
"label": "Columns",
|
||||
|
@ -3765,6 +3760,12 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Row count",
|
||||
"key": "rowCount",
|
||||
"defaultValue": 8
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Quiet",
|
||||
|
@ -3775,12 +3776,6 @@
|
|||
"label": "Compact",
|
||||
"key": "compact"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show auto columns",
|
||||
"key": "showAutoColumns",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Allow row selection",
|
||||
|
@ -3788,30 +3783,20 @@
|
|||
"defaultValue": false,
|
||||
"info": "Row selection is only compatible with internal or SQL tables"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Link table rows",
|
||||
"key": "linkRows"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Open link screens in modal",
|
||||
"key": "linkPeek"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Link screen",
|
||||
"key": "linkURL"
|
||||
},
|
||||
|
||||
{
|
||||
"section": true,
|
||||
"name": "Advanced",
|
||||
"name": "On Row Click",
|
||||
"settings": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "ID column for linking (appended to URL)",
|
||||
"key": "linkColumn",
|
||||
"placeholder": "Default"
|
||||
"type": "event",
|
||||
"key": "onClick",
|
||||
"context": [
|
||||
{
|
||||
"label": "Clicked row",
|
||||
"key": "row"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -4463,6 +4448,10 @@
|
|||
"label": "Title",
|
||||
"key": "title"
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Table",
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataSource",
|
||||
"label": "Data",
|
||||
|
@ -4470,20 +4459,16 @@
|
|||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "searchfield",
|
||||
"label": "Search Columns",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search columns",
|
||||
"info": "Only the first 5 search columns will be used"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter"
|
||||
"type": "columns",
|
||||
"label": "Table Columns",
|
||||
"key": "tableColumns",
|
||||
"dependsOn": "dataSource",
|
||||
"placeholder": "All columns",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort Column",
|
||||
"label": "Sort By",
|
||||
"key": "sortColumn"
|
||||
},
|
||||
{
|
||||
|
@ -4494,7 +4479,7 @@
|
|||
"Ascending",
|
||||
"Descending"
|
||||
],
|
||||
"defaultValue": "Descending"
|
||||
"defaultValue": "Ascending"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -4512,16 +4497,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Paginate",
|
||||
"key": "paginate",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Table",
|
||||
"settings": [
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Scroll Limit",
|
||||
|
@ -4529,16 +4504,14 @@
|
|||
"defaultValue": 8
|
||||
},
|
||||
{
|
||||
"type": "columns",
|
||||
"label": "Table Columns",
|
||||
"key": "tableColumns",
|
||||
"dependsOn": "dataSource",
|
||||
"placeholder": "All columns",
|
||||
"nested": true
|
||||
"type": "boolean",
|
||||
"label": "Paginate",
|
||||
"key": "paginate",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Quiet table variant",
|
||||
"label": "Quiet",
|
||||
"key": "quiet"
|
||||
},
|
||||
{
|
||||
|
@ -4546,11 +4519,6 @@
|
|||
"label": "Compact",
|
||||
"key": "compact"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show auto columns",
|
||||
"key": "showAutoColumns"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Allow row selection",
|
||||
|
@ -4558,58 +4526,100 @@
|
|||
"info": "Row selection is only compatible with internal or SQL tables"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Link table rows",
|
||||
"key": "linkRows"
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Open link in modal",
|
||||
"key": "linkPeek"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Link screen",
|
||||
"key": "linkURL"
|
||||
"type": "searchfield",
|
||||
"label": "Search Fields",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search fields",
|
||||
"info": "Only the first 5 search fields will be used"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Title button",
|
||||
"name": "On row click",
|
||||
"settings": [
|
||||
{
|
||||
"type": "radio",
|
||||
"key": "clickBehaviour",
|
||||
"sendEvents": true,
|
||||
"defaultValue": "actions",
|
||||
"info": "Details side panel is only compatible with internal or SQL tables",
|
||||
"options": [
|
||||
{
|
||||
"label": "Run actions",
|
||||
"value": "actions"
|
||||
},
|
||||
{
|
||||
"label": "Show details side panel",
|
||||
"value": "details"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"key": "onClick",
|
||||
"nested": true,
|
||||
"dependsOn": {
|
||||
"setting": "clickBehaviour",
|
||||
"value": "actions"
|
||||
},
|
||||
"context": [
|
||||
{
|
||||
"label": "Clicked row",
|
||||
"key": "row"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Button",
|
||||
"settings": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "showTitleButton",
|
||||
"label": "Show link button",
|
||||
"label": "Show button above table",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Open link in modal",
|
||||
"key": "titleButtonPeek"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "titleButtonText",
|
||||
"label": "Button text"
|
||||
"label": "Text",
|
||||
"defaultValue": "Create row",
|
||||
"dependsOn": "showTitleButton"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Button link",
|
||||
"key": "titleButtonURL"
|
||||
"type": "radio",
|
||||
"key": "titleButtonClickBehaviour",
|
||||
"label": "On Click",
|
||||
"dependsOn": "showTitleButton",
|
||||
"defaultValue": "actions",
|
||||
"info": "New row side panel is only compatible with internal or SQL tables",
|
||||
"options": [
|
||||
{
|
||||
"label": "Run actions",
|
||||
"value": "actions"
|
||||
},
|
||||
{
|
||||
"label": "Show new row side panel",
|
||||
"value": "new"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Advanced",
|
||||
"settings": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "ID column for linking (appended to URL)",
|
||||
"key": "linkColumn",
|
||||
"placeholder": "Default"
|
||||
"type": "event",
|
||||
"key": "onClickTitleButton",
|
||||
"nested": true,
|
||||
"dependsOn": {
|
||||
"setting": "titleButtonClickBehaviour",
|
||||
"value": "actions"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -5189,6 +5199,17 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"sidepanel": {
|
||||
"name": "Side Panel",
|
||||
"icon": "RailRight",
|
||||
"hasChildren": true,
|
||||
"illegalChildren": [
|
||||
"section"
|
||||
],
|
||||
"showEmptyState": false,
|
||||
"draggable": false,
|
||||
"info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action."
|
||||
},
|
||||
"rowexplorer": {
|
||||
"block": true,
|
||||
"name": "Row Explorer Block",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let props
|
||||
export let styles
|
||||
export let context
|
||||
export let name
|
||||
export let order = 0
|
||||
export let containsSlot = false
|
||||
|
||||
|
@ -26,7 +27,7 @@
|
|||
_blockElementHasChildren: $$slots?.default ?? false,
|
||||
_component: `@budibase/standard-components/${type}`,
|
||||
_id: id,
|
||||
_instanceName: type[0].toUpperCase() + type.slice(1),
|
||||
_instanceName: name || type[0].toUpperCase() + type.slice(1),
|
||||
_styles: {
|
||||
...styles,
|
||||
normal: styles?.normal || {},
|
||||
|
|
|
@ -190,6 +190,7 @@
|
|||
},
|
||||
empty: emptyState,
|
||||
selected,
|
||||
inSelectedPath,
|
||||
name,
|
||||
editing,
|
||||
type: instance._component,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { Heading, Icon } from "@budibase/bbui"
|
||||
import { Heading, Icon, clickOutside } from "@budibase/bbui"
|
||||
import { FieldTypes } from "constants"
|
||||
import active from "svelte-spa-router/active"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
@ -16,6 +16,7 @@
|
|||
builderStore,
|
||||
currentRole,
|
||||
environmentStore,
|
||||
sidePanelStore,
|
||||
} = sdk
|
||||
const component = getContext("component")
|
||||
const context = getContext("context")
|
||||
|
@ -71,6 +72,7 @@
|
|||
$context.device.width,
|
||||
$context.device.height
|
||||
)
|
||||
$: autoCloseSidePanel = !$builderStore.inBuilder && $sidePanelStore.open
|
||||
|
||||
// Scroll navigation into view if selected
|
||||
$: {
|
||||
|
@ -150,6 +152,7 @@
|
|||
class:desktop={!mobile}
|
||||
class:mobile={!!mobile}
|
||||
>
|
||||
<div class="layout-body">
|
||||
{#if typeClass !== "none"}
|
||||
<div
|
||||
class="interactive component navigation"
|
||||
|
@ -245,18 +248,45 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="side-panel-container"
|
||||
class:open={$sidePanelStore.open}
|
||||
use:clickOutside={autoCloseSidePanel ? sidePanelStore.actions.close : null}
|
||||
class:builder={$builderStore.inBuilder}
|
||||
>
|
||||
<div class="side-panel-header">
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
name="RailRightClose"
|
||||
hoverable
|
||||
on:click={sidePanelStore.actions.close}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Main components */
|
||||
.layout {
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
z-index: 1;
|
||||
border-top: 1px solid var(--spectrum-global-color-gray-300);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.component {
|
||||
display: contents;
|
||||
}
|
||||
.layout {
|
||||
.layout-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
|
@ -316,6 +346,43 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
#side-panel-container {
|
||||
max-width: calc(100vw - 40px);
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
z-index: 3;
|
||||
padding: var(--spacing-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
transition: transform 130ms ease-out;
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
right: 0;
|
||||
transform: translateX(100%);
|
||||
height: 100%;
|
||||
}
|
||||
#side-panel-container.builder {
|
||||
transform: translateX(0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
#side-panel-container.open {
|
||||
transform: translateX(0);
|
||||
box-shadow: 0 0 40px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#side-panel-container.builder.open {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
.side-panel-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, sidePanelStore, builderStore, dndIsDragging } =
|
||||
getContext("sdk")
|
||||
|
||||
// Automatically show and hide the side panel when inside the builder.
|
||||
// For some unknown reason, svelte reactivity breaks if we reference the
|
||||
// reactive variable "open" inside the following expression, or if we define
|
||||
// "open" above this expression.
|
||||
$: {
|
||||
if ($builderStore.inBuilder) {
|
||||
if (
|
||||
$component.inSelectedPath &&
|
||||
$sidePanelStore.contentId !== $component.id
|
||||
) {
|
||||
sidePanelStore.actions.open($component.id)
|
||||
} else if (
|
||||
!$component.inSelectedPath &&
|
||||
$sidePanelStore.contentId === $component.id &&
|
||||
!$dndIsDragging
|
||||
) {
|
||||
sidePanelStore.actions.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derive visibility
|
||||
$: open = $sidePanelStore.contentId === $component.id
|
||||
|
||||
const showInSidePanel = (el, visible) => {
|
||||
const update = visible => {
|
||||
const target = document.getElementById("side-panel-container")
|
||||
const node = el
|
||||
if (visible) {
|
||||
if (!target.contains(node)) {
|
||||
target.appendChild(node)
|
||||
}
|
||||
} else {
|
||||
if (target.contains(node)) {
|
||||
target.removeChild(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply initial visibility
|
||||
update(visible)
|
||||
|
||||
return { update }
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
use:styleable={$component.styles}
|
||||
use:showInSidePanel={open}
|
||||
class="side-panel"
|
||||
class:open
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.side-panel {
|
||||
flex: 1 1 auto;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.side-panel.open {
|
||||
display: flex;
|
||||
}
|
||||
.side-panel :global(.component > *) {
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
@ -13,50 +14,103 @@
|
|||
export let sortOrder
|
||||
export let paginate
|
||||
export let tableColumns
|
||||
export let showAutoColumns
|
||||
export let rowCount
|
||||
export let quiet
|
||||
export let compact
|
||||
export let size
|
||||
export let allowSelectRows
|
||||
export let linkRows
|
||||
export let linkURL
|
||||
export let linkColumn
|
||||
export let linkPeek
|
||||
export let clickBehaviour
|
||||
export let onClick
|
||||
export let showTitleButton
|
||||
export let titleButtonText
|
||||
export let titleButtonURL
|
||||
export let titleButtonPeek
|
||||
export let titleButtonClickBehaviour
|
||||
export let onClickTitleButton
|
||||
|
||||
const { fetchDatasourceSchema } = getContext("sdk")
|
||||
const { fetchDatasourceSchema, API } = getContext("sdk")
|
||||
const stateKey = `ID_${generate()}`
|
||||
|
||||
let formId
|
||||
let dataProviderId
|
||||
let detailsFormBlockId
|
||||
let detailsSidePanelId
|
||||
let newRowSidePanelId
|
||||
let schema
|
||||
let primaryDisplay
|
||||
let schemaLoaded = false
|
||||
|
||||
$: fetchSchema(dataSource)
|
||||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
||||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
||||
$: titleButtonAction = [
|
||||
$: editTitle = getEditTitle(detailsFormBlockId, primaryDisplay)
|
||||
$: normalFields = getNormalFields(schema)
|
||||
$: rowClickActions =
|
||||
clickBehaviour === "actions" || dataSource?.type !== "table"
|
||||
? onClick
|
||||
: [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
id: 0,
|
||||
"##eventHandlerType": "Update State",
|
||||
parameters: {
|
||||
peek: titleButtonPeek,
|
||||
url: titleButtonURL,
|
||||
key: stateKey,
|
||||
type: "set",
|
||||
persist: false,
|
||||
value: `{{ ${safe("eventContext")}.${safe("row")}._id }}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
"##eventHandlerType": "Open Side Panel",
|
||||
parameters: {
|
||||
id: detailsSidePanelId,
|
||||
},
|
||||
},
|
||||
]
|
||||
$: buttonClickActions =
|
||||
clickBehaviour === "actions" || dataSource?.type !== "table"
|
||||
? onClickTitleButton
|
||||
: [
|
||||
{
|
||||
id: 0,
|
||||
"##eventHandlerType": "Open Side Panel",
|
||||
parameters: {
|
||||
id: newRowSidePanelId,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Load the datasource schema so we can determine column types
|
||||
const fetchSchema = async dataSource => {
|
||||
if (dataSource) {
|
||||
if (dataSource?.type === "table") {
|
||||
const definition = await API.fetchTableDefinition(dataSource?.tableId)
|
||||
schema = definition.schema
|
||||
primaryDisplay = definition.primaryDisplay
|
||||
} else if (dataSource) {
|
||||
schema = await fetchDatasourceSchema(dataSource, {
|
||||
enrichRelationships: true,
|
||||
})
|
||||
}
|
||||
schemaLoaded = true
|
||||
}
|
||||
|
||||
const getNormalFields = schema => {
|
||||
if (!schema) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(schema)
|
||||
.filter(entry => {
|
||||
return !entry[1].autocolumn
|
||||
})
|
||||
.map(entry => entry[0])
|
||||
}
|
||||
|
||||
const getEditTitle = (detailsFormBlockId, primaryDisplay) => {
|
||||
if (!primaryDisplay || !detailsFormBlockId) {
|
||||
return "Edit"
|
||||
}
|
||||
const prefix = safe(detailsFormBlockId + "-repeater")
|
||||
const binding = `${prefix}.${safe(primaryDisplay)}`
|
||||
return `{{#if ${binding}}}{{${binding}}}{{else}}Details{{/if}}`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if schemaLoaded}
|
||||
|
@ -129,7 +183,7 @@
|
|||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
onClick: titleButtonAction,
|
||||
onClick: buttonClickActions,
|
||||
text: titleButtonText,
|
||||
type: "cta",
|
||||
}}
|
||||
|
@ -145,7 +199,7 @@
|
|||
props={{
|
||||
dataSource,
|
||||
filter: enrichedFilter,
|
||||
sortColumn,
|
||||
sortColumn: sortColumn || primaryDisplay,
|
||||
sortOrder,
|
||||
paginate,
|
||||
limit: rowCount,
|
||||
|
@ -158,19 +212,63 @@
|
|||
props={{
|
||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||
columns: tableColumns,
|
||||
showAutoColumns,
|
||||
rowCount,
|
||||
quiet,
|
||||
compact,
|
||||
allowSelectRows,
|
||||
size,
|
||||
linkRows,
|
||||
linkURL,
|
||||
linkColumn,
|
||||
linkPeek,
|
||||
onClick: rowClickActions,
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
{#if clickBehaviour === "details"}
|
||||
<BlockComponent
|
||||
name="Details side panel"
|
||||
type="sidepanel"
|
||||
bind:id={detailsSidePanelId}
|
||||
context="details-side-panel"
|
||||
order={2}
|
||||
>
|
||||
<BlockComponent
|
||||
name="Details form block"
|
||||
type="formblock"
|
||||
bind:id={detailsFormBlockId}
|
||||
props={{
|
||||
dataSource,
|
||||
showSaveButton: true,
|
||||
showDeleteButton: true,
|
||||
actionType: "Update",
|
||||
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
||||
fields: normalFields,
|
||||
title: editTitle,
|
||||
labelPosition: "left",
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
{#if showTitleButton && titleButtonClickBehaviour === "new"}
|
||||
<BlockComponent
|
||||
name="New row side panel"
|
||||
type="sidepanel"
|
||||
bind:id={newRowSidePanelId}
|
||||
context="new-side-panel"
|
||||
order={3}
|
||||
>
|
||||
<BlockComponent
|
||||
name="New row form block"
|
||||
type="formblock"
|
||||
props={{
|
||||
dataSource,
|
||||
showSaveButton: true,
|
||||
showDeleteButton: false,
|
||||
actionType: "Create",
|
||||
fields: normalFields,
|
||||
title: "Create Row",
|
||||
labelPosition: "left",
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
</Block>
|
||||
{/if}
|
||||
|
|
|
@ -43,6 +43,20 @@
|
|||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
// Clear a create form once submitted
|
||||
...(actionType !== "Create"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
"##eventHandlerType": "Clear Form",
|
||||
parameters: {
|
||||
componentId: formId,
|
||||
},
|
||||
},
|
||||
]),
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
|
@ -63,6 +77,9 @@
|
|||
{
|
||||
"##eventHandlerType": "Close Screen Modal",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Close Side Panel",
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
|
||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||
$: resetKey = Helpers.hashString(
|
||||
JSON.stringify(initialValues) + JSON.stringify(schema) + disabled
|
||||
JSON.stringify(initialValues) + JSON.stringify(dataSource) + disabled
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
@ -35,10 +35,10 @@ export { default as tag } from "./Tag.svelte"
|
|||
export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
||||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||
export { default as grid } from "./Grid.svelte"
|
||||
export { default as sidepanel } from "./SidePanel.svelte"
|
||||
export * from "./charts"
|
||||
export * from "./forms"
|
||||
export * from "./table"
|
||||
|
||||
export * from "./blocks"
|
||||
export * from "./dynamic-filter"
|
||||
|
||||
|
|
|
@ -7,20 +7,16 @@
|
|||
|
||||
export let dataProvider
|
||||
export let columns
|
||||
export let showAutoColumns
|
||||
export let rowCount
|
||||
export let quiet
|
||||
export let size
|
||||
export let linkRows
|
||||
export let linkURL
|
||||
export let linkColumn
|
||||
export let linkPeek
|
||||
export let allowSelectRows
|
||||
export let compact
|
||||
export let onClick
|
||||
|
||||
const loading = getContext("loading")
|
||||
const component = getContext("component")
|
||||
const { styleable, getAction, ActionTypes, routeStore, rowSelectionStore } =
|
||||
const { styleable, getAction, ActionTypes, rowSelectionStore } =
|
||||
getContext("sdk")
|
||||
const customColumnKey = `custom-${Math.random()}`
|
||||
const customRenderers = [
|
||||
|
@ -29,18 +25,19 @@
|
|||
component: SlotRenderer,
|
||||
},
|
||||
]
|
||||
|
||||
let selectedRows = []
|
||||
|
||||
$: hasChildren = $component.children
|
||||
$: data = dataProvider?.rows || []
|
||||
$: fullSchema = dataProvider?.schema ?? {}
|
||||
$: fields = getFields(fullSchema, columns, showAutoColumns)
|
||||
$: fields = getFields(fullSchema, columns, false)
|
||||
$: schema = getFilteredSchema(fullSchema, fields, hasChildren)
|
||||
$: setSorting = getAction(
|
||||
dataProvider?.id,
|
||||
ActionTypes.SetDataProviderSorting
|
||||
)
|
||||
$: table = dataProvider?.datasource?.type === "table"
|
||||
|
||||
$: {
|
||||
rowSelectionStore.actions.updateSelection(
|
||||
$component.id,
|
||||
|
@ -118,17 +115,10 @@
|
|||
})
|
||||
}
|
||||
|
||||
const onClick = e => {
|
||||
if (!linkRows || !linkURL) {
|
||||
return
|
||||
const handleClick = e => {
|
||||
if (onClick) {
|
||||
onClick({ row: e.detail })
|
||||
}
|
||||
const col = linkColumn || "_id"
|
||||
const id = e.detail?.[col]
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
const split = linkURL.split("/:")
|
||||
routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
|
@ -153,7 +143,7 @@
|
|||
disableSorting
|
||||
autoSortColumns={!columns?.length}
|
||||
on:sort={onSort}
|
||||
on:click={onClick}
|
||||
on:click={handleClick}
|
||||
>
|
||||
<div class="skeleton" slot="loadingIndicator">
|
||||
<Skeleton />
|
||||
|
|
|
@ -36,8 +36,7 @@
|
|||
|
||||
// Util to get the inner DOM node by a component ID
|
||||
const getDOMNode = id => {
|
||||
const component = document.getElementsByClassName(id)[0]
|
||||
return [...component.children][0]
|
||||
return document.getElementsByClassName(`${id}-dom`)[0]
|
||||
}
|
||||
|
||||
// Util to calculate the variance of a set of data
|
||||
|
|
|
@ -43,7 +43,8 @@
|
|||
if (callbackCount >= observers.length) {
|
||||
return
|
||||
}
|
||||
nextIndicators[idx].visible = entries[0].isIntersecting
|
||||
nextIndicators[idx].visible =
|
||||
nextIndicators[idx].isSidePanel || entries[0].isIntersecting
|
||||
if (++callbackCount === observers.length) {
|
||||
indicators = nextIndicators
|
||||
updating = false
|
||||
|
@ -91,8 +92,9 @@
|
|||
|
||||
// Extract valid children
|
||||
// Sanity limit of 100 active indicators
|
||||
const children = Array.from(parents)
|
||||
.map(parent => parent?.children?.[0])
|
||||
const children = Array.from(
|
||||
document.getElementsByClassName(`${componentId}-dom`)
|
||||
)
|
||||
.filter(x => x != null)
|
||||
.slice(0, 100)
|
||||
|
||||
|
@ -121,6 +123,7 @@
|
|||
width: elBounds.width + 4,
|
||||
height: elBounds.height + 4,
|
||||
visible: false,
|
||||
isSidePanel: child.classList.contains("side-panel"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
componentStore,
|
||||
currentRole,
|
||||
environmentStore,
|
||||
sidePanelStore,
|
||||
dndIsDragging,
|
||||
} from "stores"
|
||||
import { styleable } from "utils/styleable"
|
||||
import { linkable } from "utils/linkable"
|
||||
|
@ -30,6 +32,8 @@ export default {
|
|||
uploadStore,
|
||||
componentStore,
|
||||
environmentStore,
|
||||
sidePanelStore,
|
||||
dndIsDragging,
|
||||
currentRole,
|
||||
styleable,
|
||||
linkable,
|
||||
|
|
|
@ -24,6 +24,7 @@ export {
|
|||
dndIsNewComponent,
|
||||
dndIsDragging,
|
||||
} from "./dnd"
|
||||
export { sidePanelStore } from "./sidePanel.js"
|
||||
|
||||
// Context stores are layered and duplicated, so it is not a singleton
|
||||
export { createContextStore } from "./context"
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
|
||||
export const createSidePanelStore = () => {
|
||||
const initialState = {
|
||||
contentId: null,
|
||||
}
|
||||
const store = writable(initialState)
|
||||
const derivedStore = derived(store, $store => {
|
||||
return {
|
||||
...$store,
|
||||
open: $store.contentId != null,
|
||||
}
|
||||
})
|
||||
|
||||
const open = id => {
|
||||
store.update(state => {
|
||||
state.contentId = id
|
||||
return state
|
||||
})
|
||||
}
|
||||
const close = () => {
|
||||
store.update(state => {
|
||||
state.contentId = null
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
actions: {
|
||||
open,
|
||||
close,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const sidePanelStore = createSidePanelStore()
|
|
@ -10,6 +10,7 @@ import {
|
|||
dataSourceStore,
|
||||
uploadStore,
|
||||
rowSelectionStore,
|
||||
sidePanelStore,
|
||||
} from "stores"
|
||||
import { API } from "api"
|
||||
import { ActionTypes } from "constants"
|
||||
|
@ -312,6 +313,17 @@ const showNotificationHandler = action => {
|
|||
notificationStore.actions[type]?.(message, autoDismiss)
|
||||
}
|
||||
|
||||
const OpenSidePanelHandler = action => {
|
||||
const { id } = action.parameters
|
||||
if (id) {
|
||||
sidePanelStore.actions.open(id)
|
||||
}
|
||||
}
|
||||
|
||||
const CloseSidePanelHandler = () => {
|
||||
sidePanelStore.actions.close()
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
["Save Row"]: saveRowHandler,
|
||||
["Duplicate Row"]: duplicateRowHandler,
|
||||
|
@ -331,6 +343,8 @@ const handlerMap = {
|
|||
["Export Data"]: exportDataHandler,
|
||||
["Continue if / Stop if"]: continueIfHandler,
|
||||
["Show Notification"]: showNotificationHandler,
|
||||
["Open Side Panel"]: OpenSidePanelHandler,
|
||||
["Close Side Panel"]: CloseSidePanelHandler,
|
||||
}
|
||||
|
||||
const confirmTextMap = {
|
||||
|
|
|
@ -25,6 +25,8 @@ export const styleable = (node, styles = {}) => {
|
|||
|
||||
// Creates event listeners and applies initial styles
|
||||
const setupStyles = (newStyles = {}) => {
|
||||
node.classList.add(`${newStyles.id}-dom`)
|
||||
|
||||
let baseStyles = {}
|
||||
if (newStyles.empty) {
|
||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)"
|
||||
|
|
|
@ -117,7 +117,7 @@ export default class DataFetch {
|
|||
* Fetches a fresh set of data from the server, resetting pagination
|
||||
*/
|
||||
async getInitialData() {
|
||||
const { datasource, filter, sortColumn, paginate } = this.options
|
||||
const { datasource, filter, paginate } = this.options
|
||||
|
||||
// Fetch datasource definition and determine feature flags
|
||||
const definition = await this.getDefinition(datasource)
|
||||
|
@ -135,6 +135,17 @@ export default class DataFetch {
|
|||
return
|
||||
}
|
||||
|
||||
// If no sort order, default to descending
|
||||
if (!this.options.sortOrder) {
|
||||
this.options.sortOrder = "ascending"
|
||||
}
|
||||
|
||||
// If no sort column, use the first field in the schema
|
||||
if (!this.options.sortColumn) {
|
||||
this.options.sortColumn = Object.keys(schema)[0]
|
||||
}
|
||||
const { sortColumn } = this.options
|
||||
|
||||
// Determine what sort type to use
|
||||
let sortType = "string"
|
||||
if (sortColumn) {
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
import { getScreenParams } from "../../db/utils"
|
||||
import { Screen } from "@budibase/types"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
/**
|
||||
* Date:
|
||||
* November 2022
|
||||
*
|
||||
* Description:
|
||||
* Update table settings to use actions instead of links. We do not remove the
|
||||
* legacy values here as we cannot guarantee that their apps are up-t-date.
|
||||
* It is safe to simply save both the new and old structure in the definition.
|
||||
*
|
||||
* Migration 1:
|
||||
* Legacy "linkRows", "linkURL", "linkPeek" and "linkColumn" settings on tables
|
||||
* and table blocks are migrated into a "Navigate To" action under the new
|
||||
* "onClick" setting.
|
||||
*
|
||||
* Migration 2:
|
||||
* Legacy "titleButtonURL" and "titleButtonPeek" settings on table blocks are
|
||||
* migrated into a "Navigate To" action under the new "onClickTitleButton"
|
||||
* setting.
|
||||
*/
|
||||
export const run = async (appDb: any) => {
|
||||
// Get all app screens
|
||||
let screens: Screen[]
|
||||
try {
|
||||
screens = (
|
||||
await appDb.allDocs(
|
||||
getScreenParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
).rows.map((row: any) => row.doc)
|
||||
} catch (e) {
|
||||
// sometimes the metadata document doesn't exist
|
||||
// exit early instead of failing the migration
|
||||
console.error("Error retrieving app metadata. Skipping", e)
|
||||
return
|
||||
}
|
||||
|
||||
// Recursively update any relevant components and mutate the screen docs
|
||||
for (let screen of screens) {
|
||||
const changed = migrateTableSettings(screen.props)
|
||||
|
||||
// Save screen if we updated it
|
||||
if (changed) {
|
||||
await appDb.put(screen)
|
||||
console.log(
|
||||
`Screen ${screen.routing?.route} contained table settings which were migrated`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively searches and mutates a screen doc to migrate table component
|
||||
// and table block settings
|
||||
const migrateTableSettings = (component: any) => {
|
||||
let changed = false
|
||||
if (!component) {
|
||||
return changed
|
||||
}
|
||||
|
||||
// Migration 1: migrate table row click settings
|
||||
if (
|
||||
component._component.endsWith("/table") ||
|
||||
component._component.endsWith("/tableblock")
|
||||
) {
|
||||
const { linkRows, linkURL, linkPeek, linkColumn, onClick } = component
|
||||
if (linkRows && !onClick) {
|
||||
const column = linkColumn || "_id"
|
||||
const action = convertLinkSettingToAction(linkURL, !!linkPeek, column)
|
||||
if (action) {
|
||||
changed = true
|
||||
component.onClick = action
|
||||
if (component._component.endsWith("/tableblock")) {
|
||||
component.clickBehaviour = "actions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 2: migrate table block title button settings
|
||||
if (component._component.endsWith("/tableblock")) {
|
||||
const {
|
||||
showTitleButton,
|
||||
titleButtonURL,
|
||||
titleButtonPeek,
|
||||
onClickTitleButton,
|
||||
} = component
|
||||
if (showTitleButton && !onClickTitleButton) {
|
||||
const action = convertLinkSettingToAction(
|
||||
titleButtonURL,
|
||||
!!titleButtonPeek
|
||||
)
|
||||
if (action) {
|
||||
changed = true
|
||||
component.onClickTitleButton = action
|
||||
component.titleButtonClickBehaviour = "actions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse down the tree as needed
|
||||
component._children?.forEach((child: any) => {
|
||||
const childChanged = migrateTableSettings(child)
|
||||
changed = changed || childChanged
|
||||
})
|
||||
return changed
|
||||
}
|
||||
|
||||
// Util ti convert the legacy settings into a navigation action structure
|
||||
const convertLinkSettingToAction = (
|
||||
linkURL: string,
|
||||
linkPeek: boolean,
|
||||
linkColumn?: string
|
||||
) => {
|
||||
// Sanity check we have a URL
|
||||
if (!linkURL) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Default URL to the old URL setting
|
||||
let url = linkURL
|
||||
|
||||
// If we enriched the old URL with a column, update the url
|
||||
if (linkColumn && linkURL.includes("/:")) {
|
||||
// Convert old link URL setting, which is a screen URL, into a valid
|
||||
// binding using the new clicked row binding
|
||||
const split = linkURL.split("/:")
|
||||
const col = linkColumn || "_id"
|
||||
const binding = `{{ ${safe("eventContext")}.${safe("row")}.${safe(col)} }}`
|
||||
url = `${split[0]}/${binding}`
|
||||
}
|
||||
|
||||
// Create action structure
|
||||
return [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url,
|
||||
peek: linkPeek,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
import { App, Screen } from "@budibase/types"
|
||||
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
import TestConfig from "../../../tests/utilities/TestConfiguration"
|
||||
import { run as runMigration } from "../tableSettings"
|
||||
|
||||
describe("run", () => {
|
||||
const config = new TestConfig(false)
|
||||
let app: App
|
||||
let screen: Screen
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
app = await config.createApp("testApp")
|
||||
screen = await config.createScreen()
|
||||
})
|
||||
|
||||
afterAll(config.end)
|
||||
|
||||
it("migrates table block row on click settings", async () => {
|
||||
// Add legacy table block as first child
|
||||
screen.props._children = [
|
||||
{
|
||||
_instanceName: "Table Block",
|
||||
_styles: {},
|
||||
_component: "@budibase/standard-components/tableblock",
|
||||
_id: "foo",
|
||||
linkRows: true,
|
||||
linkURL: "/rows/:id",
|
||||
linkPeek: true,
|
||||
linkColumn: "name",
|
||||
},
|
||||
]
|
||||
await config.createScreen(screen)
|
||||
|
||||
// Run migration
|
||||
screen = await dbCore.doWithDB(app.appId, async (db: any) => {
|
||||
await runMigration(db)
|
||||
return await db.get(screen._id)
|
||||
})
|
||||
|
||||
// Verify new "onClick" setting
|
||||
const onClick = screen.props._children?.[0].onClick
|
||||
expect(onClick).toBeDefined()
|
||||
expect(onClick.length).toBe(1)
|
||||
expect(onClick[0]["##eventHandlerType"]).toBe("Navigate To")
|
||||
expect(onClick[0].parameters.url).toBe(
|
||||
`/rows/{{ [eventContext].[row].[name] }}`
|
||||
)
|
||||
expect(onClick[0].parameters.peek).toBeTruthy()
|
||||
})
|
||||
|
||||
it("migrates table row on click settings", async () => {
|
||||
// Add legacy table block as first child
|
||||
screen.props._children = [
|
||||
{
|
||||
_instanceName: "Table",
|
||||
_styles: {},
|
||||
_component: "@budibase/standard-components/table",
|
||||
_id: "foo",
|
||||
linkRows: true,
|
||||
linkURL: "/rows/:id",
|
||||
linkPeek: true,
|
||||
linkColumn: "name",
|
||||
},
|
||||
]
|
||||
await config.createScreen(screen)
|
||||
|
||||
// Run migration
|
||||
screen = await dbCore.doWithDB(app.appId, async (db: any) => {
|
||||
await runMigration(db)
|
||||
return await db.get(screen._id)
|
||||
})
|
||||
|
||||
// Verify new "onClick" setting
|
||||
const onClick = screen.props._children?.[0].onClick
|
||||
expect(onClick).toBeDefined()
|
||||
expect(onClick.length).toBe(1)
|
||||
expect(onClick[0]["##eventHandlerType"]).toBe("Navigate To")
|
||||
expect(onClick[0].parameters.url).toBe(
|
||||
`/rows/{{ [eventContext].[row].[name] }}`
|
||||
)
|
||||
expect(onClick[0].parameters.peek).toBeTruthy()
|
||||
})
|
||||
|
||||
it("migrates table block title button settings", async () => {
|
||||
// Add legacy table block as first child
|
||||
screen.props._children = [
|
||||
{
|
||||
_instanceName: "Table Block",
|
||||
_styles: {},
|
||||
_component: "@budibase/standard-components/tableblock",
|
||||
_id: "foo",
|
||||
showTitleButton: true,
|
||||
titleButtonURL: "/url",
|
||||
titleButtonPeek: true,
|
||||
},
|
||||
]
|
||||
await config.createScreen(screen)
|
||||
|
||||
// Run migration
|
||||
screen = await dbCore.doWithDB(app.appId, async (db: any) => {
|
||||
await runMigration(db)
|
||||
return await db.get(screen._id)
|
||||
})
|
||||
|
||||
// Verify new "onClickTitleButton" setting
|
||||
const onClick = screen.props._children?.[0].onClickTitleButton
|
||||
expect(onClick).toBeDefined()
|
||||
expect(onClick.length).toBe(1)
|
||||
expect(onClick[0]["##eventHandlerType"]).toBe("Navigate To")
|
||||
expect(onClick[0].parameters.url).toBe("/url")
|
||||
expect(onClick[0].parameters.peek).toBeTruthy()
|
||||
})
|
||||
|
||||
it("ignores components that have already been migrated", async () => {
|
||||
// Add legacy table block as first child
|
||||
screen.props._children = [
|
||||
{
|
||||
_instanceName: "Table Block",
|
||||
_styles: {},
|
||||
_component: "@budibase/standard-components/tableblock",
|
||||
_id: "foo",
|
||||
linkRows: true,
|
||||
linkURL: "/rows/:id",
|
||||
linkPeek: true,
|
||||
linkColumn: "name",
|
||||
onClick: "foo",
|
||||
},
|
||||
]
|
||||
const initialDefinition = JSON.stringify(screen.props._children?.[0])
|
||||
await config.createScreen(screen)
|
||||
|
||||
// Run migration
|
||||
screen = await dbCore.doWithDB(app.appId, async (db: any) => {
|
||||
await runMigration(db)
|
||||
return await db.get(screen._id)
|
||||
})
|
||||
|
||||
// Verify new "onClick" setting
|
||||
const newDefinition = JSON.stringify(screen.props._children?.[0])
|
||||
expect(initialDefinition).toEqual(newDefinition)
|
||||
})
|
||||
})
|
|
@ -12,6 +12,7 @@ import env from "../environment"
|
|||
import * as userEmailViewCasing from "./functions/userEmailViewCasing"
|
||||
import * as syncQuotas from "./functions/syncQuotas"
|
||||
import * as appUrls from "./functions/appUrls"
|
||||
import * as tableSettings from "./functions/tableSettings"
|
||||
import * as backfill from "./functions/backfill"
|
||||
/**
|
||||
* Populate the migration function and additional configuration from
|
||||
|
@ -73,6 +74,14 @@ export const buildMigrations = () => {
|
|||
})
|
||||
break
|
||||
}
|
||||
case MigrationName.TABLE_SETTINGS_LINKS_TO_ACTIONS: {
|
||||
serverMigrations.push({
|
||||
...definition,
|
||||
appOpts: { dev: true },
|
||||
fn: tableSettings.run,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { Document } from "../document"
|
||||
|
||||
export interface Component extends Document {
|
||||
_instanceName: string
|
||||
_styles: { [key: string]: any }
|
||||
_component: string
|
||||
_children?: Component[]
|
||||
[key: string]: any
|
||||
}
|
|
@ -13,3 +13,4 @@ export * from "./user"
|
|||
export * from "./backup"
|
||||
export * from "./webhook"
|
||||
export * from "./links"
|
||||
export * from "./component"
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { Document } from "../document"
|
||||
import { Component } from "./component"
|
||||
|
||||
export interface ScreenProps extends Document {
|
||||
_instanceName: string
|
||||
_styles: { [key: string]: any }
|
||||
_component: string
|
||||
_children: ScreenProps[]
|
||||
export interface ScreenProps extends Component {
|
||||
size?: string
|
||||
gap?: string
|
||||
direction?: string
|
||||
|
|
|
@ -44,6 +44,7 @@ export enum MigrationName {
|
|||
EVENT_GLOBAL_BACKFILL = "event_global_backfill",
|
||||
EVENT_INSTALLATION_BACKFILL = "event_installation_backfill",
|
||||
GLOBAL_INFO_SYNC_USERS = "global_info_sync_users",
|
||||
TABLE_SETTINGS_LINKS_TO_ACTIONS = "table_settings_links_to_actions",
|
||||
// increment this number to re-activate this migration
|
||||
SYNC_QUOTAS = "sync_quotas_1",
|
||||
}
|
||||
|
|
100
yarn.lock
100
yarn.lock
|
@ -1011,45 +1011,45 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
||||
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
||||
|
||||
"@typescript-eslint/parser@4.28.0":
|
||||
version "4.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.28.0.tgz#2404c16751a28616ef3abab77c8e51d680a12caa"
|
||||
integrity sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==
|
||||
"@typescript-eslint/parser@5.45.0":
|
||||
version "5.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.45.0.tgz#b18a5f6b3cf1c2b3e399e9d2df4be40d6b0ddd0e"
|
||||
integrity sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "4.28.0"
|
||||
"@typescript-eslint/types" "4.28.0"
|
||||
"@typescript-eslint/typescript-estree" "4.28.0"
|
||||
debug "^4.3.1"
|
||||
"@typescript-eslint/scope-manager" "5.45.0"
|
||||
"@typescript-eslint/types" "5.45.0"
|
||||
"@typescript-eslint/typescript-estree" "5.45.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@4.28.0":
|
||||
version "4.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz#6a3009d2ab64a30fc8a1e257a1a320067f36a0ce"
|
||||
integrity sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==
|
||||
"@typescript-eslint/scope-manager@5.45.0":
|
||||
version "5.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96"
|
||||
integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.0"
|
||||
"@typescript-eslint/visitor-keys" "4.28.0"
|
||||
|
||||
"@typescript-eslint/types@4.28.0":
|
||||
version "4.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.0.tgz#a33504e1ce7ac51fc39035f5fe6f15079d4dafb0"
|
||||
integrity sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==
|
||||
"@typescript-eslint/types" "5.45.0"
|
||||
"@typescript-eslint/visitor-keys" "5.45.0"
|
||||
|
||||
"@typescript-eslint/types@4.33.0":
|
||||
version "4.33.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72"
|
||||
integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@4.28.0":
|
||||
version "4.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz#e66d4e5aa2ede66fec8af434898fe61af10c71cf"
|
||||
integrity sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==
|
||||
"@typescript-eslint/types@5.45.0":
|
||||
version "5.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5"
|
||||
integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.45.0":
|
||||
version "5.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d"
|
||||
integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.0"
|
||||
"@typescript-eslint/visitor-keys" "4.28.0"
|
||||
debug "^4.3.1"
|
||||
globby "^11.0.3"
|
||||
is-glob "^4.0.1"
|
||||
semver "^7.3.5"
|
||||
"@typescript-eslint/types" "5.45.0"
|
||||
"@typescript-eslint/visitor-keys" "5.45.0"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@^4.33.0":
|
||||
|
@ -1065,14 +1065,6 @@
|
|||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.28.0":
|
||||
version "4.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz#255c67c966ec294104169a6939d96f91c8a89434"
|
||||
integrity sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "4.28.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@4.33.0":
|
||||
version "4.33.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd"
|
||||
|
@ -1081,6 +1073,14 @@
|
|||
"@typescript-eslint/types" "4.33.0"
|
||||
eslint-visitor-keys "^2.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.45.0":
|
||||
version "5.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528"
|
||||
integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.45.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
JSONStream@^1.0.4, JSONStream@^1.3.4:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
|
||||
|
@ -2015,7 +2015,7 @@ debug@^3.1.0:
|
|||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.0.0, debug@^4.3.1, debug@^4.3.3:
|
||||
debug@^4.0.0, debug@^4.3.1, debug@^4.3.3, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
|
@ -2446,6 +2446,11 @@ eslint-visitor-keys@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
|
||||
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
|
||||
|
||||
eslint-visitor-keys@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
|
||||
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
|
||||
|
||||
eslint@^7.28.0:
|
||||
version "7.32.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
|
||||
|
@ -3081,7 +3086,7 @@ globals@^13.6.0, globals@^13.9.0:
|
|||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
globby@^11.0.3:
|
||||
globby@^11.0.3, globby@^11.1.0:
|
||||
version "11.1.0"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
|
||||
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
|
||||
|
@ -3606,7 +3611,7 @@ is-glob@^3.1.0:
|
|||
dependencies:
|
||||
is-extglob "^2.1.0"
|
||||
|
||||
is-glob@^4.0.0, is-glob@^4.0.1:
|
||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
|
||||
|
@ -5843,6 +5848,13 @@ semver@^7.3.4:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@^7.3.7:
|
||||
version "7.3.8"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
|
||||
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
@ -6559,10 +6571,10 @@ typedarray@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||
|
||||
typescript@4.5.5:
|
||||
version "4.5.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
|
||||
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
|
||||
typescript@4.7.3:
|
||||
version "4.7.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
|
||||
integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
|
||||
|
||||
typescript@^3.9.10, typescript@^3.9.5, typescript@^3.9.7:
|
||||
version "3.9.10"
|
||||
|
|
Loading…
Reference in New Issue