mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
150 changed files with 3444 additions and 11188 deletions
@ -1,70 +1,65 @@ |
|||
context('Create a Table', () => { |
|||
before(() => { |
|||
cy.visit('localhost:4001/_builder') |
|||
cy.createApp('Table App', 'Table App Description') |
|||
}) |
|||
|
|||
it('should create a new Table', () => { |
|||
cy.createTable('dog') |
|||
|
|||
// Check if Table exists
|
|||
cy.get('.title').should('contain.text', 'dog') |
|||
}) |
|||
|
|||
it('adds a new column to the table', () => { |
|||
cy.addColumn('dog', 'name', 'Plain Text') |
|||
|
|||
cy.contains('name').should("be.visible") |
|||
}) |
|||
|
|||
it('creates a record in the table', () => { |
|||
cy.addRecord(["Rover"]) |
|||
|
|||
cy.contains('Rover').should("be.visible") |
|||
}) |
|||
|
|||
it('updates a column on the table', () => { |
|||
cy.contains("name").click() |
|||
cy.get("[data-cy='edit-column-header']").click() |
|||
|
|||
cy.get("[placeholder=Name]").type("updated") |
|||
cy.get("select").select("Plain Text") |
|||
|
|||
cy.contains("Save Column").click() |
|||
|
|||
cy.contains('nameupdated').should('have.text', 'nameupdated ') |
|||
}) |
|||
|
|||
it('edits a record', () => { |
|||
cy.get("tbody .ri-more-line").click() |
|||
cy.get("[data-cy=edit-row]").click() |
|||
cy.get(".actions input").type("Updated") |
|||
cy.contains("Save").click() |
|||
|
|||
cy.contains('RoverUpdated').should('have.text', 'RoverUpdated') |
|||
}) |
|||
|
|||
it('deletes a record', () => { |
|||
cy.get("tbody .ri-more-line").click() |
|||
cy.get("[data-cy=delete-row]").click() |
|||
cy.get(".modal-actions").contains("Delete").click() |
|||
|
|||
cy.contains('RoverUpdated').should('not.exist') |
|||
}) |
|||
|
|||
it('deletes a column', () => { |
|||
cy.contains("name").click() |
|||
cy.get("[data-cy='delete-column-header']").click() |
|||
|
|||
cy.contains('nameupdated').should('not.exist') |
|||
}) |
|||
|
|||
it('deletes a table', () => { |
|||
cy.contains("div", "dog").get(".ri-more-line").click() |
|||
cy.get("[data-cy=delete-table]").click() |
|||
cy.get(".modal-actions").contains("Delete").click() |
|||
|
|||
cy.contains('dog').should('not.exist') |
|||
}) |
|||
|
|||
context("Create a Table", () => { |
|||
before(() => { |
|||
cy.visit("localhost:4001/_builder") |
|||
cy.createApp("Table App", "Table App Description") |
|||
}) |
|||
|
|||
it("should create a new Table", () => { |
|||
cy.createTable("dog") |
|||
|
|||
// Check if Table exists
|
|||
cy.get(".title span").should("have.text", "dog") |
|||
}) |
|||
|
|||
it("adds a new column to the table", () => { |
|||
cy.addColumn("dog", "name", "Text") |
|||
cy.contains("name").should("be.visible") |
|||
}) |
|||
|
|||
it("creates a record in the table", () => { |
|||
cy.addRecord(["Rover"]) |
|||
cy.contains("Rover").should("be.visible") |
|||
}) |
|||
|
|||
it("updates a column on the table", () => { |
|||
cy.contains("name").click() |
|||
cy.get("[data-cy='edit-column-header']").click() |
|||
cy.get(".actions input") |
|||
.first() |
|||
.type("updated") |
|||
cy.get("select").select("Text") |
|||
cy.contains("Save Column").click() |
|||
cy.contains("nameupdated").should("have.text", "nameupdated") |
|||
}) |
|||
|
|||
it("edits a record", () => { |
|||
cy.get("tbody .ri-more-line").click() |
|||
cy.get("[data-cy=edit-row]").click() |
|||
cy.get(".modal input").type("Updated") |
|||
cy.contains("Save").click() |
|||
cy.contains("RoverUpdated").should("have.text", "RoverUpdated") |
|||
}) |
|||
|
|||
it("deletes a record", () => { |
|||
cy.get("tbody .ri-more-line").click() |
|||
cy.get("[data-cy=delete-row]").click() |
|||
cy.contains("Delete Row").click() |
|||
cy.contains("RoverUpdated").should("not.exist") |
|||
}) |
|||
|
|||
it("deletes a column", () => { |
|||
cy.contains("name").click() |
|||
cy.get("[data-cy='delete-column-header']").click() |
|||
cy.contains("Delete Column").click() |
|||
cy.contains("nameupdated").should("not.exist") |
|||
}) |
|||
|
|||
it("deletes a table", () => { |
|||
cy.contains("div", "dog") |
|||
.get(".ri-more-line") |
|||
.click() |
|||
cy.get("[data-cy=delete-table]").click() |
|||
cy.contains("Delete Table").click() |
|||
cy.contains("dog").should("not.exist") |
|||
}) |
|||
}) |
|||
|
|||
@ -1,89 +0,0 @@ |
|||
<script> |
|||
import { store, backendUiStore, automationStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
|
|||
export let onClosed |
|||
|
|||
let name |
|||
|
|||
$: valid = !!name |
|||
$: instanceId = $backendUiStore.selectedDatabase._id |
|||
|
|||
async function deleteAutomation() { |
|||
await automationStore.actions.delete({ |
|||
instanceId, |
|||
automation: $automationStore.selectedAutomation.automation, |
|||
}) |
|||
onClosed() |
|||
notifier.danger("Automation deleted.") |
|||
} |
|||
</script> |
|||
|
|||
<header> |
|||
<i class="ri-stackshare-line" /> |
|||
Delete Automation |
|||
</header> |
|||
<div> |
|||
<p> |
|||
Are you sure you want to delete this automation? This action can't be |
|||
undone. |
|||
</p> |
|||
</div> |
|||
<footer> |
|||
<a href="https://docs.budibase.com"> |
|||
<i class="ri-information-line" /> |
|||
Learn about automations |
|||
</a> |
|||
<ActionButton on:click={onClosed}>Cancel</ActionButton> |
|||
<ActionButton alert on:click={deleteAutomation}>Delete</ActionButton> |
|||
</footer> |
|||
|
|||
<style> |
|||
header { |
|||
font-size: 24px; |
|||
color: var(--ink); |
|||
font-weight: bold; |
|||
padding: 30px; |
|||
} |
|||
|
|||
header i { |
|||
margin-right: 10px; |
|||
font-size: 20px; |
|||
background: var(--blue-light); |
|||
color: var(--grey-4); |
|||
padding: 8px; |
|||
} |
|||
|
|||
div { |
|||
padding: 0 30px 30px 30px; |
|||
} |
|||
|
|||
label { |
|||
font-size: 18px; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
footer { |
|||
display: grid; |
|||
grid-auto-flow: column; |
|||
grid-gap: 5px; |
|||
grid-auto-columns: 3fr 1fr 1fr; |
|||
padding: 20px; |
|||
background: var(--grey-1); |
|||
border-radius: 0.5rem; |
|||
} |
|||
|
|||
footer a { |
|||
color: var(--primary); |
|||
font-size: 14px; |
|||
vertical-align: middle; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
footer i { |
|||
font-size: 20px; |
|||
margin-right: 10px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,39 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import CreateRowButton from "./buttons/CreateRowButton.svelte" |
|||
import CreateColumnButton from "./buttons/CreateColumnButton.svelte" |
|||
import CreateViewButton from "./buttons/CreateViewButton.svelte" |
|||
import ExportButton from "./buttons/ExportButton.svelte" |
|||
import * as api from "./api" |
|||
import Table from "./Table.svelte" |
|||
|
|||
let data = [] |
|||
let loading = false |
|||
|
|||
$: title = $backendUiStore.selectedModel.name |
|||
$: schema = $backendUiStore.selectedModel.schema |
|||
$: modelView = { |
|||
schema, |
|||
name: $backendUiStore.selectedView.name, |
|||
} |
|||
|
|||
// Fetch records for specified model |
|||
$: { |
|||
if ($backendUiStore.selectedView?.name?.startsWith("all_")) { |
|||
loading = true |
|||
api.fetchDataForView($backendUiStore.selectedView).then(records => { |
|||
data = records || [] |
|||
loading = false |
|||
}) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Table {title} {schema} {data} allowEditing={true} {loading}> |
|||
<CreateColumnButton /> |
|||
{#if Object.keys(schema).length > 0} |
|||
<CreateRowButton /> |
|||
<CreateViewButton /> |
|||
<ExportButton view={modelView} /> |
|||
{/if} |
|||
</Table> |
|||
@ -0,0 +1,34 @@ |
|||
<script> |
|||
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui" |
|||
import Dropzone from "components/common/Dropzone.svelte" |
|||
import { capitalise } from "../../../helpers" |
|||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte" |
|||
|
|||
export let meta |
|||
export let value = meta.type === "boolean" ? false : "" |
|||
|
|||
$: type = meta.type |
|||
$: label = capitalise(meta.name) |
|||
</script> |
|||
|
|||
{#if type === 'options'} |
|||
<Select thin secondary {label} data-cy="{meta.name}-select" bind:value> |
|||
<option value="">Choose an option</option> |
|||
{#each meta.constraints.inclusion as opt} |
|||
<option value={opt}>{opt}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if type === 'datetime'} |
|||
<DatePicker {label} bind:value /> |
|||
{:else if type === 'attachment'} |
|||
<div> |
|||
<Label extraSmall grey forAttr={'dropzone-label'}>{label}</Label> |
|||
<Dropzone bind:files={value} /> |
|||
</div> |
|||
{:else if type === 'boolean'} |
|||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" /> |
|||
{:else if type === 'link'} |
|||
<LinkedRecordSelector bind:linkedRecords={value} schema={meta} /> |
|||
{:else} |
|||
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value /> |
|||
{/if} |
|||
@ -0,0 +1,40 @@ |
|||
<script> |
|||
import api from "builderStore/api" |
|||
import Table from "./Table.svelte" |
|||
import { onMount } from "svelte" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
export let modelId |
|||
export let recordId |
|||
export let fieldName |
|||
|
|||
let record |
|||
let title |
|||
|
|||
$: data = record?.[fieldName] ?? [] |
|||
$: linkedModelId = data?.length ? data[0].modelId : null |
|||
$: linkedModel = $backendUiStore.models.find( |
|||
model => model._id === linkedModelId |
|||
) |
|||
$: schema = linkedModel?.schema |
|||
$: model = $backendUiStore.models.find(model => model._id === modelId) |
|||
$: fetchData(modelId, recordId) |
|||
$: { |
|||
let recordLabel = record?.[model?.primaryDisplay] |
|||
if (recordLabel) { |
|||
title = `${recordLabel} - ${fieldName}` |
|||
} else { |
|||
title = fieldName |
|||
} |
|||
} |
|||
|
|||
async function fetchData(modelId, recordId) { |
|||
const QUERY_VIEW_URL = `/api/${modelId}/${recordId}/enrich` |
|||
const response = await api.get(QUERY_VIEW_URL) |
|||
record = await response.json() |
|||
} |
|||
</script> |
|||
|
|||
{#if record && record._id === recordId} |
|||
<Table {title} {schema} {data} /> |
|||
{/if} |
|||
@ -0,0 +1,44 @@ |
|||
<script> |
|||
import api from "builderStore/api" |
|||
import Table from "./Table.svelte" |
|||
import CalculateButton from "./buttons/CalculateButton.svelte" |
|||
import GroupByButton from "./buttons/GroupByButton.svelte" |
|||
import FilterButton from "./buttons/FilterButton.svelte" |
|||
import ExportButton from "./buttons/ExportButton.svelte" |
|||
|
|||
export let view = {} |
|||
|
|||
let data = [] |
|||
|
|||
$: name = view.name |
|||
|
|||
// Fetch records for specified view |
|||
$: { |
|||
if (!name.startsWith("all_")) { |
|||
fetchViewData(name, view.field, view.groupBy) |
|||
} |
|||
} |
|||
|
|||
async function fetchViewData(name, field, groupBy) { |
|||
const params = new URLSearchParams() |
|||
if (field) { |
|||
params.set("field", field) |
|||
params.set("stats", true) |
|||
} |
|||
if (groupBy) { |
|||
params.set("group", groupBy) |
|||
} |
|||
const QUERY_VIEW_URL = `/api/views/${name}?${params}` |
|||
const response = await api.get(QUERY_VIEW_URL) |
|||
data = await response.json() |
|||
} |
|||
</script> |
|||
|
|||
<Table title={decodeURI(name)} schema={view.schema} {data}> |
|||
<FilterButton {view} /> |
|||
<CalculateButton {view} /> |
|||
{#if view.calculation} |
|||
<GroupByButton {view} /> |
|||
{/if} |
|||
<ExportButton {view} /> |
|||
</Table> |
|||
@ -0,0 +1,19 @@ |
|||
<script> |
|||
import { Popover, TextButton, Icon } from "@budibase/bbui" |
|||
import CalculatePopover from "../popovers/CalculatePopover.svelte" |
|||
|
|||
export let view = {} |
|||
|
|||
let anchor |
|||
let dropdown |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small on:click={dropdown.show} active={!!view.field}> |
|||
<Icon name="calculate" /> |
|||
Calculate |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<CalculatePopover {view} onClosed={dropdown.hide} /> |
|||
</Popover> |
|||
@ -1,20 +1,21 @@ |
|||
<script> |
|||
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
|||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
|||
import CreateEditColumnPopover from "../popovers/CreateEditColumnPopover.svelte" |
|||
|
|||
let anchor |
|||
let dropdown |
|||
let fieldName |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<Button text small on:click={dropdown.show}> |
|||
<Icon name="addrow" /> |
|||
Create New Row |
|||
<Icon name="addcolumn" /> |
|||
Create New Column |
|||
</Button> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
<h5>Add New Row</h5> |
|||
<CreateEditRecord onClosed={dropdown.hide} /> |
|||
<h5>Create Column</h5> |
|||
<CreateEditColumnPopover onClosed={dropdown.hide} /> |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
@ -0,0 +1,16 @@ |
|||
<script> |
|||
import { TextButton as Button, Icon, Modal } from "@budibase/bbui" |
|||
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte" |
|||
|
|||
let modal |
|||
</script> |
|||
|
|||
<div> |
|||
<Button text small on:click={modal.show}> |
|||
<Icon name="addrow" /> |
|||
Create New Row |
|||
</Button> |
|||
</div> |
|||
<Modal bind:this={modal}> |
|||
<CreateEditRecordModal /> |
|||
</Modal> |
|||
@ -0,0 +1,17 @@ |
|||
<script> |
|||
import { Popover, TextButton, Icon } from "@budibase/bbui" |
|||
import CreateViewPopover from "../popovers/CreateViewPopover.svelte" |
|||
|
|||
let anchor |
|||
let dropdown |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small on:click={dropdown.show}> |
|||
<Icon name="view" /> |
|||
Create New View |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<CreateViewPopover onClosed={dropdown.hide} /> |
|||
</Popover> |
|||
@ -0,0 +1,20 @@ |
|||
<script> |
|||
import { TextButton, Icon, Popover } from "@budibase/bbui" |
|||
import api from "builderStore/api" |
|||
import ExportPopover from "../popovers/ExportPopover.svelte" |
|||
|
|||
export let view |
|||
|
|||
let anchor |
|||
let dropdown |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small on:click={dropdown.show}> |
|||
<Icon name="download" /> |
|||
Export |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<ExportPopover {view} onClosed={dropdown.hide} /> |
|||
</Popover> |
|||
@ -0,0 +1,23 @@ |
|||
<script> |
|||
import { Popover, TextButton, Icon } from "@budibase/bbui" |
|||
import FilterPopover from "../popovers/FilterPopover.svelte" |
|||
|
|||
export let view = {} |
|||
|
|||
let anchor |
|||
let dropdown |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton |
|||
text |
|||
small |
|||
on:click={dropdown.show} |
|||
active={view.filters && view.filters.length}> |
|||
<Icon name="filter" /> |
|||
Filter |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<FilterPopover {view} onClosed={dropdown.hide} /> |
|||
</Popover> |
|||
@ -0,0 +1,19 @@ |
|||
<script> |
|||
import { Popover, TextButton, Icon } from "@budibase/bbui" |
|||
import GroupByPopover from "../popovers/GroupByPopover.svelte" |
|||
|
|||
export let view = {} |
|||
|
|||
let anchor |
|||
let dropdown |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small active={!!view.groupBy} on:click={dropdown.show}> |
|||
<Icon name="group" /> |
|||
Group By |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<GroupByPopover {view} onClosed={dropdown.hide} /> |
|||
</Popover> |
|||
@ -0,0 +1,46 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import RecordFieldControl from "../RecordFieldControl.svelte" |
|||
import * as api from "../api" |
|||
import { ModalContent } from "@budibase/bbui" |
|||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|||
|
|||
export let record = {} |
|||
|
|||
let errors = [] |
|||
|
|||
$: creating = record?._id == null |
|||
$: model = record.modelId |
|||
? $backendUiStore.models.find(model => model._id === record?.modelId) |
|||
: $backendUiStore.selectedModel |
|||
$: modelSchema = Object.entries(model?.schema ?? {}) |
|||
|
|||
async function saveRecord() { |
|||
const recordResponse = await api.saveRecord( |
|||
{ ...record, modelId: model._id }, |
|||
model._id |
|||
) |
|||
if (recordResponse.errors) { |
|||
errors = Object.keys(recordResponse.errors) |
|||
.map(k => ({ dataPath: k, message: recordResponse.errors[k] })) |
|||
.flat() |
|||
// Prevent modal closing if there were errors |
|||
return false |
|||
} |
|||
notifier.success("Record saved successfully.") |
|||
backendUiStore.actions.records.save(recordResponse) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title={creating ? 'Create Row' : 'Edit Row'} |
|||
confirmText={creating ? 'Create Row' : 'Save Row'} |
|||
onConfirm={saveRecord}> |
|||
<ErrorsBox {errors} /> |
|||
{#each modelSchema as [key, meta]} |
|||
<div> |
|||
<RecordFieldControl {meta} bind:value={record[key]} /> |
|||
</div> |
|||
{/each} |
|||
</ModalContent> |
|||
@ -0,0 +1,144 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { |
|||
Input, |
|||
TextArea, |
|||
Button, |
|||
Select, |
|||
Toggle, |
|||
Label, |
|||
} from "@budibase/bbui" |
|||
import { cloneDeep, merge } from "lodash/fp" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { FIELDS } from "constants/backend" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import ButtonGroup from "components/common/ButtonGroup.svelte" |
|||
import NumberBox from "components/common/NumberBox.svelte" |
|||
import ValuesList from "components/common/ValuesList.svelte" |
|||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|||
import Checkbox from "components/common/Checkbox.svelte" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import DatePicker from "components/common/DatePicker.svelte" |
|||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte" |
|||
import * as api from "../api" |
|||
|
|||
let fieldDefinitions = cloneDeep(FIELDS) |
|||
|
|||
export let onClosed |
|||
export let field = { |
|||
type: "string", |
|||
constraints: fieldDefinitions.STRING.constraints, |
|||
} |
|||
|
|||
let originalName = field.name |
|||
$: modelOptions = $backendUiStore.models.filter( |
|||
model => model._id !== $backendUiStore.draftModel._id |
|||
) |
|||
$: required = !!field?.constraints?.presence |
|||
|
|||
async function saveColumn() { |
|||
backendUiStore.update(state => { |
|||
backendUiStore.actions.models.saveField({ |
|||
originalName, |
|||
field, |
|||
}) |
|||
return state |
|||
}) |
|||
onClosed() |
|||
} |
|||
|
|||
function handleFieldConstraints(event) { |
|||
const { type, constraints } = fieldDefinitions[ |
|||
event.target.value.toUpperCase() |
|||
] |
|||
field.type = type |
|||
field.constraints = constraints |
|||
} |
|||
|
|||
function onChangeRequired(e) { |
|||
const req = e.target.checked |
|||
field.constraints.presence = req ? { allowEmpty: false } : false |
|||
required = req |
|||
} |
|||
</script> |
|||
|
|||
<div class="actions"> |
|||
<Input label="Name" thin bind:value={field.name} /> |
|||
|
|||
<Select |
|||
secondary |
|||
thin |
|||
label="Type" |
|||
on:change={handleFieldConstraints} |
|||
bind:value={field.type}> |
|||
{#each Object.values(fieldDefinitions) as field} |
|||
<option value={field.type}>{field.name}</option> |
|||
{/each} |
|||
</Select> |
|||
|
|||
{#if field.type !== 'link'} |
|||
<Toggle |
|||
checked={required} |
|||
on:change={onChangeRequired} |
|||
thin |
|||
text="Required" /> |
|||
{/if} |
|||
|
|||
{#if field.type === 'string'} |
|||
<Input |
|||
thin |
|||
type="number" |
|||
label="Max Length" |
|||
bind:value={field.constraints.length.maximum} /> |
|||
{:else if field.type === 'options'} |
|||
<ValuesList |
|||
label="Options (one per line)" |
|||
bind:values={field.constraints.inclusion} /> |
|||
{:else if field.type === 'datetime'} |
|||
<DatePicker |
|||
label="Earliest" |
|||
bind:value={field.constraints.datetime.earliest} /> |
|||
<DatePicker label="Latest" bind:value={field.constraints.datetime.latest} /> |
|||
{:else if field.type === 'number'} |
|||
<Input |
|||
thin |
|||
type="number" |
|||
label="Min Value" |
|||
bind:value={field.constraints.numericality.greaterThanOrEqualTo} /> |
|||
<Input |
|||
thin |
|||
type="number" |
|||
label="Max Value" |
|||
bind:value={field.constraints.numericality.lessThanOrEqualTo} /> |
|||
{:else if field.type === 'link'} |
|||
<Select label="Table" thin secondary bind:value={field.modelId}> |
|||
<option value="">Choose an option</option> |
|||
{#each modelOptions as model} |
|||
<option value={model._id}>{model.name}</option> |
|||
{/each} |
|||
</Select> |
|||
<Input |
|||
label={`Column Name in Other Table`} |
|||
thin |
|||
bind:value={field.fieldName} /> |
|||
{/if} |
|||
<footer> |
|||
<Button secondary on:click={onClosed}>Cancel</Button> |
|||
<Button primary on:click={saveColumn}>Save Column</Button> |
|||
</footer> |
|||
</div> |
|||
|
|||
<style> |
|||
.actions { |
|||
padding: var(--spacing-xl); |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
min-width: 400px; |
|||
} |
|||
|
|||
footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: var(--spacing-m); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,61 @@ |
|||
<script> |
|||
import api from "builderStore/api" |
|||
import { Button, Select } from "@budibase/bbui" |
|||
|
|||
const FORMATS = [ |
|||
{ |
|||
name: "CSV", |
|||
key: "csv", |
|||
}, |
|||
{ |
|||
name: "JSON", |
|||
key: "json", |
|||
}, |
|||
] |
|||
|
|||
export let view |
|||
export let onClosed |
|||
|
|||
let exportFormat = FORMATS[0].key |
|||
|
|||
async function exportView() { |
|||
const response = await api.post( |
|||
`/api/views/export?format=${exportFormat}`, |
|||
view |
|||
) |
|||
const downloadInfo = await response.json() |
|||
onClosed() |
|||
window.location = downloadInfo.url |
|||
} |
|||
</script> |
|||
|
|||
<div class="popover"> |
|||
<h5>Export Data</h5> |
|||
<Select label="Format" secondary thin bind:value={exportFormat}> |
|||
{#each FORMATS as format} |
|||
<option value={format.key}>{format.name}</option> |
|||
{/each} |
|||
</Select> |
|||
<div class="footer"> |
|||
<Button secondary on:click={onClosed}>Cancel</Button> |
|||
<Button primary on:click={exportView}>Export</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.popover { |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
} |
|||
|
|||
h5 { |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: var(--spacing-m); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,94 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { DropdownMenu, Icon, Modal } from "@budibase/bbui" |
|||
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte" |
|||
import * as api from "../api" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
|
|||
export let row |
|||
|
|||
let anchor |
|||
let dropdown |
|||
let confirmDeleteDialog |
|||
let modal |
|||
|
|||
function showModal() { |
|||
dropdown.hide() |
|||
modal.show() |
|||
} |
|||
|
|||
function showDelete() { |
|||
dropdown.hide() |
|||
confirmDeleteDialog.show() |
|||
} |
|||
|
|||
async function deleteRow() { |
|||
await api.deleteRecord(row) |
|||
notifier.success("Record deleted") |
|||
backendUiStore.actions.records.delete(row) |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor} on:click={dropdown.show}> |
|||
<i class="ri-more-line" /> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
<ul> |
|||
<li data-cy="edit-row" on:click={showModal}> |
|||
<Icon name="edit" /> |
|||
<span>Edit</span> |
|||
</li> |
|||
<li data-cy="delete-row" on:click={showDelete}> |
|||
<Icon name="delete" /> |
|||
<span>Delete</span> |
|||
</li> |
|||
</ul> |
|||
</DropdownMenu> |
|||
<ConfirmDialog |
|||
bind:this={confirmDeleteDialog} |
|||
body={`Are you sure you wish to delete this row? Your data will be deleted and this action cannot be undone.`} |
|||
okText="Delete Row" |
|||
onOk={deleteRow} |
|||
title="Confirm Delete" /> |
|||
<Modal bind:this={modal}> |
|||
<CreateEditRecordModal record={row} /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.ri-more-line:hover { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
h5 { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
ul { |
|||
list-style: none; |
|||
padding-left: 0; |
|||
margin: 0; |
|||
padding: var(--spacing-s) 0; |
|||
} |
|||
|
|||
li { |
|||
display: flex; |
|||
font-family: var(--font-sans); |
|||
color: var(--ink); |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
margin: auto 0px; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
font-size: var(--font-size-xs); |
|||
} |
|||
|
|||
li:hover { |
|||
background-color: var(--grey-2); |
|||
} |
|||
|
|||
li:active { |
|||
color: var(--blue); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,48 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { Button, Input, Label, ModalContent, Modal } from "@budibase/bbui" |
|||
import Spinner from "components/common/Spinner.svelte" |
|||
import TableDataImport from "../TableDataImport.svelte" |
|||
import analytics from "analytics" |
|||
|
|||
let modal |
|||
let name |
|||
let dataImport |
|||
|
|||
function resetState() { |
|||
name = "" |
|||
dataImport = undefined |
|||
} |
|||
|
|||
async function saveTable() { |
|||
const model = await backendUiStore.actions.models.save({ |
|||
name, |
|||
schema: dataImport.schema || {}, |
|||
dataImport, |
|||
}) |
|||
notifier.success(`Table ${name} created successfully.`) |
|||
$goto(`./model/${model._id}`) |
|||
analytics.captureEvent("Table Created", { name }) |
|||
} |
|||
</script> |
|||
|
|||
<Button primary wide on:click={modal.show}>Create New Table</Button> |
|||
<Modal bind:this={modal} on:hide={resetState}> |
|||
<ModalContent |
|||
title="Create Table" |
|||
confirmText="Create" |
|||
onConfirm={saveTable} |
|||
disabled={!name || (dataImport && !dataImport.valid)}> |
|||
<Input |
|||
data-cy="table-name-input" |
|||
thin |
|||
label="Table Name" |
|||
bind:value={name} /> |
|||
<div> |
|||
<Label grey extraSmall>Create Table from CSV (Optional)</Label> |
|||
<TableDataImport bind:dataImport /> |
|||
</div> |
|||
</ModalContent> |
|||
</Modal> |
|||
@ -0,0 +1,141 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" |
|||
import { FIELDS } from "constants/backend" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
|
|||
export let table |
|||
|
|||
let anchor |
|||
let dropdown |
|||
let editing |
|||
let confirmDeleteDialog |
|||
|
|||
$: fields = Object.keys(table.schema) |
|||
|
|||
function showEditor() { |
|||
editing = true |
|||
} |
|||
|
|||
function hideEditor() { |
|||
dropdown?.hide() |
|||
editing = false |
|||
} |
|||
|
|||
function showModal() { |
|||
hideEditor() |
|||
confirmDeleteDialog.show() |
|||
} |
|||
|
|||
async function deleteTable() { |
|||
await backendUiStore.actions.models.delete(table) |
|||
notifier.success("Table deleted") |
|||
hideEditor() |
|||
} |
|||
|
|||
async function save() { |
|||
await backendUiStore.actions.models.save(table) |
|||
notifier.success("Table renamed successfully") |
|||
hideEditor() |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor} class="icon" on:click={dropdown.show}> |
|||
<i class="ri-more-line" /> |
|||
</div> |
|||
<DropdownMenu align="left" {anchor} bind:this={dropdown}> |
|||
{#if editing} |
|||
<div class="actions"> |
|||
<h5>Edit Table</h5> |
|||
<Input label="Table Name" thin bind:value={table.name} /> |
|||
<Select |
|||
label="Primary Display Column" |
|||
thin |
|||
secondary |
|||
bind:value={table.primaryDisplay}> |
|||
<option value="">Choose an option</option> |
|||
{#each fields as field} |
|||
<option value={field}>{field}</option> |
|||
{/each} |
|||
</Select> |
|||
<footer> |
|||
<Button secondary on:click={hideEditor}>Cancel</Button> |
|||
<Button primary on:click={save}>Save</Button> |
|||
</footer> |
|||
</div> |
|||
{:else} |
|||
<ul> |
|||
<li on:click={showEditor}> |
|||
<Icon name="edit" /> |
|||
Edit |
|||
</li> |
|||
<li data-cy="delete-table" on:click={showModal}> |
|||
<Icon name="delete" /> |
|||
Delete |
|||
</li> |
|||
</ul> |
|||
{/if} |
|||
</DropdownMenu> |
|||
<ConfirmDialog |
|||
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" |
|||
onOk={deleteTable} |
|||
title="Confirm Delete" /> |
|||
|
|||
<style> |
|||
div.icon { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
} |
|||
|
|||
div.icon i { |
|||
font-size: 16px; |
|||
} |
|||
|
|||
.actions { |
|||
padding: var(--spacing-xl); |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
min-width: 400px; |
|||
} |
|||
|
|||
h5 { |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
footer { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
gap: var(--spacing-m); |
|||
} |
|||
|
|||
ul { |
|||
list-style: none; |
|||
margin: 0; |
|||
padding: var(--spacing-s) 0; |
|||
} |
|||
|
|||
li { |
|||
display: flex; |
|||
font-family: var(--font-sans); |
|||
font-size: var(--font-size-xs); |
|||
color: var(--ink); |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
margin: auto 0px; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
li:hover { |
|||
background-color: var(--grey-2); |
|||
} |
|||
|
|||
li:active { |
|||
color: var(--blue); |
|||
} |
|||
</style> |
|||
@ -1,57 +1,31 @@ |
|||
<script> |
|||
import { Modal, Button, Heading, Spacer } from "@budibase/bbui" |
|||
import { Modal, ModalContent } from "@budibase/bbui" |
|||
|
|||
export let title = "" |
|||
export let body = "" |
|||
export let okText = "OK" |
|||
export let okText = "Confirm" |
|||
export let cancelText = "Cancel" |
|||
export let onOk = () => {} |
|||
export let onCancel = () => {} |
|||
export let onOk = undefined |
|||
export let onCancel = undefined |
|||
|
|||
let modal |
|||
|
|||
export const show = () => { |
|||
theModal.show() |
|||
modal.show() |
|||
} |
|||
|
|||
export const hide = () => { |
|||
theModal.hide() |
|||
} |
|||
|
|||
let theModal |
|||
|
|||
const cancel = () => { |
|||
hide() |
|||
onCancel() |
|||
} |
|||
|
|||
const ok = () => { |
|||
const result = onOk() |
|||
// allow caller to return false, to cancel the "ok" |
|||
if (result === false) return |
|||
hide() |
|||
modal.hide() |
|||
} |
|||
</script> |
|||
|
|||
<Modal id={title} bind:this={theModal}> |
|||
<h2>{title}</h2> |
|||
<Spacer extraLarge /> |
|||
<slot class="rows">{body}</slot> |
|||
<Spacer extraLarge /> |
|||
<div class="modal-footer"> |
|||
<Button red wide on:click={ok}>{okText}</Button> |
|||
<Button secondary wide on:click={cancel}>{cancelText}</Button> |
|||
</div> |
|||
<Modal bind:this={modal} on:hide={onCancel}> |
|||
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red> |
|||
<div class="body">{body}</div> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
<style> |
|||
h2 { |
|||
font-size: var(--font-size-xl); |
|||
margin: 0; |
|||
font-family: var(--font-sans); |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.modal-footer { |
|||
display: grid; |
|||
grid-gap: var(--spacing-s); |
|||
.body { |
|||
font-size: var(--font-size-s); |
|||
} |
|||
</style> |
|||
|
|||
@ -1,124 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { fade } from "svelte/transition" |
|||
import { backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
|
|||
export let ids = [] |
|||
export let field |
|||
|
|||
let records = [] |
|||
let open = false |
|||
let model |
|||
|
|||
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel.name] |
|||
|
|||
async function fetchRecords() { |
|||
const response = await api.post("/api/records/search", { |
|||
keys: ids, |
|||
}) |
|||
const modelResponse = await api.get(`/api/models/${field.modelId}`) |
|||
records = await response.json() |
|||
model = await modelResponse.json() |
|||
} |
|||
|
|||
$: ids && fetchRecords() |
|||
|
|||
function toggleOpen() { |
|||
open = !open |
|||
} |
|||
|
|||
onMount(() => { |
|||
fetchRecords() |
|||
}) |
|||
</script> |
|||
|
|||
<section> |
|||
<a on:click={toggleOpen}>{records.length}</a> |
|||
{#if open} |
|||
<div class="popover" transition:fade> |
|||
<header> |
|||
<h3>{field.name}</h3> |
|||
<i class="ri-close-circle-fill" on:click={toggleOpen} /> |
|||
</header> |
|||
{#each records as record} |
|||
<div class="linked-record"> |
|||
<div class="fields"> |
|||
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key} |
|||
<div class="field"> |
|||
<span>{model.schema[key].name}</span> |
|||
<p>{record[key]}</p> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
</section> |
|||
|
|||
<style> |
|||
section { |
|||
display: relative; |
|||
} |
|||
|
|||
header { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 12px; |
|||
} |
|||
|
|||
i { |
|||
font-size: 24px; |
|||
color: var(--ink-lighter); |
|||
} |
|||
|
|||
i:hover { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
a { |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.popover { |
|||
width: 500px; |
|||
position: absolute; |
|||
right: 15%; |
|||
padding: 20px; |
|||
background: var(--grey-1); |
|||
border: 1px solid var(--grey); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
margin: 0; |
|||
} |
|||
|
|||
.fields { |
|||
padding: 15px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr; |
|||
grid-gap: 20px; |
|||
background: var(--white); |
|||
border: 1px solid var(--grey); |
|||
border-radius: 5px; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
.field span { |
|||
color: var(--ink-lighter); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.field p { |
|||
color: var(--ink); |
|||
font-size: 14px; |
|||
word-break: break-word; |
|||
font-weight: 500; |
|||
margin-top: 4px; |
|||
} |
|||
</style> |
|||
@ -1,149 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import fsort from "fast-sort" |
|||
import getOr from "lodash/fp/getOr" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import { Button, Icon } from "@budibase/bbui" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import AttachmentList from "./AttachmentList.svelte" |
|||
import TablePagination from "./TablePagination.svelte" |
|||
import RowPopover from "./popovers/Row.svelte" |
|||
import ColumnPopover from "./popovers/Column.svelte" |
|||
import ViewPopover from "./popovers/View.svelte" |
|||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte" |
|||
import EditRowPopover from "./popovers/EditRow.svelte" |
|||
import CalculationPopover from "./popovers/Calculate.svelte" |
|||
|
|||
export let schema = [] |
|||
export let data = [] |
|||
export let title |
|||
|
|||
const ITEMS_PER_PAGE = 10 |
|||
|
|||
let currentPage = 0 |
|||
|
|||
$: columns = schema ? Object.keys(schema) : [] |
|||
|
|||
$: sort = $backendUiStore.sort |
|||
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data |
|||
$: paginatedData = |
|||
sorted && sorted.length |
|||
? sorted.slice( |
|||
currentPage * ITEMS_PER_PAGE, |
|||
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE |
|||
) |
|||
: [] |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="table-controls"> |
|||
<h2 class="title">{title}</h2> |
|||
<div class="popovers"> |
|||
<slot /> |
|||
</div> |
|||
</div> |
|||
<table class="bb-table"> |
|||
<thead> |
|||
<tr> |
|||
{#each columns as header} |
|||
<th>{header}</th> |
|||
{/each} |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
{#if paginatedData.length === 0} |
|||
<div class="no-data">No Data.</div> |
|||
{/if} |
|||
{#each paginatedData as row} |
|||
<tr> |
|||
{#each columns as header} |
|||
<td> |
|||
{#if schema[header].type === 'attachment'} |
|||
<AttachmentList files={row[header] || []} /> |
|||
{:else}{getOr('', header, row)}{/if} |
|||
</td> |
|||
{/each} |
|||
</tr> |
|||
{/each} |
|||
</tbody> |
|||
</table> |
|||
<TablePagination |
|||
{data} |
|||
bind:currentPage |
|||
pageItemCount={paginatedData.length} |
|||
{ITEMS_PER_PAGE} /> |
|||
</section> |
|||
|
|||
<style> |
|||
section { |
|||
margin-bottom: 20px; |
|||
} |
|||
.title { |
|||
font-size: 24px; |
|||
font-weight: 600; |
|||
text-rendering: optimizeLegibility; |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
table { |
|||
border: 1px solid var(--grey-4); |
|||
background: #fff; |
|||
border-radius: 3px; |
|||
border-collapse: collapse; |
|||
} |
|||
|
|||
thead { |
|||
height: 40px; |
|||
background: var(--grey-3); |
|||
border: 1px solid var(--grey-4); |
|||
} |
|||
|
|||
thead th { |
|||
color: var(--ink); |
|||
text-transform: capitalize; |
|||
font-weight: 500; |
|||
font-size: 14px; |
|||
text-rendering: optimizeLegibility; |
|||
transition: 0.5s all; |
|||
vertical-align: middle; |
|||
} |
|||
|
|||
th:hover { |
|||
color: var(--blue); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
td { |
|||
max-width: 200px; |
|||
text-overflow: ellipsis; |
|||
border: 1px solid var(--grey-4); |
|||
} |
|||
|
|||
tbody tr { |
|||
border-bottom: 1px solid var(--grey-4); |
|||
transition: 0.3s background-color; |
|||
color: var(--ink); |
|||
font-size: 12px; |
|||
} |
|||
|
|||
tbody tr:hover { |
|||
background: var(--grey-1); |
|||
} |
|||
|
|||
.table-controls { |
|||
width: 100%; |
|||
} |
|||
|
|||
.popovers { |
|||
display: flex; |
|||
} |
|||
|
|||
:global(.popovers > div) { |
|||
margin-right: var(--spacing-m); |
|||
} |
|||
|
|||
.no-data { |
|||
padding: 14px; |
|||
} |
|||
</style> |
|||
@ -1,55 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import fsort from "fast-sort" |
|||
import getOr from "lodash/fp/getOr" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import { Button, Icon } from "@budibase/bbui" |
|||
import Table from "./Table.svelte" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import LinkedRecord from "./LinkedRecord.svelte" |
|||
import TablePagination from "./TablePagination.svelte" |
|||
import RowPopover from "./popovers/Row.svelte" |
|||
import ColumnPopover from "./popovers/Column.svelte" |
|||
import ViewPopover from "./popovers/View.svelte" |
|||
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte" |
|||
import EditRowPopover from "./popovers/EditRow.svelte" |
|||
import CalculationPopover from "./popovers/Calculate.svelte" |
|||
import GroupByPopover from "./popovers/GroupBy.svelte" |
|||
import FilterPopover from "./popovers/Filter.svelte" |
|||
import ExportPopover from "./popovers/Export.svelte" |
|||
|
|||
export let view = {} |
|||
|
|||
let data = [] |
|||
|
|||
$: name = view.name |
|||
$: filters = view.filters |
|||
$: field = view.field |
|||
$: groupBy = view.groupBy |
|||
$: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy) |
|||
|
|||
async function fetchViewData(name, field, groupBy) { |
|||
const params = new URLSearchParams() |
|||
|
|||
if (field) { |
|||
params.set("field", field) |
|||
params.set("stats", true) |
|||
} |
|||
if (groupBy) params.set("group", groupBy) |
|||
|
|||
let QUERY_VIEW_URL = `/api/views/${name}?${params}` |
|||
|
|||
const response = await api.get(QUERY_VIEW_URL) |
|||
data = await response.json() |
|||
} |
|||
</script> |
|||
|
|||
<Table title={decodeURI(name)} schema={view.schema} {data}> |
|||
<FilterPopover {view} /> |
|||
<CalculationPopover {view} /> |
|||
{#if view.calculation} |
|||
<GroupByPopover {view} /> |
|||
{/if} |
|||
<ExportPopover {view} /> |
|||
</Table> |
|||
@ -1 +0,0 @@ |
|||
export { default } from "./ModelDataTable.svelte" |
|||
@ -1,161 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { Input, TextArea, Button, Select } from "@budibase/bbui" |
|||
import { cloneDeep, merge } from "lodash/fp" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { FIELDS } from "constants/backend" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import ButtonGroup from "components/common/ButtonGroup.svelte" |
|||
import NumberBox from "components/common/NumberBox.svelte" |
|||
import ValuesList from "components/common/ValuesList.svelte" |
|||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|||
import Checkbox from "components/common/Checkbox.svelte" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import DatePicker from "components/common/DatePicker.svelte" |
|||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte" |
|||
import * as api from "../api" |
|||
|
|||
let fieldDefinitions = cloneDeep(FIELDS) |
|||
|
|||
export let onClosed |
|||
export let field = { |
|||
type: "string", |
|||
constraints: fieldDefinitions.STRING.constraints, |
|||
} |
|||
|
|||
let originalName = field.name |
|||
$: required = field && field.constraints && field.constraints.presence |
|||
|
|||
async function saveColumn() { |
|||
backendUiStore.update(state => { |
|||
backendUiStore.actions.models.saveField({ |
|||
originalName, |
|||
field, |
|||
}) |
|||
|
|||
return state |
|||
}) |
|||
onClosed() |
|||
} |
|||
|
|||
function handleFieldConstraints(event) { |
|||
const { type, constraints } = fieldDefinitions[ |
|||
event.target.value.toUpperCase() |
|||
] |
|||
|
|||
field.type = type |
|||
field.constraints = constraints |
|||
} |
|||
|
|||
const getPresence = required => (required ? { allowEmpty: false } : false) |
|||
|
|||
const requiredChanged = ev => { |
|||
const req = ev.target.checked |
|||
field.constraints.presence = req ? { allowEmpty: false } : false |
|||
required = req |
|||
} |
|||
</script> |
|||
|
|||
<div class="actions"> |
|||
<Input placeholder="Name" thin bind:value={field.name} /> |
|||
|
|||
<Select |
|||
secondary |
|||
thin |
|||
on:change={handleFieldConstraints} |
|||
bind:value={field.type}> |
|||
{#each Object.values(fieldDefinitions) as field} |
|||
<option value={field.type}>{field.name}</option> |
|||
{/each} |
|||
</Select> |
|||
|
|||
<div class="info"> |
|||
<div class="field"> |
|||
<label>Required</label> |
|||
<input type="checkbox" checked={required} on:change={requiredChanged} /> |
|||
</div> |
|||
|
|||
{#if field.type === 'string' && field.constraints} |
|||
<NumberBox |
|||
label="Max Length" |
|||
bind:value={field.constraints.length.maximum} /> |
|||
<ValuesList |
|||
label="Categories" |
|||
bind:values={field.constraints.inclusion} /> |
|||
{:else if field.type === 'datetime' && field.constraints} |
|||
<DatePicker |
|||
label="Earliest" |
|||
bind:value={field.constraints.datetime.earliest} /> |
|||
<DatePicker |
|||
label="Latest" |
|||
bind:value={field.constraints.datetime.latest} /> |
|||
{:else if field.type === 'number' && field.constraints} |
|||
<NumberBox |
|||
label="Min Value" |
|||
bind:value={field.constraints.numericality.greaterThanOrEqualTo} /> |
|||
<NumberBox |
|||
label="Max Value" |
|||
bind:value={field.constraints.numericality.lessThanOrEqualTo} /> |
|||
{:else if field.type === 'link'} |
|||
<div class="field"> |
|||
<label>Link</label> |
|||
<select class="budibase__input" bind:value={field.modelId}> |
|||
<option value={''} /> |
|||
{#each $backendUiStore.models as model} |
|||
{#if model._id !== $backendUiStore.draftModel._id} |
|||
<option value={model._id}>{model.name}</option> |
|||
{/if} |
|||
{/each} |
|||
</select> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
</div> |
|||
<footer> |
|||
<div class="button-margin-3"> |
|||
<Button secondary on:click={onClosed}>Cancel</Button> |
|||
</div> |
|||
<div class="button-margin-4"> |
|||
<Button primary on:click={saveColumn}>Save Column</Button> |
|||
</div> |
|||
</footer> |
|||
|
|||
<style> |
|||
.actions { |
|||
padding: var(--spacing-l) var(--spacing-xl); |
|||
display: grid; |
|||
grid-gap: var(--spacing-xl); |
|||
} |
|||
|
|||
footer { |
|||
padding: 20px 30px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr; |
|||
gap: 20px; |
|||
background: var(--grey-1); |
|||
border-bottom-left-radius: 0.5rem; |
|||
border-bottom-left-radius: 0.5rem; |
|||
} |
|||
|
|||
.field { |
|||
display: grid; |
|||
grid-template-columns: auto 20px 1fr; |
|||
align-items: center; |
|||
grid-gap: 5px; |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
margin-bottom: var(--spacing-l); |
|||
font-family: var(--font-normal); |
|||
} |
|||
|
|||
.button-margin-3 { |
|||
grid-column-start: 3; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-4 { |
|||
grid-column-start: 4; |
|||
display: grid; |
|||
} |
|||
</style> |
|||
@ -1,93 +0,0 @@ |
|||
<script> |
|||
import { onMount, tick } from "svelte" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { compose, map, get, flatten } from "lodash/fp" |
|||
import { Input, TextArea, Button } from "@budibase/bbui" |
|||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte" |
|||
import RecordFieldControl from "./RecordFieldControl.svelte" |
|||
import * as api from "../api" |
|||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|||
|
|||
export let record = {} |
|||
export let onClosed |
|||
|
|||
let errors = [] |
|||
let selectedModel |
|||
|
|||
$: modelSchema = $backendUiStore.selectedModel |
|||
? Object.entries($backendUiStore.selectedModel.schema) |
|||
: [] |
|||
|
|||
async function saveRecord() { |
|||
const recordResponse = await api.saveRecord( |
|||
{ |
|||
...record, |
|||
modelId: $backendUiStore.selectedModel._id, |
|||
}, |
|||
$backendUiStore.selectedModel._id |
|||
) |
|||
if (recordResponse.errors) { |
|||
errors = Object.keys(recordResponse.errors) |
|||
.map(k => ({ dataPath: k, message: recordResponse.errors[k] })) |
|||
.flat() |
|||
return |
|||
} |
|||
|
|||
onClosed() |
|||
notifier.success("Record saved successfully.") |
|||
backendUiStore.actions.records.save(recordResponse) |
|||
} |
|||
</script> |
|||
|
|||
<div class="actions"> |
|||
<ErrorsBox {errors} /> |
|||
<form on:submit|preventDefault> |
|||
{#each modelSchema as [key, meta]} |
|||
<div class="bb-margin-xl"> |
|||
{#if meta.type === 'link'} |
|||
<LinkedRecordSelector |
|||
bind:linked={record[key]} |
|||
linkName={meta.name} |
|||
modelId={meta.modelId} /> |
|||
{:else} |
|||
<RecordFieldControl {meta} bind:value={record[key]} /> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</form> |
|||
</div> |
|||
<footer> |
|||
<div class="button-margin-3"> |
|||
<Button secondary on:click={onClosed}>Cancel</Button> |
|||
</div> |
|||
<div class="button-margin-4"> |
|||
<Button primary on:click={saveRecord}>Save</Button> |
|||
</div> |
|||
</footer> |
|||
|
|||
<style> |
|||
.actions { |
|||
padding: var(--spacing-l) var(--spacing-xl); |
|||
} |
|||
|
|||
footer { |
|||
padding: 20px 30px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr; |
|||
gap: 20px; |
|||
background: var(--grey-1); |
|||
border-bottom-left-radius: 0.5rem; |
|||
border-bottom-left-radius: 0.5rem; |
|||
} |
|||
|
|||
.button-margin-3 { |
|||
grid-column-start: 3; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-4 { |
|||
grid-column-start: 4; |
|||
display: grid; |
|||
} |
|||
</style> |
|||
@ -1,61 +0,0 @@ |
|||
<script> |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import * as api from "../api" |
|||
|
|||
export let record |
|||
export let onClosed |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="content"> |
|||
<header> |
|||
<i class="ri-information-line alert" /> |
|||
<h4 class="budibase__title--4">Delete Record</h4> |
|||
</header> |
|||
<p> |
|||
Are you sure you want to delete this record? All of your data will be |
|||
permanently removed. This action cannot be undone. |
|||
</p> |
|||
</div> |
|||
<div class="modal-actions"> |
|||
<ActionButton on:click={onClosed}>Cancel</ActionButton> |
|||
<ActionButton |
|||
alert |
|||
on:click={async () => { |
|||
await api.deleteRecord(record) |
|||
notifier.danger('Record deleted') |
|||
backendUiStore.actions.records.delete(record) |
|||
onClosed() |
|||
}}> |
|||
Delete |
|||
</ActionButton> |
|||
</div> |
|||
</section> |
|||
|
|||
<style> |
|||
.alert { |
|||
color: rgba(255, 0, 31, 1); |
|||
background: var(--grey-1); |
|||
padding: 5px; |
|||
} |
|||
|
|||
.modal-actions { |
|||
padding: 10px; |
|||
background: var(--grey-1); |
|||
border-top: 1px solid #ccc; |
|||
} |
|||
|
|||
header { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.content { |
|||
padding: 30px; |
|||
} |
|||
|
|||
h4 { |
|||
margin: 0 0 0 10px; |
|||
} |
|||
</style> |
|||
@ -1,64 +0,0 @@ |
|||
<script> |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import * as api from "../api" |
|||
|
|||
export let table |
|||
export let onClosed |
|||
|
|||
function deleteTable() { |
|||
backendUiStore.actions.models.delete(table) |
|||
} |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="content"> |
|||
<header> |
|||
<i class="ri-information-line alert" /> |
|||
<h4 class="budibase__title--4">Delete Table</h4> |
|||
</header> |
|||
<p> |
|||
Are you sure you want to delete this table? All of your data will be |
|||
permanently removed. This action cannot be undone. |
|||
</p> |
|||
</div> |
|||
<div class="modal-actions"> |
|||
<ActionButton on:click={onClosed}>Cancel</ActionButton> |
|||
<ActionButton |
|||
alert |
|||
on:click={async () => { |
|||
await backendUiStore.actions.models.delete(table) |
|||
notifier.danger('Table deleted') |
|||
onClosed() |
|||
}}> |
|||
Delete |
|||
</ActionButton> |
|||
</div> |
|||
</section> |
|||
|
|||
<style> |
|||
.alert { |
|||
color: rgba(255, 0, 31, 1); |
|||
background: var(--grey-1); |
|||
padding: 5px; |
|||
} |
|||
|
|||
.modal-actions { |
|||
padding: 10px; |
|||
background: var(--grey-1); |
|||
border-top: 1px solid #ccc; |
|||
} |
|||
|
|||
header { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.content { |
|||
padding: 30px; |
|||
} |
|||
|
|||
h4 { |
|||
margin: 0 0 0 10px; |
|||
} |
|||
</style> |
|||
@ -1,62 +0,0 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import ActionButton from "components/common/ActionButton.svelte" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { store, backendUiStore } from "builderStore" |
|||
import * as api from "../api" |
|||
|
|||
export let viewName |
|||
export let onClosed |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="content"> |
|||
<header> |
|||
<i class="ri-information-line alert" /> |
|||
<h4 class="budibase__title--4">Delete View</h4> |
|||
</header> |
|||
<p> |
|||
Are you sure you want to delete this view? All of your data will be |
|||
permanently removed. This action cannot be undone. |
|||
</p> |
|||
</div> |
|||
<div class="modal-actions"> |
|||
<ActionButton on:click={onClosed}>Cancel</ActionButton> |
|||
<ActionButton |
|||
alert |
|||
on:click={async () => { |
|||
await backendUiStore.actions.views.delete(viewName) |
|||
notifier.danger(`View ${viewName} deleted.`) |
|||
$goto(`./backend`) |
|||
onClosed() |
|||
}}> |
|||
Delete |
|||
</ActionButton> |
|||
</div> |
|||
</section> |
|||
|
|||
<style> |
|||
.alert { |
|||
color: rgba(255, 0, 31, 1); |
|||
background: var(--grey-1); |
|||
padding: 5px; |
|||
} |
|||
|
|||
.modal-actions { |
|||
padding: 10px; |
|||
background: var(--grey-1); |
|||
border-top: 1px solid #ccc; |
|||
} |
|||
|
|||
header { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.content { |
|||
padding: 30px; |
|||
} |
|||
|
|||
h4 { |
|||
margin: 0 0 0 10px; |
|||
} |
|||
</style> |
|||
@ -1,75 +0,0 @@ |
|||
<script> |
|||
import { Input, Select, Label, DatePicker } from "@budibase/bbui" |
|||
import Dropzone from "components/common/Dropzone.svelte" |
|||
|
|||
export let meta |
|||
export let value = meta.type === "boolean" ? false : "" |
|||
export let originalValue |
|||
|
|||
let isSelect = |
|||
meta.type === "string" && |
|||
meta.constraints && |
|||
meta.constraints.inclusion && |
|||
meta.constraints.inclusion.length > 0 |
|||
|
|||
let type = determineInputType(meta) |
|||
|
|||
function determineInputType(meta) { |
|||
if (meta.type === "datetime") return "date" |
|||
if (meta.type === "number") return "number" |
|||
if (meta.type === "boolean") return "checkbox" |
|||
if (meta.type === "attachment") return "file" |
|||
if (isSelect) return "select" |
|||
|
|||
return "text" |
|||
} |
|||
|
|||
const handleInput = event => { |
|||
if (event.target.type === "checkbox") { |
|||
value = event.target.checked |
|||
return |
|||
} |
|||
|
|||
if (event.target.type === "number") { |
|||
value = parseInt(event.target.value) |
|||
return |
|||
} |
|||
|
|||
value = event.target.value |
|||
} |
|||
</script> |
|||
|
|||
{#if type === 'select'} |
|||
<Select thin secondary data-cy="{meta.name}-select" bind:value> |
|||
<option /> |
|||
{#each meta.constraints.inclusion as opt} |
|||
<option value={opt}>{opt}</option> |
|||
{/each} |
|||
</Select> |
|||
{:else if type === 'date'} |
|||
<Label small forAttr={'datepicker-label'}>{meta.name}</Label> |
|||
<DatePicker bind:value /> |
|||
{:else if type === 'file'} |
|||
<Label small forAttr={'dropzone-label'}>{meta.name}</Label> |
|||
<Dropzone bind:files={value} /> |
|||
{:else} |
|||
{#if type === 'checkbox'} |
|||
<label>{meta.name}</label> |
|||
{/if} |
|||
<Input |
|||
thin |
|||
placeholder={meta.name} |
|||
data-cy="{meta.name}-input" |
|||
checked={value} |
|||
{type} |
|||
{value} |
|||
on:change={handleInput} /> |
|||
{/if} |
|||
|
|||
<style> |
|||
label { |
|||
font-weight: 500; |
|||
font-size: var(--font-size-s); |
|||
margin-bottom: 12px; |
|||
} |
|||
</style> |
|||
@ -1,2 +0,0 @@ |
|||
export { default as DeleteRecordModal } from "./DeleteRecord.svelte" |
|||
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte" |
|||
@ -1,35 +0,0 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { |
|||
DropdownMenu, |
|||
TextButton as Button, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
} from "@budibase/bbui" |
|||
import { FIELDS } from "constants/backend" |
|||
import CreateEditColumn from "../modals/CreateEditColumn.svelte" |
|||
|
|||
let anchor |
|||
let dropdown |
|||
let fieldName |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<Button text small on:click={dropdown.show}> |
|||
<Icon name="addcolumn" /> |
|||
Create New Column |
|||
</Button> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
<h5>Create Column</h5> |
|||
<CreateEditColumn onClosed={dropdown.hide} /> |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
h5 { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
</style> |
|||
@ -1,103 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { backendUiStore } from "builderStore" |
|||
import { |
|||
DropdownMenu, |
|||
Button, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
Heading, |
|||
} from "@budibase/bbui" |
|||
import { FIELDS } from "constants/backend" |
|||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
|||
import DeleteRecordModal from "../modals/DeleteRecord.svelte" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
export let row |
|||
|
|||
let anchor |
|||
let dropdown |
|||
|
|||
let editing |
|||
|
|||
function showEditor() { |
|||
editing = true |
|||
} |
|||
|
|||
function hideEditor() { |
|||
dropdown.hide() |
|||
editing = false |
|||
close() |
|||
} |
|||
|
|||
const deleteRow = () => { |
|||
open( |
|||
DeleteRecordModal, |
|||
{ |
|||
onClosed: hideEditor, |
|||
record: row, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor} on:click={dropdown.show}> |
|||
<i class="ri-more-line" /> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
{#if editing} |
|||
<h5>Edit Row</h5> |
|||
<CreateEditRecord onClosed={hideEditor} record={row} /> |
|||
{:else} |
|||
<ul> |
|||
<li data-cy="edit-row" on:click={showEditor}> |
|||
<Icon name="edit" /> |
|||
<span>Edit</span> |
|||
</li> |
|||
<li data-cy="delete-row" on:click={deleteRow}> |
|||
<Icon name="delete" /> |
|||
<span>Delete</span> |
|||
</li> |
|||
</ul> |
|||
{/if} |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
.ri-more-line:hover { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
h5 { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
ul { |
|||
list-style: none; |
|||
padding-left: 0; |
|||
margin: 0; |
|||
padding: var(--spacing-s) 0; |
|||
} |
|||
|
|||
li { |
|||
display: flex; |
|||
font-family: var(--font-sans); |
|||
color: var(--ink); |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
margin: auto 0px; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
li:hover { |
|||
background-color: var(--grey-2); |
|||
} |
|||
|
|||
li:active { |
|||
color: var(--blue); |
|||
} |
|||
</style> |
|||
@ -1,71 +0,0 @@ |
|||
<script> |
|||
import { |
|||
TextButton, |
|||
Button, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
Popover, |
|||
} from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import api from "builderStore/api" |
|||
|
|||
const FORMATS = [ |
|||
{ |
|||
name: "CSV", |
|||
key: "csv", |
|||
}, |
|||
{ |
|||
name: "JSON", |
|||
key: "json", |
|||
}, |
|||
] |
|||
|
|||
export let view |
|||
|
|||
let anchor |
|||
let dropdown |
|||
let exportFormat |
|||
|
|||
async function exportView() { |
|||
const response = await api.post( |
|||
`/api/views/export?format=${exportFormat}`, |
|||
view |
|||
) |
|||
const downloadInfo = await response.json() |
|||
window.location = downloadInfo.url |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<TextButton text small on:click={dropdown.show}> |
|||
<Icon name="download" /> |
|||
Export |
|||
</TextButton> |
|||
</div> |
|||
<Popover bind:this={dropdown} {anchor} align="left"> |
|||
<h5>Export Format</h5> |
|||
<Select secondary thin bind:value={exportFormat}> |
|||
<option value={''}>Select an option</option> |
|||
{#each FORMATS as format} |
|||
<option value={format.key}>{format.name}</option> |
|||
{/each} |
|||
</Select> |
|||
<div class="button-group"> |
|||
<Button secondary on:click={dropdown.hide}>Cancel</Button> |
|||
<Button primary on:click={exportView}>Export</Button> |
|||
</div> |
|||
</Popover> |
|||
|
|||
<style> |
|||
h5 { |
|||
margin-top: 0; |
|||
} |
|||
|
|||
.button-group { |
|||
margin-top: var(--spacing-l); |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
} |
|||
</style> |
|||
@ -1,113 +0,0 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import Spinner from "components/common/Spinner.svelte" |
|||
import { |
|||
DropdownMenu, |
|||
Button, |
|||
Label, |
|||
Heading, |
|||
Icon, |
|||
Input, |
|||
Select, |
|||
Dropzone, |
|||
Spacer, |
|||
} from "@budibase/bbui" |
|||
import TableDataImport from "./TableDataImport.svelte" |
|||
import api from "builderStore/api" |
|||
import analytics from "analytics" |
|||
|
|||
let anchor |
|||
let dropdown |
|||
let name |
|||
let dataImport |
|||
let loading |
|||
|
|||
async function saveTable() { |
|||
loading = true |
|||
const model = await backendUiStore.actions.models.save({ |
|||
name, |
|||
schema: dataImport.schema || {}, |
|||
dataImport, |
|||
}) |
|||
notifier.success(`Table ${name} created successfully.`) |
|||
$goto(`./model/${model._id}`) |
|||
analytics.captureEvent("Table Created", { name }) |
|||
name = "" |
|||
dropdown.hide() |
|||
loading = false |
|||
} |
|||
|
|||
const onClosed = () => { |
|||
name = "" |
|||
dropdown.hide() |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<Button primary wide on:click={dropdown.show}>Create New Table</Button> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
<div class="container"> |
|||
<h5>Create Table</h5> |
|||
<Label grey extraSmall>Name</Label> |
|||
<Input |
|||
data-cy="table-name-input" |
|||
placeholder="Table Name" |
|||
thin |
|||
bind:value={name} /> |
|||
<Spacer medium /> |
|||
<Label grey extraSmall>Create Table from CSV (Optional)</Label> |
|||
<TableDataImport bind:dataImport /> |
|||
</div> |
|||
<footer> |
|||
<div class="button-margin-3"> |
|||
<Button secondary on:click={onClosed}>Cancel</Button> |
|||
</div> |
|||
<div class="button-margin-4"> |
|||
<Button |
|||
disabled={!name || (dataImport && !dataImport.valid)} |
|||
primary |
|||
on:click={saveTable}> |
|||
<span style={`margin-right: ${loading ? '10px' : 0};`}>Save</span> |
|||
{#if loading} |
|||
<Spinner size="10" /> |
|||
{/if} |
|||
</Button> |
|||
</div> |
|||
</footer> |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
h5 { |
|||
margin-bottom: var(--spacing-m); |
|||
margin-top: 0; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.container { |
|||
padding: var(--spacing-l); |
|||
margin: 0; |
|||
} |
|||
|
|||
footer { |
|||
padding: 20px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr; |
|||
gap: 20px; |
|||
background: var(--grey-1); |
|||
border-bottom-left-radius: 0.5rem; |
|||
border-bottom-left-radius: 0.5rem; |
|||
} |
|||
|
|||
.button-margin-3 { |
|||
grid-column-start: 3; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-4 { |
|||
grid-column-start: 4; |
|||
display: grid; |
|||
} |
|||
</style> |
|||
@ -1,137 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { backendUiStore } from "builderStore" |
|||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" |
|||
import { FIELDS } from "constants/backend" |
|||
import DeleteTableModal from "components/database/DataTable/modals/DeleteTable.svelte" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
|
|||
export let table |
|||
|
|||
let anchor |
|||
let dropdown |
|||
|
|||
let editing |
|||
|
|||
function showEditor() { |
|||
editing = true |
|||
} |
|||
|
|||
function hideEditor() { |
|||
dropdown.hide() |
|||
editing = false |
|||
close() |
|||
} |
|||
|
|||
const deleteTable = () => { |
|||
open( |
|||
DeleteTableModal, |
|||
{ |
|||
onClosed: close, |
|||
table, |
|||
}, |
|||
{ styleContent: { padding: "0" } } |
|||
) |
|||
} |
|||
|
|||
function save() { |
|||
backendUiStore.actions.models.save(table) |
|||
hideEditor() |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={anchor} on:click={dropdown.show}> |
|||
<i class="ri-more-line" /> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
{#if editing} |
|||
<h5>Edit Table</h5> |
|||
<div class="container"> |
|||
<Input placeholder="Table Name" thin bind:value={table.name} /> |
|||
</div> |
|||
<footer> |
|||
<div class="button-margin-3"> |
|||
<Button secondary on:click={hideEditor}>Cancel</Button> |
|||
</div> |
|||
<div class="button-margin-4"> |
|||
<Button primary on:click={save}>Save</Button> |
|||
</div> |
|||
</footer> |
|||
{:else} |
|||
<ul> |
|||
<li on:click={showEditor}> |
|||
<Icon name="edit" /> |
|||
Edit |
|||
</li> |
|||
<li data-cy="delete-table" on:click={deleteTable}> |
|||
<Icon name="delete" /> |
|||
Delete |
|||
</li> |
|||
</ul> |
|||
{/if} |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
h5 { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.container { |
|||
padding: var(--spacing-xl); |
|||
} |
|||
|
|||
ul { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
list-style: none; |
|||
padding-left: 0; |
|||
margin: 0; |
|||
padding: var(--spacing-s) 0; |
|||
} |
|||
|
|||
li { |
|||
display: flex; |
|||
font-family: var(--font-sans); |
|||
font-size: var(--font-size-xs); |
|||
color: var(--ink); |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
margin: auto 0px; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
li:hover { |
|||
background-color: var(--grey-2); |
|||
} |
|||
|
|||
li:active { |
|||
color: var(--blue); |
|||
} |
|||
|
|||
footer { |
|||
padding: 20px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr; |
|||
gap: 20px; |
|||
background: var(--grey-1); |
|||
border-bottom-left-radius: 0.5rem; |
|||
border-bottom-left-radius: 0.5rem; |
|||
} |
|||
|
|||
.button-margin-1 { |
|||
grid-column-start: 1; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-3 { |
|||
grid-column-start: 3; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-4 { |
|||
grid-column-start: 4; |
|||
display: grid; |
|||
} |
|||
</style> |
|||
@ -1,20 +0,0 @@ |
|||
<script> |
|||
import { isActive, url, goto } from "@sveltech/routify" |
|||
|
|||
export let label = "" |
|||
export let href |
|||
</script> |
|||
|
|||
<div |
|||
on:click={() => $goto(href)} |
|||
class="budibase__nav-item backend-nav-item" |
|||
class:selected={$isActive(href)}> |
|||
{label} |
|||
</div> |
|||
|
|||
<style> |
|||
.backend-nav-item { |
|||
padding-left: 25px; |
|||
cursor: pointer; |
|||
} |
|||
</style> |
|||
@ -1,99 +0,0 @@ |
|||
<script> |
|||
import { General, Users, DangerZone, APIKeys } from "./tabs" |
|||
|
|||
import { Input, TextArea, Button, Switcher } from "@budibase/bbui" |
|||
import { SettingsIcon, CloseIcon } from "components/common/Icons/" |
|||
import { getContext } from "svelte" |
|||
import { post } from "builderStore/api" |
|||
|
|||
const { open, close } = getContext("simple-modal") |
|||
export let name = "" |
|||
export let description = "" |
|||
const tabs = [ |
|||
{ |
|||
title: "General", |
|||
key: "GENERAL", |
|||
component: General, |
|||
}, |
|||
{ |
|||
title: "Users", |
|||
key: "USERS", |
|||
component: Users, |
|||
}, |
|||
{ |
|||
title: "API Keys", |
|||
key: "API_KEYS", |
|||
component: APIKeys, |
|||
}, |
|||
{ |
|||
title: "Danger Zone", |
|||
key: "DANGERZONE", |
|||
component: DangerZone, |
|||
}, |
|||
] |
|||
let value = "GENERAL" |
|||
$: selectedTab = tabs.find(tab => tab.key === value).component |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<div class="body"> |
|||
<div class="heading"> |
|||
<span class="icon"> |
|||
<SettingsIcon /> |
|||
</span> |
|||
<h3>Settings</h3> |
|||
</div> |
|||
<Switcher headings={tabs} bind:value> |
|||
<svelte:component this={selectedTab} /> |
|||
</Switcher> |
|||
</div> |
|||
<div class="close-button" on:click={close}> |
|||
<CloseIcon /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
position: relative; |
|||
height: 36rem; |
|||
} |
|||
|
|||
.close-button { |
|||
cursor: pointer; |
|||
position: absolute; |
|||
top: 20px; |
|||
right: 20px; |
|||
} |
|||
.close-button :global(svg) { |
|||
width: 24px; |
|||
height: 24px; |
|||
} |
|||
.heading { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
margin-bottom: 20px; |
|||
} |
|||
h3 { |
|||
margin: 0; |
|||
font-size: 24px; |
|||
font-weight: bold; |
|||
} |
|||
.icon { |
|||
display: grid; |
|||
border-radius: 3px; |
|||
align-content: center; |
|||
justify-content: center; |
|||
margin-right: 12px; |
|||
height: 20px; |
|||
width: 20px; |
|||
padding: 10px; |
|||
background-color: var(--blue-light); |
|||
color: var(--grey-7); |
|||
} |
|||
.body { |
|||
padding: 40px 40px 40px 40px; |
|||
display: grid; |
|||
grid-gap: 20px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,52 @@ |
|||
<script> |
|||
import { General, Users, DangerZone, APIKeys } from "./tabs" |
|||
import { Switcher, ModalContent } from "@budibase/bbui" |
|||
|
|||
const tabs = [ |
|||
{ |
|||
title: "General", |
|||
key: "GENERAL", |
|||
component: General, |
|||
}, |
|||
{ |
|||
title: "Users", |
|||
key: "USERS", |
|||
component: Users, |
|||
}, |
|||
{ |
|||
title: "API Keys", |
|||
key: "API_KEYS", |
|||
component: APIKeys, |
|||
}, |
|||
{ |
|||
title: "Danger Zone", |
|||
key: "DANGERZONE", |
|||
component: DangerZone, |
|||
}, |
|||
] |
|||
|
|||
let value = "GENERAL" |
|||
|
|||
$: selectedTab = tabs.find(tab => tab.key === value).component |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Settings" |
|||
showConfirmButton={false} |
|||
showCancelButton={false}> |
|||
<div class="container"> |
|||
<Switcher headings={tabs} bind:value> |
|||
<svelte:component this={selectedTab} /> |
|||
</Switcher> |
|||
</div> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.container :global(section > header) { |
|||
/* Fix margin defined in BBUI as L rather than XL */ |
|||
margin-bottom: var(--spacing-xl); |
|||
} |
|||
.container :global(textarea) { |
|||
min-height: 60px; |
|||
} |
|||
</style> |
|||
@ -1,60 +1,35 @@ |
|||
<script> |
|||
import Button from "components/common/Button.svelte" |
|||
import { TextButton } from "@budibase/bbui" |
|||
import { Heading } from "@budibase/bbui" |
|||
import { Spacer } from "@budibase/bbui" |
|||
export let name, _id |
|||
</script> |
|||
|
|||
<div class="apps-card"> |
|||
<h3 class="app-title">{name}</h3> |
|||
<Heading small black>{name}</Heading> |
|||
<Spacer medium /> |
|||
<div class="card-footer"> |
|||
<a href={`/_builder/${_id}`} class="app-button">Open {name}</a> |
|||
<TextButton text medium blue href="/_builder/{_id}"> |
|||
Open {name} → |
|||
</TextButton> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.apps-card { |
|||
background-color: var(--white); |
|||
padding: var(--spacing-xl); |
|||
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-xl) |
|||
var(--spacing-xl); |
|||
max-width: 300px; |
|||
max-height: 150px; |
|||
border-radius: var(--border-radius-m); |
|||
border: var(--border-dark); |
|||
} |
|||
|
|||
.app-button:hover { |
|||
background-color: var(--white); |
|||
color: var(--black); |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.app-title { |
|||
font-size: var(--font-size-l); |
|||
font-weight: 600; |
|||
color: var(--ink); |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
.card-footer { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: baseline; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.app-button { |
|||
align-items: center; |
|||
display: flex; |
|||
background-color: var(--ink); |
|||
color: var(--white); |
|||
border: 1.5px var(--ink) solid; |
|||
width: 100%; |
|||
justify-content: center; |
|||
padding: 8px 16px; |
|||
border-radius: var(--border-radius-s); |
|||
font-size: var(--font-size-xs); |
|||
font-weight: 500; |
|||
cursor: pointer; |
|||
transition: all 0.2s; |
|||
box-sizing: border-box; |
|||
font-family: var(--font-sans); |
|||
} |
|||
</style> |
|||
|
|||
@ -1,57 +1,25 @@ |
|||
<script> |
|||
import { store, backendUiStore } from "builderStore" |
|||
import { store } from "builderStore" |
|||
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte" |
|||
import PageLayout from "components/userInterface/PageLayout.svelte" |
|||
import PagesList from "components/userInterface/PagesList.svelte" |
|||
import NewScreen from "components/userInterface/NewScreen.svelte" |
|||
import NewScreenModal from "components/userInterface/NewScreenModal.svelte" |
|||
import { Button, Spacer, Modal } from "@budibase/bbui" |
|||
|
|||
const newScreen = () => { |
|||
newScreenPicker.show() |
|||
} |
|||
|
|||
let newScreenPicker |
|||
let modal |
|||
</script> |
|||
|
|||
<PagesList /> |
|||
|
|||
<button class="newscreen" on:click={newScreen}>Create New Screen</button> |
|||
|
|||
<Spacer medium /> |
|||
<Button primary wide on:click={modal.show}>Create New Screen</Button> |
|||
<Spacer medium /> |
|||
<PageLayout layout={$store.pages[$store.currentPageName]} /> |
|||
|
|||
<div class="nav-items-container"> |
|||
<ComponentsHierarchy screens={$store.screens} /> |
|||
</div> |
|||
|
|||
<NewScreen bind:this={newScreenPicker} /> |
|||
|
|||
<style> |
|||
.newscreen { |
|||
cursor: pointer; |
|||
border: 1px solid var(--purple); |
|||
border-radius: 5px; |
|||
width: 100%; |
|||
height: 36px; |
|||
padding: 8px 16px; |
|||
margin: 20px 0px 12px 0px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background: var(--purple); |
|||
color: var(--white); |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
transition: all 3ms; |
|||
outline: none; |
|||
} |
|||
|
|||
.newscreen:hover { |
|||
background: var(--purple-light); |
|||
color: var(--purple); |
|||
} |
|||
|
|||
.icon { |
|||
color: var(--ink); |
|||
font-size: 16px; |
|||
margin-right: 4px; |
|||
} |
|||
</style> |
|||
<Modal bind:this={modal}> |
|||
<NewScreenModal /> |
|||
</Modal> |
|||
|
|||
@ -1,117 +0,0 @@ |
|||
<script> |
|||
import { store } from "builderStore" |
|||
import { pipe } from "components/common/core" |
|||
import { isRootComponent } from "./pagesParsing/searchComponents" |
|||
import { splitName } from "./pagesParsing/splitRootComponentName.js" |
|||
import { Input, Select, Modal, Button, Spacer } from "@budibase/bbui" |
|||
|
|||
import { find, filter, some, map, includes } from "lodash/fp" |
|||
import { assign } from "lodash" |
|||
|
|||
export const show = () => { |
|||
dialog.show() |
|||
} |
|||
|
|||
let dialog |
|||
let layoutComponents |
|||
let layoutComponent |
|||
let screens |
|||
let name = "" |
|||
let routeError |
|||
|
|||
$: layoutComponents = Object.values($store.components).filter( |
|||
componentDefinition => componentDefinition.container |
|||
) |
|||
|
|||
$: layoutComponent = layoutComponent |
|||
? layoutComponents.find( |
|||
component => component._component === layoutComponent._component |
|||
) |
|||
: layoutComponents[0] |
|||
|
|||
$: route = !route && $store.screens.length === 0 ? "*" : route |
|||
|
|||
const save = () => { |
|||
if (!route) { |
|||
routeError = "Url is required" |
|||
} else { |
|||
if (routeNameExists(route)) { |
|||
routeError = "This url is already taken" |
|||
} else { |
|||
routeError = "" |
|||
} |
|||
} |
|||
|
|||
if (routeError) return false |
|||
|
|||
store.createScreen(name, route, layoutComponent._component) |
|||
name = "" |
|||
route = "" |
|||
dialog.hide() |
|||
} |
|||
|
|||
const cancel = () => { |
|||
dialog.hide() |
|||
} |
|||
|
|||
const routeNameExists = route => { |
|||
return $store.screens.some( |
|||
screen => screen.route.toLowerCase() === route.toLowerCase() |
|||
) |
|||
} |
|||
|
|||
const routeChanged = event => { |
|||
if (!event.target.value.startsWith("/")) { |
|||
route = "/" + event.target.value |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Modal bind:this={dialog} minWidth="500px"> |
|||
<h2>New Screen</h2> |
|||
<Spacer extraLarge /> |
|||
|
|||
<div data-cy="new-screen-dialog"> |
|||
<div class="bb-margin-xl"> |
|||
<Input label="Name" bind:value={name} /> |
|||
</div> |
|||
|
|||
<div class="bb-margin-xl"> |
|||
<Input |
|||
label="Url" |
|||
error={routeError} |
|||
bind:value={route} |
|||
on:change={routeChanged} /> |
|||
</div> |
|||
|
|||
<div class="bb-margin-xl"> |
|||
<label>Layout Component</label> |
|||
<Select bind:value={layoutComponent} secondary> |
|||
{#each layoutComponents as { _component, name }} |
|||
<option value={_component}>{name}</option> |
|||
{/each} |
|||
</Select> |
|||
</div> |
|||
</div> |
|||
|
|||
<Spacer extraLarge /> |
|||
|
|||
<div data-cy="create-screen-footer" class="modal-footer"> |
|||
<Button secondary medium on:click={cancel}>Cancel</Button> |
|||
<Button blue medium on:click={save}>Create Screen</Button> |
|||
</div> |
|||
</Modal> |
|||
|
|||
<style> |
|||
h2 { |
|||
font-size: var(--font-size-xl); |
|||
margin: 0; |
|||
font-family: var(--font-sans); |
|||
font-weight: 600; |
|||
} |
|||
|
|||
.modal-footer { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,68 @@ |
|||
<script> |
|||
import { store } from "builderStore" |
|||
import { Input, Select, ModalContent } from "@budibase/bbui" |
|||
import { find, filter, some } from "lodash/fp" |
|||
|
|||
let dialog |
|||
let layoutComponents |
|||
let layoutComponent |
|||
let screens |
|||
let name = "" |
|||
let routeError |
|||
|
|||
$: layoutComponents = Object.values($store.components).filter( |
|||
componentDefinition => componentDefinition.container |
|||
) |
|||
|
|||
$: layoutComponent = layoutComponent |
|||
? layoutComponents.find( |
|||
component => component._component === layoutComponent._component |
|||
) |
|||
: layoutComponents[0] |
|||
|
|||
$: route = !route && $store.screens.length === 0 ? "*" : route |
|||
|
|||
const save = () => { |
|||
if (!route) { |
|||
routeError = "Url is required" |
|||
} else { |
|||
if (routeNameExists(route)) { |
|||
routeError = "This url is already taken" |
|||
} else { |
|||
routeError = "" |
|||
} |
|||
} |
|||
if (routeError) { |
|||
return false |
|||
} |
|||
store.createScreen(name, route, layoutComponent._component) |
|||
name = "" |
|||
route = "" |
|||
} |
|||
|
|||
const routeNameExists = route => { |
|||
return $store.screens.some( |
|||
screen => screen.route.toLowerCase() === route.toLowerCase() |
|||
) |
|||
} |
|||
|
|||
const routeChanged = event => { |
|||
if (!event.target.value.startsWith("/")) { |
|||
route = "/" + event.target.value |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}> |
|||
<Input label="Name" bind:value={name} /> |
|||
<Input |
|||
label="Url" |
|||
error={routeError} |
|||
bind:value={route} |
|||
on:change={routeChanged} /> |
|||
<Select label="Layout Component" bind:value={layoutComponent} secondary> |
|||
{#each layoutComponents as { _component, name }} |
|||
<option value={_component}>{name}</option> |
|||
{/each} |
|||
</Select> |
|||
</ModalContent> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue