- This component doesn't have any additional settings.
+
This component doesn't have any additional settings.
+ {/if}
+ {#if componentDefinition?.info}
+
+ {@html componentDefinition?.info}
{/if}
@@ -185,7 +188,7 @@
height: 100%;
gap: var(--spacing-s);
}
- .empty {
+ .text {
font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spacing-m);
color: var(--grey-6);
diff --git a/packages/builder/src/global.css b/packages/builder/src/global.css
index 726f422a48..ed80f10b1c 100644
--- a/packages/builder/src/global.css
+++ b/packages/builder/src/global.css
@@ -6,10 +6,6 @@ html, body {
min-height: 100%;
}
-.spectrum--light {
- --spectrum-alias-background-color-primary: var(--spectrum-global-color-gray-75);
-}
-
body {
--background: var(--spectrum-alias-background-color-primary);
--background-alt: var(--spectrum-alias-background-color-secondary);
diff --git a/packages/client/src/api/tables.js b/packages/client/src/api/tables.js
index 248e1516c2..59381e35bf 100644
--- a/packages/client/src/api/tables.js
+++ b/packages/client/src/api/tables.js
@@ -18,19 +18,37 @@ export const fetchTableData = async tableId => {
}
/**
- * Perform a mango query against an internal table
- * @param {String} tableId - id of the table to search
- * @param {Object} search - Mango Compliant search object
- * @param {Object} pagination - the pagination controls
+ * Searches a table using Lucene.
*/
-export const searchTableData = async ({ tableId, search, pagination }) => {
- const output = await API.post({
- url: `/api/${tableId}/rows/search`,
+export const searchTable = async ({
+ tableId,
+ query,
+ bookmark,
+ limit,
+ sort,
+ sortOrder,
+ sortType,
+ paginate,
+}) => {
+ if (!tableId || !query) {
+ return {
+ rows: [],
+ }
+ }
+ const res = await API.post({
+ url: `/api/search/${tableId}/rows`,
body: {
- query: search,
- pagination,
+ query,
+ bookmark,
+ limit,
+ sort,
+ sortOrder,
+ sortType,
+ paginate,
},
})
- output.rows = await enrichRows(output.rows, tableId)
- return output
+ return {
+ ...res,
+ rows: await enrichRows(res?.rows, tableId),
+ }
}
diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte
index 8fb1b83d4c..5d07754f49 100644
--- a/packages/client/src/components/ClientApp.svelte
+++ b/packages/client/src/components/ClientApp.svelte
@@ -39,8 +39,18 @@
{#if loaded && $screenStore.activeLayout}
-
-
-
-
+
{/if}
+
+
diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js
index 6d0cc08548..1d73abea47 100644
--- a/packages/server/src/api/controllers/row.js
+++ b/packages/server/src/api/controllers/row.js
@@ -16,7 +16,6 @@ const {
const { FieldTypes } = require("../../constants")
const { isEqual } = require("lodash")
const { cloneDeep } = require("lodash/fp")
-const { QueryBuilder, search } = require("./search/utils")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
@@ -248,45 +247,6 @@ exports.fetchView = async function (ctx) {
}
}
-exports.search = async function (ctx) {
- const appId = ctx.appId
- const db = new CouchDB(appId)
- const {
- query,
- pagination: { pageSize = 10, bookmark },
- } = ctx.request.body
- const tableId = ctx.params.tableId
-
- const queryBuilder = new QueryBuilder(appId)
- .setLimit(pageSize)
- .addTable(tableId)
- if (bookmark) {
- queryBuilder.setBookmark(bookmark)
- }
-
- let searchString
- if (ctx.query && ctx.query.raw && ctx.query.raw !== "") {
- searchString = queryBuilder.complete(query["RAW"])
- } else {
- // make all strings a starts with operation rather than pure equality
- for (const [key, queryVal] of Object.entries(query)) {
- if (typeof queryVal === "string") {
- queryBuilder.addString(key, queryVal)
- } else {
- queryBuilder.addEqual(key, queryVal)
- }
- }
- searchString = queryBuilder.complete()
- }
-
- const response = await search(searchString)
- const table = await db.get(tableId)
- ctx.body = {
- rows: await outputProcessing(appId, table, response.rows),
- bookmark: response.bookmark,
- }
-}
-
exports.fetchTableRows = async function (ctx) {
const appId = ctx.appId
const db = new CouchDB(appId)
diff --git a/packages/server/src/api/controllers/search/index.js b/packages/server/src/api/controllers/search/index.js
index 234c7eb258..ede0556e18 100644
--- a/packages/server/src/api/controllers/search/index.js
+++ b/packages/server/src/api/controllers/search/index.js
@@ -1,18 +1,26 @@
-const { QueryBuilder, buildSearchUrl, search } = require("./utils")
+const { fullSearch, paginatedSearch } = require("./utils")
+const CouchDB = require("../../../db")
+const { outputProcessing } = require("../../../utilities/rowProcessor")
exports.rowSearch = async ctx => {
const appId = ctx.appId
const { tableId } = ctx.params
- const { bookmark, query, raw } = ctx.request.body
- let url
- if (query) {
- url = new QueryBuilder(appId, query, bookmark).addTable(tableId).complete()
- } else if (raw) {
- url = buildSearchUrl({
- appId,
- query: raw,
- bookmark,
- })
+ const db = new CouchDB(appId)
+ const { paginate, query, ...params } = ctx.request.body
+ params.tableId = tableId
+
+ let response
+ if (paginate) {
+ response = await paginatedSearch(appId, query, params)
+ } else {
+ response = await fullSearch(appId, query, params)
}
- ctx.body = await search(url)
+
+ // Enrich search results with relationships
+ if (response.rows && response.rows.length) {
+ const table = await db.get(tableId)
+ response.rows = await outputProcessing(appId, table, response.rows)
+ }
+
+ ctx.body = response
}
diff --git a/packages/server/src/api/controllers/search/utils.js b/packages/server/src/api/controllers/search/utils.js
index d3ffb26be7..9fc10dabf9 100644
--- a/packages/server/src/api/controllers/search/utils.js
+++ b/packages/server/src/api/controllers/search/utils.js
@@ -4,28 +4,19 @@ const env = require("../../../environment")
const fetch = require("node-fetch")
/**
- * Given a set of inputs this will generate the URL which is to be sent to the search proxy in CouchDB.
- * @param {string} appId The ID of the app which we will be searching within.
- * @param {string} query The lucene query string which is to be used for searching.
- * @param {string|null} bookmark If there were more than the limit specified can send the bookmark that was
- * returned with query for next set of search results.
- * @param {number} limit The number of entries to return per query.
- * @param {boolean} excludeDocs By default full rows are returned, if required this can be disabled.
- * @return {string} The URL which a GET can be performed on to receive results.
+ * Escapes any characters in a string which lucene searches require to be
+ * escaped.
+ * @param value The value to escape
+ * @returns {string}
*/
-function buildSearchUrl({ appId, query, bookmark, excludeDocs, limit = 50 }) {
- let url = `${env.COUCH_DB_URL}/${appId}/_design/database/_search`
- url += `/${SearchIndexes.ROWS}?q=${query}`
- url += `&limit=${limit}`
- if (!excludeDocs) {
- url += "&include_docs=true"
- }
- if (bookmark) {
- url += `&bookmark=${bookmark}`
- }
- return checkSlashesInUrl(url)
+const luceneEscape = value => {
+ return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&")
}
+/**
+ * Class to build lucene query URLs.
+ * Optionally takes a base lucene query object.
+ */
class QueryBuilder {
constructor(appId, base) {
this.appId = appId
@@ -34,10 +25,20 @@ class QueryBuilder {
fuzzy: {},
range: {},
equal: {},
+ notEqual: {},
+ empty: {},
+ notEmpty: {},
...base,
}
this.limit = 50
- this.bookmark = null
+ this.sortOrder = "ascending"
+ this.sortType = "string"
+ this.includeDocs = true
+ }
+
+ setTable(tableId) {
+ this.query.equal.tableId = tableId
+ return this
}
setLimit(limit) {
@@ -45,11 +46,31 @@ class QueryBuilder {
return this
}
+ setSort(sort) {
+ this.sort = sort
+ return this
+ }
+
+ setSortOrder(sortOrder) {
+ this.sortOrder = sortOrder
+ return this
+ }
+
+ setSortType(sortType) {
+ this.sortType = sortType
+ return this
+ }
+
setBookmark(bookmark) {
this.bookmark = bookmark
return this
}
+ excludeDocs() {
+ this.includeDocs = false
+ return this
+ }
+
addString(key, partial) {
this.query.string[key] = partial
return this
@@ -73,52 +94,113 @@ class QueryBuilder {
return this
}
- addTable(tableId) {
- this.query.equal.tableId = tableId
+ addNotEqual(key, value) {
+ this.query.notEqual[key] = value
return this
}
- complete(rawQuery = null) {
- let output = ""
+ addEmpty(key, value) {
+ this.query.empty[key] = value
+ return this
+ }
+
+ addNotEmpty(key, value) {
+ this.query.notEmpty[key] = value
+ return this
+ }
+
+ buildSearchQuery() {
+ let query = "*:*"
+
function build(structure, queryFn) {
for (let [key, value] of Object.entries(structure)) {
- if (output.length !== 0) {
- output += " AND "
+ const expression = queryFn(luceneEscape(key.replace(/ /, "_")), value)
+ if (expression == null) {
+ continue
}
- output += queryFn(key, value)
+ query += ` AND ${expression}`
}
}
+ // Construct the actual lucene search query string from JSON structure
if (this.query.string) {
- build(this.query.string, (key, value) => `${key}:${value}*`)
+ build(this.query.string, (key, value) => {
+ return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null
+ })
}
if (this.query.range) {
- build(
- this.query.range,
- (key, value) => `${key}:[${value.low} TO ${value.high}]`
- )
+ build(this.query.range, (key, value) => {
+ if (!value) {
+ return null
+ }
+ if (value.low == null || value.low === "") {
+ return null
+ }
+ if (value.high == null || value.high === "") {
+ return null
+ }
+ return `${key}:[${value.low} TO ${value.high}]`
+ })
}
if (this.query.fuzzy) {
- build(this.query.fuzzy, (key, value) => `${key}:${value}~`)
+ build(this.query.fuzzy, (key, value) => {
+ return value ? `${key}:${luceneEscape(value.toLowerCase())}~` : null
+ })
}
if (this.query.equal) {
- build(this.query.equal, (key, value) => `${key}:${value}`)
+ build(this.query.equal, (key, value) => {
+ return value ? `${key}:${luceneEscape(value.toLowerCase())}` : null
+ })
}
- if (rawQuery) {
- output = output.length === 0 ? rawQuery : `&${rawQuery}`
+ if (this.query.notEqual) {
+ build(this.query.notEqual, (key, value) => {
+ return value ? `!${key}:${luceneEscape(value.toLowerCase())}` : null
+ })
}
- return buildSearchUrl({
- appId: this.appId,
- query: output,
- bookmark: this.bookmark,
- limit: this.limit,
- })
+ if (this.query.empty) {
+ build(this.query.empty, key => `!${key}:["" TO *]`)
+ }
+ if (this.query.notEmpty) {
+ build(this.query.notEmpty, key => `${key}:["" TO *]`)
+ }
+
+ return query
+ }
+
+ buildSearchBody() {
+ let body = {
+ q: this.buildSearchQuery(),
+ limit: Math.min(this.limit, 200),
+ include_docs: this.includeDocs,
+ }
+ if (this.bookmark) {
+ body.bookmark = this.bookmark
+ }
+ if (this.sort) {
+ const order = this.sortOrder === "descending" ? "-" : ""
+ const type = `<${this.sortType}>`
+ body.sort = `${order}${this.sort.replace(/ /, "_")}${type}`
+ }
+ return body
+ }
+
+ async run() {
+ const url = `${env.COUCH_DB_URL}/${this.appId}/_design/database/_search/${SearchIndexes.ROWS}`
+ const body = this.buildSearchBody()
+ return await runQuery(url, body)
}
}
-exports.search = async query => {
- const response = await fetch(query, {
- method: "GET",
+/**
+ * Executes a lucene search query.
+ * @param url The query URL
+ * @param body The request body defining search criteria
+ * @returns {Promise<{rows: []}>}
+ */
+const runQuery = async (url, body) => {
+ const response = await fetch(url, {
+ body: JSON.stringify(body),
+ method: "POST",
})
const json = await response.json()
let output = {
@@ -133,5 +215,122 @@ exports.search = async query => {
return output
}
-exports.QueryBuilder = QueryBuilder
-exports.buildSearchUrl = buildSearchUrl
+/**
+ * Gets round the fixed limit of 200 results from a query by fetching as many
+ * pages as required and concatenating the results. This recursively operates
+ * until enough results have been found.
+ * @param appId {string} The app ID to search
+ * @param query {object} The JSON query structure
+ * @param params {object} The search params including:
+ * tableId {string} The table ID to search
+ * sort {string} The sort column
+ * sortOrder {string} The sort order ("ascending" or "descending")
+ * sortType {string} Whether to treat sortable values as strings or
+ * numbers. ("string" or "number")
+ * limit {number} The number of results to fetch
+ * bookmark {string|null} Current bookmark in the recursive search
+ * rows {array|null} Current results in the recursive search
+ * @returns {Promise<*[]|*>}
+ */
+const recursiveSearch = async (appId, query, params) => {
+ const bookmark = params.bookmark
+ const rows = params.rows || []
+ if (rows.length >= params.limit) {
+ return rows
+ }
+ let pageSize = 200
+ if (rows.length > params.limit - 200) {
+ pageSize = params.limit - rows.length
+ }
+ const page = await new QueryBuilder(appId, query)
+ .setTable(params.tableId)
+ .setBookmark(bookmark)
+ .setLimit(pageSize)
+ .setSort(params.sort)
+ .setSortOrder(params.sortOrder)
+ .setSortType(params.sortType)
+ .run()
+ if (!page.rows.length) {
+ return rows
+ }
+ if (page.rows.length < 200) {
+ return [...rows, ...page.rows]
+ }
+ const newParams = {
+ ...params,
+ bookmark: page.bookmark,
+ rows: [...rows, ...page.rows],
+ }
+ return await recursiveSearch(appId, query, newParams)
+}
+
+/**
+ * Performs a paginated search. A bookmark will be returned to allow the next
+ * page to be fetched. There is a max limit off 200 results per page in a
+ * paginated search.
+ * @param appId {string} The app ID to search
+ * @param query {object} The JSON query structure
+ * @param params {object} The search params including:
+ * tableId {string} The table ID to search
+ * sort {string} The sort column
+ * sortOrder {string} The sort order ("ascending" or "descending")
+ * sortType {string} Whether to treat sortable values as strings or
+ * numbers. ("string" or "number")
+ * limit {number} The desired page size
+ * bookmark {string} The bookmark to resume from
+ * @returns {Promise<{hasNextPage: boolean, rows: *[]}>}
+ */
+exports.paginatedSearch = async (appId, query, params) => {
+ let limit = params.limit
+ if (limit == null || isNaN(limit) || limit < 0) {
+ limit = 50
+ }
+ limit = Math.min(limit, 200)
+ const search = new QueryBuilder(appId, query)
+ .setTable(params.tableId)
+ .setSort(params.sort)
+ .setSortOrder(params.sortOrder)
+ .setSortType(params.sortType)
+ const searchResults = await search
+ .setBookmark(params.bookmark)
+ .setLimit(limit)
+ .run()
+
+ // Try fetching 1 row in the next page to see if another page of results
+ // exists or not
+ const nextResults = await search
+ .setBookmark(searchResults.bookmark)
+ .setLimit(1)
+ .run()
+
+ return {
+ ...searchResults,
+ hasNextPage: nextResults.rows && nextResults.rows.length > 0,
+ }
+}
+
+/**
+ * Performs a full search, fetching multiple pages if required to return the
+ * desired amount of results. There is a limit of 1000 results to avoid
+ * heavy performance hits, and to avoid client components breaking from
+ * handling too much data.
+ * @param appId {string} The app ID to search
+ * @param query {object} The JSON query structure
+ * @param params {object} The search params including:
+ * tableId {string} The table ID to search
+ * sort {string} The sort column
+ * sortOrder {string} The sort order ("ascending" or "descending")
+ * sortType {string} Whether to treat sortable values as strings or
+ * numbers. ("string" or "number")
+ * limit {number} The desired number of results
+ * @returns {Promise<{rows: *}>}
+ */
+exports.fullSearch = async (appId, query, params) => {
+ let limit = params.limit
+ if (limit == null || isNaN(limit) || limit < 0) {
+ limit = 1000
+ }
+ params.limit = Math.min(limit, 1000)
+ const rows = await recursiveSearch(appId, query, params)
+ return { rows }
+}
diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js
index 0b09a78bb8..5ea3ddacef 100644
--- a/packages/server/src/api/routes/index.js
+++ b/packages/server/src/api/routes/index.js
@@ -23,6 +23,7 @@ const queryRoutes = require("./query")
const hostingRoutes = require("./hosting")
const backupRoutes = require("./backup")
const devRoutes = require("./dev")
+const searchRoutes = require("./search")
exports.mainRoutes = [
authRoutes,
@@ -51,6 +52,7 @@ exports.mainRoutes = [
// this could be breaking as koa may recognise other routes as this
tableRoutes,
rowRoutes,
+ searchRoutes,
]
exports.staticRoutes = staticRoutes
diff --git a/packages/server/src/api/routes/row.js b/packages/server/src/api/routes/row.js
index e0e3c5ab81..ca1e170754 100644
--- a/packages/server/src/api/routes/row.js
+++ b/packages/server/src/api/routes/row.js
@@ -39,12 +39,6 @@ router
usage,
rowController.save
)
- .post(
- "/api/:tableId/rows/search",
- paramResource("tableId"),
- authorized(PermissionTypes.TABLE, PermissionLevels.READ),
- rowController.search
- )
.patch(
"/api/:tableId/rows/:rowId",
paramSubResource("tableId", "rowId"),
diff --git a/packages/server/src/db/views/staticViews.js b/packages/server/src/db/views/staticViews.js
index 5f5bc7db14..23f320d7eb 100644
--- a/packages/server/src/db/views/staticViews.js
+++ b/packages/server/src/db/views/staticViews.js
@@ -84,6 +84,7 @@ async function searchIndex(appId, indexName, fnString) {
designDoc.indexes = {
[indexName]: {
index: fnString,
+ analyzer: "keyword",
},
}
await db.put(designDoc)
@@ -96,11 +97,15 @@ exports.createAllSearchIndex = async appId => {
function (doc) {
function idx(input, prev) {
for (let key of Object.keys(input)) {
- const idxKey = prev != null ? `${prev}.${key}` : key
- if (key === "_id" || key === "_rev") {
+ let idxKey = prev != null ? `${prev}.${key}` : key
+ idxKey = idxKey.replace(/ /, "_")
+ if (key === "_id" || key === "_rev" || input[key] == null) {
continue
}
- if (typeof input[key] !== "object") {
+ if (typeof input[key] === "string") {
+ // eslint-disable-next-line no-undef
+ index(idxKey, input[key].toLowerCase(), { store: true })
+ } else if (typeof input[key] !== "object") {
// eslint-disable-next-line no-undef
index(idxKey, input[key], { store: true })
} else {
diff --git a/packages/server/src/utilities/rowProcessor.js b/packages/server/src/utilities/rowProcessor.js
index fd79751c3e..2267c9e986 100644
--- a/packages/server/src/utilities/rowProcessor.js
+++ b/packages/server/src/utilities/rowProcessor.js
@@ -123,24 +123,6 @@ function processAutoColumn(user, table, row) {
return { table, row }
}
-/**
- * Given a set of rows and the table they came from this function will sort by auto ID or a custom
- * method if provided (not implemented yet).
- */
-function sortRows(table, rows) {
- // sort based on auto ID (if found)
- let autoIDColumn = Object.entries(table.schema).find(
- schema => schema[1].subtype === AutoFieldSubTypes.AUTO_ID
- )
- // get the column name, this is the first element in the array (Object.entries)
- autoIDColumn = autoIDColumn && autoIDColumn.length ? autoIDColumn[0] : null
- if (autoIDColumn) {
- // sort in ascending order
- rows.sort((a, b) => a[autoIDColumn] - b[autoIDColumn])
- }
- return rows
-}
-
/**
* Looks through the rows provided and finds formulas - which it then processes.
*/
@@ -213,8 +195,6 @@ exports.outputProcessing = async (appId, table, rows) => {
rows = [rows]
wasArray = false
}
- // sort by auto ID
- rows = sortRows(table, rows)
// attach any linked row information
let enriched = await linkRows.attachFullLinkedDocs(appId, table, rows)
diff --git a/packages/standard-components/manifest.json b/packages/standard-components/manifest.json
index 163caa3dbc..a3fbc5aa52 100644
--- a/packages/standard-components/manifest.json
+++ b/packages/standard-components/manifest.json
@@ -65,41 +65,6 @@
"type": "schema"
}
},
- "search": {
- "name": "Search",
- "description": "A searchable list of items.",
- "icon": "Search",
- "styleable": true,
- "hasChildren": true,
- "settings": [
- {
- "type": "table",
- "label": "Table",
- "key": "table"
- },
- {
- "type": "multifield",
- "label": "Columns",
- "key": "columns",
- "dependsOn": "table"
- },
- {
- "type": "number",
- "label": "Rows/Page",
- "defaultValue": 25,
- "key": "pageSize"
- },
- {
- "type": "text",
- "label": "Empty Text",
- "key": "noRowsMessage",
- "defaultValue": "No rows found."
- }
- ],
- "context": {
- "type": "schema"
- }
- },
"stackedlist": {
"name": "Stacked List",
"icon": "TaskList",
@@ -1416,6 +1381,7 @@
},
"dataprovider": {
"name": "Data Provider",
+ "info": "Pagination is only available for data stored in internal tables.",
"icon": "Data",
"styleable": false,
"hasChildren": true,
@@ -1445,7 +1411,14 @@
{
"type": "number",
"label": "Limit",
- "key": "limit"
+ "key": "limit",
+ "defaultValue": 50
+ },
+ {
+ "type": "boolean",
+ "label": "Paginate",
+ "key": "paginate",
+ "defaultValue": true
}
],
"context": {
@@ -1464,12 +1437,8 @@
"key": "schema"
},
{
- "label": "Loading",
- "key": "loading"
- },
- {
- "label": "Loaded",
- "key": "loaded"
+ "label": "Page Number",
+ "key": "pageNumber"
}
]
}
diff --git a/packages/standard-components/src/DataProvider.svelte b/packages/standard-components/src/DataProvider.svelte
index a8da4925d3..e0b2ad859a 100644
--- a/packages/standard-components/src/DataProvider.svelte
+++ b/packages/standard-components/src/DataProvider.svelte
@@ -1,11 +1,13 @@
-
+
-
+ {#if !loaded}
+
+ {:else}
+
+ {#if paginate && internalTable}
+
+ {/if}
+ {/if}
+
+
diff --git a/packages/standard-components/src/Search.svelte b/packages/standard-components/src/Search.svelte
deleted file mode 100644
index 29ca3b011b..0000000000
--- a/packages/standard-components/src/Search.svelte
+++ /dev/null
@@ -1,195 +0,0 @@
-
-
-
-
-
- {#if loaded}
- {#if rows.length > 0}
- {#if $component.children === 0 && $builderStore.inBuilder}
-
Add some components to display.
- {:else}
- {#each rows as row}
-
-
-
- {/each}
- {/if}
- {:else if noRowsMessage}
-
{noRowsMessage}
- {/if}
- {/if}
-
-
-
-
-
diff --git a/packages/standard-components/src/charts/ApexChart.svelte b/packages/standard-components/src/charts/ApexChart.svelte
index cf9cade436..a7e25514e0 100644
--- a/packages/standard-components/src/charts/ApexChart.svelte
+++ b/packages/standard-components/src/charts/ApexChart.svelte
@@ -10,9 +10,9 @@
{#if options}
-{:else if builderStore.inBuilder}
-
- Use the settings panel to build your chart -->
+{:else if $builderStore.inBuilder}
+
+ Use the settings panel to build your chart.
{/if}
@@ -21,4 +21,10 @@
display: flex !important;
text-transform: capitalize;
}
+ div :global(.apexcharts-yaxis-label, .apexcharts-xaxis-label) {
+ fill: #aaa;
+ }
+ div.placeholder {
+ padding: 10px;
+ }
diff --git a/packages/standard-components/src/forms/Form.svelte b/packages/standard-components/src/forms/Form.svelte
index ccc61fab9e..afa4aeeeb4 100644
--- a/packages/standard-components/src/forms/Form.svelte
+++ b/packages/standard-components/src/forms/Form.svelte
@@ -187,5 +187,6 @@
div {
padding: 20px;
position: relative;
+ background-color: var(--spectrum-alias-background-color-secondary);
}
diff --git a/packages/standard-components/src/index.js b/packages/standard-components/src/index.js
index 2ad685033a..7b4d492fa9 100644
--- a/packages/standard-components/src/index.js
+++ b/packages/standard-components/src/index.js
@@ -27,7 +27,6 @@ export { default as embed } from "./Embed.svelte"
export { default as cardhorizontal } from "./CardHorizontal.svelte"
export { default as cardstat } from "./CardStat.svelte"
export { default as icon } from "./Icon.svelte"
-export { default as search } from "./Search.svelte"
export { default as backgroundimage } from "./BackgroundImage.svelte"
export * from "./charts"
export * from "./forms"
diff --git a/packages/standard-components/src/table/Table.svelte b/packages/standard-components/src/table/Table.svelte
index f68835d75b..35839e4722 100644
--- a/packages/standard-components/src/table/Table.svelte
+++ b/packages/standard-components/src/table/Table.svelte
@@ -94,3 +94,9 @@
+
+
diff --git a/packages/string-templates/src/index.cjs b/packages/string-templates/src/index.cjs
index e662f253c6..b62521942a 100644
--- a/packages/string-templates/src/index.cjs
+++ b/packages/string-templates/src/index.cjs
@@ -4,7 +4,7 @@ const processors = require("./processors")
const { cloneDeep } = require("lodash/fp")
const {
removeNull,
- addConstants,
+ updateContext,
removeHandlebarsStatements,
} = require("./utilities")
const manifest = require("../manifest.json")
@@ -92,8 +92,7 @@ module.exports.processStringSync = (string, context) => {
}
// take a copy of input incase error
const input = string
- let clonedContext = removeNull(cloneDeep(context))
- clonedContext = addConstants(clonedContext)
+ const clonedContext = removeNull(updateContext(cloneDeep(context)))
// remove any null/undefined properties
if (typeof string !== "string") {
throw "Cannot process non-string types."
diff --git a/packages/string-templates/src/utilities.js b/packages/string-templates/src/utilities.js
index 38496b04b4..e94b7f8ee7 100644
--- a/packages/string-templates/src/utilities.js
+++ b/packages/string-templates/src/utilities.js
@@ -23,11 +23,24 @@ module.exports.removeNull = obj => {
return obj
}
-module.exports.addConstants = obj => {
+module.exports.updateContext = obj => {
if (obj.now == null) {
- obj.now = new Date()
+ obj.now = new Date().toISOString()
}
- return obj
+ function recurse(obj) {
+ for (let key of Object.keys(obj)) {
+ if (!obj[key]) {
+ continue
+ }
+ if (obj[key] instanceof Date) {
+ obj[key] = obj[key].toISOString()
+ } else if (typeof obj[key] === "object") {
+ obj[key] = recurse(obj[key])
+ }
+ }
+ return obj
+ }
+ return recurse(obj)
}
module.exports.removeHandlebarsStatements = string => {
diff --git a/packages/string-templates/test/basic.spec.js b/packages/string-templates/test/basic.spec.js
index 5732181b13..f5c7c8be75 100644
--- a/packages/string-templates/test/basic.spec.js
+++ b/packages/string-templates/test/basic.spec.js
@@ -107,6 +107,12 @@ describe("check the utility functions", () => {
const property = makePropSafe("thing")
expect(property).toEqual("[thing]")
})
+
+ it("should be able to handle an input date object", async () => {
+ const date = new Date()
+ const output = await processString("{{ dateObj }}", { dateObj: date })
+ expect(date.toISOString()).toEqual(output)
+ })
})
describe("check manifest", () => {