forked from tsai/budibase
23 changed files with 951 additions and 252 deletions
@ -0,0 +1,215 @@ |
|||
<script> |
|||
import "@spectrum-css/inputgroup/dist/index-vars.css" |
|||
import "@spectrum-css/popover/dist/index-vars.css" |
|||
import "@spectrum-css/menu/dist/index-vars.css" |
|||
import { fly } from "svelte/transition" |
|||
import { createEventDispatcher } from "svelte" |
|||
import clickOutside from "../../Actions/click_outside" |
|||
|
|||
export let inputValue |
|||
export let dropdownValue |
|||
export let id = null |
|||
export let inputType = "text" |
|||
export let placeholder = "Choose an option or type" |
|||
export let disabled = false |
|||
export let readonly = false |
|||
export let updateOnChange = true |
|||
export let error = null |
|||
export let options = [] |
|||
export let getOptionLabel = option => extractProperty(option, "label") |
|||
export let getOptionValue = option => extractProperty(option, "value") |
|||
export let isOptionSelected = () => false |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
let open = false |
|||
let focus = false |
|||
|
|||
$: fieldText = getFieldText(dropdownValue, options, placeholder) |
|||
|
|||
const getFieldText = (dropdownValue, options, placeholder) => { |
|||
// Always use placeholder if no value |
|||
if (dropdownValue == null || dropdownValue === "") { |
|||
return placeholder || "Choose an option or type" |
|||
} |
|||
|
|||
// Wait for options to load if there is a value but no options |
|||
if (!options?.length) { |
|||
return "" |
|||
} |
|||
|
|||
// Render the label if the selected option is found, otherwise raw value |
|||
const selected = options.find( |
|||
option => getOptionValue(option) === dropdownValue |
|||
) |
|||
return selected ? getOptionLabel(selected) : dropdownValue |
|||
} |
|||
|
|||
const updateValue = newValue => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
dispatch("change", newValue) |
|||
} |
|||
|
|||
const onFocus = () => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
focus = true |
|||
} |
|||
|
|||
const onBlur = event => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
focus = false |
|||
updateValue(event.target.value) |
|||
} |
|||
|
|||
const onInput = event => { |
|||
if (readonly || !updateOnChange) { |
|||
return |
|||
} |
|||
updateValue(event.target.value) |
|||
} |
|||
|
|||
const updateValueOnEnter = event => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
if (event.key === "Enter") { |
|||
updateValue(event.target.value) |
|||
} |
|||
} |
|||
|
|||
const onClick = () => { |
|||
dispatch("click") |
|||
if (readonly) { |
|||
return |
|||
} |
|||
open = true |
|||
} |
|||
|
|||
const onPick = newValue => { |
|||
dispatch("pick", newValue) |
|||
open = false |
|||
} |
|||
|
|||
const extractProperty = (value, property) => { |
|||
if (value && typeof value === "object") { |
|||
return value[property] |
|||
} |
|||
return value |
|||
} |
|||
</script> |
|||
|
|||
<div |
|||
class="spectrum-InputGroup" |
|||
class:is-invalid={!!error} |
|||
class:is-disabled={disabled} |
|||
> |
|||
<div |
|||
class="spectrum-Textfield spectrum-InputGroup-textfield" |
|||
class:is-invalid={!!error} |
|||
class:is-disabled={disabled} |
|||
class:is-focused={focus} |
|||
> |
|||
<input |
|||
{id} |
|||
on:click |
|||
on:blur |
|||
on:focus |
|||
on:input |
|||
on:keyup |
|||
on:blur={onBlur} |
|||
on:focus={onFocus} |
|||
on:input={onInput} |
|||
on:keyup={updateValueOnEnter} |
|||
value={inputValue || ""} |
|||
placeholder={placeholder || ""} |
|||
{disabled} |
|||
{readonly} |
|||
{inputType} |
|||
class="spectrum-Textfield-input spectrum-InputGroup-input" |
|||
/> |
|||
</div> |
|||
<div style="width: 30%"> |
|||
<button |
|||
{id} |
|||
class="spectrum-Picker spectrum-Picker--sizeM override-borders" |
|||
{disabled} |
|||
class:is-open={open} |
|||
aria-haspopup="listbox" |
|||
on:mousedown={onClick} |
|||
> |
|||
<span class="spectrum-Picker-label"> |
|||
{fieldText} |
|||
</span> |
|||
<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 |
|||
use:clickOutside={() => (open = false)} |
|||
transition:fly|local={{ y: -20, duration: 200 }} |
|||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" |
|||
> |
|||
<ul class="spectrum-Menu" role="listbox"> |
|||
{#each options as option, idx} |
|||
<li |
|||
class="spectrum-Menu-item" |
|||
class:is-selected={isOptionSelected(getOptionValue(option, idx))} |
|||
role="option" |
|||
aria-selected="true" |
|||
tabindex="0" |
|||
on:click={() => onPick(getOptionValue(option, idx))} |
|||
> |
|||
<span class="spectrum-Menu-itemLabel"> |
|||
{getOptionLabel(option, idx)} |
|||
</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} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.spectrum-InputGroup { |
|||
min-width: 0; |
|||
width: 100%; |
|||
} |
|||
|
|||
.spectrum-InputGroup-input { |
|||
border-right-width: 1px; |
|||
} |
|||
.spectrum-Textfield { |
|||
width: 100%; |
|||
} |
|||
.spectrum-Textfield-input { |
|||
width: 0; |
|||
} |
|||
|
|||
.override-borders { |
|||
border-top-left-radius: 0px; |
|||
border-bottom-left-radius: 0px; |
|||
} |
|||
.spectrum-Popover { |
|||
max-height: 240px; |
|||
z-index: 999; |
|||
top: 100%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,55 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import InputDropdown from "./Core/InputDropdown.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let inputValue = null |
|||
export let dropdownValue = null |
|||
export let inputType = "text" |
|||
export let label = null |
|||
export let labelPosition = "above" |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let readonly = false |
|||
export let error = null |
|||
export let updateOnChange = true |
|||
export let quiet = false |
|||
export let dataCy |
|||
export let autofocus |
|||
export let options = [] |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
const onPick = e => { |
|||
dropdownValue = e.detail |
|||
dispatch("pick", e.detail) |
|||
} |
|||
const onChange = e => { |
|||
inputValue = e.detail |
|||
dispatch("change", e.detail) |
|||
} |
|||
</script> |
|||
|
|||
<Field {label} {labelPosition} {error}> |
|||
<InputDropdown |
|||
{dataCy} |
|||
{updateOnChange} |
|||
{error} |
|||
{disabled} |
|||
{readonly} |
|||
{inputValue} |
|||
{dropdownValue} |
|||
{placeholder} |
|||
{inputType} |
|||
{quiet} |
|||
{autofocus} |
|||
{options} |
|||
on:change={onChange} |
|||
on:pick={onPick} |
|||
on:click |
|||
on:input |
|||
on:blur |
|||
on:focus |
|||
on:keyup |
|||
/> |
|||
</Field> |
|||
@ -0,0 +1,73 @@ |
|||
<script> |
|||
import { ActionButton, Icon, Search, Divider, Detail } from "@budibase/bbui" |
|||
|
|||
export let searchTerm = "" |
|||
export let selected |
|||
export let filtered |
|||
export let addAll |
|||
export let select |
|||
export let title |
|||
export let key |
|||
</script> |
|||
|
|||
<div style="padding: var(--spacing-m)"> |
|||
<Search placeholder="Search" bind:value={searchTerm} /> |
|||
<div class="header sub-header"> |
|||
<div> |
|||
<Detail |
|||
>{filtered.length} {title}{filtered.length === 1 ? "" : "s"}</Detail |
|||
> |
|||
</div> |
|||
<div> |
|||
<ActionButton on:click={addAll} emphasized size="S">Add all</ActionButton> |
|||
</div> |
|||
</div> |
|||
<Divider noMargin /> |
|||
<div> |
|||
{#each filtered as item} |
|||
<div |
|||
on:click={select(item._id)} |
|||
style="padding-bottom: var(--spacing-m)" |
|||
class="selection" |
|||
> |
|||
<div> |
|||
{item[key]} |
|||
</div> |
|||
|
|||
{#if selected.includes(item._id)} |
|||
<div> |
|||
<Icon |
|||
color="var(--spectrum-global-color-blue-600);" |
|||
name="Checkmark" |
|||
/> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.header { |
|||
align-items: center; |
|||
padding: var(--spacing-m) 0 var(--spacing-m) 0; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.selection { |
|||
align-items: end; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.selection > :first-child { |
|||
padding-top: var(--spacing-m); |
|||
} |
|||
|
|||
.sub-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
</style> |
|||
@ -1,82 +1,78 @@ |
|||
<script> |
|||
import { |
|||
Body, |
|||
Input, |
|||
Select, |
|||
ActionButton, |
|||
Layout, |
|||
Label, |
|||
ModalContent, |
|||
Multiselect, |
|||
notifications, |
|||
Toggle, |
|||
Label, |
|||
InputDropdown, |
|||
} from "@budibase/bbui" |
|||
import { createValidationStore, emailValidator } from "helpers/validation" |
|||
import { users } from "stores/portal" |
|||
import { users, groups } from "stores/portal" |
|||
import analytics, { Events } from "analytics" |
|||
import { Constants } from "@budibase/frontend-core" |
|||
|
|||
export let disabled |
|||
|
|||
export let showOnboardingTypeModal |
|||
const options = ["Email onboarding", "Basic onboarding"] |
|||
let selected = options[0] |
|||
let builder, admin |
|||
|
|||
const [email, error, touched] = createValidationStore("", emailValidator) |
|||
$: userData = [{ email: "", role: "", error: null }] |
|||
|
|||
/* |
|||
async function createUserFlow() { |
|||
try { |
|||
const res = await users.invite({ email: $email, builder, admin }) |
|||
const res = await users.invite({ email: "", builder, admin }) |
|||
notifications.success(res.message) |
|||
analytics.captureEvent(Events.USER.INVITE, { type: selected }) |
|||
} catch (error) { |
|||
notifications.error("Error inviting user") |
|||
} |
|||
} |
|||
*/ |
|||
function addNewInput() { |
|||
userData = [...userData, { email: "", role: "" }] |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
onConfirm={createUserFlow} |
|||
onConfirm={showOnboardingTypeModal} |
|||
size="M" |
|||
title="Add new user" |
|||
confirmText="Add user" |
|||
confirmDisabled={disabled} |
|||
cancelText="Cancel" |
|||
disabled={$error} |
|||
showCloseIcon={false} |
|||
> |
|||
<Body size="S"> |
|||
If you have SMTP configured and an email for the new user, you can use the |
|||
automated email onboarding flow. Otherwise, use our basic onboarding process |
|||
with autogenerated passwords. |
|||
</Body> |
|||
<Select |
|||
placeholder={null} |
|||
bind:value={selected} |
|||
on:change |
|||
{options} |
|||
label="Add new user via:" |
|||
/> |
|||
<Input |
|||
type="email" |
|||
bind:value={$email} |
|||
error={$touched && $error} |
|||
placeholder="john@doe.com" |
|||
label="Email" |
|||
/> |
|||
<div> |
|||
<div class="toggle"> |
|||
<Label size="L">Development access</Label> |
|||
<Toggle text="" bind:value={builder} /> |
|||
</div> |
|||
<div class="toggle"> |
|||
<Label size="L">Administration access</Label> |
|||
<Toggle text="" bind:value={admin} /> |
|||
<Layout noPadding gap="XS"> |
|||
<Label>Email Address</Label> |
|||
|
|||
{#each userData as input, index} |
|||
<InputDropdown |
|||
inputType="email" |
|||
bind:inputValue={input.email} |
|||
bind:dropdownValue={input.role} |
|||
options={Constants.BbRoles} |
|||
error={input.error} |
|||
/> |
|||
{/each} |
|||
<div> |
|||
<ActionButton on:click={addNewInput} icon="Add">Add email</ActionButton> |
|||
</div> |
|||
</div> |
|||
</Layout> |
|||
|
|||
<Multiselect |
|||
placeholder="Select User Groups" |
|||
label="User Groups" |
|||
options={$groups} |
|||
getOptionLabel={option => option.name} |
|||
getOptionValue={option => option.name} |
|||
/> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.toggle { |
|||
display: grid; |
|||
grid-template-columns: 78% 1fr; |
|||
align-items: center; |
|||
width: 50%; |
|||
:global(.spectrum-Picker) { |
|||
border-top-left-radius: 0px; |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,52 @@ |
|||
<script> |
|||
import { Body, ModalContent, RadioGroup, Multiselect } from "@budibase/bbui" |
|||
import { groups } from "stores/portal" |
|||
|
|||
import { Constants } from "@budibase/frontend-core" |
|||
</script> |
|||
|
|||
<ModalContent |
|||
size="M" |
|||
title="Import users" |
|||
confirmText="Done" |
|||
showCancelButton={false} |
|||
cancelText="Cancel" |
|||
showCloseIcon={false} |
|||
> |
|||
<Body size="S">Import your users email addrresses from a CSV</Body> |
|||
|
|||
<div class="container"> |
|||
<div class="inner"> |
|||
<Body size="S">Upload</Body> |
|||
</div> |
|||
</div> |
|||
|
|||
<RadioGroup options={Constants.BuilderRoleDescriptions} /> |
|||
|
|||
<Multiselect |
|||
placeholder="Select User Groups" |
|||
label="User Groups" |
|||
options={$groups} |
|||
getOptionLabel={option => option.name} |
|||
getOptionValue={option => option.name} |
|||
/> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.inner { |
|||
display: flex; |
|||
} |
|||
|
|||
:global(.spectrum-Picker) { |
|||
border-top-left-radius: 0px; |
|||
} |
|||
|
|||
.container { |
|||
width: 100%; |
|||
height: var(--spectrum-alias-item-height-l); |
|||
background: var(--spectrum-global-color-gray-200); |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,107 @@ |
|||
<script> |
|||
import { ModalContent, Body, Layout, Icon } from "@budibase/bbui" |
|||
export let showConfirmationModal |
|||
let emailOnboardingKey = "emailOnboarding" |
|||
let basicOnboaridngKey = "basicOnboarding" |
|||
|
|||
let selectedOnboardingType |
|||
</script> |
|||
|
|||
<ModalContent |
|||
size="M" |
|||
title="Choose your onboarding" |
|||
confirmText="Done" |
|||
cancelText="Cancel" |
|||
showCloseIcon={false} |
|||
onConfirm={() => showConfirmationModal(selectedOnboardingType)} |
|||
disabled={!selectedOnboardingType} |
|||
> |
|||
<Layout noPadding gap="S"> |
|||
<div |
|||
class="onboarding-type item" |
|||
class:selected={selectedOnboardingType == emailOnboardingKey} |
|||
on:click={() => { |
|||
selectedOnboardingType = emailOnboardingKey |
|||
}} |
|||
> |
|||
<div class="content onboarding-type-wrap"> |
|||
<Icon name="WebPage" /> |
|||
<div class="onboarding-type-text"> |
|||
<Body size="S">Send email invites</Body> |
|||
</div> |
|||
</div> |
|||
<div style="color: var(--spectrum-global-color-green-600); float: right"> |
|||
{#if selectedOnboardingType == emailOnboardingKey} |
|||
<div class="checkmark-spacing"> |
|||
<Icon size="S" name="CheckmarkCircle" /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
class="onboarding-type item" |
|||
class:selected={selectedOnboardingType == basicOnboaridngKey} |
|||
on:click={() => { |
|||
selectedOnboardingType = basicOnboaridngKey |
|||
}} |
|||
> |
|||
<div class="content onboarding-type-wrap"> |
|||
<Icon name="Key" /> |
|||
<div class="onboarding-type-text"> |
|||
<Body size="S">Generate passwords for each user</Body> |
|||
</div> |
|||
</div> |
|||
<div style="color: var(--spectrum-global-color-green-600); float: right"> |
|||
{#if selectedOnboardingType == basicOnboaridngKey} |
|||
<div class="checkmark-spacing"> |
|||
<Icon size="S" name="CheckmarkCircle" /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
</Layout> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.onboarding-type.item { |
|||
padding: var(--spectrum-alias-item-padding-xl); |
|||
} |
|||
.onboarding-type-wrap { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
} |
|||
.checkmark-spacing { |
|||
margin-right: var(--spacing-m); |
|||
} |
|||
.content { |
|||
letter-spacing: 0px; |
|||
} |
|||
.item { |
|||
cursor: pointer; |
|||
grid-gap: var(--spectrum-alias-grid-margin-xsmall); |
|||
padding: var(--spectrum-alias-item-padding-s); |
|||
background: var(--spectrum-alias-background-color-primary); |
|||
transition: 0.3s all; |
|||
border: 1px solid var(--spectrum-global-color-gray-300); |
|||
border-radius: 4px; |
|||
border-width: 1px; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.item:hover, |
|||
.selected { |
|||
background: var(--spectrum-alias-background-color-tertiary); |
|||
} |
|||
.onboarding-type-wrap .onboarding-type-text { |
|||
padding-left: var(--spectrum-alias-item-padding-xl); |
|||
} |
|||
.onboarding-type-wrap :global(.spectrum-Icon) { |
|||
min-width: var(--spectrum-icon-size-m); |
|||
} |
|||
.onboarding-type-wrap :global(.spectrum-Heading) { |
|||
padding-bottom: var(--spectrum-alias-item-padding-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { InternalRenderer } from "@budibase/bbui" |
|||
|
|||
export let value |
|||
</script> |
|||
|
|||
<div style="display: flex; "> |
|||
{value} |
|||
<div style="margin-left: 1.5rem;"> |
|||
<InternalRenderer {value} /> |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
</style> |
|||
@ -0,0 +1,61 @@ |
|||
<script> |
|||
import { Body, ModalContent, Table, Icon } from "@budibase/bbui" |
|||
import PasswordCopyRenderer from "./PasswordCopyRenderer.svelte" |
|||
|
|||
const schema = { |
|||
email: {}, |
|||
password: {}, |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
size="S" |
|||
title="Accounts created!" |
|||
confirmText="Done" |
|||
showCancelButton={false} |
|||
cancelText="Cancel" |
|||
showCloseIcon={false} |
|||
> |
|||
<Body size="XS" |
|||
>All your new users can be accessed through the autogenerated passwords. |
|||
Make not of these passwords or download the csv</Body |
|||
> |
|||
|
|||
<div class="container"> |
|||
<div class="inner"> |
|||
<Icon name="Download" /> |
|||
|
|||
<div style="margin-left: var(--spacing-m)"> |
|||
<Body size="XS">Passwords CSV</Body> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<Table |
|||
{schema} |
|||
data={[{ email: "test", password: "§xz§§zvzxvxzv" }]} |
|||
allowEditColumns={false} |
|||
allowEditRows={false} |
|||
allowSelectRows={false} |
|||
customRenderers={[{ column: "password", component: PasswordCopyRenderer }]} |
|||
/> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.inner { |
|||
display: flex; |
|||
} |
|||
|
|||
:global(.spectrum-Picker) { |
|||
border-top-left-radius: 0px; |
|||
} |
|||
|
|||
.container { |
|||
width: 100%; |
|||
height: var(--spectrum-alias-item-height-l); |
|||
background: #009562; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue