Merge branch 'master' into fix/search-by-row-id

This commit is contained in:
Michael Drury 2024-07-30 11:45:36 +01:00 committed by GitHub
commit af0fa2c56e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 941 additions and 747 deletions

View File

@ -10,7 +10,7 @@
"@types/proper-lockfile": "^4.1.4", "@types/proper-lockfile": "^4.1.4",
"@typescript-eslint/parser": "6.9.0", "@typescript-eslint/parser": "6.9.0",
"esbuild": "^0.18.17", "esbuild": "^0.18.17",
"esbuild-node-externals": "^1.8.0", "esbuild-node-externals": "^1.14.0",
"eslint": "^8.52.0", "eslint": "^8.52.0",
"eslint-plugin-import": "^2.29.0", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-jest": "^27.9.0", "eslint-plugin-jest": "^27.9.0",

@ -1 +1 @@
Subproject commit b03e584e465f620b49a1b688ff4afc973e6c0758 Subproject commit 32b8fa4643b4f0f74ee89760deffe431ab347ad9

View File

@ -199,9 +199,8 @@ export const createPlatformUserView = async () => {
export const queryPlatformView = async <T extends Document>( export const queryPlatformView = async <T extends Document>(
viewName: ViewName, viewName: ViewName,
params: DatabaseQueryOpts, params: DatabaseQueryOpts
opts?: QueryViewOptions ): Promise<T[]> => {
): Promise<T[] | T> => {
const CreateFuncByName: any = { const CreateFuncByName: any = {
[ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView, [ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView,
[ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView,
@ -209,7 +208,9 @@ export const queryPlatformView = async <T extends Document>(
return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => {
const createFn = CreateFuncByName[viewName] const createFn = CreateFuncByName[viewName]
return queryView(viewName, params, db, createFn, opts) return queryView(viewName, params, db, createFn, {
arrayResponse: true,
}) as Promise<T[]>
}) })
} }

View File

@ -25,6 +25,11 @@ export async function getUserDoc(emailOrId: string): Promise<PlatformUser> {
return db.get(emailOrId) return db.get(emailOrId)
} }
export async function updateUserDoc(platformUser: PlatformUserById) {
const db = getPlatformDB()
await db.put(platformUser)
}
// CREATE // CREATE
function newUserIdDoc(id: string, tenantId: string): PlatformUserById { function newUserIdDoc(id: string, tenantId: string): PlatformUserById {

View File

@ -18,6 +18,9 @@ import {
User, User,
UserStatus, UserStatus,
UserGroup, UserGroup,
PlatformUserBySsoId,
PlatformUserById,
AnyDocument,
} from "@budibase/types" } from "@budibase/types"
import { import {
getAccountHolderFromUserIds, getAccountHolderFromUserIds,
@ -25,7 +28,11 @@ import {
isCreator, isCreator,
validateUniqueUser, validateUniqueUser,
} from "./utils" } from "./utils"
import { searchExistingEmails } from "./lookup" import {
getFirstPlatformUser,
getPlatformUsers,
searchExistingEmails,
} from "./lookup"
import { hash } from "../utils" import { hash } from "../utils"
import { validatePassword } from "../security" import { validatePassword } from "../security"
@ -446,9 +453,32 @@ export class UserDB {
creator => !!creator creator => !!creator
).length ).length
const ssoUsersToDelete: AnyDocument[] = []
for (let user of usersToDelete) { for (let user of usersToDelete) {
const platformUser = (await getFirstPlatformUser(
user._id!
)) as PlatformUserById
const ssoId = platformUser.ssoId
if (ssoId) {
// Need to get the _rev of the SSO user doc to delete it. The view also returns docs that have the ssoId property, so we need to ignore those.
const ssoUsers = (await getPlatformUsers(
ssoId
)) as PlatformUserBySsoId[]
ssoUsers
.filter(user => user.ssoId == null)
.forEach(user => {
ssoUsersToDelete.push({
...user,
_deleted: true,
})
})
}
await bulkDeleteProcessing(user) await bulkDeleteProcessing(user)
} }
// Delete any associated SSO user docs
await platform.getPlatformDB().bulkDocs(ssoUsersToDelete)
await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount) await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount)
// Build Response // Build Response

View File

@ -34,15 +34,22 @@ export async function searchExistingEmails(emails: string[]) {
} }
// lookup, could be email or userId, either will return a doc // lookup, could be email or userId, either will return a doc
export async function getPlatformUser( export async function getPlatformUsers(
identifier: string identifier: string
): Promise<PlatformUser | null> { ): Promise<PlatformUser[]> {
// use the view here and allow to find anyone regardless of casing // use the view here and allow to find anyone regardless of casing
// Use lowercase to ensure email login is case insensitive // Use lowercase to ensure email login is case insensitive
return (await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { return await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, {
keys: [identifier.toLowerCase()], keys: [identifier.toLowerCase()],
include_docs: true, include_docs: true,
})) as PlatformUser })
}
export async function getFirstPlatformUser(
identifier: string
): Promise<PlatformUser | null> {
const platformUserDocs = await getPlatformUsers(identifier)
return platformUserDocs[0] ?? null
} }
export async function getExistingTenantUsers( export async function getExistingTenantUsers(
@ -74,15 +81,10 @@ export async function getExistingPlatformUsers(
keys: lcEmails, keys: lcEmails,
include_docs: true, include_docs: true,
} }
return await dbUtils.queryPlatformView(
const opts = {
arrayResponse: true,
}
return (await dbUtils.queryPlatformView(
ViewName.PLATFORM_USERS_LOWERCASE, ViewName.PLATFORM_USERS_LOWERCASE,
params, params
opts )
)) as PlatformUserByEmail[]
} }
export async function getExistingAccounts( export async function getExistingAccounts(
@ -93,14 +95,5 @@ export async function getExistingAccounts(
keys: lcEmails, keys: lcEmails,
include_docs: true, include_docs: true,
} }
return await dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, params)
const opts = {
arrayResponse: true,
}
return (await dbUtils.queryPlatformView(
ViewName.ACCOUNT_BY_EMAIL,
params,
opts
)) as AccountMetadata[]
} }

View File

