nuxt-google-sheets-import 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # @tribeweb/nuxt-google-sheets-import
1
+ # nuxt-google-sheets-import
2
2
 
3
3
  Schema-driven Google Sheets importer for Nuxt Content.
4
4
 
5
5
  ## Status
6
6
 
7
- This package scaffold is extracted from a working local module and is ready for standalone hardening/publishing.
7
+ Nuxt module for schema-driven Google Sheets import/export workflows with Nuxt Content.
8
8
 
9
9
  ## Features
10
10
 
@@ -13,28 +13,30 @@ This package scaffold is extracted from a working local module and is ready for
13
13
  - Writes frontmatter markdown, JSON, or YAML output
14
14
  - Supports overwrite strategies (`overwrite`, `skip`, `overwrite-frontmatter`)
15
15
  - Exposes UI components and composables for import workflow
16
+ - Includes a built-in `/google-sheets-import` page with tabbed setup/import/export flow
17
+ - Supports TSV clipboard copy for Google Sheets-friendly paste
16
18
 
17
- ## Install (workspace)
19
+ ## Install
18
20
 
19
21
  ```bash
20
- pnpm add @tribeweb/nuxt-google-sheets-import
22
+ pnpm add nuxt-google-sheets-import
21
23
  ```
22
24
 
23
- ## Configure
25
+ ## Configuration
24
26
 
25
27
  ```ts
26
28
  // nuxt.config.ts
27
29
  export default defineNuxtConfig({
28
- modules: ['@tribeweb/nuxt-google-sheets-import'],
30
+ modules: ['nuxt-google-sheets-import'],
29
31
  googleSheetsImport: {
30
32
  apiBase: '/api/google-sheets-import',
31
33
  googleApiKeyRuntimeKey: 'googleApiKey',
32
- defaultContentDir: 'content'
34
+ defaultContentDir: 'content/data'
33
35
  }
34
36
  })
35
37
  ```
36
38
 
37
- Collection type (`page` vs `data`) is derived from your Nuxt Content `content.config.ts` collection definitions.
39
+ Collection type (`page` vs `data`) is derived from your Nuxt Content `content.config.ts` collections.
38
40
 
39
41
  ## Environment
40
42
 
@@ -42,74 +44,32 @@ Collection type (`page` vs `data`) is derived from your Nuxt Content `content.co
42
44
  NUXT_GOOGLE_API_KEY=your_google_sheets_api_key
43
45
  ```
44
46
 
45
- ## Google setup (permissions + API key)
47
+ ## Quick Start
46
48
 
47
- This module currently reads Google Sheets using an API key, so the sheet must be publicly readable.
49
+ 1. Add the module and runtime env var.
50
+ 2. Define your Zod `schemas` export in `~/utils/googleSheetImportSchemas.ts`.
51
+ 3. Ensure matching Nuxt Content collections exist in `content.config.ts`.
52
+ 4. Start dev server and open `/google-sheets-import`.
53
+ 5. Use tabs in order:
54
+ - `Setup Google Sheet`: confirm schema headers and copy as needed.
55
+ - `Import data`: load sheet rows and write content files.
56
+ - `Export data`: query existing records, copy TSV for Sheets, or download CSV.
48
57
 
49
- > Security note:
50
- > API-key access is best suited to non-sensitive sheets that are intentionally shared as `Anyone with the link`.
51
- > For private or sensitive spreadsheets, prefer OAuth 2.0 or a service account flow instead of API-key access.
52
-
53
- ### 1) Set sheet permissions (Google Sheets)
54
-
55
- 1. Open your sheet in Google Sheets.
56
- 2. Click `Share`.
57
- 3. Under `General access`, set to `Anyone with the link`.
58
- 4. Set role to `Viewer`.
59
- 5. Copy the spreadsheet ID from the URL:
60
- - `https://docs.google.com/spreadsheets/d/<SPREADSHEET_ID>/edit`
61
-
62
- If your Google Workspace policy blocks public link sharing, API key access will fail. In that case you need OAuth/service-account based auth (not part of this module yet).
63
-
64
- ### 2) Create API key (Google Cloud Console)
65
-
66
- 1. Open Google Cloud Console: https://console.cloud.google.com/
67
- 2. Create/select a project.
68
- 3. Enable Google Sheets API:
69
- - https://console.cloud.google.com/apis/library/sheets.googleapis.com
70
- 4. Create credentials (API key):
71
- - https://console.cloud.google.com/apis/credentials
72
- - Click `Create credentials` → `API key`
73
- 5. Restrict the key (recommended):
74
- - **API restrictions**: `Restrict key` → select `Google Sheets API`
75
- - **Application restrictions**:
76
- - Server usage: `IP addresses` (recommended for backend)
77
- - Browser-only usage: `HTTP referrers` (if applicable)
78
- 6. Put the key into `NUXT_GOOGLE_API_KEY`.
79
-
80
- ### 3) Quick verification
81
-
82
- Call your module endpoint with a known sheet ID and confirm it returns tab metadata:
58
+ Quick endpoint check:
83
59
 
