From a0926892c21c7b4ffa00a2697bc9a193cae35178 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 9 Dec 2021 17:57:08 +0100 Subject: [PATCH] Add support for Page's Icon retrieval and updates (#21) * Add support for Page's Icon retrieval and updates * Add support for creating a page with an icon * Fix tests, add icon validation * Remove unknown icon type `file` * Add icon support for "create database" endpoint Co-authored-by: David Stotijn --- client_test.go | 120 ++++++++++++++++++++++++++++++++++++++++++++++++- database.go | 9 ++++ icon.go | 38 ++++++++++++++++ page.go | 28 ++++++++++-- 4 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 icon.go diff --git a/client_test.go b/client_test.go index a6358a5..bafe309 100644 --- a/client_test.go +++ b/client_test.go @@ -943,6 +943,10 @@ func TestCreateDatabase(t *testing.T) { Title: ¬ion.EmptyMetadata{}, }, }, + Icon: ¬ion.Icon{ + Type: notion.IconTypeEmoji, + Emoji: notion.StringPtr("✌️"), + }, }, respBody: func(_ *http.Request) io.Reader { return strings.NewReader( @@ -980,6 +984,10 @@ func TestCreateDatabase(t *testing.T) { "parent": { "type": "page_id", "page_id": "b0668f48-8d66-4733-9bdb-2f82215707f7" + }, + "icon": { + "type": "emoji", + "emoji": "✌️" } }`, ) @@ -1003,6 +1011,10 @@ func TestCreateDatabase(t *testing.T) { "title": map[string]interface{}{}, }, }, + "icon": map[string]interface{}{ + "type": "emoji", + "emoji": "✌️", + }, }, expResponse: notion.Database{ ID: "b89664e3-30b4-474a-9cce-c72a4827d1e4", @@ -1031,6 +1043,10 @@ func TestCreateDatabase(t *testing.T) { Title: ¬ion.EmptyMetadata{}, }, }, + Icon: notion.Icon{ + Type: notion.IconTypeEmoji, + Emoji: notion.StringPtr("✌️"), + }, }, expError: nil, }, @@ -1808,6 +1824,106 @@ func TestUpdatePageProps(t *testing.T) { }, expError: nil, }, + { + name: "page icon, successful response", + params: notion.UpdatePageParams{ + Icon: ¬ion.Icon{ + Type: notion.IconTypeExternal, + External: ¬ion.IconExternal{ + URL: "https://www.notion.so/front-static/pages/pricing/pro.png", + }, + }, + }, + respBody: func(_ *http.Request) io.Reader { + return strings.NewReader( + `{ + "object": "page", + "id": "cb261dc5-6c85-4767-8585-3852382fb466", + "created_time": "2021-05-14T09:15:46.796Z", + "last_edited_time": "2021-05-22T15:54:31.116Z", + "parent": { + "type": "page_id", + "page_id": "b0668f48-8d66-4733-9bdb-2f82215707f7" + }, + "icon": { + "type": "external", + "external": { + "url": "https://www.notion.so/front-static/pages/pricing/pro.png" + } + }, + "archived": false, + "url": "https://www.notion.so/Avocado-251d2b5f268c4de2afe9c71ff92ca95c", + "properties": { + "title": { + "id": "title", + "type": "title", + "title": [ + { + "type": "text", + "text": { + "content": "Lorem ipsum", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "Lorem ipsum", + "href": null + } + ] + } + } + }`, + ) + }, + respStatusCode: http.StatusOK, + expPostBody: map[string]interface{}{ + "icon": map[string]interface{}{ + "type": "external", + "external": map[string]interface{}{ + "url": "https://www.notion.so/front-static/pages/pricing/pro.png", + }, + }, + }, + expResponse: notion.Page{ + ID: "cb261dc5-6c85-4767-8585-3852382fb466", + CreatedTime: mustParseTime(time.RFC3339Nano, "2021-05-14T09:15:46.796Z"), + LastEditedTime: mustParseTime(time.RFC3339Nano, "2021-05-22T15:54:31.116Z"), + URL: "https://www.notion.so/Avocado-251d2b5f268c4de2afe9c71ff92ca95c", + Parent: notion.Parent{ + Type: notion.ParentTypePage, + PageID: "b0668f48-8d66-4733-9bdb-2f82215707f7", + }, + Icon: ¬ion.Icon{ + Type: notion.IconTypeExternal, + External: ¬ion.IconExternal{ + URL: "https://www.notion.so/front-static/pages/pricing/pro.png", + }, + }, + Properties: notion.PageProperties{ + Title: notion.PageTitle{ + Title: []notion.RichText{ + { + Type: notion.RichTextTypeText, + Text: ¬ion.Text{ + Content: "Lorem ipsum", + }, + Annotations: ¬ion.Annotations{ + Color: notion.ColorDefault, + }, + PlainText: "Lorem ipsum", + }, + }, + }, + }, + }, + expError: nil, + }, { name: "database page props, successful response", params: notion.UpdatePageParams{ @@ -1943,10 +2059,10 @@ func TestUpdatePageProps(t *testing.T) { expError: errors.New("notion: failed to update page properties: foobar (code: validation_error, status: 400)"), }, { - name: "missing page title and database properties", + name: "missing any params", params: notion.UpdatePageParams{}, expResponse: notion.Page{}, - expError: errors.New("notion: invalid page params: either database page properties or title is required"), + expError: errors.New("notion: invalid page params: at least one of database page properties, title or icon is required"), }, } diff --git a/database.go b/database.go index 935085d..f5c5d19 100644 --- a/database.go +++ b/database.go @@ -15,6 +15,7 @@ type Database struct { Title []RichText `json:"title"` Properties DatabaseProperties `json:"properties"` Parent Parent `json:"parent"` + Icon Icon `json:"icon"` } // DatabaseProperties is a mapping of properties defined on a database. @@ -235,6 +236,7 @@ type CreateDatabaseParams struct { ParentPageID string Title []RichText Properties DatabaseProperties + Icon *Icon } type ( @@ -362,6 +364,11 @@ func (p CreateDatabaseParams) Validate() error { if p.Properties == nil { return errors.New("database properties are required") } + if p.Icon != nil { + if err := p.Icon.Validate(); err != nil { + return err + } + } return nil } @@ -372,6 +379,7 @@ func (p CreateDatabaseParams) MarshalJSON() ([]byte, error) { Parent Parent `json:"parent"` Title []RichText `json:"title,omitempty"` Properties DatabaseProperties `json:"properties"` + Icon *Icon `json:"icon,omitempty"` } parent := Parent{ @@ -383,6 +391,7 @@ func (p CreateDatabaseParams) MarshalJSON() ([]byte, error) { Parent: parent, Title: p.Title, Properties: p.Properties, + Icon: p.Icon, } return json.Marshal(dto) diff --git a/icon.go b/icon.go new file mode 100644 index 0000000..8a99159 --- /dev/null +++ b/icon.go @@ -0,0 +1,38 @@ +package notion + +import "errors" + +type IconType string + +const ( + IconTypeEmoji IconType = "emoji" + IconTypeExternal IconType = "external" +) + +// Icon has one non-nil Emoji or External field, denoted by the corresponding +// IconType. +type Icon struct { + Type IconType `json:"type"` + + Emoji *string `json:"emoji,omitempty"` + External *IconExternal `json:"external,omitempty"` +} + +type IconExternal struct { + URL string `json:"url"` +} + +func (icon Icon) Validate() error { + if icon.Type == "" { + return errors.New("icon type cannot be empty") + } + + if icon.Type == IconTypeEmoji && icon.Emoji == nil { + return errors.New("icon emoji cannot be empty") + } + if icon.Type == IconTypeExternal && icon.External == nil { + return errors.New("icon external cannot be empty") + } + + return nil +} diff --git a/page.go b/page.go index 9ef2020..20816d5 100644 --- a/page.go +++ b/page.go @@ -17,6 +17,7 @@ type Page struct { Parent Parent `json:"parent"` Archived bool `json:"archived"` URL string `json:"url"` + Icon *Icon `json:"icon"` // Properties differ between parent type. // See the `UnmarshalJSON` method. @@ -71,12 +72,15 @@ type CreatePageParams struct { // Optionally, children blocks are added to the page. Children []Block + + Icon *Icon } type UpdatePageParams struct { - // Either DatabasePageProperties or Title must be not nil. + // Either DatabasePageProperties, Title or Icon must be not nil. DatabasePageProperties *DatabasePageProperties Title []RichText + Icon *Icon } // Value returns the underlying database page property value, based on its `type` field. @@ -139,6 +143,11 @@ func (p CreatePageParams) Validate() error { if p.ParentType == ParentTypePage && p.Title == nil { return errors.New("title is required when parent type is page") } + if p.Icon != nil { + if err := p.Icon.Validate(); err != nil { + return err + } + } return nil } @@ -148,6 +157,7 @@ func (p CreatePageParams) MarshalJSON() ([]byte, error) { Parent Parent `json:"parent"` Properties interface{} `json:"properties"` Children []Block `json:"children,omitempty"` + Icon *Icon `json:"icon,omitempty"` } var parent Parent @@ -161,6 +171,7 @@ func (p CreatePageParams) MarshalJSON() ([]byte, error) { dto := CreatePageParamsDTO{ Parent: parent, Children: p.Children, + Icon: p.Icon, } if p.DatabasePageProperties != nil { @@ -224,15 +235,22 @@ func (p *Page) UnmarshalJSON(b []byte) error { } func (p UpdatePageParams) Validate() error { - if p.DatabasePageProperties == nil && p.Title == nil { - return errors.New("either database page properties or title is required") + // At least one of the params must be set. + if p.DatabasePageProperties == nil && p.Title == nil && p.Icon == nil { + return errors.New("at least one of database page properties, title or icon is required") + } + if p.Icon != nil { + if err := p.Icon.Validate(); err != nil { + return err + } } return nil } func (p UpdatePageParams) MarshalJSON() ([]byte, error) { type UpdatePageParamsDTO struct { - Properties interface{} `json:"properties"` + Properties interface{} `json:"properties,omitempty"` + Icon *Icon `json:"icon,omitempty"` } var dto UpdatePageParamsDTO @@ -245,5 +263,7 @@ func (p UpdatePageParams) MarshalJSON() ([]byte, error) { } } + dto.Icon = p.Icon + return json.Marshal(dto) }