add option to change icon / colour

This commit is contained in:
Peter Clement 2021-12-08 18:51:24 +00:00
parent 53557e1bb1
commit efb50f0050
9 changed files with 237 additions and 98 deletions

View File

@ -6,6 +6,7 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import clickOutside from "../../Actions/click_outside" import clickOutside from "../../Actions/click_outside"
import Search from "./Search.svelte" import Search from "./Search.svelte"
import Icon from "../../Icon/Icon.svelte"
export let id = null export let id = null
export let disabled = false export let disabled = false
@ -159,17 +160,21 @@
> >
{#if getOptionIcon(option, idx)} {#if getOptionIcon(option, idx)}
<span class="icon-Padding"> <span class="icon-Padding">
<img {#if getOptionIcon(option, idx).includes("assets")}
src={getOptionIcon(option, idx)} <img
alt="icon" src={getOptionIcon(option, idx)}
width="20" alt="icon"
height="15" width="20"
/> height="15"
/>
{:else}<Icon name={getOptionIcon(option, idx)} />{/if}
</span>
{/if}
{#if getOptionLabel(option, idx)}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span> </span>
{/if} {/if}
<span class="spectrum-Menu-itemLabel">
{getOptionLabel(option, idx)}
</span>
<svg <svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
focusable="false" focusable="false"

View File

@ -7,7 +7,7 @@
export let disabled = false export let disabled = false
export let id = null export let id = null
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focus = false let focus = false
@ -41,6 +41,7 @@
<div class="spectrum-Search" class:is-disabled={disabled}> <div class="spectrum-Search" class:is-disabled={disabled}>
<div <div
class="spectrum-Textfield" class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet}
class:is-focused={focus} class:is-focused={focus}
class:is-disabled={disabled} class:is-disabled={disabled}
> >

View File

@ -9,6 +9,7 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,6 +24,7 @@
{disabled} {disabled}
{value} {value}
{placeholder} {placeholder}
{quiet}
on:change={onChange} on:change={onChange}
on:click on:click
on:input on:input

View File

@ -1,6 +1,6 @@
<script> <script>
import { gradient } from "actions"
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui" import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
import { apps } from "stores/portal"
export let app export let app
export let exportApp export let exportApp
@ -10,14 +10,23 @@
export let deleteApp export let deleteApp
export let unpublishApp export let unpublishApp
export let releaseLock export let releaseLock
export let editIcon
$: color = $apps.filter(filtered_app => app?.appId === filtered_app.appId)[0]
.icon?.color
$: name = $apps.filter(filtered_app => app?.appId === filtered_app.appId)[0]
.icon?.name
</script> </script>
<div class="title"> <div class="title">
<div class="preview" use:gradient={{ seed: app.name }} /> <div style="display: flex;">
<div class="name" on:click={() => editApp(app)}> <div style="color: {color || ''}">
<Heading size="XS"> <Icon size="XL" name={name || "Apps"} />
{app.name} </div>
</Heading> <div class="name" on:click={() => editApp(app)}>
<Heading size="XS">
{app.name}
</Heading>
</div>
</div> </div>
</div> </div>
<div class="desktop" /> <div class="desktop" />
@ -53,15 +62,11 @@
<MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem> <MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem> <MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
{/if} {/if}
<MenuItem on:click={() => editIcon(app)} icon="Edit">Edit Icon</MenuItem>
</ActionMenu> </ActionMenu>
</div> </div>
<style> <style>
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.name { .name {
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
@ -70,6 +75,7 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-left: calc(1.5 * var(--spacing-xl));
} }
.title :global(h1:hover) { .title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600); color: var(--spectrum-global-color-blue-600);

View File

@ -0,0 +1,126 @@
<script>
import {
Input,
ModalContent,
Body,
Modal,
Icon,
ColorPicker,
Label,
} from "@budibase/bbui"
import { apps } from "stores/portal"
export let app
let modal
let dirty
let selectedIcon
let selectedColor
let iconsList = [
{ icon: "Actions", color: "" },
{ icon: "Algorithm", color: "" },
{ icon: "App", color: "" },
{ icon: "Briefcase", color: "" },
{ icon: "Money", color: "" },
{ icon: "ShoppingCart", color: "" },
{ icon: "Form", color: "" },
{ icon: "Help", color: "" },
{ icon: "Monitoring", color: "" },
{ icon: "Sandbox", color: "" },
{ icon: "Project", color: "" },
{ icon: "Organisations", color: "" },
{ icon: "Magnify", color: "" },
{ icon: "Launch", color: "" },
]
export const show = () => {
modal.show()
}
export const hide = () => {
modal.hide()
}
const onCancel = () => {
hide()
}
const onShow = () => {
dirty = false
}
const changeColor = val => {
selectedColor = val
}
const save = async () => {
await apps.updateIcon(app.instance._id, {
name: selectedIcon,
color: selectedColor,
})
}
</script>
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}>
<ModalContent
title={"Edit Icon"}
confirmText={"Save"}
onConfirm={() => save()}
>
<div class="scrollable-icons">
<div class="title-spacing">
<Label>Select an Icon:</Label>
</div>
<div class="grid">
{#each iconsList as item}
<div
class="icon-item"
style="color: {item.icon === selectedIcon ? selectedColor : ''}"
on:click={() => (selectedIcon = item.icon)}
>
<Icon name={item.icon} />
</div>
{/each}
</div>
</div>
<div class="color-selection">
<div>
<Label>Select a Color:</Label>
</div>
<div class="color-selection-item">
<ColorPicker
value={selectedColor}
on:change={e => changeColor(e.detail)}
/>
</div>
</div>
</ModalContent>
</Modal>
<style>
.scrollable-icons {
overflow-y: auto;
height: 120px;
}
.grid {
display: grid;
grid-gap: 20px;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
.color-selection {
display: flex;
align-items: center;
}
.color-selection-item {
margin-left: 20px;
}
.title-spacing {
margin-bottom: 20px;
}
.icon-item {
cursor: pointer;
}
</style>

View File

@ -1,15 +1,9 @@
<script> <script>
import { writable, get as svelteGet } from "svelte/store" import { writable, get as svelteGet } from "svelte/store"
import {
notifications, import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
Input,
ModalContent,
Dropzone,
Body,
Checkbox,
} from "@budibase/bbui"
import { store, automationStore, hostingStore } from "builderStore" import { store, automationStore, hostingStore } from "builderStore"
import { admin, auth } from "stores/portal" import { admin, auth, users } from "stores/portal"
import { string, mixed, object } from "yup" import { string, mixed, object } from "yup"
import api, { get, post } from "builderStore/api" import api, { get, post } from "builderStore/api"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
@ -21,7 +15,6 @@
export let template export let template
export let inline export let inline
const values = writable({ name: null }) const values = writable({ name: null })
const errors = writable({}) const errors = writable({})
const touched = writable({}) const touched = writable({})
@ -147,16 +140,6 @@
} }
} }
function getModalTitle() {
let title = "Create App"
if (template.fromFile) {
title = "Import App"
} else if (template.key) {
title = "Create app from template"
}
return title
}
async function onCancel() { async function onCancel() {
template = null template = null
await auth.setInitInfo({}) await auth.setInitInfo({})
@ -187,7 +170,7 @@
</ModalContent> </ModalContent>
{:else} {:else}
<ModalContent <ModalContent
title={getModalTitle()} title={"Name your app"}
confirmText={template?.fromFile ? "Import app" : "Create app"} confirmText={template?.fromFile ? "Import app" : "Create app"}
onConfirm={createNewApp} onConfirm={createNewApp}
onCancel={inline ? onCancel : null} onCancel={inline ? onCancel : null}
@ -207,16 +190,12 @@
}} }}
/> />
{/if} {/if}
<Body size="S">
Give your new app a name, and choose which groups have access (paid plans
only).
</Body>
<Input <Input
bind:value={$values.name} bind:value={$values.name}
error={$touched.name && $errors.name} error={$touched.name && $errors.name}
on:blur={() => ($touched.name = true)} on:blur={() => ($touched.name = true)}
label="Name" label="Name"
placeholder={`${$auth.user.firstName}'s first app`}
/> />
<Checkbox label="Group access" disabled value={true} text="All users" />
</ModalContent> </ModalContent>
{/if} {/if}

