browsertrix/frontend/src/components/select-browser-profile.ts

212 lines
5.6 KiB
TypeScript

import { html } from "lit";
import { property, state } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import orderBy from "lodash/fp/orderBy";
import type { AuthState } from "../utils/AuthService";
import LiteElement from "../utils/LiteElement";
import type { Profile } from "../pages/org/types";
import type { APIPaginatedList } from "../types/api";
/**
* Browser profile select dropdown
*
* Usage example:
* ```ts
* <btrix-select-browser-profile
* authState=${authState}
* orgId=${orgId}
* on-change=${({value}) => selectedProfile = value}
* ></btrix-select-browser-profile>
* ```
*
* @event on-change
*/
@localized()
export class SelectBrowserProfile extends LiteElement {
@property({ type: Object })
authState!: AuthState;
@property({ type: String })
orgId!: string;
@property({ type: String })
profileId?: string;
@state()
private selectedProfile?: Profile;
@state()
private browserProfiles?: Profile[];
protected firstUpdated() {
this.fetchBrowserProfiles();
}
render() {
return html`
<sl-select
name="profileid"
label=${msg("Browser Profile")}
value=${this.selectedProfile?.id || ""}
placeholder=${this.browserProfiles
? msg("Default Profile")
: msg("Loading")}
hoist
@sl-change=${this.onChange}
@sl-focus=${() => {
// Refetch to keep list up to date
this.fetchBrowserProfiles();
}}
@sl-hide=${this.stopProp}
@sl-after-hide=${this.stopProp}
>
${this.browserProfiles
? html`
<sl-option value="">${msg("Default Profile")}</sl-option>
<sl-divider></sl-divider>
`
: html` <sl-spinner slot="prefix"></sl-spinner> `}
${this.browserProfiles?.map(
(profile) => html`
<sl-option value=${profile.id}>
${profile.name}
<div slot="suffix">
<div class="text-xs">
<sl-format-date
date=${`${profile.created}Z` /** Z for UTC */}
month="2-digit"
day="2-digit"
year="2-digit"
></sl-format-date>
</div></div
></sl-option>
`
)}
${this.browserProfiles && !this.browserProfiles.length
? this.renderNoProfiles()
: ""}
</sl-select>
${this.browserProfiles && this.browserProfiles.length
? this.renderSelectedProfileInfo()
: ""}
`;
}
private renderSelectedProfileInfo() {
if (!this.selectedProfile) return;
return html`
<div
class="mt-2 border bg-neutral-50 rounded p-2 text-sm flex justify-between"
>
${this.selectedProfile.description
? html`<em class="text-slate-500"
>${this.selectedProfile.description}</em
>`
: ""}
<a
href=${`/orgs/${this.orgId}/browser-profiles/profile/${this.selectedProfile.id}`}
class="font-medium text-primary hover:text-indigo-500"
target="_blank"
>
<span class="inline-block align-middle mr-1"
>${msg("View profile")}</span
>
<sl-icon
class="inline-block align-middle"
name="box-arrow-up-right"
></sl-icon>
</a>
</div>
`;
}
private renderNoProfiles() {
return html`
<div class="mx-2 text-sm text-neutral-500">
<span class="inline-block align-middle"
>${msg("No additional browser profiles found.")}</span
>
<a
href=${`/orgs/${this.orgId}/browser-profiles?new`}
class="font-medium text-primary hover:text-indigo-500"
target="_blank"
@click=${(e: any) => {
const select = e.target.closest("sl-select");
if (select) {
select.blur();
select.dropdown?.hide();
}
}}
><span class="inline-block align-middle"
>${msg("Create a browser profile")}</span
>
<sl-icon
class="inline-block align-middle"
name="box-arrow-up-right"
></sl-icon
></a>
</div>
`;
}
private onChange(e: any) {
this.selectedProfile = this.browserProfiles?.find(
({ id }) => id === e.target.value
);
this.dispatchEvent(
new CustomEvent("on-change", {
detail: {
value: this.selectedProfile,
},
})
);
}
/**
* Fetch browser profiles and update internal state
*/
private async fetchBrowserProfiles(): Promise<void> {
try {
const data = await this.getProfiles();
this.browserProfiles = orderBy(["name", "created"])(["asc", "desc"])(
data
) as Profile[];
if (this.profileId && !this.selectedProfile) {
this.selectedProfile = this.browserProfiles.find(
({ id }) => id === this.profileId
);
}
} catch (e) {
this.notify({
message: msg("Sorry, couldn't retrieve browser profiles at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
}
private async getProfiles(): Promise<Profile[]> {
const data: APIPaginatedList = await this.apiFetch(
`/orgs/${this.orgId}/profiles`,
this.authState!
);
return data.items;
}
/**
* Stop propgation of sl-select events.
* Prevents bug where sl-dialog closes when dropdown closes
* https://github.com/shoelace-style/shoelace/issues/170
*/
private stopProp(e: CustomEvent) {
e.stopPropagation();
}
}