Skip to content

Commit f8a4425

Browse files
authored
Merge pull request #2332 from webrecorder/frontend-collection-editing-dialog
Collection editing and sharing revamp
1 parent d4032d4 commit f8a4425

38 files changed

+2077
-882
lines changed

backend/btrixcloud/colls.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
# pylint: disable=too-many-lines
6-
6+
from datetime import datetime
77
from collections import Counter
88
from uuid import UUID, uuid4
99
from typing import Optional, List, TYPE_CHECKING, cast, Dict, Tuple, Any, Union
@@ -20,10 +20,12 @@
2020

2121
from .pagination import DEFAULT_PAGE_SIZE, paginated_format
2222
from .models import (
23+
AnyHttpUrl,
2324
Collection,
2425
CollIn,
2526
CollOut,
2627
CollIdName,
28+
CollectionThumbnailSource,
2729
UpdateColl,
2830
AddRemoveCrawlList,
2931
BaseCrawl,
@@ -843,8 +845,17 @@ async def set_home_url(
843845

844846
return {"updated": True}
845847

848+
# pylint: disable=too-many-locals
846849
async def upload_thumbnail_stream(
847-
self, stream, filename: str, coll_id: UUID, org: Organization, user: User
850+
self,
851+
stream,
852+
filename: str,
853+
coll_id: UUID,
854+
org: Organization,
855+
user: User,
856+
source_url: Optional[AnyHttpUrl] = None,
857+
source_ts: Optional[datetime] = None,
858+
source_page_id: Optional[UUID] = None,
848859
) -> Dict[str, bool]:
849860
"""Upload file as stream to use as collection thumbnail"""
850861
coll = await self.get_collection(coll_id)
@@ -903,6 +914,13 @@ async def stream_iter():
903914

904915
coll.thumbnail = thumbnail_file
905916

917+
if source_url and source_ts and source_page_id:
918+
coll.thumbnailSource = CollectionThumbnailSource(
919+
url=source_url,
920+
urlTs=source_ts,
921+
urlPageId=source_page_id,
922+
)
923+
906924
# Update entire document to avoid bson.errors.InvalidDocument exception
907925
await self.collections.find_one_and_update(
908926
{"_id": coll_id, "oid": org.id},
@@ -1226,11 +1244,21 @@ async def upload_thumbnail_stream(
12261244
request: Request,
12271245
filename: str,
12281246
coll_id: UUID,
1247+
sourceUrl: Optional[AnyHttpUrl],
1248+
sourceTs: Optional[datetime],
1249+
sourcePageId: Optional[UUID],
12291250
org: Organization = Depends(org_crawl_dep),
12301251
user: User = Depends(user_dep),
12311252
):
12321253
return await colls.upload_thumbnail_stream(
1233-
request.stream(), filename, coll_id, org, user
1254+
request.stream(),
1255+
filename,
1256+
coll_id,
1257+
org,
1258+
user,
1259+
sourceUrl,
1260+
sourceTs,
1261+
sourcePageId,
12341262
)
12351263

12361264
@app.delete(

backend/btrixcloud/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,15 @@ class CollAccessType(str, Enum):
12361236
PUBLIC = "public"
12371237

12381238

1239+
# ============================================================================
1240+
class CollectionThumbnailSource(BaseModel):
1241+
"""The page source for a thumbnail"""
1242+
1243+
url: AnyHttpUrl
1244+
urlTs: datetime
1245+
urlPageId: UUID
1246+
1247+
12391248
# ============================================================================
12401249
class Collection(BaseMongoModel):
12411250
"""Org collection structure"""
@@ -1268,6 +1277,7 @@ class Collection(BaseMongoModel):
12681277
homeUrlPageId: Optional[UUID] = None
12691278

12701279
thumbnail: Optional[ImageFile] = None
1280+
thumbnailSource: Optional[CollectionThumbnailSource] = None
12711281
defaultThumbnailName: Optional[str] = None
12721282

12731283
allowPublicDownload: Optional[bool] = True
@@ -1323,6 +1333,7 @@ class CollOut(BaseMongoModel):
13231333

13241334
resources: List[CrawlFileOut] = []
13251335
thumbnail: Optional[ImageFileOut] = None
1336+
thumbnailSource: Optional[CollectionThumbnailSource] = None
13261337
defaultThumbnailName: Optional[str] = None
13271338

13281339
allowPublicDownload: bool = True
@@ -1372,6 +1383,7 @@ class UpdateColl(BaseModel):
13721383
access: Optional[CollAccessType] = None
13731384
defaultThumbnailName: Optional[str] = None
13741385
allowPublicDownload: Optional[bool] = None
1386+
thumbnailSource: Optional[CollectionThumbnailSource] = None
13751387

13761388

13771389
# ============================================================================

backend/test/test_collections.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,9 +1145,10 @@ def test_collection_url_list(crawler_auth_headers, default_org_id):
11451145

11461146

11471147
def test_upload_collection_thumbnail(crawler_auth_headers, default_org_id):
1148+
# https://dev.browsertrix.com/api/orgs/c69247f4-415e-4abc-b449-e85d2f26c626/collections/b764fbe1-baab-4dc5-8dca-2db6f82c250b/thumbnail?filename=page-thumbnail_47fe599e-ed62-4edd-b078-93d4bf281e0f.jpeg&sourceUrl=https%3A%2F%2Fspecs.webrecorder.net%2F&sourceTs=2024-08-16T08%3A00%3A21.601000Z&sourcePageId=47fe599e-ed62-4edd-b078-93d4bf281e0f
11481149
with open(os.path.join(curr_dir, "data", "thumbnail.jpg"), "rb") as fh:
11491150
r = requests.put(
1150-
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_public_coll_id}/thumbnail?filename=thumbnail.jpg",
1151+
f"{API_PREFIX}/orgs/{default_org_id}/collections/{_public_coll_id}/thumbnail?filename=thumbnail.jpg&sourceUrl=https%3A%2F%2Fexample.com%2F&sourceTs=2024-08-16T08%3A00%3A21.601000Z&sourcePageId=1bba4aba-d5be-4943-ad48-d6710633d754",
11511152
headers=crawler_auth_headers,
11521153
data=read_in_chunks(fh),
11531154
)
@@ -1159,7 +1160,8 @@ def test_upload_collection_thumbnail(crawler_auth_headers, default_org_id):
11591160
headers=crawler_auth_headers,
11601161
)
11611162
assert r.status_code == 200
1162-
thumbnail = r.json()["thumbnail"]
1163+
collection = r.json()
1164+
thumbnail = collection["thumbnail"]
11631165

11641166
assert thumbnail["name"]
11651167
assert thumbnail["path"]
@@ -1172,6 +1174,16 @@ def test_upload_collection_thumbnail(crawler_auth_headers, default_org_id):
11721174
assert thumbnail["userName"]
11731175
assert thumbnail["created"]
11741176

1177+
thumbnailSource = collection["thumbnailSource"]
1178+
1179+
assert thumbnailSource["url"]
1180+
assert thumbnailSource["urlTs"]
1181+
assert thumbnailSource["urlPageId"]
1182+
1183+
assert thumbnailSource["url"] == "https://example.com/"
1184+
assert thumbnailSource["urlTs"] == "2024-08-16T08:00:21.601000Z"
1185+
assert thumbnailSource["urlPageId"] == "1bba4aba-d5be-4943-ad48-d6710633d754"
1186+
11751187

11761188
def test_set_collection_default_thumbnail(crawler_auth_headers, default_org_id):
11771189
default_thumbnail_name = "orange-default.avif"

frontend/docs/docs/user-guide/org-settings.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Change Org Settings
1+
# Edit Org Settings
22

33
Settings that apply to the entire organization are found in the **Settings** page. If you're an org admin, you'll see the link to _Settings_ in the org navigation bar.
44

@@ -13,7 +13,7 @@ The org URL is where you and other org members will go to view the dashboard, co
1313
Org name and URLs are unique to each Browsertrix instance (for example, on `app.browsertrix.com`) and you may be prompted to change the org name or URL if either are already in use by another org.
1414

1515
??? info "What information will be visible to the public?"
16-
All org information is private until you make the org visible. Once your org is made visible to the public, the org name, description, and website will appear on the org's public gallery page. You can preview how information appears to the public by clicking **View as public**.
16+
All org information is private until you make the org visible. Once your org is made visible to the public, the org name, description, and website will appear on the org's public collections gallery page. You can preview how information appears to the public by visiting the linked public collections gallery page.
1717

1818
### Public Collections Gallery
1919

frontend/src/components/ui/button.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ export class Button extends TailwindElement {
7474
small: tw`min-h-6 min-w-6 rounded-md text-base`,
7575
medium: tw`min-h-8 min-w-8 rounded-sm text-lg`,
7676
}[this.size],
77-
this.raised && tw`shadow ring-1 ring-neutral-200`,
77+
this.raised &&
78+
tw`shadow ring-1 ring-stone-500/20 hover:shadow-stone-800/20 hover:ring-stone-800/20`,
7879
this.filled
7980
? [
8081
tw`text-white`,

frontend/src/components/ui/navigation/navigation-button.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable lit/binding-positions */
22
/* eslint-disable lit/no-invalid-html */
3+
import clsx from "clsx";
34
import { css, type PropertyValueMap } from "lit";
45
import { customElement, property } from "lit/decorators.js";
56
import { ifDefined } from "lit/directives/if-defined.js";
@@ -26,6 +27,9 @@ export class NavigationButton extends TailwindElement {
2627
@property({ type: String })
2728
type: "submit" | "button" = "button";
2829

30+
@property({ type: String })
31+
variant: "primary" | "error" = "primary"; // TODO expand if necessary
32+
2933
@property({ type: String })
3034
label?: string;
3135

@@ -76,8 +80,9 @@ export class NavigationButton extends TailwindElement {
7680
return html`<${tag}
7781
type=${this.type === "submit" ? "submit" : "button"}
7882
part="button"
79-
class=${[
80-
tw`flex w-full cursor-pointer items-center gap-2 rounded font-medium leading-[16px] outline-primary-600 transition hover:transition-none focus-visible:outline focus-visible:outline-3 focus-visible:outline-offset-1 disabled:cursor-not-allowed disabled:bg-transparent disabled:opacity-50`,
83+
class=${clsx([
84+
tw`flex w-full cursor-pointer items-center gap-2 rounded font-medium leading-[16px] transition hover:transition-none focus-visible:outline focus-visible:outline-3 focus-visible:outline-offset-1 disabled:cursor-not-allowed disabled:bg-transparent disabled:opacity-50`,
85+
8186
this.icon ? tw`min-h-6 min-w-6` : tw``,
8287
{
8388
small: this.icon ? tw`min-h-6 p-0` : tw`min-h-6 px-2 py-0`,
@@ -89,17 +94,27 @@ export class NavigationButton extends TailwindElement {
8994
center: "justify-center",
9095
right: "justify-end",
9196
}[this.align],
92-
this.active
93-
? tw`bg-primary-100/80 text-primary-800 shadow-sm shadow-primary-900/20`
94-
: tw`text-neutral-700 hover:bg-primary-50`,
95-
]
96-
.filter(Boolean)
97-
.join(" ")}
97+
this.active && "shadow-sm",
98+
{
99+
primary: [
100+
tw`outline-primary-600`,
101+
this.active
102+
? tw`bg-primary-100/80 text-primary-800 shadow-primary-900/20`
103+
: tw`text-neutral-700 hover:bg-primary-50`,
104+
],
105+
error: [
106+
tw`outline-red-600`,
107+
this.active
108+
? tw`bg-red-100/80 text-red-800 shadow-red-900/20`
109+
: tw`text-red-700 ring-1 ring-red-300 hover:bg-red-50`,
110+
],
111+
}[this.variant],
112+
])}
98113
?disabled=${this.disabled}
99114
href=${ifDefined(this.href)}
100115
aria-label=${ifDefined(this.label)}
101116
@click=${this.handleClick}
102-
117+
103118
>
104119
<slot></slot>
105120
</${tag}>`;

frontend/src/components/ui/tab-group/tab-group.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { TailwindElement } from "@/classes/TailwindElement";
1212
import { pageSectionsWithNav } from "@/layouts/pageSectionsWithNav";
1313

1414
/**
15+
* @fires btrix-tab-change
1516
* @example Usage:
1617
* ```ts
1718
* <btrix-tab-group>
@@ -145,5 +146,11 @@ export class TabGroup extends TailwindElement {
145146
private onSelectTab(e: CustomEvent<TabClickDetail>) {
146147
e.stopPropagation();
147148
this.active = e.detail.panel;
149+
this.dispatchEvent(
150+
new CustomEvent<string>("btrix-tab-change", {
151+
detail: this.active,
152+
bubbles: true,
153+
}),
154+
);
148155
}
149156
}

frontend/src/context/view-state.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { createContext } from "@lit/context";
2+
3+
import { type ViewState } from "@/utils/APIRouter";
4+
5+
export type ViewStateContext = ViewState | null;
6+
7+
export const viewStateContext = createContext<ViewStateContext>("viewState");

frontend/src/controllers/api.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export class APIController implements ReactiveController {
173173
async upload(
174174
path: string,
175175
file: File,
176+
abortSignal?: AbortSignal,
176177
): Promise<{ id: string; added: boolean; storageQuotaReached: boolean }> {
177178
const auth = appState.auth;
178179

@@ -185,9 +186,12 @@ export class APIController implements ReactiveController {
185186
}
186187

187188
return new Promise((resolve, reject) => {
189+
if (abortSignal?.aborted) {
190+
reject(AbortReason.UserCancel);
191+
}
188192
const xhr = new XMLHttpRequest();
189193

190-
xhr.open("PUT", `/api/${path}`);
194+
xhr.open("PUT", `/api${path}`);
191195
xhr.setRequestHeader("Content-Type", "application/octet-stream");
192196
Object.entries(auth.headers).forEach(([k, v]) => {
193197
xhr.setRequestHeader(k, v);
@@ -221,6 +225,11 @@ export class APIController implements ReactiveController {
221225

222226
xhr.send(file);
223227

228+
abortSignal?.addEventListener("abort", () => {
229+
xhr.abort();
230+
reject(AbortReason.UserCancel);
231+
});
232+
224233
this.uploadRequest = xhr;
225234
});
226235
}

frontend/src/decorators/needLogin.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe("needLogin", () => {
1919
}
2020

2121
const Element = needLogin(
22+
// @ts-expect-error not stubbing full BtrixElement
2223
class TestElement extends LiteElementMock {
2324
appState = appState;
2425
} as unknown as {

0 commit comments

Comments
 (0)