Skip to content

Commit d53032f

Browse files
committed
feat: Add QR code modal for saved searches
1 parent 407ed46 commit d53032f

10 files changed

Lines changed: 555 additions & 42 deletions

e2e/tests/overview-page.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2023 Google LLC
2+
* Copyright 2023 Google LLC.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

frontend/src/static/js/components/test/webstatus-saved-search-controls.test.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ describe('WebstatusSavedSearchControls', () => {
141141
});
142142

143143
it('does not render active search controls when no savedSearch is provided', () => {
144-
const shareButton = element.shadowRoot!.querySelector('sl-copy-button');
144+
const shareButton = element.shadowRoot!.querySelector(
145+
'sl-icon-button[name="share"]',
146+
);
145147
const bookmarkButton = element.shadowRoot!.querySelector(
146148
'sl-icon-button[name^="star"]',
147149
);
@@ -182,7 +184,9 @@ describe('WebstatusSavedSearchControls', () => {
182184
const saveButton = element.shadowRoot!.querySelector(
183185
'sl-icon-button[name="floppy"]',
184186
);
185-
const shareButton = element.shadowRoot!.querySelector('sl-copy-button');
187+
const shareButton = element.shadowRoot!.querySelector(
188+
'sl-icon-button[name="share"]',
189+
);
186190
const bookmarkButton = element.shadowRoot!.querySelector(
187191
'sl-icon-button[name="star"]',
188192
);
@@ -205,13 +209,33 @@ describe('WebstatusSavedSearchControls', () => {
205209
expect(deleteButton).to.not.exist;
206210
});
207211

208-
it('configures share button correctly', () => {
209-
const copyButton = element.shadowRoot!.querySelector('sl-copy-button');
210-
const expectedUrl = `http://localhost:8080/features?q=saved%3A${mockSavedSearchViewerNotBookmarked.id}`;
211-
expect(copyButton).to.have.attribute('value', expectedUrl);
212-
expect(formatOverviewPageUrlStub).to.have.been.calledWith(mockLocation, {
213-
q: `saved:${mockSavedSearchViewerNotBookmarked.id}`,
214-
});
212+
it('opens modal when share button is clicked and renders QR code', async () => {
213+
const shareButton = element.shadowRoot!.querySelector<SlIconButton>(
214+
'sl-icon-button[name="share"]',
215+
)!;
216+
shareButton.click();
217+
await element.updateComplete;
218+
219+
// Wait for dialog to appear in body
220+
await waitUntil(
221+
() =>
222+
document.body.querySelector('webstatus-saved-search-share-dialog') !==
223+
null,
224+
);
225+
226+
const shareDialog = document.body.querySelector(
227+
'webstatus-saved-search-share-dialog',
228+
)!;
229+
expect(shareDialog).to.exist;
230+
231+
const dialog = shareDialog.shadowRoot!.querySelector('sl-dialog');
232+
expect(dialog).to.exist;
233+
234+
const qrCode = dialog!.querySelector('sl-qr-code');
235+
expect(qrCode).to.exist;
236+
237+
const copyButton = dialog!.querySelector('button.blue-copy-button');
238+
expect(copyButton).to.exist;
215239
});
216240

217241
it('calls handleBookmarkSavedSearch to bookmark when bookmark button is clicked', async () => {
@@ -332,7 +356,9 @@ describe('WebstatusSavedSearchControls', () => {
332356
const saveButton = element.shadowRoot!.querySelector(
333357
'sl-icon-button[name="floppy"]',
334358
);
335-
const shareButton = element.shadowRoot!.querySelector('sl-copy-button');
359+
const shareButton = element.shadowRoot!.querySelector(
360+
'sl-icon-button[name="share"]',
361+
);
336362
const bookmarkButton = element.shadowRoot!.querySelector(
337363
'sl-icon-button[name="star"]',
338364
);
@@ -446,7 +472,9 @@ describe('WebstatusSavedSearchControls', () => {
446472
const saveButton = element.shadowRoot!.querySelector(
447473
'sl-icon-button[name="floppy"]',
448474
);
449-
const shareButton = element.shadowRoot!.querySelector('sl-copy-button');
475+
const shareButton = element.shadowRoot!.querySelector(
476+
'sl-icon-button[name="share"]',
477+
);
450478
const bookmarkButton = element.shadowRoot!.querySelector(
451479
'sl-icon-button[name="star"]',
452480
);

frontend/src/static/js/components/test/webstatus-subscribe-button.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,21 @@ describe('webstatus-subscribe-button', () => {
8282
expect(dialog?.open).to.be.true;
8383
});
8484

85+
it('opens dialog automatically when autoOpen is true and user is logged in', async () => {
86+
const el = await fixture<SubscribeButton>(html`
87+
<webstatus-subscribe-button
88+
.userContext=${mockUserContext}
89+
.savedSearchId=${'test-id'}
90+
.autoOpen=${true}
91+
></webstatus-subscribe-button>
92+
`);
93+
const dialog = el.shadowRoot?.querySelector<ManageSubscriptionsDialog>(
94+
'webstatus-manage-subscriptions-dialog',
95+
);
96+
expect(dialog).to.exist;
97+
expect(dialog?.open).to.be.true;
98+
});
99+
85100
it('calls toaster on successful save', async () => {
86101
const toasterSpy = sinon.spy();
87102
const el = await fixture<SubscribeButton>(html`
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {LitElement, TemplateResult, css, html} from 'lit';
18+
import {customElement, property, state} from 'lit/decorators.js';
19+
import {consume} from '@lit/context';
20+
import '@shoelace-style/shoelace/dist/components/dialog/dialog.js';
21+
import '@shoelace-style/shoelace/dist/components/button/button.js';
22+
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
23+
import {
24+
firebaseAuthContext,
25+
AuthConfig,
26+
} from '../contexts/firebase-auth-context.js';
27+
import {toast} from '../utils/toast.js';
28+
import {THEME} from '../css/_theme-css.js';
29+
30+
@customElement('webstatus-login-prompt-dialog')
31+
export class WebstatusLoginPromptDialog extends LitElement {
32+
@property({type: Boolean})
33+
open = false;
34+
35+
@property({type: String})
36+
savedSearchName = '';
37+
38+
@consume({context: firebaseAuthContext, subscribe: true})
39+
@state()
40+
firebaseAuthConfig?: AuthConfig;
41+
42+
static styles = [
43+
THEME,
44+
css`
45+
sl-dialog::part(title) {
46+
font-weight: bold;
47+
}
48+
.content {
49+
display: flex;
50+
flex-direction: column;
51+
gap: var(--content-padding);
52+
}
53+
.footer {
54+
display: flex;
55+
justify-content: flex-end;
56+
}
57+
`,
58+
];
59+
60+
async _handleLogin() {
61+
if (this.firebaseAuthConfig) {
62+
try {
63+
await this.firebaseAuthConfig.signIn();
64+
this.dispatchEvent(new CustomEvent('login-success'));
65+
this.open = false;
66+
} catch (error: any) {
67+
await toast(
68+
`Failed to login: ${error.message ?? 'unknown'}`,
69+
'danger',
70+
'exclamation-triangle',
71+
);
72+
}
73+
}
74+
}
75+
76+
_handleClose() {
77+
this.open = false;
78+
this.dispatchEvent(new CustomEvent('prompt-close'));
79+
}
80+
81+
render(): TemplateResult {
82+
return html`
83+
<sl-dialog
84+
.open=${this.open}
85+
label="Log in to Subscribe"
86+
@sl-after-hide=${this._handleClose}
87+
>
88+
<div class="content">
89+
<p>
90+
You need an account to subscribe to
91+
<strong>${this.savedSearchName}</strong> and receive latest updates.
92+
</p>
93+
</div>
94+
<div slot="footer" class="footer">
95+
<sl-button variant="primary" @click=${this._handleLogin}>
96+
<sl-icon slot="prefix" name="github"></sl-icon>
97+
Log in
98+
</sl-button>
99+
</div>
100+
</sl-dialog>
101+
`;
102+
}
103+
}

frontend/src/static/js/components/webstatus-overview-content.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import './webstatus-overview-data-loader.js';
3030
import './webstatus-overview-filters.js';
3131
import './webstatus-overview-pagination.js';
3232
import './webstatus-subscribe-button.js';
33+
import './webstatus-login-prompt-dialog.js';
3334
import {SHARED_STYLES} from '../css/shared-css.js';
3435
import {TaskTracker} from '../utils/task-tracker.js';
3536
import {ApiError} from '../api/errors.js';
@@ -55,6 +56,7 @@ import {
5556
formatOverviewPageUrl,
5657
getEditSavedSearch,
5758
getOrigin,
59+
getSubscribeToSavedSearch,
5860
QueryStringOverrides,
5961
updatePageUrl,
6062
} from '../utils/urls.js';
@@ -244,7 +246,7 @@ export class WebstatusOverviewContent extends LitElement {
244246
_updatePageUrl: (
245247
pathname: string,
246248
location: {search: string},
247-
overrides: {edit_saved_search?: boolean},
249+
overrides: QueryStringOverrides,
248250
) => void = updatePageUrl;
249251
_formatOverviewPageUrl: (
250252
location?: {search: string},
@@ -266,6 +268,14 @@ export class WebstatusOverviewContent extends LitElement {
266268
);
267269
this._updatePageUrl('', this.location, {edit_saved_search: false});
268270
}
271+
272+
if (
273+
getSubscribeToSavedSearch(this.location) &&
274+
this.userContext &&
275+
this.savedSearch
276+
) {
277+
this._updatePageUrl('', this.location, {subscribe: false});
278+
}
269279
}
270280

271281
render(): TemplateResult {
@@ -279,6 +289,7 @@ export class WebstatusOverviewContent extends LitElement {
279289
? savedSearch
280290
: undefined;
281291
const config = this.subscribeButtonConfig;
292+
const shouldSubscribe = getSubscribeToSavedSearch(this.location);
282293

283294
return html` <div class="main">
284295
<div class="hbox halign-items-space-between header-line">
@@ -287,6 +298,7 @@ export class WebstatusOverviewContent extends LitElement {
287298
? html`<webstatus-subscribe-button
288299
.savedSearchId=${config.id}
289300
.searchTitle=${config.title}
301+
.autoOpen=${shouldSubscribe}
290302
></webstatus-subscribe-button>`
291303
: nothing}
292304
</div>
@@ -323,6 +335,13 @@ export class WebstatusOverviewContent extends LitElement {
323335
.userContext=${this.userContext!}
324336
.savedSearch=${userSavedSearch?.value}
325337
.location=${this.location}
326-
></webstatus-saved-search-editor>`;
338+
></webstatus-saved-search-editor>
339+
<webstatus-login-prompt-dialog
340+
.open=${getSubscribeToSavedSearch(this.location) &&
341+
this.userContext === null}
342+
.savedSearchName=${config?.title || 'this search'}
343+
@prompt-close=${() =>
344+
this._updatePageUrl('', this.location, {subscribe: false})}
345+
></webstatus-login-prompt-dialog>`;
327346
}
328347
}

0 commit comments

Comments
 (0)