diff --git a/packages/client/src/api/rows.js b/packages/client/src/api/rows.js index 6930105d78..e6dc8879fa 100644 --- a/packages/client/src/api/rows.js +++ b/packages/client/src/api/rows.js @@ -1,4 +1,4 @@ -import { notificationStore } from "../store/notification" +import { notificationStore, datasourceStore } from "../store" import API from "./api" import { fetchTableDefinition } from "./tables" @@ -6,6 +6,9 @@ import { fetchTableDefinition } from "./tables" * Fetches data about a certain row in a table. */ export const fetchRow = async ({ tableId, rowId }) => { + if (!tableId || !rowId) { + return + } const row = await API.get({ url: `/api/${tableId}/rows/${rowId}`, }) @@ -16,6 +19,9 @@ export const fetchRow = async ({ tableId, rowId }) => { * Creates a row in a table. */ export const saveRow = async row => { + if (!row?.tableId) { + return + } const res = await API.post({ url: `/api/${row.tableId}/rows`, body: row, @@ -23,6 +29,10 @@ export const saveRow = async row => { res.error ? notificationStore.danger("An error has occurred") : notificationStore.success("Row saved") + + // Refresh related datasources + datasourceStore.actions.invalidateDatasource(row.tableId) + return res } @@ -30,6 +40,9 @@ export const saveRow = async row => { * Updates a row in a table. */ export const updateRow = async row => { + if (!row?.tableId || !row?._id) { + return + } const res = await API.patch({ url: `/api/${row.tableId}/rows/${row._id}`, body: row, @@ -37,6 +50,10 @@ export const updateRow = async row => { res.error ? notificationStore.danger("An error has occurred") : notificationStore.success("Row updated") + + // Refresh related datasources + datasourceStore.actions.invalidateDatasource(row.tableId) + return res } @@ -44,12 +61,19 @@ export const updateRow = async row => { * Deletes a row from a table. */ export const deleteRow = async ({ tableId, rowId, revId }) => { + if (!tableId || !rowId || !revId) { + return + } const res = await API.del({ url: `/api/${tableId}/rows/${rowId}/${revId}`, }) res.error ? notificationStore.danger("An error has occurred") : notificationStore.success("Row deleted") + + // Refresh related datasources + datasourceStore.actions.invalidateDatasource(tableId) + return res } @@ -57,6 +81,9 @@ export const deleteRow = async ({ tableId, rowId, revId }) => { * Deletes many rows from a table. */ export const deleteRows = async ({ tableId, rows }) => { + if (!tableId || !rows) { + return + } const res = await API.post({ url: `/api/${tableId}/rows`, body: { @@ -67,6 +94,10 @@ export const deleteRows = async ({ tableId, rows }) => { res.error ? notificationStore.danger("An error has occurred") : notificationStore.success(`${rows.length} row(s) deleted`) + + // Refresh related datasources + datasourceStore.actions.invalidateDatasource(tableId) + return res } diff --git a/packages/client/src/components/Provider.svelte b/packages/client/src/components/Provider.svelte index 8c50eeeb01..bc95412bab 100644 --- a/packages/client/src/components/Provider.svelte +++ b/packages/client/src/components/Provider.svelte @@ -1,6 +1,8 @@ diff --git a/packages/client/src/store/context.js b/packages/client/src/store/context.js index 5092161037..80777cbeff 100644 --- a/packages/client/src/store/context.js +++ b/packages/client/src/store/context.js @@ -4,28 +4,28 @@ export const createContextStore = existingContext => { const store = writable({ ...existingContext }) // Adds a data context layer to the tree - const provideData = (key, data) => { - if (!key) { + const provideData = (providerId, data) => { + if (!providerId) { return } store.update(state => { - state[key] = data + state[providerId] = data // Keep track of the closest component ID so we can later hydrate a "data" prop. // This is only required for legacy bindings that used "data" rather than a // component ID. - state.closestComponentId = key + state.closestComponentId = providerId return state }) } // Adds an action context layer to the tree - const provideAction = (key, actionType, callback) => { - if (!key || !actionType) { + const provideAction = (providerId, actionType, callback) => { + if (!providerId || !actionType) { return } store.update(state => { - state[`${key}_${actionType}`] = callback + state[`${providerId}_${actionType}`] = callback return state }) } diff --git a/packages/client/src/store/datasource.js b/packages/client/src/store/datasource.js new file mode 100644 index 0000000000..58fa632c49 --- /dev/null +++ b/packages/client/src/store/datasource.js @@ -0,0 +1,80 @@ +import { writable, get } from "svelte/store" + +export const createDatasourceStore = () => { + const store = writable([]) + + // Registers a new datasource instance + const registerDatasource = (datasource, instanceId, refresh) => { + if (!datasource || !instanceId || !refresh) { + return + } + + // Create a list of all relevant datasource IDs which would require that + // this datasource is refreshed + let datasourceIds = [] + + // Extract table ID + if (datasource.type === "table") { + if (datasource.tableId) { + datasourceIds.push(datasource.tableId) + } + } + + // Extract both table IDs from both sides of the relationship + else if (datasource.type === "link") { + if (datasource.rowTableId) { + datasourceIds.push(datasource.rowTableId) + } + if (datasource.tableId) { + datasourceIds.push(datasource.tableId) + } + } + + // Extract the datasource ID (not the query ID) for queries + else if (datasource.type === "query") { + if (datasource.datasourceId) { + datasourceIds.push(datasource.datasourceId) + } + } + + // Store configs for each relevant datasource ID + if (datasourceIds.length) { + store.update(state => { + datasourceIds.forEach(id => { + state.push({ + datasourceId: id, + instanceId, + refresh, + }) + }) + return state + }) + } + } + + // Removes all registered datasource instances belonging to a particular + // instance ID + const unregisterInstance = instanceId => { + store.update(state => { + return state.filter(instance => instance.instanceId !== instanceId) + }) + } + + // Invalidates a specific datasource ID by refreshing all instances + // which depend on data from that datasource + const invalidateDatasource = datasourceId => { + const relatedInstances = get(store).filter(instance => { + return instance.datasourceId === datasourceId + }) + relatedInstances?.forEach(instance => { + instance.refresh() + }) + } + + return { + subscribe: store.subscribe, + actions: { registerDatasource, unregisterInstance, invalidateDatasource }, + } +} + +export const datasourceStore = createDatasourceStore() diff --git a/packages/client/src/store/index.js b/packages/client/src/store/index.js index 575c5d98f2..9d08423374 100644 --- a/packages/client/src/store/index.js +++ b/packages/client/src/store/index.js @@ -3,6 +3,7 @@ export { notificationStore } from "./notification" export { routeStore } from "./routes" export { screenStore } from "./screens" export { builderStore } from "./builder" +export { datasourceStore } from "./datasource" // Context stores are layered and duplicated, so it is not a singleton export { createContextStore } from "./context" diff --git a/packages/standard-components/src/List.svelte b/packages/standard-components/src/List.svelte index 4ba10cc28c..b117bbdb70 100644 --- a/packages/standard-components/src/List.svelte +++ b/packages/standard-components/src/List.svelte @@ -2,17 +2,23 @@ import { getContext } from "svelte" import { isEmpty } from "lodash/fp" + export let datasource = [] + const { API, styleable, Provider, builderStore, ActionTypes } = getContext( "sdk" ) const component = getContext("component") - - export let datasource = [] - let rows = [] let loaded = false $: fetchData(datasource) + $: actions = [ + { + type: ActionTypes.RefreshDatasource, + callback: () => fetchData(datasource), + metadata: { datasource }, + }, + ] async function fetchData(datasource) { if (!isEmpty(datasource)) { @@ -20,13 +26,6 @@ } loaded = true } - - $: actions = [ - { - type: ActionTypes.RefreshDatasource, - callback: () => fetchData(datasource), - }, - ]