Add reltationship field. Add common picker component to dedupe options and relationship fields. Add better animation and position to picker dropdowns

This commit is contained in:
Andrew Kingston 2021-02-01 13:15:35 +00:00
parent ad86c40de6
commit b65673b77a
8 changed files with 223 additions and 66 deletions

View File

@ -15,7 +15,8 @@
"booleanfield", "booleanfield",
"longformfield", "longformfield",
"datetimefield", "datetimefield",
"attachmentfield" "attachmentfield",
"relationshipfield"
] ]
}, },
{ {

View File

@ -0,0 +1,5 @@
<script>
import FormFieldSelect from "./FormFieldSelect.svelte"
</script>
<FormFieldSelect {...$$props} type="link" />

View File

@ -24,6 +24,7 @@
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte" import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte" import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte" import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
export let componentDefinition = {} export let componentDefinition = {}
export let componentInstance = {} export let componentInstance = {}
@ -72,6 +73,7 @@
"field/longform": LongFormFieldSelect, "field/longform": LongFormFieldSelect,
"field/datetime": DateTimeFieldSelect, "field/datetime": DateTimeFieldSelect,
"field/attachment": AttachmentFieldSelect, "field/attachment": AttachmentFieldSelect,
"field/relationship": RelationshipFieldSelect,
} }
const getControl = type => { const getControl = type => {

View File

@ -1284,5 +1284,28 @@
"key": "label" "key": "label"
} }
] ]
},
"relationshipfield": {
"name": "Relationship Picker",
"icon": "ri-edit-box-line",
"styleable": true,
"settings": [
{
"type": "field/relationship",
"label": "Field",
"key": "field"
},
{
"type": "text",
"label": "Label",
"key": "label"
},
{
"type": "text",
"label": "Placeholder",
"key": "placeholder",
"placeholder": "Choose an option"
}
]
} }
} }

View File

