jekyll-notion-cms 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +87 -0
- data/CHANGELOG.md +48 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +362 -0
- data/Rakefile +26 -0
- data/docs/EXAMPLES_AND_CONFIGURATION.md +371 -0
- data/docs/architecture.excalidraw +3805 -0
- data/docs/architecture.png +0 -0
- data/docs/templates/github-actions_notion-sync.yml +101 -0
- data/docs/templates/n8n-workflow_Notion-database-change-trigger-GitHub-Actions.json +184 -0
- data/lib/jekyll-notion-cms.rb +14 -0
- data/lib/jekyll_notion_cms/data_organizers.rb +229 -0
- data/lib/jekyll_notion_cms/generator.rb +189 -0
- data/lib/jekyll_notion_cms/notion_client.rb +133 -0
- data/lib/jekyll_notion_cms/property_extractors.rb +331 -0
- data/lib/jekyll_notion_cms/version.rb +5 -0
- metadata +187 -0
|
Binary file
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
name: Notion Sync Workflow
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
notion_event:
|
|
7
|
+
description: 'Type of Notion event'
|
|
8
|
+
required: true
|
|
9
|
+
type: string
|
|
10
|
+
page_id:
|
|
11
|
+
description: 'Notion page ID'
|
|
12
|
+
required: true
|
|
13
|
+
type: string
|
|
14
|
+
database_id:
|
|
15
|
+
description: 'Notion database ID'
|
|
16
|
+
required: true
|
|
17
|
+
type: string
|
|
18
|
+
updated_at:
|
|
19
|
+
description: 'Last update timestamp'
|
|
20
|
+
required: true
|
|
21
|
+
type: string
|
|
22
|
+
|
|
23
|
+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
pages: write
|
|
27
|
+
id-token: write
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
sync:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- name: Checkout code
|
|
34
|
+
uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- name: Process Notion update
|
|
37
|
+
env:
|
|
38
|
+
NOTION_EVENT: ${{ inputs.notion_event }}
|
|
39
|
+
PAGE_ID: ${{ inputs.page_id }}
|
|
40
|
+
DATABASE_ID: ${{ inputs.database_id }}
|
|
41
|
+
UPDATED_AT: ${{ inputs.updated_at }}
|
|
42
|
+
run: |
|
|
43
|
+
echo "📝 Notion event: $NOTION_EVENT"
|
|
44
|
+
echo "🆔 Page ID: $PAGE_ID"
|
|
45
|
+
echo "🗄️ Database ID: $DATABASE_ID"
|
|
46
|
+
echo "⏰ Updated at: $UPDATED_AT"
|
|
47
|
+
|
|
48
|
+
# Ajoutez votre logique ici
|
|
49
|
+
# Exemples:
|
|
50
|
+
# - Regénérer un site statique
|
|
51
|
+
# - Déployer du contenu
|
|
52
|
+
# - Synchroniser avec un autre service
|
|
53
|
+
# - Envoyer des notifications
|
|
54
|
+
|
|
55
|
+
- name: Your custom action
|
|
56
|
+
run: |
|
|
57
|
+
echo "🚀 Executing your custom logic..."
|
|
58
|
+
# Votre code personnalisé ici
|
|
59
|
+
build:
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- name: Checkout
|
|
63
|
+
uses: actions/checkout@v4
|
|
64
|
+
- name: Setup Ruby
|
|
65
|
+
# https://github.com/ruby/setup-ruby/releases/tag/v1.207.0
|
|
66
|
+
uses: ruby/setup-ruby@4a9ddd6f338a97768b8006bf671dfbad383215f4
|
|
67
|
+
with:
|
|
68
|
+
ruby-version: '3.3.5' # Not needed with a .ruby-version file
|
|
69
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
70
|
+
cache-version: 0 # Increment this number if you need to re-download cached gems
|
|
71
|
+
- name: Setup Pages
|
|
72
|
+
id: pages
|
|
73
|
+
uses: actions/configure-pages@v5
|
|
74
|
+
- name: Build with Jekyll
|
|
75
|
+
# Outputs to the './_site' directory by default
|
|
76
|
+
run: bundle exec jekyll build
|
|
77
|
+
env:
|
|
78
|
+
JEKYLL_ENV: production
|
|
79
|
+
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
|
|
80
|
+
# Add your Notion database secrets below:
|
|
81
|
+
# NOTION_BLOG_DB: ${{ secrets.NOTION_BLOG_DB }}
|
|
82
|
+
# NOTION_PROJECTS_DB: ${{ secrets.NOTION_PROJECTS_DB }}
|
|
83
|
+
# NOTION_EXPERIENCES_DB: ${{ secrets.NOTION_EXPERIENCES_DB }}
|
|
84
|
+
# NOTION_SKILLS_DB: ${{ secrets.NOTION_SKILLS_DB }}
|
|
85
|
+
# NOTION_SERVICES_DB: ${{ secrets.NOTION_SERVICES_DB }}
|
|
86
|
+
# NOTION_TESTIMONIALS_DB: ${{ secrets.NOTION_TESTIMONIALS_DB }}
|
|
87
|
+
- name: Upload artifact
|
|
88
|
+
# Automatically uploads an artifact from the './_site' directory by default
|
|
89
|
+
uses: actions/upload-pages-artifact@v3
|
|
90
|
+
|
|
91
|
+
# Deployment job
|
|
92
|
+
deploy:
|
|
93
|
+
environment:
|
|
94
|
+
name: github-pages
|
|
95
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
needs: build
|
|
98
|
+
steps:
|
|
99
|
+
- name: Deploy to GitHub Pages
|
|
100
|
+
id: deployment
|
|
101
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Notion to GitHub Actions Sync",
|
|
3
|
+
"nodes": [
|
|
4
|
+
{
|
|
5
|
+
"parameters": {
|
|
6
|
+
"resource": "workflow",
|
|
7
|
+
"owner": {
|
|
8
|
+
"mode": "list",
|
|
9
|
+
"value": "YOUR_GITHUB_USERNAME"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"__rl": true,
|
|
13
|
+
"value": "your-repository-name",
|
|
14
|
+
"mode": "list",
|
|
15
|
+
"cachedResultName": "your-repository-name",
|
|
16
|
+
"cachedResultUrl": "https://github.com/YOUR_GITHUB_USERNAME/your-repository-name"
|
|
17
|
+
},
|
|
18
|
+
"workflowId": {
|
|
19
|
+
"__rl": true,
|
|
20
|
+
"value": 123456789,
|
|
21
|
+
"mode": "list",
|
|
22
|
+
"cachedResultName": "Notion Sync Workflow"
|
|
23
|
+
},
|
|
24
|
+
"ref": {
|
|
25
|
+
"__rl": true,
|
|
26
|
+
"value": "main",
|
|
27
|
+
"mode": "list",
|
|
28
|
+
"cachedResultName": "main"
|
|
29
|
+
},
|
|
30
|
+
"inputs": "={\n \"notion_event\": \"event\",\n \"page_id\": \"{{ $json.id }}\",\n \"database_id\": \"skills\",\n \"updated_at\": \"=now\"\n}"
|
|
31
|
+
},
|
|
32
|
+
"id": "github-dispatch-1",
|
|
33
|
+
"name": "Trigger GitHub Action",
|
|
34
|
+
"type": "n8n-nodes-base.github",
|
|
35
|
+
"position": [
|
|
36
|
+
608,
|
|
37
|
+
304
|
|
38
|
+
],
|
|
39
|
+
"typeVersion": 1.1,
|
|
40
|
+
"credentials": {
|
|
41
|
+
"githubApi": {
|
|
42
|
+
"id": "YOUR_GITHUB_CREDENTIAL_ID",
|
|
43
|
+
"name": "GitHub account"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"parameters": {
|
|
49
|
+
"pollTimes": {
|
|
50
|
+
"item": [
|
|
51
|
+
{
|
|
52
|
+
"mode": "everyHour"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"event": "pagedUpdatedInDatabase",
|
|
57
|
+
"databaseId": {
|
|
58
|
+
"__rl": true,
|
|
59
|
+
"value": "YOUR_NOTION_DATABASE_ID",
|
|
60
|
+
"mode": "list",
|
|
61
|
+
"cachedResultName": "Your Database Name",
|
|
62
|
+
"cachedResultUrl": "https://www.notion.so/YOUR_NOTION_DATABASE_ID"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"type": "n8n-nodes-base.notionTrigger",
|
|
66
|
+
"typeVersion": 1,
|
|
67
|
+
"position": [
|
|
68
|
+
0,
|
|
69
|
+
96
|
|
70
|
+
],
|
|
71
|
+
"id": "notion-trigger-1",
|
|
72
|
+
"name": "Notion Trigger",
|
|
73
|
+
"credentials": {
|
|
74
|
+
"notionApi": {
|
|
75
|
+
"id": "YOUR_NOTION_CREDENTIAL_ID",
|
|
76
|
+
"name": "Notion account"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"parameters": {
|
|
82
|
+
"pollTimes": {
|
|
83
|
+
"item": [
|
|
84
|
+
{
|
|
85
|
+
"mode": "everyHour"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"databaseId": {
|
|
90
|
+
"__rl": true,
|
|
91
|
+
"value": "YOUR_NOTION_DATABASE_ID_2",
|
|
92
|
+
"mode": "list",
|
|
93
|
+
"cachedResultName": "Your Second Database",
|
|
94
|
+
"cachedResultUrl": "https://www.notion.so/YOUR_NOTION_DATABASE_ID_2"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"type": "n8n-nodes-base.notionTrigger",
|
|
98
|
+
"typeVersion": 1,
|
|
99
|
+
"position": [
|
|
100
|
+
0,
|
|
101
|
+
304
|
|
102
|
+
],
|
|
103
|
+
"id": "notion-trigger-2",
|
|
104
|
+
"name": "Notion Trigger 2",
|
|
105
|
+
"credentials": {
|
|
106
|
+
"notionApi": {
|
|
107
|
+
"id": "YOUR_NOTION_CREDENTIAL_ID",
|
|
108
|
+
"name": "Notion account"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"parameters": {
|
|
114
|
+
"chatId": "YOUR_TELEGRAM_CHAT_ID",
|
|
115
|
+
"text": "=Deployment via GitHub Action successfully triggered!",
|
|
116
|
+
"additionalFields": {
|
|
117
|
+
"appendAttribution": false
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"id": "telegram-notification-1",
|
|
121
|
+
"name": "Send Notification",
|
|
122
|
+
"type": "n8n-nodes-base.telegram",
|
|
123
|
+
"position": [
|
|
124
|
+
880,
|
|
125
|
+
304
|
|
126
|
+
],
|
|
127
|
+
"typeVersion": 1.2,
|
|
128
|
+
"credentials": {
|
|
129
|
+
"telegramApi": {
|
|
130
|
+
"id": "YOUR_TELEGRAM_CREDENTIAL_ID",
|
|
131
|
+
"name": "Telegram Bot"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"pinData": {},
|
|
137
|
+
"connections": {
|
|
138
|
+
"Notion Trigger": {
|
|
139
|
+
"main": [
|
|
140
|
+
[
|
|
141
|
+
{
|
|
142
|
+
"node": "Trigger GitHub Action",
|
|
143
|
+
"type": "main",
|
|
144
|
+
"index": 0
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"Notion Trigger 2": {
|
|
150
|
+
"main": [
|
|
151
|
+
[
|
|
152
|
+
{
|
|
153
|
+
"node": "Trigger GitHub Action",
|
|
154
|
+
"type": "main",
|
|
155
|
+
"index": 0
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
"Trigger GitHub Action": {
|
|
161
|
+
"main": [
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
"node": "Send Notification",
|
|
165
|
+
"type": "main",
|
|
166
|
+
"index": 0
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"active": false,
|
|
173
|
+
"settings": {
|
|
174
|
+
"executionOrder": "v1",
|
|
175
|
+
"saveDataErrorExecution": "all",
|
|
176
|
+
"saveDataSuccessExecution": "all",
|
|
177
|
+
"saveManualExecutions": true,
|
|
178
|
+
"saveExecutionProgress": true
|
|
179
|
+
},
|
|
180
|
+
"meta": {
|
|
181
|
+
"templateCredsSetupCompleted": false
|
|
182
|
+
},
|
|
183
|
+
"tags": []
|
|
184
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jekyll'
|
|
4
|
+
require_relative 'jekyll_notion_cms/version'
|
|
5
|
+
require_relative 'jekyll_notion_cms/notion_client'
|
|
6
|
+
require_relative 'jekyll_notion_cms/property_extractors'
|
|
7
|
+
require_relative 'jekyll_notion_cms/data_organizers'
|
|
8
|
+
require_relative 'jekyll_notion_cms/generator'
|
|
9
|
+
|
|
10
|
+
module JekyllNotionCMS
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class ConfigurationError < Error; end
|
|
13
|
+
class APIError < Error; end
|
|
14
|
+
end
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllNotionCMS
|
|
4
|
+
# Module for organizing Notion data into different structures
|
|
5
|
+
module DataOrganizers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Organize data based on the specified organizer type
|
|
9
|
+
# @param notion_data [Hash] Raw data from Notion API
|
|
10
|
+
# @param config [Hash] Collection configuration
|
|
11
|
+
# @return [Hash, Array] Organized data
|
|
12
|
+
def organize(notion_data, config)
|
|
13
|
+
organizer = config['organizer'] || 'simple_list'
|
|
14
|
+
properties_config = config['properties'] || []
|
|
15
|
+
sort_by = config['sort_by']
|
|
16
|
+
sort_order = config['sort_order'] || 'asc'
|
|
17
|
+
|
|
18
|
+
case organizer
|
|
19
|
+
when 'simple_list'
|
|
20
|
+
organize_simple_list(notion_data, properties_config, sort_by, sort_order)
|
|
21
|
+
when 'items_by_category'
|
|
22
|
+
organize_items_by_category(notion_data, properties_config)
|
|
23
|
+
when 'grouped_by'
|
|
24
|
+
group_field = config['group_by']
|
|
25
|
+
organize_grouped_by(notion_data, properties_config, group_field, sort_by, sort_order)
|
|
26
|
+
when 'nested'
|
|
27
|
+
parent_field = config['parent_field'] || 'parent_id'
|
|
28
|
+
organize_nested(notion_data, properties_config, parent_field, sort_by, sort_order)
|
|
29
|
+
else
|
|
30
|
+
organize_simple_list(notion_data, properties_config, sort_by, sort_order)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Organize as a simple sorted list
|
|
35
|
+
# @param notion_data [Hash] Raw data from Notion API
|
|
36
|
+
# @param properties_config [Array<Hash>] Property configuration
|
|
37
|
+
# @param sort_by [String] Field to sort by
|
|
38
|
+
# @param sort_order [String] Sort order ('asc' or 'desc')
|
|
39
|
+
# @return [Array<Hash>] Sorted list of items
|
|
40
|
+
def organize_simple_list(notion_data, properties_config, sort_by, sort_order)
|
|
41
|
+
items = notion_data['results'].map do |page|
|
|
42
|
+
item = PropertyExtractors.extract_all(page['properties'], properties_config)
|
|
43
|
+
item['id'] = page['id']
|
|
44
|
+
item['created_time'] = page['created_time']
|
|
45
|
+
item['last_edited_time'] = page['last_edited_time']
|
|
46
|
+
item
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Filter out items without title
|
|
50
|
+
items = items.select { |item| item['title'] && !item['title'].to_s.empty? }
|
|
51
|
+
|
|
52
|
+
# Sort if sort_by is specified
|
|
53
|
+
sort_items(items, sort_by, sort_order)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Organize items grouped by category
|
|
57
|
+
# Useful for skills, products, team members, or any items with category grouping
|
|
58
|
+
# @param notion_data [Hash] Raw data from Notion API
|
|
59
|
+
# @param properties_config [Array<Hash>] Property configuration
|
|
60
|
+
# @return [Hash] Items grouped by category
|
|
61
|
+
def organize_items_by_category(notion_data, _properties_config)
|
|
62
|
+
items_by_category = {}
|
|
63
|
+
|
|
64
|
+
notion_data['results'].each do |page|
|
|
65
|
+
properties = page['properties']
|
|
66
|
+
|
|
67
|
+
name = PropertyExtractors.extract(properties, 'Name', 'title')
|
|
68
|
+
next if name.nil? || name.empty?
|
|
69
|
+
|
|
70
|
+
level = PropertyExtractors.extract(properties, 'Level', 'number')
|
|
71
|
+
years = PropertyExtractors.extract(properties, 'Years', 'number')
|
|
72
|
+
featured = PropertyExtractors.extract(properties, 'Featured', 'checkbox')
|
|
73
|
+
order = PropertyExtractors.extract(properties, 'Order', 'number')
|
|
74
|
+
category_name = PropertyExtractors.extract(properties, 'Category', 'rollup') || 'Other'
|
|
75
|
+
category_icon = PropertyExtractors.extract(properties, 'Icon', 'rollup')
|
|
76
|
+
category_color = PropertyExtractors.extract(properties, 'Color', 'rollup')
|
|
77
|
+
category_order = PropertyExtractors.extract(properties, 'Category Order', 'rollup')
|
|
78
|
+
|
|
79
|
+
items_by_category[category_name] ||= {
|
|
80
|
+
'title' => category_name,
|
|
81
|
+
'category' => category_name,
|
|
82
|
+
'subcategory' => nil,
|
|
83
|
+
'icon' => category_icon,
|
|
84
|
+
'order' => category_order || 999,
|
|
85
|
+
'items' => []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
items_by_category[category_name]['items'] << {
|
|
89
|
+
'name' => name,
|
|
90
|
+
'level' => level,
|
|
91
|
+
'years' => years,
|
|
92
|
+
'description' => nil,
|
|
93
|
+
'icon' => nil,
|
|
94
|
+
'color' => category_color,
|
|
95
|
+
'featured' => featured,
|
|
96
|
+
'order' => order || 999,
|
|
97
|
+
'id' => page['id']
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Sort categories by order
|
|
102
|
+
items_by_category = items_by_category.sort_by { |_, data| data['order'].to_i }.to_h
|
|
103
|
+
|
|
104
|
+
# Sort items within each category
|
|
105
|
+
items_by_category.each_value do |data|
|
|
106
|
+
data['items'].sort_by! { |item| item['order'].to_i }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
items_by_category
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Organize items grouped by a field
|
|
113
|
+
# @param notion_data [Hash] Raw data from Notion API
|
|
114
|
+
# @param properties_config [Array<Hash>] Property configuration
|
|
115
|
+
# @param group_field [String] Field to group by
|
|
116
|
+
# @param sort_by [String] Field to sort by within groups
|
|
117
|
+
# @param sort_order [String] Sort order
|
|
118
|
+
# @return [Hash] Items grouped by field
|
|
119
|
+
def organize_grouped_by(notion_data, properties_config, group_field, sort_by, sort_order)
|
|
120
|
+
grouped = {}
|
|
121
|
+
|
|
122
|
+
notion_data['results'].each do |page|
|
|
123
|
+
item = PropertyExtractors.extract_all(page['properties'], properties_config)
|
|
124
|
+
item['id'] = page['id']
|
|
125
|
+
|
|
126
|
+
next if item['title'].nil? || item['title'].to_s.empty?
|
|
127
|
+
|
|
128
|
+
group_key = item[group_field]
|
|
129
|
+
group_key = group_key.first if group_key.is_a?(Array)
|
|
130
|
+
group_key ||= 'Other'
|
|
131
|
+
|
|
132
|
+
grouped[group_key] ||= []
|
|
133
|
+
grouped[group_key] << item
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Sort within groups
|
|
137
|
+
grouped.each_value do |items|
|
|
138
|
+
sort_items(items, sort_by, sort_order)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
grouped
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Organize items in a nested tree structure
|
|
145
|
+
# @param notion_data [Hash] Raw data from Notion API
|
|
146
|
+
# @param properties_config [Array<Hash>] Property configuration
|
|
147
|
+
# @param parent_field [String] Field containing parent reference
|
|
148
|
+
# @param sort_by [String] Field to sort by
|
|
149
|
+
# @param sort_order [String] Sort order
|
|
150
|
+
# @return [Array<Hash>] Nested tree of items
|
|
151
|
+
def organize_nested(notion_data, properties_config, parent_field, sort_by, sort_order)
|
|
152
|
+
items = {}
|
|
153
|
+
roots = []
|
|
154
|
+
|
|
155
|
+
# First pass: extract all items
|
|
156
|
+
notion_data['results'].each do |page|
|
|
157
|
+
item = PropertyExtractors.extract_all(page['properties'], properties_config)
|
|
158
|
+
item['id'] = page['id']
|
|
159
|
+
item['children'] = []
|
|
160
|
+
items[page['id']] = item
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Second pass: build tree structure
|
|
164
|
+
items.each_value do |item|
|
|
165
|
+
parent_ids = item[parent_field]
|
|
166
|
+
parent_id = parent_ids.is_a?(Array) ? parent_ids.first : parent_ids
|
|
167
|
+
|
|
168
|
+
if parent_id && items[parent_id]
|
|
169
|
+
items[parent_id]['children'] << item
|
|
170
|
+
else
|
|
171
|
+
roots << item
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Sort at each level
|
|
176
|
+
sort_nested(roots, sort_by, sort_order)
|
|
177
|
+
|
|
178
|
+
roots
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Sort items by field
|
|
182
|
+
# @param items [Array<Hash>] Items to sort
|
|
183
|
+
# @param sort_by [String] Field to sort by
|
|
184
|
+
# @param sort_order [String] Sort order ('asc' or 'desc')
|
|
185
|
+
# @return [Array<Hash>] Sorted items
|
|
186
|
+
def sort_items(items, sort_by, sort_order)
|
|
187
|
+
return items unless sort_by && !sort_by.empty?
|
|
188
|
+
|
|
189
|
+
items.sort_by! do |item|
|
|
190
|
+
value = item[sort_by]
|
|
191
|
+
case value
|
|
192
|
+
when nil then sort_order == 'desc' ? -Float::INFINITY : Float::INFINITY
|
|
193
|
+
when Numeric then value
|
|
194
|
+
when String then value.downcase
|
|
195
|
+
when Hash then value['start'] || '' # For date objects
|
|
196
|
+
else value.to_s.downcase
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
items.reverse! if sort_order == 'desc'
|
|
201
|
+
items
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Recursively sort nested items
|
|
205
|
+
# @param items [Array<Hash>] Items to sort
|
|
206
|
+
# @param sort_by [String] Field to sort by
|
|
207
|
+
# @param sort_order [String] Sort order
|
|
208
|
+
def sort_nested(items, sort_by, sort_order)
|
|
209
|
+
sort_items(items, sort_by, sort_order)
|
|
210
|
+
|
|
211
|
+
items.each do |item|
|
|
212
|
+
sort_nested(item['children'], sort_by, sort_order) if item['children']&.any?
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Check if data is present (non-empty)
|
|
217
|
+
# @param data [Hash, Array] Data to check
|
|
218
|
+
# @return [Boolean] True if data is present
|
|
219
|
+
def data_present?(data)
|
|
220
|
+
return false if data.nil?
|
|
221
|
+
|
|
222
|
+
if data.is_a?(Hash)
|
|
223
|
+
data.size.positive?
|
|
224
|
+
else
|
|
225
|
+
data.length.positive?
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|