Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/docs/docs/user-guide/collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ A collection is a specific, user-directed grouping of either crawls or uploaded

You can create a collection from the Collections page directly, or the _Create New..._ shortcut from the org dashboard.

### Name and Summary

Choose a collection name that identifies your collection within the org. The name can contain letters, numbers, and special characters (like emojis.) The collection name should include at least one letter.

The summary is a short description that summarizes this collection. If the collection is shareable, this will appear next to the collection name. You can add a longer description in the “About” section after creating the collection.

### Add Collection Content

Collections are the primary way of organizing and combining archived items into groups for presentation. Collections also allow you to view a combined replay of any archived items they contain; if a link is present when viewing a collection but the actual page is missing, and another item with that captured page is added to the collection, the link will now work as expected.
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/ui/search-combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export class SearchCombobox<T> extends TailwindElement {
@property({ type: Boolean })
createNew = false;

@property({ attribute: false })
validateNew?: (value: string) => boolean;

private get hasSearchStr() {
return this.searchByValue.length >= MIN_SEARCH_LENGTH;
}
Expand Down Expand Up @@ -232,7 +235,8 @@ export class SearchCombobox<T> extends TailwindElement {
({ value }) =>
value && value.toLocaleLowerCase() === newName.toLocaleLowerCase(),
),
);
) &&
(!this.validateNew || this.validateNew(newName));

return html`
${when(
Expand Down
39 changes: 31 additions & 8 deletions frontend/src/features/collections/collection-create-dialog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { localized, msg, str } from "@lit/localize";
import type { SlInput, SlSelectEvent } from "@shoelace-style/shoelace";
import type {
SlInput,
SlInputEvent,
SlSelectEvent,
} from "@shoelace-style/shoelace";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import { html } from "lit";
import {
Expand All @@ -24,7 +28,8 @@ import {
type Collection,
} from "@/types/collection";
import { isApiError } from "@/utils/api";
import { maxLengthValidator } from "@/utils/form";
import { formValidator, maxLengthValidator } from "@/utils/form";
import slugifyStrict from "@/utils/slugify";

export type CollectionSavedEvent = CustomEvent<{
id: string;
Expand Down Expand Up @@ -54,6 +59,7 @@ export class CollectionCreateDialog extends BtrixElement {
@queryAsync("#collectionForm")
private readonly form!: Promise<HTMLFormElement>;

private readonly checkFormValidity = formValidator(this);
private readonly validateNameMax = maxLengthValidator(
COLLECTION_NAME_MAX_LENGTH,
);
Expand Down Expand Up @@ -119,7 +125,20 @@ export class CollectionCreateDialog extends BtrixElement {
autocomplete="off"
required
help-text=${this.validateNameMax.helpText}
@sl-input=${this.validateNameMax.validate}
@sl-input=${(e: SlInputEvent) => {
const valid = this.validateNameMax.validate(e);

if (!valid) return;

const input = e.target as SlInput;
const value = input.value;

if (value && !slugifyStrict(value)) {
input.setCustomValidity(
msg("Please include at least one letter or number."),
);
}
}}
>
</sl-input>
<sl-input
Expand All @@ -132,7 +151,7 @@ export class CollectionCreateDialog extends BtrixElement {
>
<span slot="label">
${msg("Summary")}
<sl-tooltip>
<btrix-popover placement="top-start" hoist>
<span slot="content">
${msg(
"Write a short description that summarizes this collection. If the collection is shareable, this will appear next to the collection name.",
Expand All @@ -146,7 +165,7 @@ export class CollectionCreateDialog extends BtrixElement {
name="info-circle"
style="vertical-align: -.175em"
></sl-icon>
</sl-tooltip>
</btrix-popover>
</span>
</sl-input>

Expand Down Expand Up @@ -199,19 +218,23 @@ export class CollectionCreateDialog extends BtrixElement {
event.stopPropagation();

const form = event.target as HTMLFormElement;
const nameInput = form.querySelector<SlInput>('sl-input[name="name"]');
const isValid = await this.checkFormValidity(form);

if (!nameInput?.checkValidity()) {
if (!isValid) {
return;
}

const { name, caption } = serialize(form);
const { name, caption } = serialize(form) as {
name: string;
caption?: string;
};

this.isSubmitting = true;
try {
const body = JSON.stringify({
name,
caption,
slug: slugifyStrict(name),
access: this.selectCollectionAccess?.value || CollectionAccess.Private,
defaultThumbnailName: DEFAULT_THUMBNAIL,
});
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/features/collections/collection-name-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
COLLECTION_NAME_MAX_LENGTH,
type Collection,
} from "@/types/collection";
import slugifyStrict from "@/utils/slugify";
import appState from "@/utils/state";

export type CollectionNameInputLoadedEvent =
Expand Down Expand Up @@ -98,6 +99,10 @@ export class CollectionNameInput extends WithSearchOrgContext(
}
}

readonly validateNew = (value: string) => {
return value ? slugifyStrict(value).length > 0 : false;
};

private readonly onSelect = async (e: BtrixSearchComboboxSelectEvent) => {
e.stopPropagation();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type CollectionUpdate,
} from "@/types/collection";
import { isApiError } from "@/utils/api";
import slugifyStrict from "@/utils/slugify";

export default function submitTask(
this: CollectionEdit,
Expand Down Expand Up @@ -75,12 +76,20 @@ export default function submitTask(
}

if (Object.keys(rest).length) {
const params = {
...rest,
};

if (rest.name) {
params.slug = slugifyStrict(rest.name);
}

tasks.push(
await this.api.fetch<{ updated: boolean }>(
`/orgs/${this.orgId}/collections/${this.collection.id}`,
{
method: "PATCH",
body: JSON.stringify(rest),
body: JSON.stringify(params),
signal,
},
),
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/features/crawl-workflows/workflow-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import { isNotEqual } from "@/utils/is-not-equal";
import localize from "@/utils/localize";
import { isArchivingDisabled } from "@/utils/orgs";
import { pluralOf } from "@/utils/pluralize";
import slugifyStrict from "@/utils/slugify";
import { AppStateService } from "@/utils/state";
import { regexEscape } from "@/utils/string";
import { tw } from "@/utils/tailwind";
Expand Down Expand Up @@ -3822,14 +3823,17 @@ https://archiveweb.page/images/${"logo.svg"}`}
}