84
60
  `GET /api/google-sheets-import/sheets?spreadsheetId=<SPREADSHEET_ID>`
85
61
 
86
- ### 4) Troubleshooting (common errors)
87
-
88
- - `403 PERMISSION_DENIED` / `The caller does not have permission`
89
- - The sheet is not publicly readable with link.
90
- - Fix: set `Share` → `General access` → `Anyone with the link` + `Viewer`.
91
-
92
- - `403 API key not valid` or `API has not been used in project`
93
- - The key is wrong, restricted to the wrong API, or Sheets API is not enabled.
94
- - Fix: enable `Google Sheets API` and ensure key restriction includes it.
95
-
96
- - `403 Requests from this referrer/IP are blocked`
97
- - Your key application restrictions do not match where requests come from.
98
- - Fix: update key restrictions (`IP addresses` for server use is preferred).
99
-
100
- - `404 Requested entity was not found`
101
- - Spreadsheet ID is incorrect or malformed.
102
- - Fix: copy ID from `https://docs.google.com/spreadsheets/d/<SPREADSHEET_ID>/edit`.
103
-
104
- - `400 Unable to parse range`
105
- - Invalid A1 range (for example typo in sheet tab or columns).
106
- - Fix: verify tab name and use ranges like `A:Z`.
107
-
108
62
  ## Exported runtime
109
63
 
110
- - Components: `GoogleSheetsImportSource`, `GoogleSheetsImportExecute`, `GoogleSheetsImportSchemaGuide`
64
+ - Components: `GoogleSheetsImportSource`, `GoogleSheetsImportExecute`, `GoogleSheetsImportSchemaGuide`, `GoogleSheetsImportExport`
111
65
  - Composables: `useGoogleSheetsImport`, `useGoogleSheetsImportWorkflow`
112
66
 
67
+ The module adds a route at `/google-sheets-import` with `UTabs` for:
68
+
69
+ - `Setup Google Sheet` (schema guide)
70
+ - `Import data` (sheet source/import flow)
71
+ - `Export data` (export existing content records)
72
+
113
73
  ### Schema helper component
114
74
 
115
75
  Use `GoogleSheetsImportSchemaGuide` to let editors choose a schema and see the expected Google Sheet column headers.
@@ -126,7 +86,7 @@ Benefits:
126
86
  - For `page` collections, shows Nuxt Content built-in page override fields and allows copying them separately
127
87
  - Supports two copy modes:
128
88
  - line-by-line copy
129
- - CSV-row copy (pastes horizontally into Google Sheets)
89
+ - tab-separated row copy (pastes horizontally into Google Sheets)
130
90
 
131
91
  Optional prop:
132
92
 
@@ -144,11 +104,12 @@ Use these value patterns when filling sheets:
144
104
  - `string[]` (scalar array): `foo, bar, baz` in a single cell
145
105
  - `object[]` (array of objects): use indexed headers like `items[0].name`, `items[0].price`
146
106
 
147
- ## Schema source
107
+ ## Schema Source
148
108
 
149
- The module resolves Zod schemas from Nuxt auto-imports using `#imports.schemas`.
109
+ Define a `schemas` export in `~/utils/googleSheetImportSchemas.ts`.
110
+ The module auto-imports this as `googleSheetsImportSchemas` for app and server runtime use.
150
111
 
151
- Define a `schemas` export in `~/utils/googleSheetImportSchemas.ts`:
112
+ Example:
152
113
 
153
114
  ```ts
154
115
  export const schemas = {
@@ -157,27 +118,12 @@ export const schemas = {
157
118
  }
158
119
  ```
159
120
 
