mirror of https://github.com/Budibase/budibase.git
149 changed files with 3576 additions and 11233 deletions
@ -1,70 +1,65 @@ |
|||||
context('Create a Table', () => { |
context("Create a Table", () => { |
||||
before(() => { |
before(() => { |
||||
cy.visit('localhost:4001/_builder') |
cy.visit("localhost:4001/_builder") |
||||
cy.createApp('Table App', 'Table App Description') |
cy.createApp("Table App", "Table App Description") |
||||
}) |
}) |
||||
|
|
||||
it('should create a new Table', () => { |
it("should create a new Table", () => { |
||||
cy.createTable('dog') |
cy.createTable("dog") |
||||
|
|
||||
// Check if Table exists
|
// Check if Table exists
|
||||
cy.get('.title').should('contain.text', 'dog') |
cy.get(".title span").should("have.text", "dog") |
||||
}) |
}) |
||||
|
|
||||
it('adds a new column to the table', () => { |
it("adds a new column to the table", () => { |
||||
cy.addColumn('dog', 'name', 'Plain Text') |
cy.addColumn("dog", "name", "Text") |
||||
|
cy.contains("name").should("be.visible") |
||||
cy.contains('name').should("be.visible") |
}) |
||||
}) |
|
||||
|
it("creates a record in the table", () => { |
||||
it('creates a record in the table', () => { |
cy.addRecord(["Rover"]) |
||||
cy.addRecord(["Rover"]) |
cy.contains("Rover").should("be.visible") |
||||
|
}) |
||||
cy.contains('Rover').should("be.visible") |
|
||||
}) |
it("updates a column on the table", () => { |
||||
|
cy.contains("name").click() |
||||
it('updates a column on the table', () => { |
cy.get("[data-cy='edit-column-header']").click() |
||||
cy.contains("name").click() |
cy.get(".actions input") |
||||
cy.get("[data-cy='edit-column-header']").click() |
.first() |
||||
|
.type("updated") |
||||
cy.get("[placeholder=Name]").type("updated") |
cy.get("select").select("Text") |
||||
cy.get("select").select("Plain Text") |
cy.contains("Save Column").click() |
||||
|
cy.contains("nameupdated").should("have.text", "nameupdated") |
||||
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() |
||||
it('edits a record', () => { |
cy.get(".modal input").type("Updated") |
||||
cy.get("tbody .ri-more-line").click() |
cy.contains("Save").click() |
||||
cy.get("[data-cy=edit-row]").click() |
cy.contains("RoverUpdated").should("have.text", "RoverUpdated") |
||||
cy.get(".actions input").type("Updated") |
}) |
||||
cy.contains("Save").click() |
|
||||
|
it("deletes a record", () => { |
||||
cy.contains('RoverUpdated').should('have.text', 'RoverUpdated') |
cy.get("tbody .ri-more-line").click() |
||||
}) |
cy.get("[data-cy=delete-row]").click() |
||||
|
cy.contains("Delete Row").click() |
||||
it('deletes a record', () => { |
cy.contains("RoverUpdated").should("not.exist") |
||||
cy.get("tbody .ri-more-line").click() |
}) |
||||
cy.get("[data-cy=delete-row]").click() |
|
||||
cy.get(".modal-actions").contains("Delete").click() |
it("deletes a column", () => { |
||||
|
cy.contains("name").click() |
||||
cy.contains('RoverUpdated').should('not.exist') |
cy.get("[data-cy='delete-column-header']").click() |
||||
}) |
cy.contains("Delete Column").click() |
||||
|
cy.contains("nameupdated").should("not.exist") |
||||
it('deletes a column', () => { |
}) |
||||
cy.contains("name").click() |
|
||||
cy.get("[data-cy='delete-column-header']").click() |
it("deletes a table", () => { |
||||
|
cy.contains("div", "dog") |
||||
cy.contains('nameupdated').should('not.exist') |
.get(".ri-more-line") |
||||
}) |
.click() |
||||
|
cy.get("[data-cy=delete-table]").click() |
||||
it('deletes a table', () => { |
cy.contains("Delete Table").click() |
||||
cy.contains("div", "dog").get(".ri-more-line").click() |
cy.contains("dog").should("not.exist") |
||||
cy.get("[data-cy=delete-table]").click() |
}) |
||||
cy.get(".modal-actions").contains("Delete").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> |
<script> |
||||
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte" |
import CreateEditColumnPopover from "../popovers/CreateEditColumnPopover.svelte" |
||||
|
|
||||
let anchor |
let anchor |
||||
let dropdown |
let dropdown |
||||
|
let fieldName |
||||
</script> |
</script> |
||||
|
|
||||
<div bind:this={anchor}> |
<div bind:this={anchor}> |
||||
<Button text small on:click={dropdown.show}> |
<Button text small on:click={dropdown.show}> |
||||
<Icon name="addrow" /> |
<Icon name="addcolumn" /> |
||||
Create New Row |
Create New Column |
||||
</Button> |
</Button> |
||||
</div> |
</div> |
||||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
||||
<h5>Add New Row</h5> |
<h5>Create Column</h5> |
||||
<CreateEditRecord onClosed={dropdown.hide} /> |
<CreateEditColumnPopover onClosed={dropdown.hide} /> |
||||
</DropdownMenu> |
</DropdownMenu> |
||||
|
|
||||
<style> |
<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> |
<script> |
||||
import { Modal, Button, Heading, Spacer } from "@budibase/bbui" |
import { Modal, ModalContent } from "@budibase/bbui" |
||||
|
|
||||
export let title = "" |
export let title = "" |
||||
export let body = "" |
export let body = "" |
||||
export let okText = "OK" |
export let okText = "Confirm" |
||||
export let cancelText = "Cancel" |
export let cancelText = "Cancel" |
||||
export let onOk = () => {} |
export let onOk = undefined |
||||
export let onCancel = () => {} |
export let onCancel = undefined |
||||
|
|
||||
|
let modal |
||||
|
|
||||
export const show = () => { |
export const show = () => { |
||||
theModal.show() |
modal.show() |
||||
} |
} |
||||
|
|
||||
export const hide = () => { |
export const hide = () => { |
||||
theModal.hide() |
modal.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() |
|
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
<Modal id={title} bind:this={theModal}> |
<Modal bind:this={modal} on:hide={onCancel}> |
||||
<h2>{title}</h2> |
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red> |
||||
<Spacer extraLarge /> |
<div class="body">{body}</div> |
||||
<slot class="rows">{body}</slot> |
</ModalContent> |
||||
<Spacer extraLarge /> |
|
||||
<div class="modal-footer"> |
|
||||
<Button red wide on:click={ok}>{okText}</Button> |
|
||||
<Button secondary wide on:click={cancel}>{cancelText}</Button> |
|
||||
</div> |
|
||||
</Modal> |
</Modal> |
||||
|
|
||||
<style> |
<style> |
||||
h2 { |
.body { |
||||
font-size: var(--font-size-xl); |
font-size: var(--font-size-s); |
||||
margin: 0; |
|
||||
font-family: var(--font-sans); |
|
||||
font-weight: 600; |
|
||||
} |
|
||||
|
|
||||
.modal-footer { |
|
||||
display: grid; |
|
||||
grid-gap: var(--spacing-s); |
|
||||
} |
} |
||||
</style> |
</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> |
<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 |
export let name, _id |
||||
</script> |
</script> |
||||
|
|
||||
<div class="apps-card"> |
<div class="apps-card"> |
||||
<h3 class="app-title">{name}</h3> |
<Heading small black>{name}</Heading> |
||||
|
<Spacer medium /> |
||||
<div class="card-footer"> |
<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> |
||||
</div> |
</div> |
||||
|
|
||||
<style> |
<style> |
||||
.apps-card { |
.apps-card { |
||||
background-color: var(--white); |
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-width: 300px; |
||||
max-height: 150px; |
max-height: 150px; |
||||
border-radius: var(--border-radius-m); |
border-radius: var(--border-radius-m); |
||||
border: var(--border-dark); |
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 { |
.card-footer { |
||||
display: flex; |
display: flex; |
||||
flex-direction: row; |
flex-direction: row; |
||||
align-items: baseline; |
align-items: baseline; |
||||
justify-content: space-between; |
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> |
</style> |
||||
|
|||||
@ -1,57 +1,25 @@ |
|||||
<script> |
<script> |
||||
import { store, backendUiStore } from "builderStore" |
import { store } from "builderStore" |
||||
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte" |
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte" |
||||
import PageLayout from "components/userInterface/PageLayout.svelte" |
import PageLayout from "components/userInterface/PageLayout.svelte" |
||||
import PagesList from "components/userInterface/PagesList.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 = () => { |
let modal |
||||
newScreenPicker.show() |
|
||||
} |
|
||||
|
|
||||
let newScreenPicker |
|
||||
</script> |
</script> |
||||
|
|
||||
<PagesList /> |
<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]} /> |
<PageLayout layout={$store.pages[$store.currentPageName]} /> |
||||
|
|
||||
<div class="nav-items-container"> |
<div class="nav-items-container"> |
||||
<ComponentsHierarchy screens={$store.screens} /> |
<ComponentsHierarchy screens={$store.screens} /> |
||||
</div> |
</div> |
||||
|
|
||||
<NewScreen bind:this={newScreenPicker} /> |
<Modal bind:this={modal}> |
||||
|
<NewScreenModal /> |
||||
<style> |
</Modal> |
||||
.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> |
|
||||
|
|||||
@ -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