diff --git a/README.md b/README.md index 62c8335..0e623c8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Full docs available at [https://theoephraim.github.io/node-google-spreadsheet](h _The following examples are meant to give you an idea of just some of the things you can do_ -> **IMPORTANT NOTE** - To keep the examples concise, I'm calling await [at the top level](https://v8.dev/features/top-level-await) which is not allowed by default in most versions of node. If you need to call await in a script at the root level, you must instead wrap it in an async function like so: +> **IMPORTANT NOTE** - To keep the examples concise, I'm calling await [at the top level](https://v8.dev/features/top-level-await) which is not allowed in some older versions of node. If you need to call await in a script at the root level and your environment does not support it, you must instead wrap it in an async function like so: ```javascript (async function () { diff --git a/docs/classes/google-spreadsheet-worksheet.md b/docs/classes/google-spreadsheet-worksheet.md index 0c510e6..d041c6d 100644 --- a/docs/classes/google-spreadsheet-worksheet.md +++ b/docs/classes/google-spreadsheet-worksheet.md @@ -333,6 +333,24 @@ Param|Type|Required|Description ?> The authentication method being used must have write access to the destination document as well + +#### `setDataValidation(range, rule)` (async) :id=fn-setDataValidation +> Sets a data validation rule to every cell in the range + +Param|Type|Required|Description +---|---|---|--- +`range`|Object
[GridRange](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange)|✅|Range of cells to apply the rule to, sheetId not required! +`rule`|Object
[DataValidationRule](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule)
or `false`|✅|Object describing the validation rule
Or `false` to unset the rule + + +- ✨ **Side Effects -** sheet is copied to the other doc + +?> The authentication method being used must have write access to the destination document as well + + + + + ### Exports See [Exports guide](guides/exports) for more info. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f94997..202f1dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: axios: specifier: ^1.4.0 @@ -436,6 +440,7 @@ packages: /@commitlint/config-validator@17.4.4: resolution: {integrity: sha512-bi0+TstqMiqoBAQDvdEP4AFh0GaKyLFlPPEObgI29utoKEYoPQTvF0EYqIwYYLEoJYhj5GfMIhPHJkTJhagfeg==} engines: {node: '>=v14'} + requiresBuild: true dependencies: '@commitlint/types': 17.4.4 ajv: 8.12.0 @@ -445,6 +450,7 @@ packages: /@commitlint/execute-rule@17.4.0: resolution: {integrity: sha512-LIgYXuCSO5Gvtc0t9bebAMSwd68ewzmqLypqI2Kke1rqOqqDbMpYcYfoPfFlv9eyLIh4jocHWwCK5FS7z9icUA==} engines: {node: '>=v14'} + requiresBuild: true dev: true optional: true @@ -476,6 +482,7 @@ packages: /@commitlint/resolve-extends@17.4.4: resolution: {integrity: sha512-znXr1S0Rr8adInptHw0JeLgumS11lWbk5xAWFVno+HUFVN45875kUtqjrI6AppmD3JI+4s0uZlqqlkepjJd99A==} engines: {node: '>=v14'} + requiresBuild: true dependencies: '@commitlint/config-validator': 17.4.4 '@commitlint/types': 17.4.4 @@ -489,6 +496,7 @@ packages: /@commitlint/types@17.4.4: resolution: {integrity: sha512-amRN8tRLYOsxRr6mTnGGGvB5EmW/4DDjLMgiwK3CCVEmN6Sr/6xePGEpWaspKkckILuUORCwe6VfDBw6uj4axQ==} engines: {node: '>=v14'} + requiresBuild: true dependencies: chalk: 4.1.2 dev: true @@ -1784,6 +1792,7 @@ packages: /ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + requiresBuild: true dependencies: fast-deep-equal: 3.1.3 json-schema-traverse: 1.0.0 @@ -2566,6 +2575,7 @@ packages: /cosmiconfig-typescript-loader@4.3.0(@types/node@20.2.5)(cosmiconfig@8.1.3)(ts-node@10.9.1)(typescript@4.9.5): resolution: {integrity: sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==} engines: {node: '>=12', npm: '>=6'} + requiresBuild: true peerDependencies: '@types/node': '*' cosmiconfig: '>=7' @@ -3858,6 +3868,7 @@ packages: /global-dirs@0.1.1: resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} engines: {node: '>=4'} + requiresBuild: true dependencies: ini: 1.3.8 dev: true @@ -5169,6 +5180,7 @@ packages: /json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + requiresBuild: true dev: true optional: true @@ -5344,11 +5356,13 @@ packages: /lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + requiresBuild: true dev: true optional: true /lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + requiresBuild: true dev: true optional: true @@ -6340,6 +6354,7 @@ packages: /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: true optional: true @@ -6384,6 +6399,7 @@ packages: /resolve-global@1.0.0: resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} engines: {node: '>=8'} + requiresBuild: true dependencies: global-dirs: 0.1.1 dev: true @@ -7549,7 +7565,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - -settings: - autoInstallPeers: false - excludeLinksFromLockfile: false diff --git a/src/lib/GoogleSpreadsheetWorksheet.ts b/src/lib/GoogleSpreadsheetWorksheet.ts index d536dc9..7437032 100644 --- a/src/lib/GoogleSpreadsheetWorksheet.ts +++ b/src/lib/GoogleSpreadsheetWorksheet.ts @@ -12,6 +12,7 @@ import { A1Range, SpreadsheetId, DimensionRangeIndexes, WorksheetDimension, WorksheetId, WorksheetProperties, A1Address, RowIndex, ColumnIndex, DataFilterWithoutWorksheetId, DataFilter, GetValuesRequestOptions, WorksheetGridProperties, WorksheetDimensionProperties, CellDataRange, AddRowOptions, GridRangeWithOptionalWorksheetId, + DataValidationRule, } from './types/sheets-types'; @@ -819,9 +820,22 @@ export class GoogleSpreadsheetWorksheet { // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SortRangeRequest } - async setDataValidation() { - // Request type = `setDataValidation` - // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SetDataValidationRequest + /** + * Sets (or unsets) a data validation rule to every cell in the range + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SetDataValidationRequest + */ + async setDataValidation( + range: GridRangeWithOptionalWorksheetId, + /** data validation rule object, or set to false to clear an existing rule */ + rule: DataValidationRule | false + ) { + return this._makeSingleUpdateRequest('setDataValidation', { + range: { + sheetId: this.sheetId, + ...range, + }, + ...rule && { rule }, + }); } async setBasicFilter() { diff --git a/src/lib/types/sheets-types.ts b/src/lib/types/sheets-types.ts index a9e85dd..3777d59 100644 --- a/src/lib/types/sheets-types.ts +++ b/src/lib/types/sheets-types.ts @@ -526,3 +526,88 @@ export type AddRowOptions = { /** set to true to insert new rows in the sheet while adding this data */ insert?: boolean, }; + +/** + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType + */ +export type ConditionType = + | 'NUMBER_GREATER' + | 'NUMBER_GREATER_THAN_EQ' + | 'NUMBER_LESS' + | 'NUMBER_LESS_THAN_EQ' + | 'NUMBER_EQ' + | 'NUMBER_NOT_EQ' + | 'NUMBER_BETWEEN' + | 'NUMBER_NOT_BETWEEN' + | 'TEXT_CONTAINS' + | 'TEXT_NOT_CONTAINS' + | 'TEXT_STARTS_WITH' + | 'TEXT_ENDS_WITH' + | 'TEXT_EQ' + | 'TEXT_IS_EMAIL' + | 'TEXT_IS_URL' + | 'DATE_EQ' + | 'DATE_BEFORE' + | 'DATE_AFTER' + | 'DATE_ON_OR_BEFORE' + | 'DATE_ON_OR_AFTER' + | 'DATE_BETWEEN' + | 'DATE_NOT_BETWEEN' + | 'DATE_IS_VALID' + | 'ONE_OF_RANGE' + | 'ONE_OF_LIST' + | 'BLANK' + | 'NOT_BLANK' + | 'CUSTOM_FORMULA' + | 'BOOLEAN' + | 'TEXT_NOT_EQ' + | 'DATE_NOT_EQ' + | 'FILTER_EXPRESSION'; + +/** + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#relativedate + */ +export type RelativeDate = + | 'PAST_YEAR' + | 'PAST_MONTH' + | 'PAST_WEEK' + | 'YESTERDAY' + | 'TODAY' + | 'TOMORROW'; + +/** + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionValue + */ +export type ConditionValue = + | { relativeDate: RelativeDate, userEnteredValue?: undefined } + | { relativeDate?: undefined, userEnteredValue: string }; + +/** + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#BooleanCondition + */ +export type BooleanCondition = { + /** The type of condition. */ + type: ConditionType; + /** + * The values of the condition. + * The number of supported values depends on the condition type. Some support zero values, others one or two values, and ConditionType.ONE_OF_LIST supports an arbitrary number of values. + */ + values: ConditionValue[]; +}; + +/** + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule + * + * example: + * - https://stackoverflow.com/a/43442775/3068233 + */ +export type DataValidationRule = { + /** The condition that data in the cell must match. */ + condition: BooleanCondition; + /** A message to show the user when adding data to the cell. */ + inputMessage?: string; + /** True if invalid data should be rejected. */ + strict: boolean; + /** True if the UI should be customized based on the kind of condition. If true, "List" conditions will show a dropdown. */ + showCustomUi: boolean; +}; diff --git a/src/test/manage.test.ts b/src/test/manage.test.ts index 10868c6..c65a7b0 100644 --- a/src/test/manage.test.ts +++ b/src/test/manage.test.ts @@ -190,6 +190,60 @@ describe('Managing doc info and sheets', () => { }); }); + describe.only('data validation rules', () => { + let sheet: GoogleSpreadsheetWorksheet; + + beforeAll(async () => { + sheet = await doc.addSheet({ title: 'validation rules test' }); + }); + afterAll(async () => { + await sheet.delete(); + }); + + + it('can set data validation', async () => { + // add a dropdown; ref: https://stackoverflow.com/a/43442775/3068233 + await sheet.setDataValidation( + { + startRowIndex: 2, + endRowIndex: 100, + startColumnIndex: 3, + endColumnIndex: 4, + }, + { + condition: { + type: 'ONE_OF_LIST', + values: [ + { + userEnteredValue: 'YES', + }, + { + userEnteredValue: 'NO', + }, + { + userEnteredValue: 'MAYBE', + }, + ], + }, + showCustomUi: true, + strict: true, + } + ); + }); + + it('can clear a data validation', async () => { + await sheet.setDataValidation( + { + startRowIndex: 2, + endRowIndex: 100, + startColumnIndex: 3, + endColumnIndex: 4, + }, + false + ); + }); + }); + describe('deleting a sheet', () => { let sheet: GoogleSpreadsheetWorksheet; let numSheets: number;