collavre_linear 0.2.0 → 0.2.1
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/javascript/__tests__/linear_modal_reopen.test.js +178 -0
- data/app/javascript/collavre_linear.js +30 -2
- data/app/javascript/linear_modal_reopen.js +74 -0
- data/app/services/collavre_linear/client.rb +27 -4
- data/app/views/collavre_linear/auth/setup.html.erb +24 -4
- data/lib/collavre_linear/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: d8004796cae3abe1389345fbd85959f2390e8d8930768c86045ba202ca94c5e1
|
|
4
|
+
data.tar.gz: 0e6f414178db0ee5513f238cac0fabf75a44b4eea80a1f7142ce94487bbff798
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 258031f64f076de07891357445af346b6bc10c75d43fb802d2a749eb1c7069bc4d81ca64a66366b0875b2f971c8bd986c223fb133ca47dcbc0ed9c86d936cf37
|
|
7
|
+
data.tar.gz: 07b41024a06d0d5074a43685a2dfbab697f92990fb6100849fcf893c38159665c553254bdaca8f40f5ea209b632dc6b880d583a133047a80a47cb0e89d2c25c0
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LINEAR_REOPEN_KEY,
|
|
3
|
+
markReopenAfterConnect,
|
|
4
|
+
consumeReopenAfterConnect,
|
|
5
|
+
safeSessionStorage,
|
|
6
|
+
} from '../linear_modal_reopen.js';
|
|
7
|
+
|
|
8
|
+
// Minimal in-memory Storage stand-in (jsdom's is fine too, but this keeps the
|
|
9
|
+
// unit test free of environment setup and lets us model a throwing storage).
|
|
10
|
+
function fakeStorage() {
|
|
11
|
+
const map = new Map();
|
|
12
|
+
return {
|
|
13
|
+
setItem: (k, v) => map.set(k, String(v)),
|
|
14
|
+
getItem: (k) => (map.has(k) ? map.get(k) : null),
|
|
15
|
+
removeItem: (k) => map.delete(k),
|
|
16
|
+
_size: () => map.size,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('safeSessionStorage', () => {
|
|
21
|
+
test('returns the storage object when Web Storage is available', () => {
|
|
22
|
+
// jsdom provides a working window.sessionStorage.
|
|
23
|
+
expect(safeSessionStorage()).toBe(window.sessionStorage);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('returns null when the window.sessionStorage getter itself throws', () => {
|
|
27
|
+
// Web Storage disabled by policy (packaged WKWebView, storage-blocked
|
|
28
|
+
// browser) throws on the property *access*, before any value is passed to
|
|
29
|
+
// mark/consume. safeSessionStorage must absorb it so callers never throw.
|
|
30
|
+
const original = Object.getOwnPropertyDescriptor(window, 'sessionStorage');
|
|
31
|
+
Object.defineProperty(window, 'sessionStorage', {
|
|
32
|
+
configurable: true,
|
|
33
|
+
get() { throw new Error('SecurityError: storage disabled'); },
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
expect(() => safeSessionStorage()).not.toThrow();
|
|
37
|
+
expect(safeSessionStorage()).toBeNull();
|
|
38
|
+
} finally {
|
|
39
|
+
if (original) Object.defineProperty(window, 'sessionStorage', original);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('markReopenAfterConnect', () => {
|
|
45
|
+
test('persists the unscoped sentinel when no creative id is given', () => {
|
|
46
|
+
const storage = fakeStorage();
|
|
47
|
+
markReopenAfterConnect(storage);
|
|
48
|
+
expect(storage.getItem(LINEAR_REOPEN_KEY)).toBe('*');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('the unscoped sentinel cannot equal any serialized creative id', () => {
|
|
52
|
+
// Creative ids are positive integers → digit strings. The sentinel must be a
|
|
53
|
+
// non-digit so a real id never gets misread as "unscoped".
|
|
54
|
+
const storage = fakeStorage();
|
|
55
|
+
markReopenAfterConnect(storage);
|
|
56
|
+
expect(storage.getItem(LINEAR_REOPEN_KEY)).not.toMatch(/^\d+$/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('persists the creative id when given (scoped reopen)', () => {
|
|
60
|
+
const storage = fakeStorage();
|
|
61
|
+
markReopenAfterConnect(storage, 42);
|
|
62
|
+
expect(storage.getItem(LINEAR_REOPEN_KEY)).toBe('42');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('falls back to the unscoped sentinel for an empty creative id', () => {
|
|
66
|
+
const storage = fakeStorage();
|
|
67
|
+
markReopenAfterConnect(storage, '');
|
|
68
|
+
expect(storage.getItem(LINEAR_REOPEN_KEY)).toBe('*');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('tolerates a null storage without throwing', () => {
|
|
72
|
+
expect(() => markReopenAfterConnect(null)).not.toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('swallows storage errors (private mode / WKWebView)', () => {
|
|
76
|
+
const throwing = { setItem: () => { throw new Error('QuotaExceeded'); } };
|
|
77
|
+
expect(() => markReopenAfterConnect(throwing)).not.toThrow();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('consumeReopenAfterConnect', () => {
|
|
82
|
+
test('returns true exactly once, then clears the flag (regression)', () => {
|
|
83
|
+
// Without the one-shot clear, every subsequent reload — including the one
|
|
84
|
+
// after a successful link — would reopen the modal. It must fire once.
|
|
85
|
+
const storage = fakeStorage();
|
|
86
|
+
markReopenAfterConnect(storage);
|
|
87
|
+
|
|
88
|
+
expect(consumeReopenAfterConnect(storage)).toBe(true);
|
|
89
|
+
expect(consumeReopenAfterConnect(storage)).toBe(false);
|
|
90
|
+
expect(storage.getItem(LINEAR_REOPEN_KEY)).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('reopens only when the current creative matches the stored one', () => {
|
|
94
|
+
const storage = fakeStorage();
|
|
95
|
+
markReopenAfterConnect(storage, 42); // popup connected creative 42
|
|
96
|
+
// Opener is on the same creative → reopen.
|
|
97
|
+
expect(consumeReopenAfterConnect(storage, 42)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('does NOT reopen when the opener navigated to a different creative', () => {
|
|
101
|
+
// Codex P2: mid-flow navigation must not surface the wrong creative's modal.
|
|
102
|
+
const storage = fakeStorage();
|
|
103
|
+
markReopenAfterConnect(storage, 42); // popup connected creative 42
|
|
104
|
+
// Opener moved to creative 99 before the reload → no reopen, flag cleared.
|
|
105
|
+
expect(consumeReopenAfterConnect(storage, 99)).toBe(false);
|
|
106
|
+
expect(storage.getItem(LINEAR_REOPEN_KEY)).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('creative id 1 stays scoped and does NOT reopen a different creative', () => {
|
|
110
|
+
// Regression: the sentinel used to be '1', colliding with creative id 1 — so
|
|
111
|
+
// OAuth from creative 1 was misread as unscoped and reopened whatever modal
|
|
112
|
+
// the opener had navigated to. It must behave like any other scoped id.
|
|
113
|
+
const storage = fakeStorage();
|
|
114
|
+
markReopenAfterConnect(storage, 1); // popup connected creative 1
|
|
115
|
+
expect(consumeReopenAfterConnect(storage, 99)).toBe(false); // opener moved away
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('creative id 1 reopens when the opener is still on creative 1', () => {
|
|
119
|
+
const storage = fakeStorage();
|
|
120
|
+
markReopenAfterConnect(storage, 1);
|
|
121
|
+
expect(consumeReopenAfterConnect(storage, 1)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('clears a mismatched intent so it never fires on a later matching visit', () => {
|
|
125
|
+
const storage = fakeStorage();
|
|
126
|
+
markReopenAfterConnect(storage, 42);
|
|
127
|
+
consumeReopenAfterConnect(storage, 99); // mismatch on the reload
|
|
128
|
+
// Later navigation back to creative 42 must NOT auto-open — intent was spent.
|
|
129
|
+
expect(consumeReopenAfterConnect(storage, 42)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('matches across id string/number coercion', () => {
|
|
133
|
+
const storage = fakeStorage();
|
|
134
|
+
markReopenAfterConnect(storage, 42); // stored as '42'
|
|
135
|
+
expect(consumeReopenAfterConnect(storage, '42')).toBe(true); // dataset is a string
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('unscoped sentinel reopens regardless of current creative (legacy)', () => {
|
|
139
|
+
const storage = fakeStorage();
|
|
140
|
+
markReopenAfterConnect(storage); // no id → '1'
|
|
141
|
+
expect(consumeReopenAfterConnect(storage, 7)).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('returns false when no reopen was pending', () => {
|
|
145
|
+
expect(consumeReopenAfterConnect(fakeStorage())).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('tolerates a null storage', () => {
|
|
149
|
+
expect(consumeReopenAfterConnect(null)).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('swallows storage errors and reports no pending reopen', () => {
|
|
153
|
+
const throwing = { getItem: () => { throw new Error('SecurityError'); } };
|
|
154
|
+
expect(consumeReopenAfterConnect(throwing)).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('end-to-end via safeSessionStorage: mark on connect, consume once', () => {
|
|
158
|
+
// The call sites pass safeSessionStorage() rather than window.sessionStorage
|
|
159
|
+
// directly, so the whole flow must survive a storage-backed session.
|
|
160
|
+
const s = safeSessionStorage();
|
|
161
|
+
if (!s) return; // jsdom always provides it; guard just in case
|
|
162
|
+
s.removeItem(LINEAR_REOPEN_KEY);
|
|
163
|
+
markReopenAfterConnect(s);
|
|
164
|
+
expect(consumeReopenAfterConnect(s)).toBe(true);
|
|
165
|
+
expect(consumeReopenAfterConnect(s)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('end-to-end: mark on connect, consume on the next load', () => {
|
|
169
|
+
// Mirrors the real flow: the postMessage handler marks, the reload happens,
|
|
170
|
+
// then turbo:load consumes the flag and opens the modal exactly once.
|
|
171
|
+
const storage = fakeStorage();
|
|
172
|
+
markReopenAfterConnect(storage); // linearConnected received
|
|
173
|
+
const reopenedFirstLoad = consumeReopenAfterConnect(storage); // turbo:load
|
|
174
|
+
const reopenedSecondLoad = consumeReopenAfterConnect(storage); // later nav
|
|
175
|
+
expect(reopenedFirstLoad).toBe(true);
|
|
176
|
+
expect(reopenedSecondLoad).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -15,19 +15,38 @@
|
|
|
15
15
|
// unconfirmed and errors would vanish. Route through the shared in-app modal
|
|
16
16
|
// like the GitHub/Slack/Notion engines do.
|
|
17
17
|
import { alertDialog, confirmDialog } from 'collavre/lib/utils/dialog';
|
|
18
|
+
import {
|
|
19
|
+
markReopenAfterConnect,
|
|
20
|
+
consumeReopenAfterConnect,
|
|
21
|
+
safeSessionStorage,
|
|
22
|
+
} from './linear_modal_reopen.js';
|
|
18
23
|
|
|
19
24
|
let linearIntegrationInitialized = false;
|
|
20
25
|
|
|
21
26
|
if (!linearIntegrationInitialized) {
|
|
22
27
|
linearIntegrationInitialized = true;
|
|
23
28
|
|
|
24
|
-
// The OAuth popup posts `linearConnected` to the opener
|
|
25
|
-
// Reload so the modal re-renders in the connected (link-a-project)
|
|
29
|
+
// The OAuth popup posts `linearConnected` to the opener once the account is
|
|
30
|
+
// connected. Reload so the modal re-renders in the connected (link-a-project)
|
|
31
|
+
// state, then reopen it (see turbo:load) so the user lands directly on the
|
|
32
|
+
// project settings step instead of a closed modal.
|
|
26
33
|
// Bound to `window` once — it survives Turbo navigations, unlike the
|
|
27
34
|
// per-navigation element listeners set up in turbo:load below.
|
|
28
35
|
window.addEventListener('message', function (event) {
|
|
29
36
|
if (event.origin !== window.location.origin) return;
|
|
30
37
|
if (event.data && event.data.type === 'linearConnected') {
|
|
38
|
+
// Close the popup from here. window.close() inside the popup is unreliable
|
|
39
|
+
// after the cross-origin OAuth round trip and in the desktop WebView, so
|
|
40
|
+
// the popup's own "Close" button looked dead — closing the window we
|
|
41
|
+
// opened is the reliable path.
|
|
42
|
+
try {
|
|
43
|
+
if (event.source && !event.source.closed) event.source.close();
|
|
44
|
+
} catch (e) { /* cross-origin or already closed */ }
|
|
45
|
+
// Survive the reload: reopen the modal on the next load so the flow lands
|
|
46
|
+
// on the project-link step automatically. Scope it to the creative the
|
|
47
|
+
// popup was opened for (event.data.creativeId) so a mid-flow navigation to
|
|
48
|
+
// a different creative doesn't auto-open the wrong creative's link modal.
|
|
49
|
+
markReopenAfterConnect(safeSessionStorage(), event.data.creativeId);
|
|
31
50
|
window.location.reload();
|
|
32
51
|
}
|
|
33
52
|
});
|
|
@@ -191,6 +210,15 @@ if (!linearIntegrationInitialized) {
|
|
|
191
210
|
showModal();
|
|
192
211
|
});
|
|
193
212
|
|
|
213
|
+
// Just returned from the OAuth popup: open the modal straight to the
|
|
214
|
+
// project-link step instead of leaving the (display:none) modal closed, so
|
|
215
|
+
// the user doesn't have to reopen the Linear menu by hand. Only reopen when
|
|
216
|
+
// this page's creative matches the one the popup connected for — otherwise a
|
|
217
|
+
// mid-flow navigation would surface the wrong creative's link modal.
|
|
218
|
+
if (consumeReopenAfterConnect(safeSessionStorage(), modal.dataset.creativeId)) {
|
|
219
|
+
showModal();
|
|
220
|
+
}
|
|
221
|
+
|
|
194
222
|
closeBtn?.addEventListener('click', closeModal);
|
|
195
223
|
|
|
196
224
|
modal.addEventListener('click', function (event) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Reopen-after-connect intent for the Linear integration modal.
|
|
2
|
+
//
|
|
3
|
+
// When the OAuth popup finishes it posts `linearConnected` and the opener does a
|
|
4
|
+
// full `window.location.reload()` so the server re-renders the modal in its
|
|
5
|
+
// project-linking state (the account now exists). But the modal defaults to
|
|
6
|
+
// `display:none` and nothing re-opens it after the reload — so the connect→link
|
|
7
|
+
// step is invisible and the user has to click the Linear connect button again to
|
|
8
|
+
// reach it.
|
|
9
|
+
//
|
|
10
|
+
// This module persists a one-shot "reopen the modal" flag across that reload.
|
|
11
|
+
// Factored out of the side-effectful collavre_linear.js opener so the behavior
|
|
12
|
+
// is unit-testable (that file is imported only for its window/turbo listeners).
|
|
13
|
+
|
|
14
|
+
export const LINEAR_REOPEN_KEY = 'linearReopenModal';
|
|
15
|
+
|
|
16
|
+
// Read window.sessionStorage defensively. When Web Storage is disabled by policy
|
|
17
|
+
// (packaged WKWebView, a browser blocking storage for the site) the property
|
|
18
|
+
// getter *itself* throws — before any value reaches mark/consume — so guarding
|
|
19
|
+
// only inside those functions is not enough; the access site must be guarded
|
|
20
|
+
// too. Returns null when unavailable so callers never take an uncaught throw.
|
|
21
|
+
export function safeSessionStorage() {
|
|
22
|
+
try {
|
|
23
|
+
return window.sessionStorage;
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Unscoped sentinel. Stored when the OAuth creative id is unknown so the reopen
|
|
30
|
+
// still fires (matches any page) — the scoped path stores the id instead. Must
|
|
31
|
+
// be a value no serialized creative id can equal: ids are positive integers and
|
|
32
|
+
// serialize to digit strings, so a non-digit marker never collides. (A previous
|
|
33
|
+
// '1' sentinel collided with creative id 1 — that page's scoped reopen was
|
|
34
|
+
// misread as unscoped and skipped the mismatch guard.)
|
|
35
|
+
const UNSCOPED = '*';
|
|
36
|
+
|
|
37
|
+
// Record that the modal should reopen after the next full-page reload, scoped to
|
|
38
|
+
// the creative the OAuth popup was opened for. The popup posts its own
|
|
39
|
+
// creativeId; storing it lets consume verify the reloaded page still shows that
|
|
40
|
+
// same creative before reopening — otherwise, if the opener navigated to a
|
|
41
|
+
// different creative while the popup was open, the reload would auto-open the
|
|
42
|
+
// wrong creative's link modal and the user could link the project to it.
|
|
43
|
+
// Tolerates a missing or throwing storage (private-mode Safari, packaged
|
|
44
|
+
// WKWebView) — the reopen is a convenience and must never throw inside the
|
|
45
|
+
// postMessage handler.
|
|
46
|
+
export function markReopenAfterConnect(storage, creativeId) {
|
|
47
|
+
const value =
|
|
48
|
+
creativeId != null && String(creativeId) !== '' ? String(creativeId) : UNSCOPED;
|
|
49
|
+
try {
|
|
50
|
+
if (storage) storage.setItem(LINEAR_REOPEN_KEY, value);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
/* storage unavailable — skip the reopen nicety */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Consume the reopen intent: returns true at most once, then clears the flag so
|
|
57
|
+
// a later unrelated reload (e.g. after linking succeeds) does not reopen it. The
|
|
58
|
+
// flag is cleared on the first consume regardless of match, so a stale intent
|
|
59
|
+
// (opener navigated elsewhere) never lingers to reopen the modal on some later
|
|
60
|
+
// visit. Returns true only when the stored creative id matches the current page
|
|
61
|
+
// (or when the legacy unscoped sentinel was stored).
|
|
62
|
+
export function consumeReopenAfterConnect(storage, currentCreativeId) {
|
|
63
|
+
try {
|
|
64
|
+
if (!storage) return false;
|
|
65
|
+
const stored = storage.getItem(LINEAR_REOPEN_KEY);
|
|
66
|
+
if (!stored) return false;
|
|
67
|
+
storage.removeItem(LINEAR_REOPEN_KEY); // one-shot regardless of match
|
|
68
|
+
if (stored === UNSCOPED) return true;
|
|
69
|
+
return String(currentCreativeId == null ? '' : currentCreativeId) === stored;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
/* storage unavailable — treat as no pending reopen */
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
@@ -423,7 +423,7 @@ module CollavreLinear
|
|
|
423
423
|
parsed = begin
|
|
424
424
|
JSON.parse(response.body)
|
|
425
425
|
rescue JSON::ParserError
|
|
426
|
-
raise Error, "Linear returned non-JSON response (HTTP #{response.code}): #{response.body.to_s[0, 200]}"
|
|
426
|
+
raise Error, "Linear returned non-JSON response (HTTP #{response.code}) from #{endpoint}: #{response.body.to_s[0, 200]}"
|
|
427
427
|
end
|
|
428
428
|
|
|
429
429
|
if parsed["errors"].present?
|
|
@@ -470,9 +470,32 @@ module CollavreLinear
|
|
|
470
470
|
end
|
|
471
471
|
|
|
472
472
|
def resolve_endpoint
|
|
473
|
-
|
|
474
|
-
Collavre::IntegrationSettings::Resolver
|
|
475
|
-
|
|
473
|
+
configured =
|
|
474
|
+
if defined?(Collavre::IntegrationSettings::Resolver)
|
|
475
|
+
Collavre::IntegrationSettings::Resolver.get(:linear_api_endpoint).presence
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
normalize_endpoint(configured) || DEFAULT_ENDPOINT
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Guard a misconfigured `linear_api_endpoint` override. Linear's GraphQL API
|
|
482
|
+
# lives at the `/graphql` path; an admin who pastes only a base URL
|
|
483
|
+
# (e.g. `https://host:port` or `.../`) would otherwise POST to `/`, which a
|
|
484
|
+
# non-Linear server answers with a 404 ("Cannot POST /"). When the configured
|
|
485
|
+
# value carries no meaningful path, point it at `/graphql`. A value that
|
|
486
|
+
# already specifies a non-root path is left untouched.
|
|
487
|
+
def normalize_endpoint(value)
|
|
488
|
+
return nil if value.blank?
|
|
489
|
+
|
|
490
|
+
uri = URI.parse(value.strip)
|
|
491
|
+
return value if uri.path.present? && uri.path != "/"
|
|
492
|
+
|
|
493
|
+
uri.path = "/graphql"
|
|
494
|
+
uri.to_s
|
|
495
|
+
rescue URI::InvalidURIError
|
|
496
|
+
# Not a parseable URI: return as-is and let the request surface the failure
|
|
497
|
+
# rather than silently rewriting an unrecognizable value.
|
|
498
|
+
value
|
|
476
499
|
end
|
|
477
500
|
|
|
478
501
|
# Convert symbol keys to camelCase strings for GraphQL variables.
|
|
@@ -50,13 +50,33 @@
|
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
<script>
|
|
53
|
-
|
|
53
|
+
// Tell the opener the account is connected so it advances straight to the
|
|
54
|
+
// project-link step. The opener also closes this popup (window.close() below
|
|
55
|
+
// is unreliable after the cross-origin OAuth round trip and in the desktop
|
|
56
|
+
// WebView, which is why the "Close" button used to look dead).
|
|
57
|
+
function notifyOpener() {
|
|
54
58
|
try {
|
|
55
|
-
if (window.opener) {
|
|
56
|
-
|
|
59
|
+
if (window.opener && !window.opener.closed) {
|
|
60
|
+
// Include the creative this popup was opened for so the opener only
|
|
61
|
+
// reopens the modal when it's still showing that same creative — not
|
|
62
|
+
// whatever creative it may have navigated to while the popup was open.
|
|
63
|
+
var wizard = document.getElementById('linear-wizard');
|
|
64
|
+
var creativeId = wizard ? wizard.dataset.creativeId : '';
|
|
65
|
+
window.opener.postMessage(
|
|
66
|
+
{ type: 'linearConnected', creativeId: creativeId },
|
|
67
|
+
window.location.origin
|
|
68
|
+
);
|
|
57
69
|
}
|
|
58
70
|
} catch (e) {}
|
|
59
|
-
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Auto-advance on load: no click required — the opener reloads and jumps to
|
|
74
|
+
// the project settings ("link a project") step.
|
|
75
|
+
notifyOpener();
|
|
76
|
+
|
|
77
|
+
document.getElementById('close-btn').addEventListener('click', function() {
|
|
78
|
+
notifyOpener();
|
|
79
|
+
try { window.close(); } catch (e) {}
|
|
60
80
|
});
|
|
61
81
|
</script>
|
|
62
82
|
</body>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: collavre_linear
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Collavre
|
|
@@ -49,7 +49,9 @@ files:
|
|
|
49
49
|
- app/controllers/collavre_linear/auth_controller.rb
|
|
50
50
|
- app/controllers/collavre_linear/creatives/integrations_controller.rb
|
|
51
51
|
- app/controllers/collavre_linear/webhooks_controller.rb
|
|
52
|
+
- app/javascript/__tests__/linear_modal_reopen.test.js
|
|
52
53
|
- app/javascript/collavre_linear.js
|
|
54
|
+
- app/javascript/linear_modal_reopen.js
|
|
53
55
|
- app/jobs/collavre_linear/inbound_apply_job.rb
|
|
54
56
|
- app/jobs/collavre_linear/outbound_archive_job.rb
|
|
55
57
|
- app/jobs/collavre_linear/outbound_comment_delete_job.rb
|