forked from tsai/budibase
113 changed files with 2914 additions and 1146 deletions
@ -1,32 +1,63 @@ |
|||
<script> |
|||
import { params } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import * as api from "./api" |
|||
import Table from "./Table.svelte" |
|||
import EditIntegrationConfigButton from "./buttons/EditIntegrationConfigButton.svelte" |
|||
import CreateQueryButton from "components/backend/DataTable/buttons/CreateQueryButton.svelte" |
|||
|
|||
export let datasourceId |
|||
export let query = {} |
|||
|
|||
let data = [] |
|||
let loading = false |
|||
let error = false |
|||
|
|||
$: table = $backendUiStore.selectedTable |
|||
$: title = table.name |
|||
$: schema = table.schema |
|||
$: tableView = { |
|||
schema, |
|||
name: $backendUiStore.selectedView.name, |
|||
} |
|||
$: datasourceId = $params.selectedDatasource |
|||
// TODO: refactor |
|||
// $: query = $backendUiStore.datasources.find( |
|||
// ds => ds._id === $params.selectedDatasource |
|||
// ).queries[$params.query] |
|||
$: title = query.name |
|||
$: schema = query.schema |
|||
|
|||
// Fetch rows for specified table |
|||
$: { |
|||
if ($backendUiStore.selectedView?.name?.startsWith("all_")) { |
|||
async function fetchData() { |
|||
try { |
|||
loading = true |
|||
api.fetchDataForView($backendUiStore.selectedView).then(rows => { |
|||
data = rows || [] |
|||
loading = false |
|||
}) |
|||
const rows = await api.fetchDataForQuery( |
|||
$params.selectedDatasource, |
|||
$params.query |
|||
) |
|||
data = rows || [] |
|||
error = false |
|||
} catch (err) { |
|||
console.log(err) |
|||
error = `${query}: Query error. (${err.message}). This could be a problem with your datasource configuration.` |
|||
notifier.danger(error) |
|||
} finally { |
|||
loading = false |
|||
} |
|||
} |
|||
|
|||
// Fetch rows for specified query |
|||
$: { |
|||
fetchData() |
|||
} |
|||
</script> |
|||
|
|||
{#if error} |
|||
<div class="errors">{error}</div> |
|||
{/if} |
|||
<Table {title} {schema} {data} {loading}> |
|||
<EditIntegrationConfigButton {table} /> |
|||
<CreateQueryButton {query} /> |
|||
</Table> |
|||
|
|||
<style> |
|||
.errors { |
|||
color: var(--red); |
|||
background: var(--red-light); |
|||
padding: var(--spacing-m); |
|||
border-radius: var(--border-radius-m); |
|||
margin-bottom: var(--spacing-m); |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,47 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import { |
|||
DropdownMenu, |
|||
TextButton as Button, |
|||
Icon, |
|||
Label, |
|||
Modal, |
|||
ModalContent, |
|||
TextArea, |
|||
} from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte" |
|||
import CreateEditQuery from "components/backend/DataTable/modals/CreateEditQuery.svelte" |
|||
|
|||
export let datasource |
|||
|
|||
let modal |
|||
let query = {} |
|||
let fields = [] |
|||
|
|||
async function saveQuery() { |
|||
try { |
|||
await backendUiStore.actions.datasources.saveQuery(datasource._id, query) |
|||
} catch (err) { |
|||
console.error(err) |
|||
// TODO: notifier |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<Button text small on:click={modal.show}> |
|||
<Icon name="filter" /> |
|||
Create Query |
|||
</Button> |
|||
</div> |
|||
<Modal bind:this={modal}> |
|||
<ModalContent |
|||
confirmText="Save" |
|||
cancelText="Cancel" |
|||
onConfirm={saveQuery} |
|||
title="Create New Query"> |
|||
<CreateEditQuery {datasource} bind:query /> |
|||
</ModalContent> |
|||
</Modal> |
|||
@ -0,0 +1,41 @@ |
|||
<script> |
|||
import { |
|||
DropdownMenu, |
|||
TextButton as Button, |
|||
Icon, |
|||
Modal, |
|||
ModalContent, |
|||
} from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
import api from "builderStore/api" |
|||
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte" |
|||
|
|||
export let table |
|||
|
|||
let modal |
|||
|
|||
// TODO: revisit |
|||
async function saveTable() { |
|||
const SAVE_TABLE_URL = `/api/tables` |
|||
const response = await api.post(SAVE_TABLE_URL, table) |
|||
const savedTable = await response.json() |
|||
await backendUiStore.actions.tables.fetch() |
|||
backendUiStore.actions.tables.select(savedTable) |
|||
} |
|||
</script> |
|||
|
|||
<div> |
|||
<Button text small on:click={modal.show}> |
|||
<Icon name="edit" /> |
|||
Configure Schema |
|||
</Button> |
|||
</div> |
|||
<Modal bind:this={modal}> |
|||
<ModalContent |
|||
confirmText="Save" |
|||
cancelText="Cancel" |
|||
onConfirm={saveTable} |
|||
title={'Datasource Configuration'}> |
|||
<EditIntegrationConfig onClosed={modal.hide} bind:table /> |
|||
</ModalContent> |
|||
</Modal> |
|||
@ -0,0 +1,23 @@ |
|||
<script> |
|||
import { TextButton as Button, Modal } from "@budibase/bbui" |
|||
import EditRolesModal from "../modals/EditRoles.svelte" |
|||
|
|||
let modal |
|||
</script> |
|||
|
|||
<div> |
|||
<Button text small on:click={modal.show}> |
|||
<i class="ri-lock-line" /> |
|||
Edit Roles |
|||
</Button> |
|||
</div> |
|||
<Modal bind:this={modal}> |
|||
<EditRolesModal /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
i { |
|||
margin-right: var(--spacing-xs); |
|||
font-size: var(--font-size-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,10 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
export let roleId |
|||
|
|||
$: role = $backendUiStore.roles.find(role => role._id === roleId) |
|||
$: roleName = role?.name ?? "Unknown role" |
|||
</script> |
|||
|
|||
<div>{roleName}</div> |
|||
@ -0,0 +1,190 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { |
|||
Select, |
|||
Button, |
|||
Label, |
|||
Input, |
|||
TextArea, |
|||
Heading, |
|||
Spacer, |
|||
Switcher, |
|||
} from "@budibase/bbui" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import api from "builderStore/api" |
|||
import { FIELDS } from "constants/backend" |
|||
import IntegrationQueryEditor from "components/integration/index.svelte" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
const PREVIEW_HEADINGS = [ |
|||
{ |
|||
title: "Preview", |
|||
key: "PREVIEW", |
|||
}, |
|||
{ |
|||
title: "Schema", |
|||
key: "SCHEMA", |
|||
}, |
|||
] |
|||
|
|||
export let datasource |
|||
export let query |
|||
export let fields = [] |
|||
|
|||
let config = {} |
|||
let queryType |
|||
let previewTab = "PREVIEW" |
|||
let preview |
|||
|
|||
$: query.schema = fields.reduce( |
|||
(acc, next) => ({ |
|||
...acc, |
|||
[next.name]: { |
|||
name: next.name, |
|||
type: "string", |
|||
}, |
|||
}), |
|||
{} |
|||
) |
|||
|
|||
function newField() { |
|||
fields = [...fields, {}] |
|||
} |
|||
|
|||
function deleteField(idx) { |
|||
fields.splice(idx, 1) |
|||
fields = fields |
|||
} |
|||
|
|||
async function fetchQueryConfig() { |
|||
try { |
|||
const response = await api.get(`/api/integrations/${datasource.source}`) |
|||
const json = await response.json() |
|||
config = json.query |
|||
} catch (err) { |
|||
// TODO: Error fetching integration config |
|||
// notifier.danger() |
|||
console.error(err) |
|||
} |
|||
} |
|||
|
|||
async function previewQuery() { |
|||
try { |
|||
const response = await api.post(`/api/datasources/queries/preview`, { |
|||
type: datasource.source, |
|||
config: datasource.config, |
|||
query: query.queryString, |
|||
}) |
|||
const json = await response.json() |
|||
if (response.status !== 200) { |
|||
throw new Error(json.message) |
|||
} |
|||
preview = json[0] || {} |
|||
|
|||
// TODO: refactor |
|||
fields = Object.keys(preview).map(field => ({ |
|||
name: field, |
|||
type: "STRING", |
|||
})) |
|||
} catch (err) { |
|||
notifier.danger(`Query Error: ${err.message}`) |
|||
console.error(err) |
|||
} |
|||
} |
|||
|
|||
onMount(() => { |
|||
fetchQueryConfig() |
|||
}) |
|||
</script> |
|||
|
|||
<section> |
|||
<div class="config"> |
|||
<h6>Datasource Type</h6> |
|||
<span>{datasource.source}</span> |
|||
|
|||
<Spacer medium /> |
|||
|
|||
<Label extraSmall grey>Query Name</Label> |
|||
<Input type="text" thin bind:value={query.name} /> |
|||
|
|||
<Spacer medium /> |
|||
|
|||
<Label extraSmall grey>Query Type</Label> |
|||
<Select secondary bind:value={queryType}> |
|||
<option value={''}>Select an option</option> |
|||
{#each Object.keys(config) as queryType} |
|||
<option value={queryType}>{queryType}</option> |
|||
{/each} |
|||
</Select> |
|||
|
|||
<Spacer medium /> |
|||
|
|||
<IntegrationQueryEditor {queryType} bind:query={query.queryString} /> |
|||
|
|||
<Spacer small /> |
|||
|
|||
<Button thin secondary on:click={previewQuery}>Preview Query</Button> |
|||
|
|||
<Spacer small /> |
|||
|
|||
{#if preview} |
|||
<Switcher headings={PREVIEW_HEADINGS} bind:value={previewTab}> |
|||
{#if previewTab === 'PREVIEW'} |
|||
<pre class="preview">{JSON.stringify(preview, undefined, 2)}</pre> |
|||
{:else if previewTab === 'SCHEMA'} |
|||
{#each fields as field, idx} |
|||
<div class="field"> |
|||
<Input thin type={'text'} bind:value={field.name} /> |
|||
<Select secondary thin bind:value={field.type}> |
|||
<option value={''}>Select an option</option> |
|||
<option value={'STRING'}>Text</option> |
|||
<option value={'NUMBER'}>Number</option> |
|||
<option value={'BOOLEAN'}>Boolean</option> |
|||
<option value={'DATETIME'}>Datetime</option> |
|||
</Select> |
|||
<i |
|||
class="ri-close-circle-line delete" |
|||
on:click={() => deleteField(idx)} /> |
|||
</div> |
|||
{/each} |
|||
<Button thin secondary on:click={newField}>Add Field</Button> |
|||
{/if} |
|||
</Switcher> |
|||
{/if} |
|||
</div> |
|||
</section> |
|||
|
|||
<style> |
|||
.field { |
|||
display: grid; |
|||
grid-gap: 10px; |
|||
grid-template-columns: 1fr 1fr 50px; |
|||
margin-bottom: var(--spacing-m); |
|||
} |
|||
|
|||
h6 { |
|||
font-family: var(--font-sans); |
|||
font-weight: 600; |
|||
text-rendering: var(--text-render); |
|||
color: var(--ink); |
|||
font-size: var(--heading-font-size-xs); |
|||
color: var(--ink); |
|||
margin-bottom: var(--spacing-xs); |
|||
margin-top: var(--spacing-xs); |
|||
} |
|||
|
|||
.config { |
|||
margin-bottom: var(--spacing-s); |
|||
} |
|||
|
|||
.delete { |
|||
align-self: center; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.preview { |
|||
width: 800px; |
|||
overflow-wrap: break-word; |
|||
white-space: pre-wrap; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,85 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import RowFieldControl from "../RowFieldControl.svelte" |
|||
import * as backendApi from "../api" |
|||
import { ModalContent, Select } from "@budibase/bbui" |
|||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|||
|
|||
export let row = {} |
|||
|
|||
let errors = [] |
|||
|
|||
$: creating = row?._id == null |
|||
$: table = row.tableId |
|||
? $backendUiStore.tables.find(table => table._id === row?.tableId) |
|||
: $backendUiStore.selectedTable |
|||
$: tableSchema = getUserSchema(table) |
|||
$: customSchemaKeys = getCustomSchemaKeys(tableSchema) |
|||
|
|||
const getUserSchema = table => { |
|||
let schema = table?.schema ?? {} |
|||
if (schema.username) { |
|||
schema.username.name = "Username" |
|||
} |
|||
return schema |
|||
} |
|||
|
|||
const getCustomSchemaKeys = schema => { |
|||
let customSchema = { ...schema } |
|||
delete customSchema["email"] |
|||
delete customSchema["roleId"] |
|||
return Object.entries(customSchema) |
|||
} |
|||
|
|||
const saveRow = async () => { |
|||
const rowResponse = await backendApi.saveRow( |
|||
{ ...row, tableId: table._id }, |
|||
table._id |
|||
) |
|||
|
|||
if (rowResponse.errors) { |
|||
if (Array.isArray(rowResponse.errors)) { |
|||
errors = rowResponse.errors.map(error => ({ message: error })) |
|||
} else { |
|||
errors = Object.entries(rowResponse.errors) |
|||
.map(([key, error]) => ({ dataPath: key, message: error })) |
|||
.flat() |
|||
} |
|||
return false |
|||
} |
|||
|
|||
notifier.success("User saved successfully.") |
|||
backendUiStore.actions.rows.save(rowResponse) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title={creating ? 'Create User' : 'Edit User'} |
|||
confirmText={creating ? 'Create User' : 'Save User'} |
|||
onConfirm={saveRow}> |
|||
<ErrorsBox {errors} /> |
|||
<RowFieldControl |
|||
meta={{ ...tableSchema.email, name: 'Email' }} |
|||
bind:value={row.email} |
|||
readonly={!creating} /> |
|||
<RowFieldControl |
|||
meta={{ name: 'password', type: 'password' }} |
|||
bind:value={row.password} /> |
|||
<!-- Defer rendering this select until roles load, otherwise the initial |
|||
selection is always undefined --> |
|||
<Select |
|||
thin |
|||
secondary |
|||
label="Role" |
|||
data-cy="roleId-select" |
|||
bind:value={row.roleId}> |
|||
<option value="">Choose an option</option> |
|||
{#each $backendUiStore.roles as role} |
|||
<option value={role._id}>{role.name}</option> |
|||
{/each} |
|||
</Select> |
|||
{#each customSchemaKeys as [key, meta]} |
|||
<RowFieldControl {meta} bind:value={row[key]} {creating} /> |
|||
{/each} |
|||
</ModalContent> |
|||
@ -0,0 +1,132 @@ |
|||
<script> |
|||
import { ModalContent, Select, Input, Button } from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import api from "builderStore/api" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
let permissions = [] |
|||
let selectedRole = {} |
|||
let errors = [] |
|||
$: selectedRoleId = selectedRole._id |
|||
$: otherRoles = $backendUiStore.roles.filter( |
|||
role => role._id !== selectedRoleId |
|||
) |
|||
$: isCreating = selectedRoleId == null || selectedRoleId === "" |
|||
|
|||
const fetchPermissions = async () => { |
|||
const permissionsResponse = await api.get("/api/permissions") |
|||
permissions = await permissionsResponse.json() |
|||
} |
|||
|
|||
// Changes the selected role |
|||
const changeRole = event => { |
|||
const id = event?.target?.value |
|||
const role = $backendUiStore.roles.find(role => role._id === id) |
|||
if (role) { |
|||
selectedRole = { |
|||
...role, |
|||
inherits: role.inherits ?? "", |
|||
permissionId: role.permissionId ?? "", |
|||
} |
|||
} else { |
|||
selectedRole = { _id: "", inherits: "", permissionId: "" } |
|||
} |
|||
errors = [] |
|||
} |
|||
|
|||
// Saves or creates the selected role |
|||
const saveRole = async () => { |
|||
errors = [] |
|||
|
|||
// Clean up empty strings |
|||
const keys = ["_id", "inherits", "permissionId"] |
|||
keys.forEach(key => { |
|||
if (selectedRole[key] === "") { |
|||
delete selectedRole[key] |
|||
} |
|||
}) |
|||
|
|||
// Validation |
|||
if (!selectedRole.name || selectedRole.name.trim() === "") { |
|||
errors.push({ message: "Please enter a role name" }) |
|||
} |
|||
if (!selectedRole.permissionId) { |
|||
errors.push({ message: "Please choose permissions" }) |
|||
} |
|||
if (errors.length) { |
|||
return false |
|||
} |
|||
|
|||
// Save/create the role |
|||
const response = await backendUiStore.actions.roles.save(selectedRole) |
|||
if (response.status === 200) { |
|||
notifier.success("Role saved successfully.") |
|||
} else { |
|||
notifier.danger("Error saving role.") |
|||
return false |
|||
} |
|||
} |
|||
|
|||
// Deletes the selected role |
|||
const deleteRole = async () => { |
|||
const response = await backendUiStore.actions.roles.delete(selectedRole) |
|||
if (response.status === 200) { |
|||
changeRole() |
|||
notifier.success("Role deleted successfully.") |
|||
} else { |
|||
notifier.danger("Error deleting role.") |
|||
} |
|||
} |
|||
|
|||
onMount(fetchPermissions) |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Edit Roles" |
|||
confirmText={isCreating ? 'Create' : 'Save'} |
|||
onConfirm={saveRole}> |
|||
{#if errors.length} |
|||
<ErrorsBox {errors} /> |
|||
{/if} |
|||
<Select |
|||
thin |
|||
secondary |
|||
label="Role" |
|||
value={selectedRoleId} |
|||
on:change={changeRole}> |
|||
<option value="">Create new role</option> |
|||
{#each $backendUiStore.roles as role} |
|||
<option value={role._id}>{role.name}</option> |
|||
{/each} |
|||
</Select> |
|||
{#if selectedRole} |
|||
<Input label="Name" bind:value={selectedRole.name} thin /> |
|||
<Select |
|||
thin |
|||
secondary |
|||
label="Inherits Role" |
|||
bind:value={selectedRole.inherits}> |
|||
<option value="">None</option> |
|||
{#each otherRoles as role} |
|||
<option value={role._id}>{role.name}</option> |
|||
{/each} |
|||
</Select> |
|||
<Select |
|||
thin |
|||
secondary |
|||
label="Permissions" |
|||
bind:value={selectedRole.permissionId}> |
|||
<option value="">Choose permissions</option> |
|||
{#each permissions as permission} |
|||
<option value={permission._id}>{permission.name}</option> |
|||
{/each} |
|||
</Select> |
|||
{/if} |
|||
<div slot="footer"> |
|||
{#if !isCreating} |
|||
<Button red on:click={deleteRole}>Delete</Button> |
|||
{/if} |
|||
</div> |
|||
</ModalContent> |
|||
@ -0,0 +1,85 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
import { goto } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
import { TableNames } from "constants" |
|||
import CreateDatasourceModal from "./modals/CreateDatasourceModal.svelte" |
|||
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte" |
|||
import { Modal, Switcher } from "@budibase/bbui" |
|||
import NavItem from "components/common/NavItem.svelte" |
|||
|
|||
let modal |
|||
|
|||
$: selectedView = |
|||
$backendUiStore.selectedView && $backendUiStore.selectedView.name |
|||
|
|||
function selectDatasource(datasource) { |
|||
// You can't actually select a datasource, just edit it |
|||
backendUiStore.actions.datasources.select(datasource) |
|||
$goto(`./datasource/${datasource._id}`) |
|||
} |
|||
|
|||
function onClickQuery(datasourceId, queryId) { |
|||
if ($backendUiStore.selectedQueryId === queryId) { |
|||
return |
|||
} |
|||
backendUiStore.actions.queries.select(queryId) |
|||
$goto(`./datasource/${datasourceId}/${queryId}`) |
|||
} |
|||
|
|||
onMount(() => { |
|||
backendUiStore.actions.datasources.fetch() |
|||
}) |
|||
</script> |
|||
|
|||
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id} |
|||
<div class="title"> |
|||
<i |
|||
data-cy="new-datasource" |
|||
on:click={modal.show} |
|||
class="ri-add-circle-fill" /> |
|||
</div> |
|||
<div class="hierarchy-items-container"> |
|||
{#each $backendUiStore.datasources as datasource, idx} |
|||
<NavItem |
|||
border={idx > 0} |
|||
icon={'ri-database-2-line'} |
|||
text={datasource.name} |
|||
selected={$backendUiStore.selectedDatasourceId === datasource._id} |
|||
on:click={() => selectDatasource(datasource)}> |
|||
<EditDatasourcePopover {datasource} /> |
|||
</NavItem> |
|||
{#each Object.keys(datasource.queries) as queryId} |
|||
<NavItem |
|||
indentLevel={1} |
|||
icon="ri-eye-line" |
|||
text={datasource.queries[queryId].name} |
|||
selected={selectedView === queryId} |
|||
on:click={() => onClickQuery(datasource._id, queryId)}> |
|||
<!-- <EditViewPopover |
|||
view={{ name: viewName, ...table.views[viewName] }} /> --> |
|||
</NavItem> |
|||
{/each} |
|||
{/each} |
|||
</div> |
|||
{/if} |
|||
<Modal bind:this={modal}> |
|||
<CreateDatasourceModal /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.title { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.title i { |
|||
font-size: 20px; |
|||
} |
|||
.title i:hover { |
|||
cursor: pointer; |
|||
color: var(--blue); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,55 @@ |
|||
<script> |
|||
export let icon |
|||
export let className |
|||
export let title |
|||
export let selected |
|||
export let indented |
|||
</script> |
|||
|
|||
<div |
|||
data-cy="table-nav-item" |
|||
class:indented |
|||
class:selected |
|||
on:click |
|||
class={className}> |
|||
<i class={icon} /> |
|||
<span>{title}</span> |
|||
<slot /> |
|||
</div> |
|||
|
|||
<style> |
|||
.indented { |
|||
grid-template-columns: 46px 1fr 20px; |
|||
} |
|||
.indented i { |
|||
justify-self: end; |
|||
} |
|||
|
|||
div { |
|||
padding: var(--spacing-s) var(--spacing-m); |
|||
border-radius: var(--border-radius-m); |
|||
display: grid; |
|||
grid-template-columns: 20px 1fr 20px; |
|||
align-items: center; |
|||
transition: 0.3s background-color; |
|||
color: var(--ink); |
|||
font-weight: 400; |
|||
font-size: 14px; |
|||
margin-bottom: var(--spacing-xs); |
|||
grid-gap: var(--spacing-s); |
|||
} |
|||
|
|||
.selected { |
|||
background-color: var(--grey-2); |
|||
} |
|||
|
|||
div:hover { |
|||
background-color: var(--grey-1); |
|||
cursor: pointer; |
|||
} |
|||
|
|||
i { |
|||
color: var(--grey-7); |
|||
font-size: 20px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,57 @@ |
|||
<script> |
|||
import { goto, params } from "@sveltech/routify" |
|||
import { backendUiStore, store } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui" |
|||
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte" |
|||
import analytics from "analytics" |
|||
|
|||
let modal |
|||
let error = "" |
|||
|
|||
let name |
|||
let source |
|||
let integration |
|||
let datasource |
|||
|
|||
function checkValid(evt) { |
|||
const datasourceName = evt.target.value |
|||
if ($backendUiStore.datasources?.some(datasource => datasource.name === datasourceName)) { |
|||
error = `Datasource with name ${datasourceName} already exists. Please choose another name.` |
|||
return |
|||
} |
|||
error = "" |
|||
} |
|||
|
|||
async function saveDatasource() { |
|||
const { type, ...config } = integration |
|||
|
|||
// Create datasource |
|||
await backendUiStore.actions.datasources.save({ |
|||
name, |
|||
source: type, |
|||
config |
|||
}) |
|||
notifier.success(`Datasource ${name} created successfully.`) |
|||
analytics.captureEvent("Datasource Created", { name }) |
|||
|
|||
// Navigate to new datasource |
|||
$goto(`./datasource/${datasource._id}`) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Create Datasource" |
|||
confirmText="Create" |
|||
onConfirm={saveDatasource} |
|||
disabled={error || !name}> |
|||
<Input |
|||
data-cy="datasource-name-input" |
|||
thin |
|||
label="Datasource Name" |
|||
on:input={checkValid} |
|||
bind:value={name} |
|||
{error} /> |
|||
<Label grey extraSmall>Create Integrated Table from External Source</Label> |
|||
<TableIntegrationMenu bind:integration /> |
|||
</ModalContent> |
|||
@ -0,0 +1,57 @@ |
|||
<script> |
|||
import { goto, params } from "@sveltech/routify" |
|||
import { backendUiStore, store } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui" |
|||
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte" |
|||
import analytics from "analytics" |
|||
|
|||
let modal |
|||
let error = "" |
|||
|
|||
let name |
|||
let source |
|||
let integration |
|||
let datasource |
|||
|
|||
function checkValid(evt) { |
|||
const datasourceName = evt.target.value |
|||
if ($backendUiStore.datasources?.some(datasource => datasource.name === datasourceName)) { |
|||
error = `Datasource with name ${tableName} already exists. Please choose another name.` |
|||
return |
|||
} |
|||
error = "" |
|||
} |
|||
|
|||
async function saveDatasource() { |
|||
const { type, ...config } = integration |
|||
|
|||
// Create datasource |
|||
await backendUiStore.actions.datasources.save({ |
|||
name, |
|||
source: type, |
|||
config |
|||
}) |
|||
notifier.success(`Datasource ${name} created successfully.`) |
|||
analytics.captureEvent("Datasource Created", { name }) |
|||
|
|||
// Navigate to new datasource |
|||
$goto(`./datasource/${datasource._id}`) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Create Datasource" |
|||
confirmText="Create" |
|||
onConfirm={saveDatasource} |
|||
disabled={error || !name}> |
|||
<Input |
|||
data-cy="datasource-name-input" |
|||
thin |
|||
label="Datasource Name" |
|||
on:input={checkValid} |
|||
bind:value={name} |
|||
{error} /> |
|||
<Label grey extraSmall>Create Integrated Table from External Source</Label> |
|||
<TableIntegrationMenu bind:integration /> |
|||
</ModalContent> |
|||
@ -0,0 +1,89 @@ |
|||
<script> |
|||
import { backendUiStore, store, allScreens } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import { DropdownMenu, Button, Input } from "@budibase/bbui" |
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|||
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte" |
|||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" |
|||
|
|||
export let datasource |
|||
|
|||
let anchor |
|||
let dropdown |
|||
let confirmDeleteDialog |
|||
let error = "" |
|||
let originalName = datasource.name |
|||
let willBeDeleted |
|||
|
|||
function hideEditor() { |
|||
dropdown?.hide() |
|||
} |
|||
|
|||
function showModal() { |
|||
hideEditor() |
|||
confirmDeleteDialog.show() |
|||
} |
|||
|
|||
async function deleteDatasource() { |
|||
// TODO: update the store correctly |
|||
await backendUiStore.actions.datasources.delete(datasource) |
|||
// await backendUiStore.actions.datasources.fetch() |
|||
notifier.success("Datasource deleted") |
|||
hideEditor() |
|||
} |
|||
</script> |
|||
|
|||
<div on:click|stopPropagation> |
|||
<div bind:this={anchor} class="icon" on:click={dropdown.show}> |
|||
<i class="ri-more-line" /> |
|||
</div> |
|||
<DropdownMenu align="left" {anchor} bind:this={dropdown}> |
|||
<DropdownContainer> |
|||
<DropdownItem |
|||
icon="ri-delete-bin-line" |
|||
title="Delete" |
|||
on:click={showModal} |
|||
data-cy="delete-datasource" /> |
|||
</DropdownContainer> |
|||
</DropdownMenu> |
|||
</div> |
|||
<ConfirmDialog |
|||
bind:this={confirmDeleteDialog} |
|||
okText="Delete Datasource" |
|||
onOk={deleteDatasource} |
|||
title="Confirm Deletion"> |
|||
Are you sure you wish to delete the datasource |
|||
<i>{datasource.name}?</i> |
|||
This action cannot be undone. |
|||
</ConfirmDialog> |
|||
|
|||
<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); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,14 @@ |
|||
<script> |
|||
import { TextArea } from "@budibase/bbui" |
|||
|
|||
const QueryTypes = { |
|||
SQL: "sql", |
|||
} |
|||
|
|||
export let queryType |
|||
export let query |
|||
</script> |
|||
|
|||
{#if queryType === QueryTypes.SQL} |
|||
<TextArea thin label="Query" bind:value={query} /> |
|||
{/if} |
|||
@ -1,11 +1,76 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import { store } from "builderStore" |
|||
import { store, selectedAccessRole } from "builderStore" |
|||
import PathTree from "./PathTree.svelte" |
|||
|
|||
let routes = {} |
|||
$: paths = Object.keys(routes || {}).sort() |
|||
|
|||
$: { |
|||
const allRoutes = $store.routes |
|||
const sortedPaths = Object.keys(allRoutes || {}).sort() |
|||
const selectedRoleId = $selectedAccessRole |
|||
const selectedScreenId = $store.selectedScreenId |
|||
|
|||
let found = false |
|||
let firstValidScreenId |
|||
let filteredRoutes = {} |
|||
let screenRoleId |
|||
|
|||
// Filter all routes down to only those which match the current role |
|||
sortedPaths.forEach(path => { |
|||
const config = allRoutes[path] |
|||
Object.entries(config.subpaths).forEach(([subpath, pathConfig]) => { |
|||
Object.entries(pathConfig.screens).forEach(([roleId, screenId]) => { |
|||
if (screenId === selectedScreenId) { |
|||
screenRoleId = roleId |
|||
found = roleId === selectedRoleId |
|||
} |
|||
if (roleId === selectedRoleId) { |
|||
if (!firstValidScreenId) { |
|||
firstValidScreenId = screenId |
|||
} |
|||
if (!filteredRoutes[path]) { |
|||
filteredRoutes[path] = { subpaths: {} } |
|||
} |
|||
filteredRoutes[path].subpaths[subpath] = { |
|||
screens: { |
|||
[selectedRoleId]: screenId, |
|||
}, |
|||
} |
|||
} |
|||
}) |
|||
}) |
|||
}) |
|||
routes = filteredRoutes |
|||
|
|||
// Select the correct role for the current screen ID |
|||
if (!found && screenRoleId) { |
|||
selectedAccessRole.set(screenRoleId) |
|||
} |
|||
|
|||
// If the selected screen isn't in this filtered list, select the first one |
|||
else if (!found && firstValidScreenId) { |
|||
store.actions.screens.select(firstValidScreenId) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
{#each Object.keys($store.routes || {}) as path} |
|||
<PathTree {path} route={$store.routes[path]} /> |
|||
{#each paths as path, idx} |
|||
<PathTree border={idx > 0} {path} route={routes[path]} /> |
|||
{/each} |
|||
|
|||
{#if !paths.length} |
|||
<div class="empty"> |
|||
There aren't any screens configured with this access role. |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
div.empty { |
|||
font-size: var(--font-size-xs); |
|||
color: var(--grey-5); |
|||
padding-top: var(--spacing-xs); |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
export let value |
|||
|
|||
let roles = [] |
|||
</script> |
|||
|
|||
<Select bind:value extraThin secondary on:change> |
|||
<option value="">Choose an option</option> |
|||
{#each $backendUiStore.roles as role} |
|||
<option value={role._id}>{role.name}</option> |
|||
{/each} |
|||
</Select> |
|||
@ -0,0 +1,16 @@ |
|||
<script> |
|||
import { params } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte" |
|||
|
|||
$: datasourceId = $params.selectedDatasource |
|||
// TODO: refactor |
|||
$: datasource = $backendUiStore.datasources.find( |
|||
ds => ds._id === $params.selectedDatasource |
|||
) |
|||
$: query = datasource.queries[$params.query] |
|||
</script> |
|||
|
|||
{#if $backendUiStore.selectedDatabase._id && query} |
|||
<ExternalDataSourceTable {query} {datasourceId} /> |
|||
{/if} |
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { params } from "@sveltech/routify" |
|||
import { backendUiStore } from "builderStore" |
|||
|
|||
if ($params.selectedDatasourceId) { |
|||
const datasource = $backendUiStore.datasources.find( |
|||
m => m._id === $params.selectedDatasource |
|||
) |
|||
if (datasource) { |
|||
backendUiStore.actions.datasources.select(datasource) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,43 @@ |
|||
<script> |
|||
import { Button, Spacer } from "@budibase/bbui" |
|||
import { backendUiStore } from "builderStore" |
|||
import { notifier } from "builderStore/store/notifications" |
|||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte" |
|||
import CreateQueryButton from "components/backend/DataTable/buttons/CreateQueryButton.svelte" |
|||
|
|||
$: datasource = $backendUiStore.datasources.find( |
|||
ds => ds._id === $backendUiStore.selectedDatasourceId |
|||
) |
|||
|
|||
async function saveDatasource() { |
|||
// Create datasource |
|||
await backendUiStore.actions.datasources.save(datasource) |
|||
notifier.success(`Datasource ${name} saved successfully.`) |
|||
} |
|||
</script> |
|||
|
|||
{#if datasource} |
|||
<CreateQueryButton {datasource} /> |
|||
<section> |
|||
<h4>{datasource.name}: Configuration</h4> |
|||
<IntegrationConfigForm integration={datasource.config} /> |
|||
<Spacer medium /> |
|||
<footer> |
|||
<Button primary wide disabled={false} on:click={saveDatasource}> |
|||
Save |
|||
</Button> |
|||
</footer> |
|||
</section> |
|||
{/if} |
|||
|
|||
<style> |
|||
h4 { |
|||
margin-top: var(--spacing-xl); |
|||
margin-bottom: var(--spacing-s); |
|||
} |
|||
section { |
|||
background: white; |
|||
border-radius: var(--border-radius-m); |
|||
padding: var(--spacing-xl); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,18 @@ |
|||
<script> |
|||
import { backendUiStore } from "builderStore" |
|||
import { goto, leftover } from "@sveltech/routify" |
|||
import { onMount } from "svelte" |
|||
|
|||
onMount(async () => { |
|||
// navigate to first datasource in list, if not already selected |
|||
if ( |
|||
!$leftover && |
|||
$backendUiStore.datasources.length > 0 && |
|||
!$backendUiStore.selectedDatasourceId |
|||
) { |
|||
$goto(`./${$backendUiStore.datasources[0]._id}`) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<slot /> |
|||
@ -0,0 +1,7 @@ |
|||
import { routeStore } from "./routes" |
|||
import { screenStore } from "./screens" |
|||
|
|||
export async function initialise() { |
|||
await routeStore.actions.fetchRoutes() |
|||
await screenStore.actions.fetchScreens() |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
const CouchDB = require("../../db") |
|||
const bcrypt = require("../../utilities/bcrypt") |
|||
const { |
|||
generateDatasourceID, |
|||
getDatasourceParams, |
|||
generateQueryID, |
|||
} = require("../../db/utils") |
|||
const { integrations } = require("../../integrations") |
|||
|
|||
exports.fetch = async function(ctx) { |
|||
const database = new CouchDB(ctx.user.appId) |
|||
const datasources = ( |
|||
await database.allDocs( |
|||
getDatasourceParams(null, { |
|||
include_docs: true, |
|||
}) |
|||
) |
|||
).rows.map(row => row.doc) |
|||
ctx.body = datasources |
|||
} |
|||
|
|||
exports.save = async function(ctx) { |
|||
const db = new CouchDB(ctx.user.appId) |
|||
|
|||
// TODO: validate the config against the integration type
|
|||
// if (!somethingIsntValid) {
|
|||
// // ctx.throw(400, "email and Password Required.")
|
|||
// }
|
|||
|
|||
const datasource = { |
|||
_id: generateDatasourceID(), |
|||
type: "datasource", |
|||
queries: {}, |
|||
...ctx.request.body, |
|||
} |
|||
|
|||
try { |
|||
const response = await db.post(datasource) |
|||
datasource._rev = response.rev |
|||
|
|||
ctx.status = 200 |
|||
ctx.message = "Datasource created successfully." |
|||
ctx.body = datasource |
|||
} catch (err) { |
|||
ctx.throw(err.status, err) |
|||
} |
|||
} |
|||
|
|||
exports.update = async function(ctx) { |
|||
const db = new CouchDB(ctx.user.appId) |
|||
const user = ctx.request.body |
|||
const dbUser = await db.get(ctx.request.body._id) |
|||
if (user.password) { |
|||
user.password = await bcrypt.hash(user.password) |
|||
} else { |
|||
delete user.password |
|||
} |
|||
const newData = { ...dbUser, ...user } |
|||
|
|||
const response = await db.put(newData) |
|||
user._rev = response.rev |
|||
|
|||
ctx.status = 200 |
|||
ctx.message = `User ${ctx.request.body.email} updated successfully.` |
|||
ctx.body = response |
|||
} |
|||
|
|||
exports.destroy = async function(ctx) { |
|||
const database = new CouchDB(ctx.user.appId) |
|||
await database.destroy(ctx.params.datasourceId) |
|||
ctx.message = `Datasource deleted.` |
|||
ctx.status = 200 |
|||
} |
|||
|
|||
exports.find = async function(ctx) { |
|||
const database = new CouchDB(ctx.user.appId) |
|||
const datasource = await database.get(ctx.params.datasourceId) |
|||
ctx.body = datasource |
|||
} |
|||
|
|||
exports.saveQuery = async function(ctx) { |
|||
const db = new CouchDB(ctx.user.appId) |
|||
const query = ctx.request.body |
|||
|
|||
//
|
|||
// {
|
|||
// type: "",
|
|||
// query: "",
|
|||
// otherStuff: ""
|
|||
// }
|
|||
|
|||
const datasource = await db.get(ctx.params.datasourceId) |
|||
|
|||
const queryId = generateQueryID() |
|||
|
|||
datasource.queries[queryId] = query |
|||
|
|||
const response = await db.put(datasource) |
|||
datasource._rev = response.rev |
|||
|
|||
ctx.body = datasource |
|||
ctx.message = `Query ${query.name} saved successfully.` |
|||
} |
|||
|
|||
exports.previewQuery = async function(ctx) { |
|||
const { type, config, query } = ctx.request.body |
|||
|
|||
const Integration = integrations[type] |
|||
|
|||
if (!Integration) { |
|||
ctx.throw(400, "Integration type does not exist.") |
|||
return |
|||
} |
|||
|
|||
ctx.body = await new Integration(config, query).query() |
|||
} |
|||
|
|||
exports.executeQuery = async function(ctx) { |
|||
const db = new CouchDB(ctx.user.appId) |
|||
|
|||
const datasource = await db.get(ctx.params.datasourceId) |
|||
|
|||
const query = datasource.queries[ctx.params.queryId] |
|||
|
|||
const Integration = integrations[datasource.source] |
|||
|
|||
if (!Integration) { |
|||
ctx.throw(400, "Integration type does not exist.") |
|||
return |
|||
} |
|||
|
|||
ctx.body = await new Integration(datasource.config, query.queryString).query() |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
const { BUILTIN_PERMISSIONS } = require("../../utilities/security/permissions") |
|||
|
|||
exports.fetch = async function(ctx) { |
|||
// TODO: need to build out custom permissions
|
|||
ctx.body = Object.values(BUILTIN_PERMISSIONS) |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
// const CouchDB = require("../../../db")
|
|||
// const { generateQueryID } = require("../../db/utils")
|
|||
// const viewTemplate = require("./viewBuilder")
|
|||
|
|||
// exports.save = async ctx => {
|
|||
// const db = new CouchDB(ctx.user.appId)
|
|||
// const { datasourceId, query } = ctx.request.body
|
|||
|
|||
// const datasource = await db.get(datasourceId)
|
|||
|
|||
// const queryId = generateQueryID()
|
|||
|
|||
// datasource.queries[queryId] = query
|
|||
|
|||
// const response = await db.put(datasource)
|
|||
|
|||
// ctx.body = query
|
|||
// ctx.message = `View ${viewToSave.name} saved successfully.`
|
|||
// }
|
|||
|
|||
// exports.destroy = async ctx => {
|
|||
// const db = new CouchDB(ctx.user.appId)
|
|||
// const designDoc = await db.get("_design/database")
|
|||
|
|||
// const viewName = decodeURI(ctx.params.viewName)
|
|||
|
|||
// const view = designDoc.views[viewName]
|
|||
|
|||
// delete designDoc.views[viewName]
|
|||
|
|||
// await db.put(designDoc)
|
|||
|
|||
// const table = await db.get(view.meta.tableId)
|
|||
// delete table.views[viewName]
|
|||
// await db.put(table)
|
|||
|
|||
// ctx.body = view
|
|||
// ctx.message = `View ${ctx.params.viewName} saved successfully.`
|
|||
// }
|
|||
@ -0,0 +1,41 @@ |
|||
const Router = require("@koa/router") |
|||
const datasourceController = require("../controllers/datasource") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { |
|||
BUILDER, |
|||
PermissionLevels, |
|||
PermissionTypes, |
|||
} = require("../../utilities/security/permissions") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.get("/api/datasources", authorized(BUILDER), datasourceController.fetch) |
|||
.get( |
|||
"/api/datasources/:id", |
|||
authorized(PermissionTypes.TABLE, PermissionLevels.READ), |
|||
datasourceController.find |
|||
) |
|||
.post("/api/datasources", authorized(BUILDER), datasourceController.save) |
|||
.post( |
|||
"/api/datasources/:datasourceId/queries", |
|||
authorized(BUILDER), |
|||
datasourceController.saveQuery |
|||
) |
|||
.post( |
|||
"/api/datasources/queries/preview", |
|||
authorized(BUILDER), |
|||
datasourceController.previewQuery |
|||
) |
|||
.get( |
|||
"/api/datasources/:datasourceId/queries/:queryId", |
|||
authorized(BUILDER), |
|||
datasourceController.executeQuery |
|||
) |
|||
.delete( |
|||
"/api/datasources/:datasourceId/:revId", |
|||
authorized(BUILDER), |
|||
datasourceController.destroy |
|||
) |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,10 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../controllers/permission") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("../../utilities/security/permissions") |
|||
|
|||
const router = Router() |
|||
|
|||
router.get("/api/permissions", authorized(BUILDER), controller.fetch) |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,28 @@ |
|||
// const Router = require("@koa/router")
|
|||
// const queryController = require("../controllers/query")
|
|||
// const authorized = require("../../middleware/authorized")
|
|||
// const { BUILDER } = require("../../utilities/security/permissions")
|
|||
|
|||
// const router = Router()
|
|||
|
|||
// // TODO: send down the datasource ID as well
|
|||
|
|||
// router
|
|||
// // .get("/api/queries", authorized(BUILDER), queryController.fetch)
|
|||
// // .get(
|
|||
// // "/api/datasources/:datasourceId/queries/:id",
|
|||
// // authorized(PermissionTypes.TABLE, PermissionLevels.READ),
|
|||
// // queryController.find
|
|||
// // )
|
|||
// .post(
|
|||
// "/api/datasources/:datasourceId/queries",
|
|||
// authorized(BUILDER),
|
|||
// queryController.save
|
|||
// )
|
|||
// .delete(
|
|||
// "/api/datasources/:datasourceId/queries/:queryId/:revId",
|
|||
// authorized(BUILDER),
|
|||
// queryController.destroy
|
|||
// )
|
|||
|
|||
// module.exports = router
|
|||
@ -1,46 +0,0 @@ |
|||
const { generateAssetCss, generateCss } = require("../../../utilities/builder/generateCss") |
|||
|
|||
describe("generate_css", () => { |
|||
it("Check how array styles are output", () => { |
|||
expect(generateCss({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0 10 0 15;") |
|||
}) |
|||
|
|||
it("Check handling of an array with empty string values", () => { |
|||
expect(generateCss({ padding: ["", "", "", ""] })).toBe("") |
|||
}) |
|||
|
|||
it("Check handling of an empty array", () => { |
|||
expect(generateCss({ margin: [] })).toBe("") |
|||
}) |
|||
|
|||
it("Check handling of valid font property", () => { |
|||
expect(generateCss({ "font-size": "10px" })).toBe("font-size: 10px;") |
|||
}) |
|||
}) |
|||
|
|||
|
|||
describe("generate_screen_css", () => { |
|||
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } } |
|||
|
|||
it("Test generation of normal css styles", () => { |
|||
expect(generateAssetCss([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } } |
|||
|
|||
it("Test generation of hover css styles", () => { |
|||
expect(generateAssetCss([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } } |
|||
|
|||
it("Test generation of selection css styles", () => { |
|||
expect(generateAssetCss([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}") |
|||
}) |
|||
|
|||
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } } |
|||
|
|||
it("Testing handling of empty component styles", () => { |
|||
expect(generateAssetCss([emptyComponent])).toBe("") |
|||
}) |
|||
}) |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue