nuxt-google-sheets-import 0.1.6 → 0.1.8

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.
Files changed (37) hide show
  1. package/README.md +38 -168
  2. package/dist/module.d.mts +0 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +16 -14
  5. package/dist/runtime/app/components/GoogleSheetsImportExport.vue +212 -0
  6. package/dist/runtime/{components → app/components}/GoogleSheetsImportSchemaGuide.vue +28 -48
  7. package/dist/runtime/app/examples/googleSheetsImportSchemasExample.d.ts +1 -0
  8. package/dist/runtime/app/examples/googleSheetsImportSchemasExample.js +0 -0
  9. package/dist/runtime/app/pages/google-sheets-import.d.vue.ts +3 -0
  10. package/dist/runtime/app/pages/google-sheets-import.vue +45 -0
  11. package/dist/runtime/app/pages/google-sheets-import.vue.d.ts +3 -0
  12. package/dist/runtime/app/utils/clipboard.d.ts +15 -0
  13. package/dist/runtime/app/utils/clipboard.js +12 -0
  14. package/dist/runtime/app/utils/delimited.d.ts +2 -0
  15. package/dist/runtime/app/utils/delimited.js +9 -0
  16. package/dist/runtime/app/utils/pathMapping.d.ts +4 -0
  17. package/dist/runtime/app/utils/pathMapping.js +86 -0
  18. package/dist/runtime/server/api/sheets.get.js +2 -2
  19. package/dist/runtime/server/api/values.post.js +2 -2
  20. package/dist/runtime/server/utils/transform.js +1 -42
  21. package/package.json +5 -2
  22. package/dist/runtime/pages/google-sheets-import.vue +0 -14
  23. /package/dist/runtime/{assets → app/assets}/css/main.css +0 -0
  24. /package/dist/runtime/{components → app/components}/GoogleSheetsImportExecute.d.vue.ts +0 -0
  25. /package/dist/runtime/{components → app/components}/GoogleSheetsImportExecute.vue +0 -0
  26. /package/dist/runtime/{components → app/components}/GoogleSheetsImportExecute.vue.d.ts +0 -0
  27. /package/dist/runtime/{pages/google-sheets-import.d.vue.ts → app/components/GoogleSheetsImportExport.d.vue.ts} +0 -0
  28. /package/dist/runtime/{pages/google-sheets-import.vue.d.ts → app/components/GoogleSheetsImportExport.vue.d.ts} +0 -0
  29. /package/dist/runtime/{components → app/components}/GoogleSheetsImportSchemaGuide.d.vue.ts +0 -0
  30. /package/dist/runtime/{components → app/components}/GoogleSheetsImportSchemaGuide.vue.d.ts +0 -0
  31. /package/dist/runtime/{components → app/components}/GoogleSheetsImportSource.d.vue.ts +0 -0
  32. /package/dist/runtime/{components → app/components}/GoogleSheetsImportSource.vue +0 -0
  33. /package/dist/runtime/{components → app/components}/GoogleSheetsImportSource.vue.d.ts +0 -0
  34. /package/dist/runtime/{composables → app/composables}/useGoogleSheetsImport.d.ts +0 -0
  35. /package/dist/runtime/{composables → app/composables}/useGoogleSheetsImport.js +0 -0
  36. /package/dist/runtime/{composables → app/composables}/useGoogleSheetsImportWorkflow.d.ts +0 -0
  37. /package/dist/runtime/{composables → app/composables}/useGoogleSheetsImportWorkflow.js +0 -0
package/README.md CHANGED
@@ -1,154 +1,59 @@
1
- # @tribeweb/nuxt-google-sheets-import
1
+ # nuxt-google-sheets-import
2
2
 
3
- Schema-driven Google Sheets importer for Nuxt Content.
3
+ Schema-driven Google Sheets import and export workflows for Nuxt Content.
4
4
 
5
- ## Status
5
+ ## Documentation
6
6
 
7
- This package scaffold is extracted from a working local module and is ready for standalone hardening/publishing.
7
+ Project documentation now lives in the Docus site under [docs/content](docs/content).
8
8
 
9
- ## Features
9
+ - Intro: [docs/content/1.getting-started/2.introduction.md](docs/content/1.getting-started/2.introduction.md)
10
+ - Installation: [docs/content/1.getting-started/3.installation.md](docs/content/1.getting-started/3.installation.md)
10
11
 