@ -1,7 +1,7 @@
import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types" import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import env from "../environment" import env from "../environment"
import { getPlatformUser } from "./lookup" import { getFirstPlatformUser } from "./lookup"
import { EmailUnavailableError } from "../errors" import { EmailUnavailableError } from "../errors"
import { getTenantId } from "../context" import { getTenantId } from "../context"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
@ -51,7 +51,7 @@ async function isCreatorByGroupMembership(user?: User | ContextUser) {
export async function validateUniqueUser(email: string, tenantId: string) { export async function validateUniqueUser(email: string, tenantId: string) {
// check budibase users in other tenants // check budibase users in other tenants
if (env.MULTI_TENANCY) { if (env.MULTI_TENANCY) {
const tenantUser = await getPlatformUser(email) const tenantUser = await getFirstPlatformUser(email)
if (tenantUser != null && tenantUser.tenantId !== tenantId) { if (tenantUser != null && tenantUser.tenantId !== tenantId) {
throw new EmailUnavailableError(email) throw new EmailUnavailableError(email)
} }

View File

@ -36,9 +36,11 @@
<use xlink:href="#spectrum-icon-18-{icon}" /> <use xlink:href="#spectrum-icon-18-{icon}" />
</svg> </svg>
<div class="spectrum-InLineAlert-header">{header}</div> <div class="spectrum-InLineAlert-header">{header}</div>
{#each split as splitMsg} <slot>
<div class="spectrum-InLineAlert-content">{splitMsg}</div> {#each split as splitMsg}
{/each} <div class="spectrum-InLineAlert-content">{splitMsg}</div>
{/each}
</slot>
{#if onConfirm} {#if onConfirm}
<div class="spectrum-InLineAlert-footer button"> <div class="spectrum-InLineAlert-footer button">
<Button {cta} secondary={cta ? false : true} on:click={onConfirm} <Button {cta} secondary={cta ? false : true} on:click={onConfirm}

View File

@ -30,7 +30,7 @@
class:custom={!!color} class:custom={!!color}
class:square class:square
class:hoverable class:hoverable
style={`--color: ${color};`} style={`--color: ${color ?? "var(--spectrum-global-color-gray-400)"};`}
class:spectrum-StatusLight--celery={celery} class:spectrum-StatusLight--celery={celery}
class:spectrum-StatusLight--yellow={yellow} class:spectrum-StatusLight--yellow={yellow}
class:spectrum-StatusLight--fuchsia={fuchsia} class:spectrum-StatusLight--fuchsia={fuchsia}
@ -61,13 +61,17 @@
min-height: 0; min-height: 0;
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
transition: color ease-out 130ms;
} }
.spectrum-StatusLight.withText::before { .spectrum-StatusLight.withText::before {
margin-right: 10px; margin-right: 10px;
} }
.spectrum-StatusLight::before {
transition: background-color ease-out 160ms;
}
.custom::before { .custom::before {
background: var(--color) !important; background-color: var(--color) !important;
} }
.square::before { .square::before {
width: 14px; width: 14px;
@ -79,4 +83,14 @@
cursor: pointer; cursor: pointer;
color: var(--spectrum-global-color-gray-900); color: var(--spectrum-global-color-gray-900);
} }
.spectrum-StatusLight--sizeXS::before {
width: 10px;
height: 10px;
border-radius: 2px;
}
.spectrum-StatusLight--disabled::before {
background-color: var(--spectrum-global-color-gray-400) !important;
}
</style> </style>

View File

@ -33,6 +33,5 @@
title="Confirm Deletion" title="Confirm Deletion"
> >
Are you sure you wish to delete the datasource Are you sure you wish to delete the datasource
<i>{datasource.name}?</i> <i>{datasource.name}</i>? This action cannot be undone.
This action cannot be undone.
</ConfirmDialog> </ConfirmDialog>

View File

@ -1,7 +1,7 @@
<script> <script>
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { tables, datasources, screenStore } from "stores/builder" import { appStore, tables, datasources, screenStore } from "stores/builder"
import { Input, notifications } from "@budibase/bbui" import { InlineAlert, Link, Input, notifications } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { DB_TYPE_EXTERNAL } from "constants/backend" import { DB_TYPE_EXTERNAL } from "constants/backend"
@ -9,28 +9,41 @@
let confirmDeleteDialog let confirmDeleteDialog
export const show = () => { let screensPossiblyAffected = []
templateScreens = $screenStore.screens.filter( let viewsMessage = ""
screen => screen.autoTableId === table._id let deleteTableName
)
willBeDeleted = ["All table data"].concat( const getViewsMessage = () => {
templateScreens.map(screen => `Screen ${screen.routing?.route || ""}`) const views = Object.values(table?.views ?? [])
) if (views.length < 1) {
confirmDeleteDialog.show() return ""
}
if (views.length === 1) {
return ", including 1 view"
}
return `, including ${views.length} views`
} }
let templateScreens export const show = () => {
let willBeDeleted viewsMessage = getViewsMessage()
let deleteTableName screensPossiblyAffected = $screenStore.screens
.filter(
screen => screen.autoTableId === table._id && screen.routing?.route
)
.map(screen => ({
text: screen.routing.route,
url: `/builder/app/${$appStore.appId}/design/${screen._id}`,
}))
confirmDeleteDialog.show()
}
async function deleteTable() { async function deleteTable() {
const isSelected = $params.tableId === table._id const isSelected = $params.tableId === table._id
try { try {
await tables.delete(table) await tables.delete(table)
// Screens need deleted one at a time because of undo/redo
for (let screen of templateScreens) {
await screenStore.delete(screen)
}
if (table.sourceType === DB_TYPE_EXTERNAL) { if (table.sourceType === DB_TYPE_EXTERNAL) {
await datasources.fetch() await datasources.fetch()
} }
@ -46,6 +59,10 @@
function hideDeleteDialog() { function hideDeleteDialog() {
deleteTableName = "" deleteTableName = ""
} }
const autofillTableName = () => {
deleteTableName = table.name
}
</script> </script>
<ConfirmDialog <ConfirmDialog
@ -56,34 +73,103 @@
title="Confirm Deletion" title="Confirm Deletion"
disabled={deleteTableName !== table.name} disabled={deleteTableName !== table.name}
> >
<p> <div class="content">
Are you sure you wish to delete the table <p class="firstWarning">
<b>{table.name}?</b> Are you sure you wish to delete the table
The following will also be deleted: <span class="tableNameLine">
</p> <!-- svelte-ignore a11y-click-events-have-key-events -->
<b> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="delete-items"> <b on:click={autofillTableName} class="tableName">{table.name}</b>
{#each willBeDeleted as item} <span>?</span>
<div>{item}</div> </span>
{/each} </p>
</div>
</b> <p class="secondWarning">All table data will be deleted{viewsMessage}.</p>
<p> <p class="thirdWarning">This action <b>cannot be undone</b>.</p>
This action cannot be undone - to continue please enter the table name below
to confirm. {#if screensPossiblyAffected.length > 0}
</p> <div class="affectedScreens">
<Input bind:value={deleteTableName} placeholder={table.name} /> <InlineAlert
header="The following screens were originally generated from this table and may no longer function as expected"
>
<ul class="affectedScreensList">
{#each screensPossiblyAffected as item}
<li>
<Link quiet overBackground target="_blank" href={item.url}
>{item.text}</Link
>
</li>
{/each}
</ul>
</InlineAlert>
</div>
{/if}
<p class="fourthWarning">Please enter the app name below to confirm.</p>
<Input bind:value={deleteTableName} placeholder={table.name} />
</div>
</ConfirmDialog> </ConfirmDialog>
<style> <style>
div.delete-items { .content {
margin-top: 10px; margin-top: 0;
margin-bottom: 10px; max-width: 320px;
margin-left: 10px;
} }
div.delete-items div { .firstWarning {
margin: 0 0 12px;
max-width: 100%;
}
.tableNameLine {
display: inline-flex;
max-width: 100%;
vertical-align: bottom;
}
.tableName {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.secondWarning {
margin: 0;
max-width: 100%;
}
.thirdWarning {
margin: 0 0 12px;
max-width: 100%;
}
.affectedScreens {
margin: 18px 0;
max-width: 100%;
margin-bottom: 24px;
}
.affectedScreens :global(.spectrum-InLineAlert) {
max-width: 100%;
}
.affectedScreensList {
padding: 0;
margin-bottom: 0;
}
.affectedScreensList li {
display: block;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 4px; margin-top: 4px;
font-weight: 600; }
.fourthWarning {
margin: 12px 0 6px;
max-width: 100%;
} }
</style> </style>

View File

@ -0,0 +1,12 @@
<script>
import { RoleUtils } from "@budibase/frontend-core"
import { StatusLight } from "@budibase/bbui"
export let id
export let size = "M"
export let disabled = false
$: color = RoleUtils.getRoleColour(id)
</script>
<StatusLight square {disabled} {size} {color} />

View File

@ -1,20 +1,32 @@
<script> <script>
import { Layout, Input } from "@budibase/bbui" import { FancyForm, FancyInput } from "@budibase/bbui"
import { createValidationStore, requiredValidator } from "helpers/validation" import { createValidationStore, requiredValidator } from "helpers/validation"
export let password export let password
export let passwordForm
export let error export let error
const validatePassword = value => {
if (!value || value.length < 12) {
return "Please enter at least 12 characters. We recommend using machine generated or random passwords."
}
return null
}
const [firstPassword, passwordError, firstTouched] = createValidationStore( const [firstPassword, passwordError, firstTouched] = createValidationStore(
"", "",
requiredValidator requiredValidator
) )
const [repeatPassword, _, repeatTouched] = createValidationStore( const [repeatPassword, _, repeatTouched] = createValidationStore(
"", "",
requiredValidator requiredValidator,
validatePassword
) )
$: password = $firstPassword $: password = $firstPassword
$: firstPasswordError =
($firstTouched && $passwordError) ||
($repeatTouched && validatePassword(password))
$: error = $: error =
!$firstPassword || !$firstPassword ||
!$firstTouched || !$firstTouched ||
@ -22,19 +34,19 @@
$firstPassword !== $repeatPassword $firstPassword !== $repeatPassword
</script> </script>
<Layout gap="XS" noPadding> <FancyForm bind:this={passwordForm}>
<Input <FancyInput
label="Password" label="Password"
type="password" type="password"
error={$firstTouched && $passwordError} error={firstPasswordError}
bind:value={$firstPassword} bind:value={$firstPassword}
/> />
<Input <FancyInput
label="Repeat Password" label="Repeat password"
type="password" type="password"
error={$repeatTouched && error={$repeatTouched &&
$firstPassword !== $repeatPassword && $firstPassword !== $repeatPassword &&
"Passwords must match"} "Passwords must match"}
bind:value={$repeatPassword} bind:value={$repeatPassword}
/> />
</Layout> </FancyForm>

View File

@ -1,108 +1,88 @@
<script> <script>
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte" import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
import DatasourceModal from "./DatasourceModal.svelte" import DatasourceModal from "./DatasourceModal.svelte"
import ScreenRoleModal from "./ScreenRoleModal.svelte"
import sanitizeUrl from "helpers/sanitizeUrl" import sanitizeUrl from "helpers/sanitizeUrl"
import FormTypeModal from "./FormTypeModal.svelte" import FormTypeModal from "./FormTypeModal.svelte"
import { Modal, notifications } from "@budibase/bbui" import { Modal, notifications } from "@budibase/bbui"
import { import {
screenStore, screenStore,
navigationStore, navigationStore,
tables, permissions as permissionsStore,
builderStore, builderStore,
} from "stores/builder" } from "stores/builder"
import { auth } from "stores/portal" import { auth } from "stores/portal"
import { get } from "svelte/store" import { get } from "svelte/store"
import getTemplates from "templates"
import { Roles } from "constants/backend"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { TOUR_KEYS } from "components/portal/onboarding/tours.js" import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
import blankScreen from "templates/blankScreen"
import formScreen from "templates/formScreen" import formScreen from "templates/formScreen"
import gridListScreen from "templates/gridListScreen" import gridScreen from "templates/gridScreen"
import gridDetailsScreen from "templates/gridDetailsScreen" import gridDetailsScreen from "templates/gridDetailsScreen"
import { Roles } from "constants/backend"
let mode let mode
let pendingScreen
// Modal refs
let screenDetailsModal let screenDetailsModal
let datasourceModal let datasourceModal
let screenAccessRoleModal
let formTypeModal let formTypeModal
// Cache variables for workflow let selectedTablesAndViews = []
let screenAccessRole = Roles.BASIC let permissions = {}
let templates = null export const show = newMode => {
let screens = null mode = newMode
selectedTablesAndViews = []
permissions = {}
let selectedDatasources = null if (mode === "grid" || mode === "gridDetails" || mode === "form") {
let blankScreenUrl = null datasourceModal.show()
let screenMode = null } else if (mode === "blank") {
let formType = null screenDetailsModal.show()
} else {
// Creates an array of screens, checking and sanitising their URLs throw new Error("Invalid mode provided")
const createScreens = async ({ screens, screenAccessRole }) => {
if (!screens?.length) {
return
} }
}
const createScreen = async screen => {
try { try {
let createdScreens = [] // Check we aren't clashing with an existing URL
if (hasExistingUrl(screen.routing.route, screen.routing.roleId)) {
for (let screen of screens) { let suffix = 2
// Check we aren't clashing with an existing URL let candidateUrl = makeCandidateUrl(screen, suffix)
if (hasExistingUrl(screen.routing.route)) { while (hasExistingUrl(candidateUrl, screen.routing.roleId)) {
let suffix = 2 candidateUrl = makeCandidateUrl(screen, ++suffix)
let candidateUrl = makeCandidateUrl(screen, suffix)
while (hasExistingUrl(candidateUrl)) {
candidateUrl = makeCandidateUrl(screen, ++suffix)
}
screen.routing.route = candidateUrl
} }
screen.routing.route = candidateUrl
// Sanitise URL
screen.routing.route = sanitizeUrl(screen.routing.route)
// Use the currently selected role
if (!screenAccessRole) {
return
}
screen.routing.roleId = screenAccessRole
// Create the screen
const response = await screenStore.save(screen)
createdScreens.push(response)
// Add link in layout. We only ever actually create 1 screen now, even
// for autoscreens, so it's always safe to do this.
await navigationStore.saveLink(
screen.routing.route,
capitalise(screen.routing.route.split("/")[1]),
screenAccessRole
)
} }
return createdScreens screen.routing.route = sanitizeUrl(screen.routing.route)
return await screenStore.save(screen)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notifications.error("Error creating screens") notifications.error("Error creating screens")
} }
} }
const addNavigationLink = async screen =>
await navigationStore.saveLink(
screen.routing.route,
capitalise(screen.routing.route.split("/")[1]),
screen.routing.roleId
)
// Checks if any screens exist in the store with the given route and // Checks if any screens exist in the store with the given route and
// currently selected role // currently selected role
const hasExistingUrl = url => { const hasExistingUrl = (url, screenAccessRole) => {
const roleId = screenAccessRole
const screens = get(screenStore).screens.filter( const screens = get(screenStore).screens.filter(
s => s.routing.roleId === roleId s => s.routing.roleId === screenAccessRole
) )
return !!screens.find(s => s.routing?.route === url) return !!screens.find(s => s.routing?.route === url)
} }
// Constructs a candidate URL for a new screen, suffixing the base of the // Constructs a candidate URL for a new screen, appending a given suffix to the
// screen's URL with a given suffix. // screen's URL
// e.g. "/sales/:id" => "/sales-1/:id" // e.g. "/sales/:id" => "/sales-1/:id"
const makeCandidateUrl = (screen, suffix) => { const makeCandidateUrl = (screen, suffix) => {
let url = screen.routing?.route || "" let url = screen.routing?.route || ""
@ -117,105 +97,79 @@
} }
} }
// Handler for NewScreenModal const onSelectDatasources = async () => {
export const show = newMode => { if (mode === "form") {
mode = newMode
templates = null
screens = null
selectedDatasources = null
blankScreenUrl = null
screenMode = mode
pendingScreen = null
screenAccessRole = Roles.BASIC
formType = null
if (mode === "grid" || mode === "gridDetails" || mode === "form") {
datasourceModal.show()
} else if (mode === "blank") {
let templates = getTemplates($tables.list)
const blankScreenTemplate = templates.find(
t => t.id === "createFromScratch"
)
pendingScreen = blankScreenTemplate.create()
screenDetailsModal.show()
} else {
throw new Error("Invalid mode provided")
}
}
// Handler for DatasourceModal confirmation, move to screen access select
const confirmScreenDatasources = async ({ datasources }) => {
selectedDatasources = datasources
if (screenMode === "form") {
formTypeModal.show() formTypeModal.show()
} else { } else if (mode === "grid") {
screenAccessRoleModal.show() await createGridScreen()
} else if (mode === "gridDetails") {
await createGridDetailsScreen()
} }
} }
// Handler for Datasource Screen Creation const createBlankScreen = async ({ screenUrl }) => {
const completeDatasourceScreenCreation = async () => { const screenTemplate = blankScreen(screenUrl)
templates = const screen = await createScreen(screenTemplate)
mode === "grid" await addNavigationLink(screenTemplate)
? gridListScreen(selectedDatasources)
: gridDetailsScreen(selectedDatasources)
const screens = templates.map(template => { loadNewScreen(screen)
let screenTemplate = template.create()
screenTemplate.autoTableId = template.resourceId
return screenTemplate
})
const createdScreens = await createScreens({ screens, screenAccessRole })
loadNewScreen(createdScreens)
} }
const confirmScreenBlank = async ({ screenUrl }) => { const createGridScreen = async () => {
blankScreenUrl = screenUrl let firstScreen = null
screenAccessRoleModal.show()
}
// Submit request for a blank screen for (let tableOrView of selectedTablesAndViews) {
const confirmBlankScreenCreation = async ({ const screenTemplate = gridScreen(
screenUrl, tableOrView,
screenAccessRole, permissions[tableOrView.id]
}) => { )
if (!pendingScreen) {
return
}
pendingScreen.routing.route = screenUrl
const createdScreens = await createScreens({
screens: [pendingScreen],
screenAccessRole,
})
loadNewScreen(createdScreens)
}
const onConfirmFormType = () => { const screen = await createScreen(screenTemplate)
screenAccessRoleModal.show() await addNavigationLink(screen)
}
const loadNewScreen = createdScreens => { firstScreen ??= screen
const lastScreen = createdScreens.slice(-1)[0]
// Go to new screen
if (lastScreen?.props?._children.length) {
// Focus on the main component for the streen type
const mainComponent = lastScreen?.props?._children?.[0]._id
$goto(`./${lastScreen._id}/${mainComponent}`)
} else {
$goto(`./${lastScreen._id}`)
} }
screenStore.select(lastScreen._id) loadNewScreen(firstScreen)
} }
const confirmFormScreenCreation = async () => { const createGridDetailsScreen = async () => {
templates = formScreen(selectedDatasources, { actionType: formType }) let firstScreen = null
screens = templates.map(template => {
let screenTemplate = template.create() for (let tableOrView of selectedTablesAndViews) {
return screenTemplate const screenTemplate = gridDetailsScreen(
}) tableOrView,
const createdScreens = await createScreens({ screens, screenAccessRole }) permissions[tableOrView.id]
)
const screen = await createScreen(screenTemplate)
await addNavigationLink(screen)
firstScreen ??= screen
}
loadNewScreen(firstScreen)
}
const createFormScreen = async formType => {
let firstScreen = null
for (let tableOrView of selectedTablesAndViews) {
const screenTemplate = formScreen(
tableOrView,
formType,
permissions[tableOrView.id]
)
const screen = await createScreen(screenTemplate)
// Only add a navigation link for `Create`, as both `Update` and `View`
// require an `id` in their URL in order to function.
if (formType === "Create") {
await addNavigationLink(screen)
}
firstScreen ??= screen
}
if (formType === "Update" || formType === "Create") { if (formType === "Update" || formType === "Create") {
const associatedTour = const associatedTour =
@ -229,66 +183,89 @@
} }
} }
// Go to new screen loadNewScreen(firstScreen)
loadNewScreen(createdScreens)
} }
// Submit screen config for creation. const loadNewScreen = screen => {
const confirmScreenCreation = async () => { if (screen?.props?._children.length) {
if (screenMode === "blank") { // Focus on the main component for the screen type
confirmBlankScreenCreation({ const mainComponent = screen?.props?._children?.[0]._id
screenUrl: blankScreenUrl, $goto(`./${screen._id}/${mainComponent}`)
screenAccessRole,
})
} else if (screenMode === "form") {
confirmFormScreenCreation()
} else { } else {
completeDatasourceScreenCreation() $goto(`./${screen._id}`)
} }
screenStore.select(screen._id)
} }
const roleSelectBack = () => { const fetchPermission = resourceId => {
if (screenMode === "blank") { permissions[resourceId] = { loading: true, read: null, write: null }
screenDetailsModal.show()
permissionsStore
.forResource(resourceId)
.then(permission => {
if (permissions[resourceId]?.loading) {
permissions[resourceId] = {
loading: false,
read: permission?.read?.role,
write: permission?.write?.role,
}
}
})
.catch(e => {
console.error("Error fetching permission data: ", e)
if (permissions[resourceId]?.loading) {
permissions[resourceId] = {
loading: false,
read: Roles.PUBLIC,
write: Roles.PUBLIC,
}
}
})
}
const deletePermission = resourceId => {
delete permissions[resourceId]
permissions = permissions
}
const handleTableOrViewToggle = ({ detail: tableOrView }) => {
const alreadySelected = selectedTablesAndViews.some(
selected => selected.id === tableOrView.id
)
if (!alreadySelected) {
fetchPermission(tableOrView.id)
selectedTablesAndViews = [...selectedTablesAndViews, tableOrView]
} else { } else {
datasourceModal.show() deletePermission(tableOrView.id)
selectedTablesAndViews = selectedTablesAndViews.filter(
selected => selected.id !== tableOrView.id
)
} }
} }
</script> </script>
<Modal bind:this={datasourceModal} autoFocus={false}> <Modal bind:this={datasourceModal} autoFocus={false}>
<DatasourceModal {mode} onConfirm={confirmScreenDatasources} /> <DatasourceModal
</Modal> {selectedTablesAndViews}
{permissions}
<Modal bind:this={screenAccessRoleModal}> onConfirm={onSelectDatasources}
<ScreenRoleModal on:toggle={handleTableOrViewToggle}
onConfirm={() => {
confirmScreenCreation()
}}
bind:screenAccessRole
onCancel={roleSelectBack}
screenUrl={blankScreenUrl}
confirmText={screenMode === "form" ? "Confirm" : "Done"}
/> />
</Modal> </Modal>
<Modal bind:this={screenDetailsModal}> <Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal <ScreenDetailsModal onConfirm={createBlankScreen} />
onConfirm={confirmScreenBlank}
initialUrl={blankScreenUrl}
/>
</Modal> </Modal>
<Modal bind:this={formTypeModal}> <Modal bind:this={formTypeModal}>
<FormTypeModal <FormTypeModal
onConfirm={onConfirmFormType} onConfirm={createFormScreen}
onCancel={() => { onCancel={() => {
formTypeModal.hide() formTypeModal.hide()
datasourceModal.show() datasourceModal.show()
}} }}
on:select={e => {
formType = e.detail
}}
type={formType}
/> />
</Modal> </Modal>

View File

@ -1,42 +1,95 @@
<script> <script>
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui" import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
import { datasources } from "stores/builder" import { datasources as datasourcesStore } from "stores/builder"
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants" import { IntegrationNames } from "constants"
import { onMount } from "svelte" import { createEventDispatcher, onMount } from "svelte"
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte" import TableOrViewOption from "./TableOrViewOption.svelte"
export let onCancel
export let onConfirm export let onConfirm
export let selectedTablesAndViews
export let permissions
let selectedSources = [] const dispatch = createEventDispatcher()
$: filteredSources = $datasources.list?.filter(datasource => { const getViews = table => {
return datasource.source !== IntegrationNames.REST && datasource["entities"] const views = Object.values(table.views || {}).filter(
}) view => view.version === 2
const toggleSelection = datasource => {
const exists = selectedSources.find(
d => d.resourceId === datasource.resourceId
) )
if (exists) {
selectedSources = selectedSources.filter( return views.map(view => ({
d => d.resourceId === datasource.resourceId icon: "Remove",
) name: view.name,
} else { id: view.id,
selectedSources = [...selectedSources, datasource] clientData: {
} ...view,
type: "viewV2",
label: view.name,
},
}))
} }
const confirmDatasourceSelection = async () => { const getTablesAndViews = datasource => {
await onConfirm({ let tablesAndViews = []
datasources: selectedSources, const rawTables = Array.isArray(datasource.entities)
}) ? datasource.entities
: Object.values(datasource.entities ?? {})
for (const rawTable of rawTables) {
if (rawTable._id === "ta_users") {
continue
}
const table = {
icon: "Table",
name: rawTable.name,
id: rawTable._id,
clientData: {
...rawTable,
label: rawTable.name,
tableId: rawTable._id,
type: "table",
},
}
tablesAndViews = tablesAndViews.concat([table, ...getViews(rawTable)])
}
return tablesAndViews
}
const getDatasources = rawDatasources => {
const datasources = []
for (const rawDatasource of rawDatasources) {
if (
rawDatasource.source === IntegrationNames.REST ||
!rawDatasource["entities"]
) {
continue
}
const datasource = {
name: rawDatasource.name,
iconComponent: ICONS[rawDatasource.source],
tablesAndViews: getTablesAndViews(rawDatasource),
}
datasources.push(datasource)
}
return datasources
}
$: datasources = getDatasources($datasourcesStore.list)
const toggleSelection = tableOrView => {
dispatch("toggle", tableOrView)
} }
onMount(async () => { onMount(async () => {
try { try {
await datasources.fetch() await datasourcesStore.fetch()
} catch (error) { } catch (error) {
notifications.error("Error fetching datasources") notifications.error("Error fetching datasources")
} }
@ -48,66 +101,35 @@
title="Autogenerated screens" title="Autogenerated screens"
confirmText="Confirm" confirmText="Confirm"
cancelText="Back" cancelText="Back"
onConfirm={confirmDatasourceSelection} {onConfirm}
{onCancel} disabled={!selectedTablesAndViews.length}
disabled={!selectedSources.length}
size="L" size="L"
> >
<Body size="S"> <Body size="S">
Select which datasources you would like to use to create your screens Select which datasources you would like to use to create your screens
</Body> </Body>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
{#each filteredSources as datasource} {#each datasources as datasource}
{@const entities = Array.isArray(datasource.entities)
? datasource.entities
: Object.values(datasource.entities || {})}
<div class="data-source-wrap"> <div class="data-source-wrap">
<div class="data-source-header"> <div class="data-source-header">
<svelte:component <svelte:component
this={ICONS[datasource.source]} this={datasource.iconComponent}
height="24" height="24"
width="24" width="24"
/> />
<div class="data-source-name">{datasource.name}</div> <div class="data-source-name">{datasource.name}</div>
</div> </div>
<!-- List all tables --> <!-- List all tables -->
{#each entities.filter(table => table._id !== "ta_users") as table} {#each datasource.tablesAndViews as tableOrView}
{@const views = Object.values(table.views || {}).filter( {@const selected = selectedTablesAndViews.some(
view => view.version === 2 selected => selected.id === tableOrView.id
)} )}
{@const tableDS = { <TableOrViewOption
tableId: table._id, roles={permissions[tableOrView.id]}
label: table.name, on:click={() => toggleSelection(tableOrView)}
resourceId: table._id,
type: "table",
}}
{@const selected = selectedSources.find(
datasource => datasource.resourceId === tableDS.resourceId
)}
<DatasourceTemplateRow
on:click={() => toggleSelection(tableDS)}
{selected} {selected}
datasource={tableDS} {tableOrView}
/> />
<!-- List all views inside this table -->
{#each views as view}
{@const viewDS = {
label: view.name,
id: view.id,
resourceId: view.id,
tableId: view.tableId,
type: "viewV2",
}}
{@const selected = selectedSources.find(
x => x.resourceId === viewDS.resourceId
)}
<DatasourceTemplateRow
on:click={() => toggleSelection(viewDS)}
{selected}
datasource={viewDS}
/>
{/each}
{/each} {/each}
</div> </div>
{/each} {/each}
@ -118,8 +140,11 @@
<style> <style>
.data-source-wrap { .data-source-wrap {
padding-bottom: var(--spectrum-alias-item-padding-s); padding-bottom: var(--spectrum-alias-item-padding-s);
display: grid; display: flex;
flex-direction: column;
grid-gap: var(--spacing-s); grid-gap: var(--spacing-s);
max-width: 100%;
min-width: 0;
} }
.data-source-header { .data-source-header {
display: flex; display: flex;

View File

@ -1,45 +0,0 @@
<script>
import { Icon } from "@budibase/bbui"
export let datasource
export let selected = false
$: icon = datasource.type === "viewV2" ? "Remove" : "Table"
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="data-source-entry" class:selected on:click>
<Icon name={icon} color="var(--spectrum-global-color-gray-600)" />
{datasource.label}
{#if selected}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
<style>
.data-source-entry {
cursor: pointer;
grid-gap: var(--spacing-m);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
display: flex;
align-items: center;
}
.data-source-entry:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.data-source-check {
margin-left: auto;
}
.data-source-check :global(.spectrum-Icon) {
color: var(--spectrum-global-color-green-600);
}
</style>

View File

@ -1,12 +1,10 @@
<script> <script>
import { ModalContent, Layout, Body, Icon } from "@budibase/bbui" import { ModalContent, Layout, Body, Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
let type = null
export let onCancel = () => {} export let onCancel = () => {}
export let onConfirm = () => {} export let onConfirm = () => {}
export let type
const dispatch = createEventDispatcher()
</script> </script>
<span> <span>
@ -14,7 +12,7 @@
title="Select form type" title="Select form type"
confirmText="Done" confirmText="Done"
cancelText="Back" cancelText="Back"
{onConfirm} onConfirm={() => onConfirm(type)}
{onCancel} {onCancel}
disabled={!type} disabled={!type}
size="L" size="L"
@ -25,9 +23,7 @@
<div <div
class="form-type" class="form-type"
class:selected={type === "Create"} class:selected={type === "Create"}
on:click={() => { on:click={() => (type = "Create")}
dispatch("select", "Create")
}}
> >
<div class="form-type-wrap"> <div class="form-type-wrap">
<div class="form-type-content"> <div class="form-type-content">
@ -46,9 +42,7 @@
<div <div
class="form-type" class="form-type"
class:selected={type === "Update"} class:selected={type === "Update"}
on:click={() => { on:click={() => (type = "Update")}
dispatch("select", "Update")
}}
> >
<div class="form-type-wrap"> <div class="form-type-wrap">
<div class="form-type-content"> <div class="form-type-content">
@ -65,9 +59,7 @@
<div <div
class="form-type" class="form-type"
class:selected={type === "View"} class:selected={type === "View"}
on:click={() => { on:click={() => (type = "View")}
dispatch("select", "View")
}}
> >
<div class="form-type-wrap"> <div class="form-type-wrap">
<div class="form-type-content"> <div class="form-type-content">

View File

@ -1,62 +0,0 @@
<script>
import { Select, ModalContent } from "@budibase/bbui"
import { RoleUtils } from "@budibase/frontend-core"
import { roles, screenStore } from "stores/builder"
import { get } from "svelte/store"
import { onMount } from "svelte"
export let onConfirm
export let onCancel
export let screenUrl
export let screenAccessRole
export let confirmText = "Done"
let error
const onChangeRole = e => {
const roleId = e.detail
if (routeExists(screenUrl, roleId)) {
error = "This URL is already taken for this access role"
} else {
error = null
}
}
const routeExists = (url, role) => {
if (!url || !role) {
return false
}
return get(screenStore).screens.some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === role
)
}
onMount(() => {
// Validate the initial role
onChangeRole({ detail: screenAccessRole })
})
</script>
<ModalContent
title="Access"
{confirmText}
cancelText="Back"
{onConfirm}
{onCancel}
disabled={!!error}
>
Select the level of access required to see these screens
<Select
bind:value={screenAccessRole}
on:change={onChangeRole}
label="Access"
{error}
getOptionLabel={role => role.name}
getOptionValue={role => role._id}
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
options={$roles}
placeholder={null}
/>
</ModalContent>

View File

@ -0,0 +1,112 @@
<script>
import { Icon, AbsTooltip } from "@budibase/bbui"
import RoleIcon from "components/common/RoleIcon.svelte"
export let tableOrView
export let roles
export let selected = false
$: hideRoles = roles == undefined || roles?.loading
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div role="button" tabindex="0" class="datasource" class:selected on:click>
<div class="content">
<Icon name={tableOrView.icon} />
<span>{tableOrView.name}</span>
</div>
<div class:hideRoles class="roles">
<AbsTooltip
type="info"
text={`Screens that only read data will be generated with access "${roles?.read?.toLowerCase()}"`}
>
<div class="role">
<span>read</span>
<RoleIcon
size="XS"
id={roles?.read}
disabled={roles?.loading !== false}
/>
</div>
</AbsTooltip>
<AbsTooltip
type="info"
text={`Screens that write data will be generated with access "${roles?.write?.toLowerCase()}"`}
>
<div class="role">
<span>write</span>
<RoleIcon
size="XS"
id={roles?.write}
disabled={roles?.loading !== false}
/>
</div>
</AbsTooltip>
</div>
</div>
<style>
.datasource {
cursor: pointer;
border: 1px solid var(--spectrum-global-color-gray-300);
transition: 160ms all;
border-radius: 4px;
display: flex;
align-items: center;
user-select: none;
background-color: var(--background);
}
.datasource :global(svg) {
transition: 160ms all;
color: var(--spectrum-global-color-gray-600);
}
.content {
padding: var(--spectrum-alias-item-padding-s);
display: flex;
align-items: center;
grid-gap: var(--spacing-m);
min-width: 0;
}
.content span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.datasource:hover {
border: 1px solid var(--grey-5);
}
.selected {
border: 1px solid var(--blue) !important;
}
.roles {
margin-left: auto;
display: flex;
flex-direction: column;
align-items: end;
padding-right: var(--spectrum-alias-item-padding-s);
opacity: 0.5;
transition: opacity 160ms;
}
.hideRoles {
opacity: 0;
pointer-events: none;
}
.role {
display: flex;
align-items: center;
}
.role span {
font-size: 11px;
margin-right: 5px;
}
</style>

View File

@ -4,47 +4,45 @@
Button, Button,
Heading, Heading,
Layout, Layout,
ProgressCircle,
notifications, notifications,
FancyForm,
FancyInput,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { auth, organisation } from "stores/portal" import { auth, organisation } from "stores/portal"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { TestimonialPage } from "@budibase/frontend-core/src/components" import { TestimonialPage } from "@budibase/frontend-core/src/components"
import { onMount } from "svelte" import { onMount } from "svelte"
import { handleError, passwordsMatch } from "./_components/utils" import PasswordRepeatInput from "../../../components/common/users/PasswordRepeatInput.svelte"
const resetCode = $params["?code"] const resetCode = $params["?code"]
let form let form
let formData = {}
let errors = {}
let loaded = false let loaded = false
let loading = false
let password
let passwordError
$: submitted = false
$: forceResetPassword = $auth?.user?.forceResetPassword $: forceResetPassword = $auth?.user?.forceResetPassword
async function reset() { async function reset() {
form.validate() if (!form.validate() || passwordError) {
if (Object.keys(errors).length > 0) {
return return
} }
submitted = true
try { try {
loading = true
if (forceResetPassword) { if (forceResetPassword) {
await auth.updateSelf({ await auth.updateSelf({
password: formData.password, password,
forceResetPassword: false, forceResetPassword: false,
}) })
$goto("../portal/") $goto("../portal/")
} else { } else {
await auth.resetPassword(formData.password, resetCode) await auth.resetPassword(password, resetCode)
notifications.success("Password reset successfully") notifications.success("Password reset successfully")
// send them to login if reset successful // send them to login if reset successful
$goto("./login") $goto("./login")
} }
} catch (err) { } catch (err) {
submitted = false loading = false
notifications.error(err.message || "Unable to reset password") notifications.error(err.message || "Unable to reset password")
} }
} }
@ -58,86 +56,37 @@
} }
loaded = true loaded = true
}) })
const handleKeydown = evt => {
if (evt.key === "Enter") {
reset()
}
}
</script> </script>
<svelte:window on:keydown={handleKeydown} />
<TestimonialPage enabled={$organisation.testimonialsEnabled}> <TestimonialPage enabled={$organisation.testimonialsEnabled}>
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
{#if loaded} {#if loaded}
<img alt="logo" src={$organisation.logoUrl || Logo} /> <img alt="logo" src={$organisation.logoUrl || Logo} />
{/if} {/if}
<Layout gap="XS" noPadding>
<Heading size="M">Reset your password</Heading>
<Body size="M">Please enter the new password you'd like to use.</Body>
</Layout>
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
<FancyForm bind:this={form}> <Heading size="M">Reset your password</Heading>
<FancyInput <Body size="M">Must contain at least 12 characters</Body>
label="Password" <PasswordRepeatInput
value={formData.password} bind:passwordForm={form}
type="password" bind:password
on:change={e => { bind:error={passwordError}
formData = { />
...formData, <Button secondary cta on:click={reset}>
password: e.detail, {#if loading}
} <ProgressCircle overBackground={true} size="S" />
}} {:else}
validate={() => { Reset
let fieldError = {} {/if}
</Button>
fieldError["password"] = !formData.password
? "Please enter a password"
: undefined
fieldError["confirmationPassword"] =
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.confirmationPassword
? "Passwords must match"
: undefined
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.password}
disabled={submitted}
/>
<FancyInput
label="Repeat Password"
value={formData.confirmationPassword}
type="password"
on:change={e => {
formData = {
...formData,
confirmationPassword: e.detail,
}
}}
validate={() => {
const isValid =
!passwordsMatch(
formData.password,
formData.confirmationPassword
) && formData.password
let fieldError = {
confirmationPassword: isValid ? "Passwords must match" : null,
}
errors = handleError({ ...errors, ...fieldError })
}}
error={errors.confirmationPassword}
disabled={submitted}
/>
</FancyForm>
</Layout> </Layout>
<div> <div />
<Button
disabled={Object.keys(errors).length > 0 ||
(forceResetPassword ? false : !resetCode)}
cta
on:click={reset}>Reset your password</Button
>
</div>
</Layout> </Layout>
</TestimonialPage> </TestimonialPage>

View File

@ -63,6 +63,11 @@ export class Screen extends BaseStructure {
return this return this
} }
autoTableId(autoTableId) {
this._json.autoTableId = autoTableId
return this
}
instanceName(name) { instanceName(name) {
this._json.props._instanceName = name this._json.props._instanceName = name
return this return this

View File

@ -0,0 +1,7 @@
import { Screen } from "./Screen"
const blankScreen = route => {
return new Screen().instanceName("New Screen").route(route).json()
}
export default blankScreen

View File

@ -1,12 +0,0 @@
import { Screen } from "./Screen"
export default {
name: `Create from scratch`,
id: `createFromScratch`,
create: () => createScreen(),
table: `Create from scratch`,
}
const createScreen = () => {
return new Screen().instanceName("New Screen").json()
}

View File

@ -3,41 +3,47 @@ import { Component } from "./Component"
import sanitizeUrl from "helpers/sanitizeUrl" import sanitizeUrl from "helpers/sanitizeUrl"
export const FORM_TEMPLATE = "FORM_TEMPLATE" export const FORM_TEMPLATE = "FORM_TEMPLATE"
export const formUrl = datasource => sanitizeUrl(`/${datasource.label}-form`) export const formUrl = (tableOrView, actionType) => {
if (actionType === "Create") {
// Mode not really necessary return sanitizeUrl(`/${tableOrView.name}/new`)
export default function (datasources, config) { } else if (actionType === "Update") {
if (!Array.isArray(datasources)) { return sanitizeUrl(`/${tableOrView.name}/edit/:id`)
return [] } else if (actionType === "View") {
return sanitizeUrl(`/${tableOrView.name}/view/:id`)
} }
return datasources.map(datasource => {
return {
name: `${datasource.label} - Form`,
create: () => createScreen(datasource, config),
id: FORM_TEMPLATE,
resourceId: datasource.resourceId,
}
})
} }
const generateMultistepFormBlock = (dataSource, { actionType } = {}) => { export const getRole = (permissions, actionType) => {
if (actionType === "View") {
return permissions.read
}
return permissions.write
}
const generateMultistepFormBlock = (tableOrView, actionType) => {
const multistepFormBlock = new Component( const multistepFormBlock = new Component(
"@budibase/standard-components/multistepformblock" "@budibase/standard-components/multistepformblock"
) )
multistepFormBlock multistepFormBlock
.customProps({ .customProps({
actionType, actionType,
dataSource, dataSource: tableOrView.clientData,
steps: [{}], steps: [{}],
rowId: actionType === "new" ? undefined : `{{ url.id }}`,
}) })
.instanceName(`${dataSource.label} - Multistep Form block`) .instanceName(`${tableOrView.name} - Multistep Form block`)
return multistepFormBlock return multistepFormBlock
} }
const createScreen = (datasource, config) => { const createScreen = (tableOrView, actionType, permissions) => {
return new Screen() return new Screen()
.route(formUrl(datasource)) .route(formUrl(tableOrView, actionType))
.instanceName(`${datasource.label} - Form`) .instanceName(`${tableOrView.name} - Form`)
.addChild(generateMultistepFormBlock(datasource, config)) .role(getRole(permissions, actionType))
.autoTableId(tableOrView.id)
.addChild(generateMultistepFormBlock(tableOrView, actionType))
.json() .json()
} }
export default createScreen

View File

@ -5,24 +5,9 @@ import { generate } from "shortid"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
export default function (datasources) { const gridDetailsUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - List with panel`,
create: () => createScreen(datasource),
id: GRID_DETAILS_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
export const GRID_DETAILS_TEMPLATE = "GRID_DETAILS_TEMPLATE" const createScreen = (tableOrView, permissions) => {
export const gridDetailsUrl = datasource => sanitizeUrl(`/${datasource.label}`)
const createScreen = datasource => {
/* /*
Create Row Create Row
*/ */
@ -47,7 +32,7 @@ const createScreen = datasource => {
type: "cta", type: "cta",
}) })
buttonGroup.instanceName(`${datasource.label} - Create`).customProps({ buttonGroup.instanceName(`${tableOrView.name} - Create`).customProps({
hAlign: "right", hAlign: "right",
buttons: [createButton.json()], buttons: [createButton.json()],
}) })
@ -62,7 +47,7 @@ const createScreen = datasource => {
const heading = new Component("@budibase/standard-components/heading") const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading") .instanceName("Table heading")
.customProps({ .customProps({
text: datasource?.label, text: tableOrView.name,
}) })
gridHeader.addChild(heading) gridHeader.addChild(heading)
@ -72,7 +57,7 @@ const createScreen = datasource => {
"@budibase/standard-components/formblock" "@budibase/standard-components/formblock"
) )
createFormBlock.instanceName("Create row form block").customProps({ createFormBlock.instanceName("Create row form block").customProps({
dataSource: datasource, dataSource: tableOrView.clientData,
labelPosition: "left", labelPosition: "left",
buttonPosition: "top", buttonPosition: "top",
actionType: "Create", actionType: "Create",
@ -83,7 +68,7 @@ const createScreen = datasource => {
showSaveButton: true, showSaveButton: true,
saveButtonLabel: "Save", saveButtonLabel: "Save",
actionType: "Create", actionType: "Create",
dataSource: datasource, dataSource: tableOrView.clientData,
}), }),
}) })
@ -99,7 +84,7 @@ const createScreen = datasource => {
const editFormBlock = new Component("@budibase/standard-components/formblock") const editFormBlock = new Component("@budibase/standard-components/formblock")
editFormBlock.instanceName("Edit row form block").customProps({ editFormBlock.instanceName("Edit row form block").customProps({
dataSource: datasource, dataSource: tableOrView.clientData,
labelPosition: "left", labelPosition: "left",
buttonPosition: "top", buttonPosition: "top",
actionType: "Update", actionType: "Update",
@ -112,7 +97,7 @@ const createScreen = datasource => {
saveButtonLabel: "Save", saveButtonLabel: "Save",
deleteButtonLabel: "Delete", deleteButtonLabel: "Delete",
actionType: "Update", actionType: "Update",
dataSource: datasource, dataSource: tableOrView.clientData,
}), }),
}) })
@ -121,7 +106,7 @@ const createScreen = datasource => {
const gridBlock = new Component("@budibase/standard-components/gridblock") const gridBlock = new Component("@budibase/standard-components/gridblock")
gridBlock gridBlock
.customProps({ .customProps({
table: datasource, table: tableOrView.clientData,
allowAddRows: false, allowAddRows: false,
allowEditRows: false, allowEditRows: false,
allowDeleteRows: false, allowDeleteRows: false,
@ -145,14 +130,18 @@ const createScreen = datasource => {
}, },
], ],
}) })
.instanceName(`${datasource.label} - Table`) .instanceName(`${tableOrView.name} - Table`)
return new Screen() return new Screen()
.route(gridDetailsUrl(datasource)) .route(gridDetailsUrl(tableOrView))
.instanceName(`${datasource.label} - List and details`) .instanceName(`${tableOrView.name} - List and details`)
.role(permissions.write)
.autoTableId(tableOrView.resourceId)
.addChild(gridHeader) .addChild(gridHeader)
.addChild(gridBlock) .addChild(gridBlock)
.addChild(createRowSidePanel) .addChild(createRowSidePanel)
.addChild(detailsSidePanel) .addChild(detailsSidePanel)
.json() .json()
} }
export default createScreen

View File

@ -1,41 +0,0 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
export default function (datasources) {
if (!Array.isArray(datasources)) {
return []
}
return datasources.map(datasource => {
return {
name: `${datasource.label} - List`,
create: () => createScreen(datasource),
id: GRID_LIST_TEMPLATE,
resourceId: datasource.resourceId,
}
})
}
export const GRID_LIST_TEMPLATE = "GRID_LIST_TEMPLATE"
export const gridListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
const createScreen = datasource => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: datasource?.label,
})
const gridBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${datasource.label} - Table`)
.customProps({
table: datasource,
})
return new Screen()
.route(gridListUrl(datasource))
.instanceName(`${datasource.label} - List`)
.addChild(heading)
.addChild(gridBlock)
.json()
}

View File

@ -0,0 +1,30 @@
import sanitizeUrl from "helpers/sanitizeUrl"
import { Screen } from "./Screen"
import { Component } from "./Component"
const gridUrl = tableOrView => sanitizeUrl(`/${tableOrView.name}`)
const createScreen = (tableOrView, permissions) => {
const heading = new Component("@budibase/standard-components/heading")
.instanceName("Table heading")
.customProps({
text: tableOrView.name,
})
const gridBlock = new Component("@budibase/standard-components/gridblock")
.instanceName(`${tableOrView.name} - Table`)
.customProps({
table: tableOrView.clientData,
})
return new Screen()
.route(gridUrl(tableOrView))
.instanceName(`${tableOrView.name} - List`)
.role(permissions.write)
.autoTableId(tableOrView.id)
.addChild(heading)
.addChild(gridBlock)
.json()
}
export default createScreen

View File

@ -1,35 +0,0 @@
import gridListScreen from "./gridListScreen"
import gridDetailsScreen from "./gridDetailsScreen"
import createFromScratchScreen from "./createFromScratchScreen"
import formScreen from "./formScreen"
const allTemplates = datasources => [
...gridListScreen(datasources),
...gridDetailsScreen(datasources),
...formScreen(datasources),
]
// Allows us to apply common behaviour to all create() functions
const createTemplateOverride = template => () => {
const screen = template.create()
screen.name = screen.props._id
screen.routing.route = screen.routing.route.toLowerCase()
screen.template = template.id
return screen
}
export default datasources => {
const enrichTemplate = template => ({
...template,
create: createTemplateOverride(template),
})
const fromScratch = enrichTemplate(createFromScratchScreen)
const tableTemplates = allTemplates(datasources).map(enrichTemplate)
return [
fromScratch,
...tableTemplates.sort((templateA, templateB) => {
return templateA.name > templateB.name ? 1 : -1
}),
]
}

View File

@ -2,12 +2,12 @@ import { Automation, AutomationTriggerStepId } from "@budibase/types"
export function isRowAction(automation: Automation) { export function isRowAction(automation: Automation) {
const result = const result =
automation.definition.trigger.stepId === AutomationTriggerStepId.ROW_ACTION automation.definition.trigger?.stepId === AutomationTriggerStepId.ROW_ACTION
return result return result
} }
export function isAppAction(automation: Automation) { export function isAppAction(automation: Automation) {
const result = const result =
automation.definition.trigger.stepId === AutomationTriggerStepId.APP automation.definition.trigger?.stepId === AutomationTriggerStepId.APP
return result return result
} }

View File

@ -13,6 +13,8 @@ export interface PlatformUserByEmail extends Document {
*/ */
export interface PlatformUserById extends Document { export interface PlatformUserById extends Document {
tenantId: string tenantId: string
email?: string
ssoId?: string
} }
/** /**
@ -22,6 +24,7 @@ export interface PlatformUserBySsoId extends Document {
tenantId: string tenantId: string
userId: string userId: string
email: string email: string
ssoId?: string
} }
export type PlatformUser = export type PlatformUser =

View File

@ -62,7 +62,7 @@ export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
const { email, ssoId } = ctx.request.body const { email, ssoId } = ctx.request.body
try { try {
// Status is changed to 404 from getUserDoc if user is not found // Status is changed to 404 from getUserDoc if user is not found
let userByEmail = (await platform.users.getUserDoc( const userByEmail = (await platform.users.getUserDoc(
email email
)) as PlatformUserByEmail )) as PlatformUserByEmail
await platform.users.addSsoUser( await platform.users.addSsoUser(
@ -71,6 +71,13 @@ export const addSsoSupport = async (ctx: Ctx<AddSSoUserRequest>) => {
userByEmail.userId, userByEmail.userId,
userByEmail.tenantId userByEmail.tenantId
) )
// Need to get the _rev of the user doc to update
const userById = await platform.users.getUserDoc(userByEmail.userId)
await platform.users.updateUserDoc({
...userById,
email,
ssoId,
})
ctx.status = 200 ctx.status = 200
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
@ -268,7 +275,7 @@ export const find = async (ctx: any) => {
export const tenantUserLookup = async (ctx: any) => { export const tenantUserLookup = async (ctx: any) => {
const id = ctx.params.id const id = ctx.params.id
const user = await userSdk.core.getPlatformUser(id) const user = await userSdk.core.getFirstPlatformUser(id)
if (user) { if (user) {
ctx.body = user ctx.body = user
} else { } else {

View File

@ -1,6 +0,0 @@
if [ -d "packages/pro/src" ]; then
cd packages/pro
yarn
lerna bootstrap
fi

View File

@ -51,20 +51,6 @@ async function runBuild(entry, outfile) {
fs.readFileSync(tsconfig, "utf-8") fs.readFileSync(tsconfig, "utf-8")
) )
if (
!fs.existsSync(path.join(__dirname, "../packages/pro/src")) &&
tsconfigPathPluginContent.compilerOptions?.paths
) {
// If we don't have pro, we cannot bundle backend-core.
// Otherwise, the main context will not be shared between libraries
delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
"@budibase/backend-core"
]
delete tsconfigPathPluginContent?.compilerOptions?.paths?.[
"@budibase/backend-core/*"
]
}
const sharedConfig = { const sharedConfig = {
entryPoints: [entry], entryPoints: [entry],
bundle: true, bundle: true,
@ -75,7 +61,7 @@ async function runBuild(entry, outfile) {
svelteCompilePlugin, svelteCompilePlugin,
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }), TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
nodeExternalsPlugin({ nodeExternalsPlugin({
allowList: ["@budibase/frontend-core", "svelte"], allowList: ["@budibase/frontend-core", "@budibase/pro", "svelte"],
}), }),
], ],
preserveSymlinks: true, preserveSymlinks: true,

View File

@ -1,11 +1,5 @@
#!/bin/bash #!/bin/bash
# Check if the pro submodule is loaded
if [ ! -d "./packages/pro/src" ]; then
echo "[ERROR] Submodule is not loaded. This is only allowed with loaded submodules."
exit 1
fi
yarn build:apps yarn build:apps
docker compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0 docker compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0

241
yarn.lock
View File

@ -1987,13 +1987,20 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": "@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.24.7" version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12"
integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@babel/runtime@^7.13.10":
version "7.25.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb"
integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3": "@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3":
version "7.22.15" version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
@ -2185,9 +2192,9 @@
"@bull-board/api" "5.10.2" "@bull-board/api" "5.10.2"
"@camunda8/sdk@^8.5.3": "@camunda8/sdk@^8.5.3":
version "8.6.7" version "8.6.10"
resolved "https://registry.yarnpkg.com/@camunda8/sdk/-/sdk-8.6.7.tgz#9a8d5fbe38a53ae1ab13ff59c24cf6747c20f575" resolved "https://registry.yarnpkg.com/@camunda8/sdk/-/sdk-8.6.10.tgz#61fdadc6bc89a234648ba4bc622b0db10f283de9"
integrity sha512-YpMsrBz5SlnagV8kei53WmB7es09ejNoOg3c6Dta+zGnSumClwGjL4IEGKqSjFk0Z4eVNIBIbRYl1i8gXRdvJg== integrity sha512-FzSoLYd0yFFElC2G3NX93GnP7r53uQDR+6njV1EEAGPhz4QQfZeEW07vMNZ9BFeNn5jhtv9IWmHdHxYwJxxmcw==
dependencies: dependencies:
"@grpc/grpc-js" "1.10.9" "@grpc/grpc-js" "1.10.9"
"@grpc/proto-loader" "0.7.13" "@grpc/proto-loader" "0.7.13"
@ -3362,6 +3369,11 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/sourcemap-codec@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@0.3.9": "@jridgewell/trace-mapping@0.3.9":
version "0.3.9" version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
@ -4144,81 +4156,161 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27"
integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==
"@rollup/rollup-android-arm-eabi@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz#7746deb85e4a8fb54fbfda8ac5c102692f102476"
integrity sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==
"@rollup/rollup-android-arm64@4.18.0": "@rollup/rollup-android-arm64@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203"
integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==
"@rollup/rollup-android-arm64@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz#93de4d867709d3313794723b5afd91e1e174f906"
integrity sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==
"@rollup/rollup-darwin-arm64@4.18.0": "@rollup/rollup-darwin-arm64@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096"
integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==
"@rollup/rollup-darwin-arm64@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz#e41e6a81673260ab196e0f59462b9940a6ac03cd"
integrity sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==
"@rollup/rollup-darwin-x64@4.18.0": "@rollup/rollup-darwin-x64@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c"
integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==
"@rollup/rollup-darwin-x64@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz#2b0a0aef6e8c5317d494cfc9076d7a16b099bdcb"
integrity sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==
"@rollup/rollup-linux-arm-gnueabihf@4.18.0": "@rollup/rollup-linux-arm-gnueabihf@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8"
integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==
"@rollup/rollup-linux-arm-gnueabihf@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz#e22319deb5367384ef315e66bc6de80d2bf2b3ae"
integrity sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==
"@rollup/rollup-linux-arm-musleabihf@4.18.0": "@rollup/rollup-linux-arm-musleabihf@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549"
integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==
"@rollup/rollup-linux-arm-musleabihf@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz#d5dd68f5d7ae21b345a5c87208c94e5c813f54b8"
integrity sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==
"@rollup/rollup-linux-arm64-gnu@4.18.0": "@rollup/rollup-linux-arm64-gnu@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577"
integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==
"@rollup/rollup-linux-arm64-gnu@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz#1703d3a418d33f8f025acaf93f39ca1efcd5b645"
integrity sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==
"@rollup/rollup-linux-arm64-musl@4.18.0": "@rollup/rollup-linux-arm64-musl@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c"
integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==
"@rollup/rollup-linux-arm64-musl@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz#3f59c2c6e60f75ce8b1090bd841c555e3bb01f0e"
integrity sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==
"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": "@rollup/rollup-linux-powerpc64le-gnu@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf"
integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==
"@rollup/rollup-linux-powerpc64le-gnu@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz#3f99a0921596a6f539121a312df29af52a205f15"
integrity sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==
"@rollup/rollup-linux-riscv64-gnu@4.18.0": "@rollup/rollup-linux-riscv64-gnu@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9"
integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==
"@rollup/rollup-linux-riscv64-gnu@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz#c08fb3e629d50d2eac31329347cfc559a1cf81d1"
integrity sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==
"@rollup/rollup-linux-s390x-gnu@4.18.0": "@rollup/rollup-linux-s390x-gnu@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec"
integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==
"@rollup/rollup-linux-s390x-gnu@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz#173722cd745779d730d4b24d21386185e0e12de8"
integrity sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==
"@rollup/rollup-linux-x64-gnu@4.18.0": "@rollup/rollup-linux-x64-gnu@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942"
integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==
"@rollup/rollup-linux-x64-gnu@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz#0af2b6541ab0f4954d2c4f96bcdc7947420dd28c"
integrity sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==
"@rollup/rollup-linux-x64-musl@4.18.0": "@rollup/rollup-linux-x64-musl@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d"
integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==
"@rollup/rollup-linux-x64-musl@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz#f973f9552744764b221128f7c3629222216ace69"
integrity sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==
"@rollup/rollup-win32-arm64-msvc@4.18.0": "@rollup/rollup-win32-arm64-msvc@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf"
integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==
"@rollup/rollup-win32-arm64-msvc@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz#21ac5ed84d914bc31821fec3dd909f7257cfb17b"
integrity sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==
"@rollup/rollup-win32-ia32-msvc@4.18.0": "@rollup/rollup-win32-ia32-msvc@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54"
integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==
"@rollup/rollup-win32-ia32-msvc@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz#0cfe740063b35dcd5a62c4e243226631a846ce11"
integrity sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==
"@rollup/rollup-win32-x64-msvc@4.18.0": "@rollup/rollup-win32-x64-msvc@4.18.0":
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4"
integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==
"@rollup/rollup-win32-x64-msvc@4.19.1":
version "4.19.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz#5f2c40d3f1b53ede80fb4e6964f840c0f8936832"
integrity sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==
"@roxi/routify@2.18.0": "@roxi/routify@2.18.0":
version "2.18.0" version "2.18.0"
resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.0.tgz#8f88bedd936312d0dbe44cbc11ab179b1f938ec2" resolved "https://registry.yarnpkg.com/@roxi/routify/-/routify-2.18.0.tgz#8f88bedd936312d0dbe44cbc11ab179b1f938ec2"
@ -5699,11 +5791,11 @@
integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg== integrity sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==
"@types/node@>=8.1.0": "@types/node@>=8.1.0":
version "20.14.9" version "22.0.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.0.0.tgz#04862a2a71e62264426083abe1e27e87cac05a30"
integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg== integrity sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~6.11.1"
"@types/node@^18.11.18": "@types/node@^18.11.18":
version "18.19.10" version "18.19.10"
@ -8005,7 +8097,20 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==
chai@^4.3.10, chai@^4.3.7: chai@^4.3.10:
version "4.5.0"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8"
integrity sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==
dependencies:
assertion-error "^1.1.0"
check-error "^1.0.3"
deep-eql "^4.1.3"
get-func-name "^2.0.2"
loupe "^2.3.6"
pathval "^1.1.1"
type-detect "^4.1.0"
chai@^4.3.7:
version "4.4.1" version "4.4.1"
resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1"
integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==
@ -9118,11 +9223,16 @@ dateformat@^4.6.3:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==
dayjs@^1.10.8, dayjs@^1.8.15: dayjs@^1.10.8:
version "1.11.11" version "1.11.11"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
dayjs@^1.8.15:
version "1.11.12"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.12.tgz#5245226cc7f40a15bf52e0b99fd2a04669ccac1d"
integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==
dc-polyfill@^0.1.2: dc-polyfill@^0.1.2:
version "0.1.3" version "0.1.3"
resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.3.tgz#fe9eefc86813439dd46d6f9ad9582ec079c39720" resolved "https://registry.yarnpkg.com/dc-polyfill/-/dc-polyfill-0.1.3.tgz#fe9eefc86813439dd46d6f9ad9582ec079c39720"
@ -10319,10 +10429,10 @@ es6-promise@^4.2.4:
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
esbuild-node-externals@^1.8.0: esbuild-node-externals@^1.14.0:
version "1.8.0" version "1.14.0"
resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.8.0.tgz#878fbe458d4e58337753c2eacfd7200dc1077bd1" resolved "https://registry.yarnpkg.com/esbuild-node-externals/-/esbuild-node-externals-1.14.0.tgz#fc2950c67a068dc2b538fd1381ad7d8e20a6f54d"
integrity sha512-pYslmT8Bl383UnfxzHQQRpCgBNIOwAzDaYheuIeI4CODxelsN/eQroVn5STDow5QOpRalMgWUR+R8LfSgUROcw== integrity sha512-jMWnTlCII3cLEjR5+u0JRSTJuP+MgbjEHKfwSIAI41NgLQ0ZjfzjchlbEn0r7v2u5gCBMSEYvYlkO7GDG8gG3A==
dependencies: dependencies:
find-up "^5.0.0" find-up "^5.0.0"
tslib "^2.4.1" tslib "^2.4.1"
@ -10920,7 +11030,14 @@ fast-xml-parser@4.2.5:
dependencies: dependencies:
strnum "^1.0.5" strnum "^1.0.5"
fast-xml-parser@^4.1.3, fast-xml-parser@^4.2.2, fast-xml-parser@^4.2.5: fast-xml-parser@^4.1.3:
version "4.4.1"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz#86dbf3f18edf8739326447bcaac31b4ae7f6514f"
integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==
dependencies:
strnum "^1.0.5"
fast-xml-parser@^4.2.2, fast-xml-parser@^4.2.5:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz#341cc98de71e9ba9e651a67f41f1752d1441a501" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz#341cc98de71e9ba9e651a67f41f1752d1441a501"
integrity sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg== integrity sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==
@ -11693,9 +11810,9 @@ glob@^10.0.0, glob@^10.2.2:
path-scurry "^1.11.1" path-scurry "^1.11.1"
glob@^10.3.7: glob@^10.3.7:
version "10.4.2" version "10.4.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.2.tgz#bed6b95dade5c1f80b4434daced233aee76160e5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
integrity sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w== integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
dependencies: dependencies:
foreground-child "^3.1.0" foreground-child "^3.1.0"
jackspeak "^3.1.2" jackspeak "^3.1.2"
@ -15131,13 +15248,20 @@ magic-string@^0.26.7:
dependencies: dependencies:
sourcemap-codec "^1.4.8" sourcemap-codec "^1.4.8"
magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5: magic-string@^0.30.3, magic-string@^0.30.4:
version "0.30.10" version "0.30.10"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15" "@jridgewell/sourcemap-codec" "^1.4.15"
magic-string@^0.30.5:
version "0.30.11"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954"
integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
make-dir@4.0.0: make-dir@4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e"
@ -17800,9 +17924,9 @@ postcss@^8.1.7, postcss@^8.2.9, postcss@^8.3.11, postcss@^8.4.12, postcss@^8.4.2
source-map-js "^1.2.0" source-map-js "^1.2.0"
postcss@^8.4.39: postcss@^8.4.39:
version "8.4.39" version "8.4.40"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8"
integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw== integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==
dependencies: dependencies:
nanoid "^3.3.7" nanoid "^3.3.7"
picocolors "^1.0.1" picocolors "^1.0.1"
@ -17839,9 +17963,9 @@ posthog-js@^1.118.0:
preact "^10.19.3" preact "^10.19.3"
posthog-js@^1.13.4: posthog-js@^1.13.4:
version "1.144.1" version "1.150.1"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.144.1.tgz#a665f324be3e78a140ec1cf9488be8e8947c052b" resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.150.1.tgz#ce2e0aa0dc30369bf1b1b9a38b9fbf25e5c01ba0"
integrity sha512-CdhKH+gTFJgVEDVdeuhD2Mm4yMRr6qhVSPNDUQ7naJfj/HMhcZjF/ImobluD4YGd85dLq9nqImEIcf4Twd9h6g== integrity sha512-jHSnqtAWkUQkiedQgHpD00+z8RUF0loDq7ORakBKfQjdntTJIEk16ewqTNRxnpE86guWDoy2J3iAqLgAYfFaLA==
dependencies: dependencies:
fflate "^0.4.8" fflate "^0.4.8"
preact "^10.19.3" preact "^10.19.3"
@ -18427,9 +18551,9 @@ q@^1.1.2:
integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
qs@^6.10.3: qs@^6.10.3:
version "6.12.2" version "6.12.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.2.tgz#5443b587f3bf73ac68968de491e5b25bafe04478" resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.3.tgz#e43ce03c8521b9c7fd7f1f13e514e5ca37727754"
integrity sha512-x+NLUpx9SYrcwXtX7ob1gnkSems4i/mGZX5SlYxwIau6RrUSODO89TR/XDGGpn5RPWSYIB+aSfuSlV5+CmbTBg== integrity sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==
dependencies: dependencies:
side-channel "^1.0.6" side-channel "^1.0.6"
@ -18943,9 +19067,9 @@ requirejs-config-file@^4.0.0:
stringify-object "^3.2.1" stringify-object "^3.2.1"
requirejs@^2.3.5: requirejs@^2.3.5:
version "2.3.6" version "2.3.7"
resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.7.tgz#0b22032e51a967900e0ae9f32762c23a87036bd0"
integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== integrity sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==
requires-port@^1.0.0: requires-port@^1.0.0:
version "1.0.0" version "1.0.0"
@ -19080,9 +19204,9 @@ rimraf@^4.4.1:
glob "^9.2.0" glob "^9.2.0"
rimraf@^5.0.7: rimraf@^5.0.7:
version "5.0.7" version "5.0.9"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.7.tgz#27bddf202e7d89cb2e0381656380d1734a854a74" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.9.tgz#c3baa1b886eadc2ec7981a06a593c3d01134ffe9"
integrity sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg== integrity sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==
dependencies: dependencies:
glob "^10.3.7" glob "^10.3.7"
@ -19248,7 +19372,32 @@ rollup@^3.27.1:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" fsevents "~2.3.2"
rollup@^4.13.0, rollup@^4.9.4, rollup@^4.9.6: rollup@^4.13.0:
version "4.19.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.19.1.tgz#21d865cd60d4a325172ce8b082e60caccd97b309"
integrity sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==
dependencies:
"@types/estree" "1.0.5"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.19.1"
"@rollup/rollup-android-arm64" "4.19.1"
"@rollup/rollup-darwin-arm64" "4.19.1"
"@rollup/rollup-darwin-x64" "4.19.1"
"@rollup/rollup-linux-arm-gnueabihf" "4.19.1"
"@rollup/rollup-linux-arm-musleabihf" "4.19.1"
"@rollup/rollup-linux-arm64-gnu" "4.19.1"
"@rollup/rollup-linux-arm64-musl" "4.19.1"
"@rollup/rollup-linux-powerpc64le-gnu" "4.19.1"
"@rollup/rollup-linux-riscv64-gnu" "4.19.1"
"@rollup/rollup-linux-s390x-gnu" "4.19.1"
"@rollup/rollup-linux-x64-gnu" "4.19.1"
"@rollup/rollup-linux-x64-musl" "4.19.1"
"@rollup/rollup-win32-arm64-msvc" "4.19.1"
"@rollup/rollup-win32-ia32-msvc" "4.19.1"
"@rollup/rollup-win32-x64-msvc" "4.19.1"
fsevents "~2.3.2"
rollup@^4.9.4, rollup@^4.9.6:
version "4.18.0" version "4.18.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda"
integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==
@ -21204,6 +21353,11 @@ type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8:
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
type-detect@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
type-fest@^0.13.1: type-fest@^0.13.1:
version "0.13.1" version "0.13.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
@ -21406,6 +21560,11 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
undici-types@~6.11.1:
version "6.11.1"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.11.1.tgz#432ea6e8efd54a48569705a699e62d8f4981b197"
integrity sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==
undici@^4.14.1: undici@^4.14.1:
version "4.16.0" version "4.16.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-4.16.0.tgz#469bb87b3b918818d3d7843d91a1d08da357d5ff" resolved "https://registry.yarnpkg.com/undici/-/undici-4.16.0.tgz#469bb87b3b918818d3d7843d91a1d08da357d5ff"
@ -21759,9 +21918,9 @@ vite-plugin-static-copy@^0.17.0:
fsevents "~2.3.2" fsevents "~2.3.2"
vite@^5.0.0: vite@^5.0.0:
version "5.3.3" version "5.3.5"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.3.tgz#5265b1f0a825b3b6564c2d07524777c83e3c04c2" resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.5.tgz#b847f846fb2b6cb6f6f4ed50a830186138cb83d8"
integrity sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A== integrity sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==
dependencies: dependencies:
esbuild "^0.21.3" esbuild "^0.21.3"
postcss "^8.4.39" postcss "^8.4.39"
@ -21896,9 +22055,9 @@ web-streams-polyfill@^3.2.1:
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
web-vitals@^4.0.1: web-vitals@^4.0.1:
version "4.2.1" version "4.2.2"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.1.tgz#67eec387ddd0ef4c25574a01ab9dae723eee2b97" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.2.tgz#e883245180b95e175eb75a5ca8903b1a11597d7a"
integrity sha512-U6bAxeudnhDqcXNl50JC4hLlqox9DZnngxfISZm3DMZnonW35xtJOVUc091L+DOY+6hVZVpKXoiCP0RiT6339Q== integrity sha512-nYfoOqb4EmElljyXU2qdeE76KsvoHdftQKY4DzA9Aw8DervCg2bG634pHLrJ/d6+B4mE3nWTSJv8Mo7B2mbZkw==
webfinger@^0.4.2: webfinger@^0.4.2:
version "0.4.2" version "0.4.2"