Skip to content

Commit 9c3b6f1

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

4 files changed

Lines changed: 339 additions & 28 deletions

File tree

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

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ 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('sl-icon-button[name="share"]');
145145
const bookmarkButton = element.shadowRoot!.querySelector(
146146
'sl-icon-button[name^="star"]',
147147
);
@@ -182,7 +182,7 @@ describe('WebstatusSavedSearchControls', () => {
182182
const saveButton = element.shadowRoot!.querySelector(
183183
'sl-icon-button[name="floppy"]',
184184
);
185-
const shareButton = element.shadowRoot!.querySelector('sl-copy-button');
185+
const shareButton = element.shadowRoot!.querySelector('sl-icon-button[name="share"]');
186186
const bookmarkButton = element.shadowRoot!.querySelector(
187187
'sl-icon-button[name="star"]',
188188
);
@@ -205,13 +205,27 @@ describe('WebstatusSavedSearchControls', () => {
205205
expect(deleteButton).to.not.exist;
206206
});
207207

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-
});
208+
it('opens modal when share button is clicked and renders QR code', async () => {
209+
const shareButton = element.shadowRoot!.querySelector<SlIconButton>(
210+
'sl-icon-button[name="share"]',
211+
)!;
212+
shareButton.click();
213+
await element.updateComplete;
214+
215+
// Wait for dialog to appear in body
216+
await waitUntil(() => document.body.querySelector('webstatus-saved-search-share-dialog') !== null);
217+
218+
const shareDialog = document.body.querySelector('webstatus-saved-search-share-dialog')!;
219+
expect(shareDialog).to.exist;
220+
221+
const dialog = shareDialog.shadowRoot!.querySelector('sl-dialog');
222+
expect(dialog).to.exist;
223+
224+
const qrCode = dialog!.querySelector('sl-qr-code');
225+
expect(qrCode).to.exist;
226+
227+
const copyButton = dialog!.querySelector('button.blue-copy-button');
228+
expect(copyButton).to.exist;
215229
});
216230