11
- - Fetches sheet list and values from Google Sheets
12
- - Validates/transforms rows with Zod schemas
13
- - Writes frontmatter markdown, JSON, or YAML output
14
- - Supports overwrite strategies (`overwrite`, `skip`, `overwrite-frontmatter`)
15
- - Exposes UI components and composables for import workflow
12
+ Use this root README as a repo quick-start and contributor guide. Keep long-form user documentation in the docs site.
16
13
 
17
- ## Install (workspace)
14
+ ## Module Quick Start
15
+
16
+ Install:
18
17
 
19
18
  ```bash
20
- pnpm add @tribeweb/nuxt-google-sheets-import
19
+ pnpm add nuxt-google-sheets-import
21
20
  ```
22
21
 
23
- ## Configure
22
+ Enable the module:
24
23
 
25
24
  ```ts
26
25
  // nuxt.config.ts
27
26
  export default defineNuxtConfig({
28
- modules: ['@tribeweb/nuxt-google-sheets-import'],
27
+ modules: ['nuxt-google-sheets-import'],
29
28
  googleSheetsImport: {
30
29
  apiBase: '/api/google-sheets-import',
31
- googleApiKeyRuntimeKey: 'googleApiKey',
32
- defaultContentDir: 'content'
30
+ defaultContentDir: 'content/data'
33
31
  }
34
32
  })
35
33
  ```
36
34
 
37
- Collection type (`page` vs `data`) is derived from your Nuxt Content `content.config.ts` collection definitions.
38
-
39
- ## Environment
35
+ Environment:
40
36
 
41
37
  ```bash
42
38
  NUXT_GOOGLE_API_KEY=your_google_sheets_api_key
43
39
  ```
44
40
 
45
- ## Google setup (permissions + API key)
46
-
47
- This module currently reads Google Sheets using an API key, so the sheet must be publicly readable.
48
-
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:
83
-
84
- `GET /api/google-sheets-import/sheets?spreadsheetId=<SPREADSHEET_ID>`
41
+ ## What The Module Provides
85
42
 
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
- ## Exported runtime
109
-
110
- - Components: `GoogleSheetsImportSource`, `GoogleSheetsImportExecute`, `GoogleSheetsImportSchemaGuide`
111
- - Composables: `useGoogleSheetsImport`, `useGoogleSheetsImportWorkflow`
112
-
113
- ### Schema helper component
114
-
115
- Use `GoogleSheetsImportSchemaGuide` to let editors choose a schema and see the expected Google Sheet column headers.
116
-
117
- ```vue
118
- <GoogleSheetsImportSchemaGuide />
119
- ```
43
+ - A built-in route at `/google-sheets-import`
44
+ - Tabbed workflow with schema setup, import, and export steps
45
+ - Zod schema-driven row validation/transforms
46
+ - Writes to markdown frontmatter, JSON, and YAML
47
+ - Export helpers:
48
+ - copy TSV to clipboard for Google Sheets paste
49
+ - download CSV
120
50
 
121
- Benefits:
51
+ Collection type (`page` vs `data`) is resolved from your Nuxt Content collections.
122
52
 
123
- - Reduces import failures by giving editors exact header names before filling a sheet
124
- - Supports nested/array header patterns used by schema mapping (for example `items[0].name`)
125
- - Uses a single column for arrays of scalar values (for example `tags` with `foo, bar, baz`)
126
- - For `page` collections, shows Nuxt Content built-in page override fields and allows copying them separately
127
- - Supports two copy modes:
128
- - line-by-line copy
129
- - CSV-row copy (pastes horizontally into Google Sheets)
53
+ ## Schema Source
130
54
 
131
- Optional prop:
132
-
133
- - `initialSchema?: string`
134
-
135
- ### Suggested Cell Examples
136
-
137
- Use these value patterns when filling sheets:
138
-
139
- - `string`: `example text`
140
- - `number`: `123`
141
- - `boolean`: `true` or `false`
142
- - `enum` / `literal`: use one of the schema's allowed values
143
- - `date-like string`: `2026-01-01`
144
- - `string[]` (scalar array): `foo, bar, baz` in a single cell
145
- - `object[]` (array of objects): use indexed headers like `items[0].name`, `items[0].price`
146
-
147
- ## Schema source
148
-
149
- The module resolves Zod schemas from Nuxt auto-imports using `#imports.schemas`.
150
-
151
- Define a `schemas` export in `~/utils/googleSheetImportSchemas.ts`:
55
+ Define and export `schemas` in `~/utils/googleSheetImportSchemas.ts`.
56
+ The module auto-imports it as `googleSheetsImportSchemas` in app and server runtime.
152
57
 
