collavre_slack 0.2.1 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa613acb914f114910acfe095639ddc59da1e61f354027db1cd787f21771936c
4
- data.tar.gz: 3920b7a19ff81f3e631056e68e94e6d635a3bf01bf27121d24b96867fa74b290
3
+ metadata.gz: 13a4005b2d4412089caa83e43e2be4868ce50aa024a77904e7644320d30a5cbb
4
+ data.tar.gz: c5a8501c5e9149b4648a10dc5eac2ede04451e0474cc0d59119f123b2d6763dd
5
5
  SHA512:
6
- metadata.gz: b7a05dd936fe10a0ad171794034c69718f1916a6eaa5f6a2f9b35e70c2b973fb6d2aad40d2ef536d6c22d4fedf5760d8ab2ea4c42554ef63189eda635ff1fe72
7
- data.tar.gz: 80ecdcf143752eced3b5fdce64d75fb788b4b541ad0ef68eb07d6a479a249ce33834147f3938ed524314c479799a1d2fe19d7227ba7a91e40e35618600284070
6
+ metadata.gz: 82d93fd5f769a694bb3afc27246b75f161b8f5d50afd9a09e1f1939eb26f5e774c193e9b6a2ce4d04c0ac5faf088c13abb6d693afdd0080463bd4b5ccb1fa463
7
+ data.tar.gz: 9de647749fb26de533513da779f9726185781071129c28080568402b8f30fb3037694175c7bf91f007f25acc005f07f44fc1186f49d01b15f719409633fe1a2a
@@ -1,3 +1,14 @@
1
+ /* Slack channel search */
2
+ .slack-channel-search {
3
+ width: 100%;
4
+ padding: 0.5em 0.75em;
5
+ margin-bottom: 0.5em;
6
+ border: 1px solid var(--color-border);
7
+ border-radius: 4px;
8
+ font-size: 0.95em;
9
+ box-sizing: border-box;
10
+ }
11
+
1
12
  /* Slack channel selection */
2
13
  .slack-channel-item {
3
14
  padding: 0.5em 1em;
@@ -13,3 +24,36 @@
13
24
  background: var(--color-drag-over);
14
25
  border-left: 3px solid var(--color-secondary-active);
15
26
  }
27
+
28
+ /* Slack badge in comments popup header */
29
+ .slack-channel-badge {
30
+ display: none;
31
+ font-size: 0.7em;
32
+ font-weight: 600;
33
+ background: #4A154B;
34
+ color: white;
35
+ padding: 0.15em 0.45em;
36
+ border-radius: 4px;
37
+ margin-left: 0.5em;
38
+ cursor: pointer;
39
+ white-space: nowrap;
40
+ vertical-align: middle;
41
+ line-height: 1.4;
42
+ }
43
+
44
+ .slack-channel-badge:hover {
45
+ background: #611f64;
46
+ }
47
+
48
+ .slack-channel-tooltip {
49
+ display: none;
50
+ font-size: 0.75em;
51
+ color: var(--color-text-secondary);
52
+ margin-left: 0.4em;
53
+ white-space: nowrap;
54
+ vertical-align: middle;
55
+ }
56
+
57
+ .slack-channel-tooltip.visible {
58
+ display: inline;
59
+ }
@@ -60,6 +60,17 @@ module CollavreSlack
60
60
  end
61
61
  end
62
62
 
63
+ # Lightweight endpoint for badge display — returns only existing links, no Slack API calls
64
+ def badge
65
+ target_creative = @creative.effective_origin
66
+ links = SlackChannelLink.where(creative: target_creative)
67
+ render json: {
68
+ links: links.map { |link|
69
+ { channel_name: link.channel_name }
70
+ }
71
+ }
72
+ end
73
+
63
74
  def destroy
64
75
  link = SlackChannelLink.find(params[:id])
65
76
  # Check against origin creative
@@ -86,10 +97,7 @@ module CollavreSlack
86
97
 
87
98
  def fetch_channels(slack_account)