160
- ## Playground smoke test (`values` + `write`)
161
-
162
- The playground includes:
163
-
164
- - `playground/utils/googleSheetImportSchemas.ts` (tiny local schema registry)
165
- - `POST /api/google-sheets-import/values-smoke` (local transform/validation payload)
166
- - `playground/scripts/smoke.mjs` (calls `values-smoke`, then module `write` for `frontmatter`, `json`, `yaml`)
167
-
168
- Run with Nuxt dev server active:
169
-
170
- ```bash
171
- pnpm --dir packages/nuxt-google-sheets-import smoke:playground
172
- ```
173
-
174
- Optional custom base URL:
121
+ ## Export Behavior
175
122
 
176
- ```bash
177
- SMOKE_BASE_URL=http://localhost:3000 pnpm --dir packages/nuxt-google-sheets-import smoke:playground
178
- ```
123
+ - `Copy for Google Sheets` in `GoogleSheetsImportExport` copies **TSV** to the clipboard for reliable Sheets paste.
124
+ - `Download .csv` provides quoted CSV output for file-based workflows.
179
125
 
180
- ## Additional API endpoint
126
+ ## Additional API Endpoint
181
127
 
182
128
  - `GET {apiBase}/schema-columns`
183
129
  - Query: `schema?`
@@ -187,31 +133,78 @@ SMOKE_BASE_URL=http://localhost:3000 pnpm --dir packages/nuxt-google-sheets-impo
187
133
  - `collectionType`: `page | data | unknown`
188
134
  - `pageOverrideColumns`: Nuxt Content page override fields (when `collectionType === 'page'`)
189
135
 
136
+ ## Google Setup (Permissions + API Key)
137
+
138
+ This module reads Google Sheets using an API key, so the sheet must be publicly readable.
139
+
140
+ > Security note:
141
+ > API-key access is best suited to non-sensitive sheets that are intentionally shared as `Anyone with the link`.
142
+ > For private or sensitive spreadsheets, prefer OAuth 2.0 or a service account flow instead of API-key access.
143
+
144
+ ### 1) Set sheet permissions (Google Sheets)
145
+
146
+ 1. Open your sheet in Google Sheets.
147
+ 2. Click `Share`.
148
+ 3. Under `General access`, set to `Anyone with the link`.
149
+ 4. Set role to `Viewer`.
150
+ 5. Copy the spreadsheet ID from the URL:
151
+ - `https://docs.google.com/spreadsheets/d/<SPREADSHEET_ID>/edit`
152
+
153
+ If your Google Workspace policy blocks public link sharing, API key access will fail. In that case you need OAuth/service-account based auth (not part of this module yet).
154
+
155
+ ### 2) Create API key (Google Cloud Console)
156
+
157
+ 1. Open Google Cloud Console: https://console.cloud.google.com/
158
+ 2. Create/select a project.
159
+ 3. Enable Google Sheets API:
160
+ - https://console.cloud.google.com/apis/library/sheets.googleapis.com
161
+ 4. Create credentials (API key):
162
+ - https://console.cloud.google.com/apis/credentials
163
+ - Click `Create credentials` -> `API key`
164
+ 5. Restrict the key (recommended):
165
+ - **API restrictions**: `Restrict key` -> select `Google Sheets API`
166
+ - **Application restrictions**:
167
+ - Server usage: `IP addresses` (recommended for backend)
168
+ - Browser-only usage: `HTTP referrers` (if applicable)
169
+ 6. Put the key into `NUXT_GOOGLE_API_KEY`.
170
+
171
+ ### 3) Troubleshooting (common errors)
172
+
173
+ - `403 PERMISSION_DENIED` / `The caller does not have permission`
174
+ - The sheet is not publicly readable with link.
175
+ - Fix: set `Share` -> `General access` -> `Anyone with the link` + `Viewer`.
176
+
177
+ - `403 API key not valid` or `API has not been used in project`
178
+ - The key is wrong, restricted to the wrong API, or Sheets API is not enabled.
179
+ - Fix: enable `Google Sheets API` and ensure key restriction includes it.
180
+
181
+ - `403 Requests from this referrer/IP are blocked`
182
+ - Your key application restrictions do not match where requests come from.
183
+ - Fix: update key restrictions (`IP addresses` for server use is preferred).
184
+
185
+ - `404 Requested entity was not found`
186
+ - Spreadsheet ID is incorrect or malformed.
187
+ - Fix: copy ID from `https://docs.google.com/spreadsheets/d/<SPREADSHEET_ID>/edit`.
188
+
189
+ - `400 Unable to parse range`
190
+ - Invalid A1 range (for example typo in sheet tab or columns).
191
+ - Fix: verify tab name and use ranges like `A:Z`.
192
+
190
193
  ## Publish checklist
