Add head tag script CRUD in app settings

This commit is contained in:
Andrew Kingston 2025-03-12 12:27:34 +00:00
parent 8f19b4250b
commit 10d8202372
No known key found for this signature in database
5 changed files with 203 additions and 11 deletions

View File

@ -44,6 +44,11 @@
url={$url("./version")}
active={$isActive("./version")}
/>
<SideNavItem
text="Scripts"
url={$url("./scripts")}
active={$isActive("./scripts")}
/>
<div class="delete-action">
<AbsTooltip
position={TooltipPosition.Bottom}

View File

@ -170,7 +170,7 @@
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading>Automations</Heading>
<Body size="S">See your automation history and edit advanced settings</Body>
<Body>See your automation history and edit advanced settings</Body>
</Layout>
<Divider />
@ -251,7 +251,6 @@
data={runHistory}
{customRenderers}
placeholderText="No history found"
border={false}
/>
<div class="pagination">
<Pagination

View File

@ -0,0 +1,163 @@
<script lang="ts">
import {
Body,
Button,
Link,
Divider,
Heading,
Layout,
Table,
Label,
Input,
TextArea,
Select,
Helpers,
notifications,
} from "@budibase/bbui"
import { appStore } from "@/stores/builder"
import { type AppScript } from "@budibase/types"
import { getSequentialName } from "@/helpers/duplicate"
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
const schema = {
name: {
type: "string",
label: "Name",
},
location: {
type: "string",
label: "Location",
},
}
let selectedScript: AppScript | undefined
let isNew = false
let confirmDeleteModal: any
$: nameError = selectedScript?.name ? undefined : "Please enter a name"
$: invalid = !!nameError
const addScript = () => {
const name = getSequentialName($appStore.scripts, "Script ", {
getName: script => script.name,
numberFirstItem: true,
})
selectedScript = { id: Helpers.uuid(), location: "Head", name }
isNew = true
}
const editScript = (e: any) => {
selectedScript = { ...(e.detail as AppScript) }
isNew = false
}
const cancel = () => {
selectedScript = undefined
}
const save = async () => {
if (!selectedScript) {
return
}
const newScripts = $appStore.scripts
.filter(script => script.id !== selectedScript!.id)
.concat([selectedScript])
await appStore.updateApp({ scripts: newScripts })
notifications.success("Script saved successfully")
selectedScript = undefined
}
const requestDeletion = () => {
confirmDeleteModal?.show()
}
const deleteScript = async () => {
if (!selectedScript) {
return
}
const newScripts = $appStore.scripts.filter(
script => script.id !== selectedScript!.id
)
await appStore.updateApp({ scripts: newScripts })
notifications.success("Script deleted successfully")
selectedScript = undefined
}
</script>
<Layout noPadding>
<Layout gap="XS" noPadding>
<Heading>Scripts</Heading>
<div class="header">
<Body>
Inject analytics, scripts or stylesheets into your app<br />
<Link href="#">Learn more about script injection in the docs</Link>
</Body>
{#if !selectedScript}
<Button cta on:click={addScript}>Add script</Button>
{/if}
</div>
</Layout>
<Divider />
{#if selectedScript}
<div class="form">
<Label size="L">Name</Label>
<Input bind:value={selectedScript.name} error={nameError} />
<Label size="L">Location</Label>
<Select
bind:value={selectedScript.location}
options={["Head", "Body"]}
placeholder={false}
/>
<Label size="L">HTML</Label>
<TextArea bind:value={selectedScript.html} minHeight={200} />
<div />
<div class="buttons">
{#if !isNew}
<Button warning quiet on:click={requestDeletion}>Delete</Button>
{/if}
<Button secondary on:click={cancel}>Cancel</Button>
<Button cta disabled={invalid} on:click={save}>Save</Button>
</div>
</div>
{:else}
<Table
on:click={editScript}
{schema}
data={$appStore.scripts}
allowSelectRows={false}
allowEditColumns={false}
allowEditRows={false}
placeholderText="You haven't added any scripts yet"
/>
{/if}
</Layout>
<ConfirmDialog
bind:this={confirmDeleteModal}
title="Delete script"
body="Are you sure you want to delete this script?"
onOk={deleteScript}
/>
<style>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.form {
display: grid;
grid-template-columns: 100px 480px;
row-gap: var(--spacing-l);
}
.form :global(.spectrum-FieldLabel) {
padding-top: 7px;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: var(--spacing-l);
}
</style>

View File

@ -4,9 +4,12 @@ import {
App,
AppFeatures,
AppIcon,
AppScript,
AutomationSettings,
Plugin,
UpdateAppRequest,
} from "@budibase/types"
import { get } from "svelte/store"
interface ClientFeatures {
spectrumThemes: boolean
@ -46,6 +49,7 @@ interface AppMetaState {
revertableVersion?: string
upgradableVersion?: string
icon?: AppIcon
scripts: AppScript[]
}
export const INITIAL_APP_META_STATE: AppMetaState = {
@ -79,6 +83,7 @@ export const INITIAL_APP_META_STATE: AppMetaState = {
usedPlugins: [],
automations: {},
routes: {},
scripts: [],
}
export class AppMetaStore extends BudiStore<AppMetaState> {
@ -90,20 +95,12 @@ export class AppMetaStore extends BudiStore<AppMetaState> {
this.store.set({ ...INITIAL_APP_META_STATE })
}
syncAppPackage(pkg: {
application: App
clientLibPath: string
hasLock: boolean
}) {
const { application: app, clientLibPath, hasLock } = pkg
syncApp(app: App) {
this.update(state => ({
...state,
name: app.name,
appId: app.appId,
url: app.url || "",
hasLock,
clientLibPath,
libraries: app.componentLibraries,
version: app.version,
appInstance: app.instance,
@ -118,9 +115,24 @@ export class AppMetaStore extends BudiStore<AppMetaState> {
initialised: true,
automations: app.automations || {},
hasAppPackage: true,
scripts: app.scripts || [],
}))
}
syncAppPackage(pkg: {
application: App
clientLibPath: string
hasLock: boolean
}) {
const { application, clientLibPath, hasLock } = pkg
this.update(state => ({
...state,
hasLock,
clientLibPath,
}))
this.syncApp(application)
}
syncClientFeatures(features: Partial<ClientFeatures>) {
this.update(state => ({
...state,
@ -146,6 +158,11 @@ export class AppMetaStore extends BudiStore<AppMetaState> {
}))
}
async updateApp(updates: UpdateAppRequest) {
const app = await API.saveAppMetadata(get(this.store).appId, updates)
this.syncApp(app)
}
// Returned from socket
syncMetadata(metadata: { name: string; url: string; icon?: AppIcon }) {
const { name, url, icon } = metadata

View File

@ -29,6 +29,7 @@ export interface App extends Document {
snippets?: Snippet[]
creationVersion?: string
updatedBy?: string
scripts?: AppScript[]
}
export interface AppInstance {
@ -82,3 +83,10 @@ export interface AppFeatures {
export interface AutomationSettings {
chainAutomations?: boolean
}
export interface AppScript {
id: string
name: string
location: "Head" | "Body"
html?: string
}