Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show search results in new viewer mode #912

Merged
merged 10 commits into from
Dec 3, 2024
194 changes: 101 additions & 93 deletions public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@
/* tslint:disable */

/**
* Mock Service Worker.
* Mock Service Worker (1.3.5).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/

const PACKAGE_VERSION = '2.6.6'
const INTEGRITY_CHECKSUM = 'ca7800994cc8bfb5eb961e037c877074'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
const activeClientIds = new Set()

self.addEventListener('install', function () {
Expand Down Expand Up @@ -49,10 +47,7 @@ self.addEventListener('message', async function (event) {
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
payload: INTEGRITY_CHECKSUM,
})
break
}
Expand All @@ -62,12 +57,7 @@ self.addEventListener('message', async function (event) {

sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
payload: true,
})
break
}
Expand Down Expand Up @@ -96,6 +86,12 @@ self.addEventListener('message', async function (event) {

self.addEventListener('fetch', function (event) {
const { request } = event
const accept = request.headers.get('accept') || ''

// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return
}

// Bypass navigation requests.
if (request.mode === 'navigate') {
Expand All @@ -116,8 +112,29 @@ self.addEventListener('fetch', function (event) {
}

// Generate unique request ID.
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId))
const requestId = Math.random().toString(16).slice(2)

event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn(
'[MSW] Successfully emulated a network error for the "%s %s" request.',
request.method,
request.url,
)
return
}

// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`,
)
}),
)
})

async function handleRequest(event, requestId) {
Expand All @@ -129,24 +146,21 @@ async function handleRequest(event, requestId) {
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const responseClone = response.clone()

sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: Object.fromEntries(clonedResponse.headers.entries()),
redirected: clonedResponse.redirected,
},
[responseClone.body],
)
})
})()
}

Expand All @@ -160,10 +174,6 @@ async function handleRequest(event, requestId) {
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)

if (activeClientIds.has(event.clientId)) {
return client
}

if (client?.frameType === 'top-level') {
return client
}
Expand All @@ -186,22 +196,20 @@ async function resolveMainClient(event) {

async function getResponse(event, client, requestId) {
const { request } = event

// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = request.clone()
const clonedRequest = request.clone()

function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const headers = Object.fromEntries(clonedRequest.headers.entries())

// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
headers.delete('accept', 'msw/passthrough')
// Remove MSW-specific request headers so the bypassed requests
// comply with the server's CORS preflight check.
// Operate with the headers as an object because request "Headers"
// are immutable.
delete headers['x-msw-bypass']

return fetch(requestClone, { headers })
return fetch(clonedRequest, { headers })
}

// Bypass mocking when the client is not active.
Expand All @@ -217,46 +225,57 @@ async function getResponse(event, client, requestId) {
return passthrough()
}

// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
if (request.headers.get('x-msw-bypass') === 'true') {
return passthrough()
}

// Notify the client that a request has been intercepted.
const requestBuffer = await request.arrayBuffer()
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
},
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.text(),
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
[requestBuffer],
)
})

switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}

case 'PASSTHROUGH': {
case 'MOCK_NOT_FOUND': {
return passthrough()
}

case 'NETWORK_ERROR': {
const { name, message } = clientMessage.data
const networkError = new Error(message)
networkError.name = name

// Rejecting a "respondWith" promise emulates a network error.
throw networkError
}
}

return passthrough()
}

function sendToClient(client, message, transferrables = []) {
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()

Expand All @@ -268,28 +287,17 @@ function sendToClient(client, message, transferrables = []) {
resolve(event.data)
}

client.postMessage(
message,
[channel.port2].concat(transferrables.filter(Boolean)),
)
client.postMessage(message, [channel.port2])
})
}

async function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}

const mockedResponse = new Response(response.body, response)

Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
function sleep(timeMs) {
return new Promise((resolve) => {
setTimeout(resolve, timeMs)
})
}

return mockedResponse
async function respondWithMock(response) {
await sleep(response.delay)
return new Response(response.body, response)
}
4 changes: 3 additions & 1 deletion src/langs/json/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"title": "Title"
},
"search": {
"loading": "Searching…",
"matchingResults": "{n, plural, one {# matching result} other {# matching results}}",
"reset": "Clear Search",
"help": "Use filters like <code>user:</code>, <code>project:</code> or <code>organization:</code> to refine searches. Use <code>sort:</code> to order results.",
"more": "Learn more"
"more": "Learn more",
"empty": "No matching results found."
},
"homeTemplate": {
"signedIn": "Signed in as {name}",
Expand Down
28 changes: 27 additions & 1 deletion src/lib/api/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
ViewerMode,
ValidationError,
Nullable,
Highlights,
} from "./types";

import { writable, type Writable } from "svelte/store";
Expand All @@ -39,6 +40,7 @@ export const READING_MODES = new Set<ReadMode>([
"text",
"grid",
"notes",
"search",
]);

export const WRITING_MODES = new Set<WriteMode>(["annotating", "redacting"]);
Expand Down Expand Up @@ -83,6 +85,28 @@ export async function search(
return getApiResponse<DocumentResults, null>(resp);
}

/** Search within a single document */
export async function searchWithin(
id: string | number,
query = "",
options: SearchOptions = {
hl: Boolean(query),
},
fetch = globalThis.fetch,
): Promise<APIResponse<Highlights, null>> {
const endpoint = new URL(`documents/${id}/search/`, BASE_API_URL);
endpoint.searchParams.set("q", query);
for (const [k, v] of Object.entries(options)) {
if (v) {
endpoint.searchParams.set(k, String(v));
}
}
const resp = await fetch(endpoint, { credentials: "include" }).catch(
console.error,
);
return getApiResponse<Highlights, null>(resp);
}

/**
* Load a single document from the API
* Example: https://api.www.documentcloud.org/api/documents/1/
Expand Down Expand Up @@ -682,7 +706,9 @@ export function shouldPreload(mode: ViewerMode): boolean {
* @returns {boolean}
*/
export function shouldPaginate(mode: ViewerMode): boolean {
return ["document", "text", "annotating", "redacting"].includes(mode);
return ["document", "text", "annotating", "redacting", "search"].includes(
mode,
);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/lib/api/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type Status = "success" | "readable" | "pending" | "error" | "nofile"; //
export type Sizes = "thumbnail" | "small" | "normal" | "large" | "xlarge";

// modes ending in -ing are writing modes
export type ReadMode = "document" | "text" | "grid" | "notes";
export type ReadMode = "document" | "text" | "grid" | "notes" | "search";

export type WriteMode = "redacting" | "annotating";

Expand Down
6 changes: 6 additions & 0 deletions src/lib/components/common/Highlight.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
font-size: var(--font-sm);
margin-bottom: 0.5rem;
}
.segment {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
.ellipsis {
white-space: nowrap;
overflow: hidden;
Expand Down
Loading
Loading