191
194
 
192
195
  - Add playground integration tests for `/values` and `/write`
193
196
  - Add CI (`lint`, `typecheck`, `build`) and release workflow
194
197
  - Verify Nuxt 4 peer compatibility matrix
195
198
 
196
- ## Publish (next step)
199
+ ## Release
197
200
 
198
201
  ```bash
199
- pnpm --dir packages/nuxt-google-sheets-import release:check
202
+ npm run release
200
203
  ```
201
204
 
202
- Then authenticate and publish:
205
+ If needed, authenticate and publish manually:
203
206
 
204
207
  ```bash
205
208
  npm login
206
- pnpm --dir packages/nuxt-google-sheets-import publish --access public
209
+ npm publish --access public
207
210
  ```
208
-
209
- Or use one-command release scripts (bumps version + checks + publishes):
210
-
211
- ```bash
212
- npm login
213
- pnpm --dir packages/nuxt-google-sheets-import release:patch
214
- # or: release:minor / release:major
215
- ```
216
-
217
- These scripts use `npm version --no-git-tag-version`, so they update `package.json` version without creating a git tag/commit.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-google-sheets-import",
3
3
  "configKey": "googleSheetsImport",
4
- "version": "0.1.6",
4
+ "version": "0.1.7",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -6,6 +6,9 @@ const module$1 = defineNuxtModule({
6
6
  configKey: "googleSheetsImport"
7
7
  },
8
8
  moduleDependencies: {
9
+ "@nuxt/content": {
10
+ version: ">=3"
11
+ },
9
12
  "@nuxt/ui": {
10
13
  version: ">=4"
11
14
  }
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,196 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import { googleSheetsImportSchemas, queryCollection, useAsyncData, useToast } from "#imports";
4
+ import { copyTextWithSuccessToast } from "../utils/clipboard";
5
+ import { toCsvRow, toTsvRow } from "../utils/delimited";
6
+ import { flattenRecordToStringMap } from "../utils/pathMapping";
7
+ const toast = useToast();
8
+ const queryCollectionLoose = queryCollection;
9
+ const selectedSchema = ref("");
10
+ const {
11
+ data: rowsData,
12
+ pending,
13
+ error: loadError,
14
+ status,
15
+ execute,
16
+ clear
17
+ } = useAsyncData(
18
+ "google-sheets-import-export-records",
19
+ async () => {
20
+ if (!selectedSchema.value) {
21
+ return [];
22
+ }
23
+ const result = await queryCollectionLoose(selectedSchema.value).all();
24
+ return result;
25
+ },
26
+ {
27
+ immediate: false,
28
+ default: () => []
29
+ }
30
+ );
31
+ const rows = computed(() => rowsData.value ?? []);
32
+ const error = computed(() => loadError.value?.message ?? "");
33
+ watch(selectedSchema, () => {
34
+ clear();
35
+ });
36
+ const schemaMap = computed(() => googleSheetsImportSchemas ?? {});
37
+ const schemaOptions = computed(() => Object.keys(schemaMap.value).sort((left, right) => left.localeCompare(right)).map((schema) => ({ label: schema, value: schema })));
38
+ const headers = computed(() => {
39
+ const all = /* @__PURE__ */ new Set();
40
+ for (const row of rows.value) {
41
+ flattenRecordToStringMap(row).forEach((_, key) => all.add(key));
42
+ }
43
+ return Array.from(all).sort((left, right) => left.localeCompare(right));
44
+ });
45
+ const csvText = computed(() => {
46
+ if (!headers.value.length) {
47
+ return "";
48
+ }
49
+ const lines = [];
50
+ lines.push(toCsvRow(headers.value));
51
+ for (const row of rows.value) {
52
+ const flat = flattenRecordToStringMap(row);
53
+ const values = headers.value.map((header) => flat.get(header) ?? "");
54
+ lines.push(toCsvRow(values));
55
+ }
56
+ return `${lines.join("\n")}
57
+ `;
58
+ });
59
+ const tsvText = computed(() => {
60
+ if (!headers.value.length) {
61
+ return "";
62
+ }
63
+ const lines = [];
64
+ lines.push(toTsvRow(headers.value));
65
+ for (const row of rows.value) {
66
+ const flat = flattenRecordToStringMap(row);
67
+ const values = headers.value.map((header) => flat.get(header) ?? "");
68
+ lines.push(toTsvRow(values));
69
+ }
70
+ return `${lines.join("\n")}
71
+ `;
72
+ });
73
+ const previewText = computed(() => {
74
+ if (!csvText.value) {
75
+ return "";
76
+ }
77
+ return csvText.value.split("\n").slice(0, 11).join("\n");
78
+ });
79
+ async function loadRecords() {
80
+ if (!selectedSchema.value) {
81
+ return;
82
+ }
83
+ await execute();
84
+ if (status.value === "success") {
85
+ toast.add({
86
+ title: "Export ready",
87
+ description: `Loaded ${rows.value.length} row(s) from collection ${selectedSchema.value}.`,
88
+ color: "success"
89
+ });
90
+ }
91
+ }
92
+ async function copyCsv() {
93
+ await copyTextWithSuccessToast({
94
+ text: tsvText.value,
95
+ toast,
96
+ description: "Tab-separated rows copied for Google Sheets paste.",
97
+ title: "Copied"
98
+ });
99
+ }
100
+ function downloadCsv() {
101
+ if (!csvText.value) {
102
+ return;
103
+ }
104
+ const blob = new Blob([csvText.value], { type: "text/csv;charset=utf-8" });
105
+ const url = URL.createObjectURL(blob);
106
+ const link = document.createElement("a");
107
+ link.href = url;
108
+ link.download = `${selectedSchema.value || "export"}.csv`;
109
+ link.click();
110
+ URL.revokeObjectURL(url);
111
+ }
112
+ </script>
113
+
114
+ <template>
115
+ <div class="space-y-4">
116
+ <UAlert
117
+ title="Export Existing Content"
118
+ description="Load records from a Nuxt Content collection and export them as CSV for Google Sheets."
119
+ color="neutral"
120
+ variant="subtle"
121
+ />
122
+
123
+ <UAlert
124
+ v-if="error"
125
+ title="Could not load collection"
126
+ :description="error"
127
+ color="error"
128
+ variant="subtle"
129
+ />
130
+
131
+ <UFormField
132
+ label="Collection schema"
133
+ name="schema"
134
+ description="Pick the schema/collection to export."
135
+ >
136
+ <USelect
137
+ v-model="selectedSchema"
138
+ :items="schemaOptions"
139
+ value-key="value"
140
+ class="w-full max-w-sm"
141
+ icon="i-heroicons-cube-20-solid"
142
+ />
143
+ </UFormField>
144
+
145
+ <div class="flex flex-wrap gap-2">
146
+ <UButton
147
+ label="Load records"
148
+ icon="i-heroicons-arrow-down-tray-20-solid"
149
+ :disabled="!selectedSchema"
150
+ :loading="pending"
151
+ @click="loadRecords"
152
+ />
153
+ <UButton
154
+ label="Copy for Google Sheets"
155
+ icon="i-heroicons-clipboard-document-20-solid"
156
+ color="neutral"
157
+ variant="subtle"
158
+ :disabled="!tsvText"
159
+ @click="copyCsv"
160
+ />
161
+ <UButton
162
+ label="Download .csv"
163
+ icon="i-heroicons-document-arrow-down-20-solid"
164
+ color="neutral"
165
+ variant="subtle"
166
+ :disabled="!csvText"
167
+ @click="downloadCsv"
168
+ />
169
+ </div>
170
+
171
+ <UAlert
172
+ v-if="status === 'success'"
173
+ title="Export prepared"
174
+ :description="`${rows.length} row(s), ${headers.length} column(s).`"
175
+ color="success"
176
+ variant="subtle"
177
+ />
178
+
179
+ <UCollapsible
180
+ v-if="previewText"
181
+ class="flex flex-col gap-2 w-full"
182
+ >
183
+ <UButton
184
+ label="Preview CSV (first 10 rows)"
185
+ color="neutral"
186
+ variant="subtle"
187
+ trailing-icon="i-lucide-chevron-down"
188
+ block
189
+ />
190
+
191
+ <template #content>
192
+ <pre class="text-xs whitespace-pre-wrap">{{ previewText }}</pre>
193
+ </template>
194
+ </UCollapsible>
195
+ </div>
196
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -2,6 +2,8 @@
2
2
  import { computed, onMounted, ref, watch } from "vue";
3
3
  import { useToast } from "#imports";
4
4
  import { useGoogleSheetsImport } from "../composables/useGoogleSheetsImport";
5
+ import { copyTextWithSuccessToast } from "../utils/clipboard";
6
+ import { toTsvRow } from "../utils/delimited";
5
7
  const props = defineProps({
6
8
  initialSchema: { type: String, required: false, default: "" }
7
9
  });
@@ -61,58 +63,36 @@ onMounted(async () => {
61
63
  await loadColumns(selectedSchema.value);
62
64
  }
63
65
  });
64
- function copyColumns() {
65
- const content = columns.value.join("\n");
66
- if (!content) {
67
- return;
68
- }
69
- navigator.clipboard.writeText(content);
70
- toast.add({
71
- title: "Copied",
66
+ async function copyColumns() {
67
+ await copyTextWithSuccessToast({
68
+ text: columns.value.join("\n"),
69
+ toast,
72
70
  description: "Column names copied to clipboard.",
73
- color: "success"
71
+ title: "Copied"
74
72
  });
75
73
  }
76
- function csvRow(values) {
77
- return values.map((value) => {
78
- const escaped = value.replaceAll('"', '""');
79
- return `"${escaped}"`;
80
- }).join(",");
81
- }
82
- function copyColumnsCsv() {
83
- const content = csvRow(columns.value);
84
- if (!content) {
85
- return;
86
- }
87
- navigator.clipboard.writeText(content);
88
- toast.add({
89
- title: "Copied",
90
- description: "Column names copied as a CSV row.",
91
- color: "success"
74
+ async function copyColumnsTsv() {
75
+ await copyTextWithSuccessToast({
76
+ text: toTsvRow(columns.value),
77
+ toast,
78
+ description: "Column names copied as a tab-separated row for Google Sheets.",
79
+ title: "Copied"
92
80
  });
93
81
  }
94
- function copyPageOverrideColumns() {
95
- const content = pageOverrideColumns.value.join("\n");
96
- if (!content) {
97
- return;
98
- }
99
- navigator.clipboard.writeText(content);
100
- toast.add({
101
- title: "Copied",
82
+ async function copyPageOverrideColumns() {
83
+ await copyTextWithSuccessToast({
84
+ text: pageOverrideColumns.value.join("\n"),
85
+ toast,
102
86
  description: "Page override column names copied to clipboard.",
103
- color: "success"
87
+ title: "Copied"
104
88
  });
105
89
  }
106
- function copyPageOverrideColumnsCsv() {
107
- const content = csvRow(pageOverrideColumns.value);
108
- if (!content) {
109
- return;
110
- }
111
- navigator.clipboard.writeText(content);
112
- toast.add({
113
- title: "Copied",
114
- description: "Page override column names copied as a CSV row.",
115
- color: "success"
90
+ async function copyPageOverrideColumnsTsv() {
91
+ await copyTextWithSuccessToast({
92
+ text: toTsvRow(pageOverrideColumns.value),
93
+ toast,
94
+ description: "Page override column names copied as a tab-separated row for Google Sheets.",
95
+ title: "Copied"
116
96
  });
117
97
  }
118
98
  </script>
@@ -170,11 +150,11 @@ function copyPageOverrideColumnsCsv() {
170
150
  @click="copyColumns"
171
151
  />
172
152
  <UButton
173
- label="Copy as CSV row"
153
+ label="Copy as tab-separated row"
174
154
  icon="i-heroicons-table-cells-20-solid"
175
155
  color="neutral"
176
156
  variant="subtle"
177
- @click="copyColumnsCsv"
157
+ @click="copyColumnsTsv"
178
158
  />
179
159
  </div>
180
160
  <pre class="text-xs whitespace-pre-wrap">{{ columns.join("\n") }}</pre>
@@ -200,11 +180,11 @@ function copyPageOverrideColumnsCsv() {
200
180
  @click="copyPageOverrideColumns"
201
181
  />
202
182
  <UButton
203
- label="Copy overrides as CSV row"
183
+ label="Copy overrides as tab-separated row"
204
184
  icon="i-heroicons-table-cells-20-solid"
205
185
  color="neutral"
206
186
  variant="subtle"
207
- @click="copyPageOverrideColumnsCsv"
187
+ @click="copyPageOverrideColumnsTsv"
208
188
  />
209
189
  </div>
210
190
  <pre class="text-xs whitespace-pre-wrap">{{ pageOverrideColumns.join("\n") }}</pre>
@@ -4,11 +4,42 @@ const sheetsList = ref([
4
4
  { id: "1NKS0cTX6u5urtgQ3Q4Z2motiR2-9JmyPxcd05yVc1bc", label: "Metzner" },
5
5
  { id: "1tGZCEoiikXfg3mOpfVWWTS1SSSsj18xv6Z3owrnnt4s", label: "Example Sheet" }
6
6
  ]);
7
+ const items = ref([
8
+ {
9
+ label: "Setup Google Sheet",
10
+ icon: "i-lucide-layout-dashboard",
11
+ slot: "sheet"
12
+ },
13
+ {
14
+ label: "Import data",
15
+ icon: "i-lucide-upload",
16
+ slot: "import"
17
+ },
18
+ {
19
+ label: "Export data",
20
+ icon: "i-lucide-download",
21
+ slot: "export"
22
+ }
23
+ ]);
7
24
  </script>
8
25
 
9
26
  <template>
10
- <UContainer>
11
- <GoogleSheetsImportSchemaGuide />
12
- <GoogleSheetsImportSource :google-sheets="sheetsList" />
27
+ <UContainer class="max-w-36">
28
+ <UTabs
29
+ :items="items"
30
+ variant="link"
31
+ color="neutral"
32
+ class="w-full"
33
+ >
34
+ <template #sheet>
35
+ <GoogleSheetsImportSchemaGuide :initial-schema="'Example Sheet'" />
36
+ </template>
37
+ <template #import>
38
+ <GoogleSheetsImportSource :google-sheets="sheetsList" />
39
+ </template>
40
+ <template #export>
41
+ <GoogleSheetsImportExport />
42
+ </template>
43
+ </UTabs>
13
44
  </UContainer>
14
45
  </template>
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { parseHeaderPath, setDeep } from "../../utils/pathMapping.js";
2
3
  function isZodType(value) {
3
4
  return typeof value === "object" && value !== null && "_def" in value;
4
5
  }
@@ -47,48 +48,6 @@ function hasWrapper(schema, wrapper) {
47
48
  }
48
49
  return false;
49
50
  }
50
- function parseHeaderPath(header) {
51
- const tokens = [];
52
- const parts = header.split(".");
53
- for (const part of parts) {
54
- const matches = part.matchAll(/([^[]+)|(\[(\d+)\])/g);
55
- for (const match of matches) {
56
- if (match[1]) {
57
- tokens.push(match[1]);
58
- }
59
- if (match[3]) {
60
- tokens.push(Number.parseInt(match[3], 10));
61
- }
62
- }
63
- }
64
- return tokens;
65
- }
66
- function setDeep(target, path, value) {
67
- if (!path.length) {
68
- return;
69
- }
70
- let cursor = target;
71
- for (let index = 0; index < path.length; index++) {
72
- const key = path[index];
73
- if (key === void 0) {
74
- return;
75
- }
76
- const isLast = index === path.length - 1;
77
- const nextKey = path[index + 1];
78
- if (isLast) {
79
- cursor[key] = value;
80
- return;
81
- }
82
- if (cursor[key] === void 0) {
83
- cursor[key] = typeof nextKey === "number" ? [] : {};
84
- }
85
- const nextCursor = cursor[key];
86
- if (!nextCursor || typeof nextCursor !== "object") {
87
- return;
88
- }
89
- cursor = nextCursor;
90
- }
91
- }
92
51
  function getObjectShape(schema) {
93
52
  const unwrapped = unwrapSchema(schema);
94
53
  if (!unwrapped) {
@@ -0,0 +1,15 @@
1
+ interface ToastLike {
2
+ add: (payload: {
3
+ title?: string;
4
+ description?: string;
5
+ color?: 'success' | 'error' | 'warning' | 'info' | 'neutral';
6
+ }) => void;
7
+ }
8
+ interface CopyTextWithToastOptions {
9
+ text: string;
10
+ toast: ToastLike;
11
+ description: string;
12
+ title?: string;
13
+ }
14
+ export declare function copyTextWithSuccessToast(options: CopyTextWithToastOptions): Promise<boolean>;
15
+ export {};
@@ -0,0 +1,12 @@
1
+ export async function copyTextWithSuccessToast(options) {
2
+ if (!options.text) {
3
+ return false;
4
+ }
5
+ await navigator.clipboard.writeText(options.text);
6
+ options.toast.add({
7
+ title: options.title ?? "Copied",
8
+ description: options.description,
9
+ color: "success"
10
+ });
11
+ return true;
12
+ }
@@ -0,0 +1,2 @@
1
+ export declare function toCsvRow(values: string[]): string;
2
+ export declare function toTsvRow(values: string[]): string;
@@ -0,0 +1,9 @@
1
+ export function toCsvRow(values) {
2
+ return values.map((value) => {
3
+ const escaped = value.replaceAll('"', '""');
4
+ return `"${escaped}"`;
5
+ }).join(",");
6
+ }
7
+ export function toTsvRow(values) {
8
+ return values.map((value) => value.replace(/[\t\r\n]+/g, " ")).join(" ");
9
+ }
@@ -0,0 +1,4 @@
1
+ export type PathSegment = string | number;
2
+ export declare function parseHeaderPath(header: string): PathSegment[];
3
+ export declare function setDeep(target: Record<string, unknown>, path: PathSegment[], value: unknown): void;
4
+ export declare function flattenRecordToStringMap(record: Record<string, unknown>, prefix?: string): Map<string, string>;
@@ -0,0 +1,86 @@
1
+ const EXCLUDED_EXPORT_PATHS = /* @__PURE__ */ new Set(["__hash__", "body"]);
2
+ export function parseHeaderPath(header) {
3
+ const tokens = [];
4
+ const parts = header.split(".");
5
+ for (const part of parts) {
6
+ const matches = part.matchAll(/([^[]+)|(\[(\d+)\])/g);
7
+ for (const match of matches) {
8
+ if (match[1]) {
9
+ tokens.push(match[1]);
10
+ }
11
+ if (match[3]) {
12
+ tokens.push(Number.parseInt(match[3], 10));
13
+ }
14
+ }
15
+ }
16
+ return tokens;
17
+ }
18
+ export function setDeep(target, path, value) {
19
+ if (!path.length) {
20
+ return;
21
+ }
22
+ let cursor = target;
23
+ for (let index = 0; index < path.length; index++) {
24
+ const key = path[index];
25
+ if (key === void 0) {
26
+ return;
27
+ }
28
+ const isLast = index === path.length - 1;
29
+ const nextKey = path[index + 1];
30
+ if (isLast) {
31
+ cursor[key] = value;
32
+ return;
33
+ }
34
+ if (cursor[key] === void 0) {
35
+ cursor[key] = typeof nextKey === "number" ? [] : {};
36
+ }
37
+ const nextCursor = cursor[key];
38
+ if (!nextCursor || typeof nextCursor !== "object") {
39
+ return;
40
+ }
41
+ cursor = nextCursor;
42
+ }
43
+ }
44
+ export function flattenRecordToStringMap(record, prefix = "") {
45
+ const out = /* @__PURE__ */ new Map();
46
+ for (const [key, value] of Object.entries(record)) {
47
+ const path = prefix ? `${prefix}.${key}` : key;
48
+ if (EXCLUDED_EXPORT_PATHS.has(path)) {
49
+ continue;
50
+ }
51
+ if (value === null || value === void 0) {
52
+ out.set(path, "");
53
+ continue;
54
+ }
55
+ if (Array.isArray(value)) {
56
+ if (value.every((item) => item === null || ["string", "number", "boolean"].includes(typeof item))) {
57
+ out.set(path, value.map((item) => item == null ? "" : String(item)).join(", "));
58
+ continue;
59
+ }
60
+ for (let index = 0; index < value.length; index++) {
61
+ const item = value[index];
62
+ const arrayPath = `${path}[${index}]`;
63
+ if (item && typeof item === "object" && !Array.isArray(item)) {
64
+ flattenRecordToStringMap(item, arrayPath).forEach((nestedValue, nestedKey) => {
65
+ out.set(nestedKey, nestedValue);
66
+ });
67
+ } else {
68
+ out.set(arrayPath, item == null ? "" : String(item));
69
+ }
70
+ }
71
+ continue;
72
+ }
73
+ if (value instanceof Date) {
74
+ out.set(path, value.toISOString());
75
+ continue;
76
+ }
77
+ if (typeof value === "object") {
78
+ flattenRecordToStringMap(value, path).forEach((nestedValue, nestedKey) => {
79
+ out.set(nestedKey, nestedValue);
80
+ });
81
+ continue;
82
+ }
83
+ out.set(path, String(value));
84
+ }
85
+ return out;
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-google-sheets-import",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Schema-driven Google Sheets import module for Nuxt Content",
5
5
  "repository": "tribeweb/nuxt-google-sheets-import",
6
6
  "license": "MIT",