@single-studio/plugin-sheets 0.6.2 → 0.7.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 +17 -8
- package/package.json +2 -2
- package/src/index.js +36 -13
- package/src/sheet.js +24 -0
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
|
-
//
|
|
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
|
|
32
|
-
|
|
31
|
+
The operator's board asks for **which sheet**, **an API key** and **how often to
|
|
32
|
+
read**.
|
|
33
33
|
|
|
34
|
-
The **
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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.
|
|
3
|
+
"version": "0.7.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.
|
|
36
|
+
"@single-studio/core": "^0.7.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
|
-
*
|
|
85
|
+
* The sheet this studio was written against: which spreadsheet, and which cells.
|
|
86
86
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
-
*
|
|
94
|
-
*
|
|
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
|
|
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
|
|
133
|
-
//
|
|
134
|
-
|
|
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.')
|