mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
231 changed files with 4141 additions and 3299 deletions
@ -1,11 +1,45 @@ |
|||
export const generateID = () => { |
|||
const rand = Math.random().toString(32).substring(2) |
|||
/** |
|||
* Generates a DOM safe UUID. |
|||
* Starting with a letter is important to make it DOM safe. |
|||
* @return {string} a random DOM safe UUID |
|||
*/ |
|||
export function uuid() { |
|||
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => { |
|||
const r = (Math.random() * 16) | 0 |
|||
const v = c === "x" ? r : (r & 0x3) | 0x8 |
|||
return v.toString(16) |
|||
}) |
|||
} |
|||
|
|||
// Starts with a letter so that its a valid DOM ID
|
|||
return `A${rand}` |
|||
/** |
|||
* Capitalises a string |
|||
* @param string the string to capitalise |
|||
* @return {string} the capitalised string |
|||
*/ |
|||
export const capitalise = string => { |
|||
if (!string) { |
|||
return string |
|||
} |
|||
return string.substring(0, 1).toUpperCase() + string.substring(1) |
|||
} |
|||
|
|||
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) |
|||
/** |
|||
* Computes a short hash of a string |
|||
* @param string the string to compute a hash of |
|||
* @return {string} the hash string |
|||
*/ |
|||
export const hashString = string => { |
|||
if (!string) { |
|||
return "0" |
|||
} |
|||
let hash = 0 |
|||
for (let i = 0; i < string.length; i++) { |
|||
let char = string.charCodeAt(i) |
|||
hash = (hash << 5) - hash + char |
|||
hash = hash & hash // Convert to 32bit integer
|
|||
} |
|||
return hash.toString() |
|||
} |
|||
|
|||
/** |
|||
* Gets a key within an object. The key supports dot syntax for retrieving deep |
|||
@ -0,0 +1,48 @@ |
|||
import { |
|||
createAPIClient, |
|||
CookieUtils, |
|||
Constants, |
|||
} from "@budibase/frontend-core" |
|||
import { store } from "./builderStore" |
|||
import { get } from "svelte/store" |
|||
import { auth } from "./stores/portal" |
|||
|
|||
export const API = createAPIClient({ |
|||
attachHeaders: headers => { |
|||
// Attach app ID header from store
|
|||
headers["x-budibase-app-id"] = get(store).appId |
|||
|
|||
// Add csrf token if authenticated
|
|||
const user = get(auth).user |
|||
if (user?.csrfToken) { |
|||
headers["x-csrf-token"] = user.csrfToken |
|||
} |
|||
}, |
|||
|
|||
onError: error => { |
|||
const { url, message, status, method, handled } = error || {} |
|||
|
|||
// Log all API errors to Sentry
|
|||
// analytics.captureException(error)
|
|||
|
|||
// Log any errors that we haven't manually handled
|
|||
if (!handled) { |
|||
console.error("Unhandled error from API client", error) |
|||
return |
|||
} |
|||
|
|||
// Log all errors to console
|
|||
console.warn(`[Builder] HTTP ${status} on ${method}:${url}\n\t${message}`) |
|||
|
|||
// Logout on 403's
|
|||
if (status === 403) { |
|||
// Remove cookies
|
|||
CookieUtils.removeCookie(Constants.Cookies.Auth) |
|||
|
|||
// Reload after removing cookie, go to login
|
|||
if (!url.includes("self") && !url.includes("login")) { |
|||
location.reload() |
|||
} |
|||
} |
|||
}, |
|||
}) |
|||
@ -1,49 +0,0 @@ |
|||
import { store } from "./index" |
|||
import { get as svelteGet } from "svelte/store" |
|||
import { removeCookie, Cookies } from "./cookies" |
|||
import { auth } from "stores/portal" |
|||
|
|||
const apiCall = |
|||
method => |
|||
async (url, body, headers = { "Content-Type": "application/json" }) => { |
|||
headers["x-budibase-app-id"] = svelteGet(store).appId |
|||
headers["x-budibase-api-version"] = "1" |
|||
|
|||
// add csrf token if authenticated
|
|||
const user = svelteGet(auth).user |
|||
if (user && user.csrfToken) { |
|||
headers["x-csrf-token"] = user.csrfToken |
|||
} |
|||
|
|||
const json = headers["Content-Type"] === "application/json" |
|||
const resp = await fetch(url, { |
|||
method: method, |
|||
body: json ? JSON.stringify(body) : body, |
|||
headers, |
|||
}) |
|||
if (resp.status === 403) { |
|||
if (url.includes("/api/templates")) { |
|||
return { json: () => [] } |
|||
} |
|||
removeCookie(Cookies.Auth) |
|||
// reload after removing cookie, go to login
|
|||
if (!url.includes("self") && !url.includes("login")) { |
|||
location.reload() |
|||
} |
|||
} |
|||
return resp |
|||
} |
|||
|
|||
export const post = apiCall("POST") |
|||
export const get = apiCall("GET") |
|||
export const patch = apiCall("PATCH") |
|||
export const del = apiCall("DELETE") |
|||
export const put = apiCall("PUT") |
|||
|
|||
export default { |
|||
post: apiCall("POST"), |
|||
get: apiCall("GET"), |
|||
patch: apiCall("PATCH"), |
|||
delete: apiCall("DELETE"), |
|||
put: apiCall("PUT"), |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
import { get } from "builderStore/api" |
|||
|
|||
/** |
|||
* Fetches the definitions for component library components. This includes |
|||
* their props and other metadata from components.json. |
|||
* @param {string} appId - ID of the currently running app |
|||
*/ |
|||
export const fetchComponentLibDefinitions = async appId => { |
|||
const LIB_DEFINITION_URL = `/api/${appId}/components/definitions` |
|||
try { |
|||
const libDefinitionResponse = await get(LIB_DEFINITION_URL) |
|||
return await libDefinitionResponse.json() |
|||
} catch (err) { |
|||
console.error(`Error fetching component definitions for ${appId}`, err) |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
import { writable } from "svelte/store" |
|||
import { generate } from "shortid" |
|||
|
|||
export const notificationStore = writable({ |
|||
notifications: [], |
|||
}) |
|||
|
|||
export function send(message, type = "default") { |
|||
notificationStore.update(state => { |
|||
state.notifications = [ |
|||
...state.notifications, |
|||
{ id: generate(), type, message }, |
|||
] |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
export const notifier = { |
|||
danger: msg => send(msg, "danger"), |
|||
warning: msg => send(msg, "warning"), |
|||
info: msg => send(msg, "info"), |
|||
success: msg => send(msg, "success"), |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
export function uuid() { |
|||
// always want to make this start with a letter, as this makes it
|
|||
// easier to use with template string bindings in the client
|
|||
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => { |
|||
const r = (Math.random() * 16) | 0, |
|||
v = c == "x" ? r : (r & 0x3) | 0x8 |
|||
return v.toString(16) |
|||
}) |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
import api from "builderStore/api" |
|||
|
|||
export async function createUser(user) { |
|||
const CREATE_USER_URL = `/api/users/metadata` |
|||
const response = await api.post(CREATE_USER_URL, user) |
|||
return await response.json() |
|||
} |
|||
|
|||
export async function saveRow(row, tableId) { |
|||
const SAVE_ROW_URL = `/api/${tableId}/rows` |
|||
const response = await api.post(SAVE_ROW_URL, row) |
|||
|
|||
return await response.json() |
|||
} |
|||
|
|||
export async function deleteRow(row) { |
|||
const DELETE_ROWS_URL = `/api/${row.tableId}/rows` |
|||
return api.delete(DELETE_ROWS_URL, { |
|||
_id: row._id, |
|||
_rev: row._rev, |
|||
}) |
|||
} |
|||
|
|||
export async function fetchDataForTable(tableId) { |
|||
const FETCH_ROWS_URL = `/api/${tableId}/rows` |
|||
|
|||
const response = await api.get(FETCH_ROWS_URL) |
|||
const json = await response.json() |
|||
|
|||
if (response.status !== 200) { |
|||
throw new Error(json.message) |
|||
} |
|||
return json |
|||
} |
|||
@ -1,96 +0,0 @@ |
|||
/** |
|||
* Operator options for lucene queries |
|||
*/ |
|||
export const OperatorOptions = { |
|||
Equals: { |
|||
value: "equal", |
|||
label: "Equals", |
|||
}, |
|||
NotEquals: { |
|||
value: "notEqual", |
|||
label: "Not equals", |
|||
}, |
|||
Empty: { |
|||
value: "empty", |
|||
label: "Is empty", |
|||
}, |
|||
NotEmpty: { |
|||
value: "notEmpty", |
|||
label: "Is not empty", |
|||
}, |
|||
StartsWith: { |
|||
value: "string", |
|||
label: "Starts with", |
|||
}, |
|||
Like: { |
|||
value: "fuzzy", |
|||
label: "Like", |
|||
}, |
|||
MoreThan: { |
|||
value: "rangeLow", |
|||
label: "More than", |
|||
}, |
|||
LessThan: { |
|||
value: "rangeHigh", |
|||
label: "Less than", |
|||
}, |
|||
Contains: { |
|||
value: "equal", |
|||
label: "Contains", |
|||
}, |
|||
NotContains: { |
|||
value: "notEqual", |
|||
label: "Does Not Contain", |
|||
}, |
|||
} |
|||
|
|||
export const NoEmptyFilterStrings = [ |
|||
OperatorOptions.StartsWith.value, |
|||
OperatorOptions.Like.value, |
|||
OperatorOptions.Equals.value, |
|||
OperatorOptions.NotEquals.value, |
|||
OperatorOptions.Contains.value, |
|||
OperatorOptions.NotContains.value, |
|||
] |
|||
|
|||
/** |
|||
* Returns the valid operator options for a certain data type |
|||
* @param type the data type |
|||
*/ |
|||
export const getValidOperatorsForType = type => { |
|||
const Op = OperatorOptions |
|||
const stringOps = [ |
|||
Op.Equals, |
|||
Op.NotEquals, |
|||
Op.StartsWith, |
|||
Op.Like, |
|||
Op.Empty, |
|||
Op.NotEmpty, |
|||
] |
|||
const numOps = [ |
|||
Op.Equals, |
|||
Op.NotEquals, |
|||
Op.MoreThan, |
|||
Op.LessThan, |
|||
Op.Empty, |
|||
Op.NotEmpty, |
|||
] |
|||
if (type === "string") { |
|||
return stringOps |
|||
} else if (type === "number") { |
|||
return numOps |
|||
} else if (type === "options") { |
|||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] |
|||
} else if (type === "array") { |
|||
return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty] |
|||
} else if (type === "boolean") { |
|||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty] |
|||
} else if (type === "longform") { |
|||
return stringOps |
|||
} else if (type === "datetime") { |
|||
return numOps |
|||
} else if (type === "formula") { |
|||
return stringOps.concat([Op.MoreThan, Op.LessThan]) |
|||
} |
|||
return [] |
|||
} |
|||
@ -1,210 +0,0 @@ |
|||
// Do not use any aliased imports in common files, as these will be bundled
|
|||
// by multiple bundlers which may not be able to resolve them.
|
|||
// This will eventually be replaced by the new client implementation when we
|
|||
// add a core package.
|
|||
import { writable, derived, get } from "svelte/store" |
|||
import * as API from "../builderStore/api" |
|||
import { buildLuceneQuery } from "./lucene" |
|||
|
|||
const defaultOptions = { |
|||
tableId: null, |
|||
filters: null, |
|||
limit: 10, |
|||
sortColumn: null, |
|||
sortOrder: "ascending", |
|||
paginate: true, |
|||
schema: null, |
|||
} |
|||
|
|||
export const fetchTableData = opts => { |
|||
// Save option set so we can override it later rather than relying on params
|
|||
let options = { |
|||
...defaultOptions, |
|||
...opts, |
|||
} |
|||
|
|||
// Local non-observable state
|
|||
let query |
|||
let sortType |
|||
let lastBookmark |
|||
|
|||
// Local observable state
|
|||
const store = writable({ |
|||
rows: [], |
|||
schema: null, |
|||
loading: false, |
|||
loaded: false, |
|||
bookmarks: [], |
|||
pageNumber: 0, |
|||
}) |
|||
|
|||
// Derive certain properties to return
|
|||
const derivedStore = derived(store, $store => { |
|||
return { |
|||
...$store, |
|||
hasNextPage: $store.bookmarks[$store.pageNumber + 1] != null, |
|||
hasPrevPage: $store.pageNumber > 0, |
|||
} |
|||
}) |
|||
|
|||
const fetchPage = async bookmark => { |
|||
lastBookmark = bookmark |
|||
const { tableId, limit, sortColumn, sortOrder, paginate } = options |
|||
const res = await API.post(`/api/${options.tableId}/search`, { |
|||
tableId, |
|||
query, |
|||
limit, |
|||
sort: sortColumn, |
|||
sortOrder: sortOrder?.toLowerCase() ?? "ascending", |
|||
sortType, |
|||
paginate, |
|||
bookmark, |
|||
}) |
|||
return await res.json() |
|||
} |
|||
|
|||
// Fetches a fresh set of results from the server
|
|||
const fetchData = async () => { |
|||
const { tableId, schema, sortColumn, filters } = options |
|||
|
|||
// Ensure table ID exists
|
|||
if (!tableId) { |
|||
return |
|||
} |
|||
|
|||
// Get and enrich schema.
|
|||
// Ensure there are "name" properties for all fields and that field schema
|
|||
// are objects
|
|||
let enrichedSchema = schema |
|||
if (!enrichedSchema) { |
|||
const definition = await API.get(`/api/tables/${tableId}`) |
|||
enrichedSchema = definition?.schema ?? null |
|||
} |
|||
if (enrichedSchema) { |
|||
Object.entries(schema).forEach(([fieldName, fieldSchema]) => { |
|||
if (typeof fieldSchema === "string") { |
|||
enrichedSchema[fieldName] = { |
|||
type: fieldSchema, |
|||
name: fieldName, |
|||
} |
|||
} else { |
|||
enrichedSchema[fieldName] = { |
|||
...fieldSchema, |
|||
name: fieldName, |
|||
} |
|||
} |
|||
}) |
|||
|
|||
// Save fixed schema so we can provide it later
|
|||
options.schema = enrichedSchema |
|||
} |
|||
|
|||
// Ensure schema exists
|
|||
if (!schema) { |
|||
return |
|||
} |
|||
store.update($store => ({ ...$store, schema, loading: true })) |
|||
|
|||
// Work out what sort type to use
|
|||
if (!sortColumn || !schema[sortColumn]) { |
|||
sortType = "string" |
|||
} |
|||
const type = schema?.[sortColumn]?.type |
|||
sortType = type === "number" ? "number" : "string" |
|||
|
|||
// Build the lucene query
|
|||
query = buildLuceneQuery(filters) |
|||
|
|||
// Actually fetch data
|
|||
const page = await fetchPage() |
|||
store.update($store => ({ |
|||
...$store, |
|||
loading: false, |
|||
loaded: true, |
|||
pageNumber: 0, |
|||
rows: page.rows, |
|||
bookmarks: page.hasNextPage ? [null, page.bookmark] : [null], |
|||
})) |
|||
} |
|||
|
|||
// Fetches the next page of data
|
|||
const nextPage = async () => { |
|||
const state = get(derivedStore) |
|||
if (state.loading || !options.paginate || !state.hasNextPage) { |
|||
return |
|||
} |
|||
|
|||
// Fetch next page
|
|||
store.update($store => ({ ...$store, loading: true })) |
|||
const page = await fetchPage(state.bookmarks[state.pageNumber + 1]) |
|||
|
|||
// Update state
|
|||
store.update($store => { |
|||
let { bookmarks, pageNumber } = $store |
|||
if (page.hasNextPage) { |
|||
bookmarks[pageNumber + 2] = page.bookmark |
|||
} |
|||
return { |
|||
...$store, |
|||
pageNumber: pageNumber + 1, |
|||
rows: page.rows, |
|||
bookmarks, |
|||
loading: false, |
|||
} |
|||
}) |
|||
} |
|||
|
|||
// Fetches the previous page of data
|
|||
const prevPage = async () => { |
|||
const state = get(derivedStore) |
|||
if (state.loading || !options.paginate || !state.hasPrevPage) { |
|||
return |
|||
} |
|||
|
|||
// Fetch previous page
|
|||
store.update($store => ({ ...$store, loading: true })) |
|||
const page = await fetchPage(state.bookmarks[state.pageNumber - 1]) |
|||
|
|||
// Update state
|
|||
store.update($store => { |
|||
return { |
|||
...$store, |
|||
pageNumber: $store.pageNumber - 1, |
|||
rows: page.rows, |
|||
loading: false, |
|||
} |
|||
}) |
|||
} |
|||
|
|||
// Resets the data set and updates options
|
|||
const update = async newOptions => { |
|||
if (newOptions) { |
|||
options = { |
|||
...options, |
|||
...newOptions, |
|||
} |
|||
} |
|||
await fetchData() |
|||
} |
|||
|
|||
// Loads the same page again
|
|||
const refresh = async () => { |
|||
if (get(store).loading) { |
|||
return |
|||
} |
|||
const page = await fetchPage(lastBookmark) |
|||
store.update($store => ({ ...$store, rows: page.rows })) |
|||
} |
|||
|
|||
// Initially fetch data but don't bother waiting for the result
|
|||
fetchData() |
|||
|
|||
// Return our derived store which will be updated over time
|
|||
return { |
|||
subscribe: derivedStore.subscribe, |
|||
nextPage, |
|||
prevPage, |
|||
update, |
|||
refresh, |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue