Merge ag-grid backend UI and update styles
This commit is contained in:
commit
1a8da28c09
Binary file not shown.
Binary file not shown.
|
@ -22,8 +22,10 @@ context("Create a Table", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("updates a column on the table", () => {
|
it("updates a column on the table", () => {
|
||||||
cy.contains("name").click()
|
cy.contains("header", "name")
|
||||||
cy.get("[data-cy='edit-column-header']").click()
|
.trigger("mouseover")
|
||||||
|
.find(".ri-pencil-line")
|
||||||
|
.click({ force: true })
|
||||||
cy.get(".actions input").first().type("updated")
|
cy.get(".actions input").first().type("updated")
|
||||||
// Unset table display column
|
// Unset table display column
|
||||||
cy.contains("display column").click()
|
cy.contains("display column").click()
|
||||||
|
@ -32,24 +34,27 @@ context("Create a Table", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("edits a row", () => {
|
it("edits a row", () => {
|
||||||
cy.get("tbody .ri-more-line").click()
|
cy.get("button").contains("Edit").click()
|
||||||
cy.get("[data-cy=edit-row]").click()
|
|
||||||
cy.get(".modal input").type("Updated")
|
cy.get(".modal input").type("Updated")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
|
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("deletes a row", () => {
|
it("deletes a row", () => {
|
||||||
cy.get("tbody .ri-more-line").click()
|
cy.get(".ag-checkbox-input").check({ force: true })
|
||||||
cy.get("[data-cy=delete-row]").click()
|
cy.contains("Delete 1 row(s)").click()
|
||||||
cy.contains("Delete Row").click()
|
cy.get(".modal").contains("Delete").click()
|
||||||
cy.contains("RoverUpdated").should("not.exist")
|
cy.contains("RoverUpdated").should("not.exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("deletes a column", () => {
|
it("deletes a column", () => {
|
||||||
cy.contains("name").click()
|
cy.contains("header", "name")
|
||||||
cy.get("[data-cy='delete-column-header']").click()
|
.trigger("mouseover")
|
||||||
cy.contains("Delete Column").click()
|
.find(".ri-pencil-line")
|
||||||
|
.click({ force: true })
|
||||||
|
cy.contains("Delete").click()
|
||||||
|
cy.wait(50)
|
||||||
|
cy.get(".buttons").contains("Delete").click()
|
||||||
cy.contains("nameupdated").should("not.exist")
|
cy.contains("nameupdated").should("not.exist")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,12 @@ context("Create a View", () => {
|
||||||
cy.contains("Save View").click()
|
cy.contains("Save View").click()
|
||||||
})
|
})
|
||||||
cy.get(".table-title h1").contains("Test View")
|
cy.get(".table-title h1").contains("Test View")
|
||||||
cy.get("thead th div").should($headers => {
|
cy.get("[data-cy=table-header]").then($headers => {
|
||||||
expect($headers).to.have.length(3)
|
expect($headers).to.have.length(3)
|
||||||
const headers = $headers.map((i, header) => Cypress.$(header).text())
|
const headers = Array.from($headers).map(header =>
|
||||||
expect(headers.get()).to.deep.eq(["group", "age", "rating"])
|
header.textContent.trim()
|
||||||
|
)
|
||||||
|
expect(headers).to.deep.eq(["group", "age", "rating"])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -35,25 +37,29 @@ context("Create a View", () => {
|
||||||
cy.contains("Add Filter").click()
|
cy.contains("Add Filter").click()
|
||||||
cy.get(".menu-container").find("select").first().select("age")
|
cy.get(".menu-container").find("select").first().select("age")
|
||||||
cy.get(".menu-container").find("select").eq(1).select("More Than")
|
cy.get(".menu-container").find("select").eq(1).select("More Than")
|
||||||
cy.get("input").type(18)
|
cy.get(".menu-container").find("input").type(18)
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.get("tbody tr").should($values => {
|
cy.get("[role=rowgroup] .ag-row").get($values => {
|
||||||
expect($values).to.have.length(5)
|
expect($values).to.have.length(5)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("creates a stats calculation view based on age", () => {
|
it("creates a stats calculation view based on age", () => {
|
||||||
|
// Required due to responsive bug with ag grid in cypress
|
||||||
|
cy.viewport("macbook-15")
|
||||||
|
|
||||||
cy.contains("Calculate").click()
|
cy.contains("Calculate").click()
|
||||||
// we may reinstate this - have commented this dropdown for now as there is only one option
|
|
||||||
//cy.get(".menu-container").find("select").first().select("Statistics")
|
|
||||||
cy.get(".menu-container").find("select").eq(0).select("Statistics")
|
cy.get(".menu-container").find("select").eq(0).select("Statistics")
|
||||||
cy.wait(50)
|
cy.wait(50)
|
||||||
cy.get(".menu-container").find("select").eq(1).select("age")
|
cy.get(".menu-container").find("select").eq(1).select("age")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.get("thead th div").should($headers => {
|
cy.get(".ag-center-cols-viewport").scrollTo("100%")
|
||||||
|
cy.get("[data-cy=table-header]").then($headers => {
|
||||||
expect($headers).to.have.length(7)
|
expect($headers).to.have.length(7)
|
||||||
const headers = $headers.map((i, header) => Cypress.$(header).text())
|
const headers = Array.from($headers).map(header =>
|
||||||
expect(headers.get()).to.deep.eq([
|
header.textContent.trim()
|
||||||
|
)
|
||||||
|
expect(headers).to.deep.eq([
|
||||||
"field",
|
"field",
|
||||||
"sum",
|
"sum",
|
||||||
"min",
|
"min",
|
||||||
|
@ -63,33 +69,30 @@ context("Create a View", () => {
|
||||||
"avg",
|
"avg",
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
cy.get("tbody td").should($values => {
|
cy.get(".ag-cell").then($values => {
|
||||||
const values = $values.map((i, value) => Cypress.$(value).text())
|
const values = Array.from($values).map(header =>
|
||||||
expect(values.get()).to.deep.eq([
|
header.textContent.trim()
|
||||||
"age",
|
)
|
||||||
"155",
|
expect(values).to.deep.eq(["age", "155", "20", "49", "5", "5347", "31"])
|
||||||
"20",
|
|
||||||
"49",
|
|
||||||
"5",
|
|
||||||
"5347",
|
|
||||||
"31",
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("groups the view by group", () => {
|
it("groups the view by group", () => {
|
||||||
|
// Required due to responsive bug with ag grid in cypress
|
||||||
|
cy.viewport("macbook-15")
|
||||||
|
|
||||||
cy.contains("Group By").click()
|
cy.contains("Group By").click()
|
||||||
cy.get("select").select("group")
|
cy.get("select").select("group")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
|
cy.get(".ag-center-cols-viewport").scrollTo("100%")
|
||||||
cy.contains("Students").should("be.visible")
|
cy.contains("Students").should("be.visible")
|
||||||
cy.contains("Teachers").should("be.visible")
|
cy.contains("Teachers").should("be.visible")
|
||||||
|
|
||||||
cy.get("tbody tr")
|
cy.get(".ag-row[row-index=0]")
|
||||||
.first()
|
.find(".ag-cell")
|
||||||
.find("td")
|
.then($values => {
|
||||||
.should($values => {
|
const values = Array.from($values).map(value => value.textContent)
|
||||||
const values = $values.map((i, value) => Cypress.$(value).text())
|
expect(values).to.deep.eq([
|
||||||
expect(values.get()).to.deep.eq([
|
|
||||||
"Students",
|
"Students",
|
||||||
"70",
|
"70",
|
||||||
"20",
|
"20",
|
||||||
|
|
|
@ -31,9 +31,7 @@ Cypress.Commands.add("createApp", name => {
|
||||||
.then($body => {
|
.then($body => {
|
||||||
if ($body.find("input[name=apiKey]").length) {
|
if ($body.find("input[name=apiKey]").length) {
|
||||||
// input was found, do something else here
|
// input was found, do something else here
|
||||||
cy.get("input[name=apiKey]")
|
cy.get("input[name=apiKey]").type(name).should("have.value", name)
|
||||||
.type(name)
|
|
||||||
.should("have.value", name)
|
|
||||||
cy.contains("Next").click()
|
cy.contains("Next").click()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -44,12 +42,8 @@ Cypress.Commands.add("createApp", name => {
|
||||||
|
|
||||||
cy.contains("Next").click()
|
cy.contains("Next").click()
|
||||||
|
|
||||||
cy.get("input[name=username]")
|
cy.get("input[name=username]").click().type("test")
|
||||||
.click()
|
cy.get("input[name=password]").click().type("test")
|
||||||
.type("test")
|
|
||||||
cy.get("input[name=password]")
|
|
||||||
.click()
|
|
||||||
.type("test")
|
|
||||||
cy.contains("Submit").click()
|
cy.contains("Submit").click()
|
||||||
cy.get("[data-cy=new-table]", {
|
cy.get("[data-cy=new-table]", {
|
||||||
timeout: 20000,
|
timeout: 20000,
|
||||||
|
@ -67,12 +61,8 @@ Cypress.Commands.add("createTable", tableName => {
|
||||||
// Enter table name
|
// Enter table name
|
||||||
cy.get("[data-cy=new-table]").click()
|
cy.get("[data-cy=new-table]").click()
|
||||||
cy.get(".modal").within(() => {
|
cy.get(".modal").within(() => {
|
||||||
cy.get("input")
|
cy.get("input").first().type(tableName)
|
||||||
.first()
|
cy.get(".buttons").contains("Create").click()
|
||||||
.type(tableName)
|
|
||||||
cy.get(".buttons")
|
|
||||||
.contains("Create")
|
|
||||||
.click()
|
|
||||||
})
|
})
|
||||||
cy.contains(tableName).should("be.visible")
|
cy.contains(tableName).should("be.visible")
|
||||||
})
|
})
|
||||||
|
@ -83,10 +73,8 @@ Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
|
||||||
cy.contains("Create New Column").click()
|
cy.contains("Create New Column").click()
|
||||||
|
|
||||||
// Configure column
|
// Configure column
|
||||||
cy.get(".menu-container").within(() => {
|
cy.get(".actions").within(() => {
|
||||||
cy.get("input")
|
cy.get("input").first().type(columnName)
|
||||||
.first()
|
|
||||||
.type(columnName)
|
|
||||||
// Unset table display column
|
// Unset table display column
|
||||||
cy.contains("display column").click()
|
cy.contains("display column").click()
|
||||||
cy.get("select").select(type)
|
cy.get("select").select(type)
|
||||||
|
@ -99,15 +87,11 @@ Cypress.Commands.add("addRow", values => {
|
||||||
|
|
||||||
cy.get(".modal").within(() => {
|
cy.get(".modal").within(() => {
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
cy.get("input")
|
cy.get("input").eq(i).type(values[i])
|
||||||
.eq(i)
|
|
||||||
.type(values[i])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
cy.get(".buttons")
|
cy.get(".buttons").contains("Create").click()
|
||||||
.contains("Create")
|
|
||||||
.click()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -116,20 +100,12 @@ Cypress.Commands.add("createUser", (username, password, accessLevel) => {
|
||||||
cy.get(".toprightnav > .settings").click()
|
cy.get(".toprightnav > .settings").click()
|
||||||
cy.contains("Users").click()
|
cy.contains("Users").click()
|
||||||
|
|
||||||
cy.get("[name=Name]")
|
cy.get("[name=Name]").first().type(username)
|
||||||
.first()
|
cy.get("[name=Password]").first().type(password)
|
||||||
.type(username)
|
cy.get("select").first().select(accessLevel)
|
||||||
cy.get("[name=Password]")
|
|
||||||
.first()
|
|
||||||
.type(password)
|
|
||||||
cy.get("select")
|
|
||||||
.first()
|
|
||||||
.select(accessLevel)
|
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
cy.get(".inputs")
|
cy.get(".inputs").contains("Create").click()
|
||||||
.contains("Create")
|
|
||||||
.click()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("addHeadlineComponent", text => {
|
Cypress.Commands.add("addHeadlineComponent", text => {
|
||||||
|
@ -155,13 +131,9 @@ Cypress.Commands.add("navigateToFrontend", () => {
|
||||||
Cypress.Commands.add("createScreen", (screenName, route) => {
|
Cypress.Commands.add("createScreen", (screenName, route) => {
|
||||||
cy.get("[data-cy=new-screen]").click()
|
cy.get("[data-cy=new-screen]").click()
|
||||||
cy.get(".modal").within(() => {
|
cy.get(".modal").within(() => {
|
||||||
cy.get("input")
|
cy.get("input").eq(0).type(screenName)
|
||||||
.eq(0)
|
|
||||||
.type(screenName)
|
|
||||||
if (route) {
|
if (route) {
|
||||||
cy.get("input")
|
cy.get("input").eq(1).type(route)
|
||||||
.eq(1)
|
|
||||||
.type(route)
|
|
||||||
}
|
}
|
||||||
cy.contains("Create Screen").click()
|
cy.contains("Create Screen").click()
|
||||||
})
|
})
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
"@budibase/bbui": "^1.47.0",
|
"@budibase/bbui": "^1.47.0",
|
||||||
"@budibase/client": "^0.2.6",
|
"@budibase/client": "^0.2.6",
|
||||||
"@budibase/colorpicker": "^1.0.1",
|
"@budibase/colorpicker": "^1.0.1",
|
||||||
|
"@budibase/svelte-ag-grid": "^0.0.16",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@svelteschool/svelte-forms": "^0.7.0",
|
"@svelteschool/svelte-forms": "^0.7.0",
|
||||||
|
@ -76,6 +77,7 @@
|
||||||
"lodash": "^4.17.13",
|
"lodash": "^4.17.13",
|
||||||
"mustache": "^4.0.1",
|
"mustache": "^4.0.1",
|
||||||
"posthog-js": "1.4.5",
|
"posthog-js": "1.4.5",
|
||||||
|
"remixicon": "^2.5.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-loading-spinners": "^0.1.1",
|
"svelte-loading-spinners": "^0.1.1",
|
||||||
"svelte-portal": "^0.1.0",
|
"svelte-portal": "^0.1.0",
|
||||||
|
|
|
@ -178,6 +178,10 @@ export default {
|
||||||
src: "node_modules/@budibase/bbui/dist/bbui.css",
|
src: "node_modules/@budibase/bbui/dist/bbui.css",
|
||||||
dest: outputpath,
|
dest: outputpath,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
src: "node_modules/remixicon/fonts/*",
|
||||||
|
dest: outputpath,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
|
|
||||||
export let tableId
|
export let tableId
|
||||||
|
|
|
@ -1,48 +1,111 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@sveltech/routify"
|
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import fsort from "fast-sort"
|
import { goto, params } from "@sveltech/routify"
|
||||||
import getOr from "lodash/fp/getOr"
|
import AgGrid from "@budibase/svelte-ag-grid"
|
||||||
import { backendUiStore } from "builderStore"
|
|
||||||
import AttachmentList from "./AttachmentList.svelte"
|
import api from "builderStore/api"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte"
|
|
||||||
import EditRowPopover from "./popovers/RowPopover.svelte"
|
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||||
|
import { getRenderer, editRowRenderer } from "./cells/cellRenderers"
|
||||||
|
import TableLoadingOverlay from "./TableLoadingOverlay"
|
||||||
|
import TableHeader from "./TableHeader"
|
||||||
|
import "@budibase/svelte-ag-grid/dist/index.css"
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 10
|
export let schema = {}
|
||||||
|
|
||||||
export let schema = []
|
|
||||||
export let data = []
|
export let data = []
|
||||||
|
export let tableId
|
||||||
export let title
|
export let title
|
||||||
export let allowEditing = false
|
export let allowEditing = false
|
||||||
export let loading = false
|
export let loading = false
|
||||||
|
export let theme = "alpine"
|
||||||
|
|
||||||
let currentPage = 0
|
let columnDefs = []
|
||||||
|
let selectedRows = []
|
||||||
|
|
||||||
$: columns = schema ? Object.keys(schema) : []
|
let options = {
|
||||||
$: sort = $backendUiStore.sort
|
defaultColDef: {
|
||||||
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
|
flex: 1,
|
||||||
$: paginatedData =
|
filter: true,
|
||||||
sorted && sorted.length
|
},
|
||||||
? sorted.slice(
|
rowSelection: allowEditing ? "multiple" : false,
|
||||||
currentPage * ITEMS_PER_PAGE,
|
rowMultiSelectWithClick: true,
|
||||||
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
|
suppressRowClickSelection: false,
|
||||||
)
|
paginationAutoPageSize: true,
|
||||||
: []
|
pagination: true,
|
||||||
$: tableId = data?.length ? data[0].tableId : null
|
enableRangeSelection: true,
|
||||||
|
popupParent: document.body,
|
||||||
|
components: {
|
||||||
|
customLoadingOverlay: TableLoadingOverlay,
|
||||||
|
},
|
||||||
|
loadingOverlayComponent: "customLoadingOverlay",
|
||||||
|
animateRows: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
let result = []
|
||||||
|
if (allowEditing) {
|
||||||
|
result = [
|
||||||
|
{
|
||||||
|
pinned: "left",
|
||||||
|
headerName: "Edit",
|
||||||
|
sortable: false,
|
||||||
|
resizable: false,
|
||||||
|
suppressMovable: true,
|
||||||
|
suppressMenu: true,
|
||||||
|
minWidth: 84,
|
||||||
|
width: 84,
|
||||||
|
cellRenderer: editRowRenderer,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(schema || {}).forEach((key, idx) => {
|
||||||
|
result.push({
|
||||||
|
headerCheckboxSelection: false,
|
||||||
|
checkboxSelection: idx === 0 && allowEditing,
|
||||||
|
headerComponent: TableHeader,
|
||||||
|
headerComponentParams: {
|
||||||
|
field: schema[key],
|
||||||
|
editable: allowEditing,
|
||||||
|
},
|
||||||
|
headerName: key,
|
||||||
|
field: key,
|
||||||
|
sortable: true,
|
||||||
|
cellRenderer: getRenderer(schema[key], true),
|
||||||
|
cellRendererParams: {
|
||||||
|
selectRelationship,
|
||||||
|
},
|
||||||
|
autoHeight: true,
|
||||||
|
resizable: true,
|
||||||
|
minWidth: 200,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
columnDefs = result
|
||||||
|
}
|
||||||
|
|
||||||
function selectRelationship(row, fieldName) {
|
function selectRelationship(row, fieldName) {
|
||||||
if (!row?.[fieldName]?.length) {
|
if (!row?.[fieldName]?.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
$goto(
|
$goto(
|
||||||
`/${$params.application}/data/table/${tableId}/relationship/${row._id}/${fieldName}`
|
`/${$params.application}/data/table/${row.tableId}/relationship/${row._id}/${fieldName}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteRows = async () => {
|
||||||
|
await api.post(`/api/${tableId}/rows`, {
|
||||||
|
rows: selectedRows,
|
||||||
|
type: "delete",
|
||||||
|
})
|
||||||
|
data = data.filter(row => !selectedRows.includes(row))
|
||||||
|
notifier.success(`Successfully deleted ${selectedRows.length} rows`)
|
||||||
|
selectedRows = []
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="table-container">
|
<div>
|
||||||
<div class="table-title">
|
<div class="table-title">
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
|
@ -53,78 +116,22 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<slot />
|
<slot />
|
||||||
|
{#if selectedRows.length > 0}
|
||||||
|
<DeleteRowsButton {selectedRows} {deleteRows} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<table class="bb-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{#if allowEditing}
|
|
||||||
<th class="edit-header">
|
|
||||||
<div>Edit</div>
|
|
||||||
</th>
|
|
||||||
{/if}
|
|
||||||
{#each columns as header}
|
|
||||||
<th>
|
|
||||||
{#if allowEditing}
|
|
||||||
<ColumnHeaderPopover field={schema[header]} />
|
|
||||||
{:else}
|
|
||||||
<div class="header">{header}</div>
|
|
||||||
{/if}
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#if paginatedData.length === 0}
|
|
||||||
{#if allowEditing}
|
|
||||||
<td class="no-border">No data.</td>
|
|
||||||
{/if}
|
|
||||||
{#each columns as header, idx}
|
|
||||||
<td class="no-border">
|
|
||||||
{#if idx === 0 && !allowEditing}No data.{/if}
|
|
||||||
</td>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{#each paginatedData as row}
|
|
||||||
<tr>
|
|
||||||
{#if allowEditing}
|
|
||||||
<td>
|
|
||||||
<EditRowPopover {row} />
|
|
||||||
</td>
|
|
||||||
{/if}
|
|
||||||
{#each columns as header}
|
|
||||||
<td>
|
|
||||||
{#if schema[header].type === 'link'}
|
|
||||||
<div
|
|
||||||
class:link={row[header] && row[header].length}
|
|
||||||
on:click={() => selectRelationship(row, header)}>
|
|
||||||
{row[header] ? row[header].length : 0}
|
|
||||||
related row(s)
|
|
||||||
</div>
|
</div>
|
||||||
{:else if schema[header].type === 'attachment'}
|
<div class="grid-wrapper">
|
||||||
<AttachmentList files={row[header] || []} />
|
<AgGrid
|
||||||
{:else}{getOr('', header, row)}{/if}
|
{theme}
|
||||||
</td>
|
{options}
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<TablePagination
|
|
||||||
{data}
|
{data}
|
||||||
bind:currentPage
|
{columnDefs}
|
||||||
pageItemCount={paginatedData.length}
|
{loading}
|
||||||
{ITEMS_PER_PAGE} />
|
on:select={({ detail }) => (selectedRows = detail)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.table-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-title {
|
.table-title {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -150,62 +157,43 @@
|
||||||
}
|
}
|
||||||
.popovers :global(button) {
|
.popovers :global(button) {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
.popovers :global(button svg) {
|
.popovers :global(button svg) {
|
||||||
margin-right: var(--spacing-xs);
|
margin-right: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.grid-wrapper {
|
||||||
border: 1px solid var(--grey-4);
|
flex: 1 1 auto;
|
||||||
background: #fff;
|
display: flex;
|
||||||
border-collapse: collapse;
|
flex-direction: column;
|
||||||
margin-top: 0;
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.grid-wrapper :global(> *) {
|
||||||
|
height: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
:global(.ag-theme-alpine) {
|
||||||
background: var(--grey-3);
|
--ag-border-color: var(--grey-4);
|
||||||
border: 1px solid var(--grey-4);
|
--ag-header-background-color: var(--grey-1);
|
||||||
|
--ag-odd-row-background-color: var(--grey-1);
|
||||||
|
--ag-row-border-color: var(--grey-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th {
|
:global(.ag-menu) {
|
||||||
color: var(--ink);
|
border: var(--border-dark) !important;
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
transition: 0.5s all;
|
|
||||||
vertical-align: middle;
|
|
||||||
height: 48px;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th:hover {
|
:global(.ag-popup-child) {
|
||||||
color: var(--blue);
|
border-radius: var(--border-radius-m) !important;
|
||||||
cursor: pointer;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
:global(.ag-header-cell-text) {
|
||||||
text-transform: capitalize;
|
font-family: var(--font-sans);
|
||||||
}
|
font-weight: 600;
|
||||||
|
|
||||||
td {
|
|
||||||
max-width: 200px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--grey-4);
|
|
||||||
white-space: nowrap;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: var(--spacing-l) var(--spacing-m);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
td.no-border {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr {
|
|
||||||
border-bottom: 1px solid var(--grey-4);
|
|
||||||
transition: 0.3s background-color;
|
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,21 +201,59 @@
|
||||||
background: var(--grey-1);
|
background: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-header {
|
:global(.ag-filter) {
|
||||||
width: 60px;
|
padding: var(--spacing-s);
|
||||||
}
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
.edit-header:hover {
|
|
||||||
cursor: default;
|
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
background: #fff;
|
||||||
|
font-family: var(--font-sans) !important;
|
||||||
|
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
:global(.ag-menu) {
|
||||||
text-decoration: underline;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link:hover {
|
:global(.ag-simple-filter-body-wrapper > *) {
|
||||||
color: var(--grey-6);
|
margin-bottom: var(--spacing-m) !important;
|
||||||
cursor: pointer;
|
}
|
||||||
|
|
||||||
|
:global(.ag-select) {
|
||||||
|
height: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ag-menu input) {
|
||||||
|
color: var(--ink) !important;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
border-radius: var(--border-radius-s) !important;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--grey-2) !important;
|
||||||
|
padding: var(--spacing-m);
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
border: var(--border-transparent) !important;
|
||||||
|
transition: 0.2s all;
|
||||||
|
}
|
||||||
|
:global(.ag-menu input:focus) {
|
||||||
|
border: var(--border-blue) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ag-picker-field-display) {
|
||||||
|
color: var(--ink) !important;
|
||||||
|
font-size: var(--font-size-xs) !important;
|
||||||
|
border-radius: var(--border-radius-s) !important;
|
||||||
|
background-color: var(--grey-2) !important;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
border: var(--border-transparent) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ag-picker-field-wrapper) {
|
||||||
|
background: var(--grey-2) !important;
|
||||||
|
border: var(--border-transparent) !important;
|
||||||
|
padding-top: var(--spacing-xs);
|
||||||
|
padding-bottom: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { Modal, ModalContent } from "@budibase/bbui"
|
||||||
|
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||||
|
|
||||||
|
const SORT_ICON_MAP = {
|
||||||
|
asc: "ri-arrow-down-fill",
|
||||||
|
desc: "ri-arrow-up-fill",
|
||||||
|
}
|
||||||
|
|
||||||
|
export let field
|
||||||
|
export let displayName
|
||||||
|
export let column
|
||||||
|
export let enableSorting = true
|
||||||
|
export let showColumnMenu
|
||||||
|
export let progressSort
|
||||||
|
export let editable
|
||||||
|
|
||||||
|
let menuButton
|
||||||
|
let sortDirection = ""
|
||||||
|
let modal
|
||||||
|
let hovered
|
||||||
|
let filterActive
|
||||||
|
|
||||||
|
function toggleMenu() {
|
||||||
|
showColumnMenu(menuButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort(event) {
|
||||||
|
progressSort(event.shiftKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal() {
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSort() {
|
||||||
|
sortDirection = column.getSort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFilterActive(e) {
|
||||||
|
filterActive = e.column.filterActive
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
column.addEventListener("sortChanged", setSort)
|
||||||
|
column.addEventListener("filterActiveChanged", setFilterActive)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
column.removeEventListener("sortChanged", setSort)
|
||||||
|
column.removeEventListener("filterActiveChanged", setFilterActive)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header
|
||||||
|
on:click={onSort}
|
||||||
|
data-cy="table-header"
|
||||||
|
on:mouseover={() => (hovered = true)}
|
||||||
|
on:mouseleave={() => (hovered = false)}>
|
||||||
|
<div>
|
||||||
|
<span class="column-header-name">{displayName}</span>
|
||||||
|
<i class={`${SORT_ICON_MAP[sortDirection]} sort-icon`} />
|
||||||
|
</div>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<ModalContent
|
||||||
|
showCancelButton={false}
|
||||||
|
showConfirmButton={false}
|
||||||
|
title={`Edit Column: ${field.name}`}>
|
||||||
|
<CreateEditColumn onClosed={modal.hide} {field} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<section class:show={hovered || filterActive}>
|
||||||
|
{#if editable && hovered}
|
||||||
|
<span on:click|stopPropagation={showModal}>
|
||||||
|
<i class="ri-pencil-line" />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<span on:click|stopPropagation={toggleMenu} bind:this={menuButton}>
|
||||||
|
<i class="ri-filter-line" class:active={filterActive} />
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
header {
|
||||||
|
font-family: Inter;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.3s all;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-icon {
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
transition: 0.2s all;
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
i:hover {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.active,
|
||||||
|
i:hover {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
import TableHeader from "./TableHeader.svelte"
|
||||||
|
|
||||||
|
export default class TableHeaderWrapper {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
init(params) {
|
||||||
|
this.agParams = params
|
||||||
|
this.container = document.createElement("div")
|
||||||
|
this.container.style.height = "100%"
|
||||||
|
this.container.style.width = "100%"
|
||||||
|
|
||||||
|
this.headerComponent = new TableHeader({
|
||||||
|
target: this.container,
|
||||||
|
props: params,
|
||||||
|
})
|
||||||
|
this.gui = this.container
|
||||||
|
}
|
||||||
|
|
||||||
|
// can get called more than once, you should return the HTML element
|
||||||
|
getGui() {
|
||||||
|
return this.gui
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets called when a new Column Definition has been set for this header
|
||||||
|
refresh(params) {
|
||||||
|
this.agParams = params
|
||||||
|
this.headerComponent = new TableHeader({
|
||||||
|
target: this.container,
|
||||||
|
props: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional method, gets called once, when component is destroyed
|
||||||
|
destroy() {}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="ag-overlay-loading-center loading-container">
|
||||||
|
<div transition:fade class="loading-overlay">
|
||||||
|
<img
|
||||||
|
height="30"
|
||||||
|
width="30"
|
||||||
|
src="/_builder/assets/bb-logo.svg"
|
||||||
|
alt="Budibase icon" />
|
||||||
|
<span> Loading Your Data </span>
|
||||||
|
<Spinner size="12" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loading-overlay {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay > * {
|
||||||
|
margin-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,17 @@
|
||||||
|
import LoadingOverlay from "./LoadingOverlay.svelte"
|
||||||
|
|
||||||
|
export default class LoadingOverlayWrapper {
|
||||||
|
init(params) {
|
||||||
|
this.gui = document.createElement("div")
|
||||||
|
new LoadingOverlay({
|
||||||
|
target: this.gui,
|
||||||
|
props: {
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getGui() {
|
||||||
|
return this.gui
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,136 +0,0 @@
|
||||||
<script>
|
|
||||||
export let data
|
|
||||||
export let currentPage = 0
|
|
||||||
export let pageItemCount
|
|
||||||
export let ITEMS_PER_PAGE
|
|
||||||
|
|
||||||
let numPages = 0
|
|
||||||
$: numPages = Math.ceil((data?.length ?? 0) / ITEMS_PER_PAGE)
|
|
||||||
$: displayAllPages = numPages <= 10
|
|
||||||
$: pagesAroundCurrent = getPagesAroundCurrent(currentPage, numPages)
|
|
||||||
|
|
||||||
const next = () => {
|
|
||||||
if (currentPage + 1 === numPages) return
|
|
||||||
currentPage = currentPage + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const previous = () => {
|
|
||||||
if (currentPage == 0) return
|
|
||||||
currentPage = currentPage - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectPage = page => {
|
|
||||||
currentPage = page
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPagesAroundCurrent(current, max) {
|
|
||||||
const start = Math.max(current - 2, 1)
|
|
||||||
const end = Math.min(current + 2, max - 2)
|
|
||||||
let pages = []
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
pages.push(i)
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="pagination">
|
|
||||||
<div class="pagination__buttons">
|
|
||||||
<button on:click={previous} disabled={currentPage === 0}><</button>
|
|
||||||
{#if displayAllPages}
|
|
||||||
{#each Array(numPages) as _, idx}
|
|
||||||
<button
|
|
||||||
class:selected={idx === currentPage}
|
|
||||||
on:click={() => selectPage(idx)}>
|
|
||||||
{idx + 1}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<button class:selected={currentPage === 0} on:click={() => selectPage(0)}>
|
|
||||||
1
|
|
||||||
</button>
|
|
||||||
{#if currentPage > 3}<button disabled>...</button>{/if}
|
|
||||||
{#each pagesAroundCurrent as idx}
|
|
||||||
<button
|
|
||||||
class:selected={idx === currentPage}
|
|
||||||
on:click={() => selectPage(idx)}>
|
|
||||||
{idx + 1}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{#if currentPage < numPages - 4}<button disabled>...</button>{/if}
|
|
||||||
<button
|
|
||||||
class:selected={currentPage === numPages - 1}
|
|
||||||
on:click={() => selectPage(numPages - 1)}>
|
|
||||||
{numPages}
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button
|
|
||||||
on:click={next}
|
|
||||||
disabled={currentPage === numPages - 1 || numPages === 0}>
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{#if numPages > 1}
|
|
||||||
Showing
|
|
||||||
{ITEMS_PER_PAGE * currentPage + 1}
|
|
||||||
-
|
|
||||||
{ITEMS_PER_PAGE * currentPage + pageItemCount}
|
|
||||||
of
|
|
||||||
{data.length}
|
|
||||||
rows
|
|
||||||
{:else if numPages === 1}Showing all {data.length} row(s){/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.pagination {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination__buttons {
|
|
||||||
display: flex;
|
|
||||||
border: 1px solid var(--grey-4);
|
|
||||||
border-radius: var(--border-radius-s);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination__buttons button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: var(--spacing-s) 0;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
background: #fff;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
border-right: 1px solid var(--grey-4);
|
|
||||||
text-transform: capitalize;
|
|
||||||
min-width: 20px;
|
|
||||||
transition: 0.3s background-color;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
color: var(--grey-6);
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
.pagination__buttons button:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
.pagination__buttons button:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--grey-1);
|
|
||||||
}
|
|
||||||
.pagination__buttons button.selected {
|
|
||||||
background: var(--blue);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
margin: var(--spacing-xl) 0;
|
|
||||||
color: var(--grey-6);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,27 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
|
import {
|
||||||
import CreateEditColumnPopover from "../popovers/CreateEditColumnPopover.svelte"
|
DropdownMenu,
|
||||||
|
TextButton as Button,
|
||||||
|
Icon,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||||
|
|
||||||
let anchor
|
let modal
|
||||||
let dropdown
|
|
||||||
let fieldName
|
let fieldName
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div>
|
||||||
<Button text small on:click={dropdown.show}>
|
<Button text small on:click={modal.show}>
|
||||||
<Icon name="addcolumn" />
|
<Icon name="addcolumn" />
|
||||||
Create New Column
|
Create New Column
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
<Modal bind:this={modal}>
|
||||||
<h5>Create Column</h5>
|
<ModalContent
|
||||||
<CreateEditColumnPopover onClosed={dropdown.hide} />
|
showCancelButton={false}
|
||||||
</DropdownMenu>
|
showConfirmButton={false}
|
||||||
|
title={'Create Column'}>
|
||||||
<style>
|
<CreateEditColumn onClosed={modal.hide} />
|
||||||
h5 {
|
</ModalContent>
|
||||||
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
|
</Modal>
|
||||||
margin: 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script>
|
||||||
|
import { TextButton, Icon } from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
export let selectedRows
|
||||||
|
export let deleteRows
|
||||||
|
|
||||||
|
let modal
|
||||||
|
|
||||||
|
async function confirmDeletion() {
|
||||||
|
await deleteRows()
|
||||||
|
modal.hide()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextButton small text on:click={modal.show}>
|
||||||
|
<Icon name="delete" />
|
||||||
|
Delete
|
||||||
|
{selectedRows.length}
|
||||||
|
row(s)
|
||||||
|
</TextButton>
|
||||||
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={modal}
|
||||||
|
okText="Delete"
|
||||||
|
onOk={confirmDeletion}
|
||||||
|
title="Confirm Delete">
|
||||||
|
Are you sure you want to delete
|
||||||
|
{selectedRows.length}
|
||||||
|
row{selectedRows.length > 1 ? 's' : ''}?
|
||||||
|
</ConfirmDialog>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script>
|
||||||
|
export let columnName
|
||||||
|
export let row
|
||||||
|
export let selectRelationship
|
||||||
|
|
||||||
|
$: count =
|
||||||
|
row && columnName && Array.isArray(row[columnName])
|
||||||
|
? row[columnName].length
|
||||||
|
: 0
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class:link={count} on:click={() => selectRelationship(row, columnName)}>
|
||||||
|
{count}
|
||||||
|
related row(s)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
color: var(--grey-6);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,79 @@
|
||||||
|
import AttachmentList from "./AttachmentCell.svelte"
|
||||||
|
import EditRow from "../modals/EditRow.svelte"
|
||||||
|
import DeleteRow from "../modals/DeleteRow.svelte"
|
||||||
|
import RelationshipDisplay from "./RelationshipCell.svelte"
|
||||||
|
|
||||||
|
const renderers = {
|
||||||
|
attachment: attachmentRenderer,
|
||||||
|
link: linkedRowRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRenderer(schema, editable) {
|
||||||
|
if (renderers[schema.type]) {
|
||||||
|
return renderers[schema.type](schema.options, schema.constraints, editable)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteRowRenderer(params) {
|
||||||
|
const container = document.createElement("div")
|
||||||
|
|
||||||
|
new DeleteRow({
|
||||||
|
target: container,
|
||||||
|
props: {
|
||||||
|
row: params.data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editRowRenderer(params) {
|
||||||
|
const container = document.createElement("div")
|
||||||
|
|
||||||
|
new EditRow({
|
||||||
|
target: container,
|
||||||
|
props: {
|
||||||
|
row: params.data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
function attachmentRenderer(options, constraints, editable) {
|
||||||
|
return params => {
|
||||||
|
const container = document.createElement("div")
|
||||||
|
|
||||||
|
const attachmentInstance = new AttachmentList({
|
||||||
|
target: container,
|
||||||
|
props: {
|
||||||
|
files: params.value || [],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
function linkedRowRenderer() {
|
||||||
|
return params => {
|
||||||
|
let container = document.createElement("div")
|
||||||
|
container.style.display = "grid"
|
||||||
|
container.style.height = "100%"
|
||||||
|
|
||||||
|
new RelationshipDisplay({
|
||||||
|
target: container,
|
||||||
|
props: {
|
||||||
|
row: params.data,
|
||||||
|
columnName: params.column.colId,
|
||||||
|
selectRelationship: params.selectRelationship,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Button, Select, Toggle } from "@budibase/bbui"
|
import { Input, Button, TextButton, Select, Toggle } from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { backendUiStore } from "builderStore"
|
import { backendUiStore } from "builderStore"
|
||||||
import { FIELDS } from "constants/backend"
|
import { FIELDS } from "constants/backend"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ValuesList from "components/common/ValuesList.svelte"
|
import ValuesList from "components/common/ValuesList.svelte"
|
||||||
import DatePicker from "components/common/DatePicker.svelte"
|
import DatePicker from "components/common/DatePicker.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
let fieldDefinitions = cloneDeep(FIELDS)
|
let fieldDefinitions = cloneDeep(FIELDS)
|
||||||
|
|
||||||
|
@ -21,6 +23,9 @@
|
||||||
let primaryDisplay =
|
let primaryDisplay =
|
||||||
$backendUiStore.selectedTable.primaryDisplay == null ||
|
$backendUiStore.selectedTable.primaryDisplay == null ||
|
||||||
$backendUiStore.selectedTable.primaryDisplay === field.name
|
$backendUiStore.selectedTable.primaryDisplay === field.name
|
||||||
|
let confirmDeleteDialog
|
||||||
|
let deletion
|
||||||
|
|
||||||
$: tableOptions = $backendUiStore.tables.filter(
|
$: tableOptions = $backendUiStore.tables.filter(
|
||||||
table => table._id !== $backendUiStore.draftTable._id
|
table => table._id !== $backendUiStore.draftTable._id
|
||||||
)
|
)
|
||||||
|
@ -38,6 +43,16 @@
|
||||||
onClosed()
|
onClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteColumn() {
|
||||||
|
if (field.name === $backendUiStore.selectedTable.primaryDisplay) {
|
||||||
|
notifier.danger("You cannot delete the display column")
|
||||||
|
} else {
|
||||||
|
backendUiStore.actions.tables.deleteField(field)
|
||||||
|
notifier.success(`Column ${field.name} deleted.`)
|
||||||
|
onClosed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleFieldConstraints(event) {
|
function handleFieldConstraints(event) {
|
||||||
const { type, constraints } = fieldDefinitions[
|
const { type, constraints } = fieldDefinitions[
|
||||||
event.target.value.toUpperCase()
|
event.target.value.toUpperCase()
|
||||||
|
@ -59,9 +74,19 @@
|
||||||
field.constraints.presence = { allowEmpty: false }
|
field.constraints.presence = { allowEmpty: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function confirmDelete() {
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
deletion = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDeleteDialog() {
|
||||||
|
confirmDeleteDialog.hide()
|
||||||
|
deletion = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions" class:hidden={deletion}>
|
||||||
<Input label="Name" thin bind:value={field.name} />
|
<Input label="Name" thin bind:value={field.name} />
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
@ -131,15 +156,24 @@
|
||||||
thin
|
thin
|
||||||
bind:value={field.fieldName} />
|
bind:value={field.fieldName} />
|
||||||
{/if}
|
{/if}
|
||||||
<footer>
|
<footer class="create-column-options">
|
||||||
|
{#if originalName}
|
||||||
|
<TextButton text on:click={confirmDelete}>Delete Column</TextButton>
|
||||||
|
{/if}
|
||||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
<Button primary on:click={saveColumn}>Save Column</Button>
|
<Button primary on:click={saveColumn}>Save Column</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDeleteDialog}
|
||||||
|
body={`Are you sure you wish to delete this column? Your data will be deleted and this action cannot be undone.`}
|
||||||
|
okText="Delete Column"
|
||||||
|
onOk={deleteColumn}
|
||||||
|
onCancel={hideDeleteDialog}
|
||||||
|
title="Confirm Delete" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.actions {
|
.actions {
|
||||||
padding: var(--spacing-xl);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: var(--spacing-xl);
|
grid-gap: var(--spacing-xl);
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
|
@ -150,4 +184,12 @@
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.create-column-options button:first-child) {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
import { backendUiStore } from "builderStore"
|
||||||
|
import * as api from "../api"
|
||||||
|
import { notifier } from "builderStore/store/notifications"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
let confirmDeleteDialog
|
||||||
|
|
||||||
|
function showDelete() {
|
||||||
|
confirmDeleteDialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRow() {
|
||||||
|
await api.deleteRow(row)
|
||||||
|
notifier.success("Row deleted")
|
||||||
|
backendUiStore.actions.rows.delete(row)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div on:click={showDelete}><i class="ri-delete-row" /></div>
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ri-delete-bin-line:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, Button } from "@budibase/bbui"
|
||||||
|
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
let modal
|
||||||
|
|
||||||
|
function showModal(e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
modal.show()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button translucent small on:click={showModal}>Edit</Button>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateEditRowModal {row} />
|
||||||
|
</Modal>
|
|
@ -1,145 +0,0 @@
|
||||||
<script>
|
|
||||||
import { backendUiStore } from "builderStore"
|
|
||||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
|
||||||
import { FIELDS } from "constants/backend"
|
|
||||||
import CreateEditColumnPopover from "./CreateEditColumnPopover.svelte"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import { notifier } from "../../../../builderStore/store/notifications"
|
|
||||||
|
|
||||||
export let field
|
|
||||||
|
|
||||||
let anchor
|
|
||||||
let dropdown
|
|
||||||
let editing
|
|
||||||
let confirmDeleteDialog
|
|
||||||
|
|
||||||
$: sortColumn = $backendUiStore.sort && $backendUiStore.sort.column
|
|
||||||
$: sortDirection = $backendUiStore.sort && $backendUiStore.sort.direction
|
|
||||||
$: type = field?.type
|
|
||||||
|
|
||||||
function showEditor() {
|
|
||||||
editing = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideEditor() {
|
|
||||||
dropdown.hide()
|
|
||||||
editing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDelete() {
|
|
||||||
dropdown.hide()
|
|
||||||
confirmDeleteDialog.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteColumn() {
|
|
||||||
if (field.name === $backendUiStore.selectedTable.primaryDisplay) {
|
|
||||||
notifier.danger("You cannot delete the display column")
|
|
||||||
} else {
|
|
||||||
backendUiStore.actions.tables.deleteField(field)
|
|
||||||
notifier.success("Column deleted")
|
|
||||||
}
|
|
||||||
hideEditor()
|
|
||||||
}
|
|
||||||
|
|
||||||
function sort(direction, column) {
|
|
||||||
backendUiStore.update(state => {
|
|
||||||
if (direction !== "none") {
|
|
||||||
state.sort = { direction, column }
|
|
||||||
} else {
|
|
||||||
state.sort = undefined
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
hideEditor()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container" bind:this={anchor} on:click={dropdown.show}>
|
|
||||||
<span>{field.name}</span>
|
|
||||||
<Icon name="arrowdown" />
|
|
||||||
</div>
|
|
||||||
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
|
||||||
{#if editing}
|
|
||||||
<h5>Edit Column</h5>
|
|
||||||
<CreateEditColumnPopover onClosed={hideEditor} {field} />
|
|
||||||
{:else}
|
|
||||||
<ul>
|
|
||||||
{#if type !== 'link'}
|
|
||||||
<li data-cy="edit-column-header" on:click={showEditor}>
|
|
||||||
<Icon name="edit" />
|
|
||||||
Edit
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
<li data-cy="delete-column-header" on:click={showDelete}>
|
|
||||||
<Icon name="delete" />
|
|
||||||
Delete
|
|
||||||
</li>
|
|
||||||
{#if sortDirection === 'desc' || sortDirection === 'asc'}
|
|
||||||
<li on:click={() => sort('none', field.name)}>
|
|
||||||
<Icon name="close" />
|
|
||||||
Remove sort
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#if sortDirection === 'desc' || sortColumn !== field.name}
|
|
||||||
<li on:click={() => sort('asc', field.name)}>
|
|
||||||
<Icon name="sortascending" />
|
|
||||||
Sort A - Z
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
{#if sortDirection === 'asc' || sortColumn !== field.name}
|
|
||||||
<li on:click={() => sort('desc', field.name)}>
|
|
||||||
<Icon name="sortdescending" />
|
|
||||||
Sort Z - A
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</DropdownMenu>
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={confirmDeleteDialog}
|
|
||||||
body={`Are you sure you wish to delete this column? Your data will be deleted and this action cannot be undone.`}
|
|
||||||
okText="Delete Column"
|
|
||||||
onOk={deleteColumn}
|
|
||||||
title="Confirm Delete" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
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,65 +0,0 @@
|
||||||
<script>
|
|
||||||
import { backendUiStore } from "builderStore"
|
|
||||||
import { DropdownMenu, Modal } from "@budibase/bbui"
|
|
||||||
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
|
|
||||||
import * as api from "../api"
|
|
||||||
import { notifier } from "builderStore/store/notifications"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
|
|
||||||
|
|
||||||
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.deleteRow(row)
|
|
||||||
notifier.success("Row deleted")
|
|
||||||
backendUiStore.actions.rows.delete(row)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor} on:click={dropdown.show}>
|
|
||||||
<i class="ri-more-line" />
|
|
||||||
</div>
|
|
||||||
<DropdownMenu bind:this={dropdown} {anchor} align="left">
|
|
||||||
<DropdownContainer>
|
|
||||||
<DropdownItem
|
|
||||||
icon="ri-edit-line"
|
|
||||||
title="Edit"
|
|
||||||
on:click={showModal}
|
|
||||||
data-cy="edit-row" />
|
|
||||||
<DropdownItem
|
|
||||||
icon="ri-delete-bin-line"
|
|
||||||
title="Delete"
|
|
||||||
on:click={showDelete}
|
|
||||||
data-cy="delete-row" />
|
|
||||||
</DropdownContainer>
|
|
||||||
</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}>
|
|
||||||
<CreateEditRowModal {row} />
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.ri-more-line:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -7,8 +7,6 @@
|
||||||
|
|
||||||
<title>Budibase Builder</title>
|
<title>Budibase Builder</title>
|
||||||
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<link rel='icon' type='image/png' href='/_builder/favicon.png'>
|
<link rel='icon' type='image/png' href='/_builder/favicon.png'>
|
||||||
<link rel='stylesheet' href='/_builder/bundle.css'>
|
<link rel='stylesheet' href='/_builder/bundle.css'>
|
||||||
<link rel='stylesheet' href='/_builder/external.css'>
|
<link rel='stylesheet' href='/_builder/external.css'>
|
||||||
|
|
|
@ -10,6 +10,8 @@ import "/assets/Inter-ExtraBold"
|
||||||
import "/assets/Inter-Black"
|
import "/assets/Inter-Black"
|
||||||
import "/_builder/assets/budibase-logo.png"
|
import "/_builder/assets/budibase-logo.png"
|
||||||
import "/_builder/assets/budibase-logo-only.png"
|
import "/_builder/assets/budibase-logo-only.png"
|
||||||
|
import "remixicon/fonts/remixicon.css"
|
||||||
|
|
||||||
import App from "./App.svelte"
|
import App from "./App.svelte"
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore } from "builderStore"
|
|
||||||
import * as api from "components/backend/DataTable/api"
|
|
||||||
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -25,6 +23,11 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding: var(--spacing-l) 40px;
|
padding: var(--spacing-l) 40px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-l);
|
||||||
}
|
}
|
||||||
.nav {
|
.nav {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
@ -3,10 +3,6 @@
|
||||||
import { goto, leftover } from "@sveltech/routify"
|
import { goto, leftover } from "@sveltech/routify"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
async function selectTable(table) {
|
|
||||||
backendUiStore.actions.tables.select(table)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// navigate to first table in list, if not already selected
|
// navigate to first table in list, if not already selected
|
||||||
// and this is the final url (i.e. no selectedTable)
|
// and this is the final url (i.e. no selectedTable)
|
||||||
|
@ -20,13 +16,4 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.root {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -722,6 +722,13 @@
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
|
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
|
||||||
|
|
||||||
|
"@budibase/svelte-ag-grid@^0.0.16":
|
||||||
|
version "0.0.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@budibase/svelte-ag-grid/-/svelte-ag-grid-0.0.16.tgz#1b91dc1e27dad034b827dc7b258fa16d3d3bf68f"
|
||||||
|
integrity sha512-Yxnfe03Mo7VhuB4wJSGNoc8jaorH9lertptPt2halef9Z93kkYwdwpldnWVzQT07YdX6soPaVLhupxKrI5Hvtw==
|
||||||
|
dependencies:
|
||||||
|
ag-grid-community "^24.0.0"
|
||||||
|
|
||||||
"@cnakazawa/watch@^1.0.3":
|
"@cnakazawa/watch@^1.0.3":
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
||||||
|
@ -1297,6 +1304,11 @@ acorn@^7.1.1:
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe"
|
||||||
|
|
||||||
|
ag-grid-community@^24.0.0:
|
||||||
|
version "24.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-24.1.0.tgz#1e3cab51211822e08d56f03a491b7c0deaa398e6"
|
||||||
|
integrity sha512-pWnWphuDcejZ8ahf6C734EpCx3XQ6dHEZWMWTlCdHNT0mZBLJ4YKCGACX+ttAEtSX2MGM3G13JncvuratUlYag==
|
||||||
|
|
||||||
ajv@^6.5.5:
|
ajv@^6.5.5:
|
||||||
version "6.12.2"
|
version "6.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
|
||||||
|
@ -5096,6 +5108,11 @@ regjsparser@^0.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
jsesc "~0.5.0"
|
jsesc "~0.5.0"
|
||||||
|
|
||||||
|
remixicon@^2.5.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/remixicon/-/remixicon-2.5.0.tgz#b5e245894a1550aa23793f95daceadbf96ad1a41"
|
||||||
|
integrity sha512-q54ra2QutYDZpuSnFjmeagmEiN9IMo56/zz5dDNitzKD23oFRw77cWo4TsrAdmdkPiEn8mxlrTqxnkujDbEGww==
|
||||||
|
|
||||||
remove-trailing-separator@^1.0.1:
|
remove-trailing-separator@^1.0.1:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,201 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { cssVars } from "./cssVars"
|
|
||||||
import ArrowUp from "./icons/ArrowUp.svelte"
|
|
||||||
import ArrowDown from "./icons/ArrowDown.svelte"
|
|
||||||
import fsort from "fast-sort"
|
|
||||||
import fetchData from "./fetchData.js"
|
|
||||||
import { isEmpty } from "lodash/fp"
|
|
||||||
import AttachmentList from "./attachments/AttachmentList.svelte"
|
|
||||||
|
|
||||||
export let backgroundColor
|
|
||||||
export let color
|
|
||||||
export let stripeColor
|
|
||||||
export let borderColor
|
|
||||||
export let datasource
|
|
||||||
export let _bb
|
|
||||||
|
|
||||||
let data = []
|
|
||||||
let headers = []
|
|
||||||
let sort = {}
|
|
||||||
let sorted = []
|
|
||||||
let schema = {}
|
|
||||||
let store = _bb.store
|
|
||||||
|
|
||||||
$: cssVariables = {
|
|
||||||
backgroundColor,
|
|
||||||
color,
|
|
||||||
stripeColor,
|
|
||||||
borderColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
$: sorted = sort.direction ? fsort(data)[sort.direction](sort.column) : data
|
|
||||||
|
|
||||||
async function fetchTable(tableId) {
|
|
||||||
const FETCH_TABLE_URL = `/api/tables/${tableId}`
|
|
||||||
const response = await _bb.api.get(FETCH_TABLE_URL)
|
|
||||||
const table = await response.json()
|
|
||||||
return table.schema
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!isEmpty(datasource)) {
|
|
||||||
data = await fetchData(datasource, $store)
|
|
||||||
|
|
||||||
// Get schema for datasource
|
|
||||||
// Views with "Calculate" applied provide their own schema.
|
|
||||||
// For everything else, use the tableId property to pull to table schema
|
|
||||||
if (datasource.schema) {
|
|
||||||
schema = datasource.schema
|
|
||||||
headers = Object.keys(schema).filter(shouldDisplayField)
|
|
||||||
} else {
|
|
||||||
schema = await fetchTable(datasource.tableId)
|
|
||||||
headers = Object.keys(schema).filter(shouldDisplayField)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const shouldDisplayField = name => {
|
|
||||||
if (name.startsWith("_")) return false
|
|
||||||
// always 'row'
|
|
||||||
if (name === "type") return false
|
|
||||||
// tables are always tied to a single tableId, this is irrelevant
|
|
||||||
if (name === "tableId") return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortColumn(column) {
|
|
||||||
if (column === sort.column) {
|
|
||||||
sort = {
|
|
||||||
direction: sort.direction === "asc" ? "desc" : null,
|
|
||||||
column: sort.direction === "asc" ? sort.column : null,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sort = {
|
|
||||||
column,
|
|
||||||
direction: "asc",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<table use:cssVars={cssVariables}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
{#each headers as header}
|
|
||||||
<th on:click={() => sortColumn(header)}>
|
|
||||||
<span>
|
|
||||||
{header}
|
|
||||||
{#if sort.column === header}
|
|
||||||
<svelte:component
|
|
||||||
this={sort.direction === 'asc' ? ArrowDown : ArrowUp}
|
|
||||||
style="height: 1em;" />
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</th>
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{#each sorted as row (row._id)}
|
|
||||||
<tr>
|
|
||||||
{#each headers as header}
|
|
||||||
{#if schema[header] !== undefined}
|
|
||||||
<!-- Rudimentary solution for attachments on array given this entire table will be replaced by AG Grid -->
|
|
||||||
{#if schema[header] && schema[header].type === 'attachment'}
|
|
||||||
<AttachmentList files={row[header]} />
|
|
||||||
{:else if schema[header] && schema[header].type === 'link'}
|
|
||||||
<td>{row[header] ? row[header].length : 0} related row(s)</td>
|
|
||||||
{:else}
|
|
||||||
<td>{row[header] == null ? '' : row[header]}</td>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
overflow: scroll; /* Scrollbar are always visible */
|
|
||||||
overflow: auto; /* Scrollbar is displayed as it's needed */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Zebra striping */
|
|
||||||
tr:nth-of-type(odd) {
|
|
||||||
background: var(--stripeColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: var(--backgroundColor);
|
|
||||||
color: var(--color);
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: capitalize;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
th span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid var(--borderColor);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 760px),
|
|
||||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force table to not be like tables anymore */
|
|
||||||
table,
|
|
||||||
thead,
|
|
||||||
tbody,
|
|
||||||
th,
|
|
||||||
td,
|
|
||||||
tr {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide table headers (but not display: none;, for accessibility) */
|
|
||||||
thead tr {
|
|
||||||
position: absolute;
|
|
||||||
top: -9999px;
|
|
||||||
left: -9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr {
|
|
||||||
border: 1px solid var(--borderColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
/* Behave like a "row" */
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:before {
|
|
||||||
/* Now like a table header */
|
|
||||||
position: absolute;
|
|
||||||
/* Top/left values mimic padding */
|
|
||||||
top: 6px;
|
|
||||||
left: 6px;
|
|
||||||
width: 45%;
|
|
||||||
padding-right: 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
/* Label the data */
|
|
||||||
content: attr(data-column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
Reference in New Issue