@single-studio/plugin-sheets 0.6.2 → 0.8.0

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
@@ -11,7 +11,7 @@ npm i @single-studio/plugin-sheets
11
11
  ```js
12
12
  import { sheets, GoogleSheetsHandler } from '@single-studio/plugin-sheets'
13
13
 
14
- // Which sheet, and which cells, are the *studio's* — see below.
14
+ // The range is yours. The id is a default the operator may replace — see below.
15
15
  const SHEET = { id: '1AbC…', range: 'Standings!A1:D20' }
16
16
 
17
17
  class MyShow extends GoogleSheetsHandler {
@@ -28,14 +28,23 @@ your graphics as `teamName`.
28
28
 
29
29
  ## What the operator sets, and what you set
30
30
 
31
- The operator's board asks for **an API key** and **how often to read**. That is
32
- deliberately all of it.
31
+ The operator's board asks for **which sheet**, **an API key** and **how often to
32
+ read**.
33
33
 
34
- The **spreadsheet id and range are yours**, passed to the factory. A range is not a
35
- preference: your graphics have code expecting columns in an order, and a range that
36
- does not match does not show a different view of the same data — it shows the wrong
37
- column, confidently, with no error anywhere. Both are also already decided by the time
38
- anyone opens the panel, so offering them is offering a choice that has been made.
34
+ The **range is yours** and cannot be overridden. A range is not a preference: your
35
+ graphics have code expecting columns in an order, and a range that does not match does
36
+ not show a different view of the same data — it shows the wrong column, confidently,
37
+ with no error anywhere.
38
+
39
+ The **id is a default the operator may replace**. That is a different question from
40
+ the range, and it took a broadcaster asking to see why: the range is the _shape_ of a
41
+ sheet, the id is _which copy of that shape_. A team running two setups off duplicates
42
+ of one layout needs each pointed at its own, and the graphics cannot tell the
43
+ difference — which is exactly what makes it safe where a range is not.
44
+
45
+ Leaving the field blank uses the id you shipped, so changing it in a later release
46
+ reaches everyone who never overrode it. An operator may paste the sheet's whole
47
+ address rather than its id; that works, because it is what people actually copy.
39
48
 
40
49
  ## The key
41
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@single-studio/plugin-sheets",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Google Sheets for Single Studio: a shared spreadsheet as a data source, with an API key and no backend.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -33,7 +33,7 @@
33
33
  "test": "vitest run"
34
34
  },
35
35
  "peerDependencies": {
36
- "@single-studio/core": "^0.6.0"
36
+ "@single-studio/core": "^0.8.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@single-studio/core": "workspace:*",
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { definePlugin, PluginHandler, PollingService } from '@single-studio/core/worker'
2
2
 
3
- import { explain, parse, urlFor } from './sheet.js'
3
+ import { explain, idFrom, parse, urlFor } from './sheet.js'
4
4
 
5
5
  export { explain, keyOf, parse, same, urlFor } from './sheet.js'
6
6
 
@@ -82,16 +82,22 @@ export class GoogleSheetsHandler extends PluginHandler {
82
82
  /**
83
83
  * @param {typeof GoogleSheetsHandler} [Handler]
84
84
  * @param {{ id?: string, range?: string, header?: boolean }} [sheet]
85
- * Which spreadsheet, and which cells. **Given by the studio, not by the operator.**
85
+ * The sheet this studio was written against: which spreadsheet, and which cells.
86
86
  *
87
- * A range is not a preference. The studio reading it has code that expects columns
88
- * in an order, and a range that does not match is not a different view of the same
89
- * data -- it is a graphic quietly showing the wrong column, which is the worst way
90
- * for this to fail. The same goes for the id: a studio ships knowing the shape of
91
- * the sheet it was written against.
87
+ * **The range is the studio's and cannot be overridden.** It is not a preference.
88
+ * The graphics reading it have code that expects columns in an order, and a range
89
+ * that does not match is not a different view of the same data -- it is a graphic
90
+ * quietly showing the wrong column, which is the worst way for this to fail.
92
91
  *
93
- * So the operator is asked for the one thing that is genuinely theirs and cannot
94
- * be shipped -- an API key, which is a credential.
92
+ * **The id is a default the operator may replace**, which is a different question
93
+ * from the range and was once wrongly treated as the same one. The range is the
94
+ * *shape* of a sheet; the id is *which copy of that shape*. A team running two
95
+ * setups off duplicates of one layout needs to point each at its own, and the
96
+ * graphics cannot tell the difference -- that is what makes it safe where a range
97
+ * is not.
98
+ *
99
+ * Leave the field on the panel blank and the studio's id is used, so a studio that
100
+ * ships a new sheet in a later release reaches everybody who never overrode it.
95
101
  */
96
102
  export const sheets = (Handler = GoogleSheetsHandler, sheet = {}) =>
97
103
  definePlugin({
@@ -110,7 +116,7 @@ export const sheets = (Handler = GoogleSheetsHandler, sheet = {}) =>
110
116
  },
111
117
  {
112
118
  type: 'note',
113
- text: 'Which sheet this reads, and which cells, come from the studio rather than from here. They have to match the graphics that read them, so they are not something to change during a show.',
119
+ text: 'Which cells this reads comes from the studio and cannot be changed here — the graphics expect those columns in that order. Which sheet it reads can: leave Spreadsheet blank for the one the studio ships, or paste another that uses the same layout.',
114
120
  },
115
121
  { type: 'link', href: 'https://console.cloud.google.com/apis/credentials', label: 'Google Cloud credentials' },
116
122
  {
@@ -120,6 +126,17 @@ export const sheets = (Handler = GoogleSheetsHandler, sheet = {}) =>
120
126
  { type: 'text', text: 'The first row is used as column names by default, so a heading of "Team Name" becomes teamName in your graphics.' },
121
127
  ],
122
128
  config: [
129
+ {
130
+ key: 'id',
131
+ label: 'Spreadsheet',
132
+ type: 'text',
133
+ // The studio's own id, greyed out rather than filled in. Filling it in would
134
+ // store it, and a stored copy is a copy that stops following the studio --
135
+ // a later release pointing at a new sheet would reach nobody who had ever
136
+ // opened this panel.
137
+ placeholder: sheet.id ?? 'The one this studio ships with',
138
+ help: 'Paste the sheet’s address from your browser, or just its id. Leave it blank to use the sheet this studio ships with.',
139
+ },
123
140
  {
124
141
  key: 'key',
125
142
  label: 'API key',
@@ -129,9 +146,15 @@ export const sheets = (Handler = GoogleSheetsHandler, sheet = {}) =>
129
146
  { key: 'every', label: 'Read every (seconds)', type: 'number', default: 10, help: 'Five is the floor. Google allows sixty reads a minute.' },
130
147
  ],
131
148
  create: (context) => {
132
- // The studio's sheet wins over anything stored, because there is nowhere to
133
- // store it from -- these are not fields on the panel.
134
- const plugin = new GoogleSheets({ ...context, config: { ...context.config, ...sheet } })
149
+ // The shape is the studio's and the copy is the operator's. `...sheet` puts the
150
+ // studio's range and header beyond reach of the panel; the line after it hands
151
+ // the id back, because that one *is* on the panel now.
152
+ //
153
+ // Blank falls through to the studio's rather than being stored as empty, so an
154
+ // operator who opens the panel to change the read interval does not silently
155
+ // pin the sheet to whatever it was that day.
156
+ const chosen = idFrom(context.config?.id) || sheet.id || ''
157
+ const plugin = new GoogleSheets({ ...context, config: { ...context.config, ...sheet, id: chosen } })
135
158
 
136
159
  new Handler({ ...context, plugin }).attach(plugin.events)
137
160
 
package/src/sheet.js CHANGED
@@ -92,6 +92,30 @@ export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
92
92
  * @param {string} options.range A1 notation, e.g. `Standings!A1:D20`.
93
93
  * @param {string} options.key
94
94
  */
95
+ /**
96
+ * The spreadsheet id, from whatever somebody pasted.
97
+ *
98
+ * Nobody copies the id. They copy the address bar, because that is where the sheet
99
+ * is -- and a bare id is a 44-character string in the middle of it that has to be
100
+ * found and trimmed by hand. Pasting the whole URL is the ordinary thing to do, so
101
+ * it is the thing that has to work; the alternative is a 404 on a show and an
102
+ * operator with no reason to suspect the field they filled in correctly.
103
+ *
104
+ * Anything that is not a Sheets URL is handed back trimmed and used as-is, so a bare
105
+ * id still works and a typo still reaches Google to be refused with a real message
106
+ * rather than being second-guessed here.
107
+ */
108
+ export function idFrom(value) {
109
+ const text = String(value ?? '').trim()
110
+
111
+ if (!text) return ''
112
+
113
+ // /spreadsheets/d/<id>/edit#gid=0, and every other tail Google hangs off it.
114
+ const inUrl = /\/spreadsheets\/d\/([A-Za-z0-9_-]+)/.exec(text)
115
+
116
+ return inUrl ? inUrl[1] : text
117
+ }
118
+
95
119
  export function urlFor({ id, range, key }) {
96
120
  if (!id) throw new Error('A spreadsheet id is needed.')
97
121
  if (!key) throw new Error('A Google API key is needed.')