88
99
  client = SlackClient.new(access_token: slack_account.access_token)
89
- response = client.list_channels
90
- return [] unless response[:ok]
91
-
92
- (response[:channels] || []).map do |channel|
100
+ client.list_all_channels.map do |channel|
93
101
  { id: channel[:id], name: channel[:name] }
94
102
  end
95
103
  rescue StandardError => e
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import { filterChannels, reconcileSelection, buildChannelViewModels } from '../slack_channel_list.js';
5
+
6
+ const channels = [
7
+ { id: 'C01', name: 'general' },
8
+ { id: 'C02', name: 'random' },
9
+ { id: 'C03', name: 'engineering' },
10
+ { id: 'C04', name: 'design' },
11
+ { id: 'C05', name: 'general-kr' },
12
+ ];
13
+
14
+ describe('filterChannels', () => {
15
+ test('returns all channels when query is empty', () => {
16
+ expect(filterChannels(channels, '')).toEqual(channels);
17
+ expect(filterChannels(channels, null)).toEqual(channels);
18
+ expect(filterChannels(channels, undefined)).toEqual(channels);
19
+ });
20
+
21
+ test('filters by substring match (case-insensitive)', () => {
22
+ const result = filterChannels(channels, 'gen');
23
+ expect(result).toEqual([
24
+ { id: 'C01', name: 'general' },
25
+ { id: 'C05', name: 'general-kr' },
26
+ ]);
27
+ });
28
+
29
+ test('is case-insensitive', () => {
30
+ expect(filterChannels(channels, 'DESIGN')).toEqual([
31
+ { id: 'C04', name: 'design' },
32
+ ]);
33
+ });
34
+
35
+ test('returns empty array when nothing matches', () => {
36
+ expect(filterChannels(channels, 'zzz')).toEqual([]);
37
+ });
38
+
39
+ test('returns empty array when channels list is empty', () => {
40
+ expect(filterChannels([], 'gen')).toEqual([]);
41
+ });
42
+ });
43
+
44
+ describe('reconcileSelection', () => {
45
+ test('returns null when no channel is selected', () => {
46
+ expect(reconcileSelection(null, channels)).toBeNull();
47
+ });
48
+
49
+ test('keeps selection when channel is in filtered list', () => {
50
+ const selected = { id: 'C01', name: 'general' };
51
+ expect(reconcileSelection(selected, channels)).toBe(selected);
52
+ });
53
+
54
+ test('clears selection when channel is filtered out', () => {
55
+ const selected = { id: 'C01', name: 'general' };
56
+ const filtered = [{ id: 'C04', name: 'design' }];
57
+ expect(reconcileSelection(selected, filtered)).toBeNull();
58
+ });
59
+
60
+ test('clears selection when filtered list is empty', () => {
61
+ const selected = { id: 'C01', name: 'general' };
62
+ expect(reconcileSelection(selected, [])).toBeNull();
63
+ });
64
+ });
65
+
66
+ describe('buildChannelViewModels', () => {
67
+ test('marks linked channels', () => {
68
+ const links = [{ channel_id: 'C02' }];
69
+ const result = buildChannelViewModels(channels, links, null);
70
+
71
+ const linkedItem = result.find(vm => vm.channel.id === 'C02');
72
+ expect(linkedItem.isLinked).toBe(true);
73
+
74
+ const unlinkedItem = result.find(vm => vm.channel.id === 'C01');
75
+ expect(unlinkedItem.isLinked).toBe(false);
76
+ });
77
+
78
+ test('marks selected channel', () => {
79
+ const selected = { id: 'C03', name: 'engineering' };
80
+ const result = buildChannelViewModels(channels, [], selected);
81
+
82
+ const selectedItem = result.find(vm => vm.channel.id === 'C03');
83
+ expect(selectedItem.isSelected).toBe(true);
84
+
85
+ const otherItem = result.find(vm => vm.channel.id === 'C01');
86
+ expect(otherItem.isSelected).toBe(false);
87
+ });
88
+
89
+ test('no channel is selected when selection is null', () => {
90
+ const result = buildChannelViewModels(channels, [], null);
91
+ expect(result.every(vm => !vm.isSelected)).toBe(true);
92
+ });
93
+
94
+ test('handles empty inputs', () => {
95
+ expect(buildChannelViewModels([], [], null)).toEqual([]);
96
+ });
97
+
98
+ test('channel can be both linked and selected', () => {
99
+ const selected = { id: 'C02', name: 'random' };
100
+ const links = [{ channel_id: 'C02' }];
101
+ const result = buildChannelViewModels(channels, links, selected);
102
+
103
+ const item = result.find(vm => vm.channel.id === 'C02');
104
+ expect(item.isLinked).toBe(true);
105
+ expect(item.isSelected).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe('search → selection regression', () => {
110
+ test('selecting a channel then filtering it out clears selection', () => {
111
+ // User selects #general
112
+ const selected = { id: 'C01', name: 'general' };
113
+
114
+ // Then types "design" in search
115
+ const filtered = filterChannels(channels, 'design');
116
+ expect(filtered).toEqual([{ id: 'C04', name: 'design' }]);
117
+
118
+ // Selection should be cleared because #general is not in results
119
+ const reconciled = reconcileSelection(selected, filtered);
120
+ expect(reconciled).toBeNull();
121
+ });
122
+
123
+ test('selecting a channel then filtering to include it keeps selection', () => {
124
+ const selected = { id: 'C01', name: 'general' };
125
+ const filtered = filterChannels(channels, 'gen');
126
+
127
+ // #general is still in the results
128
+ const reconciled = reconcileSelection(selected, filtered);
129
+ expect(reconciled).toBe(selected);
130
+ });
131
+
132
+ test('clearing the search restores selection if channel exists', () => {
133
+ const selected = { id: 'C01', name: 'general' };
134
+
135
+ // Filter out, selection cleared
136
+ const filtered1 = filterChannels(channels, 'design');
137
+ const reconciled1 = reconcileSelection(selected, filtered1);
138
+ expect(reconciled1).toBeNull();
139
+
140
+ // Clear search — but reconcileSelection won't magically restore,
141
+ // it returns null because reconciled1 was already null
142
+ const filtered2 = filterChannels(channels, '');
143
+ const reconciled2 = reconcileSelection(reconciled1, filtered2);
144
+ expect(reconciled2).toBeNull();
145
+ });
146
+
147
+ test('full flow: filter → select → widen filter keeps new selection', () => {
148
+ // User types "des", sees only #design
149
+ const filtered1 = filterChannels(channels, 'des');
150
+ expect(filtered1).toEqual([{ id: 'C04', name: 'design' }]);
151
+
152
+ // User selects #design
153
+ const selected = { id: 'C04', name: 'design' };
154
+
155
+ // User clears search — #design is still in full list
156
+ const filtered2 = filterChannels(channels, '');
157
+ const reconciled = reconcileSelection(selected, filtered2);
158
+ expect(reconciled).toBe(selected);
159
+
160
+ // View models reflect selection correctly
161
+ const vms = buildChannelViewModels(filtered2, [], reconciled);
162
+ const designVm = vms.find(vm => vm.channel.id === 'C04');
163
+ expect(designVm.isSelected).toBe(true);
164
+ expect(vms.filter(vm => vm.isSelected)).toHaveLength(1);
165
+ });
166
+ });
@@ -1,3 +1,5 @@
1
+ import { filterChannels, reconcileSelection, buildChannelViewModels } from './slack_channel_list.js';
2
+
1
3
  let slackIntegrationInitialized = false;
2
4
 
3
5
  if (!slackIntegrationInitialized) {
@@ -23,6 +25,7 @@ if (!slackIntegrationInitialized) {
23
25
  const addChannelBtn = document.getElementById('slack-add-channel-btn');
24
26
  const connectMessage = document.getElementById('slack-connect-message');
25
27
  const channelListEl = document.getElementById('slack-channel-list');
28
+ const channelSearchEl = document.getElementById('slack-channel-search');
26
29
  const channelSummaryEl = document.getElementById('slack-channel-summary');
27
30
 
28
31
  let creativeId = null;
@@ -250,31 +253,46 @@ if (!slackIntegrationInitialized) {
250
253
  });
251
254
  }
252
255
 
253
- function renderChannelList() {
256
+ function renderChannelList(filter) {
254
257
  if (!channelListEl) return;
255
258
 
256
259
  channelListEl.innerHTML = '';
257
260
 
258
261
  if (availableChannels.length === 0) {
259
- channelListEl.innerHTML = '<p style="padding:0.5em;color:var(--color-text-secondary);">No channels available</p>';
262
+ channelListEl.innerHTML = `<p style="padding:0.5em;color:var(--color-text-secondary);">${modal.dataset.noChannelsAvailable || 'No channels available'}</p>`;
263
+ return;
264
+ }
265
+
266
+ const filtered = filterChannels(availableChannels, filter);
267
+
268
+ // Clear selection if the selected channel is not in the filtered results
269
+ selectedChannel = reconcileSelection(selectedChannel, filtered);
270
+ nextBtn.disabled = !selectedChannel;
271
+
272
+ if (filtered.length === 0) {
273
+ channelListEl.innerHTML = `<p style="padding:0.5em;color:var(--color-text-secondary);">${modal.dataset.noMatchingChannels || 'No matching channels'}</p>`;
260
274
  return;
261
275
  }
262
276
 
263
- availableChannels.forEach(function (channel) {
277
+ const viewModels = buildChannelViewModels(filtered, existingLinks, selectedChannel);
278
+
279
+ viewModels.forEach(function ({ channel, isLinked, isSelected }) {
264
280
  const div = document.createElement('div');
265
281
  div.className = 'slack-channel-item';
266
282
 
267
- const isLinked = existingLinks.some(link => link.channel_id === channel.id);
268
-
269
283
  const linkedLabel = modal.dataset.linkedLabel || '(linked)';
270
284
  div.innerHTML = `
271
285
  <strong>#${channel.name}</strong>
272
286
  ${isLinked ? `<span style="color:green;margin-left:0.5em;">${linkedLabel}</span>` : ''}
273
287
  `;
274
288
 
289
+ if (isSelected) {
290
+ div.classList.add('active');
291
+ }
292
+
275
293
  if (!isLinked) {
276
294
  div.addEventListener('click', function () {
277
- document.querySelectorAll('.slack-channel-item').forEach(el => {
295
+ channelListEl.querySelectorAll('.slack-channel-item').forEach(el => {
278
296
  el.classList.remove('active');
279
297
  });
280
298
  div.classList.add('active');
@@ -396,6 +414,7 @@ if (!slackIntegrationInitialized) {
396
414
  clearError();
397
415
  if (currentStep === 'connect') {
398
416
  currentStep = 'channels';
417
+ if (channelSearchEl) channelSearchEl.value = '';
399
418
  renderChannelList();
400
419
  } else if (currentStep === 'channels') {
401
420
  if (!selectedChannel) {
@@ -410,10 +429,17 @@ if (!slackIntegrationInitialized) {
410
429
 
411
430
  finishBtn.addEventListener('click', performLink);
412
431
 
432
+ if (channelSearchEl) {
433
+ channelSearchEl.addEventListener('input', function () {
434
+ renderChannelList(this.value);
435
+ });
436
+ }
437
+
413
438
  if (addChannelBtn) {
414
439
  addChannelBtn.addEventListener('click', function () {
415
440
  currentStep = 'channels';
416
441
  selectedChannel = null;
442
+ if (channelSearchEl) channelSearchEl.value = '';
417
443
  renderChannelList();
418
444
  updateStep();
419
445
  });
@@ -437,7 +463,7 @@ if (!slackIntegrationInitialized) {
437
463
  if (data.connected) {
438
464
  availableChannels = data.channels || [];
439
465
  existingLinks = data.links || [];
440
- renderChannelList();
466
+ renderChannelList(channelSearchEl ? channelSearchEl.value : '');
441
467
  }
442
468
  })
443
469
  .catch(error => {
@@ -461,35 +487,79 @@ if (!slackIntegrationInitialized) {
461
487
  });
462
488
 
463
489
  // Slack badge for comments popup
490
+ // Store references for cleanup to avoid handler accumulation across popup opens
491
+ let slackBadgeClickHandler = null;
492
+ let slackDocumentClickHandler = null;
493
+ let slackBadgeRequestId = 0; // Guard against stale async responses
494
+
464
495
  document.addEventListener('comments-popup:opened', async function (event) {
465
496
  const { creativeId, badgeContainer } = event.detail;
466
497
  if (!badgeContainer || !creativeId) return;
467
498
 
499
+ // Increment request id to invalidate any in-flight fetch
500
+ const currentRequestId = ++slackBadgeRequestId;
501
+
468
502
  // Create or find slack badge element
469
503
  let badge = badgeContainer.querySelector('[data-slack-badge]');
470
504
  if (!badge) {
471
505
  badge = document.createElement('span');
472
506
  badge.setAttribute('data-slack-badge', '');
473
507
  badge.className = 'slack-channel-badge';
474
- badge.style.cssText = 'display:none;font-size:0.75em;background:#4A154B;color:white;padding:0.15em 0.5em;border-radius:4px;margin-left:0.5em;';
475
508
  badgeContainer.appendChild(badge);
476
509
  }
477
510
 
511
+ // Clean up previous handlers before re-registering
512
+ if (slackBadgeClickHandler) {
513
+ badge.removeEventListener('click', slackBadgeClickHandler);
514
+ slackBadgeClickHandler = null;
515
+ }
516
+ if (slackDocumentClickHandler) {
517
+ document.removeEventListener('click', slackDocumentClickHandler);
518
+ slackDocumentClickHandler = null;
519
+ }
520
+
521
+ // Remove any existing tooltip
522
+ const existingTooltip = badgeContainer.querySelector('[data-slack-tooltip]');
523
+ if (existingTooltip) existingTooltip.remove();
524
+
478
525
  // Hide badge initially
479
526
  badge.style.display = 'none';
480
527
  badge.textContent = '';
481
528
 
482
529
  try {
483
- const response = await fetch(`/slack/creatives/${creativeId}/slack_integrations`, {
530
+ const response = await fetch(`/slack/creatives/${creativeId}/slack_integrations/badge`, {
484
531
  headers: { Accept: 'application/json' }
485
532
  });
533
+
534
+ // Discard response if a newer popup open has occurred
535
+ if (currentRequestId !== slackBadgeRequestId) return;
536
+
486
537
  if (!response.ok) return;
487
538
 
488
539
  const data = await response.json();
489
540
  if (data.links && data.links.length > 0) {
490
- const channelNames = data.links.map(link => `#${link.channel_name}`).join(', ');
491
- badge.textContent = `Slack: ${channelNames}`;
541
+ badge.textContent = 'Slack';
492
542
  badge.style.display = 'inline-block';
543
+ badge.title = data.links.map(link => `#${link.channel_name}`).join(', ');
544
+
545
+ // Create tooltip element for click display
546
+ const tooltip = document.createElement('span');
547
+ tooltip.setAttribute('data-slack-tooltip', '');
548
+ tooltip.className = 'slack-channel-tooltip';
549
+ tooltip.textContent = data.links.map(link => `#${link.channel_name}`).join(', ');
550
+ badgeContainer.appendChild(tooltip);
551
+
552
+ slackBadgeClickHandler = function (e) {
553
+ e.stopPropagation();
554
+ tooltip.classList.toggle('visible');
555
+ };
556
+ badge.addEventListener('click', slackBadgeClickHandler);
557
+
558
+ // Close tooltip when clicking elsewhere
559
+ slackDocumentClickHandler = function () {
560
+ tooltip.classList.remove('visible');
561
+ };
562
+ document.addEventListener('click', slackDocumentClickHandler);
493
563
  }
494
564
  } catch (error) {
495
565
  console.warn('Failed to load Slack badge:', error);
@@ -500,10 +570,23 @@ if (!slackIntegrationInitialized) {
500
570
  const { badgeContainer } = event.detail;
501
571
  if (!badgeContainer) return;
502
572
 
573
+ // Invalidate any in-flight badge fetch so late responses are discarded
574
+ slackBadgeRequestId++;
575
+
503
576
  const badge = badgeContainer.querySelector('[data-slack-badge]');
504
577
  if (badge) {
505
578
  badge.style.display = 'none';
506
579
  badge.textContent = '';
580
+ if (slackBadgeClickHandler) {
581
+ badge.removeEventListener('click', slackBadgeClickHandler);
582
+ slackBadgeClickHandler = null;
583
+ }
584
+ }
585
+ if (slackDocumentClickHandler) {
586
+ document.removeEventListener('click', slackDocumentClickHandler);
587
+ slackDocumentClickHandler = null;
507
588
  }
589
+ const tooltip = badgeContainer.querySelector('[data-slack-tooltip]');
590
+ if (tooltip) tooltip.remove();
508
591
  });
509
592
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Pure-logic helpers for the Slack channel list.
3
+ * Extracted so they can be unit-tested without a full DOM wizard.
4
+ */
5
+
6
+ /**
7
+ * Filter channels by a search query (case-insensitive substring match on name).
8
+ * @param {Array<{id:string, name:string}>} channels
9
+ * @param {string} query
10
+ * @returns {Array<{id:string, name:string}>}
11
+ */
12
+ export function filterChannels(channels, query) {
13
+ const q = (query || '').toLowerCase();
14
+ if (!q) return channels;
15
+ return channels.filter(ch => ch.name.toLowerCase().includes(q));
16
+ }
17
+
18
+ /**
19
+ * Decide whether selectedChannel should be cleared after a filter change.
20
+ * Returns the selectedChannel as-is if it still exists in `filtered`,
21
+ * or null if it has been filtered out.
22
+ *
23
+ * @param {{id:string, name:string}|null} selectedChannel
24
+ * @param {Array<{id:string, name:string}>} filtered
25
+ * @returns {{id:string, name:string}|null}
26
+ */
27
+ export function reconcileSelection(selectedChannel, filtered) {
28
+ if (!selectedChannel) return null;
29
+ return filtered.some(ch => ch.id === selectedChannel.id) ? selectedChannel : null;
30
+ }
31
+
32
+ /**
33
+ * Build the view-model array consumed by the DOM renderer.
34
+ *
35
+ * @param {Array<{id:string, name:string}>} channels – filtered channel list
36
+ * @param {Array<{channel_id:string}>} links – already-linked channels
37
+ * @param {{id:string}|null} selected – currently selected channel
38
+ * @returns {Array<{channel, isLinked:boolean, isSelected:boolean}>}
39
+ */
40
+ export function buildChannelViewModels(channels, links, selected) {
41
+ const linkedIds = new Set(links.map(l => l.channel_id));
42
+ return channels.map(channel => ({
43
+ channel,
44
+ isLinked: linkedIds.has(channel.id),
45
+ isSelected: selected !== null && selected.id === channel.id,
46
+ }));
47
+ }
@@ -28,8 +28,26 @@ module CollavreSlack
28
28
  parse_response(response)
29
29
  end
30
30
 
31
- def list_channels
32
- get("conversations.list", limit: 1000, types: "public_channel,private_channel")
31
+ def list_channels(cursor: nil)
32
+ params = { limit: 200, types: "public_channel,private_channel" }
33
+ params[:cursor] = cursor if cursor.present?
34
+ get("conversations.list", params)
35
+ end
36
+
37
+ def list_all_channels
38
+ channels = []
39
+ cursor = nil
40
+
41
+ loop do
42
+ response = list_channels(cursor: cursor)
43
+ break unless response[:ok]
44
+
45
+ channels.concat(Array(response[:channels]))
46
+ cursor = response.dig(:response_metadata, :next_cursor)
47
+ break if cursor.blank?
48
+ end
49
+
50
+ channels
33
51
  end
34
52
 
35
53
  def list_messages(channel:, oldest: nil, cursor: nil)
@@ -14,6 +14,8 @@
14
14
  data-refresh="<%= t('collavre_slack.modal.refresh') %>"
15
15
  data-refresh-error="<%= t('collavre_slack.modal.refresh_error') %>"
16
16
  data-linked-label="<%= t('collavre_slack.modal.linked_label') %>"
17
+ data-no-channels-available="<%= t('collavre_slack.modal.no_channels_available') %>"
18
+ data-no-matching-channels="<%= t('collavre_slack.modal.no_matching_channels') %>"
17
19
  style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1000;align-items:center;justify-content:center;">
18
20
  <div class="popup-box" style="min-width:360px;max-width:90vw;">
19
21
  <button type="button" id="close-slack-modal" class="popup-close-btn">&times;</button>
@@ -41,6 +43,7 @@
41
43
 
42
44
  <div class="slack-wizard-step" id="slack-step-channels" style="display:none;">
43
45
  <p class="slack-modal-subtext"><%= t('collavre_slack.modal.choose_channel') %></p>
46
+ <input type="text" id="slack-channel-search" class="slack-channel-search" placeholder="<%= t('collavre_slack.modal.search_channels') %>" autocomplete="off">
44
47
  <div id="slack-channel-list" class="slack-list slack-modal-list-box" style="max-height:240px;overflow:auto;"></div>
45
48
  </div>
46
49
 
@@ -55,3 +55,6 @@ en:
55
55
  refresh: "Refresh"
56
56
  refresh_error: "Failed to refresh channels"
57
57
  linked_label: "(Linked)"
58
+ search_channels: "Search channels..."
59
+ no_channels_available: "No channels available"
60
+ no_matching_channels: "No matching channels"
@@ -55,3 +55,6 @@ ko:
55
55
  refresh: "새로고침"
56
56
  refresh_error: "채널 목록을 새로고침하는데 실패했습니다"
57
57
  linked_label: "(연결됨)"
58
+ search_channels: "채널 검색..."
59
+ no_channels_available: "사용 가능한 채널이 없습니다"
60
+ no_matching_channels: "일치하는 채널이 없습니다"
data/config/routes.rb CHANGED
@@ -3,7 +3,11 @@ CollavreSlack::Engine.routes.draw do
3
3
  get "/auth/slack/callback", to: "slack_auth#callback"
4
4
 
5
5
  resources :creatives, only: [] do
6
- resources :slack_integrations, module: :creatives, only: [ :index, :create, :destroy ]
6
+ resources :slack_integrations, module: :creatives, only: [ :index, :create, :destroy ] do
7
+ collection do
8
+ get :badge
9
+ end
10
+ end
7
11
  resources :slack_messages, only: [ :create ]
8
12
  end
9
13
 
@@ -1,3 +1,3 @@
1
1
  module CollavreSlack
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collavre_slack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Collavre
@@ -81,7 +81,9 @@ files:
81
81
  - app/controllers/collavre_slack/slack_auth_controller.rb
82
82
  - app/controllers/collavre_slack/slack_events_controller.rb
83
83
  - app/controllers/collavre_slack/slack_messages_controller.rb
84
+ - app/javascript/__tests__/slack_channel_list.test.js
84
85
  - app/javascript/collavre_slack.js
86
+ - app/javascript/slack_channel_list.js
85
87
  - app/jobs/collavre_slack/application_job.rb
86
88
  - app/jobs/collavre_slack/slack_channel_sync_job.rb
87
89
  - app/jobs/collavre_slack/slack_inbound_message_delete_job.rb