From 0f2218458f29239fcd933e1620e3e5dea92511be Mon Sep 17 00:00:00 2001 From: James Date: Thu, 4 Jul 2024 22:57:32 +1200 Subject: [PATCH] feat(import)!: :sparkles: update existing pages if assignment has changed (#385) (#387) * Fix import of assignments without due dates (#251) (#384) * fix(canvas): :bug: do not store absence of due date as unix epoch (#251) * fix(import): :bug: correctly import assignments without due dates to notion (#251) * refactor(types): :label: refactor type declaration files change to regular typescript files * fix(validator): :bug: fix timezone validation (#252) * build(notion): :alien: pin notion api version * feat(notion): :sparkles: parse notion page properties * feat(options): :sparkles: add `notion.importChanges` options * fix(interface): :lipstick: properly hide segmented controls * refactor(types): :technologist: strongly type `Storage` key methods * docs(readme): :memo: update `readme` * docs(readme): :memo: update advanced options heading size * feat(import)!: :sparkles: update existing pages if assignment has changed (#385) BREAKING CHANGE: This requires a new previously unheld capability on the integration. Existing users must reauthorise with Notion. * build(actions): :green_heart: fix gulp build --- README.md | 52 ++++-- src/apis/notion.ts | 30 +++- src/apis/storage.ts | 11 +- src/elements/SegmentedControl.ts | 10 ++ src/options/configuration.ts | 127 +++++++++++++++ src/options/options.html | 78 ++++++++- src/options/options.ts | 13 ++ src/popup/import.ts | 266 ++++++++++++++++++++++++++++--- src/popup/popup.ts | 17 +- src/types/storage.ts | 12 ++ 10 files changed, 563 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index aa72cdad..45aa85d0 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,18 @@ A fully-configurable [Chromium](https://chrome.google.com/webstore/detail/elbkjc - [Setup Instructions](#setup-instructions) - [Safari Instructions](#safari-instructions) - [Configurable Options](#configurable-options) + - [`Unlock Date`, `Due Date`, and `Date Span`](#what-is-the-difference-between-unlock-date-due-date-and-date-span) - [`Course Code Overrides`](#course-code-overrides) - [`Page Emojis`](#page-emojis) - [Troubleshooting](#troubleshooting) + - [Wrong Timezone](#wrong-timezone) - [Release Notes](#release-notes) - [BREAKING CHANGES](#breaking-changes) - - [`v4.3.0`](#v430) + - [`v5`](#v5) + - [Migration Instructions](#migration-instructions) + - [`v4`](#v4) + - [`v4.3.0`](#v430) + - [Migration Instructions](#migration-instructions-1) - [Building For Local Development](#building-for-local-development) - [How It Works](#how-it-works) - [Assignment Fetching](#assignment-fetching) @@ -96,17 +102,17 @@ A fully-configurable [Chromium](https://chrome.google.com/webstore/detail/elbkjc # Features -- Simple, user-friendly interface -- Provided [Notion database template](https://jamesnzl-sandbox.notion.site/c4d73bebd39c4103b96b2edb8be9e0bd?v=9afaf4b4faee4a5a977c00291be06c9e) -- Configurable options, with input validation -- `Category` database property for seamless integration into an existing tasks database -- No duplication of assignments already in the database -- Fine-tuning of database property names & values -- Overriding of Canvas course names -- Emojis for Notion page icons -- Open source -- Private and secureβ€”all data is stored locally! - > My integration does not save or log any user data. +- πŸ’„ Simple, user-friendly interface +- πŸ“„ Provided [Notion database template](https://jamesnzl-sandbox.notion.site/c4d73bebd39c4103b96b2edb8be9e0bd?v=9afaf4b4faee4a5a977c00291be06c9e) +- πŸ”§ Configurable options, with input validation +- πŸ—„οΈ `Category` database property for seamless integration into an existing tasks database +- πŸ” No duplication of assignments already in the database +- πŸ“ Updating previously imported assignments with new Canvas changes +- βš™οΈ Fine-tuning of database property names & values +- ✏️ Overriding of Canvas course names +- πŸ˜€ Emojis for Notion page icons +- πŸ§‘β€πŸ’» Open source +- πŸ”’ Private and secureβ€”all data is stored locally. I do not log or save any user data. # Screenshots @@ -246,9 +252,14 @@ chmod +x /Users/YOUR_USERNAME/Downloads/Notion\ Canvas\ Assignment\ Import/Conte | `Due Date Property` | The name of a database Due property, used to set the assignment due date | | `Date Span Property` | The name of a database Date Span property, used to set the date span of the assignment as `unlock date`–`due date` | | `Canvas Category` | The value of a database Category property to categorise all Canvas assignments as | +| `Name Changes` | Whether to `Import` (ie update the existing page) or `Ignore` Canvas changes to previously imported assignment names | +| `Points Changes` | Whether to `Import` (ie update the existing page) or `Ignore` Canvas changes to previously imported assignment points | +| `Unlock Date Changes` | Whether to `Import` (ie update the existing page) or `Ignore` Canvas changes to previously imported assignment unlock dates | +| `Due Date Changes` | Whether to `Import` (ie update the existing page) or `Ignore` Canvas changes to previously imported assignment due dates | +| `Date Span Changes` | Whether to `Import` (ie update the existing page) or `Ignore` Canvas changes to previously imported assignment date spans | | `Page Emojis` | Any Notion page emojis to apply | -**Advanced Options** +### Advanced Options | Option | Purpose/Remarks | | ---------------------------- | ---------------------------------------------------------------------- | @@ -319,19 +330,30 @@ If your assignments are being imported into Notion in the wrong timezone, please ## BREAKING CHANGES +> [!CAUTION] +> If you are an existing user, you may be affected by these breaking changes following an update. + +### `v5` + +1. Support has been added to update existing pages with changed assignment details (eg name, due date, points, etc.). This requires an additional integration capability, so existing users must reauthorise with Notion. + +##### Migration Instructions + +1. To leverage the support for updating existing pages, you must reauthorise with Notion via the Options page. + ### `v4` 1. Support for a configurable Notion `'Status'` property has been removed, as Notion has implemented their own built-in `Status` property. To fix assignments being imported without a `'Status'` value, change your database property to be of type `Status`, and assign a `DEFAULT` value. > [Read more here](https://www.notion.so/help/guides/status-property-gives-clarity-on-tasks). -## `v4.3.0` +#### `v4.3.0` 1. Notion pages now include the assignment's Canvas description ([#59](https://github.com/JamesNZL/notion-assignment-import/issues/59)). 2. Added support for a new 'Points' `number` database property ([#61](https://github.com/JamesNZL/notion-assignment-import/issues/61)) to tag the assignment's points value. 3. Changed the colour of **Advanced Options** headings to ease identification. 4. Renamed `Available Date` to `Unlock Date`. -### Migration Instructions +##### Migration Instructions 1. To leverage the new support for a `Points` property, you must create a new Notion [`number`](https://www.notion.so/help/database-properties) database property, and configure it on the Options Page. diff --git a/src/apis/notion.ts b/src/apis/notion.ts index 59d4aa12..064b2681 100644 --- a/src/apis/notion.ts +++ b/src/apis/notion.ts @@ -1,5 +1,16 @@ import { APIErrorCode, Client, isNotionClientError } from '@notionhq/client'; -import { CreatePageParameters, CreatePageResponse, GetDatabaseResponse, GetSelfResponse, QueryDatabaseParameters, QueryDatabaseResponse, SearchParameters, SearchResponse } from '@notionhq/client/build/src/api-endpoints'; +import { + CreatePageParameters, + CreatePageResponse, + GetDatabaseResponse, + GetSelfResponse, + QueryDatabaseParameters, + QueryDatabaseResponse, + SearchParameters, + SearchResponse, + UpdatePageParameters, + UpdatePageResponse, +} from '@notionhq/client/build/src/api-endpoints'; import { HandlerClientOptions, PaginatedRequest, PaginatedResponse, RichTextItemResponse } from '../types/notion'; import { ArrayElement } from '../types/utils'; @@ -25,7 +36,10 @@ export class NotionClient extends Client { private requestCache = new Map(); private constructor(options: HandlerClientOptions) { - super(options); + super({ + ...options, + notionVersion: '2022-06-28', + }); this.auth = options.auth; NotionClient.rateLimits.set(options.auth, { @@ -211,6 +225,18 @@ export class NotionClient extends Client { ); } + public async updatePageProperties(parameters: UpdatePageParameters): Promise { + return await this.makeRequest( + this.pages.update, + 'pages.update', + parameters, + { + cache: false, + force: true, + } + ); + } + public async searchShared({ query, sort, filter }: SearchParameters, { cache, force } = { cache: true, force: false }): Promise { return await this.makePaginatedRequest( this.search, diff --git a/src/apis/storage.ts b/src/apis/storage.ts index 60270fcd..24b117cb 100644 --- a/src/apis/storage.ts +++ b/src/apis/storage.ts @@ -115,11 +115,11 @@ export const Storage = { return await browser.storage.local.remove(databaseIdKey); }, - async getStorageKey(key: string, defaultValue: unknown) { + async getStorageKey(key: K, defaultValue: SavedFields[K]) { return (await browser.storage.local.get({ [key]: defaultValue }))[key]; }, - async clearStorageKey(key: string) { + async clearStorageKey(key: keyof SavedFields) { return await browser.storage.local.remove(key); }, @@ -163,6 +163,13 @@ export const Storage = { propertyValues: { categoryCanvas: savedFields['notion.propertyValues.categoryCanvas'], }, + importChanges: { + name: savedFields['notion.importChanges.name'], + points: savedFields['notion.importChanges.points'], + available: savedFields['notion.importChanges.available'], + due: savedFields['notion.importChanges.due'], + span: savedFields['notion.importChanges.span'], + }, courseEmojis: JSON.parse(savedFields['notion.courseEmojis']), }, }; diff --git a/src/elements/SegmentedControl.ts b/src/elements/SegmentedControl.ts index b74b4b01..8cd5512e 100644 --- a/src/elements/SegmentedControl.ts +++ b/src/elements/SegmentedControl.ts @@ -94,11 +94,21 @@ export class SegmentedControl extends Element { } public override show() { + // Show the extra
that wraps the segmented-control-wrapper + const segmentedControlGroup = this.element.parentElement?.parentElement; + if (segmentedControlGroup && segmentedControlGroup.classList.contains('segmented-control-group')) { + segmentedControlGroup.classList.remove('hidden'); + } super.show(); this.dispatchInputEvent(); } public override hide() { + // Hide the extra
that wraps the segmented-control-wrapper + const segmentedControlGroup = this.element.parentElement?.parentElement; + if (segmentedControlGroup && segmentedControlGroup.classList.contains('segmented-control-group')) { + segmentedControlGroup.classList.add('hidden'); + } super.hide(); this.dispatchInputEvent(); } diff --git a/src/options/configuration.ts b/src/options/configuration.ts index 7f526096..72b3c374 100644 --- a/src/options/configuration.ts +++ b/src/options/configuration.ts @@ -98,6 +98,21 @@ interface InputElements { 'notion.propertyNames.due': 'notion-property-due'; 'notion.propertyNames.span': 'notion-property-span'; 'notion.propertyValues.categoryCanvas': 'notion-category-canvas'; + 'notion.importChanges.name': 'notion-changes-name'; + noChangesName: 'no-changes-name-button'; + yesChangesName: 'yes-changes-name-button'; + 'notion.importChanges.points': 'notion-changes-points'; + noChangesPoints: 'no-changes-points-button'; + yesChangesPoints: 'yes-changes-points-button'; + 'notion.importChanges.available': 'notion-changes-available'; + noChangesAvailable: 'no-changes-available-button'; + yesChangesAvailable: 'yes-changes-available-button'; + 'notion.importChanges.due': 'notion-changes-due'; + noChangesDue: 'no-changes-due-button'; + yesChangesDue: 'yes-changes-due-button'; + 'notion.importChanges.span': 'notion-changes-span'; + noChangesSpan: 'no-changes-span-button'; + yesChangesSpan: 'yes-changes-span-button'; 'notion.courseEmojis': 'course-emojis-group'; } @@ -287,6 +302,7 @@ export const CONFIGURATION: { }); }, validateOn: 'change', + dependents: ['notion-changes-name'], }, category: { defaultValue: 'Category', @@ -328,6 +344,7 @@ export const CONFIGURATION: { Validator: StringField, }); }, + dependents: ['notion-changes-points'], }, available: { defaultValue: 'Reminder', @@ -338,6 +355,7 @@ export const CONFIGURATION: { Validator: StringField, }); }, + dependents: ['notion-changes-available'], }, due: { defaultValue: 'Due', @@ -348,6 +366,7 @@ export const CONFIGURATION: { Validator: StringField, }); }, + dependents: ['notion-changes-due'], }, span: { defaultValue: 'Date Span', @@ -358,6 +377,7 @@ export const CONFIGURATION: { Validator: StringField, }); }, + dependents: ['notion-changes-span'], }, }, propertyValues: { @@ -372,6 +392,113 @@ export const CONFIGURATION: { }, }, }, + importChanges: { + 'name': { + defaultValue: true, + get input() { + delete (>this).input; + return this.input = SegmentedControl.getInstance({ + id: 'notion-changes-name', + segments: [ + { + id: 'no-changes-name-button', + value: false, + }, + { + id: 'yes-changes-name-button', + value: true, + default: true, + showDependents: true, + }, + ], + }); + }, + }, + 'points': { + defaultValue: true, + get input() { + delete (>this).input; + return this.input = SegmentedControl.getInstance({ + id: 'notion-changes-points', + segments: [ + { + id: 'no-changes-points-button', + value: false, + }, + { + id: 'yes-changes-points-button', + value: true, + default: true, + showDependents: true, + }, + ], + }); + }, + }, + 'available': { + defaultValue: true, + get input() { + delete (>this).input; + return this.input = SegmentedControl.getInstance({ + id: 'notion-changes-available', + segments: [ + { + id: 'no-changes-available-button', + value: false, + }, + { + id: 'yes-changes-available-button', + value: true, + default: true, + showDependents: true, + }, + ], + }); + }, + }, + 'due': { + defaultValue: true, + get input() { + delete (>this).input; + return this.input = SegmentedControl.getInstance({ + id: 'notion-changes-due', + segments: [ + { + id: 'no-changes-due-button', + value: false, + }, + { + id: 'yes-changes-due-button', + value: true, + default: true, + showDependents: true, + }, + ], + }); + }, + }, + 'span': { + defaultValue: true, + get input() { + delete (>this).input; + return this.input = SegmentedControl.getInstance({ + id: 'notion-changes-span', + segments: [ + { + id: 'no-changes-span-button', + value: false, + }, + { + id: 'yes-changes-span-button', + value: true, + default: true, + showDependents: true, + }, + ], + }); + }, + }, + }, courseEmojis: { defaultValue: '{}', get input() { diff --git a/src/options/options.html b/src/options/options.html index 40d3c27d..d961b326 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -30,7 +30,7 @@

Extension Options

Appearance

-
+
@@ -49,7 +49,7 @@

Appearance

Options Page

-
+
@@ -75,7 +75,7 @@

Timezone