@mcpher/gas-fakes 2.0.4 → 2.0.6
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 +14 -0
- package/package.json +1 -1
- package/src/cli/setup.js +9 -2
- package/src/index.js +1 -0
- package/src/services/chartsapp/app.js +8 -0
- package/src/services/chartsapp/fakechartsapp.js +64 -0
- package/src/services/enums/chartsenums.js +24 -0
- package/src/services/enums/sheetsenums.js +1 -1
- package/src/services/formapp/fakecheckboxgriditem.js +30 -0
- package/src/services/formapp/fakecheckboxitem.js +23 -0
- package/src/services/formapp/fakedateitem.js +67 -0
- package/src/services/formapp/fakedurationitem.js +54 -0
- package/src/services/formapp/fakeform.js +90 -0
- package/src/services/formapp/fakegriditem.js +31 -1
- package/src/services/formapp/fakeitemresponse.js +5 -1
- package/src/services/formapp/fakelistitem.js +25 -0
- package/src/services/formapp/fakemultiplechoiceitem.js +27 -0
- package/src/services/formapp/fakescaleitem.js +23 -0
- package/src/services/formapp/faketextitem.js +35 -2
- package/src/services/formapp/faketimeitem.js +53 -0
- package/src/services/formapp/formitems.js +4 -0
- package/src/services/spreadsheetapp/fakeembeddedchart.js +87 -0
- package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +154 -0
- package/src/services/spreadsheetapp/fakesheet.js +59 -4
- package/src/support/auth.js +4 -1
- package/src/support/sxauth.js +2 -2
package/README.md
CHANGED
|
@@ -54,7 +54,20 @@ The optional `gasfakes.json` file holds various location and behavior parameters
|
|
|
54
54
|
| properties | string | /tmp/gas-fakes/properties | gas-fakes uses a local file to emulate apps script's PropertiesService. This is where it should put the files. You may want to put it somewhere other than /tmp to avoid accidental deletion, but don't put it in a place that'll get commited to public git repo |
|
|
55
55
|
| scriptId | string | from clasp, or some random value | If you have a clasp file, it'll pick up the scriptId from there. If not you can enter your scriptId manually, or just leave it to create a fake one. It's use for the moment is to return something useful from ScriptApp.getScriptId() and to partition the cache and properties stores |
|
|
56
56
|
|
|
57
|
+
### Troubleshooting: Missing Environment Tags
|
|
57
58
|
|
|
59
|
+
If you see a warning or error like `Project '...' lacks an 'environment' tag`, it means your Google Cloud Organization has a policy requiring projects to be designated with an environment tag (e.g., `Development`, `Production`).
|
|
60
|
+
|
|
61
|
+
To resolve this, you need to bind an environment tag to your project. Replace `YOUR_ORG_ID` and `YOUR_PROJECT_ID` with your actual identifiers:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Bind the 'Development' environment tag to your project
|
|
65
|
+
gcloud resource-manager tags bindings create \
|
|
66
|
+
--tag-value=YOUR_ORG_ID/environment/Development \
|
|
67
|
+
--parent=//cloudresourcemanager.googleapis.com/projects/YOUR_PROJECT_ID
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
*Note: The tag key `environment` and the value `Development` must already exist at the organization level. If they don't, you (or your admin) will need to create them first using `gcloud resource-manager tags keys create` and `gcloud resource-manager tags values create`.*
|
|
58
71
|
|
|
59
72
|
### Cloud Logging Integration
|
|
60
73
|
|
|
@@ -168,6 +181,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
168
181
|
- [oddities](oddities.md) - a collection of oddities uncovered during this project
|
|
169
182
|
- [named colors](named-colors.md)
|
|
170
183
|
- [sandbox](sandbox.md)
|
|
184
|
+
- [senstive scopes](senstive_scopes.md)
|
|
171
185
|
- [using apps script libraries with gas-fakes](libraries.md)
|
|
172
186
|
- [how libhandler works](libhandler.md)
|
|
173
187
|
- [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
|
package/package.json
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"name": "@mcpher/gas-fakes",
|
|
35
35
|
"author": "bruce mcpherson",
|
|
36
|
-
"version": "2.0.
|
|
36
|
+
"version": "2.0.6",
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"main": "main.js",
|
|
39
39
|
"description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
|
package/src/cli/setup.js
CHANGED
|
@@ -483,7 +483,14 @@ export async function authenticateUser() {
|
|
|
483
483
|
scopes += (extraScopes.startsWith(",") ? "" : ",") + extraScopes;
|
|
484
484
|
}
|
|
485
485
|
*/
|
|
486
|
-
const scopes = Array.from(
|
|
486
|
+
const scopes = Array.from(
|
|
487
|
+
new Set([
|
|
488
|
+
...(DEFAULT_SCOPES || "").split(","),
|
|
489
|
+
...(EXTRA_SCOPES || "").split(","),
|
|
490
|
+
])
|
|
491
|
+
)
|
|
492
|
+
.filter((s) => s)
|
|
493
|
+
.join(",");
|
|
487
494
|
const driveAccessFlag = "--enable-gdrive-access";
|
|
488
495
|
|
|
489
496
|
console.log(`...requesting scopes ${scopes}`);
|
|
@@ -630,7 +637,7 @@ export async function authenticateUser() {
|
|
|
630
637
|
// Service Account Lifecycle
|
|
631
638
|
let existing_sa = false
|
|
632
639
|
try {
|
|
633
|
-
execSync(`gcloud iam service-accounts describe "${sa_email}"`, { shell: true });
|
|
640
|
+
execSync(`gcloud iam service-accounts describe "${sa_email}"`, { stdio: "ignore", shell: true });
|
|
634
641
|
existing_sa = true;
|
|
635
642
|
} catch (error) {
|
|
636
643
|
/* ignore */
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import './services/utilities/app.js'
|
|
|
8
8
|
import './services/spreadsheetapp/app.js'
|
|
9
9
|
import './services/gmailapp/app.js'
|
|
10
10
|
import './services/calendarapp/app.js'
|
|
11
|
+
import './services/chartsapp/app.js'
|
|
11
12
|
import './services/session/app.js'
|
|
12
13
|
import './services/advdrive/app.js'
|
|
13
14
|
import './services/advsheets/app.js'
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* the idea here is to create an empty global entry for the singleton
|
|
3
|
+
* but only load it when it is actually used.
|
|
4
|
+
*/
|
|
5
|
+
import { lazyLoaderApp } from '../common/lazyloader.js'
|
|
6
|
+
import { newFakeChartsApp as maker } from './fakechartsapp.js'
|
|
7
|
+
let _app = null;
|
|
8
|
+
_app = lazyLoaderApp(_app, 'Charts', maker)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Proxies } from "../../support/proxies.js";
|
|
2
|
+
import { notYetImplemented } from "../../support/helpers.js";
|
|
3
|
+
import * as Enums from "../enums/chartsenums.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* create a new FakeChartsApp instance
|
|
7
|
+
* @param {...any} args
|
|
8
|
+
* @returns {FakeChartsApp}
|
|
9
|
+
*/
|
|
10
|
+
export const newFakeChartsApp = (...args) => {
|
|
11
|
+
return Proxies.guard(new FakeChartsApp(...args));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* basic fake FakeChartsApp
|
|
16
|
+
* @class FakeChartsApp
|
|
17
|
+
* @returns {FakeChartsApp}
|
|
18
|
+
*/
|
|
19
|
+
export class FakeChartsApp {
|
|
20
|
+
constructor() {
|
|
21
|
+
const enumProps = [
|
|
22
|
+
"ChartType", // ChartType An enumeration of the possible chart types.
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// import all known enums as props of chartsapp
|
|
26
|
+
enumProps.forEach((f) => {
|
|
27
|
+
this[f] = Enums[f];
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const props = [
|
|
31
|
+
"newAreaChart",
|
|
32
|
+
"newBarChart",
|
|
33
|
+
"newColumnChart",
|
|
34
|
+
"newComboChart",
|
|
35
|
+
"newHistogramChart",
|
|
36
|
+
"newLineChart",
|
|
37
|
+
"newPieChart",
|
|
38
|
+
"newScatterChart",
|
|
39
|
+
"newSteppedAreaChart",
|
|
40
|
+
"newWaterfallChart",
|
|
41
|
+
"newScorecardChart",
|
|
42
|
+
"newRadarChart",
|
|
43
|
+
"newGaugeChart",
|
|
44
|
+
"newOrgChart",
|
|
45
|
+
"newTimelineChart",
|
|
46
|
+
"newTreeMapChart",
|
|
47
|
+
"newTableChart",
|
|
48
|
+
"newCandlestickChart",
|
|
49
|
+
"newGeoMapChart",
|
|
50
|
+
"newBubbleChart",
|
|
51
|
+
"newDataTable",
|
|
52
|
+
"newTextStyle",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
props.forEach((f) => {
|
|
56
|
+
this[f] = () => {
|
|
57
|
+
return notYetImplemented(f);
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
toString() {
|
|
62
|
+
return "Charts";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { newFakeGasenum } from "@mcpher/fake-gasenum";
|
|
2
|
+
|
|
3
|
+
export const ChartType = newFakeGasenum([
|
|
4
|
+
"AREA", // Enum Area chart.
|
|
5
|
+
"BAR", // Enum Bar chart.
|
|
6
|
+
"COLUMN", // Enum Column chart.
|
|
7
|
+
"COMBO", // Enum Combo chart.
|
|
8
|
+
"HISTOGRAM", // Enum Histogram chart.
|
|
9
|
+
"LINE", // Enum Line chart.
|
|
10
|
+
"PIE", // Enum Pie chart.
|
|
11
|
+
"SCATTER", // Enum Scatter chart.
|
|
12
|
+
"STEPPED_AREA", // Enum Stepped area chart.
|
|
13
|
+
"WATERFALL", // Enum Waterfall chart.
|
|
14
|
+
"SCORECARD", // Enum Scorecard chart.
|
|
15
|
+
"RADAR", // Enum Radar chart.
|
|
16
|
+
"GAUGE", // Enum Gauge chart.
|
|
17
|
+
"ORG", // Enum Org chart.
|
|
18
|
+
"TIMELINE", // Enum Timeline chart.
|
|
19
|
+
"TREE_MAP", // Enum Tree map chart.
|
|
20
|
+
"TABLE", // Enum Table chart.
|
|
21
|
+
"CANDLESTICK", // Enum Candlestick chart.
|
|
22
|
+
"GEOMAP", // Enum Geo map chart.
|
|
23
|
+
"BUBBLE", // Enum Bubble chart.
|
|
24
|
+
]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { newFakeGasenum} from "@mcpher/fake-gasenum";
|
|
2
|
+
import { newFakeGasenum } from "@mcpher/fake-gasenum";
|
|
3
3
|
export const AutoFillSeries = newFakeGasenum(["DEFAULT_SERIES", "ALTERNATE_SERIES"])
|
|
4
4
|
export const BandingTheme = newFakeGasenum([
|
|
5
5
|
"LIGHT_GREY", // Enum A light grey banding theme.
|
|
@@ -4,6 +4,7 @@ import { registerFormItem } from './formitemregistry.js';
|
|
|
4
4
|
import { signatureArgs } from '../../support/helpers.js';
|
|
5
5
|
import { Utils } from '../../support/utils.js';
|
|
6
6
|
import { ItemType } from '../enums/formsenums.js';
|
|
7
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
7
8
|
|
|
8
9
|
export const newFakeCheckboxGridItem = (...args) => {
|
|
9
10
|
return Proxies.guard(new FakeCheckboxGridItem(...args));
|
|
@@ -19,6 +20,35 @@ export class FakeCheckboxGridItem extends FakeFormItem {
|
|
|
19
20
|
super(form, itemId);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new ItemResponse for this checkbox grid item.
|
|
25
|
+
* @param {string[][]} responses a two-dimensional array of responses, where each inner array represents the selected columns for a row
|
|
26
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
27
|
+
*/
|
|
28
|
+
createResponse(responses) {
|
|
29
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxGridItem.createResponse');
|
|
30
|
+
if (nargs !== 1 || !Utils.is.array(responses) || !responses.every(Utils.is.array)) {
|
|
31
|
+
matchThrow('Invalid arguments: expected a two-dimensional string array.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const questions = this.__resource.questionGroupItem?.questions || [];
|
|
35
|
+
if (responses.length !== questions.length) {
|
|
36
|
+
throw new Error(`The number of responses (${responses.length}) must match the number of rows (${questions.length}).`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const answers = responses.map((rowResponses, index) => {
|
|
40
|
+
const questionId = questions[index].questionId;
|
|
41
|
+
return {
|
|
42
|
+
questionId,
|
|
43
|
+
textAnswers: {
|
|
44
|
+
answers: rowResponses.map(value => ({ value }))
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return newFakeItemResponse(this, answers);
|
|
50
|
+
}
|
|
51
|
+
|
|
22
52
|
/**
|
|
23
53
|
* Gets the values for every column in the grid.
|
|
24
54
|
* @returns {string[]} an array of column values
|
|
@@ -6,6 +6,7 @@ const { is } = Utils;
|
|
|
6
6
|
import { registerFormItem } from './formitemregistry.js';
|
|
7
7
|
import { newFakeChoice } from './fakechoice.js';
|
|
8
8
|
import { ItemType } from '../enums/formsenums.js';
|
|
9
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
9
10
|
|
|
10
11
|
export const newFakeCheckboxItem = (...args) => {
|
|
11
12
|
return Proxies.guard(new FakeCheckboxItem(...args));
|
|
@@ -25,6 +26,28 @@ export class FakeCheckboxItem extends FakeChoiceItem {
|
|
|
25
26
|
super(form, itemId);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new ItemResponse for this checkbox item.
|
|
31
|
+
* @param {string[]} responses the selected choices
|
|
32
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
33
|
+
*/
|
|
34
|
+
createResponse(responses) {
|
|
35
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxItem.createResponse');
|
|
36
|
+
if (nargs !== 1 || !Utils.is.array(responses) || !responses.every(Utils.is.string)) {
|
|
37
|
+
matchThrow('Invalid arguments: expected a string array.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
41
|
+
const answers = [{
|
|
42
|
+
questionId,
|
|
43
|
+
textAnswers: {
|
|
44
|
+
answers: responses.map(value => ({ value }))
|
|
45
|
+
}
|
|
46
|
+
}];
|
|
47
|
+
|
|
48
|
+
return newFakeItemResponse(this, answers);
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
toString() {
|
|
29
52
|
return 'CheckboxItem';
|
|
30
53
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { FakeFormItem } from './fakeformitem.js';
|
|
3
|
+
import { registerFormItem } from './formitemregistry.js';
|
|
4
|
+
import { ItemType } from '../enums/formsenums.js';
|
|
5
|
+
import { signatureArgs } from '../../support/helpers.js';
|
|
6
|
+
import { Utils } from '../../support/utils.js';
|
|
7
|
+
const { is } = Utils;
|
|
8
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
9
|
+
|
|
10
|
+
export const newFakeDateItem = (...args) => {
|
|
11
|
+
return Proxies.guard(new FakeDateItem(...args));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @class FakeDateItem
|
|
16
|
+
* @see https://developers.google.com/apps-script/reference/forms/date-item
|
|
17
|
+
*/
|
|
18
|
+
export class FakeDateItem extends FakeFormItem {
|
|
19
|
+
constructor(...args) {
|
|
20
|
+
super(...args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new ItemResponse for this date item.
|
|
25
|
+
* @param {Date} date the date
|
|
26
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
27
|
+
*/
|
|
28
|
+
createResponse(date) {
|
|
29
|
+
const typeName = this.getType() === ItemType.DATETIME ? 'DateTimeItem' : 'DateItem';
|
|
30
|
+
const { nargs, matchThrow } = signatureArgs(arguments, `${typeName}.createResponse`);
|
|
31
|
+
if (nargs !== 1 || !is.date(date)) {
|
|
32
|
+
matchThrow('Invalid arguments: expected a Date object.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
36
|
+
|
|
37
|
+
// Format date as YYYY-MM-DD or YYYY-MM-DDTHH:mm:ssZ for API?
|
|
38
|
+
// Actually, the Forms API expects text answers.
|
|
39
|
+
// For DATE, it's "YYYY-MM-DD". For DATETIME, it's "YYYY-MM-DD HH:mm".
|
|
40
|
+
const year = date.getFullYear();
|
|
41
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
42
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
43
|
+
let value = `${year}-${month}-${day}`;
|
|
44
|
+
|
|
45
|
+
if (this.getType() === ItemType.DATETIME) {
|
|
46
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
47
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
48
|
+
value += ` ${hours}:${minutes}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const answers = [{
|
|
52
|
+
questionId,
|
|
53
|
+
textAnswers: {
|
|
54
|
+
answers: [{ value }]
|
|
55
|
+
}
|
|
56
|
+
}];
|
|
57
|
+
|
|
58
|
+
return newFakeItemResponse(this, answers);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
toString() {
|
|
62
|
+
return this.getType() === ItemType.DATETIME ? 'DateTimeItem' : 'DateItem';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
registerFormItem(ItemType.DATE, newFakeDateItem);
|
|
67
|
+
registerFormItem(ItemType.DATETIME, newFakeDateItem);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { FakeFormItem } from './fakeformitem.js';
|
|
3
|
+
import { registerFormItem } from './formitemregistry.js';
|
|
4
|
+
import { ItemType } from '../enums/formsenums.js';
|
|
5
|
+
import { signatureArgs } from '../../support/helpers.js';
|
|
6
|
+
import { Utils } from '../../support/utils.js';
|
|
7
|
+
const { is } = Utils;
|
|
8
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
9
|
+
|
|
10
|
+
export const newFakeDurationItem = (...args) => {
|
|
11
|
+
return Proxies.guard(new FakeDurationItem(...args));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @class FakeDurationItem
|
|
16
|
+
* @see https://developers.google.com/apps-script/reference/forms/duration-item
|
|
17
|
+
*/
|
|
18
|
+
export class FakeDurationItem extends FakeFormItem {
|
|
19
|
+
constructor(...args) {
|
|
20
|
+
super(...args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new ItemResponse for this duration item.
|
|
25
|
+
* @param {number} hours the hours
|
|
26
|
+
* @param {number} minutes the minutes
|
|
27
|
+
* @param {number} seconds the seconds
|
|
28
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
29
|
+
*/
|
|
30
|
+
createResponse(hours, minutes, seconds) {
|
|
31
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'DurationItem.createResponse');
|
|
32
|
+
if (nargs !== 3 || !is.number(hours) || !is.number(minutes) || !is.number(seconds)) {
|
|
33
|
+
matchThrow('Invalid arguments: expected three numbers.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
37
|
+
const value = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
38
|
+
|
|
39
|
+
const answers = [{
|
|
40
|
+
questionId,
|
|
41
|
+
textAnswers: {
|
|
42
|
+
answers: [{ value }]
|
|
43
|
+
}
|
|
44
|
+
}];
|
|
45
|
+
|
|
46
|
+
return newFakeItemResponse(this, answers);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
toString() {
|
|
50
|
+
return 'DurationItem';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
registerFormItem(ItemType.DURATION, newFakeDurationItem);
|
|
@@ -12,6 +12,9 @@ import { newFakeCheckboxItem } from './fakecheckboxitem.js';
|
|
|
12
12
|
import { newFakeListItem } from './fakelistitem.js';
|
|
13
13
|
import { newFakePageBreakItem } from './fakepagebreakitem.js';
|
|
14
14
|
import { newFakeTextItem } from './faketextitem.js';
|
|
15
|
+
import { newFakeDateItem } from './fakedateitem.js';
|
|
16
|
+
import { newFakeTimeItem } from './faketimeitem.js';
|
|
17
|
+
import { newFakeDurationItem } from './fakedurationitem.js';
|
|
15
18
|
import { signatureArgs } from '../../support/helpers.js';
|
|
16
19
|
import { Utils } from '../../support/utils.js';
|
|
17
20
|
const { is } = Utils
|
|
@@ -261,6 +264,93 @@ export class FakeForm {
|
|
|
261
264
|
return this.__addItem(itemResource, newFakeTextItem);
|
|
262
265
|
}
|
|
263
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Appends a new question item that allows the respondent to enter a paragraph of text.
|
|
269
|
+
* @returns {import('./faketextitem.js').FakeTextItem} The new paragraph text item.
|
|
270
|
+
*/
|
|
271
|
+
addParagraphTextItem() {
|
|
272
|
+
const itemResource = {
|
|
273
|
+
questionItem: {
|
|
274
|
+
question: {
|
|
275
|
+
textQuestion: {
|
|
276
|
+
paragraph: true,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
return this.__addItem(itemResource, newFakeTextItem);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Appends a new question item that allows the respondent to choose a date.
|
|
286
|
+
* @returns {import('./fakedateitem.js').FakeDateItem} The new date item.
|
|
287
|
+
*/
|
|
288
|
+
addDateItem() {
|
|
289
|
+
const itemResource = {
|
|
290
|
+
questionItem: {
|
|
291
|
+
question: {
|
|
292
|
+
dateQuestion: {
|
|
293
|
+
includeYear: true,
|
|
294
|
+
includeTime: false,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
return this.__addItem(itemResource, newFakeDateItem);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Appends a new question item that allows the respondent to choose a date and time.
|
|
304
|
+
* @returns {import('./fakedateitem.js').FakeDateItem} The new date-time item.
|
|
305
|
+
*/
|
|
306
|
+
addDateTimeItem() {
|
|
307
|
+
const itemResource = {
|
|
308
|
+
questionItem: {
|
|
309
|
+
question: {
|
|
310
|
+
dateQuestion: {
|
|
311
|
+
includeYear: true,
|
|
312
|
+
includeTime: true,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
return this.__addItem(itemResource, newFakeDateItem);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Appends a new question item that allows the respondent to choose a time.
|
|
322
|
+
* @returns {import('./faketimeitem.js').FakeTimeItem} The new time item.
|
|
323
|
+
*/
|
|
324
|
+
addTimeItem() {
|
|
325
|
+
const itemResource = {
|
|
326
|
+
questionItem: {
|
|
327
|
+
question: {
|
|
328
|
+
timeQuestion: {
|
|
329
|
+
duration: false,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
return this.__addItem(itemResource, newFakeTimeItem);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Appends a new question item that allows the respondent to choose a duration.
|
|
339
|
+
* @returns {import('./fakedurationitem.js').FakeDurationItem} The new duration item.
|
|
340
|
+
*/
|
|
341
|
+
addDurationItem() {
|
|
342
|
+
const itemResource = {
|
|
343
|
+
questionItem: {
|
|
344
|
+
question: {
|
|
345
|
+
timeQuestion: {
|
|
346
|
+
duration: true,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
return this.__addItem(itemResource, newFakeDurationItem);
|
|
352
|
+
}
|
|
353
|
+
|
|
264
354
|
/**
|
|
265
355
|
* Gets the ID of the form's response destination.
|
|
266
356
|
* @returns {string | null} The destination ID, or null if no destination is set.
|
|
@@ -4,6 +4,7 @@ import { registerFormItem } from './formitemregistry.js';
|
|
|
4
4
|
import { signatureArgs } from '../../support/helpers.js';
|
|
5
5
|
import { Utils } from '../../support/utils.js';
|
|
6
6
|
import { ItemType } from '../enums/formsenums.js';
|
|
7
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
7
8
|
|
|
8
9
|
export const newFakeGridItem = (...args) => {
|
|
9
10
|
return Proxies.guard(new FakeGridItem(...args));
|
|
@@ -19,6 +20,35 @@ export class FakeGridItem extends FakeFormItem {
|
|
|
19
20
|
super(form, itemId);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new ItemResponse for this grid item.
|
|
25
|
+
* @param {string[]} responses an array of responses, where each element represents the selected column for a row
|
|
26
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
27
|
+
*/
|
|
28
|
+
createResponse(responses) {
|
|
29
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'GridItem.createResponse');
|
|
30
|
+
if (nargs !== 1 || !Utils.is.array(responses) || !responses.every(Utils.is.string)) {
|
|
31
|
+
matchThrow('Invalid arguments: expected a string array.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const questions = this.__resource.questionGroupItem?.questions || [];
|
|
35
|
+
if (responses.length !== questions.length) {
|
|
36
|
+
throw new Error(`The number of responses (${responses.length}) must match the number of rows (${questions.length}).`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const answers = responses.map((rowResponse, index) => {
|
|
40
|
+
const questionId = questions[index].questionId;
|
|
41
|
+
return {
|
|
42
|
+
questionId,
|
|
43
|
+
textAnswers: {
|
|
44
|
+
answers: [{ value: rowResponse }]
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return newFakeItemResponse(this, answers);
|
|
50
|
+
}
|
|
51
|
+
|
|
22
52
|
/**
|
|
23
53
|
* Gets the values for every column in the grid.
|
|
24
54
|
* @returns {string[]} an array of column values
|
|
@@ -40,7 +70,7 @@ export class FakeGridItem extends FakeFormItem {
|
|
|
40
70
|
if (!questions) {
|
|
41
71
|
return [];
|
|
42
72
|
}
|
|
43
|
-
return questions.map(question => question.rowQuestion?.title || null).filter(f=>f);
|
|
73
|
+
return questions.map(question => question.rowQuestion?.title || null).filter(f => f);
|
|
44
74
|
}
|
|
45
75
|
|
|
46
76
|
/**
|
|
@@ -78,7 +78,11 @@ export class FakeItemResponse {
|
|
|
78
78
|
);
|
|
79
79
|
|
|
80
80
|
if (allTextAnswers.length === 0) {
|
|
81
|
-
return '';
|
|
81
|
+
return itemType === 'CHECKBOX' ? [] : '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (itemType === 'CHECKBOX') {
|
|
85
|
+
return allTextAnswers.map(a => a.value);
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
// For items like grids, there can be multiple answer values. The live script joins them with a comma.
|
|
@@ -4,6 +4,9 @@ import { newFakeChoice } from './fakechoice.js';
|
|
|
4
4
|
import { registerFormItem } from './formitemregistry.js';
|
|
5
5
|
import { ItemType } from '../enums/formsenums.js';
|
|
6
6
|
import { Utils } from '../../support/utils.js';
|
|
7
|
+
import { signatureArgs } from '../../support/helpers.js';
|
|
8
|
+
const { is } = Utils;
|
|
9
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
7
10
|
|
|
8
11
|
export const newFakeListItem = (...args) => {
|
|
9
12
|
return Proxies.guard(new FakeListItem(...args));
|
|
@@ -18,6 +21,28 @@ export class FakeListItem extends FakeFormItem {
|
|
|
18
21
|
super(...args);
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new ItemResponse for this list item.
|
|
26
|
+
* @param {string} response the selected choice
|
|
27
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
28
|
+
*/
|
|
29
|
+
createResponse(response) {
|
|
30
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.createResponse');
|
|
31
|
+
if (nargs !== 1 || !is.string(response)) {
|
|
32
|
+
matchThrow('Invalid arguments: expected a string.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
36
|
+
const answers = [{
|
|
37
|
+
questionId,
|
|
38
|
+
textAnswers: {
|
|
39
|
+
answers: [{ value: response }]
|
|
40
|
+
}
|
|
41
|
+
}];
|
|
42
|
+
|
|
43
|
+
return newFakeItemResponse(this, answers);
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
/**
|
|
22
47
|
* Creates a new choice for this item.
|
|
23
48
|
* @param {string} value The value for the new choice.
|
|
@@ -2,6 +2,10 @@ import { Proxies } from '../../support/proxies.js';
|
|
|
2
2
|
import { FakeChoiceItem } from './fakechoiceitem.js';
|
|
3
3
|
import { registerFormItem } from './formitemregistry.js';
|
|
4
4
|
import { ItemType } from '../enums/formsenums.js';
|
|
5
|
+
import { signatureArgs } from '../../support/helpers.js';
|
|
6
|
+
import { Utils } from '../../support/utils.js';
|
|
7
|
+
const { is } = Utils;
|
|
8
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
5
9
|
|
|
6
10
|
export const newFakeMultipleChoiceItem = (form, itemId) => {
|
|
7
11
|
return Proxies.guard(new FakeMultipleChoiceItem(form, itemId));
|
|
@@ -16,6 +20,29 @@ export class FakeMultipleChoiceItem extends FakeChoiceItem {
|
|
|
16
20
|
constructor(form, itemId) {
|
|
17
21
|
super(form, itemId);
|
|
18
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new ItemResponse for this multiple choice item.
|
|
26
|
+
* @param {string} response the selected choice
|
|
27
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
28
|
+
*/
|
|
29
|
+
createResponse(response) {
|
|
30
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'MultipleChoiceItem.createResponse');
|
|
31
|
+
if (nargs !== 1 || !is.string(response)) {
|
|
32
|
+
matchThrow('Invalid arguments: expected a string.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
36
|
+
const answers = [{
|
|
37
|
+
questionId,
|
|
38
|
+
textAnswers: {
|
|
39
|
+
answers: [{ value: response }]
|
|
40
|
+
}
|
|
41
|
+
}];
|
|
42
|
+
|
|
43
|
+
return newFakeItemResponse(this, answers);
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
toString() {
|
|
20
47
|
return 'MultipleChoiceItem';
|
|
21
48
|
}
|
|
@@ -5,6 +5,7 @@ import { ItemType } from '../enums/formsenums.js';
|
|
|
5
5
|
import { signatureArgs } from '../../support/helpers.js';
|
|
6
6
|
import { Utils } from '../../support/utils.js';
|
|
7
7
|
const { is } = Utils;
|
|
8
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
8
9
|
|
|
9
10
|
export const newFakeScaleItem = (...args) => {
|
|
10
11
|
return Proxies.guard(new FakeScaleItem(...args));
|
|
@@ -20,6 +21,28 @@ export class FakeScaleItem extends FakeFormItem {
|
|
|
20
21
|
super(form, itemId);
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new ItemResponse for this scale item.
|
|
26
|
+
* @param {number} response the selected value
|
|
27
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
28
|
+
*/
|
|
29
|
+
createResponse(response) {
|
|
30
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ScaleItem.createResponse');
|
|
31
|
+
if (nargs !== 1 || !is.number(response)) {
|
|
32
|
+
matchThrow('Invalid arguments: expected a number.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
36
|
+
const answers = [{
|
|
37
|
+
questionId,
|
|
38
|
+
textAnswers: {
|
|
39
|
+
answers: [{ value: response.toString() }]
|
|
40
|
+
}
|
|
41
|
+
}];
|
|
42
|
+
|
|
43
|
+
return newFakeItemResponse(this, answers);
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
/**
|
|
24
47
|
* Gets the lower bound of the scale.
|
|
25
48
|
* @returns {Integer} the lower bound
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Proxies } from '../../support/proxies.js';
|
|
2
2
|
import { FakeFormItem } from './fakeformitem.js';
|
|
3
|
+
import { registerFormItem } from './formitemregistry.js';
|
|
4
|
+
import { ItemType } from '../enums/formsenums.js';
|
|
5
|
+
import { signatureArgs } from '../../support/helpers.js';
|
|
6
|
+
import { Utils } from '../../support/utils.js';
|
|
7
|
+
const { is } = Utils;
|
|
8
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
3
9
|
|
|
4
10
|
export const newFakeTextItem = (...args) => {
|
|
5
11
|
return Proxies.guard(new FakeTextItem(...args));
|
|
@@ -14,7 +20,34 @@ export class FakeTextItem extends FakeFormItem {
|
|
|
14
20
|
super(...args);
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new ItemResponse for this text item.
|
|
25
|
+
* @param {string} response the response text
|
|
26
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
27
|
+
*/
|
|
28
|
+
createResponse(response) {
|
|
29
|
+
// Both TextItem and ParagraphTextItem have the same createResponse signature.
|
|
30
|
+
const typeName = this.getType() === ItemType.PARAGRAPH_TEXT ? 'ParagraphTextItem' : 'TextItem';
|
|
31
|
+
const { nargs, matchThrow } = signatureArgs(arguments, `${typeName}.createResponse`);
|
|
32
|
+
if (nargs !== 1 || !is.string(response)) {
|
|
33
|
+
matchThrow('Invalid arguments: expected a string.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
37
|
+
const answers = [{
|
|
38
|
+
questionId,
|
|
39
|
+
textAnswers: {
|
|
40
|
+
answers: [{ value: response }]
|
|
41
|
+
}
|
|
42
|
+
}];
|
|
43
|
+
|
|
44
|
+
return newFakeItemResponse(this, answers);
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
toString() {
|
|
18
|
-
return 'TextItem';
|
|
48
|
+
return this.getType() === ItemType.PARAGRAPH_TEXT ? 'ParagraphTextItem' : 'TextItem';
|
|
19
49
|
}
|
|
20
|
-
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
registerFormItem(ItemType.TEXT, newFakeTextItem);
|
|
53
|
+
registerFormItem(ItemType.PARAGRAPH_TEXT, newFakeTextItem);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { FakeFormItem } from './fakeformitem.js';
|
|
3
|
+
import { registerFormItem } from './formitemregistry.js';
|
|
4
|
+
import { ItemType } from '../enums/formsenums.js';
|
|
5
|
+
import { signatureArgs } from '../../support/helpers.js';
|
|
6
|
+
import { Utils } from '../../support/utils.js';
|
|
7
|
+
const { is } = Utils;
|
|
8
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
9
|
+
|
|
10
|
+
export const newFakeTimeItem = (...args) => {
|
|
11
|
+
return Proxies.guard(new FakeTimeItem(...args));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @class FakeTimeItem
|
|
16
|
+
* @see https://developers.google.com/apps-script/reference/forms/time-item
|
|
17
|
+
*/
|
|
18
|
+
export class FakeTimeItem extends FakeFormItem {
|
|
19
|
+
constructor(...args) {
|
|
20
|
+
super(...args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new ItemResponse for this time item.
|
|
25
|
+
* @param {number} hour the hour
|
|
26
|
+
* @param {number} minute the minute
|
|
27
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse} the item response
|
|
28
|
+
*/
|
|
29
|
+
createResponse(hour, minute) {
|
|
30
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'TimeItem.createResponse');
|
|
31
|
+
if (nargs !== 2 || !is.number(hour) || !is.number(minute)) {
|
|
32
|
+
matchThrow('Invalid arguments: expected two numbers.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const questionId = this.__resource.questionItem?.question?.questionId;
|
|
36
|
+
const value = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
|
37
|
+
|
|
38
|
+
const answers = [{
|
|
39
|
+
questionId,
|
|
40
|
+
textAnswers: {
|
|
41
|
+
answers: [{ value }]
|
|
42
|
+
}
|
|
43
|
+
}];
|
|
44
|
+
|
|
45
|
+
return newFakeItemResponse(this, answers);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toString() {
|
|
49
|
+
return 'TimeItem';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
registerFormItem(ItemType.TIME, newFakeTimeItem);
|
|
@@ -8,4 +8,8 @@ import './fakecheckboxgriditem.js';
|
|
|
8
8
|
import './fakescaleitem.js';
|
|
9
9
|
import './fakepagebreakitem.js';
|
|
10
10
|
import './fakelistitem.js';
|
|
11
|
+
import './faketextitem.js';
|
|
12
|
+
import './fakedateitem.js';
|
|
13
|
+
import './faketimeitem.js';
|
|
14
|
+
import './fakedurationitem.js';
|
|
11
15
|
// Add other item types here as they are implemented (e.g., './faketextitem.js')
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Proxies } from "../../support/proxies.js";
|
|
2
|
+
import { notYetImplemented, signatureArgs } from "../../support/helpers.js";
|
|
3
|
+
import { batchUpdate } from "./sheetrangehelpers.js";
|
|
4
|
+
import { newFakeEmbeddedChartBuilder } from "./fakeembeddedchartbuilder.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @returns {FakeEmbeddedChart}
|
|
8
|
+
*/
|
|
9
|
+
export const newFakeEmbeddedChart = (apiChart, sheet) => {
|
|
10
|
+
return Proxies.guard(new FakeEmbeddedChart(apiChart, sheet));
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents a chart that has been embedded into a spreadsheet.
|
|
15
|
+
*/
|
|
16
|
+
export class FakeEmbeddedChart {
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} apiChart The EmbeddedChart object from Sheets API
|
|
19
|
+
* @param {FakeSheet} sheet The parent sheet
|
|
20
|
+
*/
|
|
21
|
+
constructor(apiChart, sheet) {
|
|
22
|
+
this.__apiChart = apiChart;
|
|
23
|
+
this.__sheet = sheet;
|
|
24
|
+
|
|
25
|
+
const props = [
|
|
26
|
+
"getAs",
|
|
27
|
+
"getBlob",
|
|
28
|
+
"getContainerInfo",
|
|
29
|
+
];
|
|
30
|
+
props.forEach((f) => {
|
|
31
|
+
this[f] = () => notYetImplemented(f);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns the ID of this chart.
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
getChartId() {
|
|
40
|
+
return this.__apiChart.chartId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the ID of this chart (alias for getChartId).
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
getId() {
|
|
48
|
+
return this.getChartId().toString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns the options of this chart.
|
|
53
|
+
* @returns {object}
|
|
54
|
+
*/
|
|
55
|
+
getOptions() {
|
|
56
|
+
return {
|
|
57
|
+
get: (key) => {
|
|
58
|
+
if (key === "title") return this.__apiChart.spec.title;
|
|
59
|
+
return this.__apiChart.spec.basicChart?.options?.[key];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns a builder to modify this chart.
|
|
66
|
+
* @returns {FakeEmbeddedChartBuilder}
|
|
67
|
+
*/
|
|
68
|
+
modify() {
|
|
69
|
+
return newFakeEmbeddedChartBuilder(this.__sheet).setChartId(this.getChartId()).setApiChart(this.__apiChart);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Deletes the chart from the sheet.
|
|
74
|
+
*/
|
|
75
|
+
remove() {
|
|
76
|
+
const request = {
|
|
77
|
+
deleteEmbeddedObject: {
|
|
78
|
+
objectId: this.getChartId(),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
batchUpdate({ spreadsheet: this.__sheet.getParent(), requests: [request] });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toString() {
|
|
85
|
+
return "EmbeddedChart";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Proxies } from "../../support/proxies.js";
|
|
2
|
+
import { signatureArgs } from "../../support/helpers.js";
|
|
3
|
+
import { Utils } from "../../support/utils.js";
|
|
4
|
+
import { makeSheetsGridRange } from "./sheetrangehelpers.js";
|
|
5
|
+
import { newFakeEmbeddedChart } from "./fakeembeddedchart.js";
|
|
6
|
+
|
|
7
|
+
const { is } = Utils;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @returns {FakeEmbeddedChartBuilder}
|
|
11
|
+
*/
|
|
12
|
+
export const newFakeEmbeddedChartBuilder = (sheet) => {
|
|
13
|
+
return Proxies.guard(new FakeEmbeddedChartBuilder(sheet));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builder for embedded charts.
|
|
18
|
+
*/
|
|
19
|
+
export class FakeEmbeddedChartBuilder {
|
|
20
|
+
constructor(sheet) {
|
|
21
|
+
this.__sheet = sheet;
|
|
22
|
+
this.__apiChart = {
|
|
23
|
+
spec: {
|
|
24
|
+
title: "",
|
|
25
|
+
basicChart: {
|
|
26
|
+
chartType: "COLUMN", // Default
|
|
27
|
+
legendPosition: "RIGHT_LEGEND",
|
|
28
|
+
axis: [],
|
|
29
|
+
domains: [],
|
|
30
|
+
series: [],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
position: {
|
|
34
|
+
overlayPosition: {
|
|
35
|
+
anchorCell: {
|
|
36
|
+
sheetId: sheet.getSheetId(),
|
|
37
|
+
rowIndex: 0,
|
|
38
|
+
columnIndex: 0,
|
|
39
|
+
},
|
|
40
|
+
offsetXPixels: 0,
|
|
41
|
+
offsetYPixels: 0,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setChartId(id) {
|
|
48
|
+
this.__apiChart.chartId = id;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setApiChart(apiChart) {
|
|
53
|
+
this.__apiChart = JSON.parse(JSON.stringify(apiChart));
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addRange(range) {
|
|
58
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "EmbeddedChartBuilder.addRange");
|
|
59
|
+
if (nargs !== 1) matchThrow();
|
|
60
|
+
|
|
61
|
+
const gridRange = makeSheetsGridRange(range);
|
|
62
|
+
|
|
63
|
+
// In Sheets API, ranges are usually mapped to domains or series.
|
|
64
|
+
// Simplifying: if the range has multiple columns, split it into separate sources if it's a series.
|
|
65
|
+
// But for this fake, we'll just encourage the user to use single-column ranges or handle it simply.
|
|
66
|
+
|
|
67
|
+
if (this.__apiChart.spec.basicChart.domains.length === 0) {
|
|
68
|
+
this.__apiChart.spec.basicChart.domains.push({
|
|
69
|
+
domain: { sourceRange: { sources: [gridRange] } },
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
// Check if gridRange has multiple columns/rows and split if necessary?
|
|
73
|
+
// For now, just add it. The test will be updated to use single column ranges.
|
|
74
|
+
this.__apiChart.spec.basicChart.series.push({
|
|
75
|
+
series: { sourceRange: { sources: [gridRange] } },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setChartType(type) {
|
|
82
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "EmbeddedChartBuilder.setChartType");
|
|
83
|
+
if (nargs !== 1) matchThrow();
|
|
84
|
+
|
|
85
|
+
// type is Charts.ChartType enum (which should match API strings in our fakes)
|
|
86
|
+
this.__apiChart.spec.basicChart.chartType = type.toString();
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the chart type.
|
|
92
|
+
* @returns {ChartType}
|
|
93
|
+
*/
|
|
94
|
+
getChartType() {
|
|
95
|
+
const spec = this.__apiChart.spec;
|
|
96
|
+
const chartType = Charts.ChartType;
|
|
97
|
+
if (spec.basicChart) return chartType[spec.basicChart.chartType];
|
|
98
|
+
if (spec.pieChart) return chartType.PIE;
|
|
99
|
+
if (spec.bubbleChart) return chartType.BUBBLE;
|
|
100
|
+
if (spec.candlestickChart) return chartType.CANDLESTICK;
|
|
101
|
+
if (spec.orgChart) return chartType.ORG;
|
|
102
|
+
if (spec.waterfallChart) return chartType.WATERFALL;
|
|
103
|
+
if (spec.treemapChart) return chartType.TREE_MAP;
|
|
104
|
+
if (spec.scorecardChart) return chartType.SCORECARD;
|
|
105
|
+
if (spec.histogramChart) return chartType.HISTOGRAM;
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setPosition(anchorRowPos, anchorColPos, offsetX, offsetY) {
|
|
111
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "EmbeddedChartBuilder.setPosition");
|
|
112
|
+
if (nargs !== 4) matchThrow();
|
|
113
|
+
|
|
114
|
+
this.__apiChart.position.overlayPosition.anchorCell.rowIndex = anchorRowPos - 1;
|
|
115
|
+
this.__apiChart.position.overlayPosition.anchorCell.columnIndex = anchorColPos - 1;
|
|
116
|
+
this.__apiChart.position.overlayPosition.offsetXPixels = offsetX;
|
|
117
|
+
this.__apiChart.position.overlayPosition.offsetYPixels = offsetY;
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setOption(option, value) {
|
|
122
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "EmbeddedChartBuilder.setOption");
|
|
123
|
+
if (nargs !== 2) matchThrow();
|
|
124
|
+
|
|
125
|
+
if (option === "title") {
|
|
126
|
+
this.__apiChart.spec.title = value;
|
|
127
|
+
} else {
|
|
128
|
+
// For other options, we might want to store them in a general options object if needed,
|
|
129
|
+
// but for now, we'll just focus on "title" as requested.
|
|
130
|
+
// SpreadsheetApp.EmbeddedChartBuilder.setOption(option, value)
|
|
131
|
+
if (!this.__apiChart.spec.basicChart.options) {
|
|
132
|
+
this.__apiChart.spec.basicChart.options = {};
|
|
133
|
+
}
|
|
134
|
+
this.__apiChart.spec.basicChart.options[option] = value;
|
|
135
|
+
}
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
build() {
|
|
140
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "EmbeddedChartBuilder.build");
|
|
141
|
+
if (nargs !== 0) matchThrow();
|
|
142
|
+
|
|
143
|
+
// In a real GAS, build() returns an EmbeddedChart that is NOT yet inserted.
|
|
144
|
+
// Here we return the api object wrapped in a FakeEmbeddedChart if needed,
|
|
145
|
+
// or just the raw object that insertChart will use.
|
|
146
|
+
// Actually, gas.Sheet.insertChart expects an EmbeddedChart object.
|
|
147
|
+
return newFakeEmbeddedChart(this.__apiChart, this.__sheet);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
toString() {
|
|
151
|
+
const hex = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, "0");
|
|
152
|
+
return `com.google.apps.maestro.server.beans.trix.impl.ChartPropertyApiEmbeddedChartBuilder@${hex}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -14,6 +14,8 @@ import { FakeTextFinder, newFakeTextFinder } from "./faketextfinder.js";
|
|
|
14
14
|
import { newFakeNamedRange } from "./fakenamedrange.js";
|
|
15
15
|
import { newFakeProtection } from "./fakeprotection.js";
|
|
16
16
|
import { newFakeOverGridImage } from "./fakeovergridimage.js";
|
|
17
|
+
import { newFakeEmbeddedChart } from "./fakeembeddedchart.js";
|
|
18
|
+
import { newFakeEmbeddedChartBuilder } from "./fakeembeddedchartbuilder.js";
|
|
17
19
|
|
|
18
20
|
import { XMLParser } from "fast-xml-parser";
|
|
19
21
|
import { slogger } from "../../support/slogger.js";
|
|
@@ -34,10 +36,6 @@ export class FakeSheet {
|
|
|
34
36
|
this.__parent = parent;
|
|
35
37
|
|
|
36
38
|
const props = [
|
|
37
|
-
"getCharts",
|
|
38
|
-
"insertChart",
|
|
39
|
-
"removeChart",
|
|
40
|
-
"updateChart",
|
|
41
39
|
// "getImages",
|
|
42
40
|
"insertImage",
|
|
43
41
|
"removeImage",
|
|
@@ -399,6 +397,63 @@ export class FakeSheet {
|
|
|
399
397
|
return pivotTables;
|
|
400
398
|
}
|
|
401
399
|
|
|
400
|
+
getCharts() {
|
|
401
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.getCharts");
|
|
402
|
+
if (nargs !== 0) matchThrow();
|
|
403
|
+
|
|
404
|
+
const meta = this.getParent().__getMetaProps(
|
|
405
|
+
`sheets(charts,properties.sheetId)`
|
|
406
|
+
);
|
|
407
|
+
const sheetMeta = meta.sheets.find(
|
|
408
|
+
(s) => s.properties.sheetId === this.getSheetId()
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return (sheetMeta?.charts || []).map((c) => newFakeEmbeddedChart(c, this));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
insertChart(chart) {
|
|
415
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.insertChart");
|
|
416
|
+
if (nargs !== 1 || !is.object(chart)) matchThrow();
|
|
417
|
+
|
|
418
|
+
const request = {
|
|
419
|
+
addChart: {
|
|
420
|
+
chart: chart.__apiChart,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
batchUpdate({ spreadsheet: this.getParent(), requests: [request] });
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
newChart() {
|
|
428
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.newChart");
|
|
429
|
+
if (nargs !== 0) matchThrow();
|
|
430
|
+
|
|
431
|
+
return newFakeEmbeddedChartBuilder(this);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
removeChart(chart) {
|
|
435
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.removeChart");
|
|
436
|
+
if (nargs !== 1 || !is.object(chart)) matchThrow();
|
|
437
|
+
|
|
438
|
+
chart.remove();
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
updateChart(chart) {
|
|
443
|
+
const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.updateChart");
|
|
444
|
+
if (nargs !== 1 || !is.object(chart)) matchThrow();
|
|
445
|
+
|
|
446
|
+
const request = {
|
|
447
|
+
updateEmbeddedObjectPosition: {
|
|
448
|
+
objectId: chart.getChartId(),
|
|
449
|
+
newPosition: chart.__apiChart.position,
|
|
450
|
+
fields: "*",
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
batchUpdate({ spreadsheet: this.getParent(), requests: [request] });
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
|
|
402
457
|
getBandings() {
|
|
403
458
|
const meta = this.getParent().__getMetaProps(
|
|
404
459
|
`sheets(bandedRanges,properties.sheetId)`
|
package/src/support/auth.js
CHANGED
|
@@ -117,7 +117,7 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
117
117
|
// 2. if AUTH_TYPE is ADC, use ADC
|
|
118
118
|
// 3. if AUTH_TYPE is not set, use DWD if saName is present, else ADC
|
|
119
119
|
const saName = process.env.GOOGLE_SERVICE_ACCOUNT_NAME
|
|
120
|
-
const authType = process.env.AUTH_TYPE?.toLowerCase()
|
|
120
|
+
const authType = process.env.AUTH_TYPE?.toLowerCase()
|
|
121
121
|
const useDwd = authType === 'dwd' || (authType !== 'adc' && saName)
|
|
122
122
|
|
|
123
123
|
if (!useDwd) {
|
|
@@ -127,6 +127,9 @@ const setAuth = async (scopes = [], mcpLoading = false) => {
|
|
|
127
127
|
})
|
|
128
128
|
_sourceClient = _authClient
|
|
129
129
|
} else {
|
|
130
|
+
if (!saName) {
|
|
131
|
+
throw new Error("Domain-Wide Delegation (DWD) requested or inferred, but GOOGLE_SERVICE_ACCOUNT_NAME is not set in environment.");
|
|
132
|
+
}
|
|
130
133
|
mayLog(`...using service account: ${saName}`)
|
|
131
134
|
const targetPrincipal = `${saName}@${_projectId}.iam.gserviceaccount.com`
|
|
132
135
|
mayLog(`...attempting to use service account: ${targetPrincipal}`)
|
package/src/support/sxauth.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import got from 'got';
|
|
9
9
|
import { Auth } from './auth.js';
|
|
10
|
-
import { syncError, syncLog } from './workersync/synclogger.js';
|
|
10
|
+
import { syncError, syncLog, syncWarn } from './workersync/synclogger.js';
|
|
11
11
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
12
12
|
import path from 'path'
|
|
13
13
|
|
|
@@ -140,7 +140,7 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
|
|
|
140
140
|
const allowedScopes = new Set(effectiveScopes)
|
|
141
141
|
const missingScopes = scopes.filter(scope => !allowedScopes.has(scope))
|
|
142
142
|
if (missingScopes.length > 0) {
|
|
143
|
-
|
|
143
|
+
syncWarn(`...these scopes were asked for but not granted: ${missingScopes.join(', ')}. Note: Operations may still succeed if the 'https://www.googleapis.com/auth/cloud-platform' scope was granted.`)
|
|
144
144
|
}
|
|
145
145
|
return {
|
|
146
146
|
// these will be the scopes we're allowed to get
|