View File

@ -1,46 +1,10 @@
<script> <script>
import { Heading, Layout, Icon, Body } from "@budibase/bbui" import { Heading, Layout, Icon } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import api from "builderStore/api"
export let onSelect export let onSelect
async function fetchTemplates() {
const response = await api.get("/api/templates?type=app")
return await response.json()
}
let templatesPromise = fetchTemplates()
</script> </script>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#await templatesPromise}
<div class="spinner-container">
<Spinner size="30" />
</div>
{:then templates}
{#if templates?.length > 0}
<Body size="M">Select a template below, or start from scratch.</Body>
{:else}
<Body size="M">Start your app from scratch below.</Body>
{/if}
<div class="templates">
{#each templates as template}
<div class="template" on:click={() => onSelect(template)}>
<div
class="background-icon"
style={`background: ${template.background};`}
>
<Icon name={template.icon} />
</div>
<Heading size="XS">{template.name}</Heading>
<p class="detail">{template?.category?.toUpperCase()}</p>
</div>
{/each}
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<div class="template start-from-scratch" on:click={() => onSelect(null)}> <div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div <div
class="background-icon" class="background-icon"

View File

@ -16,6 +16,8 @@
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte" import UpdateAppModal from "components/start/UpdateAppModal.svelte"
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import api, { del, post, get } from "builderStore/api" import api, { del, post, get } from "builderStore/api"
import { onMount } from "svelte" import { onMount } from "svelte"
@ -34,6 +36,7 @@
let updatingModal let updatingModal
let deletionModal let deletionModal
let unpublishModal let unpublishModal
let iconModal
let creatingApp = false let creatingApp = false
let loaded = false let loaded = false
let searchTerm = "" let searchTerm = ""
@ -170,6 +173,11 @@
$goto(`../../app/${app.devId}`) $goto(`../../app/${app.devId}`)
} }
const editIcon = app => {
selectedApp = app
iconModal.show()
}
const exportApp = app => { const exportApp = app => {
const id = app.deployed ? app.prodId : app.devId const id = app.deployed ? app.prodId : app.devId
const appName = encodeURIComponent(app.name) const appName = encodeURIComponent(app.name)
@ -279,7 +287,7 @@
</script> </script>
<Page wide> <Page wide>
<Layout gap="S" noPadding> <Layout noPadding>
<div class="title"> <div class="title">
<Heading size="S">Welcome to Budibase</Heading> <Heading size="S">Welcome to Budibase</Heading>
@ -287,32 +295,45 @@
{#if cloud} {#if cloud}
<Button secondary on:click={initiateAppsExport}>Export apps</Button> <Button secondary on:click={initiateAppsExport}>Export apps</Button>
{/if} {/if}
<Button secondary on:click={initiateAppImport}>Import app</Button> <Button icon="Import" quiet secondary on:click={initiateAppImport}
<Button cta on:click={initiateAppCreation}>Create app</Button> >Import app</Button
>
<Button icon="Add" cta on:click={initiateAppCreation}>Create app</Button
>
</ButtonGroup> </ButtonGroup>
</div> </div>
<Body size="XS">Manage your apps and get a head start with templates</Body>
<div class="title-text">
<Body size="XS">Manage your apps and get a head start with templates</Body
>
</div>
<Detail>Quick Start Templates</Detail> <Detail>Quick Start Templates</Detail>
<div class="grid"> <div class="grid">
{#each templates as val} {#each templates as item}
<div class="template-card"> <div
on:click={() => {
template = item
creationModal.show()
creatingApp = true
}}
class="template-card"
>
<div class="card-body"> <div class="card-body">
<div style="color: {val.background}" class="iconAlign"> <div style="color: {item.background}" class="iconAlign">
<svg <svg
width="26px" width="26px"
height="26px" height="26px"
class="spectrum-Icon" class="spectrum-Icon"
style="color:{val.background};" style="color:{item.background};"
focusable="false" focusable="false"
> >
<use xlink:href="#spectrum-icon-18-{val.icon}" /> <use xlink:href="#spectrum-icon-18-{item.icon}" />
</svg> </svg>
</div> </div>
<div class="iconAlign"> <div class="iconAlign">
<Body weight="900" size="XS">{val.name}</Body> <Body weight="900" size="XS">{item.name}</Body>
<div style="font-size: 10px;"> <div style="font-size: 10px;">
<Body size="XS">{val.category.toUpperCase()}</Body> <Body size="XS">{item.category.toUpperCase()}</Body>
</div> </div>
</div> </div>
</div> </div>
@ -326,6 +347,7 @@
<div class="filter"> <div class="filter">
<div class="select"> <div class="select">
<Select <Select
quiet
autoWidth autoWidth
bind:value={sortBy} bind:value={sortBy}
placeholder={null} placeholder={null}
@ -336,7 +358,7 @@
]} ]}
/> />
<div class="desktop-search"> <div class="desktop-search">
<Search placeholder="Search" bind:value={searchTerm} /> <Search quiet placeholder="Search" bind:value={searchTerm} />
</div> </div>
</div> </div>
</div> </div>
@ -348,6 +370,7 @@
<svelte:component <svelte:component
this={AppRow} this={AppRow}
{releaseLock} {releaseLock}
{editIcon}
{app} {app}
{unpublishApp} {unpublishApp}
{viewApp} {viewApp}
@ -374,6 +397,7 @@
{/if} {/if}
</Layout> </Layout>
</Page> </Page>
<Modal <Modal
bind:this={creationModal} bind:this={creationModal}
padding={false} padding={false}
@ -409,6 +433,7 @@
</ConfirmDialog> </ConfirmDialog>
<UpdateAppModal app={selectedApp} bind:this={updatingModal} /> <UpdateAppModal app={selectedApp} bind:this={updatingModal} />
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
<style> <style>
.title, .title,
@ -440,6 +465,11 @@
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
cursor: pointer;
}
.title-text {
margin-top: calc(var(--spacing-xl) * -1);
} }
.card-body { .card-body {
@ -459,8 +489,8 @@
.select { .select {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: auto auto;
grid-gap: 10px; grid-gap: 50px;
} }
.filter :global(.spectrum-ActionGroup) { .filter :global(.spectrum-ActionGroup) {
flex-wrap: nowrap; flex-wrap: nowrap;
@ -509,4 +539,8 @@
display: block; display: block;
} }
} }
.template-card:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
</style> </style>

View File

@ -65,6 +65,27 @@ export function createAppStore() {
} }
} }
async function updateIcon(appId, icon) {
const response = await api.put(`/api/applications/${appId}`, { icon })
if (response.status === 200) {
store.update(state => {
const updatedAppIndex = state.findIndex(
app => app.instance._id === appId
)
if (updatedAppIndex !== -1) {
const updatedApp = state[updatedAppIndex]
updatedApp.icon = icon
state.apps = state.splice(updatedAppIndex, 1, updatedApp)
}
return state
})
} else {
throw new Error("Error updating icon")
}
}
async function update(appId, name) { async function update(appId, name) {
const response = await api.put(`/api/applications/${appId}`, { name }) const response = await api.put(`/api/applications/${appId}`, { name })
if (response.status === 200) { if (response.status === 200) {
@ -88,6 +109,7 @@ export function createAppStore() {
subscribe: store.subscribe, subscribe: store.subscribe,
load, load,
update, update,
updateIcon,
} }
} }