Favorites frontend - Added a dialog to capture favorite inputs from user - Added a button to Add favorite on landing page Bug: b/344960153, b/344960015 Change-Id: If852c8afea6612e18fb025f2b301ac768d7b4249 Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/865882 Commit-Queue: Vidit Chitkara <viditchitkara@google.com> Reviewed-by: Ashwin Verleker <ashwinpv@google.com>
diff --git a/perf/go/config/config.go b/perf/go/config/config.go index d2fb059..32e980c 100644 --- a/perf/go/config/config.go +++ b/perf/go/config/config.go
@@ -772,6 +772,9 @@ } type FavoritesSectionLinkConfig struct { + // Id of a user's personalized favorite + Id string `json:"id,omitempty"` + // Text to display on the link Text string `json:"text"`
diff --git a/perf/go/config/validate/instanceConfigSchema.json b/perf/go/config/validate/instanceConfigSchema.json index 323da8e..4ad83e2 100644 --- a/perf/go/config/validate/instanceConfigSchema.json +++ b/perf/go/config/validate/instanceConfigSchema.json
@@ -140,6 +140,9 @@ }, "FavoritesSectionLinkConfig": { "properties": { + "id": { + "type": "string" + }, "text": { "type": "string" },
diff --git a/perf/go/frontend/frontend.go b/perf/go/frontend/frontend.go index fa805da..4e375f0 100644 --- a/perf/go/frontend/frontend.go +++ b/perf/go/frontend/frontend.go
@@ -2188,6 +2188,7 @@ for _, favorite := range favsFromDb { favoriteList = append(favoriteList, config.FavoritesSectionLinkConfig{ + Id: favorite.ID, Text: favorite.Name, Href: favorite.Url, Description: favorite.Description,
diff --git a/perf/modules/explore-multi-sk/BUILD.bazel b/perf/modules/explore-multi-sk/BUILD.bazel index e5d1515..2f26bf7 100644 --- a/perf/modules/explore-multi-sk/BUILD.bazel +++ b/perf/modules/explore-multi-sk/BUILD.bazel
@@ -7,6 +7,8 @@ "//perf/modules/explore-simple-sk", "//perf/modules/test-picker-sk", "//golden/modules/pagination-sk", + "//perf/modules/favorites-dialog-sk", + "//infra-sk/modules/alogin-sk", ], ts_deps = [ "//elements-sk/modules:define_ts_lib", @@ -18,6 +20,8 @@ "//:node_modules/lit-html", "//infra-sk/modules:jsonorthrow_ts_lib", "//perf/modules/json:index_ts_lib", + "//infra-sk/modules:dom_ts_lib", + "//infra-sk/modules/json:index_ts_lib", ], ts_srcs = [ "explore-multi-sk.ts",
diff --git a/perf/modules/explore-multi-sk/explore-multi-sk.scss b/perf/modules/explore-multi-sk/explore-multi-sk.scss index 7d33da7..35157b3 100644 --- a/perf/modules/explore-multi-sk/explore-multi-sk.scss +++ b/perf/modules/explore-multi-sk/explore-multi-sk.scss
@@ -24,4 +24,7 @@ #add-graph-button.hidden { display: none; } + #favBtn { + margin: 16px 0 0 16px; + } }
diff --git a/perf/modules/explore-multi-sk/explore-multi-sk.ts b/perf/modules/explore-multi-sk/explore-multi-sk.ts index 6997e1b..46fe9c8 100644 --- a/perf/modules/explore-multi-sk/explore-multi-sk.ts +++ b/perf/modules/explore-multi-sk/explore-multi-sk.ts
@@ -30,14 +30,18 @@ import { HintableObject } from '../../../infra-sk/modules/hintable'; import { errorMessage } from '../errorMessage'; import { ElementSk } from '../../../infra-sk/modules/ElementSk'; +import { QueryConfig } from '../json'; import '../explore-simple-sk'; +import '../favorites-dialog-sk'; import '../test-picker-sk'; import '../../../golden/modules/pagination-sk/pagination-sk'; -import { QueryConfig } from '../json'; - +import { $$ } from '../../../infra-sk/modules/dom'; import { jsonOrThrow } from '../../../infra-sk/modules/jsonOrThrow'; +import { LoggedIn } from '../../../infra-sk/modules/alogin-sk/alogin-sk'; +import { Status as LoginStatus } from '../../../infra-sk/modules/json'; +import { FavoritesDialogSk } from '../favorites-dialog-sk/favorites-dialog-sk'; import { PaginationSkPageChangedEventDetail } from '../../../golden/modules/pagination-sk/pagination-sk'; class State { @@ -93,6 +97,8 @@ private defaults: QueryConfig | null = null; + private userEmail: string = ''; + constructor() { super(ExploreMultiSk.template); } @@ -151,8 +157,24 @@ this.updateButtons(); } ); + + LoggedIn() + .then((status: LoginStatus) => { + this.userEmail = status.email; + this._render(); + }) + .catch(errorMessage); } + private canAddFav(): boolean { + return this.userEmail !== null && this.userEmail !== ''; + } + + private openAddFavoriteDialog = async () => { + const d = $$<FavoritesDialogSk>('#fav-dialog', this) as FavoritesDialogSk; + await d!.open(); + }; + private static template = (ele: ExploreMultiSk) => html` <div id="menu"> <h1>MultiGraph Menu</h1> @@ -184,6 +206,15 @@ title="Merge all graphs into a single graph."> Merge Graphs </button> + <button + id="favBtn" + ?disabled=${!ele.canAddFav()} + @click=${() => { + ele.openAddFavoriteDialog(); + }}> + Add to Favorites + </button> + <favorites-dialog-sk id="fav-dialog"></favorites-dialog-sk> <test-picker-sk id="test-picker" class="hidden"></test-picker-sk> </div> <hr />
diff --git a/perf/modules/explore-sk/BUILD.bazel b/perf/modules/explore-sk/BUILD.bazel index ab3d0fe..55d84b8 100644 --- a/perf/modules/explore-sk/BUILD.bazel +++ b/perf/modules/explore-sk/BUILD.bazel
@@ -4,6 +4,8 @@ name = "explore-sk", sk_element_deps = [ "//perf/modules/explore-simple-sk", + "//perf/modules/favorites-dialog-sk", + "//infra-sk/modules/alogin-sk", ], ts_deps = [ "//elements-sk/modules:define_ts_lib", @@ -13,6 +15,9 @@ "//:node_modules/lit-html", "//infra-sk/modules:jsonorthrow_ts_lib", "//perf/modules/json:index_ts_lib", + "//infra-sk/modules:dom_ts_lib", + "//infra-sk/modules/json:index_ts_lib", + "//perf/modules/errorMessage:index_ts_lib", ], ts_srcs = [ "explore-sk.ts",
diff --git a/perf/modules/explore-sk/explore-sk-demo.scss b/perf/modules/explore-sk/explore-sk-demo.scss index 9c32d21..668dda3 100644 --- a/perf/modules/explore-sk/explore-sk-demo.scss +++ b/perf/modules/explore-sk/explore-sk-demo.scss
@@ -1,4 +1,8 @@ explore-sk { display: block; height: 1400px; + + button { + margin: 16px 0 0 16px; + } }
diff --git a/perf/modules/explore-sk/explore-sk.ts b/perf/modules/explore-sk/explore-sk.ts index 540cc89..0540af2 100644 --- a/perf/modules/explore-sk/explore-sk.ts +++ b/perf/modules/explore-sk/explore-sk.ts
@@ -14,6 +14,12 @@ import { jsonOrThrow } from '../../../infra-sk/modules/jsonOrThrow'; import '../explore-simple-sk'; +import '../favorites-dialog-sk'; +import { FavoritesDialogSk } from '../favorites-dialog-sk/favorites-dialog-sk'; +import { $$ } from '../../../infra-sk/modules/dom'; +import { LoggedIn } from '../../../infra-sk/modules/alogin-sk/alogin-sk'; +import { Status as LoginStatus } from '../../../infra-sk/modules/json'; +import { errorMessage } from '../errorMessage'; export class ExploreSk extends ElementSk { private exploreSimpleSk: ExploreSimpleSk | null = null; @@ -24,6 +30,8 @@ private defaults: QueryConfig | null = null; + private userEmail: string = ''; + constructor() { super(ExploreSk.template); } @@ -58,17 +66,35 @@ this.showMultiViewButton = true; this._render(); }); + + LoggedIn() + .then((status: LoginStatus) => { + this.userEmail = status.email; + }) + .catch(errorMessage); } + private openAddFavoriteDialog = async () => { + const d = $$<FavoritesDialogSk>('#fav-dialog', this) as FavoritesDialogSk; + await d!.open(); + }; + private static template = (ele: ExploreSk) => html` <div ?hidden=${!ele.showMultiViewButton}> + <favorites-dialog-sk id="fav-dialog"></favorites-dialog-sk> <button - style="margin: 16px 0 0 16px;" @click=${() => { ele.exploreSimpleSk?.viewMultiGraph(); }}> View in multi-graph </button> + <button + ?disabled=${!ele.userEmail || ele.userEmail === ''} + @click=${() => { + ele.openAddFavoriteDialog(); + }}> + Add to Favorites + </button> </div> <explore-simple-sk></explore-simple-sk> `;
diff --git a/perf/modules/favorites-dialog-sk/BUILD.bazel b/perf/modules/favorites-dialog-sk/BUILD.bazel new file mode 100644 index 0000000..4c82f79 --- /dev/null +++ b/perf/modules/favorites-dialog-sk/BUILD.bazel
@@ -0,0 +1,68 @@ +load( + "//infra-sk:index.bzl", + "karma_test", + "sk_demo_page_server", + "sk_element", + "sk_element_puppeteer_test", + "sk_page", +) + +sk_demo_page_server( + name = "demo_page_server", + sk_page = ":favorites-dialog-sk-demo", +) + +sk_element( + name = "favorites-dialog-sk", + sass_deps = [ + "//perf/modules/themes:themes_sass_lib", + ], + sass_srcs = ["favorites-dialog-sk.scss"], + sk_element_deps = [ + "//elements-sk/modules/spinner-sk", + ], + ts_deps = [ + "//:node_modules/lit-html", + "//infra-sk/modules/ElementSk:index_ts_lib", + "//elements-sk/modules:define_ts_lib", + "//elements-sk/modules:errormessage_ts_lib", + "//infra-sk/modules:dom_ts_lib", + ], + ts_srcs = [ + "favorites-dialog-sk.ts", + "index.ts", + ], + visibility = ["//visibility:public"], +) + +sk_page( + name = "favorites-dialog-sk-demo", + html_file = "favorites-dialog-sk-demo.html", + sk_element_deps = [":favorites-dialog-sk"], + ts_deps = [ + "//infra-sk/modules:dom_ts_lib", + ], + ts_entry_point = "favorites-dialog-sk-demo.ts", +) + +sk_element_puppeteer_test( + name = "favorites-dialog-sk_puppeteer_test", + src = "favorites-dialog-sk_puppeteer_test.ts", + sk_demo_page_server = ":demo_page_server", + deps = [ + "//:node_modules/@types/chai", + "//:node_modules/chai", + "//puppeteer-tests:util_ts_lib", + ], +) + +karma_test( + name = "favorites-dialog-sk_test", + src = "favorites-dialog-sk_test.ts", + deps = [ + ":favorites-dialog-sk", + "//:node_modules/@types/chai", + "//:node_modules/chai", + "//infra-sk/modules:test_util_ts_lib", + ], +)
diff --git a/perf/modules/favorites-dialog-sk/favorites-dialog-sk-demo.html b/perf/modules/favorites-dialog-sk/favorites-dialog-sk-demo.html new file mode 100644 index 0000000..3e8a759 --- /dev/null +++ b/perf/modules/favorites-dialog-sk/favorites-dialog-sk-demo.html
@@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <title>favorites-dialog-sk</title> + <meta charset="utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> +</head> +<body class="body-sk"> + <h1>favorites-dialog-sk</h1> + <favorites-dialog-sk></favorites-dialog-sk> + <section> + <h2>Events</h2> + <button id="newFav">New Favorite</button> + <button id="editFav">Edit Favorite</button> + </section> +</body> +</html>
diff --git a/perf/modules/favorites-dialog-sk/favorites-dialog-sk-demo.ts b/perf/modules/favorites-dialog-sk/favorites-dialog-sk-demo.ts new file mode 100644 index 0000000..7381123 --- /dev/null +++ b/perf/modules/favorites-dialog-sk/favorites-dialog-sk-demo.ts
@@ -0,0 +1,14 @@ +import { $$ } from '../../../infra-sk/modules/dom'; +import { FavoritesDialogSk } from './favorites-dialog-sk'; +import './index'; + +const elem: FavoritesDialogSk | null = document.querySelector( + 'favorites-dialog-sk' +); +$$('#newFav')!.addEventListener('click', () => { + elem!.open('', '', '', 'a.com'); +}); + +$$('#editFav')!.addEventListener('click', () => { + elem!.open('1234', 'Fav', 'Fav Desc', 'b.com'); +});
diff --git a/perf/modules/favorites-dialog-sk/favorites-dialog-sk.scss b/perf/modules/favorites-dialog-sk/favorites-dialog-sk.scss new file mode 100644 index 0000000..dd9610f --- /dev/null +++ b/perf/modules/favorites-dialog-sk/favorites-dialog-sk.scss
@@ -0,0 +1,37 @@ +@import '../themes/themes.scss'; + +favorites-dialog-sk { + dialog { + padding: 16px; + border: solid var(--on-background) 1px; + color: var(--on-background); + background: var(--background); + padding: 24px; + + h2 { + font-size: 2em !important; + margin-bottom: 40px !important; + } + + & > span { + display: inline-block; + width: 100px; + + label { + font-size: 1.2em !important; + } + } + + & > input { + margin-bottom: 20px; + width: 500px; + margin-left: 12px; + font-size: 1.2em !important; + padding: 6px; + } + + button { + font-size: 1.2em !important; + } + } +}
diff --git a/perf/modules/favorites-dialog-sk/favorites-dialog-sk.ts b/perf/modules/favorites-dialog-sk/favorites-dialog-sk.ts new file mode 100644 index 0000000..3a1d8e2 --- /dev/null +++ b/perf/modules/favorites-dialog-sk/favorites-dialog-sk.ts
@@ -0,0 +1,191 @@ +/** + * @module modules/favorites-dialog-sk + * @description <h2><code>favorites-dialog-sk</code></h2> + * + * This module is a modal that contains a form to capture user + * input for adding/editing a new favorite. + */ +import { html } from 'lit-html'; +import { ElementSk } from '../../../infra-sk/modules/ElementSk'; +import { $$ } from '../../../infra-sk/modules/dom'; +import { define } from '../../../elements-sk/modules/define'; +import { errorMessage } from '../../../elements-sk/modules/errorMessage'; +import '../../../elements-sk/modules/spinner-sk'; + +// FavoritesDialogSk is a modal that contains a form to capture user +// input for adding/editing a new favorite. +export class FavoritesDialogSk extends ElementSk { + favId: string = ''; + + name: string = ''; + + description: string = ''; + + url: string = ''; + + private dialog: HTMLDialogElement | null = null; + + private updatingFavorite: boolean = false; + + private resolve: ((value?: any) => void) | null = null; + + private reject: ((value?: any) => void) | null = null; + + constructor() { + super(FavoritesDialogSk.template); + } + + connectedCallback(): void { + super.connectedCallback(); + this._render(); + this.dialog = $$('dialog', this); + } + + private dismiss(): void { + this.dialog!.close(); + this.reject!(); + } + + private async confirm(): Promise<void> { + if (this.name === '' || this.description === '' || this.url === '') { + errorMessage('All the fields must be non empty'); + return; + } + + let apiUrl = '/_/favorites/new'; + let body: { + id?: string; + name: string; + description: string; + url: string; + } = { + name: this.name, + description: this.description, + url: this.url, + }; + if (this.favId !== '') { + body = { ...body, id: this.favId }; + apiUrl = '/_/favorites/edit'; + } + + try { + this.updatingFavorite = true; + this._render(); + const resp = await fetch(apiUrl, { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!resp.ok) { + const msg = await resp.text(); + errorMessage(`${resp.statusText}: ${msg}`); + } + } finally { + this.updatingFavorite = false; + this.dialog!.close(); + this.resolve!(); + } + } + + // open shows the popup dialog when called. + public open( + favId?: string, + name?: string, + description?: string, + url?: string + ): Promise<void> { + this.favId = favId || ''; + this.name = name || ''; + this.description = description || ''; + this.url = url || window.location.href; + + this._render(); + this.dialog!.showModal(); + + // If the dialog closes it could be due to 2 reasons: + // 1: User pressed on close + // 2: The favorite got added/edited. + // In this module, we want to re-fetch the favorites when the dialog is closed + // but we only want to re-fetch if closed due to reason 2. + // So we're using the reject function when the user presses on close dialog + // which is eventually used in favorites-sk to decide if it wants to + // re-fetch the favorites or not. + return new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + + private filterName(e: Event): void { + this.name = (e.target as HTMLInputElement).value; + this._render(); + } + + private filterDescription(e: Event): void { + this.description = (e.target as HTMLInputElement).value; + this._render(); + } + + private filterUrl(e: Event): void { + this.url = (e.target as HTMLInputElement).value; + this._render(); + } + + private static template = (ele: FavoritesDialogSk) => html` + <dialog id="favDialog"> + <h2>Favorite</h2> + + <div id=spinContainer> + <spinner-sk ?active=${ele.updatingFavorite}></spinner-sk> + </div> + + <span class="label"> + <label>Name*</label> + </span> + <input + id="name" + placeholder="Name" + .value="${ele.name}" + @input=${(e: Event) => ele.filterName(e)}> + </input> + <br/> + + <span class="label"> + <label>Description*</label> + </span> + <input + id="desc" + placeholder="Description" + .value="${ele.description}" + @input=${(e: Event) => ele.filterDescription(e)}></input> + <br/> + + <span class="label"> + <label>URL*</label> + </span> + <input + id="url" + placeholder="URL" + value="${ele.url}" + @input=${(e: Event) => ele.filterUrl(e)}></input> + <br/><br/> + + <div ?hidden="${!ele.updatingFavorite}"> + Working on it... + </div> + + <div class="buttons"> + <button ?disabled="${ele.updatingFavorite}" @click=${ + ele.dismiss + }>Cancel</button> + <button ?disabled="${ele.updatingFavorite}" @click=${ + ele.confirm + }>Save</button> + </div> + </dialog>`; +} + +define('favorites-dialog-sk', FavoritesDialogSk);
diff --git a/perf/modules/favorites-dialog-sk/favorites-dialog-sk_puppeteer_test.ts b/perf/modules/favorites-dialog-sk/favorites-dialog-sk_puppeteer_test.ts new file mode 100644 index 0000000..3e5b800 --- /dev/null +++ b/perf/modules/favorites-dialog-sk/favorites-dialog-sk_puppeteer_test.ts
@@ -0,0 +1,34 @@ +import { expect } from 'chai'; +import { + loadCachedTestBed, + takeScreenshot, + TestBed, +} from '../../../puppeteer-tests/util'; + +describe('favorites-dialog-sk', () => { + let testBed: TestBed; + before(async () => { + testBed = await loadCachedTestBed(); + }); + + beforeEach(async () => { + await testBed.page.goto(testBed.baseUrl); + await testBed.page.setViewport({ width: 400, height: 550 }); + }); + + it('should render the demo page (smoke test)', async () => { + expect(await testBed.page.$$('favorites-dialog-sk')).to.have.length(1); + }); + + describe('screenshots', () => { + it('shows dialog for new favorite', async () => { + await testBed.page.click('#newFav'); + await takeScreenshot(testBed.page, 'perf', 'favorites-dialog-sk'); + }); + + it('shows dialog for editing an existing favorite', async () => { + await testBed.page.click('#editFav'); + await takeScreenshot(testBed.page, 'perf', 'favorites-dialog-sk'); + }); + }); +});
diff --git a/perf/modules/favorites-dialog-sk/favorites-dialog-sk_test.ts b/perf/modules/favorites-dialog-sk/favorites-dialog-sk_test.ts new file mode 100644 index 0000000..daef56b --- /dev/null +++ b/perf/modules/favorites-dialog-sk/favorites-dialog-sk_test.ts
@@ -0,0 +1,40 @@ +import './index'; +import { expect } from 'chai'; +import { FavoritesDialogSk } from './favorites-dialog-sk'; + +import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util'; + +describe('favorites-dialog-sk', () => { + const newInstance = setUpElementUnderTest<FavoritesDialogSk>( + 'favorites-dialog-sk' + ); + + let element: FavoritesDialogSk; + beforeEach(() => { + element = newInstance((el: FavoritesDialogSk) => { + // Place here any code that must run after the element is instantiated but + // before it is attached to the DOM (e.g. property setter calls, + // document-level event listeners, etc.). + }); + }); + + it('renders for new', async () => { + expect(element).to.not.be.null; + element.open('12345', '', '', 'url1.com').then(() => { + const n = document.getElementById('name'); + expect(n?.nodeValue).to.be.equal(''); + expect(n?.nodeValue).to.be.equal(''); + expect(n?.nodeValue).to.be.equal('url1.com'); + }); + }); + + it('renders for update', async () => { + expect(element).to.not.be.null; + element.open('', 'Fav', 'Fav Desc', 'url.com').then(() => { + const n = document.getElementById('name'); + expect(n?.nodeValue).to.be.equal('Fav'); + expect(n?.nodeValue).to.be.equal('Fav Desc'); + expect(n?.nodeValue).to.be.equal('url.com'); + }); + }); +});
diff --git a/perf/modules/favorites-dialog-sk/index.ts b/perf/modules/favorites-dialog-sk/index.ts new file mode 100644 index 0000000..34c14ca --- /dev/null +++ b/perf/modules/favorites-dialog-sk/index.ts
@@ -0,0 +1 @@ +import './favorites-dialog-sk';
diff --git a/perf/modules/favorites-sk/BUILD.bazel b/perf/modules/favorites-sk/BUILD.bazel index 0bf6692..9e0e180 100644 --- a/perf/modules/favorites-sk/BUILD.bazel +++ b/perf/modules/favorites-sk/BUILD.bazel
@@ -3,6 +3,10 @@ sk_element( name = "favorites-sk", sass_srcs = ["favorites-sk.scss"], + sk_element_deps = [ + "//elements-sk/modules/icons/delete-icon-sk", + "//perf/modules/favorites-dialog-sk", + ], ts_deps = [ "//infra-sk/modules/ElementSk:index_ts_lib", "//infra-sk/modules:jsonorthrow_ts_lib", @@ -11,6 +15,7 @@ "//elements-sk/modules:errormessage_ts_lib", "//perf/modules/json:index_ts_lib", "//:node_modules/lit-html", + "//infra-sk/modules:dom_ts_lib", ], ts_srcs = [ "favorites-sk.ts",
diff --git a/perf/modules/favorites-sk/favorites-sk.ts b/perf/modules/favorites-sk/favorites-sk.ts index 578c9a3..447bdc4 100644 --- a/perf/modules/favorites-sk/favorites-sk.ts +++ b/perf/modules/favorites-sk/favorites-sk.ts
@@ -15,6 +15,10 @@ import { Favorites } from '../json'; import '../window/window'; import { errorMessage } from '../../../elements-sk/modules/errorMessage'; +import '../../../elements-sk/modules/icons/delete-icon-sk'; +import '../favorites-dialog-sk'; +import { FavoritesDialogSk } from '../favorites-dialog-sk/favorites-dialog-sk'; +import { $$ } from '../../../infra-sk/modules/dom'; export class FavoritesSk extends ElementSk { private favoritesConfig: Favorites | null = null; @@ -23,6 +27,62 @@ super(FavoritesSk.template); } + private deleteFavorite = async (favId: string) => { + const body = { + id: favId, + }; + const resp = await fetch('/_/favorites/delete', { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!resp.ok) { + const msg = await resp.text(); + errorMessage(`${resp.statusText}: ${msg}`); + return; + } + + await this.fetchFavorites(); + }; + + private deleteFavoriteConfirm = async ( + id: string | undefined, + name: string + ) => { + if (id === undefined) return; + + const confirmed = window.confirm( + `Deleting favorite: ${name}. Are you sure?` + ); + if (!confirmed) { + return; + } + + this.deleteFavorite(id); + }; + + private editFavorite = async ( + id: string | undefined, + name: string, + desc: string, + url: string + ) => { + const d = $$<FavoritesDialogSk>('#fav-dialog', this) as FavoritesDialogSk; + + d! + .open(id, name, desc, url) + .then(() => { + this.fetchFavorites(); + }) + .catch((e) => { + if (e !== undefined) { + errorMessage(`${e}`); + } + }); + }; + private static template = (ele: FavoritesSk) => html` <header><h1 class="name">Favorites</h1></header> <hr /> @@ -34,38 +94,81 @@ if (sections == null || sections.length === 0) { return html`No favorites have been configured for this instance.`; } - return html`${sections.map( - (section) => - html` <div class="section"> + return html`${sections.map((section) => { + if (section.name === 'My Favorites') { + return html` <div class="section"> <h3>${section.name}</h3> <table> <tr> <th>Link</th> <th>Description</th> + <th>Actions</th> </tr> ${section.links?.map( (link) => html` <tr> <td><a href=${link.href}>${link.text}</a></td> <td>${link.description}</td> + <td> + <button + @click=${() => + this.editFavorite( + link.id, + link.text, + link.description, + link.href + )}> + Edit + </button> + <button + @click=${() => + this.deleteFavoriteConfirm(link.id, link.text)}> + Delete + </button> + </td> </tr> ` )} </table> + <favorites-dialog-sk id="fav-dialog"></favorites-dialog-sk> </div> - <hr />` - )}`; + <hr />`; + } + + return html` <div class="section"> + <h3>${section.name}</h3> + <table> + <tr> + <th>Link</th> + <th>Description</th> + </tr> + ${section.links?.map( + (link) => html` + <tr> + <td><a href=${link.href}>${link.text}</a></td> + <td>${link.description}</td> + </tr> + ` + )} + </table> + </div> + <hr />`; + })}`; } + private fetchFavorites = async () => { + const response = await fetch('/_/favorites/'); + const json = await jsonOrThrow(response); + this.favoritesConfig = json; + this._render(); + }; + async connectedCallback(): Promise<void> { super.connectedCallback(); this._render(); if (this.favoritesConfig == null) { try { - const response = await fetch('/_/favorites/'); - const json = await jsonOrThrow(response); - this.favoritesConfig = json; - this._render(); + this.fetchFavorites(); } catch (error) { errorMessage(String(error)); }
diff --git a/perf/modules/json/index.ts b/perf/modules/json/index.ts index 6a2d968..4421c9b 100644 --- a/perf/modules/json/index.ts +++ b/perf/modules/json/index.ts
@@ -83,6 +83,7 @@ } export interface FavoritesSectionLinkConfig { + id?: string; text: string; href: string; description: string;