153
58
  ```ts
154
59
  export const schemas = {
@@ -157,61 +62,26 @@ export const schemas = {
157
62
  }
158
63
  ```
159
64
 
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:
175
-
176
- ```bash
177
- SMOKE_BASE_URL=http://localhost:3000 pnpm --dir packages/nuxt-google-sheets-import smoke:playground
178
- ```
65
+ ## Repository Development
179
66
 
180
- ## Additional API endpoint
181
-
182
- - `GET {apiBase}/schema-columns`
183
- - Query: `schema?`
184
- - Returns:
185
- - `schemas`: available schema keys
186
- - `columns`: expected import header names for selected schema
187
- - `collectionType`: `page | data | unknown`
188
- - `pageOverrideColumns`: Nuxt Content page override fields (when `collectionType === 'page'`)
189
-
190
- ## Publish checklist
191
-
192
- - Add playground integration tests for `/values` and `/write`
193
- - Add CI (`lint`, `typecheck`, `build`) and release workflow
194
- - Verify Nuxt 4 peer compatibility matrix
195
-
196
- ## Publish (next step)
67
+ Module development:
197
68
 
198
69
  ```bash
199
- pnpm --dir packages/nuxt-google-sheets-import release:check
70
+ npm install
71
+ npm run dev:prepare
72
+ npm run test
200
73
  ```
201
74
 
202
- Then authenticate and publish:
75
+ Playground app:
203
76
 
204
77
  ```bash
205
- npm login
206
- pnpm --dir packages/nuxt-google-sheets-import publish --access public
78
+ npm run dev
207
79
  ```
208
80
 
209
- Or use one-command release scripts (bumps version + checks + publishes):
81
+ Docs site:
210
82
 
211
83
  ```bash
212
- npm login
213
- pnpm --dir packages/nuxt-google-sheets-import release:patch
214
- # or: release:minor / release:major
84
+ cd docs
85
+ npm install
86
+ npm run dev
215
87
  ```
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.d.mts CHANGED
@@ -2,7 +2,6 @@ import * as _nuxt_schema from '@nuxt/schema';
2
2
 
3
3
  interface ModuleOptions {
4
4
  apiBase: string;
5
- googleApiKeyRuntimeKey: string;
6
5
  defaultContentDir: string;
7
6
  }
8
7
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
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.8",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { defineNuxtModule, createResolver, extendPages, addServerHandler, addImportsDir, addComponentsDir, addImports, addServerImports } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addTemplate, extendPages, addServerHandler, addImportsDir, addComponentsDir, addImports, addServerImports } from '@nuxt/kit';
2
2
 
