Merge branch 'master' of github.com:Budibase/budibase into feature/app-updated-at
This commit is contained in:
commit
ede7e38442
|
@ -41,4 +41,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid-template-columns: 200px 20px;
|
grid-template-columns: 200px 20px;
|
||||||
}
|
}
|
||||||
|
.icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Layout,
|
|
||||||
Body,
|
|
||||||
Heading,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { organisation, auth } from "stores/portal"
|
|
||||||
|
|
||||||
let email = ""
|
|
||||||
|
|
||||||
async function forgot() {
|
|
||||||
try {
|
|
||||||
await auth.forgotPassword(email)
|
|
||||||
notifications.success("Email sent - please check your inbox")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Unable to send reset password link")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="login">
|
|
||||||
<div class="main">
|
|
||||||
<Layout>
|
|
||||||
<Layout noPadding justifyItems="center">
|
|
||||||
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Heading textAlign="center">Forgotten your password?</Heading>
|
|
||||||
<Body size="S" textAlign="center">
|
|
||||||
No problem! Just enter your account's email address and we'll send you
|
|
||||||
a link to reset it.
|
|
||||||
</Body>
|
|
||||||
<Input label="Email" bind:value={email} />
|
|
||||||
</Layout>
|
|
||||||
<Button cta on:click={forgot} disabled={!email}>
|
|
||||||
Reset your password
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.login {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,84 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto, params } from "@roxi/routify"
|
|
||||||
import {
|
|
||||||
notifications,
|
|
||||||
Input,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
ActionButton,
|
|
||||||
Layout,
|
|
||||||
Body,
|
|
||||||
Heading,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import GoogleButton from "./GoogleButton.svelte"
|
|
||||||
import { organisation, auth } from "stores/portal"
|
|
||||||
|
|
||||||
let username = ""
|
|
||||||
let password = ""
|
|
||||||
|
|
||||||
async function login() {
|
|
||||||
try {
|
|
||||||
await auth.login({
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
if ($params["?returnUrl"]) {
|
|
||||||
window.location = decodeURIComponent($params["?returnUrl"])
|
|
||||||
} else {
|
|
||||||
notifications.success("Logged in successfully")
|
|
||||||
$goto("../portal")
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
notifications.error("Invalid credentials")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="login">
|
|
||||||
<div class="main">
|
|
||||||
<Layout>
|
|
||||||
<Layout noPadding justifyItems="center">
|
|
||||||
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
|
||||||
<Heading>Sign in to Budibase</Heading>
|
|
||||||
</Layout>
|
|
||||||
<GoogleButton />
|
|
||||||
<Divider noGrid />
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Body size="S" textAlign="center">Sign in with email</Body>
|
|
||||||
<Input label="Email" bind:value={username} />
|
|
||||||
<Input
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
on:change
|
|
||||||
bind:value={password}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Button cta on:click={login}>Sign in to Budibase</Button>
|
|
||||||
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
|
||||||
Forgot password?
|
|
||||||
</ActionButton>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.login {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,59 +0,0 @@
|
||||||
<script>
|
|
||||||
import { notifications, Button, Layout, Body, Heading } from "@budibase/bbui"
|
|
||||||
import { organisation, auth } from "stores/portal"
|
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
|
||||||
import { params, goto } from "@roxi/routify"
|
|
||||||
|
|
||||||
const resetCode = $params["?code"]
|
|
||||||
let password, error
|
|
||||||
|
|
||||||
async function reset() {
|
|
||||||
try {
|
|
||||||
await auth.resetPassword(password, resetCode)
|
|
||||||
notifications.success("Password reset successfully")
|
|
||||||
// send them to login if reset successful
|
|
||||||
$goto("./login")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Unable to reset password")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="login">
|
|
||||||
<div class="main">
|
|
||||||
<Layout>
|
|
||||||
<Layout noPadding justifyItems="center">
|
|
||||||
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
|
||||||
</Layout>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Heading textAlign="center">Reset your password</Heading>
|
|
||||||
<Body size="S" textAlign="center">
|
|
||||||
Please enter the new password you'd like to use.
|
|
||||||
</Body>
|
|
||||||
<PasswordRepeatInput bind:password bind:error />
|
|
||||||
</Layout>
|
|
||||||
<Button cta on:click={reset} disabled={error || !resetCode}>
|
|
||||||
Reset your password
|
|
||||||
</Button>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.login {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
width: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1 +0,0 @@
|
||||||
export { LoginForm } from "./LoginForm.svelte"
|
|
|
@ -8,7 +8,6 @@
|
||||||
MenuItem,
|
MenuItem,
|
||||||
StatusLight,
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { auth } from "stores/portal"
|
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let exportApp
|
export let exportApp
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { isActive, redirect } from "@roxi/routify"
|
import { isActive, redirect } from "@roxi/routify"
|
||||||
import { admin, auth } from "stores/portal"
|
import { admin, auth } from "stores/portal"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
||||||
|
@ -30,6 +30,8 @@
|
||||||
) {
|
) {
|
||||||
const returnUrl = encodeURIComponent(window.location.pathname)
|
const returnUrl = encodeURIComponent(window.location.pathname)
|
||||||
$redirect("./auth/login?", { returnUrl })
|
$redirect("./auth/login?", { returnUrl })
|
||||||
|
} else if ($auth?.user?.forceResetPassword) {
|
||||||
|
$redirect("./auth/reset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,62 @@
|
||||||
<script>
|
<script>
|
||||||
import ForgotForm from "components/login/ForgotForm.svelte"
|
import {
|
||||||
|
notifications,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
|
Heading,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { organisation, auth } from "stores/portal"
|
||||||
|
|
||||||
|
let email = ""
|
||||||
|
|
||||||
|
async function forgot() {
|
||||||
|
try {
|
||||||
|
await auth.forgotPassword(email)
|
||||||
|
notifications.success("Email sent - please check your inbox")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Unable to send reset password link")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ForgotForm />
|
<div class="login">
|
||||||
|
<div class="main">
|
||||||
|
<Layout>
|
||||||
|
<Layout noPadding justifyItems="center">
|
||||||
|
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading textAlign="center">Forgotten your password?</Heading>
|
||||||
|
<Body size="S" textAlign="center">
|
||||||
|
No problem! Just enter your account's email address and we'll send you
|
||||||
|
a link to reset it.
|
||||||
|
</Body>
|
||||||
|
<Input label="Email" bind:value={email} />
|
||||||
|
</Layout>
|
||||||
|
<Button cta on:click={forgot} disabled={!email}>
|
||||||
|
Reset your password
|
||||||
|
</Button>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,5 +1,89 @@
|
||||||
<script>
|
<script>
|
||||||
import LoginForm from "components/login/LoginForm.svelte"
|
import {
|
||||||
|
ActionButton,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { goto, params } from "@roxi/routify"
|
||||||
|
import { auth, organisation } from "stores/portal"
|
||||||
|
import GoogleButton from "./_components/GoogleButton.svelte"
|
||||||
|
|
||||||
|
let username = ""
|
||||||
|
let password = ""
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
try {
|
||||||
|
await auth.login({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
notifications.success("Logged in successfully")
|
||||||
|
if ($auth?.user?.forceResetPassword) {
|
||||||
|
$goto("./reset")
|
||||||
|
} else {
|
||||||
|
if ($params["?returnUrl"]) {
|
||||||
|
window.location = decodeURIComponent($params["?returnUrl"])
|
||||||
|
} else {
|
||||||
|
notifications.success("Logged in successfully")
|
||||||
|
$goto("../portal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
notifications.error("Invalid credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LoginForm />
|
<div class="login">
|
||||||
|
<div class="main">
|
||||||
|
<Layout>
|
||||||
|
<Layout noPadding justifyItems="center">
|
||||||
|
<img src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"} />
|
||||||
|
<Heading>Sign in to Budibase</Heading>
|
||||||
|
</Layout>
|
||||||
|
<GoogleButton />
|
||||||
|
<Divider noGrid />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Body size="S" textAlign="center">Sign in with email</Body>
|
||||||
|
<Input label="Email" bind:value={username} />
|
||||||
|
<Input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
on:change
|
||||||
|
bind:value={password}
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Button cta on:click={login}>Sign in to Budibase</Button>
|
||||||
|
<ActionButton quiet on:click={() => $goto("./forgot")}>
|
||||||
|
Forgot password?
|
||||||
|
</ActionButton>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,5 +1,76 @@
|
||||||
<script>
|
<script>
|
||||||
import ResetForm from "components/login/ResetForm.svelte"
|
import { Body, Button, Heading, Layout, notifications } from "@budibase/bbui"
|
||||||
|
import { goto, params } from "@roxi/routify"
|
||||||
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
|
import { auth, organisation } from "stores/portal"
|
||||||
|
|
||||||
|
const resetCode = $params["?code"]
|
||||||
|
let password, error
|
||||||
|
|
||||||
|
$: forceResetPassword = $auth?.user?.forceResetPassword
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
try {
|
||||||
|
if (forceResetPassword) {
|
||||||
|
await auth.updateSelf({
|
||||||
|
...$auth.user,
|
||||||
|
password,
|
||||||
|
forceResetPassword: false,
|
||||||
|
})
|
||||||
|
$goto("../portal/")
|
||||||
|
} else {
|
||||||
|
await auth.resetPassword(password, resetCode)
|
||||||
|
notifications.success("Password reset successfully")
|
||||||
|
// send them to login if reset successful
|
||||||
|
$goto("./login")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Unable to reset password")
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ResetForm />
|
<div class="login">
|
||||||
|
<div class="main">
|
||||||
|
<Layout>
|
||||||
|
<Layout noPadding justifyItems="center">
|
||||||
|
<img
|
||||||
|
src={$organisation.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
|
||||||
|
alt="Organisation logo"
|
||||||
|
/>
|
||||||
|
</Layout>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading textAlign="center">Reset your password</Heading>
|
||||||
|
<Body size="S" textAlign="center">
|
||||||
|
Please enter the new password you'd like to use.
|
||||||
|
</Body>
|
||||||
|
<PasswordRepeatInput bind:password bind:error />
|
||||||
|
</Layout>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={reset}
|
||||||
|
disabled={error || (forceResetPassword ? false : !resetCode)}
|
||||||
|
>Reset your password</Button
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.login {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,35 +1,34 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { onMount, tick } from "svelte"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Detail,
|
Detail,
|
||||||
Heading,
|
Heading,
|
||||||
notifications,
|
ActionButton,
|
||||||
Icon,
|
Body,
|
||||||
Page,
|
Page,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { fade } from "svelte/transition"
|
|
||||||
import { email } from "stores/portal"
|
import { email } from "stores/portal"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
import Editor from "components/integration/QueryEditor.svelte"
|
||||||
import TemplateBindings from "./_components/TemplateBindings.svelte"
|
import TemplateBindings from "./_components/TemplateBindings.svelte"
|
||||||
|
|
||||||
const ConfigTypes = {
|
|
||||||
SMTP: "smtp",
|
|
||||||
}
|
|
||||||
|
|
||||||
export let template
|
export let template
|
||||||
|
|
||||||
let selected = "Edit"
|
|
||||||
let selectedBindingTab = "Template"
|
|
||||||
let htmlEditor
|
let htmlEditor
|
||||||
|
let mounted = false
|
||||||
|
|
||||||
$: selectedTemplate = $email.templates.find(
|
$: selectedTemplate = $email.templates?.find(
|
||||||
({ purpose }) => purpose === template
|
({ purpose }) => purpose === template
|
||||||
)
|
)
|
||||||
|
$: baseTemplate = $email.templates?.find(({ purpose }) => purpose === "base")
|
||||||
$: templateBindings =
|
$: templateBindings =
|
||||||
$email.definitions?.bindings[selectedTemplate.purpose] || []
|
$email.definitions?.bindings?.[selectedTemplate.purpose] || []
|
||||||
|
$: previewContent = makePreviewContent(baseTemplate, selectedTemplate)
|
||||||
|
|
||||||
async function saveTemplate() {
|
async function saveTemplate() {
|
||||||
try {
|
try {
|
||||||
|
@ -44,12 +43,43 @@
|
||||||
function setTemplateBinding(binding) {
|
function setTemplateBinding(binding) {
|
||||||
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`))
|
htmlEditor.update((selectedTemplate.contents += `{{ ${binding.name} }}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const makePreviewContent = (baseTemplate, selectedTemplate) => {
|
||||||
|
if (!selectedTemplate) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (selectedTemplate.purpose === "base") {
|
||||||
|
return selectedTemplate.contents
|
||||||
|
}
|
||||||
|
const base = baseTemplate?.contents ?? ""
|
||||||
|
return base.replace("{{ body }}", selectedTemplate?.contents ?? "")
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mounted = true
|
||||||
|
})
|
||||||
|
|
||||||
|
async function fixMountBug({ detail }) {
|
||||||
|
if (detail === "Edit") {
|
||||||
|
await tick()
|
||||||
|
mounted = true
|
||||||
|
} else {
|
||||||
|
mounted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Page wide gap="L">
|
<Page wide>
|
||||||
<div class="backbutton" on:click={() => $goto("./")}>
|
<Layout gap="XS" noPadding>
|
||||||
<Icon name="BackAndroid" />
|
<div class="back">
|
||||||
<span>Back</span>
|
<ActionButton
|
||||||
|
on:click={() => $goto("./")}
|
||||||
|
quiet
|
||||||
|
size="S"
|
||||||
|
icon="BackAndroid"
|
||||||
|
>
|
||||||
|
Back to email settings
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<header>
|
<header>
|
||||||
<Heading>
|
<Heading>
|
||||||
|
@ -57,7 +87,12 @@
|
||||||
</Heading>
|
</Heading>
|
||||||
<Button cta on:click={saveTemplate}>Save</Button>
|
<Button cta on:click={saveTemplate}>Save</Button>
|
||||||
</header>
|
</header>
|
||||||
<Tabs {selected}>
|
<Body
|
||||||
|
>Change the email template here. Add dynamic content by using the bindings
|
||||||
|
menu on the right.</Body
|
||||||
|
>
|
||||||
|
</Layout>
|
||||||
|
<Tabs selected="Edit" on:select={fixMountBug}>
|
||||||
<Tab title="Edit">
|
<Tab title="Edit">
|
||||||
<div class="template-editor">
|
<div class="template-editor">
|
||||||
<Editor
|
<Editor
|
||||||
|
@ -67,11 +102,12 @@
|
||||||
on:change={e => {
|
on:change={e => {
|
||||||
selectedTemplate.contents = e.detail.value
|
selectedTemplate.contents = e.detail.value
|
||||||
}}
|
}}
|
||||||
value={selectedTemplate.contents}
|
value={selectedTemplate?.contents}
|
||||||
/>
|
/>
|
||||||
<div class="bindings-editor">
|
<div class="bindings-editor">
|
||||||
<Detail size="L">Bindings</Detail>
|
<Detail size="L">Bindings</Detail>
|
||||||
<Tabs selected={selectedBindingTab}>
|
{#if mounted}
|
||||||
|
<Tabs selected="Template">
|
||||||
<Tab title="Template">
|
<Tab title="Template">
|
||||||
<TemplateBindings
|
<TemplateBindings
|
||||||
title="Template Bindings"
|
title="Template Bindings"
|
||||||
|
@ -82,17 +118,18 @@
|
||||||
<Tab title="Common">
|
<Tab title="Common">
|
||||||
<TemplateBindings
|
<TemplateBindings
|
||||||
title="Common Bindings"
|
title="Common Bindings"
|
||||||
bindings={$email.definitions.bindings.common}
|
bindings={$email?.definitions?.bindings?.common}
|
||||||
onBindingClick={setTemplateBinding}
|
onBindingClick={setTemplateBinding}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div></Tab
|
</div>
|
||||||
>
|
</Tab>
|
||||||
<Tab title="Preview">
|
<Tab title="Preview">
|
||||||
<div class="preview" transition:fade>
|
<div class="preview">
|
||||||
{@html selectedTemplate.contents}
|
<iframe srcdoc={previewContent} />
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -101,7 +138,7 @@
|
||||||
<style>
|
<style>
|
||||||
.template-editor {
|
.template-editor {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 20%;
|
grid-template-columns: 1fr minmax(250px, 20%);
|
||||||
grid-gap: var(--spacing-xl);
|
grid-gap: var(--spacing-xl);
|
||||||
margin-top: var(--spacing-xl);
|
margin-top: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
@ -110,20 +147,17 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: var(--spacing-l);
|
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
background: white;
|
|
||||||
height: 800px;
|
height: 800px;
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl) 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
iframe {
|
||||||
.backbutton {
|
width: 100%;
|
||||||
display: flex;
|
height: 100%;
|
||||||
gap: var(--spacing-m);
|
border: none;
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Body, Menu, MenuItem, Detail } from "@budibase/bbui"
|
||||||
Body,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Detail,
|
|
||||||
MenuSection,
|
|
||||||
DetailSummary,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let bindings
|
export let bindings
|
||||||
export let onBindingClick = () => {}
|
export let onBindingClick = () => {}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<script>
|
||||||
|
import { email } from "stores/portal"
|
||||||
|
email.templates.fetch()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -1,29 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import {
|
import {
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Button,
|
Button,
|
||||||
Heading,
|
Heading,
|
||||||
Divider,
|
Divider,
|
||||||
Label,
|
Label,
|
||||||
Modal,
|
Page,
|
||||||
ModalContent,
|
|
||||||
notifications,
|
notifications,
|
||||||
Layout,
|
Layout,
|
||||||
Input,
|
Input,
|
||||||
TextArea,
|
|
||||||
Body,
|
Body,
|
||||||
Page,
|
|
||||||
Select,
|
|
||||||
MenuSection,
|
|
||||||
MenuSeparator,
|
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { email } from "stores/portal"
|
import { email } from "stores/portal"
|
||||||
import Editor from "components/integration/QueryEditor.svelte"
|
|
||||||
import TemplateBindings from "./_components/TemplateBindings.svelte"
|
|
||||||
import TemplateLink from "./_components/TemplateLink.svelte"
|
import TemplateLink from "./_components/TemplateLink.svelte"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
|
@ -46,9 +35,6 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
let smtpConfig
|
let smtpConfig
|
||||||
let bindingsOpen = false
|
|
||||||
let htmlModal
|
|
||||||
let htmlEditor
|
|
||||||
let loading
|
let loading
|
||||||
|
|
||||||
async function saveSmtp() {
|
async function saveSmtp() {
|
||||||
|
@ -66,16 +52,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTemplate() {
|
|
||||||
try {
|
|
||||||
await email.templates.save(selectedTemplate)
|
|
||||||
notifications.success(`Template saved.`)
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error(`Failed to update template settings. ${err}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchSmtp() {
|
async function fetchSmtp() {
|
||||||
|
loading = true
|
||||||
// fetch the configs for smtp
|
// fetch the configs for smtp
|
||||||
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
|
const smtpResponse = await api.get(`/api/admin/configs/${ConfigTypes.SMTP}`)
|
||||||
const smtpDoc = await smtpResponse.json()
|
const smtpDoc = await smtpResponse.json()
|
||||||
|
@ -92,17 +70,14 @@
|
||||||
} else {
|
} else {
|
||||||
smtpConfig = smtpDoc
|
smtpConfig = smtpDoc
|
||||||
}
|
}
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
fetchSmtp()
|
||||||
loading = true
|
|
||||||
await fetchSmtp()
|
|
||||||
await email.templates.fetch()
|
|
||||||
loading = false
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout>
|
<Page>
|
||||||
|
<Layout>
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
<Heading size="M">Email</Heading>
|
<Heading size="M">Email</Heading>
|
||||||
<Body>
|
<Body>
|
||||||
|
@ -158,12 +133,14 @@
|
||||||
data={$email.templates}
|
data={$email.templates}
|
||||||
schema={templateSchema}
|
schema={templateSchema}
|
||||||
{loading}
|
{loading}
|
||||||
|
on:click={({ detail }) => $goto(`./${detail.purpose}`)}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.form-row {
|
.form-row {
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
Divider,
|
Divider,
|
||||||
Label,
|
Label,
|
||||||
Input,
|
Input,
|
||||||
|
Select,
|
||||||
|
Toggle,
|
||||||
Modal,
|
Modal,
|
||||||
Table,
|
Table,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
@ -19,10 +21,12 @@
|
||||||
|
|
||||||
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
|
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
|
||||||
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
|
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
|
||||||
|
import ForceResetPasswordModal from "./_components/ForceResetPasswordModal.svelte"
|
||||||
|
|
||||||
export let userId
|
export let userId
|
||||||
let deleteUserModal
|
let deleteUserModal
|
||||||
let editRolesModal
|
let editRolesModal
|
||||||
|
let resetPasswordModal
|
||||||
|
|
||||||
const roleSchema = {
|
const roleSchema = {
|
||||||
name: { displayName: "App" },
|
name: { displayName: "App" },
|
||||||
|
@ -33,23 +37,32 @@
|
||||||
$: appList = Object.keys($apps?.data).map(id => ({
|
$: appList = Object.keys($apps?.data).map(id => ({
|
||||||
...$apps?.data?.[id],
|
...$apps?.data?.[id],
|
||||||
_id: id,
|
_id: id,
|
||||||
role: [$roleFetch?.data?.roles?.[id]],
|
role: [$userFetch?.data?.roles?.[id]],
|
||||||
}))
|
}))
|
||||||
let selectedApp
|
let selectedApp
|
||||||
|
|
||||||
const roleFetch = fetchData(`/api/admin/users/${userId}`)
|
const userFetch = fetchData(`/api/admin/users/${userId}`)
|
||||||
const apps = fetchData(`/api/admin/roles`)
|
const apps = fetchData(`/api/admin/roles`)
|
||||||
|
|
||||||
async function deleteUser() {
|
async function deleteUser() {
|
||||||
const res = await users.del(userId)
|
const res = await users.delete(userId)
|
||||||
if (res.message) {
|
if (res.message) {
|
||||||
notifications.success(`User ${$roleFetch?.data?.email} deleted.`)
|
notifications.success(`User ${$userFetch?.data?.email} deleted.`)
|
||||||
$goto("./")
|
$goto("./")
|
||||||
} else {
|
} else {
|
||||||
notifications.error("Failed to delete user.")
|
notifications.error("Failed to delete user.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let toggleDisabled = false
|
||||||
|
|
||||||
|
async function toggleBuilderAccess({ detail }) {
|
||||||
|
toggleDisabled = true
|
||||||
|
await users.save({ ...$userFetch?.data, builder: { global: detail } })
|
||||||
|
await userFetch.refresh()
|
||||||
|
toggleDisabled = false
|
||||||
|
}
|
||||||
|
|
||||||
async function openUpdateRolesModal({ detail }) {
|
async function openUpdateRolesModal({ detail }) {
|
||||||
selectedApp = detail
|
selectedApp = detail
|
||||||
editRolesModal.show()
|
editRolesModal.show()
|
||||||
|
@ -68,11 +81,10 @@
|
||||||
Back to users
|
Back to users
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Heading>User: {$roleFetch?.data?.email}</Heading>
|
<Heading>User: {$userFetch?.data?.email}</Heading>
|
||||||
<Body>
|
<Body>
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis porro ut
|
Change user settings and update their app roles. Also contains the ability
|
||||||
nesciunt ipsam perspiciatis aliquam et hic minus alias beatae. Odit
|
to delete the user as well as force reset their password..
|
||||||
veritatis quos quas laborum magnam tenetur perspiciatis ex hic.
|
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
|
@ -81,21 +93,37 @@
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Email</Label>
|
<Label size="L">Email</Label>
|
||||||
<Input disabled thin value={$roleFetch?.data?.email} />
|
<Input disabled thin value={$userFetch?.data?.email} />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Group(s)</Label>
|
||||||
|
<Select disabled options={["All users"]} value="All users" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">First name</Label>
|
<Label size="L">First name</Label>
|
||||||
<Input disabled thin value={$roleFetch?.data?.firstName} />
|
<Input disabled thin value={$userFetch?.data?.firstName} />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Last name</Label>
|
<Label size="L">Last name</Label>
|
||||||
<Input disabled thin value={$roleFetch?.data?.lastName} />
|
<Input disabled thin value={$userFetch?.data?.lastName} />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Development access?</Label>
|
||||||
|
<Toggle
|
||||||
|
text=""
|
||||||
|
value={$userFetch?.data?.builder?.global}
|
||||||
|
on:change={toggleBuilderAccess}
|
||||||
|
disabled={toggleDisabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="regenerate">
|
<div class="regenerate">
|
||||||
<ActionButton size="S" icon="Refresh" quiet>
|
<ActionButton
|
||||||
Regenerate password
|
size="S"
|
||||||
</ActionButton>
|
icon="Refresh"
|
||||||
|
quiet
|
||||||
|
on:click={resetPasswordModal.show}>Force password reset</ActionButton
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
|
@ -131,15 +159,21 @@
|
||||||
showCloseIcon={false}
|
showCloseIcon={false}
|
||||||
>
|
>
|
||||||
<Body>
|
<Body>
|
||||||
Are you sure you want to delete <strong>{$roleFetch?.data?.email}</strong>
|
Are you sure you want to delete <strong>{$userFetch?.data?.email}</strong>
|
||||||
</Body>
|
</Body>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal bind:this={editRolesModal}>
|
<Modal bind:this={editRolesModal}>
|
||||||
<UpdateRolesModal
|
<UpdateRolesModal
|
||||||
app={selectedApp}
|
app={selectedApp}
|
||||||
user={$roleFetch.data}
|
user={$userFetch.data}
|
||||||
on:update={roleFetch.refresh}
|
on:update={userFetch.refresh}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<Modal bind:this={resetPasswordModal}>
|
||||||
|
<ForceResetPasswordModal
|
||||||
|
user={$userFetch.data}
|
||||||
|
on:update={userFetch.refresh}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
|
||||||
|
import { users } from "stores/portal"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let user
|
||||||
|
|
||||||
|
const password = Math.random().toString(36).substr(2, 20)
|
||||||
|
|
||||||
|
async function resetPassword() {
|
||||||
|
const res = await users.save({
|
||||||
|
...user,
|
||||||
|
password,
|
||||||
|
forceResetPassword: true,
|
||||||
|
})
|
||||||
|
if (res.status) {
|
||||||
|
notifications.error(res.message)
|
||||||
|
} else {
|
||||||
|
notifications.success("Password reset.")
|
||||||
|
dispatch("update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={resetPassword}
|
||||||
|
size="M"
|
||||||
|
title="Force Reset User Password"
|
||||||
|
confirmText="Reset password"
|
||||||
|
cancelText="Cancel"
|
||||||
|
showCloseIcon={false}
|
||||||
|
>
|
||||||
|
<Body noPadding
|
||||||
|
>Before you reset the users password, do not forget to copy the new
|
||||||
|
password. The user will need this to login. Once the user has logged in they
|
||||||
|
will be asked to create a new password that is more secure.</Body
|
||||||
|
>
|
||||||
|
<Input disabled label="Password" value={password} />
|
||||||
|
</ModalContent>
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
<Tags>
|
<Tags>
|
||||||
{#each tags as tag}
|
{#each tags as tag}
|
||||||
<Tag>
|
<Tag disabled>
|
||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
let selectedRole = user?.roles?.[app?._id]
|
let selectedRole = user?.roles?.[app?._id]
|
||||||
|
|
||||||
async function updateUserRoles() {
|
async function updateUserRoles() {
|
||||||
const res = await users.updateRoles({
|
const res = await users.save({
|
||||||
...user,
|
...user,
|
||||||
roles: {
|
roles: {
|
||||||
...user.roles,
|
...user.roles,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script>
|
||||||
|
import { Page } from "@budibase/bbui"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Page>
|
||||||
|
<slot />
|
||||||
|
</Page>
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
email: {},
|
email: {},
|
||||||
status: { displayName: "Development Access", type: "boolean" },
|
developmentAccess: { displayName: "Development Access", type: "boolean" },
|
||||||
// role: { type: "options" },
|
// role: { type: "options" },
|
||||||
group: {},
|
group: {},
|
||||||
// access: {},
|
// access: {},
|
||||||
|
@ -32,7 +32,11 @@
|
||||||
let email
|
let email
|
||||||
$: filteredUsers = $users
|
$: filteredUsers = $users
|
||||||
.filter(user => user.email.includes(search || ""))
|
.filter(user => user.email.includes(search || ""))
|
||||||
.map(user => ({ ...user, group: ["All"] }))
|
.map(user => ({
|
||||||
|
...user,
|
||||||
|
group: ["All users"],
|
||||||
|
developmentAccess: user.builder.global,
|
||||||
|
}))
|
||||||
|
|
||||||
let createUserModal
|
let createUserModal
|
||||||
let basicOnboardingModal
|
let basicOnboardingModal
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { writable } from "svelte/store"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
export function createEmailStore() {
|
export function createEmailStore() {
|
||||||
const store = writable([])
|
const store = writable({})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: store.subscribe,
|
subscribe: store.subscribe,
|
||||||
|
|
|
@ -40,7 +40,7 @@ export function createUsersStore() {
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateRoles(data) {
|
async function save(data) {
|
||||||
try {
|
try {
|
||||||
const res = await post(`/api/admin/users`, data)
|
const res = await post(`/api/admin/users`, data)
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
|
@ -57,8 +57,8 @@ export function createUsersStore() {
|
||||||
invite,
|
invite,
|
||||||
acceptInvite,
|
acceptInvite,
|
||||||
create,
|
create,
|
||||||
updateRoles,
|
save,
|
||||||
del,
|
delete: del,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ function buildUserSaveValidation(isSelf = false) {
|
||||||
let schema = {
|
let schema = {
|
||||||
email: Joi.string().allow(null, ""),
|
email: Joi.string().allow(null, ""),
|
||||||
password: Joi.string().allow(null, ""),
|
password: Joi.string().allow(null, ""),
|
||||||
|
forceResetPassword: Joi.boolean().optional(),
|
||||||
firstName: Joi.string().allow(null, ""),
|
firstName: Joi.string().allow(null, ""),
|
||||||
lastName: Joi.string().allow(null, ""),
|
lastName: Joi.string().allow(null, ""),
|
||||||
builder: Joi.object({
|
builder: Joi.object({
|
||||||
|
|
Loading…
Reference in New Issue