217231
it('calls handleBookmarkSavedSearch to bookmark when bookmark button is clicked', async () => {
@@ -332,7 +346,7 @@ describe('WebstatusSavedSearchControls', () => {
332346
const saveButton = element.shadowRoot!.querySelector(
333347
'sl-icon-button[name="floppy"]',
334348
);
335-
const shareButton = element.shadowRoot!.querySelector('sl-copy-button');
349+
const shareButton = element.shadowRoot!.querySelector('sl-icon-button[name="share"]');
336350
const bookmarkButton = element.shadowRoot!.querySelector(
337351
'sl-icon-button[name="star"]',
338352
);
@@ -446,7 +460,7 @@ describe('WebstatusSavedSearchControls', () => {
446460
const saveButton = element.shadowRoot!.querySelector(
447461
'sl-icon-button[name="floppy"]',
448462
);
449-
const shareButton = element.shadowRoot!.querySelector('sl-copy-button');
463+
const shareButton = element.shadowRoot!.querySelector('sl-icon-button[name="share"]');
450464
const bookmarkButton = element.shadowRoot!.querySelector(
451465
'sl-icon-button[name="star"]',
452466
);

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

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import {LitElement, TemplateResult, css, html, nothing} from 'lit';
1818
import {customElement, property, query, state} from 'lit/decorators.js';
19+
import {openShareDialog} from './webstatus-saved-search-share-dialog.js';
1920
import {APIClient} from '../contexts/api-client-context.js';
2021
import {UserContext} from '../contexts/firebase-user-context.js';
2122
import {
@@ -44,6 +45,8 @@ export class WebstatusSavedSearchControls extends LitElement {
4445
@property({type: Object})
4546
apiClient!: APIClient;
4647

48+
49+
4750
@property({type: Object})
4851
userContext!: UserContext;
4952

@@ -73,6 +76,103 @@ export class WebstatusSavedSearchControls extends LitElement {
7376
#bookmark-task-spinner {
7477
font-size: 1rem;
7578
}
79+
80+
/* Modal Styles - Default (Light theme) */
81+
sl-dialog::part(panel) {
82+
background-color: white !important; /* Solid white background */
83+
color: black;
84+
}
85+
86+
sl-dialog::part(body) {
87+
background-color: white !important; /* Solid white background for body */
88+
}
89+
90+
/* Z-Index Fix (Light Theme) */
91+
sl-dialog {
92+
--sl-z-index-dialog: 1500 !important; /* Force high z-index */
93+
}
94+
95+
/* Content specific transparency fix (Light Theme) */
96+
.link-share-box {
97+
background-color: white !important; /* Ensure solid background */
98+
}
99+
100+
.link-share-box sl-input::part(base) {
101+
background-color: white !important; /* Solid background for input */
102+
}
103+
104+
/* Dark theme override */
105+
:host(.sl-theme-dark) sl-dialog::part(panel),
106+
.sl-theme-dark sl-dialog::part(panel) {
107+
background-color: #1e1e1e !important; /* Solid dark background */
108+
color: white;
109+
}
110+
111+
:host(.sl-theme-dark) sl-dialog::part(body),
112+
.sl-theme-dark sl-dialog::part(body) {
113+
background-color: #1e1e1e !important; /* Solid dark background for body */
114+
}
115+
116+
/* Z-Index Fix (Dark Theme) */
117+
:host(.sl-theme-dark) sl-dialog,
118+
.sl-theme-dark sl-dialog {
119+
--sl-z-index-dialog: 1500 !important; /* Force high z-index in dark theme */
120+
}
121+
122+
/* Content specific transparency fix (Dark Theme) */
123+
:host(.sl-theme-dark) .link-share-box,
124+
.sl-theme-dark .link-share-box {
125+
background-color: #1e1e1e !important; /* Solid dark background */
126+
}
127+
128+
:host(.sl-theme-dark) .link-share-box sl-input::part(base),
129+
.sl-theme-dark .link-share-box sl-input::part(base) {
130+
background-color: #121212 !important; /* Solid dark background for input */
131+
}
132+
133+
.qr-code-box {
134+
border: 1px solid var(--border-color);
135+
border-radius: var(--border-radius, 4px);
136+
padding: var(--content-padding-large, 24px);
137+
display: flex;
138+
flex-direction: column;
139+
align-items: center;
140+
gap: var(--content-padding-large);
141+
width: 300px; /* Larger fixed size */
142+
height: 300px; /* Square */
143+
margin: 0 auto; /* Center horizontally */
144+
background-color: var(--color-background); /* Solid background */
145+
}
146+
147+
.link-share-box {
148+
display: flex;
149+
flex-direction: column; /* Change to column to stack title and row */
150+
gap: var(--spacing-medium);
151+
width: 100%;
152+
margin-top: var(--content-padding-large, 24px);
153+
background-color: var(--color-background); /* Solid background */
154+
padding: var(--content-padding);
155+
border-radius: var(--border-radius);
156+
}
157+
158+
.input-button-row {
159+
display: flex;
160+
flex-direction: row;
161+
align-items: center;
162+
gap: var(--spacing-medium);
163+
width: 100%;
164+
}
165+
166+
.input-button-row sl-input {
167+
flex-grow: 1;
168+
}
169+
170+
.input-button-row sl-input::part(base) {
171+
background-color: var(--background-primary, #121212); /* Follow theme */
172+
color: var(--text-color, #ffffff);
173+
}
174+
175+
76176
`;
77177

78178
openSavedSearch(
@@ -207,24 +307,15 @@ export class WebstatusSavedSearchControls extends LitElement {
207307
savedSearch: UserSavedSearch,
208308
): TemplateResult {
209309
const isOwner = savedSearch.permissions?.role === BookmarkOwnerRole;
210-
const shareableUrl = `${this._getOrigin()}${this._formatOverviewPageUrl(this.location, {q: `saved:${savedSearch.id}`})}`;
211310
return html`
212-
<sl-copy-button
213-
value="${shareableUrl}"
214-
copy-label="Copy saved search URL to clipboard"
215-
success-label="Copied"
216-
error-label="Whoops, your browser doesn't support this!"
217-
><sl-icon-button
218-
slot="copy-icon"
219-
name="share"
220-
label="Copy"
221-
></sl-icon-button
222-
><sl-icon-button
223-
slot="success-icon"
224-
name="share-fill"
225-
label="Copy Success"
226-
></sl-icon-button>
227-
</sl-copy-button>
311+
<sl-icon-button
312+
name="share"
313+
label="Share"
314+
@click=${() => {
315+
const shareableUrl = `${this._getOrigin()}${this._formatOverviewPageUrl(this.location, {q: `saved:${savedSearch.id}`})}`;
316+
openShareDialog(savedSearch, shareableUrl);
317+
}}
318+
></sl-icon-button>
228319
${this.renderBookmarkControl(savedSearch, isOwner)}
229320
${isOwner
230321
? html`
@@ -252,6 +343,10 @@ export class WebstatusSavedSearchControls extends LitElement {
252343
`;
253344
}
254345

346+
347+
348+
349+
255350
render() {
256351
return html`
257352
<div slot="anchor" class="popup-anchor saved-search-controls"></div>

0 commit comments

Comments
 (0)