@ -3,6 +3,7 @@
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import "@spectrum-css/menu/dist/index-vars.css" import "@spectrum-css/menu/dist/index-vars.css"
import SpectrumField from "./SpectrumField.svelte" import SpectrumField from "./SpectrumField.svelte"
import Picker from "./Picker.svelte"
export let field export let field
export let label export let label
@ -17,6 +18,7 @@
$: options = fieldSchema?.constraints?.inclusion ?? [] $: options = fieldSchema?.constraints?.inclusion ?? []
$: placeholderText = placeholder || "Choose an option" $: placeholderText = placeholder || "Choose an option"
$: isNull = $fieldState?.value == null || $fieldState?.value === "" $: isNull = $fieldState?.value == null || $fieldState?.value === ""
$: fieldText = isNull ? placeholderText : $fieldState?.value
const selectOption = value => { const selectOption = value => {
fieldApi.setValue(value) fieldApi.setValue(value)
@ -26,70 +28,14 @@
<SpectrumField {field} {label} bind:fieldState bind:fieldApi bind:fieldSchema> <SpectrumField {field} {label} bind:fieldState bind:fieldApi bind:fieldSchema>
{#if fieldState} {#if fieldState}
<button <Picker
id={$fieldState.fieldId} bind:open
class="spectrum-Picker" {fieldState}
class:is-invalid={!$fieldState.valid} {fieldText}
class:is-open={open} {options}
aria-haspopup="listbox" isPlaceholder={isNull}
on:click={() => (open = true)}> placeholderOption={placeholderText}
<span class="spectrum-Picker-label" class:is-placeholder={isNull}> isOptionSelected={option => option === $fieldState.value}
{isNull ? placeholderText : $fieldState.value} onSelectOption={selectOption} />
</span>
{#if !$fieldState.valid}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder">
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
<div
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover"
class:is-open={open}
style="z-index: 1;">
<ul class="spectrum-Menu" role="listbox">
<li
class="spectrum-Menu-item"
class:is-selected={isNull}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(null)}>
<span class="spectrum-Menu-itemLabel">{placeholderText}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{#each options as option}
<li
class="spectrum-Menu-item"
class:is-selected={option === $fieldState.value}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(option)}>
<span class="spectrum-Menu-itemLabel">{option}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if} {/if}
</SpectrumField> </SpectrumField>

View File

@ -0,0 +1,103 @@
<script>
import { fly } from "svelte/transition"
export let fieldState
export let fieldText = ""
export let isPlaceholder = false
export let placeholderOption = null
export let options = []
export let isOptionSelected = () => false
export let onSelectOption = () => {}
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let open = false
</script>
{#if fieldState}
<button
id={$fieldState.fieldId}
class="spectrum-Picker"
class:is-invalid={!$fieldState.valid}
class:is-open={open}
aria-haspopup="listbox"
on:click={() => (open = true)}>
<span class="spectrum-Picker-label" class:is-placeholder={isPlaceholder}>
{fieldText}
</span>
{#if !$fieldState.valid}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder">
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Chevron100" />
</svg>
</button>
{#if open}
<div class="overlay" on:mousedown|self={() => (open = false)} />
<div
transition:fly={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open">
<ul class="spectrum-Menu" role="listbox">
{#if placeholderOption}
<li
class="spectrum-Menu-item"
class:is-selected={isPlaceholder}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(null)}>
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/if}
{#each options as option}
<li
class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option))}
role="option"
aria-selected="true"
tabindex="0"
on:click={() => onSelectOption(getOptionValue(option))}>
<span
class="spectrum-Menu-itemLabel">{getOptionLabel(option)}</span>
<svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Checkmark100" />
</svg>
</li>
{/each}
</ul>
</div>
{/if}
{/if}
<style>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 999;
}
.spectrum-Popover {
max-height: 240px;
width: var(--spectrum-global-dimension-size-2400);
z-index: 999;
}
</style>

View File

@ -0,0 +1,76 @@
<script>
import { getContext } from "svelte"
import SpectrumField from "./SpectrumField.svelte"
import Picker from "./Picker.svelte"
const { API } = getContext("sdk")
export let field
export let label
let fieldState
let fieldApi
let fieldSchema
// Picker state
let options = []
let tableDefinition
$: fieldText = `${$fieldState?.value?.length ?? 0} selected rows`
$: valueLookupMap = getValueLookupMap($fieldState?.value)
$: isOptionSelected = option => valueLookupMap[option] === true
$: linkedTableId = fieldSchema?.tableId
$: fetchRows(linkedTableId)
$: fetchTable(linkedTableId)
const fetchTable = async id => {
if (id != null) {
tableDefinition = await API.fetchTableDefinition(id)
}
}
const fetchRows = async id => {
if (id != null) {
options = await API.fetchTableData(id)
}
}
const getDisplayName = row => {
return row[tableDefinition?.primaryDisplay || "_id"]
}
const getValueLookupMap = value => {
let map = {}
if (value?.length) {
value.forEach(option => {
map[option] = true
})
}
return map
}
const toggleOption = option => {
if ($fieldState.value.includes(option)) {
fieldApi.setValue($fieldState.value.filter(x => x !== option))
} else {
fieldApi.setValue([...$fieldState.value, option])
}
}
</script>
<SpectrumField
{label}
{field}
bind:fieldState
bind:fieldApi
bind:fieldSchema
defaultValue={[]}>
<Picker
{fieldState}
{fieldText}
{options}
{isOptionSelected}
onSelectOption={toggleOption}
getOptionLabel={getDisplayName}
getOptionValue={option => option._id} />
</SpectrumField>

View File

@ -7,3 +7,4 @@ export { default as booleanfield } from "./BooleanField.svelte"
export { default as longformfield } from "./LongFormField.svelte" export { default as longformfield } from "./LongFormField.svelte"
export { default as datetimefield } from "./DateTimeField.svelte" export { default as datetimefield } from "./DateTimeField.svelte"
export { default as attachmentfield } from "./AttachmentField.svelte" export { default as attachmentfield } from "./AttachmentField.svelte"
export { default as relationshipfield } from "./RelationshipField.svelte"