Skip to content

Commit 4f8c519

Browse files
authored
fix: preserve gsheet column order during sync (#574)
1 parent 323a0e5 commit 4f8c519

File tree

4 files changed

+49
-17
lines changed

4 files changed

+49
-17
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@blinkk/root-cms': patch
3+
---
4+
5+
fix: preserve column headers in data sources (#574)

packages/root-cms/core/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export interface DataSource {
7373
export interface DataSourceData<T = any> {
7474
dataSource: DataSource;
7575
data: T;
76+
/** Optional list of column headers (for gsheet sources). */
77+
headers?: string[];
7678
}
7779

7880
export type DataSourceMode = 'draft' | 'published';
@@ -822,6 +824,7 @@ export class RootCMSClient {
822824
batch.set(dataDocRefPublished, {
823825
dataSource: updatedDataSource,
824826
data: dataRes?.data || null,
827+
...(dataRes?.headers ? {headers: dataRes.headers} : {}),
825828
});
826829
batch.update(dataDocRefDraft, {
827830
dataSource: updatedDataSource,

packages/root-cms/ui/pages/DataSourcePage/DataSourcePage.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ DataSourcePage.DataSection = (props: {
137137
dataSource: DataSource;
138138
data: DataSourceData;
139139
}) => {
140-
const {data} = props.data || {};
140+
const {data, headers: storedHeaders} = props.data || {};
141141
const dataSource = props.dataSource;
142142

143143
if (!data) {
@@ -146,22 +146,25 @@ DataSourcePage.DataSection = (props: {
146146

147147
const dataFormat = dataSource.dataFormat || 'map';
148148
if (dataSource.type === 'gsheet') {
149-
let headers: string[] | undefined = undefined;
149+
let headers: string[] | undefined = storedHeaders;
150150
let rows: any[] = [];
151151
if (dataFormat === 'array') {
152152
rows = data as string[][];
153153
} else if (dataFormat === 'map') {
154-
// Reformat Array<Record<string, string>> to string[][].
155-
const headersSet = new Set<string>();
156154
const items = data as any[];
157-
items.forEach((item) => {
158-
for (const key in item) {
159-
if (key) {
160-
headersSet.add(key);
155+
if (!headers) {
156+
// Reformat Array<Record<string, string>> to string[][]. Preserve key order
157+
// by iterating through object keys as encountered.
158+
const headersSet = new Set<string>();
159+
items.forEach((item) => {
160+
for (const key in item) {
161+
if (key) {
162+
headersSet.add(key);
163+
}
161164
}
162-
}
163-
});
164-
headers = Array.from(headersSet);
165+
});
166+
headers = Array.from(headersSet);
167+
}
165168
items.forEach((item) => {
166169
rows.push(headers!.map((header) => item[header] || ''));
167170
});

packages/root-cms/ui/utils/data-source.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export interface DataSource {
4949
export interface DataSourceData<T = any> {
5050
dataSource: DataSource;
5151
data: T;
52+
/** Optional list of column headers (for gsheet sources). */
53+
headers?: string[];
5254
}
5355

5456
export async function addDataSource(
@@ -157,7 +159,7 @@ export async function syncDataSource(id: string) {
157159
throw new Error(`sync failed: ${err}`);
158160
}
159161
} else {
160-
const data = await fetchData(dataSource);
162+
const {data, headers} = await fetchData(dataSource);
161163

162164
const projectId = window.__ROOT_CTX.rootConfig.projectId;
163165
const db = window.firebase.db;
@@ -183,6 +185,7 @@ export async function syncDataSource(id: string) {
183185
batch.set(dataDocRef, {
184186
dataSource: updatedDataSource,
185187
data: data,
188+
...(headers ? {headers} : {}),
186189
});
187190
batch.update(dataSourceDocRef, {
188191
syncedAt: Timestamp.now(),
@@ -235,6 +238,7 @@ export async function publishDataSource(id: string) {
235238
batch.set(dataDocRefPublished, {
236239
dataSource: updatedDataSource,
237240
data: dataRes?.data || null,
241+
...(dataRes?.headers ? {headers: dataRes.headers} : {}),
238242
});
239243
batch.update(dataDocRefDraft, {
240244
dataSource: updatedDataSource,
@@ -280,17 +284,23 @@ export async function deleteDataSource(id: string) {
280284
logAction('datasource.delete', {metadata: {datasourceId: id}});
281285
}
282286

283-
async function fetchData(dataSource: DataSource) {
287+
interface FetchedData {
288+
data: any;
289+
headers?: string[];
290+
}
291+
292+
async function fetchData(dataSource: DataSource): Promise<FetchedData> {
284293
if (dataSource.type === 'http') {
285-
return await fetchHttpData(dataSource);
294+
const data = await fetchHttpData(dataSource);
295+
return {data};
286296
}
287297
if (dataSource.type === 'gsheet') {
288298
return await fetchGsheetData(dataSource);
289299
}
290300
throw new Error(`unsupported data source: ${dataSource.type}`);
291301
}
292302

293-
async function fetchGsheetData(dataSource: DataSource) {
303+
async function fetchGsheetData(dataSource: DataSource): Promise<FetchedData> {
294304
const gsheetId = parseSpreadsheetUrl(dataSource.url);
295305
if (!gsheetId?.spreadsheetId) {
296306
throw new Error(`failed to parse google sheet url: ${dataSource.url}`);
@@ -303,10 +313,21 @@ async function fetchGsheetData(dataSource: DataSource) {
303313
}
304314

305315
const dataFormat = dataSource.dataFormat || 'map';
316+
const [headers, rows] = await gsheet.getValues();
306317
if (dataFormat === 'array') {
307-
return await gsheet.getValues();
318+
return {data: [headers, rows], headers};
308319
}
309-
return await gsheet.getValuesMap();
320+
const mapData = rows.map((row) => {
321+
const item: Record<string, string> = {};
322+
row.forEach((val, i) => {
323+
const key = headers[i];
324+
if (key) {
325+
item[key] = String(val || '');
326+
}
327+
});
328+
return item;
329+
});
330+
return {data: mapData, headers};
310331
}
311332

312333
async function fetchHttpData(dataSource: DataSource) {

0 commit comments

Comments
 (0)