collavre_slack 0.2.0 → 0.2.2
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_slack/slack_integration.css +11 -0
- data/app/controllers/collavre_slack/creatives/slack_integrations_controller.rb +1 -4
- data/app/javascript/__tests__/slack_channel_list.test.js +166 -0
- data/app/javascript/collavre_slack.js +33 -7
- data/app/javascript/slack_channel_list.js +47 -0
- data/app/jobs/collavre_slack/slack_inbound_message_job.rb +2 -1
- data/app/services/collavre_slack/slack_client.rb +20 -2
- data/app/views/collavre_slack/integrations/_modal.html.erb +3 -0
- data/config/locales/en.yml +3 -0
- data/config/locales/ko.yml +3 -0
- data/lib/collavre_slack/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50c834145802449d01365be47375e1d4bbb4a05a2622abee20449d75b24c8c2c
|
|
4
|
+
data.tar.gz: d7b9d88bad8aac159618cd4f336937aea08e43362c2c2891304a0a895bfd2ece
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1c34b49e220e6b2cd44e1978f5b3b9fd088d2f18d85f6e8628817486ae7ecf110e2f09a81cf25fb6ea9f6aa4e3a76fcbba61970067a3a5fb866495a82d9050b
|
|
7
|
+
data.tar.gz: 60b924d455319f87ea3de4ab8a340fb7b93b20df413cb1e96d5e3c78bacb09d94888eda06f0ed08028072e3cae19265b3c88aa5403856147dd0bb7c2766b67cc
|
|
@@ -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;
|
|
@@ -86,10 +86,7 @@ module CollavreSlack
|
|
|
86
86
|
|
|
87
87
|
def fetch_channels(slack_account)
|
|
88
88
|
client = SlackClient.new(access_token: slack_account.access_token)
|
|
89
|
-
|
|
90
|
-
return [] unless response[:ok]
|
|
91
|
-
|
|
92
|
-
(response[:channels] || []).map do |channel|
|
|
89
|
+
client.list_all_channels.map do |channel|
|
|
93
90
|
{ id: channel[:id], name: channel[:name] }
|
|
94
91
|
end
|
|
95
92
|
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 =
|
|
262
|
+
channelListEl.innerHTML = `<p style="padding:0.5em;color:var(--color-text-secondary);">${modal.dataset.noChannelsAvailable || 'No channels available'}</p>`;
|
|
260
263
|
return;
|
|
261
264
|
}
|
|
262
265
|
|
|
263
|
-
availableChannels
|
|
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>`;
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
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
|
-
|
|
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 => {
|
|
@@ -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
|
+
}
|
|
@@ -50,7 +50,8 @@ module CollavreSlack
|
|
|
50
50
|
comment.save!
|
|
51
51
|
|
|
52
52
|
# Dispatch system event to trigger AI agents (same as CommentsController#create)
|
|
53
|
-
|
|
53
|
+
# Skip dispatch for slash command messages (e.g. /topic, /calendar)
|
|
54
|
+
unless comment.private? || response.present?
|
|
54
55
|
Collavre::SystemEvents::Dispatcher.dispatch("comment_created", {
|
|
55
56
|
comment: {
|
|
56
57
|
id: comment.id,
|
|
@@ -28,8 +28,26 @@ module CollavreSlack
|
|
|
28
28
|
parse_response(response)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def list_channels
|
|
32
|
-
|
|
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">×</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
|
|
data/config/locales/en.yml
CHANGED
data/config/locales/ko.yml
CHANGED
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.
|
|
4
|
+
version: 0.2.2
|
|
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
|