private async createCollection(
params: { name: string },
{ name }: { name: string },
signal?: AbortSignal,
) {
return this.api.fetch<{ added: boolean; id: string; name: string }>(
`/orgs/${this.orgId}/collections`,
{
method: "POST",
body: JSON.stringify(params),
body: JSON.stringify({
name,
slug: slugifyStrict(name),
}),
signal,
},
);
Expand Down
32 changes: 25 additions & 7 deletions frontend/src/pages/admin/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,27 @@ export class Admin extends BtrixElement {
this.validateOrgNameMax.validate(e);

const input = e.target as SlInput;
const slug = slugifyStrict(input.value);
const isInvalid = this.orgSlugs.includes(slug);
const value = input.value;
const slug = slugifyStrict(value);
let isInvalid = !value;

if (isInvalid) {
input.setCustomValidity(msg("This org name is already taken."));
} else {
if (value) {
if (!slug) {
isInvalid = true;

input.setCustomValidity(
msg("Please include at least one letter or number."),
);
} else {
isInvalid = this.orgSlugs.includes(slug);

if (isInvalid) {
input.setCustomValidity(msg("This org name is already taken."));
}
}
}

if (!isInvalid) {
input.setCustomValidity("");
}

Expand All @@ -300,14 +315,17 @@ export class Admin extends BtrixElement {
const formEl = e.target as HTMLFormElement;
if (!(await this.checkFormValidity(formEl))) return;

const params = serialize(formEl);
const params = serialize(formEl) as { name: string };
this.isSubmittingNewOrg = true;

try {
// TODO return entire object from API
await this.api.fetch<{ added: true; id: string }>(`/orgs/create`, {
method: "POST",
body: JSON.stringify(params),
body: JSON.stringify({
...params,
slug: slugifyStrict(params.name),
}),
});
await this.fetchOrgs();
const userInfo = await this.getUserInfo();
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/pages/invite/ui/org-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,21 @@ export class OrgForm extends BtrixElement {
autocomplete="off"
value=${this.slug}
minlength="2"
maxlength="30"
maxlength="50"
help-text=${helpText(this.slug)}
required
@sl-input=${(e: InputEvent) => {
const input = e.target as SlInput;
// Ideally this would match against the full character map that slugify uses
// but this'll do for most use cases
const end = input.value.match(/[\s*_+~.,()'"!\-:@]$/g) ? "-" : "";
input.value = slugifyStrict(input.value) + end;
const slugValue = slugifyStrict(input.value);
input.helpText = helpText(slugifyStrict(input.value));

input.setCustomValidity(
slugValue.length < 2 ? msg("URL too short") : "",
);
}}
>
</sl-input>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/org/collection-detail/collection-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { isNotEqual } from "@/utils/is-not-equal";
import { pluralOf } from "@/utils/pluralize";
import { formatRwpTimestamp } from "@/utils/replay";
import { richText } from "@/utils/rich-text";
import slugifyStrict from "@/utils/slugify";
import { tw } from "@/utils/tailwind";

const ABORT_REASON_THROTTLE = "throttled";
Expand Down Expand Up @@ -1569,6 +1570,7 @@ export class CollectionDetail extends BtrixElement {
method: "PATCH",
body: JSON.stringify({
name,
slug: slugifyStrict(name),
}),
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { dedupeTypeLabelFor } from "@/strings/dedupe";
import { CrawlerChannelImage } from "@/types/crawler";
import { crawlingDefaultsSchema, type CrawlingDefaults } from "@/types/org";
import { formValidator } from "@/utils/form";
import slugifyStrict from "@/utils/slugify";
import {
appDefaults,
BYTES_PER_GB,
Expand Down Expand Up @@ -544,14 +545,17 @@ export class OrgSettingsCrawlWorkflows extends BtrixElement {
}

private async createCollection(
params: { name: string },
{ name }: { name: string },
signal?: AbortSignal,
) {
return this.api.fetch<{ added: boolean; id: string; name: string }>(
`/orgs/${this.orgId}/collections`,
{
method: "POST",
body: JSON.stringify(params),
body: JSON.stringify({
name,
slug: slugifyStrict(name),
}),
signal,
},
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/org/settings/components/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class OrgSettingsGeneral extends BtrixElement {
autocomplete="off"
value=${this.orgSlugState || ""}
minlength="2"
maxlength="30"
maxlength="50"
required
@sl-input=${this.handleSlugInput}
>
Expand Down
Loading