collavre_notion 0.1.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/README.md +58 -0
- data/Rakefile +3 -0
- data/app/controllers/collavre_notion/application_controller.rb +12 -0
- data/app/controllers/collavre_notion/creatives/notion_integrations_controller.rb +196 -0
- data/app/controllers/collavre_notion/notion_auth_controller.rb +48 -0
- data/app/javascript/collavre_notion.js +506 -0
- data/app/jobs/collavre_notion/notion_export_job.rb +29 -0
- data/app/jobs/collavre_notion/notion_sync_job.rb +47 -0
- data/app/models/collavre_notion/notion_account.rb +17 -0
- data/app/models/collavre_notion/notion_block_link.rb +10 -0
- data/app/models/collavre_notion/notion_page_link.rb +19 -0
- data/app/services/collavre_notion/notion_client.rb +231 -0
- data/app/services/collavre_notion/notion_creative_exporter.rb +296 -0
- data/app/services/collavre_notion/notion_service.rb +249 -0
- data/app/views/collavre_notion/integrations/_modal.html.erb +91 -0
- data/config/locales/en.yml +36 -0
- data/config/locales/ko.yml +36 -0
- data/config/routes.rb +5 -0
- data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
- data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
- data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
- data/lib/collavre_notion/engine.rb +89 -0
- data/lib/collavre_notion/version.rb +3 -0
- data/lib/collavre_notion.rb +5 -0
- metadata +109 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 80b38ec1b831d76c7062b71483225adb5985e90ea4abb7d6b29023e53182c299
|
|
4
|
+
data.tar.gz: a57dd0b62793e9129f3a0190d4f42cf86c5687b7c39737ec220cd0e5fad90664
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 19697a81efd966dc00d0290f4d827dc48a471dcf3e65d4d8e5b0e618830bf9ac237ac67181d74ee75a82ad648aa77d863521357183fbcebe318955f62dd38550
|
|
7
|
+
data.tar.gz: acbfc93ef56e9ac9955a988ed204cfb4f39230c096d555cef7f62115f908d9b53332dc4062914fe39ab939afc01f659c92c501d5994a0494efbe3aae4da061b8
|
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Collavre Notion Engine
|
|
2
|
+
|
|
3
|
+
Notion integration plugin engine for Collavre. Provides OAuth connection, page linking, and creative export/sync to Notion.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
- [Setup Guide](docs/SETUP.md) - Step-by-step instructions for creating and configuring a Notion integration
|
|
8
|
+
- [Architecture](docs/ARCHITECTURE.md) - Technical documentation about the integration
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
### 1. Add to Gemfile
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem "collavre_notion", path: "engines/collavre_notion"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Add JavaScript Import (Required)
|
|
20
|
+
|
|
21
|
+
The JavaScript must be explicitly imported in your host application:
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
// app/javascript/application.js
|
|
25
|
+
import "collavre_notion"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This is required because the host app controls which integrations are loaded. Without this import, the Notion integration modal will not work.
|
|
29
|
+
|
|
30
|
+
### 3. Configure Environment Variables
|
|
31
|
+
|
|
32
|
+
| Variable | Required | Description |
|
|
33
|
+
|----------|----------|-------------|
|
|
34
|
+
| `NOTION_CLIENT_ID` | Yes | OAuth Client ID |
|
|
35
|
+
| `NOTION_CLIENT_SECRET` | Yes | OAuth Client Secret |
|
|
36
|
+
|
|
37
|
+
### 4. Run Migrations
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
rails db:migrate
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Automatic Configuration
|
|
44
|
+
|
|
45
|
+
The following are automatically configured by the engine:
|
|
46
|
+
|
|
47
|
+
- **Routes**: Mounted at `/notion` (no manual `mount` needed in `routes.rb`)
|
|
48
|
+
- **OAuth Callback**: Handles `/auth/notion/callback`
|
|
49
|
+
- **Migrations**: Database migrations are automatically included
|
|
50
|
+
- **i18n**: Locale files (en, ko) are automatically loaded
|
|
51
|
+
- **Integration Registry**: Automatically registers with `Collavre::IntegrationRegistry`
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- OAuth 2.0 connection to Notion workspaces
|
|
56
|
+
- Export a creative tree to a Notion page
|
|
57
|
+
- Sync linked pages with updated creative content
|
|
58
|
+
- Manage linked page connections per creative
|
data/Rakefile
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
module CollavreNotion
|
|
2
|
+
module Creatives
|
|
3
|
+
class NotionIntegrationsController < ApplicationController
|
|
4
|
+
before_action :set_creative
|
|
5
|
+
before_action :ensure_read_permission
|
|
6
|
+
before_action :ensure_admin_permission, only: [ :show, :update ]
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
account = Current.user.notion_account
|
|
10
|
+
links = linked_page_links(account)
|
|
11
|
+
|
|
12
|
+
Rails.logger.info("Notion Integration: Showing status for user #{Current.user.id}, connected: #{account.present?}")
|
|
13
|
+
|
|
14
|
+
render json: {
|
|
15
|
+
connected: account.present?,
|
|
16
|
+
creative_title: helpers.strip_tags(@creative.description).strip.presence || "Untitled Creative",
|
|
17
|
+
account: account && {
|
|
18
|
+
workspace_name: account.workspace_name,
|
|
19
|
+
workspace_id: account.workspace_id,
|
|
20
|
+
bot_id: account.bot_id
|
|
21
|
+
},
|
|
22
|
+
linked_pages: links.map do |link|
|
|
23
|
+
{
|
|
24
|
+
page_id: link.page_id,
|
|
25
|
+
page_title: link.page_title,
|
|
26
|
+
page_url: link.page_url,
|
|
27
|
+
last_synced_at: link.last_synced_at
|
|
28
|
+
}
|
|
29
|
+
end,
|
|
30
|
+
available_pages: account.present? ? fetch_available_pages(account) : []
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update
|
|
35
|
+
account = Current.user.notion_account
|
|
36
|
+
unless account
|
|
37
|
+
render json: { error: "not_connected" }, status: :unprocessable_entity
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Rails.logger.info("Notion Integration Update: Full params = #{params.to_unsafe_h}")
|
|
42
|
+
|
|
43
|
+
integration_attributes = integration_params
|
|
44
|
+
Rails.logger.info("Notion Integration Update: integration_params = #{integration_attributes}")
|
|
45
|
+
|
|
46
|
+
request_action = request.request_parameters[:action]
|
|
47
|
+
action = integration_attributes[:action]
|
|
48
|
+
if action.blank? || action.to_s == action_name
|
|
49
|
+
action = request_action.presence
|
|
50
|
+
end
|
|
51
|
+
parent_page_id = integration_attributes[:parent_page_id]
|
|
52
|
+
|
|
53
|
+
Rails.logger.info("Notion Integration Update: action=#{action}, parent_page_id=#{parent_page_id}")
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
case action
|
|
57
|
+
when "export"
|
|
58
|
+
# Export creative to Notion
|
|
59
|
+
CollavreNotion::NotionExportJob.perform_later(@creative, account, parent_page_id)
|
|
60
|
+
render json: { success: true, message: "Export started" }
|
|
61
|
+
when "sync"
|
|
62
|
+
# Sync existing page
|
|
63
|
+
link = linked_page_links(account).first
|
|
64
|
+
if link
|
|
65
|
+
CollavreNotion::NotionSyncJob.perform_later(@creative, account, link.page_id)
|
|
66
|
+
render json: { success: true, message: "Sync started" }
|
|
67
|
+
else
|
|
68
|
+
render json: { error: "no_linked_page" }, status: :unprocessable_entity
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
render json: { error: "invalid_action" }, status: :unprocessable_entity
|
|
72
|
+
end
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
Rails.logger.error("Notion integration error: #{e.message}")
|
|
75
|
+
render json: { error: "operation_failed", message: e.message }, status: :internal_server_error
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def destroy
|
|
80
|
+
unless @creative.has_permission?(Current.user, :write)
|
|
81
|
+
render json: { error: "forbidden" }, status: :forbidden
|
|
82
|
+
return
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
account = Current.user.notion_account
|
|
86
|
+
unless account
|
|
87
|
+
render json: { error: "not_connected" }, status: :unprocessable_entity
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
page_id = params[:page_id]
|
|
92
|
+
|
|
93
|
+
if page_id
|
|
94
|
+
# Remove specific page link
|
|
95
|
+
link = linked_page_links(account).find_by(page_id: page_id)
|
|
96
|
+
unless link
|
|
97
|
+
render json: { error: "not_found" }, status: :not_found
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
link.destroy!
|
|
102
|
+
else
|
|
103
|
+
# Remove all page links for this creative
|
|
104
|
+
linked_page_links(account).destroy_all
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
links = linked_page_links(account)
|
|
108
|
+
render json: {
|
|
109
|
+
success: true,
|
|
110
|
+
linked_pages: links.map do |link|
|
|
111
|
+
{
|
|
112
|
+
page_id: link.page_id,
|
|
113
|
+
page_title: link.page_title,
|
|
114
|
+
page_url: link.page_url,
|
|
115
|
+
last_synced_at: link.last_synced_at
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def set_creative
|
|
124
|
+
@creative = Collavre::Creative.find(params[:creative_id])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def ensure_read_permission
|
|
128
|
+
return if @creative.has_permission?(Current.user, :read)
|
|
129
|
+
|
|
130
|
+
render json: { error: "forbidden" }, status: :forbidden
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def ensure_admin_permission
|
|
134
|
+
return if @creative.has_permission?(Current.user, :admin)
|
|
135
|
+
|
|
136
|
+
render json: { error: "forbidden" }, status: :forbidden
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def linked_page_links(account)
|
|
140
|
+
return CollavreNotion::NotionPageLink.none unless account
|
|
141
|
+
|
|
142
|
+
@creative.notion_page_links.where(notion_account: account)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def integration_params
|
|
146
|
+
permitted_keys = [ :action, :parent_page_id, :page_id ]
|
|
147
|
+
|
|
148
|
+
if params[:notion_integration].present?
|
|
149
|
+
params.require(:notion_integration).permit(*permitted_keys)
|
|
150
|
+
else
|
|
151
|
+
params.permit(*permitted_keys)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def fetch_available_pages(account)
|
|
156
|
+
Rails.logger.info("Notion Integration: Fetching available pages for account #{account.id}")
|
|
157
|
+
|
|
158
|
+
begin
|
|
159
|
+
service = CollavreNotion::NotionService.new(user: account.user)
|
|
160
|
+
pages_response = service.search_pages(page_size: 50)
|
|
161
|
+
|
|
162
|
+
Rails.logger.info("Notion Integration: Search pages response - #{pages_response["results"]&.length || 0} pages found")
|
|
163
|
+
|
|
164
|
+
pages = pages_response["results"]&.map do |page|
|
|
165
|
+
{
|
|
166
|
+
id: page["id"],
|
|
167
|
+
title: extract_page_title(page),
|
|
168
|
+
url: page["url"],
|
|
169
|
+
parent: page["parent"]
|
|
170
|
+
}
|
|
171
|
+
end || []
|
|
172
|
+
|
|
173
|
+
Rails.logger.info("Notion Integration: Returning #{pages.length} formatted pages")
|
|
174
|
+
pages
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
Rails.logger.error("Notion Integration: Error fetching pages - #{e.class}: #{e.message}")
|
|
177
|
+
Rails.logger.error(e.backtrace.join("\n"))
|
|
178
|
+
[]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def extract_page_title(page)
|
|
183
|
+
# Handle different title property structures
|
|
184
|
+
title_property = page.dig("properties", "title")
|
|
185
|
+
|
|
186
|
+
if title_property && title_property["title"]
|
|
187
|
+
title_property["title"].map { |t| t.dig("text", "content") }.compact.join("")
|
|
188
|
+
elsif page["title"]
|
|
189
|
+
page["title"].map { |t| t.dig("text", "content") }.compact.join("")
|
|
190
|
+
else
|
|
191
|
+
"Untitled"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module CollavreNotion
|
|
2
|
+
class NotionAuthController < ApplicationController
|
|
3
|
+
allow_unauthenticated_access only: :callback
|
|
4
|
+
before_action -> { enforce_auth_provider!(:notion) }, only: :callback
|
|
5
|
+
|
|
6
|
+
def callback
|
|
7
|
+
auth = request.env["omniauth.auth"]
|
|
8
|
+
notion = CollavreNotion::NotionAccount.find_or_initialize_by(notion_uid: auth.uid)
|
|
9
|
+
|
|
10
|
+
if notion.new_record?
|
|
11
|
+
unless Current.user
|
|
12
|
+
redirect_to collavre.new_session_path, alert: I18n.t("collavre_notion.notion_auth.login_first")
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
notion.user = Current.user
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
notion.token = auth.credentials.token
|
|
19
|
+
notion.workspace_name = auth.info.name
|
|
20
|
+
notion.save!
|
|
21
|
+
|
|
22
|
+
# If opened in popup, close it and notify parent window
|
|
23
|
+
if params[:popup] || request.referer&.include?("popup=true") || session[:oauth_popup]
|
|
24
|
+
session.delete(:oauth_popup)
|
|
25
|
+
render html: <<-HTML.html_safe
|
|
26
|
+
<!DOCTYPE html>
|
|
27
|
+
<html>
|
|
28
|
+
<head><title>Notion Connected</title></head>
|
|
29
|
+
<body>
|
|
30
|
+
<script>
|
|
31
|
+
if (window.opener) {
|
|
32
|
+
window.opener.postMessage({ type: 'notion_oauth_success', connected: true }, '*');
|
|
33
|
+
window.close();
|
|
34
|
+
} else {
|
|
35
|
+
window.location.href = '#{collavre.creatives_path}';
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
<p>#{I18n.t("collavre_notion.notion_auth.connected")}</p>
|
|
39
|
+
<p>This window should close automatically. If not, you can close it manually.</p>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
42
|
+
HTML
|
|
43
|
+
else
|
|
44
|
+
redirect_to collavre.creatives_path, notice: I18n.t("collavre_notion.notion_auth.connected")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|