Skip to content

Commit

Permalink
feat: Make collection public (#2208)
Browse files Browse the repository at this point in the history
  • Loading branch information
SuaYoo committed Dec 16, 2024
1 parent 5e7bc26 commit efc3e1d
Show file tree
Hide file tree
Showing 26 changed files with 1,109 additions and 442 deletions.
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@prettier/plugin-xml": "^3.4.1",
"@rollup/plugin-commonjs": "^18.0.0",
"@shoelace-style/localize": "^3.2.1",
"@shoelace-style/shoelace": "~2.15.1",
"@shoelace-style/shoelace": "~2.18.0",
"@tailwindcss/container-queries": "^0.1.1",
"@types/color": "^3.0.2",
"@types/diff": "^5.0.9",
Expand Down
53 changes: 13 additions & 40 deletions frontend/src/components/ui/copy-button.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { localized, msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { customElement, property } from "lit/decorators.js";

import { TailwindElement } from "@/classes/TailwindElement";
import { ClipboardController } from "@/controllers/clipboard";

/**
* Copy text to clipboard on click
Expand All @@ -16,7 +17,7 @@ import { TailwindElement } from "@/classes/TailwindElement";
* <btrix-copy-button .getValue=${() => value}></btrix-copy-button>
* ```
*
* @event on-copied
* @fires btrix-copied
*/
@localized()
@customElement("btrix-copy-button")
Expand All @@ -42,31 +43,17 @@ export class CopyButton extends TailwindElement {
@property({ type: String })
size: "x-small" | "small" | "medium" = "small";

@state()
private isCopied = false;

timeoutId?: number;

static copyToClipboard(value: string) {
void navigator.clipboard.writeText(value);
}

disconnectedCallback() {
window.clearTimeout(this.timeoutId);
super.disconnectedCallback();
}
private readonly clipboardController = new ClipboardController(this);

render() {
return html`
<sl-tooltip
content=${this.isCopied
? msg("Copied to clipboard!")
content=${this.clipboardController.isCopied
? ClipboardController.text.copied
: this.content
? this.content
: msg("Copy")}
: ClipboardController.text.copy}
?hoist=${this.hoist}
@sl-hide=${this.stopProp}
@sl-after-hide=${this.stopProp}
>
<btrix-button
size=${this.size}
Expand All @@ -76,7 +63,11 @@ export class CopyButton extends TailwindElement {
?raised=${this.raised}
>
<sl-icon
name=${this.isCopied ? "check-lg" : this.name ? this.name : "copy"}
name=${this.clipboardController.isCopied
? "check-lg"
: this.name
? this.name
: "copy"}
label=${msg("Copy to clipboard")}
class="size-3.5"
></sl-icon>
Expand All @@ -87,25 +78,7 @@ export class CopyButton extends TailwindElement {

private onClick() {
const value = (this.getValue ? this.getValue() : this.value) || "";
CopyButton.copyToClipboard(value);

this.isCopied = true;

this.dispatchEvent(new CustomEvent("on-copied", { detail: value }));

this.timeoutId = window.setTimeout(() => {
this.isCopied = false;
const button = this.shadowRoot?.querySelector("sl-icon-button");
button?.blur(); // Remove focus from the button to set it back to its default state
}, 3000);
}

/**
* Stop propgation of sl-tooltip events.
* Prevents bug where sl-dialog closes when tooltip closes
* https://github.com/shoelace-style/shoelace/issues/170
*/
private stopProp(e: Event) {
e.stopPropagation();
void this.clipboardController.copy(value);
}
}
19 changes: 11 additions & 8 deletions frontend/src/components/ui/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,20 @@ export class Dialog extends SlDialog {
// optionally re-emitting them as "sl-inner-hide" events
protected createRenderRoot() {
const root = super.createRenderRoot();
root.addEventListener("sl-hide", (event: Event) => {
if (!(event.target instanceof Dialog)) {
event.stopPropagation();
if (this.reEmitInnerSlHideEvents) {
this.dispatchEvent(new CustomEvent("sl-inner-hide", { ...event }));
}
}
});
root.addEventListener("sl-hide", this.handleSlEvent);
root.addEventListener("sl-after-hide", this.handleSlEvent);
return root;
}

private readonly handleSlEvent = (event: Event) => {
if (!(event.target instanceof Dialog)) {
event.stopPropagation();
if (this.reEmitInnerSlHideEvents) {
this.dispatchEvent(new CustomEvent("sl-inner-hide", { ...event }));
}
}
};

/**
* Submit form using external buttons to bypass
* incorrect `getRootNode` in Chrome.
Expand Down
61 changes: 61 additions & 0 deletions frontend/src/controllers/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { msg } from "@lit/localize";
import type { ReactiveController, ReactiveControllerHost } from "lit";

export type CopiedEventDetail = string;

export interface CopiedEventMap {
"btrix-copied": CustomEvent<CopiedEventDetail>;
}

/**
* Copy to clipboard
*
* @fires btrix-copied
*/
export class ClipboardController implements ReactiveController {
static readonly text = {
copy: msg("Copy"),
copied: msg("Copied"),
};

static copyToClipboard(value: string) {
void navigator.clipboard.writeText(value);
}

private readonly host: ReactiveControllerHost & EventTarget;

private timeoutId?: number;

isCopied = false;

constructor(host: ClipboardController["host"]) {
this.host = host;
host.addController(this);
}

hostConnected() {}

hostDisconnected() {
window.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}

async copy(value: string) {
ClipboardController.copyToClipboard(value);

this.isCopied = true;

this.timeoutId = window.setTimeout(() => {
this.isCopied = false;
this.host.requestUpdate();
}, 3000);

this.host.requestUpdate();

await this.host.updateComplete;

this.host.dispatchEvent(
new CustomEvent<CopiedEventDetail>("btrix-copied", { detail: value }),
);
}
}
38 changes: 17 additions & 21 deletions frontend/src/features/collections/collection-metadata-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { when } from "lit/directives/when.js";
import { BtrixElement } from "@/classes/BtrixElement";
import type { Dialog } from "@/components/ui/dialog";
import type { MarkdownEditor } from "@/components/ui/markdown-editor";
import type { SelectCollectionAccess } from "@/features/collections/select-collection-access";
import { CollectionAccess, type Collection } from "@/types/collection";
import { isApiError } from "@/utils/api";
import { maxLengthValidator } from "@/utils/form";
Expand Down Expand Up @@ -43,10 +44,20 @@ export class CollectionMetadataDialog extends BtrixElement {
@query("btrix-markdown-editor")
private readonly descriptionEditor?: MarkdownEditor | null;

@query("btrix-select-collection-access")
private readonly selectCollectionAccess?: SelectCollectionAccess | null;

@queryAsync("#collectionForm")
private readonly form!: Promise<HTMLFormElement>;

private readonly validateNameMax = maxLengthValidator(50);

protected firstUpdated(): void {
if (this.open) {
this.isDialogVisible = true;
}
}

render() {
return html` <btrix-dialog
label=${this.collection
Expand Down Expand Up @@ -126,23 +137,7 @@ export class CollectionMetadataDialog extends BtrixElement {
!this.collection,
() => html`
<sl-divider></sl-divider>
<label>
<sl-switch name="isPublic"
>${msg("Publicly Accessible")}</sl-switch
>
<sl-tooltip
content=${msg(
"Enable public access to make Collections shareable. Only people with the shared link can view your Collection.",
)}
hoist
@sl-hide=${this.stopProp}
@sl-after-hide=${this.stopProp}
><sl-icon
class="ml-1 inline-block align-middle text-slate-500"
name="info-circle"
></sl-icon
></sl-tooltip>
</label>
<btrix-select-collection-access></btrix-select-collection-access>
`,
)}
Expand Down Expand Up @@ -172,17 +167,18 @@ export class CollectionMetadataDialog extends BtrixElement {
return;
}

const { name, isPublic } = serialize(form);
const { name } = serialize(form);
const description = this.descriptionEditor.value;

this.isSubmitting = true;
try {
const body = JSON.stringify({
name,
description,
access: !isPublic
? CollectionAccess.Private
: CollectionAccess.Unlisted,
access:
this.selectCollectionAccess?.value ||
this.collection?.access ||
CollectionAccess.Private,
});
let path = `/orgs/${this.orgId}/collections`;
let method = "POST";
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/features/collections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ import("./collections-add");
import("./collection-items-dialog");
import("./collection-metadata-dialog");
import("./collection-workflow-list");
import("./select-collection-access");
import("./share-collection");
95 changes: 95 additions & 0 deletions frontend/src/features/collections/select-collection-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { localized, msg } from "@lit/localize";
import type { SlIcon, SlSelectEvent } from "@shoelace-style/shoelace";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";

import { BtrixElement } from "@/classes/BtrixElement";
import { CollectionAccess } from "@/types/collection";

@localized()
@customElement("btrix-select-collection-access")
export class SelectCollectionAccess extends BtrixElement {
static readonly Options: Record<
CollectionAccess,
{ label: string; icon: NonNullable<SlIcon["name"]>; detail: string }
> = {
[CollectionAccess.Private]: {
label: msg("Private"),
icon: "lock",
detail: msg("Only org members can view"),
},
[CollectionAccess.Unlisted]: {
label: msg("Unlisted"),
icon: "link-45deg",
detail: msg("Only people with the link can view"),
},

[CollectionAccess.Public]: {
label: msg("Public"),
icon: "globe2",
detail: msg("Anyone can view on the org's public profile"),
},
};

@property({ type: String })
value: CollectionAccess = CollectionAccess.Private;

@property({ type: Boolean })
readOnly = false;

render() {
const selected = SelectCollectionAccess.Options[this.value];

if (this.readOnly) {
return html`
<sl-input label=${msg("Visibility")} readonly value=${selected.label}>
<sl-icon slot="prefix" name=${selected.icon}></sl-icon>
<span slot="suffix">${selected.detail}</span>
</sl-input>
`;
}

return html`
<div>
<div class="form-label" id="collectionAccessLabel">
${msg("Visibility")}
</div>
<sl-dropdown
class="block w-full"
aria-labelledby="collectionAccessLabel"
hoist
sync="width"
@sl-select=${(e: SlSelectEvent) => {
const { value } = e.detail.item;
this.value = value as CollectionAccess;
}}
>
<sl-button slot="trigger" size="large" class="button-card" caret>
<sl-icon
slot="prefix"
name=${selected.icon}
class="size-6"
></sl-icon>
<span>${selected.label}</span>
<div class="font-normal text-neutral-500">${selected.detail}</div>
</sl-button>
<sl-menu>
${Object.entries(SelectCollectionAccess.Options).map(
([value, { label, icon, detail }]) => html`
<sl-menu-item
value=${value}
type="checkbox"
?checked=${label === selected.label}
>
<sl-icon slot="prefix" name=${icon}></sl-icon>
<span class="font-medium">${label}</span>
<span slot="suffix" class="text-neutral-500">${detail}</span>
</sl-menu-item>
`,
)}
</sl-menu>
</sl-dropdown>
</div>
`;
}
}
Loading

0 comments on commit efc3e1d

Please sign in to comment.