forked from tsai/budibase
65 changed files with 1888 additions and 1421 deletions
@ -0,0 +1,239 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
import "@spectrum-css/popover/dist/index-vars.css" |
||||
|
import clickOutside from "../Actions/click_outside" |
||||
|
import { fly } from "svelte/transition" |
||||
|
import Icon from "../Icon/Icon.svelte" |
||||
|
import Input from "../Form/Input.svelte" |
||||
|
import { capitalise } from "../utils/helpers" |
||||
|
|
||||
|
export let value |
||||
|
export let size = "M" |
||||
|
|
||||
|
let open = false |
||||
|
|
||||
|
$: color = value || "transparent" |
||||
|
$: customValue = getCustomValue(value) |
||||
|
$: checkColor = getCheckColor(value) |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
const categories = [ |
||||
|
{ |
||||
|
label: "Grays", |
||||
|
colors: [ |
||||
|
"white", |
||||
|
"gray-100", |
||||
|
"gray-200", |
||||
|
"gray-300", |
||||
|
"gray-400", |
||||
|
"gray-500", |
||||
|
"gray-600", |
||||
|
"gray-700", |
||||
|
"gray-800", |
||||
|
"gray-900", |
||||
|
"black", |
||||
|
], |
||||
|
}, |
||||
|
{ |
||||
|
label: "Colors", |
||||
|
colors: [ |
||||
|
"red-400", |
||||
|
"orange-400", |
||||
|
"yellow-400", |
||||
|
"green-400", |
||||
|
"seafoam-400", |
||||
|
"blue-400", |
||||
|
"indigo-400", |
||||
|
"magenta-400", |
||||
|
|
||||
|
"red-500", |
||||
|
"orange-500", |
||||
|
"yellow-500", |
||||
|
"green-500", |
||||
|
"seafoam-500", |
||||
|
"blue-500", |
||||
|
"indigo-500", |
||||
|
"magenta-500", |
||||
|
|
||||
|
"red-600", |
||||
|
"orange-600", |
||||
|
"yellow-600", |
||||
|
"green-600", |
||||
|
"seafoam-600", |
||||
|
"blue-600", |
||||
|
"indigo-600", |
||||
|
"magenta-600", |
||||
|
|
||||
|
"red-700", |
||||
|
"orange-700", |
||||
|
"yellow-700", |
||||
|
"green-700", |
||||
|
"seafoam-700", |
||||
|
"blue-700", |
||||
|
"indigo-700", |
||||
|
"magenta-700", |
||||
|
], |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
const onChange = value => { |
||||
|
dispatch("change", value) |
||||
|
open = false |
||||
|
} |
||||
|
|
||||
|
const getCustomValue = value => { |
||||
|
if (!value) { |
||||
|
return value |
||||
|
} |
||||
|
let found = false |
||||
|
const comparisonValue = value.substring(35, value.length - 1) |
||||
|
for (let category of categories) { |
||||
|
found = category.colors.includes(comparisonValue) |
||||
|
if (found) { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
return found ? null : value |
||||
|
} |
||||
|
|
||||
|
const prettyPrint = color => { |
||||
|
return capitalise(color.split("-").join(" ")) |
||||
|
} |
||||
|
|
||||
|
const getCheckColor = value => { |
||||
|
return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value) |
||||
|
? "black" |
||||
|
: "white" |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="container"> |
||||
|
<div |
||||
|
class="preview size--{size || 'M'}" |
||||
|
style="background: {color};" |
||||
|
on:click={() => (open = true)} |
||||
|
/> |
||||
|
{#if open} |
||||
|
<div |
||||
|
use:clickOutside={() => (open = false)} |
||||
|
transition:fly={{ y: -20, duration: 200 }} |
||||
|
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" |
||||
|
> |
||||
|
{#each categories as category} |
||||
|
<div class="category"> |
||||
|
<div class="heading">{category.label}</div> |
||||
|
<div class="colors"> |
||||
|
{#each category.colors as color} |
||||
|
<div |
||||
|
on:click={() => { |
||||
|
onChange(`var(--spectrum-global-color-static-${color})`) |
||||
|
}} |
||||
|
class="color" |
||||
|
style="background: var(--spectrum-global-color-static-{color}); color: {checkColor};" |
||||
|
title={prettyPrint(color)} |
||||
|
> |
||||
|
{#if value === `var(--spectrum-global-color-static-${color})`} |
||||
|
<Icon name="Checkmark" size="S" /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</div> |
||||
|
{/each} |
||||
|
<div class="category category--custom"> |
||||
|
<div class="heading">Custom</div> |
||||
|
<div class="custom"> |
||||
|
<Input |
||||
|
updateOnChange={false} |
||||
|
quiet |
||||
|
placeholder="Hex, RGB, HSL..." |
||||
|
value={customValue} |
||||
|
on:change |
||||
|
/> |
||||
|
<Icon |
||||
|
size="S" |
||||
|
name="Close" |
||||
|
hoverable |
||||
|
on:click={() => onChange(null)} |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.container { |
||||
|
position: relative; |
||||
|
} |
||||
|
.preview { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
border-radius: 100%; |
||||
|
transition: border-color 130ms ease-in-out; |
||||
|
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); |
||||
|
} |
||||
|
.preview:hover { |
||||
|
cursor: pointer; |
||||
|
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300); |
||||
|
} |
||||
|
.size--S { |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
} |
||||
|
.size--M { |
||||
|
width: 32px; |
||||
|
height: 32px; |
||||
|
} |
||||
|
.size--L { |
||||
|
width: 48px; |
||||
|
height: 48px; |
||||
|
} |
||||
|
.spectrum-Popover { |
||||
|
width: 210px; |
||||
|
z-index: 999; |
||||
|
top: 100%; |
||||
|
padding: var(--spacing-l) var(--spacing-xl); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
gap: var(--spacing-xl); |
||||
|
} |
||||
|
.colors { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; |
||||
|
gap: var(--spacing-xs); |
||||
|
} |
||||
|
.heading { |
||||
|
font-size: var(--font-size-s); |
||||
|
font-weight: 600; |
||||
|
letter-spacing: 0.14px; |
||||
|
flex: 1 1 auto; |
||||
|
text-transform: uppercase; |
||||
|
grid-column: 1 / 5; |
||||
|
margin-bottom: var(--spacing-s); |
||||
|
} |
||||
|
.color { |
||||
|
height: 16px; |
||||
|
width: 16px; |
||||
|
border-radius: 100%; |
||||
|
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300); |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
} |
||||
|
.color:hover { |
||||
|
cursor: pointer; |
||||
|
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300); |
||||
|
} |
||||
|
.custom { |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr auto; |
||||
|
align-items: center; |
||||
|
gap: var(--spacing-m); |
||||
|
margin-right: var(--spacing-xs); |
||||
|
} |
||||
|
.category--custom .heading { |
||||
|
margin-bottom: var(--spacing-xs); |
||||
|
} |
||||
|
</style> |
||||
@ -1,40 +0,0 @@ |
|||||
<script> |
|
||||
export let categories = [] |
|
||||
export let selectedCategory = {} |
|
||||
export let onClick = () => {} |
|
||||
</script> |
|
||||
|
|
||||
<div class="tabs"> |
|
||||
{#each categories as category} |
|
||||
<li |
|
||||
data-cy={category.name} |
|
||||
on:click={() => onClick(category)} |
|
||||
class:active={selectedCategory === category} |
|
||||
> |
|
||||
{category.name} |
|
||||
</li> |
|
||||
{/each} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.tabs { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: flex-start; |
|
||||
align-items: center; |
|
||||
list-style: none; |
|
||||
font-size: var(--font-size-m); |
|
||||
font-weight: 600; |
|
||||
height: 24px; |
|
||||
} |
|
||||
|
|
||||
li { |
|
||||
color: var(--grey-5); |
|
||||
cursor: pointer; |
|
||||
margin-right: 20px; |
|
||||
} |
|
||||
|
|
||||
.active { |
|
||||
color: var(--ink); |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,60 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
TextArea, |
||||
|
DetailSummary, |
||||
|
ActionButton, |
||||
|
Drawer, |
||||
|
DrawerContent, |
||||
|
Layout, |
||||
|
Body, |
||||
|
Button, |
||||
|
} from "@budibase/bbui" |
||||
|
import { store } from "builderStore" |
||||
|
|
||||
|
export let componentInstance |
||||
|
|
||||
|
let tempValue |
||||
|
let drawer |
||||
|
|
||||
|
const openDrawer = () => { |
||||
|
tempValue = componentInstance?._styles?.custom |
||||
|
drawer.show() |
||||
|
} |
||||
|
|
||||
|
const save = () => { |
||||
|
store.actions.components.updateCustomStyle(tempValue) |
||||
|
drawer.hide() |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<DetailSummary |
||||
|
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`} |
||||
|
collapsible={false} |
||||
|
> |
||||
|
<div> |
||||
|
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton> |
||||
|
</div> |
||||
|
</DetailSummary> |
||||
|
<Drawer bind:this={drawer} title="Custom CSS"> |
||||
|
<Button cta slot="buttons" on:click={save}>Save</Button> |
||||
|
<DrawerContent slot="body"> |
||||
|
<div class="content"> |
||||
|
<Layout gap="S"> |
||||
|
<Body size="S">Custom CSS overrides all other component styles.</Body> |
||||
|
<TextArea bind:value={tempValue} placeholder="Enter some CSS..." /> |
||||
|
</Layout> |
||||
|
</div> |
||||
|
</DrawerContent> |
||||
|
</Drawer> |
||||
|
|
||||
|
<style> |
||||
|
.content { |
||||
|
max-width: 800px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
.content :global(textarea) { |
||||
|
font-family: monospace; |
||||
|
min-height: 240px !important; |
||||
|
font-size: var(--font-size-s); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,34 @@ |
|||||
|
<script> |
||||
|
import StyleSection from "./StyleSection.svelte" |
||||
|
import * as ComponentStyles from "./componentStyles" |
||||
|
|
||||
|
export let componentDefinition |
||||
|
export let componentInstance |
||||
|
|
||||
|
const getStyles = def => { |
||||
|
if (!def?.styles?.length) { |
||||
|
return [...ComponentStyles.all] |
||||
|
} |
||||
|
let styles = [...ComponentStyles.all] |
||||
|
def.styles.forEach(style => { |
||||
|
if (ComponentStyles[style]) { |
||||
|
styles.push(ComponentStyles[style]) |
||||
|
} |
||||
|
}) |
||||
|
return styles |
||||
|
} |
||||
|
|
||||
|
$: styles = getStyles(componentDefinition) |
||||
|
</script> |
||||
|
|
||||
|
{#if styles?.length > 0} |
||||
|
{#each styles as style} |
||||
|
<StyleSection |
||||
|
{style} |
||||
|
name={style.label} |
||||
|
columns={style.columns} |
||||
|
properties={style.settings} |
||||
|
{componentInstance} |
||||
|
/> |
||||
|
{/each} |
||||
|
{/if} |
||||
@ -1,111 +0,0 @@ |
|||||
<script> |
|
||||
import { TextArea, DetailSummary, Button } from "@budibase/bbui" |
|
||||
import PropertyGroup from "./PropertyControls/PropertyGroup.svelte" |
|
||||
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup" |
|
||||
import { allStyles } from "./componentStyles" |
|
||||
|
|
||||
export let componentDefinition = {} |
|
||||
export let componentInstance = {} |
|
||||
export let onStyleChanged = () => {} |
|
||||
export let onCustomStyleChanged = () => {} |
|
||||
export let onResetStyles = () => {} |
|
||||
|
|
||||
let selectedCategory = "normal" |
|
||||
let currentGroup |
|
||||
|
|
||||
function onChange(category) { |
|
||||
selectedCategory = category |
|
||||
} |
|
||||
|
|
||||
const buttonProps = [ |
|
||||
{ value: "normal", text: "Normal" }, |
|
||||
{ value: "hover", text: "Hover" }, |
|
||||
{ value: "active", text: "Active" }, |
|
||||
] |
|
||||
|
|
||||
$: groups = componentDefinition?.styleable ? Object.keys(allStyles) : [] |
|
||||
</script> |
|
||||
|
|
||||
<div class="container"> |
|
||||
<div class="state-categories"> |
|
||||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} /> |
|
||||
</div> |
|
||||
|
|
||||
<div class="positioned-wrapper"> |
|
||||
<div class="property-groups"> |
|
||||
{#if groups.length > 0} |
|
||||
{#each groups as groupName} |
|
||||
<PropertyGroup |
|
||||
name={groupName} |
|
||||
properties={allStyles[groupName]} |
|
||||
styleCategory={selectedCategory} |
|
||||
{onStyleChanged} |
|
||||
{componentInstance} |
|
||||
open={currentGroup === groupName} |
|
||||
on:open={() => (currentGroup = groupName)} |
|
||||
/> |
|
||||
{/each} |
|
||||
<DetailSummary |
|
||||
name={`Custom Styles${componentInstance._styles.custom ? " *" : ""}`} |
|
||||
on:open={() => (currentGroup = "custom")} |
|
||||
show={currentGroup === "custom"} |
|
||||
thin |
|
||||
> |
|
||||
<div class="custom-styles"> |
|
||||
<TextArea |
|
||||
value={componentInstance._styles.custom} |
|
||||
on:change={event => onCustomStyleChanged(event.detail)} |
|
||||
placeholder="Enter some CSS..." |
|
||||
/> |
|
||||
</div> |
|
||||
</DetailSummary> |
|
||||
<Button secondary wide on:click={onResetStyles}>Reset Styles</Button> |
|
||||
{:else} |
|
||||
<div class="no-design"> |
|
||||
This component doesn't have any design properties. |
|
||||
</div> |
|
||||
{/if} |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.container { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
gap: var(--spacing-l); |
|
||||
} |
|
||||
|
|
||||
.positioned-wrapper { |
|
||||
position: relative; |
|
||||
display: flex; |
|
||||
min-height: 0; |
|
||||
flex: 1 1 auto; |
|
||||
} |
|
||||
|
|
||||
.property-groups { |
|
||||
flex: 1; |
|
||||
overflow-y: auto; |
|
||||
min-height: 0; |
|
||||
margin: 0 -20px; |
|
||||
padding: 0 20px; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: stretch; |
|
||||
gap: var(--spacing-m); |
|
||||
} |
|
||||
|
|
||||
.no-design { |
|
||||
font-size: var(--font-size-xs); |
|
||||
color: var(--grey-5); |
|
||||
} |
|
||||
|
|
||||
.custom-styles :global(textarea) { |
|
||||
font-family: monospace; |
|
||||
min-height: 120px; |
|
||||
font-size: var(--font-size-xs); |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,84 +1,33 @@ |
|||||
<script> |
<script> |
||||
import { get } from "svelte/store" |
import { store, selectedComponent } from "builderStore" |
||||
import { store, selectedComponent, currentAsset } from "builderStore" |
|
||||
import { Tabs, Tab } from "@budibase/bbui" |
import { Tabs, Tab } from "@budibase/bbui" |
||||
import { FrontendTypes } from "constants" |
import ScreenSettingsSection from "./ScreenSettingsSection.svelte" |
||||
import DesignView from "./DesignView.svelte" |
import ComponentSettingsSection from "./ComponentSettingsSection.svelte" |
||||
import SettingsView from "./SettingsView.svelte" |
import DesignSection from "./DesignSection.svelte" |
||||
import { setWith } from "lodash" |
import CustomStylesSection from "./CustomStylesSection.svelte" |
||||
|
|
||||
$: definition = store.actions.components.getDefinition( |
$: componentInstance = $selectedComponent |
||||
|
$: componentDefinition = store.actions.components.getDefinition( |
||||
$selectedComponent?._component |
$selectedComponent?._component |
||||
) |
) |
||||
$: isComponentOrScreen = |
|
||||
$store.currentView === "component" || |
|
||||
$store.currentFrontEndType === FrontendTypes.SCREEN |
|
||||
$: isNotScreenslot = !$selectedComponent._component.endsWith("screenslot") |
|
||||
$: showDisplayName = isComponentOrScreen && isNotScreenslot |
|
||||
|
|
||||
const onStyleChanged = store.actions.components.updateStyle |
|
||||
const onCustomStyleChanged = store.actions.components.updateCustomStyle |
|
||||
const onResetStyles = store.actions.components.resetStyles |
|
||||
|
|
||||
function setAssetProps(name, value) { |
|
||||
const selectedAsset = get(currentAsset) |
|
||||
store.update(state => { |
|
||||
if ( |
|
||||
name === "_instanceName" && |
|
||||
state.currentFrontEndType === FrontendTypes.SCREEN |
|
||||
) { |
|
||||
selectedAsset.props._instanceName = value |
|
||||
} else { |
|
||||
setWith(selectedAsset, name.split("."), value, Object) |
|
||||
} |
|
||||
return state |
|
||||
}) |
|
||||
store.actions.preview.saveSelected() |
|
||||
} |
|
||||
</script> |
</script> |
||||
|
|
||||
<Tabs selected="Settings"> |
<Tabs selected="Settings" noPadding> |
||||
<Tab title="Settings"> |
<Tab title="Settings"> |
||||
<div class="tab-content-padding"> |
<div class="container"> |
||||
{#if definition && definition.name} |
<ScreenSettingsSection {componentInstance} {componentDefinition} /> |
||||
<div class="instance-name">{definition.name}</div> |
<ComponentSettingsSection {componentInstance} {componentDefinition} /> |
||||
{/if} |
<DesignSection {componentInstance} {componentDefinition} /> |
||||
<SettingsView |
<CustomStylesSection {componentInstance} {componentDefinition} /> |
||||
componentInstance={$selectedComponent} |
|
||||
componentDefinition={definition} |
|
||||
{showDisplayName} |
|
||||
onChange={store.actions.components.updateProp} |
|
||||
onScreenPropChange={setAssetProps} |
|
||||
assetInstance={$store.currentView !== "component" && $currentAsset} |
|
||||
/> |
|
||||
</div> |
|
||||
</Tab> |
|
||||
<Tab title="Design"> |
|
||||
<div class="tab-content-padding"> |
|
||||
{#if definition && definition.name} |
|
||||
<div class="instance-name">{definition.name}</div> |
|
||||
{/if} |
|
||||
<DesignView |
|
||||
componentInstance={$selectedComponent} |
|
||||
componentDefinition={definition} |
|
||||
{onStyleChanged} |
|
||||
{onCustomStyleChanged} |
|
||||
{onResetStyles} |
|
||||
/> |
|
||||
</div> |
</div> |
||||
</Tab> |
</Tab> |
||||
</Tabs> |
</Tabs> |
||||
|
|
||||
<style> |
<style> |
||||
.tab-content-padding { |
.container { |
||||
padding: 0 var(--spacing-xl); |
display: flex; |
||||
} |
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
.instance-name { |
align-items: stretch; |
||||
font-size: var(--spectrum-global-dimension-font-size-75); |
|
||||
margin-bottom: var(--spacing-m); |
|
||||
margin-top: var(--spacing-xs); |
|
||||
font-weight: 600; |
|
||||
color: var(--grey-7); |
|
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
@ -1,54 +0,0 @@ |
|||||
<script> |
|
||||
import PropertyControl from "./PropertyControl.svelte" |
|
||||
import { DetailSummary } from "@budibase/bbui" |
|
||||
|
|
||||
export let name = "" |
|
||||
export let styleCategory = "normal" |
|
||||
export let properties = [] |
|
||||
export let componentInstance = {} |
|
||||
export let onStyleChanged = () => {} |
|
||||
export let open = false |
|
||||
|
|
||||
$: style = componentInstance["_styles"][styleCategory] || {} |
|
||||
$: changed = properties.some(prop => hasPropChanged(style, prop)) |
|
||||
|
|
||||
const hasPropChanged = (style, prop) => { |
|
||||
return style[prop.key] != null && style[prop.key] !== "" |
|
||||
} |
|
||||
|
|
||||
const getControlProps = props => { |
|
||||
let controlProps = { ...(props || {}) } |
|
||||
delete controlProps.label |
|
||||
delete controlProps.key |
|
||||
delete controlProps.control |
|
||||
return controlProps |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<DetailSummary name={`${name}${changed ? " *" : ""}`} on:open show={open} thin> |
|
||||
{#if open} |
|
||||
<div> |
|
||||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)} |
|
||||
<PropertyControl |
|
||||
bindable={false} |
|
||||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`} |
|
||||
control={prop.control} |
|
||||
key={prop.key} |
|
||||
value={style[prop.key]} |
|
||||
onChange={value => onStyleChanged(styleCategory, prop.key, value)} |
|
||||
props={getControlProps(prop)} |
|
||||
/> |
|
||||
{/each} |
|
||||
</div> |
|
||||
{/if} |
|
||||
</DetailSummary> |
|
||||
|
|
||||
<style> |
|
||||
div { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: stretch; |
|
||||
gap: var(--spacing-s); |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,43 @@ |
|||||
|
<script> |
||||
|
import { ActionButton } from "@budibase/bbui" |
||||
|
import { currentAsset, store } from "builderStore" |
||||
|
import { findClosestMatchingComponent } from "builderStore/storeUtils" |
||||
|
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents" |
||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
||||
|
|
||||
|
export let componentInstance |
||||
|
|
||||
|
let confirmResetFieldsDialog |
||||
|
|
||||
|
const resetFormFields = () => { |
||||
|
const form = findClosestMatchingComponent( |
||||
|
$currentAsset.props, |
||||
|
componentInstance._id, |
||||
|
component => component._component.endsWith("/form") |
||||
|
) |
||||
|
const dataSource = form?.dataSource |
||||
|
const fields = makeDatasourceFormComponents(dataSource) |
||||
|
store.actions.components.updateProp( |
||||
|
"_children", |
||||
|
fields.map(field => field.json()) |
||||
|
) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div> |
||||
|
<ActionButton |
||||
|
secondary |
||||
|
wide |
||||
|
on:click={() => confirmResetFieldsDialog?.show()} |
||||
|
> |
||||
|
Update form fields |
||||
|
</ActionButton> |
||||
|
</div> |
||||
|
|
||||
|
<ConfirmDialog |
||||
|
bind:this={confirmResetFieldsDialog} |
||||
|
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`} |
||||
|
okText="Update" |
||||
|
onOk={resetFormFields} |
||||
|
title="Confirm Form Field Update" |
||||
|
/> |
||||
@ -0,0 +1,50 @@ |
|||||
|
<script> |
||||
|
import { get } from "svelte/store" |
||||
|
import { get as deepGet, setWith } from "lodash" |
||||
|
import { Input, DetailSummary } from "@budibase/bbui" |
||||
|
import PropertyControl from "./PropertyControls/PropertyControl.svelte" |
||||
|
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" |
||||
|
import RoleSelect from "./PropertyControls/RoleSelect.svelte" |
||||
|
import { currentAsset, store } from "builderStore" |
||||
|
import { FrontendTypes } from "constants" |
||||
|
|
||||
|
export let componentInstance |
||||
|
|
||||
|
function setAssetProps(name, value) { |
||||
|
const selectedAsset = get(currentAsset) |
||||
|
store.update(state => { |
||||
|
if ( |
||||
|
name === "_instanceName" && |
||||
|
state.currentFrontEndType === FrontendTypes.SCREEN |
||||
|
) { |
||||
|
selectedAsset.props._instanceName = value |
||||
|
} else { |
||||
|
setWith(selectedAsset, name.split("."), value, Object) |
||||
|
} |
||||
|
return state |
||||
|
}) |
||||
|
store.actions.preview.saveSelected() |
||||
|
} |
||||
|
|
||||
|
const screenSettings = [ |
||||
|
// { key: "description", label: "Description", control: Input }, |
||||
|
{ key: "routing.route", label: "Route", control: Input }, |
||||
|
{ key: "routing.roleId", label: "Access", control: RoleSelect }, |
||||
|
{ key: "layoutId", label: "Layout", control: LayoutSelect }, |
||||
|
] |
||||
|
</script> |
||||
|
|
||||
|
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN} |
||||
|
<DetailSummary name="Screen" collapsible={false}> |
||||
|
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)} |
||||
|
<PropertyControl |
||||
|
bindable={false} |
||||
|
control={def.control} |
||||
|
label={def.label} |
||||
|
key={def.key} |
||||
|
value={deepGet($currentAsset, def.key)} |
||||
|
onChange={val => setAssetProps(def.key, val)} |
||||
|
/> |
||||
|
{/each} |
||||
|
</DetailSummary> |
||||
|
{/if} |
||||
@ -0,0 +1,51 @@ |
|||||
|
<script> |
||||
|
import PropertyControl from "./PropertyControls/PropertyControl.svelte" |
||||
|
import { DetailSummary } from "@budibase/bbui" |
||||
|
import { store } from "builderStore" |
||||
|
|
||||
|
export let name |
||||
|
export let columns |
||||
|
export let properties |
||||
|
export let componentInstance |
||||
|
|
||||
|
$: style = componentInstance._styles.normal || {} |
||||
|
$: changed = properties?.some(prop => hasPropChanged(style, prop)) ?? false |
||||
|
|
||||
|
const hasPropChanged = (style, prop) => { |
||||
|
return style[prop.key] != null && style[prop.key] !== "" |
||||
|
} |
||||
|
|
||||
|
const getControlProps = props => { |
||||
|
let controlProps = { ...(props || {}) } |
||||
|
delete controlProps.label |
||||
|
delete controlProps.key |
||||
|
delete controlProps.control |
||||
|
return controlProps |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}> |
||||
|
<div class="group-content" style="grid-template-columns: {columns || '1fr'}"> |
||||
|
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)} |
||||
|
<div style="grid-column: {prop.column || 'auto'}"> |
||||
|
<PropertyControl |
||||
|
bindable={false} |
||||
|
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`} |
||||
|
control={prop.control} |
||||
|
key={prop.key} |
||||
|
value={style[prop.key]} |
||||
|
onChange={val => store.actions.components.updateStyle(prop.key, val)} |
||||
|
props={getControlProps(prop)} |
||||
|
/> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</DetailSummary> |
||||
|
|
||||
|
<style> |
||||
|
.group-content { |
||||
|
display: grid; |
||||
|
align-items: stretch; |
||||
|
gap: var(--spacing-l); |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
@ -0,0 +1,26 @@ |
|||||
|
<script> |
||||
|
import { ColorPicker } from "@budibase/bbui" |
||||
|
import { builderStore } from "../../store" |
||||
|
|
||||
|
export let prop |
||||
|
|
||||
|
$: currentValue = $builderStore.selectedComponent?.[prop] |
||||
|
</script> |
||||
|
|
||||
|
<div> |
||||
|
<ColorPicker |
||||
|
size="S" |
||||
|
value={currentValue} |
||||
|
on:change={e => { |
||||
|
if (prop) { |
||||
|
builderStore.actions.updateProp(prop, e.detail) |
||||
|
} |
||||
|
}} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div { |
||||
|
padding: 0 4px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,31 @@ |
|||||
|
<script> |
||||
|
import { Select } from "@budibase/bbui" |
||||
|
import { builderStore } from "../../store" |
||||
|
|
||||
|
export let prop |
||||
|
export let options |
||||
|
export let label |
||||
|
|
||||
|
$: currentValue = $builderStore.selectedComponent?.[prop] |
||||
|
</script> |
||||
|
|
||||
|
<div> |
||||
|
<Select |
||||
|
quiet |
||||
|
autoWidth |
||||
|
placeholder={label} |
||||
|
{options} |
||||
|
value={currentValue} |
||||
|
on:change={e => { |
||||
|
if (prop) { |
||||
|
builderStore.actions.updateProp(prop, e.detail) |
||||
|
} |
||||
|
}} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
div { |
||||
|
padding: 0 4px; |
||||
|
} |
||||
|
</style> |
||||
@ -1,23 +1,29 @@ |
|||||
<script> |
<script> |
||||
import { getContext } from "svelte" |
import { getContext } from "svelte" |
||||
|
import Placeholder from "./Placeholder.svelte" |
||||
|
|
||||
const { styleable } = getContext("sdk") |
const { styleable, builderStore } = getContext("sdk") |
||||
const component = getContext("component") |
const component = getContext("component") |
||||
|
|
||||
export let embed |
export let embed |
||||
</script> |
</script> |
||||
|
|
||||
<div use:styleable={$component.styles}> |
{#if embed} |
||||
{@html embed} |
<div class="embed" use:styleable={$component.styles}> |
||||
</div> |
{@html embed} |
||||
|
</div> |
||||
|
{:else if $builderStore.inBuilder} |
||||
|
<div use:styleable={{ ...$component.styles, empty: true }}> |
||||
|
<Placeholder /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
<style> |
<style> |
||||
div { |
.embed { |
||||
position: relative; |
position: relative; |
||||
} |
} |
||||
div :global(> *) { |
.embed :global(> *) { |
||||
width: 100%; |
width: 100%; |
||||
height: 100%; |
height: 100%; |
||||
position: absolute; |
|
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
@ -1,21 +1,27 @@ |
|||||
<script> |
<script> |
||||
import { getContext } from "svelte" |
import { getContext } from "svelte" |
||||
|
import Placeholder from "./Placeholder.svelte" |
||||
|
|
||||
const { styleable } = getContext("sdk") |
const { styleable, builderStore } = getContext("sdk") |
||||
const component = getContext("component") |
const component = getContext("component") |
||||
|
|
||||
export let className = "" |
export let url |
||||
export let url = "" |
|
||||
export let description = "" |
|
||||
export let height |
|
||||
export let width |
|
||||
</script> |
</script> |
||||
|
|
||||
<img |
{#if url} |
||||
{height} |
<img src={url} alt={$component.name} use:styleable={$component.styles} /> |
||||
{width} |
{:else if $builderStore.inBuilder} |
||||
class={className} |
<div |
||||
src={url} |
class="placeholder" |
||||
alt={description} |
use:styleable={{ ...$component.styles, empty: true }} |
||||
use:styleable={$component.styles} |
> |
||||
/> |
<Placeholder /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
.placeholder { |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
} |
||||
|
</style> |
||||
|
|||||
@ -1,31 +1,105 @@ |
|||||
<script> |
<script> |
||||
import { getContext } from "svelte" |
import { getContext } from "svelte" |
||||
|
|
||||
const { linkable, styleable } = getContext("sdk") |
const { linkable, styleable, builderStore } = getContext("sdk") |
||||
const component = getContext("component") |
const component = getContext("component") |
||||
|
|
||||
export let url = "" |
export let url |
||||
export let text = "" |
export let text |
||||
export let openInNewTab = false |
export let openInNewTab |
||||
export let external = false |
export let color |
||||
|
export let align |
||||
|
export let bold |
||||
|
export let italic |
||||
|
export let underline |
||||
|
export let size |
||||
|
|
||||
|
$: external = url && !url.startsWith("/") |
||||
$: target = openInNewTab ? "_blank" : "_self" |
$: target = openInNewTab ? "_blank" : "_self" |
||||
|
$: placeholder = $builderStore.inBuilder && !text |
||||
|
$: componentText = $builderStore.inBuilder |
||||
|
? text || "Placeholder link" |
||||
|
: text || "" |
||||
|
|
||||
|
// Add color styles to main styles object, otherwise the styleable helper |
||||
|
// overrides the color when it's passed as inline style. |
||||
|
$: styles = { |
||||
|
...$component.styles, |
||||
|
normal: { |
||||
|
...$component.styles?.normal, |
||||
|
color, |
||||
|
}, |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
{#if external} |
{#if $builderStore.inBuilder || componentText} |
||||
<a href={url || "/"} {target} use:styleable={$component.styles}> |
{#if external} |
||||
{text} |
<a |
||||
<slot /> |
{target} |
||||
</a> |
href={url || "/"} |
||||
{:else} |
use:styleable={styles} |
||||
<a href={url || "/"} use:linkable {target} use:styleable={$component.styles}> |
class:placeholder |
||||
{text} |
class:bold |
||||
<slot /> |
class:italic |
||||
</a> |
class:underline |
||||
|
class="align--{align || 'left'} size--{size || 'M'}" |
||||
|
> |
||||
|
{componentText} |
||||
|
</a> |
||||
|
{:else} |
||||
|
<a |
||||
|
use:linkable |
||||
|
href={url || "/"} |
||||
|
use:styleable={styles} |
||||
|
class:placeholder |
||||
|
class:bold |
||||
|
class:italic |
||||
|
class:underline |
||||
|
class="align--{align || 'left'} size--{size || 'M'}" |
||||
|
> |
||||
|
{componentText} |
||||
|
</a> |
||||
|
{/if} |
||||
{/if} |
{/if} |
||||
|
|
||||
<style> |
<style> |
||||
a { |
a { |
||||
color: var(--spectrum-alias-text-color); |
color: var(--spectrum-alias-text-color); |
||||
|
display: inline-block; |
||||
|
white-space: pre-wrap; |
||||
|
} |
||||
|
.placeholder { |
||||
|
font-style: italic; |
||||
|
color: var(--grey-6); |
||||
|
} |
||||
|
.bold { |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
.italic { |
||||
|
font-style: italic; |
||||
|
} |
||||
|
.underline { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
.size--S { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
.size--M { |
||||
|
font-size: 16px; |
||||
|
} |
||||
|
.size--L { |
||||
|
font-size: 18px; |
||||
|
} |
||||
|
.align--left { |
||||
|
text-align: left; |
||||
|
} |
||||
|
.align--center { |
||||
|
text-align: center; |
||||
|
} |
||||
|
.align--right { |
||||
|
text-align: right; |
||||
|
} |
||||
|
.align-justify { |
||||
|
text-align: justify; |
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
Loading…
Reference in new issue