From 39269506e06d608acba7ebd2086bcff4701d89bc Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 09:51:53 -0500 Subject: [PATCH 01/14] Add constructor `withBlocks` for Children Deprecate main constructor parameters Add test for new constructor --- lib/notion/general/lists/children.dart | 22 +++++++++++++++------- test/lists/children_test.dart | 21 ++++++++++++++++++++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/notion/general/lists/children.dart b/lib/notion/general/lists/children.dart index 4d0924c..d8417bb 100644 --- a/lib/notion/general/lists/children.dart +++ b/lib/notion/general/lists/children.dart @@ -19,11 +19,13 @@ class Children { /// Main children constructor. /// + /// _Parameters deprecated:_ Do not use the parameters, soon will be removed. + /// /// Can receive a single [heading], a single [paragraph], and a list of [toDo] blocks. If all three are included then the three fields are added to the blocks list adding first the [heading] field, then the [paragraph], and the list of [toDo] at the end. Children({ - Heading? heading, - Paragraph? paragraph, - List? toDo, + @deprecated Heading? heading, + @deprecated Paragraph? paragraph, + @deprecated List? toDo, }) { if (heading != null) { _blocks.add(heading); @@ -36,14 +38,20 @@ class Children { } } + /// Constructor that initialize a Children instance with a list of blocks. + /// + /// Receive a list of blocks and avoid create first the Children instance and then add the blocks. + Children.withBlocks(List blocks) { + this._blocks.addAll(blocks); + } + /// Map a new children instance from a [json] blocks list. /// /// The blocks with the block type None are excluded because that type represent blocks than can't be mapped as a knowing Notion block type. factory Children.fromJson(List json) { - Children children = Children(); - children._blocks = Block.fromListJson(json); - children._blocks = children.filterBlocks(exclude: [BlockTypes.None]); - return children; + List blocks = Block.fromListJson(json); + blocks.removeWhere((block) => block.type == BlockTypes.None); + return Children.withBlocks(blocks); } /// Add a new [block] to the list of blocks and returns this instance. diff --git a/test/lists/children_test.dart b/test/lists/children_test.dart index 5866531..a1f7b79 100644 --- a/test/lists/children_test.dart +++ b/test/lists/children_test.dart @@ -86,7 +86,7 @@ void main() { }); test('Add blocks in distinct ways', () { - Children children1 = Children( + Children deprecated = Children( heading: Heading(text: Text('Test')), paragraph: Paragraph( texts: [ @@ -103,6 +103,23 @@ void main() { ), ); + Children children1 = Children.withBlocks([ + Heading(text: Text('Test')), + Paragraph( + texts: [ + Text('Lorem ipsum (A)'), + Text( + 'Lorem ipsum (B)', + annotations: TextAnnotations( + bold: true, + underline: true, + color: ColorsTypes.Orange, + ), + ), + ], + ), + ]); + Children children2 = Children().add(Heading(text: Text('Test'))).add(Paragraph(texts: [ Text('Lorem ipsum (A)'), @@ -127,10 +144,12 @@ void main() { ]) ]); + var json0 = deprecated.toJson(); var json1 = children1.toJson(); var json2 = children2.toJson(); var json3 = children3.toJson(); + expect(json0, json1); expect(json1, json2); expect(json2, json3); }); From e29507c9e0c553fad06ab8af43653b58c3164037 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 10:13:40 -0500 Subject: [PATCH 02/14] Add final for type fields block --- lib/notion/blocks/block.dart | 3 ++- lib/notion/blocks/heading.dart | 2 +- lib/notion/blocks/paragraph.dart | 2 +- lib/notion/blocks/todo.dart | 2 +- lib/notion/objects/database.dart | 2 +- lib/notion/objects/pages.dart | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/notion/blocks/block.dart b/lib/notion/blocks/block.dart index 335ab03..8cc37f1 100644 --- a/lib/notion/blocks/block.dart +++ b/lib/notion/blocks/block.dart @@ -5,7 +5,8 @@ import 'package:notion_api/utils/utils.dart'; /// A base representation of any Notion block object. class Block extends BaseFields { /// The type of object. Always Block for this. - ObjectTypes object = ObjectTypes.Block; + @override + final ObjectTypes object = ObjectTypes.Block; /// The block id. String id = ''; diff --git a/lib/notion/blocks/heading.dart b/lib/notion/blocks/heading.dart index d840ae0..34b7238 100644 --- a/lib/notion/blocks/heading.dart +++ b/lib/notion/blocks/heading.dart @@ -6,7 +6,7 @@ import 'package:notion_api/notion/general/rich_text.dart'; class Heading extends Block { /// The block type. Always H1, H2 or H3 for this. @override - BlockTypes type = BlockTypes.H1; + final BlockTypes type = BlockTypes.H1; List _content = []; diff --git a/lib/notion/blocks/paragraph.dart b/lib/notion/blocks/paragraph.dart index 6197035..7749f55 100644 --- a/lib/notion/blocks/paragraph.dart +++ b/lib/notion/blocks/paragraph.dart @@ -6,7 +6,7 @@ import 'package:notion_api/notion/general/rich_text.dart'; class Paragraph extends Block { /// The block type. Always Paragraph for this. @override - BlockTypes type = BlockTypes.Paragraph; + final BlockTypes type = BlockTypes.Paragraph; List _content = []; diff --git a/lib/notion/blocks/todo.dart b/lib/notion/blocks/todo.dart index b399ffe..5ee8a11 100644 --- a/lib/notion/blocks/todo.dart +++ b/lib/notion/blocks/todo.dart @@ -6,7 +6,7 @@ import 'package:notion_api/notion/general/rich_text.dart'; class ToDo extends Block { /// The block type. Always ToDo for this. @override - BlockTypes type = BlockTypes.ToDo; + final BlockTypes type = BlockTypes.ToDo; List _content = []; diff --git a/lib/notion/objects/database.dart b/lib/notion/objects/database.dart index 0500ac6..9a8af1a 100644 --- a/lib/notion/objects/database.dart +++ b/lib/notion/objects/database.dart @@ -9,7 +9,7 @@ import 'package:notion_api/utils/utils.dart'; class Database extends BaseFields { /// The type of this object. Always Database for this. @override - ObjectTypes object = ObjectTypes.Database; + final ObjectTypes object = ObjectTypes.Database; /// The title of this database. List title = []; diff --git a/lib/notion/objects/pages.dart b/lib/notion/objects/pages.dart index 9761d56..ce57f27 100644 --- a/lib/notion/objects/pages.dart +++ b/lib/notion/objects/pages.dart @@ -10,7 +10,7 @@ import 'package:notion_api/utils/utils.dart'; class Page extends BaseFields { /// The type of this object. Always Page for this. @override - ObjectTypes object = ObjectTypes.Page; + final ObjectTypes object = ObjectTypes.Page; /// The information of the page parent. Parent parent; From a72adb7a21993811700dae71206d7fe1bf45c1a5 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 10:14:51 -0500 Subject: [PATCH 03/14] Add base class for clients to avoid duplicated code --- lib/base_client.dart | 25 +++++++++++++++++++++++++ lib/notion_blocks.dart | 33 +++++++++++---------------------- lib/notion_databases.dart | 33 +++++++++++---------------------- lib/notion_pages.dart | 33 +++++++++++---------------------- 4 files changed, 58 insertions(+), 66 deletions(-) create mode 100644 lib/base_client.dart diff --git a/lib/base_client.dart b/lib/base_client.dart new file mode 100644 index 0000000..4b8afe2 --- /dev/null +++ b/lib/base_client.dart @@ -0,0 +1,25 @@ +import 'package:notion_api/statics.dart'; + +abstract class BaseClient { + /// The API integration secret token. + String token; + + /// The API version. + String v; + + /// The API date version. + /// + /// It's not the same as the API version. + String dateVersion; + + /// The path of the requests group. + String path = ''; + + BaseClient({ + required String token, + String version: latestVersion, + String dateVersion: latestDateVersion, + }) : this.token = token, + this.v = version, + this.dateVersion = dateVersion; +} diff --git a/lib/notion_blocks.dart b/lib/notion_blocks.dart index b9922b7..4095dc5 100644 --- a/lib/notion_blocks.dart +++ b/lib/notion_blocks.dart @@ -1,26 +1,17 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:notion_api/base_client.dart'; import 'notion/general/lists/children.dart'; import 'responses/notion_response.dart'; import 'statics.dart'; /// A client for Notion API block children requests. -class NotionBlockClient { - /// The API integration secret token. - String _token; - - /// The API version. - String _v; - - /// The API date version. - /// - /// It's not the same as the API version. - String _dateVersion; - +class NotionBlockClient extends BaseClient { /// The path of the requests group. - String _path = 'blocks'; + @override + final String path = 'blocks'; /// Main Notion block client constructor. /// @@ -29,9 +20,7 @@ class NotionBlockClient { required String token, String version: latestVersion, String dateVersion: latestDateVersion, - }) : this._token = token, - this._v = version, - this._dateVersion = dateVersion; + }) : super(token: token, version: version, dateVersion: dateVersion); /// Retrieve the block children from block with [id]. /// @@ -51,9 +40,9 @@ class NotionBlockClient { } http.Response response = await http - .get(Uri.https(host, '/$_v/$_path/$id/children', query), headers: { - 'Authorization': 'Bearer $_token', - 'Notion-Version': _dateVersion, + .get(Uri.https(host, '/$v/$path/$id/children', query), headers: { + 'Authorization': 'Bearer $token', + 'Notion-Version': dateVersion, }); return NotionResponse.fromResponse(response); @@ -65,12 +54,12 @@ class NotionBlockClient { required Children children, }) async { http.Response res = await http.patch( - Uri.https(host, '/$_v/$_path/$to/children'), + Uri.https(host, '/$v/$path/$to/children'), body: jsonEncode(children.toJson()), headers: { - 'Authorization': 'Bearer $_token', + 'Authorization': 'Bearer $token', 'Content-Type': 'application/json; charset=UTF-8', - 'Notion-Version': _dateVersion, + 'Notion-Version': dateVersion, }); return NotionResponse.fromResponse(res); diff --git a/lib/notion_databases.dart b/lib/notion_databases.dart index 4578e7a..0a58c10 100644 --- a/lib/notion_databases.dart +++ b/lib/notion_databases.dart @@ -1,23 +1,14 @@ import 'package:http/http.dart' as http; +import 'package:notion_api/base_client.dart'; import 'responses/notion_response.dart'; import 'statics.dart'; /// A client for Notion API databases requests. -class NotionDatabasesClient { - /// The API integration secret token. - String _token; - - /// The API version. - String _v; - - /// The API date version. - /// - /// It's not the same as the API version. - String _dateVersion; - +class NotionDatabasesClient extends BaseClient { /// The path of the requests group. - String _path = 'databases'; + @override + final String path = 'databases'; /// Main Notion database client constructor. /// @@ -26,16 +17,14 @@ class NotionDatabasesClient { required String token, String version: latestVersion, String dateVersion: latestDateVersion, - }) : this._token = token, - this._v = version, - this._dateVersion = dateVersion; + }) : super(token: token, version: version, dateVersion: dateVersion); /// Retrieve the database with [id]. Future fetch(String id) async { http.Response res = - await http.get(Uri.https(host, '/$_v/$_path/$id'), headers: { - 'Authorization': 'Bearer $_token', - 'Notion-Version': _dateVersion, + await http.get(Uri.https(host, '/$v/$path/$id'), headers: { + 'Authorization': 'Bearer $token', + 'Notion-Version': dateVersion, }); return NotionResponse.fromResponse(res); @@ -55,9 +44,9 @@ class NotionDatabasesClient { } http.Response res = - await http.get(Uri.https(host, '/$_v/$_path', query), headers: { - 'Authorization': 'Bearer $_token', - 'Notion-Version': _dateVersion, + await http.get(Uri.https(host, '/$v/$path', query), headers: { + 'Authorization': 'Bearer $token', + 'Notion-Version': dateVersion, }); return NotionResponse.fromResponse(res); diff --git a/lib/notion_pages.dart b/lib/notion_pages.dart index 7f35ff1..830adaf 100644 --- a/lib/notion_pages.dart +++ b/lib/notion_pages.dart @@ -1,26 +1,17 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:notion_api/base_client.dart'; import 'notion/objects/pages.dart'; import 'responses/notion_response.dart'; import 'statics.dart'; /// A client for Notion API pages requests. -class NotionPagesClient { - /// The API integration secret token. - String _token; - - /// The API version. - String _v; - - /// The API date version. - /// - /// It's not the same as the API version. - String _dateVersion; - +class NotionPagesClient extends BaseClient { /// The path of the requests group. - String _path = 'pages'; + @override + final String path = 'pages'; /// Main Notion page client constructor. /// @@ -29,16 +20,14 @@ class NotionPagesClient { required String token, String version: latestVersion, String dateVersion: latestDateVersion, - }) : this._token = token, - this._v = version, - this._dateVersion = latestDateVersion; + }) : super(token: token, version: version, dateVersion: dateVersion); /// Retrieve the page with [id]. Future fetch(String id) async { http.Response res = - await http.get(Uri.https(host, '/$_v/$_path/$id'), headers: { - 'Authorization': 'Bearer $_token', - 'Notion-Version': _dateVersion, + await http.get(Uri.https(host, '/$v/$path/$id'), headers: { + 'Authorization': 'Bearer $token', + 'Notion-Version': dateVersion, }); return NotionResponse.fromResponse(res); @@ -46,12 +35,12 @@ class NotionPagesClient { /// Create a new [page]. Future create(Page page) async { - http.Response res = await http.post(Uri.https(host, '/$_v/$_path'), + http.Response res = await http.post(Uri.https(host, '/$v/$path'), body: jsonEncode(page.toJson()), headers: { - 'Authorization': 'Bearer $_token', + 'Authorization': 'Bearer $token', 'Content-Type': 'application/json; charset=UTF-8', - 'Notion-Version': _dateVersion, + 'Notion-Version': dateVersion, }); return NotionResponse.fromResponse(res); From bd49a600af020d270dd88ea58f81f4b48a7d50cf Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 10:39:00 -0500 Subject: [PATCH 04/14] Add bulleted list item support --- lib/notion/blocks/block.dart | 4 +- lib/notion/blocks/bulleted_list_item.dart | 46 ++++++++++++++ lib/notion/general/types/notion_types.dart | 4 +- lib/utils/utils.dart | 8 +-- test/blocks/bulleted_list_item_test.dart | 71 ++++++++++++++++++++++ test/notion_api_test.dart | 18 ++++++ test/utils_test.dart | 4 +- 7 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 lib/notion/blocks/bulleted_list_item.dart create mode 100644 test/blocks/bulleted_list_item_test.dart diff --git a/lib/notion/blocks/block.dart b/lib/notion/blocks/block.dart index 8cc37f1..152f90f 100644 --- a/lib/notion/blocks/block.dart +++ b/lib/notion/blocks/block.dart @@ -39,10 +39,10 @@ class Block extends BaseFields { bool get isToogle => this.type == BlockTypes.Toggle; /// Returns true if is a Bulleted block. - bool get isBulleted => this.type == BlockTypes.BulletedList; + bool get isBulletedItem => this.type == BlockTypes.BulletedListItem; /// Returns true if is a Numbered block. - bool get isNumbered => this.type == BlockTypes.NumberedList; + bool get isNumberedItem => this.type == BlockTypes.NumberedListItem; /// Returns true if is a Child block. bool get isChild => this.type == BlockTypes.Child; diff --git a/lib/notion/blocks/bulleted_list_item.dart b/lib/notion/blocks/bulleted_list_item.dart new file mode 100644 index 0000000..fb399ce --- /dev/null +++ b/lib/notion/blocks/bulleted_list_item.dart @@ -0,0 +1,46 @@ +import 'package:notion_api/notion/blocks/block.dart'; +import 'package:notion_api/notion/general/rich_text.dart'; +import 'package:notion_api/notion/general/types/notion_types.dart'; + +/// A representation of the Bulleted List Item Notion block object; +class BulletedItem extends Block { + /// The block type. Always BulletedListItem. + @override + final BlockTypes type = BlockTypes.BulletedListItem; + + List _content = []; + + /// The content of this block. + List get content => _content.toList(); + + /// Main bulleted list item constructor. + /// + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. + BulletedItem({ + Text? text, + List? texts, + }) { + if (text != null) { + _content.add(text); + } + if (texts != null) { + _content.addAll(texts); + } + } + + /// Add a new [text] to the rich text array and returns this instance. + BulletedItem add(Text text) { + this._content.add(text); + return this; + } + + /// Convert this to a valid json representation for the Notion API. + @override + Map toJson() => { + 'object': strObject, + 'type': strType, + strType: { + 'text': _content.map((e) => e.toJson()).toList(), + }, + }; +} diff --git a/lib/notion/general/types/notion_types.dart b/lib/notion/general/types/notion_types.dart index 6eccc8a..f9d0e3c 100644 --- a/lib/notion/general/types/notion_types.dart +++ b/lib/notion/general/types/notion_types.dart @@ -5,8 +5,8 @@ enum BlockTypes { H2, H3, Paragraph, - BulletedList, - NumberedList, + BulletedListItem, + NumberedListItem, ToDo, Toggle, Child, diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 5e39bdc..2d9760a 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -28,9 +28,9 @@ String blockTypeToString(BlockTypes type) { return 'heading_3'; case BlockTypes.Paragraph: return 'paragraph'; - case BlockTypes.BulletedList: + case BlockTypes.BulletedListItem: return 'bulleted_list_item'; - case BlockTypes.NumberedList: + case BlockTypes.NumberedListItem: return 'numbered_list_item'; case BlockTypes.Toggle: return 'toggle'; @@ -53,9 +53,9 @@ BlockTypes stringToBlockType(String type) { case 'paragraph': return BlockTypes.Paragraph; case 'bulleted_list_item': - return BlockTypes.BulletedList; + return BlockTypes.BulletedListItem; case 'numbered_list_item': - return BlockTypes.NumberedList; + return BlockTypes.NumberedListItem; case 'toogle': return BlockTypes.Toggle; case 'to_do': diff --git a/test/blocks/bulleted_list_item_test.dart b/test/blocks/bulleted_list_item_test.dart new file mode 100644 index 0000000..1ad7174 --- /dev/null +++ b/test/blocks/bulleted_list_item_test.dart @@ -0,0 +1,71 @@ +import 'package:notion_api/notion/blocks/bulleted_list_item.dart'; +import 'package:notion_api/notion/general/rich_text.dart'; +import 'package:notion_api/notion/general/types/notion_types.dart'; +import 'package:notion_api/utils/utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('BulletedListItem tests =>', () { + test('Create an empty instance', () { + BulletedItem block = BulletedItem(); + + expect(block, isNotNull); + expect(block.strType, blockTypeToString(BlockTypes.BulletedListItem)); + expect(block.content, allOf([isList, isEmpty])); + expect(block.isBulletedItem, true); + expect(block.type, BlockTypes.BulletedListItem); + }); + + test('Create an instance with information', () { + BulletedItem block = BulletedItem(text: Text('A')).add(Text('B')); + + expect(block.content.length, 2); + expect(block.content.first.text, 'A'); + expect(block.content.last.text, 'B'); + }); + + test('Create an instance with mixed information', () { + BulletedItem block = BulletedItem( + text: Text('first'), + texts: [ + Text('foo'), + Text('bar'), + ], + ).add(Text('last')); + + expect(block.content.length, 4); + expect(block.content.first.text, 'first'); + expect(block.content.last.text, 'last'); + }); + + test('Create json from instance', () { + Map json = BulletedItem(text: Text('A')).toJson(); + + expect( + json['type'], + allOf([ + isNotNull, + isNotEmpty, + blockTypeToString(BlockTypes.BulletedListItem) + ])); + expect(json, contains(blockTypeToString(BlockTypes.BulletedListItem))); + expect(json[blockTypeToString(BlockTypes.BulletedListItem)]['text'], + allOf([isList, isNotEmpty])); + }); + + test('Create json from empty instance', () { + Map json = BulletedItem().toJson(); + + expect( + json['type'], + allOf([ + isNotNull, + isNotEmpty, + blockTypeToString(BlockTypes.BulletedListItem) + ])); + expect(json, contains(blockTypeToString(BlockTypes.BulletedListItem))); + expect(json[blockTypeToString(BlockTypes.BulletedListItem)]['text'], + allOf([isList, isEmpty])); + }); + }); +} diff --git a/test/notion_api_test.dart b/test/notion_api_test.dart index c795686..82234b4 100644 --- a/test/notion_api_test.dart +++ b/test/notion_api_test.dart @@ -1,6 +1,7 @@ import 'dart:io' show Platform; import 'package:dotenv/dotenv.dart' show load, env, clean; +import 'package:notion_api/notion/blocks/bulleted_list_item.dart'; import 'package:notion_api/notion/blocks/heading.dart'; import 'package:notion_api/notion/blocks/paragraph.dart'; import 'package:notion_api/notion/blocks/todo.dart'; @@ -204,5 +205,22 @@ void main() { expect(res.status, 200); expect(res.isOk, true); }); + + test('Append bulleted list item block', () async { + final NotionBlockClient blocks = NotionBlockClient(token: token ?? ''); + + NotionResponse res = await blocks.append( + to: testBlockId as String, + children: Children.withBlocks( + [ + BulletedItem(text: Text('This is a bulleted list item A')), + BulletedItem(text: Text('This is a bulleted list item B')), + ], + ), + ); + + expect(res.status, 200); + expect(res.isOk, true); + }); }); } diff --git a/test/utils_test.dart b/test/utils_test.dart index 4deffc8..c227db2 100644 --- a/test/utils_test.dart +++ b/test/utils_test.dart @@ -151,8 +151,8 @@ void main() { group('(Types to String) || (String to Type tests) =>', () { test('Block types', () { String strParagraph = blockTypeToString(BlockTypes.Paragraph); - String strBulleted = blockTypeToString(BlockTypes.BulletedList); - String strNumbered = blockTypeToString(BlockTypes.NumberedList); + String strBulleted = blockTypeToString(BlockTypes.BulletedListItem); + String strNumbered = blockTypeToString(BlockTypes.NumberedListItem); String strToogle = blockTypeToString(BlockTypes.Toggle); String strChild = blockTypeToString(BlockTypes.Child); From d6b65447aa8be9ae3b52f3fd33a29f5403764840 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 10:45:31 -0500 Subject: [PATCH 05/14] Add numbered list item support --- lib/notion/blocks/numbered_list_item.dart | 46 +++++++++++++++ test/blocks/numbered_list_item_test.dart | 71 +++++++++++++++++++++++ test/notion_api_test.dart | 18 ++++++ 3 files changed, 135 insertions(+) create mode 100644 lib/notion/blocks/numbered_list_item.dart create mode 100644 test/blocks/numbered_list_item_test.dart diff --git a/lib/notion/blocks/numbered_list_item.dart b/lib/notion/blocks/numbered_list_item.dart new file mode 100644 index 0000000..6fec1a8 --- /dev/null +++ b/lib/notion/blocks/numbered_list_item.dart @@ -0,0 +1,46 @@ +import 'package:notion_api/notion/blocks/block.dart'; +import 'package:notion_api/notion/general/rich_text.dart'; +import 'package:notion_api/notion/general/types/notion_types.dart'; + +/// A representation of the Bulleted List Item Notion block object; +class NumberedItem extends Block { + /// The block type. Always NumberedListItem. + @override + final BlockTypes type = BlockTypes.NumberedListItem; + + List _content = []; + + /// The content of this block. + List get content => _content.toList(); + + /// Main numbered list item constructor. + /// + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. + NumberedItem({ + Text? text, + List? texts, + }) { + if (text != null) { + _content.add(text); + } + if (texts != null) { + _content.addAll(texts); + } + } + + /// Add a new [text] to the rich text array and returns this instance. + NumberedItem add(Text text) { + this._content.add(text); + return this; + } + + /// Convert this to a valid json representation for the Notion API. + @override + Map toJson() => { + 'object': strObject, + 'type': strType, + strType: { + 'text': _content.map((e) => e.toJson()).toList(), + }, + }; +} diff --git a/test/blocks/numbered_list_item_test.dart b/test/blocks/numbered_list_item_test.dart new file mode 100644 index 0000000..74d90a3 --- /dev/null +++ b/test/blocks/numbered_list_item_test.dart @@ -0,0 +1,71 @@ +import 'package:notion_api/notion/blocks/numbered_list_item.dart'; +import 'package:notion_api/notion/general/rich_text.dart'; +import 'package:notion_api/notion/general/types/notion_types.dart'; +import 'package:notion_api/utils/utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('NumberedListItem tests =>', () { + test('Create an empty instance', () { + NumberedItem block = NumberedItem(); + + expect(block, isNotNull); + expect(block.strType, blockTypeToString(BlockTypes.NumberedListItem)); + expect(block.content, allOf([isList, isEmpty])); + expect(block.isNumberedItem, true); + expect(block.type, BlockTypes.NumberedListItem); + }); + + test('Create an instance with information', () { + NumberedItem block = NumberedItem(text: Text('A')).add(Text('B')); + + expect(block.content.length, 2); + expect(block.content.first.text, 'A'); + expect(block.content.last.text, 'B'); + }); + + test('Create an instance with mixed information', () { + NumberedItem block = NumberedItem( + text: Text('first'), + texts: [ + Text('foo'), + Text('bar'), + ], + ).add(Text('last')); + + expect(block.content.length, 4); + expect(block.content.first.text, 'first'); + expect(block.content.last.text, 'last'); + }); + + test('Create json from instance', () { + Map json = NumberedItem(text: Text('A')).toJson(); + + expect( + json['type'], + allOf([ + isNotNull, + isNotEmpty, + blockTypeToString(BlockTypes.NumberedListItem) + ])); + expect(json, contains(blockTypeToString(BlockTypes.NumberedListItem))); + expect(json[blockTypeToString(BlockTypes.NumberedListItem)]['text'], + allOf([isList, isNotEmpty])); + }); + + test('Create json from empty instance', () { + Map json = NumberedItem().toJson(); + + expect( + json['type'], + allOf([ + isNotNull, + isNotEmpty, + blockTypeToString(BlockTypes.NumberedListItem) + ])); + expect(json, contains(blockTypeToString(BlockTypes.NumberedListItem))); + expect(json[blockTypeToString(BlockTypes.NumberedListItem)]['text'], + allOf([isList, isEmpty])); + }); + }); +} diff --git a/test/notion_api_test.dart b/test/notion_api_test.dart index 82234b4..4831d0e 100644 --- a/test/notion_api_test.dart +++ b/test/notion_api_test.dart @@ -3,6 +3,7 @@ import 'dart:io' show Platform; import 'package:dotenv/dotenv.dart' show load, env, clean; import 'package:notion_api/notion/blocks/bulleted_list_item.dart'; import 'package:notion_api/notion/blocks/heading.dart'; +import 'package:notion_api/notion/blocks/numbered_list_item.dart'; import 'package:notion_api/notion/blocks/paragraph.dart'; import 'package:notion_api/notion/blocks/todo.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; @@ -222,5 +223,22 @@ void main() { expect(res.status, 200); expect(res.isOk, true); }); + + test('Append numbered list item block', () async { + final NotionBlockClient blocks = NotionBlockClient(token: token ?? ''); + + NotionResponse res = await blocks.append( + to: testBlockId as String, + children: Children.withBlocks( + [ + NumberedItem(text: Text('This is a numbered list item A')), + NumberedItem(text: Text('This is a numbered list item B')), + ], + ), + ); + + expect(res.status, 200); + expect(res.isOk, true); + }); }); } From cd5a92da30bf554db07853d2a961b33e9dd756ff Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 11:08:30 -0500 Subject: [PATCH 06/14] Add toggle block support --- lib/notion/blocks/toggle.dart | 67 ++++++++++++++++++++++++++ test/blocks/toggle_test.dart | 88 +++++++++++++++++++++++++++++++++++ test/notion_api_test.dart | 25 ++++++++++ 3 files changed, 180 insertions(+) create mode 100644 lib/notion/blocks/toggle.dart create mode 100644 test/blocks/toggle_test.dart diff --git a/lib/notion/blocks/toggle.dart b/lib/notion/blocks/toggle.dart new file mode 100644 index 0000000..36e0e99 --- /dev/null +++ b/lib/notion/blocks/toggle.dart @@ -0,0 +1,67 @@ +import 'package:notion_api/notion/blocks/block.dart'; +import 'package:notion_api/notion/general/rich_text.dart'; +import 'package:notion_api/notion/general/types/notion_types.dart'; + +/// A representation of the Bulleted List Item Notion block object; +class Toggle extends Block { + /// The block type. Always Toggle. + @override + final BlockTypes type = BlockTypes.Toggle; + + List _content = []; + List _children = []; + + /// The content of this block. + List get content => _content.toList(); + + /// The children of this block. + List get children => _children.toList(); + + /// Main toggle constructor. + /// + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the content of the toggle item. + Toggle({ + Text? text, + List? texts, + List? children, + }) { + if (text != null) { + _content.add(text); + } + if (texts != null) { + _content.addAll(texts); + } + if (children != null) { + _children.addAll(children); + } + } + + /// Add a new [text] to the rich text array and returns this instance. + Toggle addText(Text text) { + this._content.add(text); + return this; + } + + /// Add a new [block] to the children block and returns this instance. + Toggle addChild(Block block) { + this._children.add(block); + return this; + } + + /// Add a list of [blocks] to the children block and returns this instance. + Toggle addChildren(List blocks) { + this._children.addAll(blocks); + return this; + } + + /// Convert this to a valid json representation for the Notion API. + @override + Map toJson() => { + 'object': strObject, + 'type': strType, + strType: { + 'text': _content.map((e) => e.toJson()).toList(), + 'children': _children.map((e) => e.toJson()).toList(), + }, + }; +} diff --git a/test/blocks/toggle_test.dart b/test/blocks/toggle_test.dart new file mode 100644 index 0000000..2232df0 --- /dev/null +++ b/test/blocks/toggle_test.dart @@ -0,0 +1,88 @@ +import 'package:notion_api/notion/blocks/bulleted_list_item.dart'; +import 'package:notion_api/notion/blocks/numbered_list_item.dart'; +import 'package:notion_api/notion/blocks/paragraph.dart'; +import 'package:notion_api/notion/blocks/toggle.dart'; +import 'package:notion_api/notion/general/rich_text.dart'; +import 'package:notion_api/notion/general/types/notion_types.dart'; +import 'package:notion_api/utils/utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('Toggle tests =>', () { + test('Create an empty instance', () { + Toggle block = Toggle(); + + expect(block, isNotNull); + expect(block.strType, blockTypeToString(BlockTypes.Toggle)); + expect(block.content, allOf([isList, isEmpty])); + expect(block.children, allOf([isList, isEmpty])); + expect(block.isToogle, true); + expect(block.type, BlockTypes.Toggle); + }); + + test('Create an instance with information', () { + Toggle block = Toggle(text: Text('A')) + .addText(Text('B')) + .addChild( + Paragraph(text: Text('This is a child of the toggle item.'))) + .addChildren([ + BulletedItem(text: Text('First bulleted item')), + NumberedItem(text: Text('First numbered item')), + ]); + + expect(block.content.length, 2); + expect(block.content.first.text, 'A'); + expect(block.content.last.text, 'B'); + expect(block.children.length, 3); + }); + + test('Create an instance with mixed information', () { + Toggle block = Toggle( + text: Text('first'), + texts: [ + Text('foo'), + Text('bar'), + ], + ) + .addText(Text('last')) + .addChild( + Paragraph(text: Text('This is a child of the toggle item.'))) + .addChildren([ + BulletedItem(text: Text('First bulleted item')), + NumberedItem(text: Text('First numbered item')), + ]); + + expect(block.content.length, 4); + expect(block.content.first.text, 'first'); + expect(block.content.last.text, 'last'); + expect(block.children.length, 3); + }); + + test('Create json from instance', () { + Map json = Toggle(text: Text('A'), children: [ + BulletedItem(text: Text('First bulleted item')), + NumberedItem(text: Text('First numbered item')), + ]).toJson(); + + expect(json['type'], + allOf([isNotNull, isNotEmpty, blockTypeToString(BlockTypes.Toggle)])); + expect(json, contains(blockTypeToString(BlockTypes.Toggle))); + expect(json[blockTypeToString(BlockTypes.Toggle)]['text'], + allOf([isList, isNotEmpty])); + expect(json[blockTypeToString(BlockTypes.Toggle)]['children'], + allOf([isList, isNotEmpty])); + }); + + test('Create json from empty instance', () { + Map json = Toggle().toJson(); + + expect(json['type'], + allOf([isNotNull, isNotEmpty, blockTypeToString(BlockTypes.Toggle)])); + expect(json, contains(blockTypeToString(BlockTypes.Toggle))); + expect(json[blockTypeToString(BlockTypes.Toggle)]['text'], + allOf([isList, isEmpty])); + expect(json[blockTypeToString(BlockTypes.Toggle)]['children'], + allOf([isList, isEmpty])); + }); + }); +} diff --git a/test/notion_api_test.dart b/test/notion_api_test.dart index 4831d0e..4b7f7fc 100644 --- a/test/notion_api_test.dart +++ b/test/notion_api_test.dart @@ -6,6 +6,7 @@ import 'package:notion_api/notion/blocks/heading.dart'; import 'package:notion_api/notion/blocks/numbered_list_item.dart'; import 'package:notion_api/notion/blocks/paragraph.dart'; import 'package:notion_api/notion/blocks/todo.dart'; +import 'package:notion_api/notion/blocks/toggle.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; import 'package:notion_api/notion/general/lists/children.dart'; import 'package:notion_api/notion/objects/pages.dart'; @@ -240,5 +241,29 @@ void main() { expect(res.status, 200); expect(res.isOk, true); }); + + test('Append toggle block', () async { + final NotionBlockClient blocks = NotionBlockClient(token: token ?? ''); + + NotionResponse res = await blocks.append( + to: testBlockId as String, + children: Children.withBlocks( + [ + Toggle(text: Text('This is a toggle block'), children: [ + Paragraph(texts: [ + Text( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas venenatis dolor sed ex egestas, et vehicula tellus faucibus. Sed pellentesque tellus eget imperdiet vulputate.') + ]), + BulletedItem(text: Text('A')), + BulletedItem(text: Text('B')), + BulletedItem(text: Text('B')), + ]), + ], + ), + ); + + expect(res.status, 200); + expect(res.isOk, true); + }); }); } From cd47e9b4d10e198a49dc163d10ba801be488ac27 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 12:09:31 -0500 Subject: [PATCH 07/14] Add children field for blocks and deprecate some fields --- lib/notion/blocks/bulleted_list_item.dart | 32 +++++++++++---- lib/notion/blocks/heading.dart | 7 ++++ lib/notion/blocks/numbered_list_item.dart | 33 +++++++++++---- lib/notion/blocks/paragraph.dart | 45 ++++++++++++++++---- lib/notion/blocks/todo.dart | 50 ++++++++++++++++++----- lib/notion/blocks/toggle.dart | 24 +++++------ lib/notion/general/rich_text.dart | 5 ++- test/blocks/bulleted_list_item_test.dart | 21 ++++++++-- test/blocks/heading_test.dart | 4 +- test/blocks/numbered_list_item_test.dart | 20 +++++++-- test/blocks/paragraph_test.dart | 39 ++++++++++++------ test/blocks/todo_test.dart | 20 +++++++-- test/blocks/toggle_test.dart | 4 +- test/objects/page_test.dart | 4 +- 14 files changed, 233 insertions(+), 75 deletions(-) diff --git a/lib/notion/blocks/bulleted_list_item.dart b/lib/notion/blocks/bulleted_list_item.dart index fb399ce..07b11ff 100644 --- a/lib/notion/blocks/bulleted_list_item.dart +++ b/lib/notion/blocks/bulleted_list_item.dart @@ -9,28 +9,44 @@ class BulletedItem extends Block { final BlockTypes type = BlockTypes.BulletedListItem; List _content = []; + List _children = []; /// The content of this block. List get content => _content.toList(); + /// The children of this block. + List get children => _children.toList(); + /// Main bulleted list item constructor. /// - /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the [children] of the block. BulletedItem({ Text? text, - List? texts, + List texts: const [], + List children: const [], }) { if (text != null) { _content.add(text); } - if (texts != null) { - _content.addAll(texts); - } + _content.addAll(texts); + _children.addAll(children); + } + + /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. + BulletedItem addText(String text, {TextAnnotations? annotations}) { + this._content.add(Text(text, annotations: annotations)); + return this; + } + + /// Add a new [block] to the children and returns this instance. + BulletedItem addChild(Block block) { + this._children.add(block); + return this; } - /// Add a new [text] to the rich text array and returns this instance. - BulletedItem add(Text text) { - this._content.add(text); + /// Add a list of [blocks] to the children and returns this instance. + BulletedItem addChildren(List blocks) { + this._children.addAll(blocks); return this; } diff --git a/lib/notion/blocks/heading.dart b/lib/notion/blocks/heading.dart index 34b7238..ba261eb 100644 --- a/lib/notion/blocks/heading.dart +++ b/lib/notion/blocks/heading.dart @@ -44,11 +44,18 @@ class Heading extends Block { } /// Add a new [text] to the paragraph content and returns this instance. + @Deprecated('Use `addText(Block)` instead') Heading add(Text text) { this._content.add(text); return this; } + /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. + Heading addText(String text, {TextAnnotations? annotations}) { + this._content.add(Text(text, annotations: annotations)); + return this; + } + /// Convert this to a valid json representation for the Notion API. @override Map toJson() => { diff --git a/lib/notion/blocks/numbered_list_item.dart b/lib/notion/blocks/numbered_list_item.dart index 6fec1a8..7b7b5b9 100644 --- a/lib/notion/blocks/numbered_list_item.dart +++ b/lib/notion/blocks/numbered_list_item.dart @@ -9,28 +9,44 @@ class NumberedItem extends Block { final BlockTypes type = BlockTypes.NumberedListItem; List _content = []; + List _children = []; /// The content of this block. List get content => _content.toList(); + /// The children of this block. + List get children => _children.toList(); + /// Main numbered list item constructor. /// - /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the [children] of the block. NumberedItem({ Text? text, - List? texts, + List texts: const [], + List children: const [], }) { if (text != null) { _content.add(text); } - if (texts != null) { - _content.addAll(texts); - } + _content.addAll(texts); + _children.addAll(children); + } + + /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. + NumberedItem addText(String text, {TextAnnotations? annotations}) { + this._content.add(Text(text, annotations: annotations)); + return this; + } + + /// Add a new [block] to the children and returns this instance. + NumberedItem addChild(Block block) { + this._children.add(block); + return this; } - /// Add a new [text] to the rich text array and returns this instance. - NumberedItem add(Text text) { - this._content.add(text); + /// Add a list of [blocks] to the children and returns this instance. + NumberedItem addChildren(List blocks) { + this._children.addAll(blocks); return this; } @@ -41,6 +57,7 @@ class NumberedItem extends Block { 'type': strType, strType: { 'text': _content.map((e) => e.toJson()).toList(), + 'children': _children.map((e) => e.toJson()).toList(), }, }; } diff --git a/lib/notion/blocks/paragraph.dart b/lib/notion/blocks/paragraph.dart index 7749f55..a3c0e97 100644 --- a/lib/notion/blocks/paragraph.dart +++ b/lib/notion/blocks/paragraph.dart @@ -9,37 +9,67 @@ class Paragraph extends Block { final BlockTypes type = BlockTypes.Paragraph; List _content = []; + List _children = []; /// The separator for the Text objects. + @Deprecated('Text separation will be by your own') String textSeparator; /// The content of this block. + @Deprecated('Instead use `content`') List get texts => _content.toList(); + /// The content of this block. + List get content => _content.toList(); + + /// The children of this block. + List get children => _children.toList(); + /// Main paragraph constructor. /// - /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the [children] of the block. + /// + /// _Deprecated:_ [textSeparator] will be removed and the separation will be by your own. This because that's the same way that `Text` & `RichText` works on Flutter. In this way you can add annotations for a part of a word instead of only full words or phrases. /// /// Also a [textSeparator] can be anexed to separate the texts on the json generated using the `toJson()` function. The separator is used because when the text is displayed is all together without any kind of separation and adding the separator that behavior is avoided. By default the [textSeparator] is an space (" "). Paragraph({ Text? text, - List? texts, - this.textSeparator: ' ', + List texts: const [], + List children: const [], + @deprecated this.textSeparator: ' ', }) { if (text != null) { this._content.add(text); } - if (texts != null) { - this._content.addAll(texts); - } + this._content.addAll(texts); + this._children.addAll(children); } /// Add a new [text] to the paragraph content and returns this instance. + @Deprecated('Use `addText(Block)` instead') Paragraph add(Text text) { this._content.add(text); return this; } + /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. + Paragraph addText(String text, {TextAnnotations? annotations}) { + this._content.add(Text(text, annotations: annotations)); + return this; + } + + /// Add a new [block] to the children and returns this instance. + Paragraph addChild(Block block) { + this._children.add(block); + return this; + } + + /// Add a list of [blocks] to the children and returns this instance. + Paragraph addChildren(List blocks) { + this._children.addAll(blocks); + return this; + } + /// Convert this to a valid json representation for the Notion API. @override Map toJson() => { @@ -48,7 +78,8 @@ class Paragraph extends Block { strType: { 'text': _content .map((e) => e.toJson(textSeparator: textSeparator)) - .toList() + .toList(), + 'children': _children.map((e) => e.toJson()).toList(), } }; } diff --git a/lib/notion/blocks/todo.dart b/lib/notion/blocks/todo.dart index 5ee8a11..056696e 100644 --- a/lib/notion/blocks/todo.dart +++ b/lib/notion/blocks/todo.dart @@ -9,8 +9,10 @@ class ToDo extends Block { final BlockTypes type = BlockTypes.ToDo; List _content = []; + List _children = []; /// The separator for the Text objects. + @Deprecated('Text separation will be by your own') String textSeparator; /// The checked value. @@ -19,24 +21,30 @@ class ToDo extends Block { /// The content of this block. List get content => _content.toList(); + /// The children of this block. + List get children => _children.toList(); + /// Main to do constructor. /// - /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. - /// - /// Also a [textSeparator] can be anexed to separate the texts on the json generated using the `toJson()` function. The separator is used because when the text is displayed is all together without any kind of separation and adding the separator that behavior is avoided. By default the [textSeparator] is an space (" "). + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the [children] of the block. /// /// The [checked] field define if the To do option is marked as done. By default is false. - ToDo( - {Text? text, - List? texts, - this.textSeparator: ' ', - this.checked: false}) { + /// + /// _Deprecated:_ [textSeparator] will be removed and the separation will be by your own. This because that's the same way that `Text` & `RichText` works on Flutter. In this way you can add annotations for a part of a word instead of only full words or phrases. + /// + /// Also a [textSeparator] can be anexed to separate the texts on the json generated using the `toJson()` function. The separator is used because when the text is displayed is all together without any kind of separation and adding the separator that behavior is avoided. By default the [textSeparator] is an space (" "). + ToDo({ + Text? text, + List texts: const [], + List children: const [], + this.checked: false, + @deprecated this.textSeparator: ' ', + }) { if (text != null) { this._content.add(text); } - if (texts != null) { - this._content.addAll(texts); - } + this._content.addAll(texts); + this._children.addAll(children); } // TODO: A function that create an instance of ToDo (or Paragraph or Heading) from a Block. @@ -49,11 +57,30 @@ class ToDo extends Block { // } /// Add a new [text] to the paragraph content and returns this instance. + @Deprecated('Use `addText(Block)` instead') ToDo add(Text text) { this._content.add(text); return this; } + /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. + ToDo addText(String text, {TextAnnotations? annotations}) { + this._content.add(Text(text, annotations: annotations)); + return this; + } + + /// Add a new [block] to the children and returns this instance. + ToDo addChild(Block block) { + this._children.add(block); + return this; + } + + /// Add a list of [blocks] to the children and returns this instance. + ToDo addChildren(List blocks) { + this._children.addAll(blocks); + return this; + } + /// Convert this to a valid json representation for the Notion API. @override Map toJson() => { @@ -63,6 +90,7 @@ class ToDo extends Block { 'text': _content .map((e) => e.toJson(textSeparator: textSeparator)) .toList(), + 'children': _children.map((e) => e.toJson()).toList(), 'checked': checked, } }; diff --git a/lib/notion/blocks/toggle.dart b/lib/notion/blocks/toggle.dart index 36e0e99..db15fd1 100644 --- a/lib/notion/blocks/toggle.dart +++ b/lib/notion/blocks/toggle.dart @@ -19,36 +19,32 @@ class Toggle extends Block { /// Main toggle constructor. /// - /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the content of the toggle item. + /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the [children] of the block. Toggle({ Text? text, - List? texts, - List? children, + List texts: const [], + List children: const [], }) { if (text != null) { _content.add(text); } - if (texts != null) { - _content.addAll(texts); - } - if (children != null) { - _children.addAll(children); - } + _content.addAll(texts); + _children.addAll(children); } - /// Add a new [text] to the rich text array and returns this instance. - Toggle addText(Text text) { - this._content.add(text); + /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. + Toggle addText(String text, {TextAnnotations? annotations}) { + this._content.add(Text(text, annotations: annotations)); return this; } - /// Add a new [block] to the children block and returns this instance. + /// Add a new [block] to the children and returns this instance. Toggle addChild(Block block) { this._children.add(block); return this; } - /// Add a list of [blocks] to the children block and returns this instance. + /// Add a list of [blocks] to the children and returns this instance. Toggle addChildren(List blocks) { this._children.addAll(blocks); return this; diff --git a/lib/notion/general/rich_text.dart b/lib/notion/general/rich_text.dart index 58b7a78..47201b9 100644 --- a/lib/notion/general/rich_text.dart +++ b/lib/notion/general/rich_text.dart @@ -29,6 +29,8 @@ class Text { /// Convert this to a json representation valid for the Notion API. /// + ///_Deprecated:_ [textSeparator] will be removed and the separation will be by your own. This because that's the same way that `Text` & `RichText` works on Flutter. In this way you can add annotations for a part of a word instead of only full words or phrases. + /// /// If a [textSeparator] is given, then it's value (by default a space) is append /// at the end of the string to allow be at the same level of other Text objects without /// being all together. For example: @@ -55,7 +57,8 @@ class Text { /// textSeparator: '-'))); /// // append => "A-B-" /// ``` - Map toJson({String textSeparator: ''}) { + Map toJson( + {@Deprecated('Will not have replacement') String textSeparator: ''}) { Map json = { 'type': _type, 'text': { diff --git a/test/blocks/bulleted_list_item_test.dart b/test/blocks/bulleted_list_item_test.dart index 1ad7174..84a3561 100644 --- a/test/blocks/bulleted_list_item_test.dart +++ b/test/blocks/bulleted_list_item_test.dart @@ -1,4 +1,5 @@ import 'package:notion_api/notion/blocks/bulleted_list_item.dart'; +import 'package:notion_api/notion/blocks/paragraph.dart'; import 'package:notion_api/notion/general/rich_text.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; import 'package:notion_api/utils/utils.dart'; @@ -12,12 +13,13 @@ void main() { expect(block, isNotNull); expect(block.strType, blockTypeToString(BlockTypes.BulletedListItem)); expect(block.content, allOf([isList, isEmpty])); + expect(block.children, allOf([isList, isEmpty])); expect(block.isBulletedItem, true); expect(block.type, BlockTypes.BulletedListItem); }); test('Create an instance with information', () { - BulletedItem block = BulletedItem(text: Text('A')).add(Text('B')); + BulletedItem block = BulletedItem(text: Text('A')).addText('B'); expect(block.content.length, 2); expect(block.content.first.text, 'A'); @@ -31,15 +33,24 @@ void main() { Text('foo'), Text('bar'), ], - ).add(Text('last')); + ).addText('last').addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])); expect(block.content.length, 4); expect(block.content.first.text, 'first'); expect(block.content.last.text, 'last'); + expect(block.children.length, 1); }); test('Create json from instance', () { - Map json = BulletedItem(text: Text('A')).toJson(); + Map json = BulletedItem(text: Text('A')) + .addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])) + .toJson(); expect( json['type'], @@ -51,6 +62,8 @@ void main() { expect(json, contains(blockTypeToString(BlockTypes.BulletedListItem))); expect(json[blockTypeToString(BlockTypes.BulletedListItem)]['text'], allOf([isList, isNotEmpty])); + expect(json[blockTypeToString(BlockTypes.BulletedListItem)]['children'], + allOf([isList, isNotEmpty])); }); test('Create json from empty instance', () { @@ -66,6 +79,8 @@ void main() { expect(json, contains(blockTypeToString(BlockTypes.BulletedListItem))); expect(json[blockTypeToString(BlockTypes.BulletedListItem)]['text'], allOf([isList, isEmpty])); + expect(json[blockTypeToString(BlockTypes.BulletedListItem)]['children'], + allOf([isList, isEmpty])); }); }); } diff --git a/test/blocks/heading_test.dart b/test/blocks/heading_test.dart index 375cdb0..5955ce0 100644 --- a/test/blocks/heading_test.dart +++ b/test/blocks/heading_test.dart @@ -33,7 +33,7 @@ void main() { }); test('Create an instance with information', () { - Heading heading = Heading(text: Text('A')).add(Text('B')); + Heading heading = Heading(text: Text('A')).addText('B'); expect(heading.content.length, 2); expect(heading.content.first.text, 'A'); @@ -43,7 +43,7 @@ void main() { test('Create an instance with mixed information', () { Heading heading = Heading(text: Text('first'), texts: [Text('foo'), Text('bar')]) - .add(Text('last')); + .addText('last'); expect(heading.content.length, 4); expect(heading.content.first.text, 'first'); diff --git a/test/blocks/numbered_list_item_test.dart b/test/blocks/numbered_list_item_test.dart index 74d90a3..1443bdd 100644 --- a/test/blocks/numbered_list_item_test.dart +++ b/test/blocks/numbered_list_item_test.dart @@ -1,4 +1,5 @@ import 'package:notion_api/notion/blocks/numbered_list_item.dart'; +import 'package:notion_api/notion/blocks/paragraph.dart'; import 'package:notion_api/notion/general/rich_text.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; import 'package:notion_api/utils/utils.dart'; @@ -17,7 +18,7 @@ void main() { }); test('Create an instance with information', () { - NumberedItem block = NumberedItem(text: Text('A')).add(Text('B')); + NumberedItem block = NumberedItem(text: Text('A')).addText('B'); expect(block.content.length, 2); expect(block.content.first.text, 'A'); @@ -31,15 +32,24 @@ void main() { Text('foo'), Text('bar'), ], - ).add(Text('last')); + ).addText('last').addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])); expect(block.content.length, 4); expect(block.content.first.text, 'first'); expect(block.content.last.text, 'last'); + expect(block.content.length, 1); }); test('Create json from instance', () { - Map json = NumberedItem(text: Text('A')).toJson(); + Map json = NumberedItem(text: Text('A')) + .addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])) + .toJson(); expect( json['type'], @@ -51,6 +61,8 @@ void main() { expect(json, contains(blockTypeToString(BlockTypes.NumberedListItem))); expect(json[blockTypeToString(BlockTypes.NumberedListItem)]['text'], allOf([isList, isNotEmpty])); + expect(json[blockTypeToString(BlockTypes.NumberedListItem)]['children'], + allOf([isList, isNotEmpty])); }); test('Create json from empty instance', () { @@ -66,6 +78,8 @@ void main() { expect(json, contains(blockTypeToString(BlockTypes.NumberedListItem))); expect(json[blockTypeToString(BlockTypes.NumberedListItem)]['text'], allOf([isList, isEmpty])); + expect(json[blockTypeToString(BlockTypes.NumberedListItem)]['children'], + allOf([isList, isEmpty])); }); }); } diff --git a/test/blocks/paragraph_test.dart b/test/blocks/paragraph_test.dart index 7faf7ee..4970560 100644 --- a/test/blocks/paragraph_test.dart +++ b/test/blocks/paragraph_test.dart @@ -11,32 +11,43 @@ void main() { expect(paragraph, isNotNull); expect(paragraph.strType, blockTypeToString(BlockTypes.Paragraph)); - expect(paragraph.texts, allOf([isList, isEmpty])); + expect(paragraph.content, allOf([isList, isEmpty])); expect(paragraph.isParagraph, true); expect(paragraph.type, BlockTypes.Paragraph); }); test('Create an instance with information', () { - Paragraph paragraph = Paragraph().add(Text('A')).add(Text('B')); + Paragraph paragraph = Paragraph().addText('A').addText('B'); - expect(paragraph.texts.length, 2); - expect(paragraph.texts.first.text, 'A'); - expect(paragraph.texts.last.text, 'B'); + expect(paragraph.content.length, 2); + expect(paragraph.content.first.text, 'A'); + expect(paragraph.content.last.text, 'B'); }); test('Create an instance with mixed information', () { Paragraph paragraph = Paragraph(text: Text('first'), texts: [Text('foo'), Text('bar')]) - .add(Text('last')); + .addText('last') + .addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])); - expect(paragraph.texts.length, 4); - expect(paragraph.texts.first.text, 'first'); - expect(paragraph.texts.last.text, 'last'); + expect(paragraph.content.length, 4); + expect(paragraph.content.first.text, 'first'); + expect(paragraph.content.last.text, 'last'); + expect(paragraph.children.length, 4); }); test('Create json from instance', () { - Map json = - Paragraph().add(Text('A')).add(Text('B')).toJson(); + Map json = Paragraph() + .addText('A') + .addText('B') + .addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])) + .toJson(); expect( json['type'], @@ -48,6 +59,8 @@ void main() { expect(json, contains(blockTypeToString(BlockTypes.Paragraph))); expect(json[blockTypeToString(BlockTypes.Paragraph)]['text'], allOf([isList, isNotEmpty])); + expect(json[blockTypeToString(BlockTypes.Paragraph)]['children'], + allOf([isList, isNotEmpty])); }); test('Create json from emppty instance', () { @@ -63,6 +76,8 @@ void main() { expect(json, contains(blockTypeToString(BlockTypes.Paragraph))); expect(json[blockTypeToString(BlockTypes.Paragraph)]['text'], allOf([isList, isEmpty])); + expect(json[blockTypeToString(BlockTypes.Paragraph)]['children'], + allOf([isList, isEmpty])); }); test('Create json with separator', () { @@ -70,7 +85,7 @@ void main() { String separator = '-'; Map json = - Paragraph(textSeparator: separator).add(Text(char)).toJson(); + Paragraph(textSeparator: separator).addText(char).toJson(); List jsonTexts = json[blockTypeToString(BlockTypes.Paragraph)]['text']; diff --git a/test/blocks/todo_test.dart b/test/blocks/todo_test.dart index 86ebe66..b8c59b2 100644 --- a/test/blocks/todo_test.dart +++ b/test/blocks/todo_test.dart @@ -1,3 +1,4 @@ +import 'package:notion_api/notion/blocks/paragraph.dart'; import 'package:notion_api/notion/blocks/todo.dart'; import 'package:notion_api/notion/general/rich_text.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; @@ -18,7 +19,7 @@ void main() { }); test('Create an instance with information', () { - ToDo todo = ToDo(text: Text('A'), checked: true).add(Text('B')); + ToDo todo = ToDo(text: Text('A'), checked: true).addText('B'); expect(todo.checked, true); expect(todo.content.length, 2); @@ -34,22 +35,33 @@ void main() { Text('bar'), ], checked: true, - ).add(Text('last')); + ).addText('last').addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])); expect(todo.checked, true); expect(todo.content.length, 4); expect(todo.content.first.text, 'first'); expect(todo.content.last.text, 'last'); + expect(todo.children.length, 4); }); test('Create json from instance', () { - Map json = ToDo(text: Text('A')).toJson(); + Map json = ToDo(text: Text('A')) + .addChild(Paragraph(texts: [ + Text('A'), + Text('B'), + ])) + .toJson(); expect(json['type'], allOf([isNotNull, isNotEmpty, blockTypeToString(BlockTypes.ToDo)])); expect(json, contains(blockTypeToString(BlockTypes.ToDo))); expect(json[blockTypeToString(BlockTypes.ToDo)]['text'], allOf([isList, isNotEmpty])); + expect(json[blockTypeToString(BlockTypes.ToDo)]['children'], + allOf([isList, isNotEmpty])); }); test('Create json from empty instance', () { @@ -60,6 +72,8 @@ void main() { expect(json, contains(blockTypeToString(BlockTypes.ToDo))); expect(json[blockTypeToString(BlockTypes.ToDo)]['text'], allOf([isList, isEmpty])); + expect(json[blockTypeToString(BlockTypes.ToDo)]['children'], + allOf([isList, isEmpty])); }); }); } diff --git a/test/blocks/toggle_test.dart b/test/blocks/toggle_test.dart index 2232df0..1a5f4e8 100644 --- a/test/blocks/toggle_test.dart +++ b/test/blocks/toggle_test.dart @@ -22,7 +22,7 @@ void main() { test('Create an instance with information', () { Toggle block = Toggle(text: Text('A')) - .addText(Text('B')) + .addText('B') .addChild( Paragraph(text: Text('This is a child of the toggle item.'))) .addChildren([ @@ -44,7 +44,7 @@ void main() { Text('bar'), ], ) - .addText(Text('last')) + .addText('last') .addChild( Paragraph(text: Text('This is a child of the toggle item.'))) .addChildren([ diff --git a/test/objects/page_test.dart b/test/objects/page_test.dart index ca444d6..152f62f 100644 --- a/test/objects/page_test.dart +++ b/test/objects/page_test.dart @@ -147,7 +147,9 @@ void main() { Page pageWithChildren = Page( parent: parent, - children: Children(heading: Heading(text: Text('A')))); + children: Children.withBlocks([ + Heading(text: Text('A')), + ])); Page pageWithoutChildren = Page(parent: parent); Map jsonWithChildren = pageWithChildren.toJson(); From ab8297574d14abd16926f35f83e5459d5b89e75c Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 12:40:19 -0500 Subject: [PATCH 08/14] Fix errors detected with tests --- lib/notion/blocks/bulleted_list_item.dart | 1 + test/notion_api_test.dart | 101 ++++++++++++++-------- test/response_test.dart | 28 ++++++ 3 files changed, 94 insertions(+), 36 deletions(-) diff --git a/lib/notion/blocks/bulleted_list_item.dart b/lib/notion/blocks/bulleted_list_item.dart index 07b11ff..2804b2b 100644 --- a/lib/notion/blocks/bulleted_list_item.dart +++ b/lib/notion/blocks/bulleted_list_item.dart @@ -57,6 +57,7 @@ class BulletedItem extends Block { 'type': strType, strType: { 'text': _content.map((e) => e.toJson()).toList(), + 'children': _children.map((e) => e.toJson()).toList(), }, }; } diff --git a/test/notion_api_test.dart b/test/notion_api_test.dart index 4b7f7fc..93e3dbe 100644 --- a/test/notion_api_test.dart +++ b/test/notion_api_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io' show Platform; import 'package:dotenv/dotenv.dart' show load, env, clean; @@ -162,21 +163,22 @@ void main() { NotionResponse res = await blocks.append( to: testBlockId as String, - children: Children().add(Heading(text: Text('Test'))).add( - Paragraph( - texts: [ - Text('Lorem ipsum (A)'), - Text( - 'Lorem ipsum (B)', - annotations: TextAnnotations( - bold: true, - underline: true, - color: ColorsTypes.Orange, - ), - ), - ], + children: Children.withBlocks([ + Heading(text: Text('Test')), + Paragraph(texts: [ + Text('Lorem ipsum (A)'), + Text( + 'Lorem ipsum (B)', + annotations: TextAnnotations( + bold: true, + underline: true, + color: ColorsTypes.Orange, ), ), + ], children: [ + Heading(text: Text('Subtitle'), type: 3), + ]), + ]), ); expect(res.status, 200); @@ -188,20 +190,22 @@ void main() { NotionResponse res = await blocks.append( to: testBlockId as String, - children: Children().addAll( - [ - ToDo(text: Text('This is a todo item A')), - ToDo( - texts: [ - Text('This is a todo item'), - Text( - 'B', - annotations: TextAnnotations(bold: true), - ), - ], - ), - ], - ), + children: Children.withBlocks([ + ToDo(text: Text('This is a todo item A')), + ToDo( + texts: [ + Text('This is a todo item'), + Text( + 'B', + annotations: TextAnnotations(bold: true), + ), + ], + ), + ToDo(text: Text('Todo item with children'), children: [ + BulletedItem(text: Text('A')), + BulletedItem(text: Text('B')), + ]) + ]), ); expect(res.status, 200); @@ -217,6 +221,16 @@ void main() { [ BulletedItem(text: Text('This is a bulleted list item A')), BulletedItem(text: Text('This is a bulleted list item B')), + BulletedItem( + text: Text('This is a bulleted list item with children'), + children: [ + Paragraph(texts: [ + Text('A'), + Text('B'), + Text('C'), + ]) + ], + ), ], ), ); @@ -234,6 +248,16 @@ void main() { [ NumberedItem(text: Text('This is a numbered list item A')), NumberedItem(text: Text('This is a numbered list item B')), + NumberedItem( + text: Text('This is a bulleted list item with children'), + children: [ + Paragraph(texts: [ + Text('A'), + Text('B'), + Text('C'), + ]) + ], + ), ], ), ); @@ -249,15 +273,20 @@ void main() { to: testBlockId as String, children: Children.withBlocks( [ - Toggle(text: Text('This is a toggle block'), children: [ - Paragraph(texts: [ - Text( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas venenatis dolor sed ex egestas, et vehicula tellus faucibus. Sed pellentesque tellus eget imperdiet vulputate.') - ]), - BulletedItem(text: Text('A')), - BulletedItem(text: Text('B')), - BulletedItem(text: Text('B')), - ]), + Toggle( + text: Text('This is a toggle block'), + children: [ + Paragraph( + texts: [ + Text( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas venenatis dolor sed ex egestas, et vehicula tellus faucibus. Sed pellentesque tellus eget imperdiet vulputate.') + ], + ), + BulletedItem(text: Text('A')), + BulletedItem(text: Text('B')), + BulletedItem(text: Text('B')), + ], + ), ], ), ); diff --git a/test/response_test.dart b/test/response_test.dart index c6731c0..312fdf2 100644 --- a/test/response_test.dart +++ b/test/response_test.dart @@ -2,6 +2,8 @@ import 'dart:io' show Platform; import 'package:dotenv/dotenv.dart' show load, env, clean; import 'package:notion_api/notion.dart'; +import 'package:notion_api/notion/blocks/paragraph.dart'; +import 'package:notion_api/notion/general/lists/children.dart'; import 'package:notion_api/notion/general/property.dart'; import 'package:notion_api/notion/general/rich_text.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; @@ -17,6 +19,7 @@ void main() { String? testDatabaseId = Platform.environment['TEST_DATABASE_ID']; String? testPageId = Platform.environment['TEST_PAGE_ID']; String? testBlockId = Platform.environment['TEST_BLOCK_ID']; + String? testBlockHeadingId = Platform.environment['TEST_BLOCK_HEADING_ID']; String execEnv = env['EXEC_ENV'] ?? Platform.environment['EXEC_ENV'] ?? ''; if (execEnv != 'github_actions') { @@ -27,6 +30,8 @@ void main() { testDatabaseId = env['TEST_DATABASE_ID'] ?? testDatabaseId ?? ''; testPageId = env['TEST_PAGE_ID'] ?? testPageId ?? ''; testBlockId = env['TEST_BLOCK_ID'] ?? testBlockId ?? ''; + testBlockHeadingId = + env['TEST_BLOCK_HEADING_ID'] ?? testBlockHeadingId ?? ''; }); tearDownAll(() { @@ -55,6 +60,29 @@ void main() { expect(res.code, 'unauthorized'); }); + test('Invalid field (children) for block', () async { + final NotionBlockClient blocks = NotionBlockClient(token: token ?? ''); + + // Heading block do not support children + var res = await blocks.append( + to: testBlockHeadingId ?? '', + children: Children.withBlocks( + [ + Paragraph( + texts: [ + Text('A'), + Text('B'), + ], + ) + ], + ), + ); + + expect(res.status, 400); + expect(res.isError, true); + expect(res.code, 'validation_error'); + }); + test('Invalid property', () async { final NotionPagesClient pages = NotionPagesClient(token: token ?? ''); From 62d52e12c0367b5a611e46b00ad7eea4d90ba012 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 12:44:13 -0500 Subject: [PATCH 09/14] Add environment variable to GitHub actions --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7e20d38..159ec06 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,6 +29,7 @@ jobs: TEST_DATABASE_ID: ${{ secrets.TEST_DATABASE_ID }} TEST_PAGE_ID: ${{ secrets.TEST_PAGE_ID }} TEST_BLOCK_ID: ${{ secrets.TEST_BLOCK_ID }} + TEST_BLOCK_HEADING_ID: ${{ secrets.TEST_BLOCK_HEADING_ID }} EXEC_ENV: 'github_actions' - name: Format coverage From 2036c4d540ade06559eec69ac9bb8a5c616776e5 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 13:07:32 -0500 Subject: [PATCH 10/14] Rename list item class Bulleted and Numbered --- lib/notion/blocks/bulleted_list_item.dart | 10 +++++----- lib/notion/blocks/numbered_list_item.dart | 10 +++++----- test/blocks/bulleted_list_item_test.dart | 10 +++++----- test/blocks/numbered_list_item_test.dart | 10 +++++----- test/blocks/toggle_test.dart | 12 ++++++------ test/notion_api_test.dart | 22 +++++++++++----------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/notion/blocks/bulleted_list_item.dart b/lib/notion/blocks/bulleted_list_item.dart index 2804b2b..5288aad 100644 --- a/lib/notion/blocks/bulleted_list_item.dart +++ b/lib/notion/blocks/bulleted_list_item.dart @@ -3,7 +3,7 @@ import 'package:notion_api/notion/general/rich_text.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; /// A representation of the Bulleted List Item Notion block object; -class BulletedItem extends Block { +class BulletedListItem extends Block { /// The block type. Always BulletedListItem. @override final BlockTypes type = BlockTypes.BulletedListItem; @@ -20,7 +20,7 @@ class BulletedItem extends Block { /// Main bulleted list item constructor. /// /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the [children] of the block. - BulletedItem({ + BulletedListItem({ Text? text, List texts: const [], List children: const [], @@ -33,19 +33,19 @@ class BulletedItem extends Block { } /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. - BulletedItem addText(String text, {TextAnnotations? annotations}) { + BulletedListItem addText(String text, {TextAnnotations? annotations}) { this._content.add(Text(text, annotations: annotations)); return this; } /// Add a new [block] to the children and returns this instance. - BulletedItem addChild(Block block) { + BulletedListItem addChild(Block block) { this._children.add(block); return this; } /// Add a list of [blocks] to the children and returns this instance. - BulletedItem addChildren(List blocks) { + BulletedListItem addChildren(List blocks) { this._children.addAll(blocks); return this; } diff --git a/lib/notion/blocks/numbered_list_item.dart b/lib/notion/blocks/numbered_list_item.dart index 7b7b5b9..7dc93c8 100644 --- a/lib/notion/blocks/numbered_list_item.dart +++ b/lib/notion/blocks/numbered_list_item.dart @@ -3,7 +3,7 @@ import 'package:notion_api/notion/general/rich_text.dart'; import 'package:notion_api/notion/general/types/notion_types.dart'; /// A representation of the Bulleted List Item Notion block object; -class NumberedItem extends Block { +class NumberedListItem extends Block { /// The block type. Always NumberedListItem. @override final BlockTypes type = BlockTypes.NumberedListItem; @@ -20,7 +20,7 @@ class NumberedItem extends Block { /// Main numbered list item constructor. /// /// Can receive a single [text] or a list of [texts]. If both are included also both fields are added to the heading content adding first the [text] field. Also can receive the [children] of the block. - NumberedItem({ + NumberedListItem({ Text? text, List texts: const [], List children: const [], @@ -33,19 +33,19 @@ class NumberedItem extends Block { } /// Add a [text] to the rich text array and returns this instance. Also can receive the [annotations] of the text. - NumberedItem addText(String text, {TextAnnotations? annotations}) { + NumberedListItem addText(String text, {TextAnnotations? annotations}) { this._content.add(Text(text, annotations: annotations)); return this; } /// Add a new [block] to the children and returns this instance. - NumberedItem addChild(Block block) { + NumberedListItem addChild(Block block) { this._children.add(block); return this; } /// Add a list of [blocks] to the children and returns this instance. - NumberedItem addChildren(List blocks) { + NumberedListItem addChildren(List blocks) { this._children.addAll(blocks); return this; } diff --git a/test/blocks/bulleted_list_item_test.dart b/test/blocks/bulleted_list_item_test.dart index 84a3561..4267b32 100644 --- a/test/blocks/bulleted_list_item_test.dart +++ b/test/blocks/bulleted_list_item_test.dart @@ -8,7 +8,7 @@ import 'package:test/test.dart'; void main() { group('BulletedListItem tests =>', () { test('Create an empty instance', () { - BulletedItem block = BulletedItem(); + BulletedListItem block = BulletedListItem(); expect(block, isNotNull); expect(block.strType, blockTypeToString(BlockTypes.BulletedListItem)); @@ -19,7 +19,7 @@ void main() { }); test('Create an instance with information', () { - BulletedItem block = BulletedItem(text: Text('A')).addText('B'); + BulletedListItem block = BulletedListItem(text: Text('A')).addText('B'); expect(block.content.length, 2); expect(block.content.first.text, 'A'); @@ -27,7 +27,7 @@ void main() { }); test('Create an instance with mixed information', () { - BulletedItem block = BulletedItem( + BulletedListItem block = BulletedListItem( text: Text('first'), texts: [ Text('foo'), @@ -45,7 +45,7 @@ void main() { }); test('Create json from instance', () { - Map json = BulletedItem(text: Text('A')) + Map json = BulletedListItem(text: Text('A')) .addChild(Paragraph(texts: [ Text('A'), Text('B'), @@ -67,7 +67,7 @@ void main() { }); test('Create json from empty instance', () { - Map json = BulletedItem().toJson(); + Map json = BulletedListItem().toJson(); expect( json['type'], diff --git a/test/blocks/numbered_list_item_test.dart b/test/blocks/numbered_list_item_test.dart index 1443bdd..f7b8b76 100644 --- a/test/blocks/numbered_list_item_test.dart +++ b/test/blocks/numbered_list_item_test.dart @@ -8,7 +8,7 @@ import 'package:test/test.dart'; void main() { group('NumberedListItem tests =>', () { test('Create an empty instance', () { - NumberedItem block = NumberedItem(); + NumberedListItem block = NumberedListItem(); expect(block, isNotNull); expect(block.strType, blockTypeToString(BlockTypes.NumberedListItem)); @@ -18,7 +18,7 @@ void main() { }); test('Create an instance with information', () { - NumberedItem block = NumberedItem(text: Text('A')).addText('B'); + NumberedListItem block = NumberedListItem(text: Text('A')).addText('B'); expect(block.content.length, 2); expect(block.content.first.text, 'A'); @@ -26,7 +26,7 @@ void main() { }); test('Create an instance with mixed information', () { - NumberedItem block = NumberedItem( + NumberedListItem block = NumberedListItem( text: Text('first'), texts: [ Text('foo'), @@ -44,7 +44,7 @@ void main() { }); test('Create json from instance', () { - Map json = NumberedItem(text: Text('A')) + Map json = NumberedListItem(text: Text('A')) .addChild(Paragraph(texts: [ Text('A'), Text('B'), @@ -66,7 +66,7 @@ void main() { }); test('Create json from empty instance', () { - Map json = NumberedItem().toJson(); + Map json = NumberedListItem().toJson(); expect( json['type'], diff --git a/test/blocks/toggle_test.dart b/test/blocks/toggle_test.dart index 1a5f4e8..9cb07a4 100644 --- a/test/blocks/toggle_test.dart +++ b/test/blocks/toggle_test.dart @@ -26,8 +26,8 @@ void main() { .addChild( Paragraph(text: Text('This is a child of the toggle item.'))) .addChildren([ - BulletedItem(text: Text('First bulleted item')), - NumberedItem(text: Text('First numbered item')), + BulletedListItem(text: Text('First bulleted item')), + NumberedListItem(text: Text('First numbered item')), ]); expect(block.content.length, 2); @@ -48,8 +48,8 @@ void main() { .addChild( Paragraph(text: Text('This is a child of the toggle item.'))) .addChildren([ - BulletedItem(text: Text('First bulleted item')), - NumberedItem(text: Text('First numbered item')), + BulletedListItem(text: Text('First bulleted item')), + NumberedListItem(text: Text('First numbered item')), ]); expect(block.content.length, 4); @@ -60,8 +60,8 @@ void main() { test('Create json from instance', () { Map json = Toggle(text: Text('A'), children: [ - BulletedItem(text: Text('First bulleted item')), - NumberedItem(text: Text('First numbered item')), + BulletedListItem(text: Text('First bulleted item')), + NumberedListItem(text: Text('First numbered item')), ]).toJson(); expect(json['type'], diff --git a/test/notion_api_test.dart b/test/notion_api_test.dart index 93e3dbe..2e8dc17 100644 --- a/test/notion_api_test.dart +++ b/test/notion_api_test.dart @@ -202,8 +202,8 @@ void main() { ], ), ToDo(text: Text('Todo item with children'), children: [ - BulletedItem(text: Text('A')), - BulletedItem(text: Text('B')), + BulletedListItem(text: Text('A')), + BulletedListItem(text: Text('B')), ]) ]), ); @@ -219,9 +219,9 @@ void main() { to: testBlockId as String, children: Children.withBlocks( [ - BulletedItem(text: Text('This is a bulleted list item A')), - BulletedItem(text: Text('This is a bulleted list item B')), - BulletedItem( + BulletedListItem(text: Text('This is a bulleted list item A')), + BulletedListItem(text: Text('This is a bulleted list item B')), + BulletedListItem( text: Text('This is a bulleted list item with children'), children: [ Paragraph(texts: [ @@ -246,9 +246,9 @@ void main() { to: testBlockId as String, children: Children.withBlocks( [ - NumberedItem(text: Text('This is a numbered list item A')), - NumberedItem(text: Text('This is a numbered list item B')), - NumberedItem( + NumberedListItem(text: Text('This is a numbered list item A')), + NumberedListItem(text: Text('This is a numbered list item B')), + NumberedListItem( text: Text('This is a bulleted list item with children'), children: [ Paragraph(texts: [ @@ -282,9 +282,9 @@ void main() { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas venenatis dolor sed ex egestas, et vehicula tellus faucibus. Sed pellentesque tellus eget imperdiet vulputate.') ], ), - BulletedItem(text: Text('A')), - BulletedItem(text: Text('B')), - BulletedItem(text: Text('B')), + BulletedListItem(text: Text('A')), + BulletedListItem(text: Text('B')), + BulletedListItem(text: Text('B')), ], ), ], From a699ba9486524bae3eaa153a8e117f310395da8a Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 13:23:52 -0500 Subject: [PATCH 11/14] Update examples --- example/example.md | 181 ++++++++++++++++++++----- example/images/bulletedListItem.png | Bin 0 -> 14241 bytes example/images/headingAndParagraph.png | Bin 0 -> 10302 bytes example/images/numberedListItem.png | Bin 0 -> 14773 bytes example/images/todo.png | Bin 4675 -> 11522 bytes example/images/toggle.png | Bin 0 -> 24688 bytes lib/notion/blocks/heading.dart | 2 +- 7 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 example/images/bulletedListItem.png create mode 100644 example/images/headingAndParagraph.png create mode 100644 example/images/numberedListItem.png create mode 100644 example/images/toggle.png diff --git a/example/example.md b/example/example.md index 4fb6751..e4f7048 100644 --- a/example/example.md +++ b/example/example.md @@ -11,13 +11,11 @@ - [Block children](#block-children) - [Retrieve block children](#retrieve-block-children) - [Append block children](#append-block-children) - - [Example](#example) - - [Heading & Paragraph](#heading--paragraph) - - [Code](#code) - - [Result](#result) - - [To do](#to-do) - - [Code](#code-1) - - [Result](#result-1) + - [Heading & Paragraph](#heading--paragraph) + - [To do](#to-do) + - [Toggle](#toggle) + - [Bulleted List Item](#bulleted-list-item) + - [Numbered List Item](#numbered-list-item) # Initialization ## Full instance @@ -99,12 +97,11 @@ _Parameters:_ - The `Paragraph` object can contain only `Text` objects. - `Text` can receive a `TextAnnotations` class with the style of the text. -### Example -#### Heading & Paragraph -##### Code +### Heading & Paragraph +**Code** ```dart // Create children instance: -// * Old way +// * Deprecated way // Children oldWay = Children( // heading: Heading('Test'), // paragraph: Paragraph( @@ -127,13 +124,17 @@ Children childrenA = Children().addAll([ Heading(text: Text('Test')), Paragraph(texts: [ Text('Lorem ipsum (A)'), - Text('Lorem ipsum (B)', - annotations: TextAnnotations( - bold: true, - underline: true, - color: ColorsTypes.Orange, - )) - ]) + Text( + 'Lorem ipsum (B)', + annotations: TextAnnotations( + bold: true, + underline: true, + color: ColorsTypes.Orange, + ), + ), + ], children: [ + Heading(text: Text('Subtitle'), type: 3), + ]), ]); // * New way using single `add()` @@ -141,28 +142,51 @@ Children childrenB = Children().add(Heading(text: Text('Test'))).add(Paragraph(texts: [ Text('Lorem ipsum (A)'), Text('Lorem ipsum (B)', - annotations: TextAnnotations( - bold: true, - underline: true, - color: ColorsTypes.Orange, - )) - ])); + annotations: TextAnnotations( + bold: true, + underline: true, + color: ColorsTypes.Orange, + ), + ), + ], children: [ + Heading(text: Text('Subtitle'), type: 3), + ], +)); + +// * New way using `withBlocks()` constructor +Children childrenC = Children.withBlocks([ + Heading(text: Text('Test')), + Paragraph(texts: [ + Text('Lorem ipsum (A)'), + Text( + 'Lorem ipsum (B)', + annotations: TextAnnotations( + bold: true, + underline: true, + color: ColorsTypes.Orange, + ), + ), + ], children: [ + Heading(text: Text('Subtitle'), type: 3), + ]), +]); // Send the instance to Notion notion.blocks.append( to: 'YOUR_BLOCK_ID', - children: childrenB, // or `childrenA`, both are the same. + children: childrenA, // or `childrenB` or `childrenC`, any of these will produce the same result. ); ``` -##### Result -![heading¶graph](https://raw.githubusercontent.com/jonathangomz/notion_api/main/example/images/heading_paragraph.png) +**Result** + +![heading¶graph](https://raw.githubusercontent.com/jonathangomz/notion_api/main/example/images/headingAndParagraph.png) -#### To do -##### Code +### To do +**Code** ```dart // Create children instance: -// * Old way +// * Deprecated way // Children children = // Children( // toDo: [ @@ -181,7 +205,7 @@ notion.blocks.append( // // * New way Children children = - Children().addAll([ + Children.withBlocks([ ToDo(text: Text('This is a todo item A')), ToDo( texts: [ @@ -192,6 +216,10 @@ Children children = ), ], ), + ToDo(text: Text('Todo item with children'), children: [ + BulletedListItem(text: Text('A')), + BulletedListItem(text: Text('B')), + ]), ], ); @@ -202,7 +230,96 @@ notion.blocks.append( ); ``` -##### Result +**Result** ![todo](https://raw.githubusercontent.com/jonathangomz/notion_api/main/example/images/todo.png) +### Toggle +**Code** +```dart +Children children = + Children.withBlocks([ + Toggle( + text: Text('This is a toggle block'), + children: [ + Paragraph( + texts: [ + Text( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas venenatis dolor sed ex egestas, et vehicula tellus faucibus. Sed pellentesque tellus eget imperdiet vulputate.') + ], + ), + BulletedListItem(text: Text('A')), + BulletedListItem(text: Text('B')), + BulletedListItem(text: Text('B')), + ], + ), + ], +); + +// Send the instance to Notion +notion.blocks.append( + to: 'YOUR_BLOCK_ID', + children: children, +); +``` +**Result** +![toggle](https://raw.githubusercontent.com/jonathangomz/notion_api/main/example/images/toggle.png) + +### Bulleted List Item +**Code** +```dart +Children children = + Children.withBlocks([ + BulletedListItem(text: Text('This is a bulleted list item A')), + BulletedListItem(text: Text('This is a bulleted list item B')), + BulletedListItem( + text: Text('This is a bulleted list item with children'), + children: [ + Paragraph(texts: [ + Text('A'), + Text('B'), + Text('C'), + ]) + ], + ), + ], +); + +// Send the instance to Notion +notion.blocks.append( + to: 'YOUR_BLOCK_ID', + children: children, +); +``` +**Result** +![bulletedListItem](https://raw.githubusercontent.com/jonathangomz/notion_api/main/example/images/bulletedListItem.png) + +### Numbered List Item +**Code** +```dart +Children children = + Children.withBlocks([ + NumberedListItem(text: Text('This is a numbered list item A')), + NumberedListItem(text: Text('This is a numbered list item B')), + NumberedListItem( + text: Text('This is a bulleted list item with children'), + children: [ + Paragraph(texts: [ + Text('A'), + Text('B'), + Text('C'), + ]) + ], + ), + ], +); + +// Send the instance to Notion +notion.blocks.append( + to: 'YOUR_BLOCK_ID', + children: children, +); +``` +**Result** +![numberedListItem](https://raw.githubusercontent.com/jonathangomz/notion_api/main/example/images/numberedListItem.png) + [1]: https://developers.notion.com/reference/get-databases \ No newline at end of file diff --git a/example/images/bulletedListItem.png b/example/images/bulletedListItem.png new file mode 100644 index 0000000000000000000000000000000000000000..f6bb3c6947606a6fd455d507d0d7c8c07783216b GIT binary patch literal 14241 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 literal 0 HcmV?d00001 diff --git a/example/images/headingAndParagraph.png b/example/images/headingAndParagraph.png new file mode 100644 index 0000000000000000000000000000000000000000..c5f4e699e914f75e5e749a43717391a0ceb6a1c0 GIT binary patch literal 10302 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

!W{73H<_*_ zwu}tl&M7a*TS4Oiu5dV*54lJ9%g!Iu*i5LYGFei##N+(o&?$L2eBjrpH{^Jw-&wpj zJkXdV)4D}wNzmnLoq|b?mU_dC>FMbz*v(ESoMnkr+PwM4AUFs*`?u~KXbNx=p2zXJ z&K$43aP)w^(V)M}Gj@FS;ag}Hgnk?Jit`Rqsb8Qa^~T5Jm%!B zXvJ2LXwxMWgU6E(kk5hNt`5#N74s$AkPqYkE)W4hzEBm`s1QcT?gN$x%-tL>lU+Cy z>`f+&6sb`G(tX9lY=KY-tKCoAEPS8GFhIRDQR7jrRHdYQ|Iee>LH)aI1qJ~$h7or# z5-XwJXnf2VoK|gyrsvEMwngw#KX0>!`26;C-I3zQr=8I6em{q}?T=)yZ*TYAV@-pA z9^plXp#j4qn^mq+56X8F0w~4!`S7n1_u0EcQ6nSl_{;e-k|5s#O#}3Zh3(y*#j@DZ zC$pKO(pk*-ZG#Cn#zp=7{2(+e%M;0>M(wHpz9pR{F@4GX>w9v}N{f6IC&eg5v$~R#v0#?Y8bAU5+6|U&i9o$LN7Nqr!#^W+pJB`umVDq{fgeAOs zy1&aeS1J!3!~w$+dw-`0khy1R;B=K`hNj%hd^DP0G4&a@snjLW$5Tpo5aXzvY5`fFrG?`OKY3r@!J33^m{Jpw3S^d8hFzPU`!!8 zJ?)<$?eMdRL{gA107$Ms5F1j5i@#8;x|5JId(5{vQ3};Avs_zT;#Dt^;tNC^P{~wCS B2jKt! literal 0 HcmV?d00001 diff --git a/example/images/todo.png b/example/images/todo.png index 5be787c3e56a12d0f998fb2f3addb8323c159e30..8b5fc460ac296f98a7b64d3774f78ac3a83fa42f 100644 GIT binary patch literal 11522 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 xa6%e(%Vy(6$mWZ+V)CGrR% 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 diff --git a/example/images/toggle.png b/example/images/toggle.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b1b356624d11c378a80d8136f5b744117a61ff GIT binary patch literal 24688 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 literal 0 HcmV?d00001 diff --git a/lib/notion/blocks/heading.dart b/lib/notion/blocks/heading.dart index ba261eb..03fa8dd 100644 --- a/lib/notion/blocks/heading.dart +++ b/lib/notion/blocks/heading.dart @@ -6,7 +6,7 @@ import 'package:notion_api/notion/general/rich_text.dart'; class Heading extends Block { /// The block type. Always H1, H2 or H3 for this. @override - final BlockTypes type = BlockTypes.H1; + BlockTypes type = BlockTypes.H1; List _content = []; From b9f41cb0dbe8a6edc7af04573426547a00152d17 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 13:25:32 -0500 Subject: [PATCH 12/14] Update information for new version (1.1.0) --- CHANGELOG.md | 30 +++++++++++++++++++++++++++++- README.md | 15 ++------------- ROADMAP.md | 35 ++++++++++++++++++++++++++--------- pubspec.yaml | 2 +- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa9e33..f2e58bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,4 +77,32 @@ > Release date: 05/Jul/2021 * Fix warnings for documentation * Improve documentation -* Add contribution rules \ No newline at end of file +* Add contribution rules + +## v1.1.0: +> Release date: 10/Jul/2021 +* Add more blocks support for `(PATCH): block children` endpoint + * `BulletedListItem` block + * `NumberedListItem` block + * `Toggle` block +* Add `children` field for blocks: + * `BulletedListItem` + * `NumberedListItem` + * `ToDo` + * `Toggle` + * `Paragraph` +* Add methods to manipulate `content` and `children` for blocks: + * `addText(String text, {TextAnnotations? annotations})` + * `addChild(Block block)` + * `addChildren(List blocks)` +* Add `Children.withBlocks(List blocks)` constructor +* Add `final` for `type` fields to not allow overwrite: + * Objects + * Blocks +* Add `BaseClient` class to avoid duplicated code for clients +* Add `deprecated` annotations for future changes: + * Remove `textSeparation` parameter/field + * Remove `add(Text text)` function + * Remove `texts` getter for `Paragraph` + * Remove named parameters for `Children` class +* Update documentation \ No newline at end of file diff --git a/README.md b/README.md index d000a69..0d1388f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ See the [ROADMAP](ROADMAP.md) file to see what is coming next. - [Tests](#tests) - [Example:](#example) - [Next release](#next-release) - - [v1.1.0:](#v110) # Usage **Important**: The methods return a `NotionResponse`. You can find how to use it in its [documentation][1]. @@ -47,7 +46,7 @@ _To see more examples [go here](https://github.com/jonathangomz/notion_api/blob/ ### Append blocks children ```dart // Create children instance: -Children children = Children().addAll([ +Children children = Children.withBlocks([ Heading(text: Text('Test')), Paragraph(texts: [ Text('Lorem ipsum (A)'), @@ -109,16 +108,6 @@ TEST_BLOCK_ID=c8hac4bb32af48889228bf483d938e34 ``` # Next release -## v1.1.0: -> Release date: 10/Jul/2021 -* Add more blocks for `(PATCH): block children` endpoint - * `BulletedList` block - * `NumberedList` block - * `Toggle` block -* Add `Children.with(List blocks)` constructor -* Add singleton (_if possible_) -* Add `final` for override types to not allow change the field: - * Objects - * Blocks +I don't know yet. If you have suggestions feel free to create an Issue or to create a PR with the feature. [1]:https://pub.dev/documentation/notion_api/1.0.0-beta1/responses_notion_response/NotionResponse-class.html \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md index 39ecce5..f7ceaaa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,18 +1,35 @@ # Roadmap ## More coming soon... +I don't know yet. If you have suggestions feel free to create an Issue or to create a PR with the feature. -## v1.1.0: +## v1.1.0: ✅ > Release date: 10/Jul/2021 -* Add more blocks for `(PATCH): block children` endpoint - * `BulletedList` block - * `NumberedList` block +* Add more blocks support for `(PATCH): block children` endpoint + * `BulletedListItem` block + * `NumberedListItem` block * `Toggle` block -* Add `Children.with(List blocks)` constructor -* Add singleton (_if possible_) -* Add `final` for override types to not allow change the field: - * Objects - * Blocks +* Add `children` field for blocks: + * `BulletedListItem` + * `NumberedListItem` + * `ToDo` + * `Toggle` + * `Paragraph` +* Add methods to manipulate `content` and `children` for blocks: + * `addText(String text, {TextAnnotations? annotations})` + * `addChild(Block block)` + * `addChildren(List blocks)` +* Add `Children.withBlocks(List blocks)` constructor +* Add `final` for `type` fields to not allow overwrite: + * Objects + * Blocks +* Add `BaseClient` class to avoid duplicated code for clients +* Add `deprecated` annotations for future changes: + * Remove `textSeparation` parameter/field + * Remove `add(Text text)` function + * Remove `texts` getter for `Paragraph` + * Remove named parameters for `Children` class +* Update documentation ## v1.0.2: ✅ > Release date: 05/Jul/2021 diff --git a/pubspec.yaml b/pubspec.yaml index 3d15cef..88510a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: notion_api description: A wrapper for the public beta Notion API to manage it like a Notion SDK package for dart. -version: 1.0.2 +version: 1.1.0 homepage: https://github.com/jonathangomz/notion_api environment: From 930d3adaa1d646c175733608fdbcf208015dd92d Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 13:28:55 -0500 Subject: [PATCH 13/14] Fix tests --- test/blocks/numbered_list_item_test.dart | 2 +- test/blocks/paragraph_test.dart | 2 +- test/blocks/todo_test.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/blocks/numbered_list_item_test.dart b/test/blocks/numbered_list_item_test.dart index f7b8b76..6a567b2 100644 --- a/test/blocks/numbered_list_item_test.dart +++ b/test/blocks/numbered_list_item_test.dart @@ -40,7 +40,7 @@ void main() { expect(block.content.length, 4); expect(block.content.first.text, 'first'); expect(block.content.last.text, 'last'); - expect(block.content.length, 1); + expect(block.children.length, 1); }); test('Create json from instance', () { diff --git a/test/blocks/paragraph_test.dart b/test/blocks/paragraph_test.dart index 4970560..629952a 100644 --- a/test/blocks/paragraph_test.dart +++ b/test/blocks/paragraph_test.dart @@ -36,7 +36,7 @@ void main() { expect(paragraph.content.length, 4); expect(paragraph.content.first.text, 'first'); expect(paragraph.content.last.text, 'last'); - expect(paragraph.children.length, 4); + expect(paragraph.children.length, 1); }); test('Create json from instance', () { diff --git a/test/blocks/todo_test.dart b/test/blocks/todo_test.dart index b8c59b2..ddabfbe 100644 --- a/test/blocks/todo_test.dart +++ b/test/blocks/todo_test.dart @@ -44,7 +44,7 @@ void main() { expect(todo.content.length, 4); expect(todo.content.first.text, 'first'); expect(todo.content.last.text, 'last'); - expect(todo.children.length, 4); + expect(todo.children.length, 1); }); test('Create json from instance', () { From 983e67e2e6afc36ab80ae15c1f3f3fdeb3081b98 Mon Sep 17 00:00:00 2001 From: jonathangomz Date: Fri, 9 Jul 2021 13:30:27 -0500 Subject: [PATCH 14/14] Add new environment variable to documentation --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0d1388f..b082d18 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ To be able to run the tests you will have to have a `.env` file on the root dire * TEST_DATABASE_ID: The database id where you will be working on. * TEST_PAGE_ID: Some page id inside the database specified above. * TEST_BLOCK_ID: Some block id inside the page specified above. +* TEST_BLOCK_HEADING_ID: Some heading block id inside the page specified above. ### Example: _The values are not valid of course._ @@ -105,6 +106,7 @@ TOKEN=secret_Oa24V8FbJ49JluJankVOQihyLiMXwqSQeeHuSFobQDW TEST_DATABASE_ID=366da3d646bb458128071fdb2fbbf427 TEST_PAGE_ID=c3b53019-4470-443b-a141-95a3a1a44g60 TEST_BLOCK_ID=c8hac4bb32af48889228bf483d938e34 +TEST_BLOCK_HEADING_ID=c8hac4bb32af48889228bf483d938e34 ``` # Next release