collavre 0.7.2 β 0.8.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 +4 -4
- data/app/assets/stylesheets/collavre/comments_popup.css +36 -0
- data/app/assets/stylesheets/collavre/creatives.css +27 -1
- data/app/controllers/collavre/comments_controller.rb +11 -1
- data/app/controllers/collavre/creatives_controller.rb +21 -2
- data/app/controllers/collavre/topics_controller.rb +36 -1
- data/app/helpers/collavre/application_helper.rb +37 -0
- data/app/helpers/collavre/creatives_helper.rb +3 -1
- data/app/javascript/components/creative_tree_row.js +1 -0
- data/app/javascript/controllers/comments/list_controller.js +13 -2
- data/app/javascript/controllers/comments/popup_controller.js +18 -0
- data/app/javascript/controllers/comments/topics_controller.js +80 -0
- data/app/javascript/controllers/creatives/tree_controller.js +44 -0
- data/app/javascript/creatives/tree_renderer.js +1 -0
- data/app/javascript/lib/api/creatives.js +14 -0
- data/app/javascript/modules/creative_row_editor.js +42 -0
- data/app/models/collavre/creative.rb +62 -0
- data/app/models/collavre/topic.rb +16 -0
- data/app/services/collavre/creatives/index_query.rb +13 -3
- data/app/services/collavre/creatives/progress_service.rb +11 -6
- data/app/services/collavre/creatives/tree_builder.rb +2 -0
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +10 -0
- data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +3 -0
- data/app/views/collavre/creatives/index.html.erb +4 -1
- data/config/locales/comments.en.yml +4 -0
- data/config/locales/comments.ko.yml +4 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +6 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20260313011922_add_archived_at_to_creatives_and_topics.rb +9 -0
- data/lib/collavre/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 220ee539008a01bf0df3c62d9c815ff16ed8687fb27e2c98d1a62288b3f302e1
|
|
4
|
+
data.tar.gz: 97d41b82062cdc7c2223e4ab707bfc46465d65bf45555e7fb418f50626ed9795
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 770cddad2594765fdfea31511f0f5f1479c16345688fcd29102417024a3e6685c627cf43f8b159b2b7fbffbe3a29e4fb3b62d8674d1be0828beedf0666ac3af8
|
|
7
|
+
data.tar.gz: f9efa032715bf3e1493fa29ef98b46d6d1a98460d364595bc8ef2692bb6cd78ce464afeed35f3c456fdc3177e380434db87b8f8d4cea48f25d240f32a35e2e3c
|
|
@@ -1275,6 +1275,42 @@ body.chat-fullscreen {
|
|
|
1275
1275
|
outline: none;
|
|
1276
1276
|
}
|
|
1277
1277
|
|
|
1278
|
+
/* Archive topic styles */
|
|
1279
|
+
.topic-archived-toggle {
|
|
1280
|
+
display: inline-flex;
|
|
1281
|
+
align-items: center;
|
|
1282
|
+
padding: 0.15em var(--space-2);
|
|
1283
|
+
border-radius: var(--radius-3);
|
|
1284
|
+
font-size: var(--text-0);
|
|
1285
|
+
cursor: pointer;
|
|
1286
|
+
opacity: 0.6;
|
|
1287
|
+
border: 1px dashed var(--border-color);
|
|
1288
|
+
}
|
|
1289
|
+
.topic-archived-toggle:hover {
|
|
1290
|
+
opacity: 1;
|
|
1291
|
+
}
|
|
1292
|
+
.topic-tag.topic-archived {
|
|
1293
|
+
opacity: 0.5;
|
|
1294
|
+
font-style: italic;
|
|
1295
|
+
}
|
|
1296
|
+
.archive-topic-btn,
|
|
1297
|
+
.unarchive-topic-btn {
|
|
1298
|
+
background: none;
|
|
1299
|
+
border: none;
|
|
1300
|
+
cursor: pointer;
|
|
1301
|
+
font-size: var(--text-00);
|
|
1302
|
+
padding: 0 var(--space-1);
|
|
1303
|
+
opacity: 0.6;
|
|
1304
|
+
vertical-align: middle;
|
|
1305
|
+
}
|
|
1306
|
+
.archive-topic-btn:hover,
|
|
1307
|
+
.unarchive-topic-btn:hover {
|
|
1308
|
+
opacity: 1;
|
|
1309
|
+
}
|
|
1310
|
+
.delete-topic-btn {
|
|
1311
|
+
margin-left: var(--space-1);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1278
1314
|
/* Selection action bar */
|
|
1279
1315
|
.selection-action-bar {
|
|
1280
1316
|
position: sticky;
|
|
@@ -678,4 +678,30 @@ creative-tree-row:not([expanded]) .creative-toggle-btn {
|
|
|
678
678
|
left: 0.5em;
|
|
679
679
|
width: auto;
|
|
680
680
|
}
|
|
681
|
-
}
|
|
681
|
+
}
|
|
682
|
+
/* Archive toggle button */
|
|
683
|
+
.archive-toggle-btn {
|
|
684
|
+
background: none;
|
|
685
|
+
border: none;
|
|
686
|
+
cursor: pointer;
|
|
687
|
+
font-size: var(--text-2);
|
|
688
|
+
padding: var(--space-1) var(--space-2);
|
|
689
|
+
opacity: 0.5;
|
|
690
|
+
transition: opacity 0.2s var(--ease-2);
|
|
691
|
+
}
|
|
692
|
+
.archive-toggle-btn:hover {
|
|
693
|
+
opacity: 0.8;
|
|
694
|
+
}
|
|
695
|
+
.archive-toggle-btn.active {
|
|
696
|
+
opacity: 1.0;
|
|
697
|
+
background: var(--surface-input);
|
|
698
|
+
border-radius: var(--radius-2);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/* Archived creative row */
|
|
702
|
+
creative-tree-row[archived] .creative-tree {
|
|
703
|
+
opacity: 0.5;
|
|
704
|
+
}
|
|
705
|
+
creative-tree-row[archived] .creative-row {
|
|
706
|
+
background-color: var(--surface-input);
|
|
707
|
+
}
|
|
@@ -60,7 +60,13 @@ module Collavre
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Apply the Topic Filter
|
|
63
|
-
|
|
63
|
+
if effective_topic_id.present?
|
|
64
|
+
scope = scope.where(topic_id: effective_topic_id)
|
|
65
|
+
else
|
|
66
|
+
# Main view: exclude comments from archived topics
|
|
67
|
+
archived_topic_ids = @creative.topics.archived.pluck(:id)
|
|
68
|
+
scope = scope.where.not(topic_id: archived_topic_ids) if archived_topic_ids.any?
|
|
69
|
+
end
|
|
64
70
|
|
|
65
71
|
# Default order: Newest first (created_at DESC)
|
|
66
72
|
# This matches the column-reverse layout where the first item in the list is the visual bottom (Newest).
|
|
@@ -150,6 +156,10 @@ module Collavre
|
|
|
150
156
|
end
|
|
151
157
|
|
|
152
158
|
def create
|
|
159
|
+
if @creative.archived?
|
|
160
|
+
render json: { error: I18n.t("collavre.comments.archived_creative") }, status: :forbidden and return
|
|
161
|
+
end
|
|
162
|
+
|
|
153
163
|
unless @creative.has_permission?(Current.user, :feedback)
|
|
154
164
|
render json: { error: I18n.t("collavre.comments.no_permission") }, status: :forbidden and return
|
|
155
165
|
end
|
|
@@ -9,7 +9,7 @@ module Collavre
|
|
|
9
9
|
# Removed unauthenticated access to index and show actions
|
|
10
10
|
allow_unauthenticated_access only: %i[ index children export_markdown show slide_view ]
|
|
11
11
|
before_action :enforce_creatives_login_policy, only: %i[ index children export_markdown show slide_view ]
|
|
12
|
-
before_action :set_creative, only: %i[ show edit update destroy parent_suggestions slide_view request_permission unconvert contexts update_contexts update_metadata ]
|
|
12
|
+
before_action :set_creative, only: %i[ show edit update destroy parent_suggestions slide_view request_permission unconvert contexts update_contexts update_metadata archive unarchive ]
|
|
13
13
|
|
|
14
14
|
def index
|
|
15
15
|
respond_to do |format|
|
|
@@ -310,6 +310,24 @@ module Collavre
|
|
|
310
310
|
end
|
|
311
311
|
end
|
|
312
312
|
|
|
313
|
+
def archive
|
|
314
|
+
unless @creative.has_permission?(Current.user, :write) || @creative.user == Current.user
|
|
315
|
+
render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
@creative.archive!
|
|
319
|
+
head :ok
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def unarchive
|
|
323
|
+
unless @creative.has_permission?(Current.user, :write) || @creative.user == Current.user
|
|
324
|
+
render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden and return
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
@creative.unarchive!
|
|
328
|
+
head :ok
|
|
329
|
+
end
|
|
330
|
+
|
|
313
331
|
def destroy
|
|
314
332
|
parent = @creative.parent
|
|
315
333
|
unless @creative.has_permission?(Current.user, :admin)
|
|
@@ -355,7 +373,8 @@ module Collavre
|
|
|
355
373
|
params[:due_after].present? ||
|
|
356
374
|
params[:has_due_date].present? ||
|
|
357
375
|
params[:assignee_id].present? ||
|
|
358
|
-
params[:unassigned].present?
|
|
376
|
+
params[:unassigned].present? ||
|
|
377
|
+
params[:show_archived].present?
|
|
359
378
|
end
|
|
360
379
|
|
|
361
380
|
def serialize_creatives(collection)
|
|
@@ -6,8 +6,13 @@ module Collavre
|
|
|
6
6
|
is_owner = @creative.user == Current.user
|
|
7
7
|
can_manage = @creative.has_permission?(Current.user, :admin) || is_owner
|
|
8
8
|
can_create_topic = can_manage || @creative.has_permission?(Current.user, :write)
|
|
9
|
+
|
|
10
|
+
active_topics = @creative.topics.active.order(:created_at)
|
|
11
|
+
archived_topics = @creative.topics.archived.order(:created_at)
|
|
12
|
+
|
|
9
13
|
render json: {
|
|
10
|
-
topics:
|
|
14
|
+
topics: active_topics,
|
|
15
|
+
archived_topics: archived_topics,
|
|
11
16
|
can_manage: can_manage,
|
|
12
17
|
can_create_topic: can_create_topic
|
|
13
18
|
}
|
|
@@ -100,6 +105,36 @@ module Collavre
|
|
|
100
105
|
render json: { success: true, topic: topic.slice(:id, :name), target_creative_id: target_creative.id }
|
|
101
106
|
end
|
|
102
107
|
|
|
108
|
+
def archive
|
|
109
|
+
unless @creative.has_permission?(Current.user, :write) || @creative.user == Current.user
|
|
110
|
+
render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
topic = @creative.topics.find(params[:id])
|
|
114
|
+
topic.archive!
|
|
115
|
+
|
|
116
|
+
TopicsChannel.broadcast_to(
|
|
117
|
+
@creative,
|
|
118
|
+
{ action: "archived", topic: topic.slice(:id, :name) }
|
|
119
|
+
)
|
|
120
|
+
render json: { success: true }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def unarchive
|
|
124
|
+
unless @creative.has_permission?(Current.user, :write) || @creative.user == Current.user
|
|
125
|
+
render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
topic = @creative.topics.find(params[:id])
|
|
129
|
+
topic.unarchive!
|
|
130
|
+
|
|
131
|
+
TopicsChannel.broadcast_to(
|
|
132
|
+
@creative,
|
|
133
|
+
{ action: "unarchived", topic: topic.slice(:id, :name, :archived_at) }
|
|
134
|
+
)
|
|
135
|
+
render json: { success: true }
|
|
136
|
+
end
|
|
137
|
+
|
|
103
138
|
def reorder
|
|
104
139
|
unless @creative.has_permission?(Current.user, :admin) || @creative.user == Current.user
|
|
105
140
|
render json: { error: I18n.t("collavre.topics.no_permission") }, status: :forbidden and return
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
module ApplicationHelper
|
|
3
|
+
# All Collavre engine stylesheets in load order.
|
|
4
|
+
# Host apps should call <%= collavre_stylesheets %> in their layout <head>
|
|
5
|
+
# instead of listing individual stylesheet_link_tags.
|
|
6
|
+
COLLAVRE_STYLESHEETS = %w[
|
|
7
|
+
collavre/design_tokens
|
|
8
|
+
collavre/dark_mode
|
|
9
|
+
collavre/creatives
|
|
10
|
+
collavre/actiontext
|
|
11
|
+
collavre/activity_logs
|
|
12
|
+
collavre/user_menu
|
|
13
|
+
collavre/org_chart
|
|
14
|
+
collavre/popup
|
|
15
|
+
collavre/comments_popup
|
|
16
|
+
collavre/comment_versions
|
|
17
|
+
collavre/mention_menu
|
|
18
|
+
collavre/slide_view
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
COLLAVRE_PRINT_STYLESHEETS = %w[
|
|
22
|
+
collavre/print
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
# Renders all Collavre engine stylesheet tags.
|
|
26
|
+
# Call this once in the host app's layout <head> section.
|
|
27
|
+
#
|
|
28
|
+
# <%= collavre_stylesheets %>
|
|
29
|
+
#
|
|
30
|
+
def collavre_stylesheets
|
|
31
|
+
tags = COLLAVRE_STYLESHEETS.map do |sheet|
|
|
32
|
+
stylesheet_link_tag(sheet)
|
|
33
|
+
end
|
|
34
|
+
tags += COLLAVRE_PRINT_STYLESHEETS.map do |sheet|
|
|
35
|
+
stylesheet_link_tag(sheet, media: "print")
|
|
36
|
+
end
|
|
37
|
+
safe_join(tags, "\n ")
|
|
38
|
+
end
|
|
39
|
+
|
|
3
40
|
# Returns the body CSS class for the current user's theme.
|
|
4
41
|
# Custom themes get "light-mode" to disable OS dark mode overrides,
|
|
5
42
|
# ensuring the custom theme controls all colors regardless of OS setting.
|
|
@@ -34,7 +34,9 @@ module Collavre
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
content_tag(:div, class: "creative-row-end") do
|
|
37
|
-
comment_part = if creative.
|
|
37
|
+
comment_part = if creative.archived?
|
|
38
|
+
safe_join([])
|
|
39
|
+
elsif creative.has_permission?(Current.user, :feedback)
|
|
38
40
|
origin = creative.effective_origin
|
|
39
41
|
comments_count = origin.comments.size
|
|
40
42
|
pointer = CommentReadPointer.find_by(user: Current.user, creative: origin)
|
|
@@ -23,6 +23,7 @@ class CreativeTreeRow extends LitElement {
|
|
|
23
23
|
editOffIconHtml: { state: true },
|
|
24
24
|
originLinkHtml: { state: true },
|
|
25
25
|
isTitle: { type: Boolean, attribute: "is-title", reflect: true },
|
|
26
|
+
archived: { type: Boolean, attribute: "archived", reflect: true },
|
|
26
27
|
loadingChildren: { type: Boolean, attribute: "loading-children", reflect: true },
|
|
27
28
|
_loadingDotsState: { state: true }
|
|
28
29
|
};
|
|
@@ -59,6 +59,13 @@ export default class extends Controller {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
handleTopicChange(event) {
|
|
62
|
+
// During notifyChildControllers, topic loading fires change events before
|
|
63
|
+
// onPopupOpened sets up highlightAfterLoad. Suppress these to avoid a
|
|
64
|
+
// race where a non-highlight load overwrites the deep-link highlight load.
|
|
65
|
+
if (this.suppressTopicChangeLoad) {
|
|
66
|
+
this.currentTopicId = event.detail.topicId
|
|
67
|
+
return
|
|
68
|
+
}
|
|
62
69
|
this.currentTopicId = event.detail.topicId
|
|
63
70
|
this.resetToLatest()
|
|
64
71
|
}
|
|
@@ -148,10 +155,13 @@ export default class extends Controller {
|
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
const requestTopicId = this.currentTopicId || ""
|
|
158
|
+
const requestCreativeId = this.creativeId
|
|
151
159
|
|
|
152
160
|
this.fetchComments(params).then((html) => {
|
|
153
|
-
//
|
|
154
|
-
//
|
|
161
|
+
// Discard stale responses if creative or topic changed while fetching.
|
|
162
|
+
// This prevents a race condition where switching creatives causes
|
|
163
|
+
// the old creative's comments to overwrite the new creative's list.
|
|
164
|
+
if (this.creativeId !== requestCreativeId) return
|
|
155
165
|
if (String(this.currentTopicId || "") !== String(requestTopicId)) return
|
|
156
166
|
|
|
157
167
|
this.listTarget.innerHTML = html
|
|
@@ -307,6 +317,7 @@ export default class extends Controller {
|
|
|
307
317
|
if (!comment) return
|
|
308
318
|
comment.scrollIntoView({ behavior: 'auto', block: 'center' })
|
|
309
319
|
comment.classList.add('highlight-flash')
|
|
320
|
+
comment.dataset.highlighted = 'true'
|
|
310
321
|
window.setTimeout(() => comment.classList.remove('highlight-flash'), 2000)
|
|
311
322
|
}
|
|
312
323
|
|
|
@@ -199,11 +199,29 @@ export default class extends Controller {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
async notifyChildControllers({ creativeId, canComment, highlightId }) {
|
|
202
|
+
// Pre-set creativeId on list controller BEFORE loading topics.
|
|
203
|
+
// Topics loading triggers a change event that list controller handles.
|
|
204
|
+
// Without this, list controller still holds the previous creative's ID
|
|
205
|
+
// and would fetch comments for the wrong creative (race condition).
|
|
206
|
+
//
|
|
207
|
+
// Also suppress topic-change-triggered loads during topic initialization.
|
|
208
|
+
// Without this, the topic change event fires loadInitialComments() before
|
|
209
|
+
// onPopupOpened sets highlightAfterLoad, causing a race where the non-highlight
|
|
210
|
+
// load can overwrite the deep-link highlight load.
|
|
211
|
+
if (this.listController) {
|
|
212
|
+
this.listController.creativeId = creativeId
|
|
213
|
+
this.listController.suppressTopicChangeLoad = true
|
|
214
|
+
}
|
|
215
|
+
|
|
202
216
|
// Load topics first to establish context
|
|
203
217
|
if (this.topicsController) {
|
|
204
218
|
await this.topicsController.onPopupOpened({ creativeId })
|
|
205
219
|
}
|
|
206
220
|
|
|
221
|
+
if (this.listController) {
|
|
222
|
+
this.listController.suppressTopicChangeLoad = false
|
|
223
|
+
}
|
|
224
|
+
|
|
207
225
|
if (this.formController) {
|
|
208
226
|
this.formController.onPopupOpened({ creativeId, canComment })
|
|
209
227
|
}
|
|
@@ -67,6 +67,8 @@ export default class extends Controller {
|
|
|
67
67
|
this.topics = topics
|
|
68
68
|
this.canManageTopics = canManage
|
|
69
69
|
this.canCreateTopic = canCreateTopic
|
|
70
|
+
this.archivedTopics = data.archived_topics || []
|
|
71
|
+
|
|
70
72
|
this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
|
|
71
73
|
this.restoreSelection()
|
|
72
74
|
}
|
|
@@ -115,12 +117,28 @@ export default class extends Controller {
|
|
|
115
117
|
#${topic.name}`
|
|
116
118
|
|
|
117
119
|
if (canManage) {
|
|
120
|
+
html += `<button class="archive-topic-btn" data-action="click->comments--topics#archiveTopic" data-id="${topic.id}" title="π¦">π¦</button>`
|
|
118
121
|
html += `<button class="delete-topic-btn" data-action="click->comments--topics#deleteTopic" data-id="${topic.id}">×</button>`
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
html += `</span>`
|
|
122
125
|
})
|
|
123
126
|
|
|
127
|
+
// Archived topics section
|
|
128
|
+
if (this.archivedTopics && this.archivedTopics.length > 0) {
|
|
129
|
+
html += `<span class="topic-archived-toggle" data-action="click->comments--topics#toggleArchivedTopics">
|
|
130
|
+
π¦ ${this.archivedTopics.length}
|
|
131
|
+
</span>`
|
|
132
|
+
if (this.showingArchived) {
|
|
133
|
+
this.archivedTopics.forEach(topic => {
|
|
134
|
+
html += `<span class="topic-tag topic-archived" data-id="${topic.id}">
|
|
135
|
+
#${topic.name}
|
|
136
|
+
${canManage ? `<button class="unarchive-topic-btn" data-action="click->comments--topics#unarchiveTopic" data-id="${topic.id}" title="Restore">β©</button>` : ''}
|
|
137
|
+
</span>`
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
// Add create button container (write permission is sufficient for topic creation)
|
|
125
143
|
if (canCreateTopic) {
|
|
126
144
|
html += `<span class="topic-creation-container" data-comments--topics-target="creationContainer">
|
|
@@ -323,6 +341,63 @@ export default class extends Controller {
|
|
|
323
341
|
}
|
|
324
342
|
}
|
|
325
343
|
|
|
344
|
+
async archiveTopic(event) {
|
|
345
|
+
event.stopPropagation()
|
|
346
|
+
const topicId = event.target.dataset.id
|
|
347
|
+
if (!topicId) return
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const response = await fetch(`/creatives/${this.creativeId}/topics/${topicId}/archive`, {
|
|
351
|
+
method: 'PATCH',
|
|
352
|
+
headers: {
|
|
353
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
if (response.ok) {
|
|
358
|
+
if (String(this.currentTopicId) === String(topicId)) {
|
|
359
|
+
this.currentTopicId = ""
|
|
360
|
+
this.dispatch("change", { detail: { topicId: "" } })
|
|
361
|
+
}
|
|
362
|
+
this.loadTopics()
|
|
363
|
+
} else {
|
|
364
|
+
alert("Failed to archive topic")
|
|
365
|
+
}
|
|
366
|
+
} catch (e) {
|
|
367
|
+
console.error("Error archiving topic", e)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async unarchiveTopic(event) {
|
|
372
|
+
event.stopPropagation()
|
|
373
|
+
const topicId = event.target.dataset.id
|
|
374
|
+
if (!topicId) return
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const response = await fetch(`/creatives/${this.creativeId}/topics/${topicId}/unarchive`, {
|
|
378
|
+
method: 'PATCH',
|
|
379
|
+
headers: {
|
|
380
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
if (response.ok) {
|
|
385
|
+
this.loadTopics()
|
|
386
|
+
} else {
|
|
387
|
+
alert("Failed to restore topic")
|
|
388
|
+
}
|
|
389
|
+
} catch (e) {
|
|
390
|
+
console.error("Error restoring topic", e)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
toggleArchivedTopics(event) {
|
|
395
|
+
event.stopPropagation()
|
|
396
|
+
this.showingArchived = !this.showingArchived
|
|
397
|
+
this.renderTopics(this.topics, this.canManageTopics, this.canCreateTopic)
|
|
398
|
+
this.restoreSelection()
|
|
399
|
+
}
|
|
400
|
+
|
|
326
401
|
showInput(event) {
|
|
327
402
|
event.preventDefault()
|
|
328
403
|
const container = this.element.querySelector('[data-comments--topics-target="creationContainer"]')
|
|
@@ -614,6 +689,11 @@ export default class extends Controller {
|
|
|
614
689
|
return
|
|
615
690
|
}
|
|
616
691
|
|
|
692
|
+
if (action === "archived" || action === "unarchived") {
|
|
693
|
+
this.loadTopics()
|
|
694
|
+
return
|
|
695
|
+
}
|
|
696
|
+
|
|
617
697
|
if (action === "reordered" && data.topic_ids) {
|
|
618
698
|
this.reorderTopicsFromServer(data.topic_ids)
|
|
619
699
|
return
|
|
@@ -20,6 +20,7 @@ export default class extends Controller {
|
|
|
20
20
|
this.queueAlignmentUpdate()
|
|
21
21
|
window.addEventListener('resize', this.handleResize)
|
|
22
22
|
this.element.addEventListener('creative-tree:updated', this.handleTreeUpdated)
|
|
23
|
+
this._setupArchiveToggle()
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
disconnect() {
|
|
@@ -29,6 +30,49 @@ export default class extends Controller {
|
|
|
29
30
|
}
|
|
30
31
|
window.removeEventListener('resize', this.handleResize)
|
|
31
32
|
this.element.removeEventListener('creative-tree:updated', this.handleTreeUpdated)
|
|
33
|
+
if (this._archiveToggleHandler) {
|
|
34
|
+
document.getElementById('toggle-archived-btn')?.removeEventListener('click', this._archiveToggleHandler)
|
|
35
|
+
document.getElementById('toggle-archived-btn-mobile')?.removeEventListener('click', this._archiveToggleHandler)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_setupArchiveToggle() {
|
|
40
|
+
const btn = document.getElementById('toggle-archived-btn')
|
|
41
|
+
const mobileBtn = document.getElementById('toggle-archived-btn-mobile')
|
|
42
|
+
if (!btn && !mobileBtn) return
|
|
43
|
+
|
|
44
|
+
this._showingArchived = false
|
|
45
|
+
|
|
46
|
+
const toggle = () => {
|
|
47
|
+
this._showingArchived = !this._showingArchived
|
|
48
|
+
|
|
49
|
+
// Update desktop button
|
|
50
|
+
if (btn) {
|
|
51
|
+
btn.classList.toggle('active', this._showingArchived)
|
|
52
|
+
btn.title = this._showingArchived ? (btn.dataset.hideText || '') : (btn.dataset.showText || '')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Update mobile button text
|
|
56
|
+
if (mobileBtn) {
|
|
57
|
+
const label = this._showingArchived ? (mobileBtn.dataset.hideText || '') : (mobileBtn.dataset.showText || '')
|
|
58
|
+
mobileBtn.textContent = 'π¦ ' + label
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const url = new URL(this.urlValue, window.location.origin)
|
|
62
|
+
if (this._showingArchived) {
|
|
63
|
+
url.searchParams.set('show_archived', 'true')
|
|
64
|
+
} else {
|
|
65
|
+
url.searchParams.delete('show_archived')
|
|
66
|
+
}
|
|
67
|
+
this.urlValue = url.pathname + url.search
|
|
68
|
+
this.element.innerHTML = ''
|
|
69
|
+
delete this.element.dataset.loaded
|
|
70
|
+
this.load()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._archiveToggleHandler = toggle
|
|
74
|
+
if (btn) btn.addEventListener('click', toggle)
|
|
75
|
+
if (mobileBtn) mobileBtn.addEventListener('click', toggle)
|
|
32
76
|
}
|
|
33
77
|
|
|
34
78
|
load() {
|
|
@@ -64,6 +64,7 @@ function applyRowProperties(row, node) {
|
|
|
64
64
|
updateBooleanAttr('hasChildren', 'has-children', node.has_children)
|
|
65
65
|
updateBooleanAttr('expanded', 'expanded', node.expanded)
|
|
66
66
|
updateBooleanAttr('isRoot', 'is-root', node.is_root)
|
|
67
|
+
updateBooleanAttr('archived', 'archived', node.archived)
|
|
67
68
|
|
|
68
69
|
if (node.link_url) {
|
|
69
70
|
if (row.linkUrl !== node.link_url) {
|
|
@@ -58,6 +58,18 @@ export function destroy(id, withChildren = false) {
|
|
|
58
58
|
})
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function archive(id) {
|
|
62
|
+
return csrfFetch(`/creatives/${id}/archive`, {
|
|
63
|
+
method: 'PATCH',
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function unarchive(id) {
|
|
68
|
+
return csrfFetch(`/creatives/${id}/unarchive`, {
|
|
69
|
+
method: 'PATCH',
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export function unconvert(id) {
|
|
62
74
|
return csrfFetch(`/creatives/${id}/unconvert`, {
|
|
63
75
|
method: 'POST',
|
|
@@ -84,6 +96,8 @@ const creativesApi = {
|
|
|
84
96
|
save,
|
|
85
97
|
linkExisting,
|
|
86
98
|
destroy,
|
|
99
|
+
archive,
|
|
100
|
+
unarchive,
|
|
87
101
|
unconvert,
|
|
88
102
|
updateMetadata,
|
|
89
103
|
}
|
|
@@ -86,6 +86,7 @@ export function initializeCreativeRowEditor() {
|
|
|
86
86
|
const levelDownBtn = document.getElementById('inline-level-down');
|
|
87
87
|
const levelUpBtn = document.getElementById('inline-level-up');
|
|
88
88
|
const deletePopupToggle = document.getElementById('inline-delete-popup-toggle');
|
|
89
|
+
const archiveBtn = document.getElementById('inline-archive');
|
|
89
90
|
const deleteBtn = document.getElementById('inline-delete');
|
|
90
91
|
const deleteWithChildrenBtn = document.getElementById('inline-delete-with-children');
|
|
91
92
|
const linkBtn = document.getElementById('inline-link');
|
|
@@ -677,6 +678,13 @@ export function initializeCreativeRowEditor() {
|
|
|
677
678
|
if (levelUpBtn) levelUpBtn.disabled = !canLevelUp;
|
|
678
679
|
|
|
679
680
|
if (deletePopupToggle) deletePopupToggle.disabled = !hasCreativeId;
|
|
681
|
+
if (archiveBtn) {
|
|
682
|
+
archiveBtn.disabled = !hasCreativeId;
|
|
683
|
+
// Toggle label between Archive / Restore based on creative state
|
|
684
|
+
const targetRow = hasCreativeId ? document.querySelector(`creative-tree-row[creative-id="${form.dataset.creativeId}"]`) : null;
|
|
685
|
+
const isArchived = targetRow?.hasAttribute('archived');
|
|
686
|
+
archiveBtn.textContent = isArchived ? (archiveBtn.dataset.restoreLabel || 'Restore') : (archiveBtn.dataset.archiveLabel || 'Archive');
|
|
687
|
+
}
|
|
680
688
|
if (deleteBtn) deleteBtn.disabled = !hasCreativeId;
|
|
681
689
|
if (deleteWithChildrenBtn) deleteWithChildrenBtn.disabled = !hasCreativeId;
|
|
682
690
|
if (linkBtn) linkBtn.disabled = !hasCreativeId || linkBtn.style.display === 'none';
|
|
@@ -1848,6 +1856,40 @@ export function initializeCreativeRowEditor() {
|
|
|
1848
1856
|
levelUpBtn.addEventListener('click', levelUp);
|
|
1849
1857
|
}
|
|
1850
1858
|
|
|
1859
|
+
if (archiveBtn) {
|
|
1860
|
+
archiveBtn.addEventListener('click', function () {
|
|
1861
|
+
const creativeId = form.dataset.creativeId;
|
|
1862
|
+
if (!creativeId) return;
|
|
1863
|
+
const row = document.querySelector(`creative-tree-row[creative-id="${creativeId}"]`);
|
|
1864
|
+
const isArchived = row?.hasAttribute('archived');
|
|
1865
|
+
const confirmMsg = isArchived ? archiveBtn.dataset.restoreConfirm : archiveBtn.dataset.confirm;
|
|
1866
|
+
|
|
1867
|
+
if (confirm(confirmMsg)) {
|
|
1868
|
+
const apiCall = isArchived ? creativesApi.unarchive(creativeId) : creativesApi.archive(creativeId);
|
|
1869
|
+
apiCall.then(res => {
|
|
1870
|
+
if (res.ok) {
|
|
1871
|
+
if (!isArchived) {
|
|
1872
|
+
// Archiving: remove from view
|
|
1873
|
+
const childrenContainer = document.getElementById(`creative-children-${creativeId}`);
|
|
1874
|
+
if (childrenContainer) childrenContainer.remove();
|
|
1875
|
+
if (row) row.remove();
|
|
1876
|
+
} else {
|
|
1877
|
+
// Restoring: reload tree to show updated state
|
|
1878
|
+
const treeEl = document.querySelector('[data-controller="creatives--tree"]');
|
|
1879
|
+
if (treeEl) {
|
|
1880
|
+
treeEl.innerHTML = '';
|
|
1881
|
+
delete treeEl.dataset.loaded;
|
|
1882
|
+
const ctrl = window.Stimulus?.getControllerForElementAndIdentifier(treeEl, 'creatives--tree');
|
|
1883
|
+
if (ctrl) ctrl.load();
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
closeEditor();
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1851
1893
|
if (deleteBtn) {
|
|
1852
1894
|
deleteBtn.addEventListener('click', function () {
|
|
1853
1895
|
if (confirm(deleteBtn.dataset.confirm)) deleteCurrent(false);
|
|
@@ -22,6 +22,10 @@ module Collavre
|
|
|
22
22
|
|
|
23
23
|
has_closure_tree order: :sequence, name_column: :description, hierarchy_table_name: "creative_hierarchies"
|
|
24
24
|
|
|
25
|
+
# --- Archive scopes ---
|
|
26
|
+
scope :active, -> { where(archived_at: nil) }
|
|
27
|
+
scope :archived, -> { where.not(archived_at: nil) }
|
|
28
|
+
|
|
25
29
|
attr_accessor :filtered_progress
|
|
26
30
|
|
|
27
31
|
belongs_to :user, class_name: Collavre.configuration.user_class_name, optional: true
|
|
@@ -143,6 +147,64 @@ module Collavre
|
|
|
143
147
|
end
|
|
144
148
|
end
|
|
145
149
|
|
|
150
|
+
# --- Archive ---
|
|
151
|
+
def archived?
|
|
152
|
+
archived_at.present?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def archive!
|
|
156
|
+
now = Time.current
|
|
157
|
+
self.class.transaction do
|
|
158
|
+
# Archive self and descendants
|
|
159
|
+
self_and_descendants.where(archived_at: nil).update_all(archived_at: now)
|
|
160
|
+
|
|
161
|
+
# If this is a linked creative, also archive the origin and its descendants
|
|
162
|
+
origin = effective_origin(Set.new)
|
|
163
|
+
if origin != self
|
|
164
|
+
origin.self_and_descendants.where(archived_at: nil).update_all(archived_at: now)
|
|
165
|
+
# Archive all other linked creatives of the origin
|
|
166
|
+
origin.linked_creatives.where.not(id: id).find_each do |linked|
|
|
167
|
+
linked.self_and_descendants.where(archived_at: nil).update_all(archived_at: now)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Also archive any linked creatives that point to this one
|
|
172
|
+
linked_creatives.find_each do |linked|
|
|
173
|
+
linked.self_and_descendants.where(archived_at: nil).update_all(archived_at: now)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
reload
|
|
177
|
+
parent&.reload
|
|
178
|
+
Collavre::Creatives::ProgressService.new(parent).update_progress_from_children! if parent
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def unarchive!
|
|
183
|
+
self.class.transaction do
|
|
184
|
+
# Unarchive self and descendants
|
|
185
|
+
self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)
|
|
186
|
+
|
|
187
|
+
# If this is a linked creative, also unarchive the origin and its descendants
|
|
188
|
+
origin = effective_origin(Set.new)
|
|
189
|
+
if origin != self
|
|
190
|
+
origin.self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)
|
|
191
|
+
# Unarchive all other linked creatives of the origin
|
|
192
|
+
origin.linked_creatives.where.not(id: id).find_each do |linked|
|
|
193
|
+
linked.self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Also unarchive any linked creatives that point to this one
|
|
198
|
+
linked_creatives.find_each do |linked|
|
|
199
|
+
linked.self_and_descendants.where.not(archived_at: nil).update_all(archived_at: nil)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
reload
|
|
203
|
+
parent&.reload
|
|
204
|
+
Collavre::Creatives::ProgressService.new(parent).update_progress_from_children! if parent
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
146
208
|
|
|
147
209
|
private
|
|
148
210
|
|
|
@@ -7,12 +7,28 @@ module Collavre
|
|
|
7
7
|
|
|
8
8
|
has_many :comments, class_name: "Collavre::Comment", dependent: :destroy
|
|
9
9
|
|
|
10
|
+
# --- Archive scopes ---
|
|
11
|
+
scope :active, -> { where(archived_at: nil) }
|
|
12
|
+
scope :archived, -> { where.not(archived_at: nil) }
|
|
13
|
+
|
|
10
14
|
validates :name, presence: true, uniqueness: { scope: :creative_id }
|
|
11
15
|
|
|
12
16
|
before_create :set_default_position
|
|
13
17
|
|
|
14
18
|
default_scope { order(:position) }
|
|
15
19
|
|
|
20
|
+
def archived?
|
|
21
|
+
archived_at.present?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def archive!
|
|
25
|
+
update!(archived_at: Time.current)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def unarchive!
|
|
29
|
+
update!(archived_at: nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
16
32
|
private
|
|
17
33
|
|
|
18
34
|
def set_default_position
|
|
@@ -112,8 +112,11 @@ module Creatives
|
|
|
112
112
|
creative = Creative.find_by(id: params[:id])
|
|
113
113
|
return empty_result unless creative && readable?(creative)
|
|
114
114
|
|
|
115
|
+
children = creative.children_with_permission(user, :read)
|
|
116
|
+
children = children.select { |c| !c.archived? } unless params[:show_archived]
|
|
117
|
+
|
|
115
118
|
{
|
|
116
|
-
creatives:
|
|
119
|
+
creatives: children,
|
|
117
120
|
parent: creative,
|
|
118
121
|
allowed_ids: nil,
|
|
119
122
|
overall_progress: nil,
|
|
@@ -122,8 +125,11 @@ module Creatives
|
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
def handle_root_query
|
|
128
|
+
roots = Creative.where(user: user).roots
|
|
129
|
+
roots = roots.where(archived_at: nil) unless params[:show_archived]
|
|
130
|
+
|
|
125
131
|
{
|
|
126
|
-
creatives:
|
|
132
|
+
creatives: roots,
|
|
127
133
|
parent: nil,
|
|
128
134
|
allowed_ids: nil,
|
|
129
135
|
overall_progress: nil,
|
|
@@ -132,7 +138,7 @@ module Creatives
|
|
|
132
138
|
end
|
|
133
139
|
|
|
134
140
|
def determine_scope
|
|
135
|
-
if params[:id]
|
|
141
|
+
base_scope = if params[:id]
|
|
136
142
|
base = Creative.find_by(id: params[:id])&.effective_origin
|
|
137
143
|
return Creative.none unless base
|
|
138
144
|
|
|
@@ -163,6 +169,10 @@ module Creatives
|
|
|
163
169
|
else
|
|
164
170
|
Creative.where(origin_id: nil) # Only real creatives (not shells)
|
|
165
171
|
end
|
|
172
|
+
|
|
173
|
+
# Exclude archived creatives from all filters/search unless explicitly requested
|
|
174
|
+
base_scope = base_scope.where(archived_at: nil) unless params[:show_archived]
|
|
175
|
+
base_scope
|
|
166
176
|
end
|
|
167
177
|
|
|
168
178
|
def determine_start_nodes(allowed_ids)
|
|
@@ -8,10 +8,11 @@ module Collavre
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def update_progress_from_children!
|
|
11
|
-
|
|
11
|
+
active_children = creative.children.active
|
|
12
|
+
if active_children.any?
|
|
12
13
|
# Use Ruby calculation to get effective progress (handling delegation for linked creatives)
|
|
13
14
|
# instead of SQL average which reads potentially stale DB columns.
|
|
14
|
-
new_progress =
|
|
15
|
+
new_progress = active_children.map(&:progress).sum.to_f / active_children.size
|
|
15
16
|
creative.update(progress: new_progress)
|
|
16
17
|
else
|
|
17
18
|
creative.update(progress: 0)
|
|
@@ -37,8 +38,9 @@ module Collavre
|
|
|
37
38
|
rescue ActiveRecord::RecordNotFound
|
|
38
39
|
return
|
|
39
40
|
end
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
active_children = parent.children.active
|
|
42
|
+
new_progress = if active_children.any?
|
|
43
|
+
active_children.map(&:progress).sum.to_f / active_children.size
|
|
42
44
|
else
|
|
43
45
|
0
|
|
44
46
|
end
|
|
@@ -51,9 +53,10 @@ module Collavre
|
|
|
51
53
|
|
|
52
54
|
def progress_for_tags(tag_ids, user)
|
|
53
55
|
return creative.progress if tag_ids.blank?
|
|
56
|
+
return nil if creative.archived?
|
|
54
57
|
|
|
55
58
|
tag_ids = Array(tag_ids).map(&:to_s)
|
|
56
|
-
visible_children = creative.children_with_permission(user)
|
|
59
|
+
visible_children = creative.children_with_permission(user).select { |c| !c.archived? }
|
|
57
60
|
child_values = visible_children.map do |child|
|
|
58
61
|
self.class.new(child).progress_for_tags(tag_ids, user)
|
|
59
62
|
end.compact
|
|
@@ -70,7 +73,9 @@ module Collavre
|
|
|
70
73
|
|
|
71
74
|
# `tagged_ids` should be a Set of creative IDs tagged with the plan.
|
|
72
75
|
def progress_for_plan(tagged_ids)
|
|
73
|
-
|
|
76
|
+
return nil if creative.archived?
|
|
77
|
+
|
|
78
|
+
child_values = creative.children.active.map do |child|
|
|
74
79
|
self.class.new(child).progress_for_plan(tagged_ids)
|
|
75
80
|
end.compact
|
|
76
81
|
|
|
@@ -121,6 +121,7 @@ module Creatives
|
|
|
121
121
|
has_children: filtered_children.any?,
|
|
122
122
|
expanded: expanded,
|
|
123
123
|
is_root: creative.parent.nil?,
|
|
124
|
+
archived: creative.archived?,
|
|
124
125
|
link_url: view_context.collavre.creative_path(creative),
|
|
125
126
|
templates: template_payload_for(creative),
|
|
126
127
|
inline_editor_payload: inline_editor_payload_for(creative),
|
|
@@ -148,6 +149,7 @@ module Creatives
|
|
|
148
149
|
return [] if raw_params["comment"] == "true" || raw_params["search"].present?
|
|
149
150
|
|
|
150
151
|
children = creative.children_with_permission(user)
|
|
152
|
+
children = children.select { |c| !c.archived? } unless raw_params["show_archived"]
|
|
151
153
|
if allowed_creative_ids
|
|
152
154
|
children.select { |c| allowed_creative_ids.include?(c.id.to_s) }
|
|
153
155
|
else
|
|
@@ -58,6 +58,16 @@
|
|
|
58
58
|
button_attributes: { id: 'inline-delete-popup-toggle' },
|
|
59
59
|
menu_id: 'inline-delete-popup-menu'
|
|
60
60
|
) do %>
|
|
61
|
+
<button type="button"
|
|
62
|
+
id="inline-archive"
|
|
63
|
+
class="popup-menu-item"
|
|
64
|
+
title="<%= t('collavre.creatives.index.archive') %>"
|
|
65
|
+
data-confirm="<%= t('collavre.creatives.index.archive_confirm') %>"
|
|
66
|
+
data-archive-label="<%= t('collavre.creatives.index.archive') %>"
|
|
67
|
+
data-restore-label="<%= t('collavre.creatives.index.unarchive') %>"
|
|
68
|
+
data-restore-confirm="<%= t('collavre.creatives.index.unarchive_confirm', default: 'Restore this creative?') %>">
|
|
69
|
+
<%= t('collavre.creatives.index.archive') %>
|
|
70
|
+
</button>
|
|
61
71
|
<button type="button"
|
|
62
72
|
id="inline-delete"
|
|
63
73
|
class="popup-menu-item danger-link"
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
<% if @parent_creative.present? %>
|
|
26
26
|
<%= button_to t('collavre.creatives.index.slide_view'), slide_view_creative_path(@parent_creative), method: :get, class: 'popup-menu-item' %>
|
|
27
27
|
<% end %>
|
|
28
|
+
<% if authenticated? %>
|
|
29
|
+
<button type="button" id="toggle-archived-btn-mobile" class="popup-menu-item" data-show-text="<%= t('collavre.creatives.index.show_archived') %>" data-hide-text="<%= t('collavre.creatives.index.hide_archived') %>">π¦ <%= t('collavre.creatives.index.show_archived') %></button>
|
|
30
|
+
<% end %>
|
|
28
31
|
<% if authenticated? && (!params[:id] || (current_creative && current_creative.has_permission?(Current.user, :write))) %>
|
|
29
32
|
<button type="button" class="popup-menu-item" data-controller="click-target" data-click-target-id-value="import-markdown-btn" data-action="click->click-target#trigger"><%= t('collavre.creatives.index.import_markdown') %></button>
|
|
30
33
|
<% end %>
|
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
<button id="expand-all-btn" class="expand-btn" style="width: 36px;" data-expand-text="<%= t('app.expand_all') %>" data-collapse-text="<%= t('app.collapse_all') %>" data-action="click->creatives--expansion#toggleAll" data-creatives--expansion-target="expand"><span aria-hidden="true">βΌ</span></button>
|
|
55
55
|
<button id="toggle-edit-btn" class="edit-toggle-btn mobile-only" aria-label="<%= t('.edit') %>" title="<%= t('.edit') %>">
|
|
56
56
|
<span aria-hidden="true"><%= svg_tag 'edit.svg', class: 'icon-edit', width: 16, height: 16 %></span></button>
|
|
57
|
+
<% if authenticated? %>
|
|
58
|
+
<button id="toggle-archived-btn" class="archive-toggle-btn desktop-only" title="<%= t('collavre.creatives.index.show_archived') %>" data-show-text="<%= t('collavre.creatives.index.show_archived') %>" data-hide-text="<%= t('collavre.creatives.index.hide_archived') %>"><span aria-hidden="true">π¦</span></button>
|
|
59
|
+
<% end %>
|
|
57
60
|
<% if authenticated? && (!params[:id] || (@parent_creative && @parent_creative.has_permission?(Current.user, :write))) %>
|
|
58
61
|
<button id="import-markdown-btn" class="import-btn desktop-only" data-action="click->creatives--import#toggle" data-creatives--import-target="toggle"><%= t('.import_markdown') %></button>
|
|
59
62
|
<% end %>
|
|
@@ -179,7 +182,7 @@
|
|
|
179
182
|
tree_params = params.to_unsafe_h.slice(
|
|
180
183
|
"id", "tags", "min_progress", "max_progress", "search", "comment",
|
|
181
184
|
"has_comments", "due_before", "due_after", "has_due_date",
|
|
182
|
-
"assignee_id", "unassigned"
|
|
185
|
+
"assignee_id", "unassigned", "show_archived"
|
|
183
186
|
).merge(format: :json)
|
|
184
187
|
tree_url = creatives_path(tree_params)
|
|
185
188
|
%>
|
|
@@ -5,6 +5,9 @@ en:
|
|
|
5
5
|
delete_confirm: This will delete all messages in this topic. Are you sure?
|
|
6
6
|
new_placeholder: New Topic
|
|
7
7
|
no_permission: You don't have permission to perform this action.
|
|
8
|
+
archive: Archive
|
|
9
|
+
unarchive: Restore
|
|
10
|
+
archived_topics: "Archived topics (%{count})"
|
|
8
11
|
move:
|
|
9
12
|
no_target_permission: You don't have write permission on the target creative.
|
|
10
13
|
duplicate_name: A topic named '%{name}' already exists in the target creative.
|
|
@@ -25,6 +28,7 @@ en:
|
|
|
25
28
|
delete_button: Delete
|
|
26
29
|
not_owner: Only the comment owner can modify this.
|
|
27
30
|
no_permission: You do not have permission to comment.
|
|
31
|
+
archived_creative: This creative is archived. Comments are disabled.
|
|
28
32
|
convert_not_allowed: Only the comment owner or a creative admin can convert
|
|
29
33
|
this message.
|
|
30
34
|
convert_button: Convert
|
|
@@ -5,6 +5,9 @@ ko:
|
|
|
5
5
|
delete_confirm: μ΄ ν ν½μ λͺ¨λ λ©μμ§κ° μμ λ©λλ€. νμ€ν©λκΉ?
|
|
6
6
|
new_placeholder: μ ν ν½
|
|
7
7
|
no_permission: μ΄ μμ
μ μνν κΆνμ΄ μμ΅λλ€.
|
|
8
|
+
archive: μμΉ΄μ΄λΈ
|
|
9
|
+
unarchive: 볡μ
|
|
10
|
+
archived_topics: "μμΉ΄μ΄λΈλ ν ν½ (%{count}κ°)"
|
|
8
11
|
move:
|
|
9
12
|
no_target_permission: λμ ν¬λ¦¬μμ΄ν°λΈμ λν μ°κΈ° κΆνμ΄ μμ΅λλ€.
|
|
10
13
|
duplicate_name: "'%{name}' ν ν½μ΄ λμ ν¬λ¦¬μμ΄ν°λΈμ μ΄λ―Έ μ‘΄μ¬ν©λλ€."
|
|
@@ -25,6 +28,7 @@ ko:
|
|
|
25
28
|
delete_button: μμ
|
|
26
29
|
not_owner: λκΈ μμ μλ§ μμ ν μ μμ΅λλ€.
|
|
27
30
|
no_permission: λκΈμ μΆκ°ν κΆνμ΄ μμ΅λλ€.
|
|
31
|
+
archived_creative: μμΉ΄μ΄λΈλ ν¬λ¦¬μμ΄ν°λΈμ
λλ€. μ±ν
μ΄ λΉνμ±νλμμ΅λλ€.
|
|
28
32
|
convert_not_allowed: λκΈ μμ μ λλ ν¬λ¦¬μμ΄ν°λΈ κ΄λ¦¬μλ§ μ΄ λ©μμ§λ₯Ό μ νν μ μμ΅λλ€.
|
|
29
33
|
convert_button: μ ν
|
|
30
34
|
convert_to_creative: νμ ν¬λ¦¬μμ΄ν°λΈλ‘ μ ν
|
|
@@ -11,6 +11,12 @@ en:
|
|
|
11
11
|
append_as_parent_creative: New upper-creative
|
|
12
12
|
append_below_creative: Add below
|
|
13
13
|
delete: Delete
|
|
14
|
+
archive: Archive
|
|
15
|
+
archive_confirm: Archive this item and all children?
|
|
16
|
+
unarchive_confirm: Restore this creative from archive?
|
|
17
|
+
unarchive: Restore
|
|
18
|
+
show_archived: Show archived
|
|
19
|
+
hide_archived: Hide archived
|
|
14
20
|
delete_only_this: Delete only this Creative
|
|
15
21
|
delete_with_children: Delete this Creative and all its children
|
|
16
22
|
are_you_sure: Are you sure?
|
|
@@ -11,6 +11,12 @@ ko:
|
|
|
11
11
|
append_as_parent_creative: μμμ μΆκ°
|
|
12
12
|
append_below_creative: μλμ μΆκ°
|
|
13
13
|
delete: μμ
|
|
14
|
+
archive: μμΉ΄μ΄λΈ
|
|
15
|
+
archive_confirm: μ΄ νλͺ©κ³Ό λͺ¨λ νμ νλͺ©μ μμΉ΄μ΄λΈν κΉμ?
|
|
16
|
+
unarchive_confirm: μ΄ ν¬λ¦¬μμ΄ν°λΈλ₯Ό μμΉ΄μ΄λΈμμ 볡μν κΉμ?
|
|
17
|
+
unarchive: 볡μ
|
|
18
|
+
show_archived: μμΉ΄μ΄λΈ νμ
|
|
19
|
+
hide_archived: μμΉ΄μ΄λΈ μ¨κΈ°κΈ°
|
|
14
20
|
delete_only_this: μ΄ νλͺ©λ§ μμ
|
|
15
21
|
delete_with_children: μ΄ νλͺ©κ³Ό λͺ¨λ νμ νλͺ© μμ
|
|
16
22
|
are_you_sure: μ λ§ μμ νμκ² μ΅λκΉ?
|
data/config/routes.rb
CHANGED
|
@@ -57,6 +57,8 @@ Collavre::Engine.routes.draw do
|
|
|
57
57
|
end
|
|
58
58
|
member do
|
|
59
59
|
patch :move
|
|
60
|
+
patch :archive
|
|
61
|
+
patch :unarchive
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
resources :comments, only: [ :index, :create, :destroy, :show, :update ] do
|
|
@@ -95,6 +97,8 @@ Collavre::Engine.routes.draw do
|
|
|
95
97
|
post :share, to: "creatives#share", as: :share_creative
|
|
96
98
|
post :request_permission, to: "creatives#request_permission"
|
|
97
99
|
post :unconvert
|
|
100
|
+
patch :archive
|
|
101
|
+
patch :unarchive
|
|
98
102
|
get :parent_suggestions
|
|
99
103
|
get :slide_view
|
|
100
104
|
get :contexts
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class AddArchivedAtToCreativesAndTopics < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :creatives, :archived_at, :datetime
|
|
4
|
+
add_column :topics, :archived_at, :datetime
|
|
5
|
+
|
|
6
|
+
add_index :creatives, :archived_at, where: "archived_at IS NOT NULL", name: "index_creatives_on_archived_at"
|
|
7
|
+
add_index :topics, :archived_at, where: "archived_at IS NOT NULL", name: "index_topics_on_archived_at"
|
|
8
|
+
end
|
|
9
|
+
end
|
data/lib/collavre/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: collavre
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Collavre
|
|
@@ -646,6 +646,7 @@ files:
|
|
|
646
646
|
- db/migrate/20260223173533_add_review_type_to_comments.rb
|
|
647
647
|
- db/migrate/20260225065200_create_comment_versions.rb
|
|
648
648
|
- db/migrate/20260225074416_add_selected_version_id_to_comments.rb
|
|
649
|
+
- db/migrate/20260313011922_add_archived_at_to_creatives_and_topics.rb
|
|
649
650
|
- lib/collavre.rb
|
|
650
651
|
- lib/collavre/configuration.rb
|
|
651
652
|
- lib/collavre/engine.rb
|