3
3
  const module$1 = defineNuxtModule({
4
4
  meta: {
@@ -6,21 +6,28 @@ 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
  }
12
15
  },
13
16
  defaults: {
14
17
  apiBase: "/api/google-sheets-import",
15
- googleApiKeyRuntimeKey: "googleApiKey",
16
18
  defaultContentDir: "content/data"
17
19
  },
18
20
  setup(options, nuxt) {
19
21
  const resolver = createResolver(import.meta.url);
22
+ addTemplate({
23
+ filename: "googleSheetsImportSchemaExample.ts",
24
+ src: resolver.resolve("./runtime/app/examples/googleSheetsImportSchemasExample.ts"),
25
+ dst: "utils/googleSheetsImportSchemasExample.ts",
26
+ write: true
27
+ });
20
28
  nuxt.options.runtimeConfig.googleSheetsImport = {
21
29
  ...nuxt.options.runtimeConfig.googleSheetsImport,
22
30
  apiBase: options.apiBase,
23
- googleApiKeyRuntimeKey: options.googleApiKeyRuntimeKey,
24
31
  defaultContentDir: options.defaultContentDir
25
32
  };
26
33
  nuxt.options.runtimeConfig.public.googleSheetsImport = {
@@ -28,12 +35,12 @@ const module$1 = defineNuxtModule({
28
35
  apiBase: options.apiBase,
29
36
  defaultContentDir: options.defaultContentDir
30
37
  };
31
- nuxt.options.css.push(resolver.resolve("./runtime/assets/css/main.css"));
38
+ nuxt.options.css.push(resolver.resolve("./runtime/app/assets/css/main.css"));
32
39
  extendPages((pages) => {
33
40
  pages.push({
34
41
  name: "google-sheets-import",
35
42
  path: "/google-sheets-import",
36
- file: resolver.resolve("./runtime/pages/google-sheets-import.vue")
43
+ file: resolver.resolve("./runtime/app/pages/google-sheets-import.vue")
37
44
  });
38
45
  });
39
46
  addServerHandler({
@@ -61,16 +68,11 @@ const module$1 = defineNuxtModule({
61
68
  method: "post",
62
69
  handler: resolver.resolve("./runtime/server/api/write.post")
63
70
  });
64
- addImportsDir(resolver.resolve("./runtime/composables"));
71
+ addImportsDir(resolver.resolve("./runtime/app/composables"));
65
72
  addComponentsDir({
66
- path: resolver.resolve("./runtime/components")
73
+ path: resolver.resolve("./runtime/app/components")
67
74
  });
68
75
  addImports([
69
- {
70
- from: "~/utils/googleSheetImportSchemas",
71
- name: "schemas",
72
- as: "googleSheetsImportSchemas"
73
- },
74
76
  {
75
77
  from: resolver.resolve("./runtime/types/googleSheetsApi"),
76
78
  name: "GoogleSheetsApiValues",
@@ -104,8 +106,8 @@ const module$1 = defineNuxtModule({
104
106
  ]);
105
107
  addServerImports([
106
108
  {
107
- from: "~/utils/googleSheetImportSchemas",
108
- name: "schemas",
109
+ from: "~/utils/googleSheetsImportSchemas",
110
+ name: "googleSheetsImportSchemas",
109
111
  as: "googleSheetsImportSchemas"
110
112
  }
111
113
  ]);
@@ -0,0 +1,212 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from "vue";
3
+ import { queryCollection, useAsyncData, useToast } from "#imports";
4
+ import { useGoogleSheetsImport } from "../composables/useGoogleSheetsImport";
5
+ import { copyTextWithSuccessToast } from "../utils/clipboard";
6
+ import { toCsvRow, toTsvRow } from "../utils/delimited";
7
+ import { flattenRecordToStringMap } from "../utils/pathMapping";
8
+ const toast = useToast();
9
+ const { getSchemaColumns } = useGoogleSheetsImport();
10
+ const queryCollectionAny = queryCollection;
11
+ function queryCollectionRows(collection) {
12
+ const query = queryCollectionAny(collection);
13
+ return query.all();
14
+ }
15
+ const selectedSchema = ref("");
16
+ const {
17
+ data: rowsData,
18
+ pending,
19
+ error: loadError,
20
+ status,
21
+ execute,
22
+ clear
23
+ } = useAsyncData(
24
+ async () => {
25
+ if (!selectedSchema.value) {
26
+ return [];
27
+ }
28
+ return await queryCollectionRows(selectedSchema.value);
29
+ },
30
+ {
31
+ immediate: false,
32
+ default: () => []
33
+ }
34
+ );
35
+ const {
36
+ data: schemaNames,
37
+ error: schemaNamesError
38
+ } = useAsyncData(
39
+ "google-sheets-import-export-schemas",
40
+ async () => {
41
+ const response = await getSchemaColumns();
42
+ return response.schemas ?? [];
43
+ },
44
+ {
45
+ default: () => []
46
+ }
47
+ );
48
+ const rows = computed(() => rowsData.value ?? []);
49
+ const error = computed(() => loadError.value?.message ?? schemaNamesError.value?.message ?? "");
50
+ watch(selectedSchema, () => {
51
+ clear();
52
+ });
53
+ const schemaOptions = computed(() => (schemaNames.value ?? []).sort((left, right) => left.localeCompare(right)).map((schema) => ({ label: schema, value: schema })));
54
+ const headers = computed(() => {
55
+ const all = /* @__PURE__ */ new Set();
56
+ for (const row of rows.value) {
57
+ flattenRecordToStringMap(row).forEach((_, key) => all.add(key));
58
+ }
59
+ return Array.from(all).sort((left, right) => left.localeCompare(right));
60
+ });
61
+ const csvText = computed(() => {
62
+ if (!headers.value.length) {
63
+ return "";
64
+ }
65
+ const lines = [];
66
+ lines.push(toCsvRow(headers.value));
67
+ for (const row of rows.value) {
68
+ const flat = flattenRecordToStringMap(row);
69
+ const values = headers.value.map((header) => flat.get(header) ?? "");
70
+ lines.push(toCsvRow(values));
71
+ }
72
+ return `${lines.join("\n")}
73
+ `;
74
+ });
75
+ const tsvText = computed(() => {
76
+ if (!headers.value.length) {
77
+ return "";
78
+ }
79
+ const lines = [];
80
+ lines.push(toTsvRow(headers.value));
81
+ for (const row of rows.value) {
82
+ const flat = flattenRecordToStringMap(row);
83
+ const values = headers.value.map((header) => flat.get(header) ?? "");
84
+ lines.push(toTsvRow(values));
85
+ }
86
+ return `${lines.join("\n")}
87
+ `;
88
+ });
89
+ const previewText = computed(() => {
90
+ if (!csvText.value) {
91
+ return "";
92
+ }
93
+ return csvText.value.split("\n").slice(0, 11).join("\n");
94
+ });
95
+ async function loadRecords() {
96
+ if (!selectedSchema.value) {
97
+ return;
98
+ }
99
+ await execute();
100
+ if (status.value === "success") {
101
+ toast.add({
102
+ title: "Export ready",
103
+ description: `Loaded ${rows.value.length} row(s) from collection ${selectedSchema.value}.`,
104
+ color: "success"
105
+ });
106
+ }
107
+ }
108
+ async function copyCsv() {
109
+ await copyTextWithSuccessToast({
110
+ text: tsvText.value,
111
+ toast,
112
+ description: "Tab-separated rows copied for Google Sheets paste.",
113
+ title: "Copied"
114
+ });
115
+ }
116
+ function downloadCsv() {
117
+ if (!csvText.value) {
118
+ return;
119
+ }
120
+ const blob = new Blob([csvText.value], { type: "text/csv;charset=utf-8" });
121
+ const url = URL.createObjectURL(blob);
122
+ const link = document.createElement("a");
123
+ link.href = url;
124
+ link.download = `${selectedSchema.value || "export"}.csv`;
125
+ link.click();
126
+ URL.revokeObjectURL(url);
127
+ }
128
+ </script>
129
+
130
+ <template>
131
+ <div class="space-y-4">
132
+ <UAlert
133
+ title="Export Existing Content"
134
+ description="Load records from a Nuxt Content collection and export them as CSV for Google Sheets."
135
+ color="neutral"
136
+ variant="subtle"
137
+ />
138
+
139
+ <UAlert
140
+ v-if="error"
141
+ title="Could not load collection"
142
+ :description="error"
143
+ color="error"
144
+ variant="subtle"
145
+ />
146
+
147
+ <UFormField
148
+ label="Collection schema"
149
+ name="schema"
150
+ description="Pick the schema/collection to export."
151
+ >
152
+ <USelect
153
+ v-model="selectedSchema"
154
+ :items="schemaOptions"
155
+ value-key="value"
156
+ class="w-full max-w-sm"
157
+ icon="i-heroicons-cube-20-solid"
158
+ />
159
+ </UFormField>
160
+
161
+ <div class="flex flex-wrap gap-2">
162
+ <UButton
163
+ label="Load records"
164
+ icon="i-heroicons-arrow-down-tray-20-solid"
165
+ :disabled="!selectedSchema"
166
+ :loading="pending"
167
+ @click="loadRecords"
168
+ />
169
+ <UButton
170
+ label="Copy for Google Sheets"
171
+ icon="i-heroicons-clipboard-document-20-solid"
172
+ color="neutral"
173
+ variant="subtle"
174
+ :disabled="!tsvText"
175
+ @click="copyCsv"
176
+ />
177
+ <UButton
178
+ label="Download .csv"
179
+ icon="i-heroicons-document-arrow-down-20-solid"
180
+ color="neutral"
181
+ variant="subtle"
182
+ :disabled="!csvText"
183
+ @click="downloadCsv"
184
+ />
185
+ </div>
186
+
187
+ <UAlert
188
+ v-if="status === 'success'"
189
+ title="Export prepared"
190
+ :description="`${rows.length} row(s), ${headers.length} column(s).`"
191
+ color="success"
192
+ variant="subtle"
193
+ />
194
+
195
+ <UCollapsible
196
+ v-if="previewText"
197
+ class="flex flex-col gap-2 w-full"
198
+ >
199
+ <UButton
200
+ label="Preview CSV (first 10 rows)"
201
+ color="neutral"
202
+ variant="subtle"
203
+ trailing-icon="i-lucide-chevron-down"
204
+ block
205
+ />
206
+
207
+ <template #content>
208
+ <pre class="text-xs whitespace-pre-wrap">{{ previewText }}</pre>
209
+ </template>
210
+ </UCollapsible>
211
+ </div>
212
+ </template>
@@ -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>
@@ -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,45 @@
1
+ <script setup>
2
+ import { ref } from "vue";
3
+ const sheetsList = ref([
4
+ { id: "1NKS0cTX6u5urtgQ3Q4Z2motiR2-9JmyPxcd05yVc1bc", label: "Metzner" },
5
+ { id: "1tGZCEoiikXfg3mOpfVWWTS1SSSsj18xv6Z3owrnnt4s", label: "Example Sheet" }
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
+ ]);
24
+ </script>
25
+
26
+ <template>
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>
44
+ </UContainer>
45
+ </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;
@@ -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
+ }
@@ -6,8 +6,8 @@ const querySchema = z.object({
6
6
  spreadsheetId: z.string().length(44)
7
7
  });
8
8
  export default defineEventHandler(async (event) => {
9
- const { googleSheetsImport } = useRuntimeConfig();
10
- const apiKey = googleSheetsImport?.googleApiKeyRuntimeKey;
9
+ const config = useRuntimeConfig(event);
10
+ const apiKey = config.googleApiKey;
11
11
  const { spreadsheetId } = await getValidatedQuery(event, (query) => querySchema.parse(query));
12
12
  if (!apiKey || typeof apiKey !== "string") {
13
13
  throw createError({ statusCode: 500, statusMessage: `Missing Google API key in nuxt.config googleSheetsImport: { googleApiKeyRuntimeKey: '${apiKey}' }` });
@@ -10,8 +10,8 @@ const bodySchema = z.object({
10
10
  });
11
11
  export default defineEventHandler(async (event) => {
12
12
  const body = bodySchema.parse(await readBody(event));
13
- const { googleSheetsImport } = useRuntimeConfig();
14
- const apiKey = googleSheetsImport?.googleApiKeyRuntimeKey;
13
+ const config = useRuntimeConfig(event);
14
+ const apiKey = config.googleApiKey;
15
15
  if (!apiKey || typeof apiKey !== "string") {
16
16
  throw createError({ statusCode: 500, statusMessage: `Missing Google API key in nuxt.config googleSheetsImport: { googleApiKeyRuntimeKey: '${apiKey}' }` });
17
17
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { parseHeaderPath, setDeep } from "../../../runtime/app/utils/pathMapping";
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) {
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.8",
4
4
  "description": "Schema-driven Google Sheets import module for Nuxt Content",
5
5
  "repository": "tribeweb/nuxt-google-sheets-import",
6
6
  "license": "MIT",
@@ -23,9 +23,12 @@
23
23
  "dist"
24
24
  ],
25
25
  "workspaces": [
26
- "playground"
26
+ "playground",
27
+ "docs"
27
28
  ],
28
29
  "scripts": {
30
+ "docs:dev": "cd docs && nuxt dev --extends docus",
31
+ "docs:build": "nuxt build docs --extends docus",
29
32
  "prepack": "nuxt-module-build build",
30
33
  "dev": "npm run dev:prepare && nuxt dev playground",
31
34
  "dev:build": "nuxt build playground",
@@ -1,14 +0,0 @@
1
- <script setup>
2
- import { ref } from "vue";
3
- const sheetsList = ref([
4
- { id: "1NKS0cTX6u5urtgQ3Q4Z2motiR2-9JmyPxcd05yVc1bc", label: "Metzner" },
5
- { id: "1tGZCEoiikXfg3mOpfVWWTS1SSSsj18xv6Z3owrnnt4s", label: "Example Sheet" }
6
- ]);
7
- </script>
8
-
9
- <template>
10
- <UContainer>
11
- <GoogleSheetsImportSchemaGuide />
12
- <GoogleSheetsImportSource :google-sheets="sheetsList" />
13
- </UContainer>
14
- </template>