Merge branch 'master' of github.com:Budibase/budibase into screen-updates

This commit is contained in:
Andrew Kingston 2020-10-14 17:09:50 +01:00
commit 3de238c66a
61 changed files with 269 additions and 252 deletions

View File

@ -10,7 +10,7 @@
"eslint-plugin-svelte3": "^2.7.3", "eslint-plugin-svelte3": "^2.7.3",
"lerna": "3.14.1", "lerna": "3.14.1",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"prettier-plugin-svelte": "^0.7.0", "prettier-plugin-svelte": "^1.4.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0", "rollup-plugin-replace": "^2.2.0",
"svelte": "^3.28.0" "svelte": "^3.28.0"

View File

@ -27,7 +27,6 @@ context("Create a Table", () => {
cy.get(".actions input") cy.get(".actions input")
.first() .first()
.type("updated") .type("updated")
cy.get("select").select("Text")
cy.contains("Save Column").click() cy.contains("Save Column").click()
cy.contains("nameupdated").should("have.text", "nameupdated") cy.contains("nameupdated").should("have.text", "nameupdated")
}) })

View File

@ -53,6 +53,7 @@ export const getStore = () => {
store.saveScreen = saveScreen(store) store.saveScreen = saveScreen(store)
store.setCurrentScreen = setCurrentScreen(store) store.setCurrentScreen = setCurrentScreen(store)
store.deleteScreens = deleteScreens(store)
store.setCurrentPage = setCurrentPage(store) store.setCurrentPage = setCurrentPage(store)
store.createScreen = createScreen(store) store.createScreen = createScreen(store)
store.addStylesheet = addStylesheet(store) store.addStylesheet = addStylesheet(store)
@ -185,6 +186,26 @@ const setCurrentScreen = store => screenName => {
}) })
} }
const deleteScreens = store => (screens, pageName = null) => {
if (!(screens instanceof Array)) {
screens = [screens]
}
store.update(state => {
if (pageName == null) {
pageName = state.pages.main.name
}
for (let screen of screens) {
state.screens = state.screens.filter(c => c.name !== screen.name)
// Remove screen from current page as well
state.pages[pageName]._screens = state.pages[pageName]._screens.filter(
scr => scr.name !== screen.name
)
api.delete(`/_builder/api/pages/${pageName}/screens/${screen.name}`)
}
return state
})
}
const savePage = store => async page => { const savePage = store => async page => {
store.update(state => { store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) { if (state.currentFrontEndType !== "page" || !state.currentPageName) {

View File

@ -23,9 +23,7 @@
class="automation-block hoverable" class="automation-block hoverable"
on:click={addBlockToAutomation} on:click={addBlockToAutomation}
data-cy={stepId}> data-cy={stepId}>
<div> <div><i class={blockDefinition.icon} /></div>
<i class={blockDefinition.icon} />
</div>
<div class="automation-text"> <div class="automation-text">
<h4>{blockDefinition.name}</h4> <h4>{blockDefinition.name}</h4>
<p>{blockDefinition.description}</p> <p>{blockDefinition.description}</p>

View File

@ -62,10 +62,7 @@
{#if $automationStore.selectedBlock} {#if $automationStore.selectedBlock}
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} /> <AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
{:else if $automationStore.selectedAutomation} {:else if $automationStore.selectedAutomation}
<div class="block-label"> <div class="block-label">Automation <b>{automation.name}</b></div>
Automation
<b>{automation.name}</b>
</div>
<Button secondary wide on:click={testAutomation}>Test Automation</Button> <Button secondary wide on:click={testAutomation}>Test Automation</Button>
{/if} {/if}
</div> </div>

View File

@ -108,7 +108,8 @@
<div <div
class:link={row[header] && row[header].length} class:link={row[header] && row[header].length}
on:click={() => selectRelationship(row, header)}> on:click={() => selectRelationship(row, header)}>
{row[header] ? row[header].length : 0} related row(s) {row[header] ? row[header].length : 0}
related row(s)
</div> </div>
{:else if schema[header].type === 'attachment'} {:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} /> <AttachmentList files={row[header] || []} />

View File

@ -49,9 +49,7 @@
<button class:selected={currentPage === 0} on:click={() => selectPage(0)}> <button class:selected={currentPage === 0} on:click={() => selectPage(0)}>
1 1
</button> </button>
{#if currentPage > 3} {#if currentPage > 3}<button disabled>...</button>{/if}
<button disabled>...</button>
{/if}
{#each pagesAroundCurrent as idx} {#each pagesAroundCurrent as idx}
<button <button
class:selected={idx === currentPage} class:selected={idx === currentPage}
@ -59,9 +57,7 @@
{idx + 1} {idx + 1}
</button> </button>
{/each} {/each}
{#if currentPage < numPages - 4} {#if currentPage < numPages - 4}<button disabled>...</button>{/if}
<button disabled>...</button>
{/if}
<button <button
class:selected={currentPage === numPages - 1} class:selected={currentPage === numPages - 1}
on:click={() => selectPage(numPages - 1)}> on:click={() => selectPage(numPages - 1)}>
@ -77,8 +73,13 @@
<p> <p>
{#if numPages > 1} {#if numPages > 1}
Showing {ITEMS_PER_PAGE * currentPage + 1} - {ITEMS_PER_PAGE * currentPage + pageItemCount} Showing
of {data.length} rows {ITEMS_PER_PAGE * currentPage + 1}
-
{ITEMS_PER_PAGE * currentPage + pageItemCount}
of
{data.length}
rows
{:else if numPages === 1}Showing all {data.length} row(s){/if} {:else if numPages === 1}Showing all {data.length} row(s){/if}
</p> </p>
</div> </div>

View File

@ -65,16 +65,18 @@
<div class="actions"> <div class="actions">
<Input label="Name" thin bind:value={field.name} /> <Input label="Name" thin bind:value={field.name} />
<Select {#if !originalName}
secondary <Select
thin secondary
label="Type" thin
on:change={handleFieldConstraints} label="Type"
bind:value={field.type}> on:change={handleFieldConstraints}
{#each Object.values(fieldDefinitions) as field} bind:value={field.type}>
<option value={field.type}>{field.name}</option> {#each Object.values(fieldDefinitions) as field}
{/each} <option value={field.type}>{field.name}</option>
</Select> {/each}
</Select>
{/if}
{#if field.type !== 'link'} {#if field.type !== 'link'}
<Toggle <Toggle

View File

@ -20,10 +20,21 @@
let modal let modal
let name let name
let dataImport let dataImport
let error = ""
function resetState() { function resetState() {
name = "" name = ""
dataImport = undefined dataImport = undefined
error = ""
}
function checkValid(evt) {
const tableName = evt.target.value
if ($backendUiStore.models?.some(model => model.name === tableName)) {
error = `Table with name ${tableName} already exists. Please choose another name.`
return
}
error = ""
} }
async function saveTable() { async function saveTable() {
@ -61,12 +72,14 @@
title="Create Table" title="Create Table"
confirmText="Create" confirmText="Create"
onConfirm={saveTable} onConfirm={saveTable}
disabled={!name || (dataImport && !dataImport.valid)}> disabled={error || !name || (dataImport && !dataImport.valid)}>
<Input <Input
data-cy="table-name-input" data-cy="table-name-input"
thin thin
label="Table Name" label="Table Name"
bind:value={name} /> on:input={checkValid}
bind:value={name}
{error} />
<div> <div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label> <Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport /> <TableDataImport bind:dataImport />

View File

@ -1,9 +1,11 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import screenTemplates from "builderStore/store/screenTemplates"
import api from "builderStore/api"
export let table export let table
@ -11,6 +13,10 @@
let dropdown let dropdown
let editing let editing
let confirmDeleteDialog let confirmDeleteDialog
let error = ""
let originalName = table.name
let templateScreens
let willBeDeleted
$: fields = Object.keys(table.schema) $: fields = Object.keys(table.schema)
@ -24,12 +30,18 @@
} }
function showModal() { function showModal() {
const screens = $store.allScreens
templateScreens = screens.filter(screen => screen.props.table === table._id)
willBeDeleted = ["All table data"].concat(
templateScreens.map(screen => `Screen ${screen.props._instanceName}`)
)
hideEditor() hideEditor()
confirmDeleteDialog.show() confirmDeleteDialog.show()
} }
async function deleteTable() { async function deleteTable() {
await backendUiStore.actions.tables.delete(table) await backendUiStore.actions.tables.delete(table)
store.deleteScreens(templateScreens)
notifier.success("Table deleted") notifier.success("Table deleted")
hideEditor() hideEditor()
} }
@ -39,6 +51,18 @@
notifier.success("Table renamed successfully") notifier.success("Table renamed successfully")
hideEditor() hideEditor()
} }
function checkValid(evt) {
const tableName = evt.target.value
if (
originalName !== tableName &&
$backendUiStore.models?.some(model => model.name === tableName)
) {
error = `Table with name ${tableName} already exists. Please choose another name.`
return
}
error = ""
}
</script> </script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}> <div bind:this={anchor} class="icon" on:click={dropdown.show}>
@ -48,7 +72,12 @@
{#if editing} {#if editing}
<div class="actions"> <div class="actions">
<h5>Edit Table</h5> <h5>Edit Table</h5>
<Input label="Table Name" thin bind:value={table.name} /> <Input
label="Table Name"
thin
bind:value={table.name}
on:input={checkValid}
{error} />
<Select <Select
label="Primary Display Column" label="Primary Display Column"
thin thin
@ -61,7 +90,7 @@
</Select> </Select>
<footer> <footer>
<Button secondary on:click={hideEditor}>Cancel</Button> <Button secondary on:click={hideEditor}>Cancel</Button>
<Button primary on:click={save}>Save</Button> <Button primary disabled={error} on:click={save}>Save</Button>
</footer> </footer>
</div> </div>
{:else} {:else}
@ -79,10 +108,21 @@
</DropdownMenu> </DropdownMenu>
<ConfirmDialog <ConfirmDialog
bind:this={confirmDeleteDialog} bind:this={confirmDeleteDialog}
body={`Are you sure you wish to delete the table '${table.name}'? Your data will be deleted and this action cannot be undone.`}
okText="Delete Table" okText="Delete Table"
onOk={deleteTable} onOk={deleteTable}
title="Confirm Delete" /> title="Confirm Delete">
Are you sure you wish to delete the table
<i>{table.name}?</i>
The following will also be deleted:
<b>
<div class="delete-items">
{#each willBeDeleted as item}
<div>{item}</div>
{/each}
</div>
</b>
This action cannot be undone.
</ConfirmDialog>
<style> <style>
div.icon { div.icon {
@ -96,6 +136,17 @@
font-size: 16px; font-size: 16px;
} }
div.delete-items {
margin-top: 10px;
margin-bottom: 10px;
margin-left: 10px;
}
div.delete-items div {
margin-top: 4px;
font-weight: 500;
}
.actions { .actions {
padding: var(--spacing-xl); padding: var(--spacing-xl);
display: grid; display: grid;

View File

@ -39,7 +39,6 @@
transition: 0.2s ease transform, 0.2s ease background-color, transition: 0.2s ease transform, 0.2s ease background-color,
0.2s ease box-shadow; 0.2s ease box-shadow;
overflow: hidden; overflow: hidden;
z-index: 1;
border-radius: 4px; border-radius: 4px;
} }

View File

@ -20,7 +20,10 @@
<Modal bind:this={modal} on:hide={onCancel}> <Modal bind:this={modal} on:hide={onCancel}>
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red> <ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red>
<div class="body">{body}</div> <div class="body">
{body}
<slot />
</div>
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -31,9 +31,7 @@
style="background: {themes[notification.type]};" style="background: {themes[notification.type]};"
transition:fly={{ y: -30 }}> transition:fly={{ y: -30 }}>
<div class="content">{notification.message}</div> <div class="content">{notification.message}</div>
{#if notification.icon} {#if notification.icon}<i class={notification.icon} />{/if}
<i class={notification.icon} />
{/if}
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -12,15 +12,12 @@
</script> </script>
<div class="root"> <div class="root">
<div class="switcher"> <div class="switcher">
{#each tabs as tab} {#each tabs as tab}
<button class:selected={selected === tab} on:click={() => selectTab(tab)}> <button class:selected={selected === tab} on:click={() => selectTab(tab)}>
{tab} {tab}
</button> </button>
{/each} {/each}
</div> </div>
<div class="panel"> <div class="panel">
@ -34,7 +31,6 @@
<slot name="3" /> <slot name="3" />
{/if} {/if}
</div> </div>
</div> </div>
<style> <style>

View File

@ -5,9 +5,7 @@
<div class="container"> <div class="container">
<div class="content"> <div class="content">
<div class="img"> <div class="img"><img src="https://picsum.photos/60/60" alt="zoom" /></div>
<img src="https://picsum.photos/60/60" alt="zoom" />
</div>
<div class="body"> <div class="body">
<div class="title">Zoom</div> <div class="title">Zoom</div>
<div class="description"> <div class="description">

View File

@ -58,6 +58,7 @@
<Input thin bind:value={username} name="Name" placeholder="Username" /> <Input thin bind:value={username} name="Name" placeholder="Username" />
<Input <Input
thin thin
type="password"
bind:value={password} bind:value={password}
name="Password" name="Password"
placeholder="Password" /> placeholder="Password" />

View File

@ -10,7 +10,9 @@
<Spacer medium /> <Spacer medium />
<div class="card-footer"> <div class="card-footer">
<TextButton text medium blue href="/_builder/{_id}"> <TextButton text medium blue href="/_builder/{_id}">
Open {name} Open
{name}
</TextButton> </TextButton>
</div> </div>
</div> </div>

View File

@ -28,12 +28,11 @@
<Spacer small /> <Spacer small />
<Body medium grey>{template.category}</Body> <Body medium grey>{template.category}</Body>
<Body lh small black>{template.description}</Body> <Body lh small black>{template.description}</Body>
<div> <div><img src={template.image} width="100%" /></div>
<img src={template.image} width="100%" />
</div>
<div class="card-footer"> <div class="card-footer">
<Button secondary on:click={() => onSelect(template)}> <Button secondary on:click={() => onSelect(template)}>
Create {template.name} Create
{template.name}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -13,7 +13,6 @@
{category.name} {category.name}
</li> </li>
{/each} {/each}
</div> </div>
<style> <style>

View File

@ -99,9 +99,7 @@
</script> </script>
<div bind:this={anchor} on:click|stopPropagation={() => {}}> <div bind:this={anchor} on:click|stopPropagation={() => {}}>
<div class="icon" on:click={dropdown.show}> <div class="icon" on:click={dropdown.show}><i class="ri-more-line" /></div>
<i class="ri-more-line" />
</div>
</div> </div>
<DropdownMenu <DropdownMenu
bind:this={dropdown} bind:this={dropdown}

View File

@ -1,21 +1,10 @@
<script> <script>
import { setContext, onMount } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import {
LayoutIcon,
PaintIcon,
TerminalIcon,
CircleIndicator,
EventsIcon,
} from "components/common/Icons/"
import panelStructure from "./temporaryPanelStructure.js" import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte" import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte" import DesignView from "./DesignView.svelte"
import SettingsView from "./SettingsView.svelte" import SettingsView from "./SettingsView.svelte"
let current_view = "design"
let codeEditor
let flattenedPanel = flattenComponents(panelStructure.categories) let flattenedPanel = flattenComponents(panelStructure.categories)
let categories = [ let categories = [
{ value: "settings", name: "Settings" }, { value: "settings", name: "Settings" },
@ -23,7 +12,6 @@
] ]
let selectedCategory = categories[0] let selectedCategory = categories[0]
$: components = $store.components
$: componentInstance = $: componentInstance =
$store.currentView !== "component" $store.currentView !== "component"
? { ...$store.currentPreviewItem, ...$store.currentComponentInfo } ? { ...$store.currentPreviewItem, ...$store.currentComponentInfo }
@ -76,7 +64,6 @@
</script> </script>
<div class="root"> <div class="root">
<CategoryTab <CategoryTab
onClick={category => (selectedCategory = category)} onClick={category => (selectedCategory = category)}
{categories} {categories}
@ -99,9 +86,7 @@
onScreenPropChange={store.setPageOrScreenProp} onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} /> screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{/if} {/if}
</div> </div>
</div> </div>
<style> <style>

View File

@ -1,33 +1,13 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js" import { store } from "builderStore"
import components from "./temporaryPanelStructure.js" import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import CategoryTab from "./CategoryTab.svelte" import CategoryTab from "./CategoryTab.svelte"
import {
find,
sortBy,
groupBy,
values,
filter,
map,
uniqBy,
flatten,
} from "lodash/fp"
import { pipe } from "components/common/core"
import Tab from "./ItemTab/Tab.svelte" import Tab from "./ItemTab/Tab.svelte"
import { store } from "builderStore"
export let toggleTab export let toggleTab
let selectTemplateDialog
let selectedTemplateInstance
let templateInstances = []
let selectedComponent = null
const categories = components.categories const categories = components.categories
let selectedCategory = categories[0] let selectedCategory = categories[0]
@ -45,7 +25,6 @@
</script> </script>
<div class="root"> <div class="root">
<CategoryTab <CategoryTab
onClick={category => (selectedCategory = category)} onClick={category => (selectedCategory = category)}
{selectedCategory} {selectedCategory}

View File

@ -38,13 +38,11 @@
</script> </script>
<div class="root"> <div class="root">
{#each screens as screen} {#each screens as screen}
<div <div
class="budibase__nav-item screen-header-row" class="budibase__nav-item screen-header-row"
class:selected={$store.currentComponentInfo._id === screen.props._id} class:selected={$store.currentComponentInfo._id === screen.props._id}
on:click|stopPropagation={() => changeScreen(screen)}> on:click|stopPropagation={() => changeScreen(screen)}>
<span <span
class="icon" class="icon"
class:rotate={$store.currentPreviewItem.name !== screen.props._instanceName}> class:rotate={$store.currentPreviewItem.name !== screen.props._instanceName}>
@ -69,7 +67,6 @@
{dragDropStore} /> {dragDropStore} />
{/if} {/if}
{/each} {/each}
</div> </div>
<style> <style>

View File

@ -129,7 +129,6 @@
<ul> <ul>
{#each components as component, index (component._id)} {#each components as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}> <li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'} {#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'}
<div <div
on:drop={drop} on:drop={drop}

View File

@ -20,7 +20,6 @@
<div class="root"> <div class="root">
{#if $store.currentFrontEndType === 'page' || $store.screens.length} {#if $store.currentFrontEndType === 'page' || $store.screens.length}
<div class="switcher"> <div class="switcher">
<button <button
class:selected={selected === COMPONENT_SELECTION_TAB} class:selected={selected === COMPONENT_SELECTION_TAB}
on:click={() => selectTab(COMPONENT_SELECTION_TAB)}> on:click={() => selectTab(COMPONENT_SELECTION_TAB)}>
@ -32,7 +31,6 @@
on:click={() => selectTab(PROPERTIES_TAB)}> on:click={() => selectTab(PROPERTIES_TAB)}>
Edit Edit
</button> </button>
</div> </div>
<div class="panel"> <div class="panel">
@ -43,10 +41,8 @@
{#if selected === COMPONENT_SELECTION_TAB} {#if selected === COMPONENT_SELECTION_TAB}
<ComponentSelectionList {toggleTab} /> <ComponentSelectionList {toggleTab} />
{/if} {/if}
</div> </div>
{/if} {/if}
</div> </div>
<style> <style>

View File

@ -28,7 +28,6 @@
</script> </script>
<div class="design-view-container"> <div class="design-view-container">
<div class="design-view-state-categories"> <div class="design-view-state-categories">
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} /> <FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
</div> </div>

View File

@ -18,9 +18,7 @@
</script> </script>
<div class="handler-option"> <div class="handler-option">
{#if parameter.name === 'automation'} {#if parameter.name === 'automation'}<span>{parameter.name}</span>{/if}
<span>{parameter.name}</span>
{/if}
{#if parameter.name === 'automation'} {#if parameter.name === 'automation'}
<Select on:change bind:value={parameter.value}> <Select on:change bind:value={parameter.value}>
<option value="" /> <option value="" />

View File

@ -48,7 +48,6 @@
{schemaFields} {schemaFields}
on:fieldschanged={onFieldsChanged} /> on:fieldschanged={onFieldsChanged} />
{/if} {/if}
</div> </div>
<style> <style>

View File

@ -109,7 +109,6 @@
{schemaFields} {schemaFields}
on:fieldschanged={onFieldsChanged} /> on:fieldschanged={onFieldsChanged} />
{/if} {/if}
</div> </div>
<style> <style>

View File

@ -107,7 +107,6 @@
{schemaFields} {schemaFields}
on:fieldschanged={onFieldsChanged} /> on:fieldschanged={onFieldsChanged} />
{/if} {/if}
</div> </div>
<style> <style>

View File

@ -126,9 +126,7 @@
on:click={() => switchLetter(letter)}> on:click={() => switchLetter(letter)}>
{letter} {letter}
</span> </span>
{#if idx !== alphabet.length - 1} {#if idx !== alphabet.length - 1}<span>-</span>{/if}
<span>-</span>
{/if}
{/each} {/each}
</div> </div>
<div class="search-input"> <div class="search-input">

View File

@ -3,9 +3,7 @@
</script> </script>
<div data-cy={item.name} class="item-item" on:click> <div data-cy={item.name} class="item-item" on:click>
<div class="item-icon"> <div class="item-icon"><i class={item.icon} /></div>
<i class={item.icon} />
</div>
<div class="item-text"> <div class="item-text">
<div class="item-name">{item.name}</div> <div class="item-name">{item.name}</div>
</div> </div>

View File

@ -1,8 +1,10 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store } from "builderStore"
import Item from "./Item.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
import Item from "./Item.svelte"
export let list export let list
let category = list let category = list
@ -21,10 +23,12 @@
</script> </script>
{#if !list.isCategory} {#if !list.isCategory}
<button class="back-button" on:click={() => (list = category)}>Back</button> <button class="back-button" on:click={goBack}>Back</button>
{/if} {/if}
{#each list.children as item} {#each list.children as item}
<Item {item} on:click={() => handleClick(item)} /> {#if !item.showOnPages || item.showOnPages.includes($store.currentPageName)}
<Item {item} on:click={() => handleClick(item)} />
{/if}
{/each} {/each}
<style> <style>

View File

@ -89,7 +89,6 @@
</script> </script>
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}> <ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}>
<Select <Select
label="Choose a Template" label="Choose a Template"
bind:value={templateIndex} bind:value={templateIndex}
@ -109,5 +108,4 @@
error={routeError} error={routeError}
bind:value={route} bind:value={route}
on:change={routeChanged} /> on:change={routeChanged} />
</ModalContent> </ModalContent>

View File

@ -17,26 +17,13 @@
} }
const deleteScreen = () => { const deleteScreen = () => {
store.deleteScreens(screen, $store.currentPageName)
// update the page if required
store.update(state => { store.update(state => {
// Remove screen from screens
const screens = state.screens.filter(c => c.name !== screen.name)
state.screens = screens
// Remove screen from current page as well
const pageScreens = state.pages[state.currentPageName]._screens.filter(
scr => scr.name !== screen.name
)
state.pages[state.currentPageName]._screens = pageScreens
if (state.currentPreviewItem.name === screen.name) { if (state.currentPreviewItem.name === screen.name) {
store.setCurrentPage($store.currentPageName) store.setCurrentPage($store.currentPageName)
$goto(`./:page/page-layout`) $goto(`./:page/page-layout`)
} }
api.delete(
`/_builder/api/pages/${state.currentPageName}/screens/${screen.name}`
)
return state return state
}) })
} }

View File

@ -1165,14 +1165,6 @@ export default {
}, },
children: [], children: [],
}, },
// {
// name: "Map",
// _component: "@budibase/standard-components/datamap",
// description: "Shiny map",
// icon: "ri-map-pin-line",
// properties: { design: { ...all } },
// children: [],
// },
], ],
}, },
{ {
@ -1208,6 +1200,7 @@ export default {
"A component that automatically generates a login screen for your app.", "A component that automatically generates a login screen for your app.",
icon: "ri-login-box-line", icon: "ri-login-box-line",
children: [], children: [],
showOnPages: ["unauthenticated"],
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [

View File

@ -68,7 +68,10 @@
<span <span
class:active={false} class:active={false}
class="topnavitemright" class="topnavitemright"
on:click={() => window.open(`/${application}`)}> on:click={() => {
document.cookie = 'budibase:token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
window.open(`/${application}`)
}}>
<PreviewIcon /> <PreviewIcon />
</span> </span>
</div> </div>
@ -77,7 +80,7 @@
{#await promise} {#await promise}
<!-- This should probably be some kind of loading state? --> <!-- This should probably be some kind of loading state? -->
<div /> <div />
{:then _} {:then results}
<slot /> <slot />
{:catch error} {:catch error}
<p>Something went wrong: {error.message}</p> <p>Something went wrong: {error.message}</p>

View File

@ -7,9 +7,7 @@
{#if $backendUiStore.selectedDatabase._id && selectedTable.name} {#if $backendUiStore.selectedDatabase._id && selectedTable.name}
<TableDataTable /> <TableDataTable />
{:else} {:else}<i>Create your first table to start building</i>{/if}
<i>Create your first table to start building</i>
{/if}
<style> <style>
i { i {

View File

@ -23,9 +23,7 @@
{#if $backendUiStore.tables.length === 0} {#if $backendUiStore.tables.length === 0}
<i>Create your first table to start building</i> <i>Create your first table to start building</i>
{:else} {:else}<i>Select a table to edit</i>{/if}
<i>Select a table to edit</i>
{/if}
<style> <style>
i { i {

View File

@ -7,9 +7,7 @@
{#if $backendUiStore.selectedDatabase._id && selectedView} {#if $backendUiStore.selectedDatabase._id && selectedView}
<ViewDataTable view={selectedView} /> <ViewDataTable view={selectedView} />
{:else} {:else}<i>Create your first table to start building</i>{/if}
<i>Create your first table to start building</i>
{/if}
<style> <style>
i { i {

View File

@ -3,7 +3,7 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder, buildPage } = require("../../utilities/builder") const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
const env = require("../../environment") const env = require("../../environment")
const instanceController = require("./instance") const instanceController = require("./instance")
const { copy, exists, readFile, writeFile } = require("fs-extra") const { copy, existsSync, readFile, writeFile } = require("fs-extra")
const { budibaseAppsDir } = require("../../utilities/budibaseDir") const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const sqrl = require("squirrelly") const sqrl = require("squirrelly")
const setBuilderToken = require("../../utilities/builder/setBuilderToken") const setBuilderToken = require("../../utilities/builder/setBuilderToken")
@ -116,6 +116,12 @@ exports.delete = async function(ctx) {
const db = new CouchDB(ClientDb.name(getClientId(ctx))) const db = new CouchDB(ClientDb.name(getClientId(ctx)))
const app = await db.get(ctx.params.applicationId) const app = await db.get(ctx.params.applicationId)
const result = await db.remove(app) const result = await db.remove(app)
for (let instance of app.instances) {
const instanceDb = new CouchDB(instance._id)
await instanceDb.destroy()
}
// remove top level directory
await fs.rmdir(join(budibaseAppsDir(), ctx.params.applicationId), { await fs.rmdir(join(budibaseAppsDir(), ctx.params.applicationId), {
recursive: true, recursive: true,
}) })
@ -137,7 +143,7 @@ const createEmptyAppPackage = async (ctx, app) => {
const appsFolder = budibaseAppsDir() const appsFolder = budibaseAppsDir()
const newAppFolder = resolve(appsFolder, app._id) const newAppFolder = resolve(appsFolder, app._id)
if (await exists(newAppFolder)) { if (existsSync(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application") ctx.throw(400, "App folder already exists for this application")
} }

View File

@ -41,7 +41,7 @@ exports.authenticate = async ctx => {
dbUser = await instanceDb.get(generateUserID(username)) dbUser = await instanceDb.get(generateUserID(username))
} catch (_) { } catch (_) {
// do not want to throw a 404 - as this could be // do not want to throw a 404 - as this could be
// used to dtermine valid usernames // used to determine valid usernames
ctx.throw(401, "Invalid Credentials") ctx.throw(401, "Invalid Credentials")
} }

View File

@ -73,6 +73,12 @@ exports.save = async function(ctx) {
let row = ctx.request.body let row = ctx.request.body
row.tableId = ctx.params.tableId row.tableId = ctx.params.tableId
if (ctx.request.body.type === "delete") {
await bulkDelete(ctx)
ctx.body = ctx.request.body.rows
return
}
if (!row._rev && !row._id) { if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId) row._id = generateRowID(row.tableId)
} }
@ -348,3 +354,25 @@ const TYPE_TRANSFORM_MAP = {
false: false, false: false,
}, },
} }
async function bulkDelete(ctx) {
const instanceId = ctx.user.instanceId
const { rows } = ctx.request.body
const db = new CouchDB(instanceId)
const linkUpdates = rows.map(row =>
linkRows.updateLinks({
instanceId,
eventType: linkRows.EventType.ROW_DELETE,
row,
tableId: row.tableId,
})
)
await db.bulkDocs(rows.map(row => ({ ...row, _deleted: true })))
await Promise.all(linkUpdates)
rows.forEach(row => {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, instanceId, row)
})
}

View File

@ -0,0 +1,17 @@
/**
* This controller is not currently fully implemented. Screens are
* currently managed as part of the pages API, please look in api/routes/page.js
* for routes and controllers.
*/
exports.fetch = async ctx => {
ctx.throw(501)
}
exports.save = async ctx => {
ctx.throw(501)
}
exports.destroy = async ctx => {
ctx.throw(501)
}

View File

@ -1,6 +1,5 @@
const send = require("koa-send") const send = require("koa-send")
const { resolve, join } = require("../../utilities/centralPath") const { resolve, join } = require("../../utilities/centralPath")
const jwt = require("jsonwebtoken")
const fetch = require("node-fetch") const fetch = require("node-fetch")
const fs = require("fs-extra") const fs = require("fs-extra")
const uuid = require("uuid") const uuid = require("uuid")
@ -13,8 +12,8 @@ const {
} = require("../../utilities/budibaseDir") } = require("../../utilities/budibaseDir")
const CouchDB = require("../../db") const CouchDB = require("../../db")
const setBuilderToken = require("../../utilities/builder/setBuilderToken") const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
const fileProcessor = require("../../utilities/fileProcessor") const fileProcessor = require("../../utilities/fileProcessor")
const { AuthTypes } = require("../../constants")
exports.serveBuilder = async function(ctx) { exports.serveBuilder = async function(ctx) {
let builderPath = resolve(__dirname, "../../../builder") let builderPath = resolve(__dirname, "../../../builder")
@ -136,7 +135,8 @@ exports.performLocalFileProcessing = async function(ctx) {
} }
exports.serveApp = async function(ctx) { exports.serveApp = async function(ctx) {
const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated" const mainOrAuth =
ctx.auth.authenticated === AuthTypes.APP ? "main" : "unauthenticated"
// default to homedir // default to homedir
const appPath = resolve( const appPath = resolve(
@ -146,26 +146,7 @@ exports.serveApp = async function(ctx) {
mainOrAuth mainOrAuth
) )
let appId = ctx.params.appId const appId = ctx.user.appId
if (process.env.CLOUD) {
appId = ctx.subdomains[1]
}
// only set the appId cookie for /appId .. we COULD check for valid appIds
// but would like to avoid that DB hit
const looksLikeAppId = /^(app_)?[0-9a-f]{32}$/.test(appId)
if (looksLikeAppId && !ctx.auth.authenticated) {
const anonUser = {
userId: "ANON",
accessLevelId: ANON_LEVEL_ID,
appId,
}
const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret)
ctx.cookies.set("budibase:token", anonToken, {
path: "/",
httpOnly: false,
})
}
if (process.env.CLOUD) { if (process.env.CLOUD) {
const S3_URL = `https://${appId}.app.budi.live/assets/${appId}/${mainOrAuth}/${ctx.file || const S3_URL = `https://${appId}.app.budi.live/assets/${appId}/${mainOrAuth}/${ctx.file ||
@ -200,7 +181,8 @@ exports.serveAttachment = async function(ctx) {
exports.serveAppAsset = async function(ctx) { exports.serveAppAsset = async function(ctx) {
// default to homedir // default to homedir
const mainOrAuth = ctx.auth.authenticated ? "main" : "unauthenticated" const mainOrAuth =
ctx.auth.authenticated === AuthTypes.APP ? "main" : "unauthenticated"
const appPath = resolve( const appPath = resolve(
budibaseAppsDir(), budibaseAppsDir(),

View File

@ -0,0 +1,7 @@
const AuthTypes = {
APP: "app",
BUILDER: "builder",
EXTERNAL: "external",
}
exports.AuthTypes = AuthTypes

View File

@ -7,6 +7,8 @@ const {
BUILDER_LEVEL_ID, BUILDER_LEVEL_ID,
ANON_LEVEL_ID, ANON_LEVEL_ID,
} = require("../utilities/accessLevels") } = require("../utilities/accessLevels")
const environment = require("../environment")
const { AuthTypes } = require("../constants")
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
if (ctx.path === "/_builder") { if (ctx.path === "/_builder") {
@ -17,36 +19,32 @@ module.exports = async (ctx, next) => {
const appToken = ctx.cookies.get("budibase:token") const appToken = ctx.cookies.get("budibase:token")
const builderToken = ctx.cookies.get("builder:token") const builderToken = ctx.cookies.get("builder:token")
if (builderToken) { let token
try { // if running locally in the builder itself
const jwtPayload = jwt.verify(builderToken, ctx.config.jwtSecret) if (!environment.CLOUD && !appToken) {
ctx.auth = { token = builderToken
apiKey: jwtPayload.apiKey, ctx.auth.authenticated = AuthTypes.BUILDER
authenticated: jwtPayload.accessLevelId === BUILDER_LEVEL_ID, } else {
} token = appToken
ctx.user = { ctx.auth.authenticated = AuthTypes.APP
...jwtPayload,
accessLevel: await getAccessLevel(
jwtPayload.instanceId,
jwtPayload.accessLevelId
),
}
} catch (_) {
// empty: do nothing
}
await next()
return
} }
if (!appToken) { if (!token) {
ctx.auth.authenticated = false ctx.auth.authenticated = false
const appId = process.env.CLOUD ? ctx.subdomains[1] : ctx.params.appId
ctx.user = {
// if appId can't be determined from path param or subdomain
appId: appId || ctx.referer.split("/").pop(),
}
await next() await next()
return return
} }
try { try {
const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret) const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
ctx.auth.apiKey = jwtPayload.apiKey
ctx.user = { ctx.user = {
...jwtPayload, ...jwtPayload,
accessLevel: await getAccessLevel( accessLevel: await getAccessLevel(
@ -54,10 +52,6 @@ module.exports = async (ctx, next) => {
jwtPayload.accessLevelId jwtPayload.accessLevelId
), ),
} }
ctx.auth = {
authenticated: ctx.user.accessLevelId !== ANON_LEVEL_ID,
apiKey: jwtPayload.apiKey,
}
} catch (err) { } catch (err) {
ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text) ctx.throw(err.status || STATUS_CODES.FORBIDDEN, err.text)
} }

View File

@ -7,6 +7,7 @@ const {
} = require("../utilities/accessLevels") } = require("../utilities/accessLevels")
const environment = require("../environment") const environment = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient") const { apiKeyTable } = require("../db/dynamoClient")
const { AuthTypes } = require("../constants")
module.exports = (permName, getItemId) => async (ctx, next) => { module.exports = (permName, getItemId) => async (ctx, next) => {
if ( if (
@ -21,8 +22,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
if (apiKeyInfo) { if (apiKeyInfo) {
ctx.auth = { ctx.auth = {
authenticated: true, authenticated: AuthTypes.EXTERNAL,
external: true,
apiKey: ctx.headers["x-api-key"], apiKey: ctx.headers["x-api-key"],
} }
ctx.user = { ctx.user = {
@ -34,6 +34,9 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
ctx.throw(403, "API key invalid") ctx.throw(403, "API key invalid")
} }
// don't expose builder endpoints in the cloud
if (environment.CLOUD && permName === BUILDER) return
if (!ctx.auth.authenticated) { if (!ctx.auth.authenticated) {
ctx.throw(403, "Session not authenticated") ctx.throw(403, "Session not authenticated")
} }
@ -42,6 +45,10 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
ctx.throw(403, "User not found") ctx.throw(403, "User not found")
} }
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
return next()
}
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) { if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) {
return next() return next()
} }
@ -53,10 +60,6 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "") const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
return next()
}
const thisPermissionId = permissionId({ const thisPermissionId = permissionId({
name: permName, name: permName,
itemId: getItemId && getItemId(ctx), itemId: getItemId && getItemId(ctx),

View File

@ -21,7 +21,6 @@ module.exports.PRETTY_ACCESS_LEVELS = {
[module.exports.ADMIN_LEVEL_ID]: "Admin", [module.exports.ADMIN_LEVEL_ID]: "Admin",
[module.exports.POWERUSER_LEVEL_ID]: "Power user", [module.exports.POWERUSER_LEVEL_ID]: "Power user",
[module.exports.BUILDER_LEVEL_ID]: "Builder", [module.exports.BUILDER_LEVEL_ID]: "Builder",
[module.exports.ANON_LEVEL_ID]: "Anonymous",
} }
module.exports.adminPermissions = [ module.exports.adminPermissions = [
{ {

View File

@ -7,6 +7,7 @@ const VALIDATORS = {
} }
const PARSERS = { const PARSERS = {
number: attribute => Number(attribute),
datetime: attribute => new Date(attribute).toISOString(), datetime: attribute => new Date(attribute).toISOString(),
} }
@ -24,7 +25,7 @@ function parse(path, parsers) {
} }
} }
}) })
result.fromFile(path).subscribe(row => { result.subscribe(row => {
// For each CSV row parse all the columns that need parsed // For each CSV row parse all the columns that need parsed
for (let key in parsers) { for (let key in parsers) {
if (!schema[key] || schema[key].success) { if (!schema[key] || schema[key].success) {

View File

@ -10,7 +10,6 @@ async function processImage(file) {
const imgMeta = await sharp(file.path) const imgMeta = await sharp(file.path)
.resize(300) .resize(300)
.toFile(file.outputPath) .toFile(file.outputPath)
return { return {
...file, ...file,
...imgMeta, ...imgMeta,

View File

@ -1,4 +1,4 @@
const { exists, readFile, writeFile, ensureDir } = require("fs-extra") const { existsSync, readFile, writeFile, ensureDir } = require("fs-extra")
const { join, resolve } = require("./centralPath") const { join, resolve } = require("./centralPath")
const Sqrl = require("squirrelly") const Sqrl = require("squirrelly")
const uuid = require("uuid") const uuid = require("uuid")
@ -28,7 +28,7 @@ const setCouchDbUrl = async opts => {
const createDevEnvFile = async opts => { const createDevEnvFile = async opts => {
const destConfigFile = join(opts.dir, "./.env") const destConfigFile = join(opts.dir, "./.env")
let createConfig = !(await exists(destConfigFile)) || opts.quiet let createConfig = !existsSync(destConfigFile) || opts.quiet
if (createConfig) { if (createConfig) {
const template = await readFile( const template = await readFile(
resolve(__dirname, "..", "..", ".env.template"), resolve(__dirname, "..", "..", ".env.template"),

View File

@ -23,9 +23,7 @@
</script> </script>
<div use:cssVars={cssVariables} class="container"> <div use:cssVars={cssVariables} class="container">
{#if showImage} {#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
<img class="image" src={imageUrl} alt="" />
{/if}
<div class="content"> <div class="content">
<h2 class="heading">{heading}</h2> <h2 class="heading">{heading}</h2>
<h4 class="text">{description}</h4> <h4 class="text">{description}</h4>

View File

@ -26,9 +26,7 @@
</script> </script>
<div use:cssVars={cssVariables} class="container"> <div use:cssVars={cssVariables} class="container">
{#if showImage} {#if showImage}<img class="image" src={imageUrl} alt="" />{/if}
<img class="image" src={imageUrl} alt="" />
{/if}
<div class="content"> <div class="content">
<main> <main>
<h2 class="heading">{heading}</h2> <h2 class="heading">{heading}</h2>

View File

@ -27,15 +27,4 @@
on:newRow={() => dispatch('newRow')} /> on:newRow={() => dispatch('newRow')} />
</DropdownMenu> --> </DropdownMenu> -->
<!-- <style> <!--<style ✂prettier:content✂="CiAgZGl2IHsKICAgIGRpc3BsYXk6IGdyaWQ7CiAgICBncmlkLXRlbXBsYXRlLWNvbHVtbnM6IGF1dG8gYXV0bzsKICAgIHBsYWNlLWl0ZW1zOiBzdGFydCBjZW50ZXI7CiAgfQogIGg1IHsKICAgIHBhZGRpbmc6IHZhcigtLXNwYWNpbmcteGwpIDAgMCB2YXIoLS1zcGFjaW5nLXhsKTsKICAgIG1hcmdpbjogMDsKICAgIGZvbnQtd2VpZ2h0OiA1MDA7CiAgfQo=" ✂prettier:content✂="" ✂prettier:content✂="" ✂prettier:content✂="" ✂prettier:content✂=""></style>-->
div {
display: grid;
grid-template-columns: auto auto;
place-items: start center;
}
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
</style> -->

View File

@ -135,7 +135,9 @@
{#if selectedRows.length > 0} {#if selectedRows.length > 0}
<DeleteButton text small on:click={deleteRows}> <DeleteButton text small on:click={deleteRows}>
<Icon name="addrow" /> <Icon name="addrow" />
Delete {selectedRows.length} row(s) Delete
{selectedRows.length}
row(s)
</DeleteButton> </DeleteButton>
{/if} {/if}
</div> </div>

View File

@ -42,9 +42,7 @@
<div class="root"> <div class="root">
<div class="content"> <div class="content">
{#if logo} {#if logo}
<div class="logo-container"> <div class="logo-container"><img src={logo} alt="logo" /></div>
<img src={logo} alt="logo" />
</div>
{/if} {/if}
{#if title} {#if title}

View File

@ -33,6 +33,4 @@
<sub class={className}>{text}</sub> <sub class={className}>{text}</sub>
{:else if isTag('sup')} {:else if isTag('sup')}
<sup class={className}>{text}</sup> <sup class={className}>{text}</sup>
{:else} {:else}<span>{text}</span>{/if}
<span>{text}</span>
{/if}

View File

@ -12,9 +12,7 @@
<div class="file"> <div class="file">
{#if FILE_TYPES.IMAGE.includes(file.extension.toLowerCase())} {#if FILE_TYPES.IMAGE.includes(file.extension.toLowerCase())}
<img {width} {height} src={file.url} alt="preview of {file.name}" /> <img {width} {height} src={file.url} alt="preview of {file.name}" />
{:else} {:else}<i class="far fa-file" />{/if}
<i class="far fa-file" />
{/if}
</div> </div>
<span>{file.name}</span> <span>{file.name}</span>
</a> </a>

View File

@ -3690,11 +3690,10 @@ prettier-linter-helpers@^1.0.0:
dependencies: dependencies:
fast-diff "^1.1.2" fast-diff "^1.1.2"
prettier-plugin-svelte@^0.7.0: prettier-plugin-svelte@^1.4.0:
version "0.7.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-0.7.0.tgz#5ac0b9f194e0450c88ff1e167cbf3b32d2642df2" resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-1.4.0.tgz#bb992759fb77ec2c3545d454a7c60f7a258cb745"
dependencies: integrity sha512-KXO2He7Kql0Lz4DdlzVli1j2JTDUR9jPV/DqyfnJmY1pCeSV1qZkxgdsyYma35W6OLrCAr/G6yKdmzo+75u2Ng==
tslib "^1.9.3"
prettier@^1.19.1: prettier@^1.19.1:
version "1.19.1" version "1.19.1"
@ -4529,7 +4528,7 @@ trim-off-newlines@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
tslib@^1.9.0, tslib@^1.9.3: tslib@^1.9.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"