+
{(props) => (
@@ -76,7 +76,7 @@ const FilterOptions = ({
))}
-
+
);
};
@@ -98,7 +98,7 @@ const LabelWithNote = styled.div`
}
`;
-const StyledButton = styled(Button)`
+export const StyledButton = styled(Button)`
box-shadow: none;
text-transform: none;
border-color: transparent;
@@ -120,8 +120,4 @@ const Icon = styled.div`
height: 18px;
`;
-const Wrapper = styled.div`
- margin-right: 8px;
-`;
-
export default FilterOptions;
diff --git a/app/hooks/useActionContext.ts b/app/hooks/useActionContext.ts
index e006bf171..134b01056 100644
--- a/app/hooks/useActionContext.ts
+++ b/app/hooks/useActionContext.ts
@@ -21,7 +21,7 @@ export default function useActionContext(
isContextMenu: false,
isCommandBar: false,
isButton: false,
- activeCollectionId: stores.ui.activeCollectionId,
+ activeCollectionId: stores.ui.activeCollectionId ?? undefined,
activeDocumentId: stores.ui.activeDocumentId,
currentUserId: stores.auth.user?.id,
currentTeamId: stores.auth.team?.id,
diff --git a/app/menus/CollectionMenu.tsx b/app/menus/CollectionMenu.tsx
index fca327bc2..b8e0a4c27 100644
--- a/app/menus/CollectionMenu.tsx
+++ b/app/menus/CollectionMenu.tsx
@@ -26,6 +26,7 @@ import {
editCollectionPermissions,
starCollection,
unstarCollection,
+ searchInCollection,
} from "~/actions/definitions/collections";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
@@ -205,6 +206,7 @@ function CollectionMenu({
onClick: handleExport,
icon: ,
},
+ actionToMenuItem(searchInCollection, context),
{
type: "separator",
},
diff --git a/app/menus/DocumentMenu.tsx b/app/menus/DocumentMenu.tsx
index df9b0e7b3..1ad080cee 100644
--- a/app/menus/DocumentMenu.tsx
+++ b/app/menus/DocumentMenu.tsx
@@ -44,6 +44,7 @@ import {
createNestedDocument,
shareDocument,
copyDocument,
+ searchInDocument,
} from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -94,7 +95,7 @@ function DocumentMenu({
const context = useActionContext({
isContextMenu: true,
activeDocumentId: document.id,
- activeCollectionId: document.collectionId,
+ activeCollectionId: document.collectionId ?? undefined,
});
const { t } = useTranslation();
const isMobile = useMobile();
@@ -305,6 +306,7 @@ function DocumentMenu({
actionToMenuItem(downloadDocument, context),
actionToMenuItem(copyDocument, context),
actionToMenuItem(printDocument, context),
+ actionToMenuItem(searchInDocument, context),
{
type: "separator",
},
diff --git a/app/scenes/Search/Search.tsx b/app/scenes/Search/Search.tsx
index a138fac4d..8e5405fd7 100644
--- a/app/scenes/Search/Search.tsx
+++ b/app/scenes/Search/Search.tsx
@@ -33,6 +33,7 @@ import { searchPath } from "~/utils/routeHelpers";
import { decodeURIComponentSafe } from "~/utils/urls";
import CollectionFilter from "./components/CollectionFilter";
import DateFilter from "./components/DateFilter";
+import { DocumentFilter } from "./components/DocumentFilter";
import DocumentTypeFilter from "./components/DocumentTypeFilter";
import RecentSearches from "./components/RecentSearches";
import SearchInput from "./components/SearchInput";
@@ -59,11 +60,13 @@ function Search(props: Props) {
const query = decodeURIComponentSafe(routeMatch.params.term ?? "");
const collectionId = params.get("collectionId") ?? undefined;
const userId = params.get("userId") ?? undefined;
+ const documentId = params.get("documentId") ?? undefined;
const dateFilter = (params.get("dateFilter") as TDateFilter) ?? undefined;
const statusFilter = params.getAll("statusFilter")?.length
? (params.getAll("statusFilter") as TStatusFilter[])
: [TStatusFilter.Published, TStatusFilter.Draft];
const titleFilter = params.get("titleFilter") === "true";
+ const hasFilters = !!(documentId || collectionId || userId || dateFilter);
const filters = React.useMemo(
() => ({
@@ -73,6 +76,7 @@ function Search(props: Props) {
userId,
dateFilter,
titleFilter,
+ documentId,
}),
[
query,
@@ -81,6 +85,7 @@ function Search(props: Props) {
userId,
dateFilter,
titleFilter,
+ documentId,
]
);
@@ -107,6 +112,8 @@ function Search(props: Props) {
limit: Pagination.defaultLimit,
});
+ const document = documentId ? documents.get(documentId) : undefined;
+
const updateLocation = (query: string) => {
history.replace({
pathname: searchPath(query),
@@ -118,6 +125,7 @@ function Search(props: Props) {
// some complexity as the query string is the source of truth for the filters.
const handleFilterChange = (search: {
collectionId?: string | undefined;
+ documentId?: string | undefined;
userId?: string | undefined;
dateFilter?: TDateFilter;
statusFilter?: TStatusFilter[];
@@ -206,44 +214,58 @@ function Search(props: Props) {
+ {(query || hasFilters) && (
+
+ {document && (
+ {
+ handleFilterChange({ documentId: undefined });
+ }}
+ />
+ )}
+
+ handleFilterChange({ statusFilter })
+ }
+ />
+ handleFilterChange({ collectionId })}
+ />
+ handleFilterChange({ userId })}
+ />
+ handleFilterChange({ dateFilter })}
+ />
+ ) => {
+ handleFilterChange({ titleFilter: ev.target.checked });
+ }}
+ checked={titleFilter}
+ />
+
+ )}
{query ? (
<>
-
-
- handleFilterChange({ statusFilter })
- }
- />
-
- handleFilterChange({ collectionId })
- }
- />
- handleFilterChange({ userId })}
- />
- handleFilterChange({ dateFilter })}
- />
- ) => {
- handleFilterChange({ titleFilter: ev.target.checked });
- }}
- checked={titleFilter}
- />
-
{showEmpty && (
@@ -282,7 +304,7 @@ function Search(props: Props) {
/>
>
- ) : (
+ ) : documentId || collectionId ? null : (
+
+ } neutral>
+ {props.document.title}
+
+
+
+ );
+}
diff --git a/app/types.ts b/app/types.ts
index afd95a5bd..c5754358d 100644
--- a/app/types.ts
+++ b/app/types.ts
@@ -83,7 +83,7 @@ export type ActionContext = {
isCommandBar: boolean;
isButton: boolean;
inStarredSection?: boolean;
- activeCollectionId?: string | null;
+ activeCollectionId?: string | undefined;
activeDocumentId: string | undefined;
currentUserId: string | undefined;
currentTeamId: string | undefined;
diff --git a/app/utils/routeHelpers.ts b/app/utils/routeHelpers.ts
index 9f64ab1af..7a8132e9b 100644
--- a/app/utils/routeHelpers.ts
+++ b/app/utils/routeHelpers.ts
@@ -101,6 +101,7 @@ export function searchPath(
query?: string,
params: {
collectionId?: string;
+ documentId?: string;
ref?: string;
} = {}
): string {
diff --git a/server/models/helpers/SearchHelper.ts b/server/models/helpers/SearchHelper.ts
index 429faa7a8..ce347350c 100644
--- a/server/models/helpers/SearchHelper.ts
+++ b/server/models/helpers/SearchHelper.ts
@@ -40,6 +40,8 @@ type SearchOptions = {
dateFilter?: DateFilter;
/** Status of the documents to return */
statusFilter?: StatusFilter[];
+ /** Limit results to a list of documents. */
+ documentIds?: string[];
/** Limit results to a list of users that collaborated on the document. */
collaboratorIds?: string[];
/** The minimum number of words to be returned in the contextual snippet */
@@ -367,6 +369,12 @@ export default class SearchHelper {
});
}
+ if (options.documentIds) {
+ where[Op.and].push({
+ id: options.documentIds,
+ });
+ }
+
const statusQuery = [];
if (options.statusFilter?.includes(StatusFilter.Published)) {
statusQuery.push({
diff --git a/server/routes/api/documents/documents.ts b/server/routes/api/documents/documents.ts
index 048353765..c41eda6b6 100644
--- a/server/routes/api/documents/documents.ts
+++ b/server/routes/api/documents/documents.ts
@@ -785,6 +785,7 @@ router.post(
const {
query,
collectionId,
+ documentId,
userId,
dateFilter,
statusFilter = [],
@@ -853,6 +854,18 @@ router.post(
authorize(user, "readDocument", collection);
}
+ let documentIds = undefined;
+ if (documentId) {
+ const document = await Document.findByPk(documentId, {
+ userId: user.id,
+ });
+ authorize(user, "read", document);
+ documentIds = [
+ documentId,
+ ...(await document.findAllChildDocumentIds()),
+ ];
+ }
+
let collaboratorIds = undefined;
if (userId) {
@@ -862,6 +875,7 @@ router.post(
response = await SearchHelper.searchForUser(user, query, {
collaboratorIds,
collectionId,
+ documentIds,
dateFilter,
statusFilter,
offset,
diff --git a/server/routes/api/documents/schema.ts b/server/routes/api/documents/schema.ts
index e3c05991f..97f1749d2 100644
--- a/server/routes/api/documents/schema.ts
+++ b/server/routes/api/documents/schema.ts
@@ -153,6 +153,9 @@ export const DocumentsSearchSchema = BaseSchema.extend({
/** Filter results based on user */
userId: z.string().uuid().optional(),
+ /** Filter results based on content within a document and it's children */
+ documentId: z.string().uuid().optional(),
+
/**
* Whether to include archived documents in results
*
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index 48f27253a..83487c75e 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -6,6 +6,7 @@
"Edit collection": "Edit collection",
"Permissions": "Permissions",
"Collection permissions": "Collection permissions",
+ "Search in collection": "Search in collection",
"Star": "Star",
"Unstar": "Unstar",
"Delete": "Delete",
@@ -52,6 +53,7 @@
"Pin to home": "Pin to home",
"Pinned to home": "Pinned to home",
"Pin": "Pin",
+ "Search in document": "Search in document",
"Print": "Print",
"Print document": "Print document",
"Import document": "Import document",
@@ -483,7 +485,6 @@
"API token created": "API token created",
"Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".": "Name your token something that will help you to remember it's use in the future, for example \"local development\", \"production\", or \"continuous integration\".",
"The document archive is empty at the moment.": "The document archive is empty at the moment.",
- "Search in collection": "Search in collection",
"This collection is only visible to those given access": "This collection is only visible to those given access",
"Private": "Private",
"Recently updated": "Recently updated",
@@ -763,6 +764,7 @@
"Past week": "Past week",
"Past month": "Past month",
"Past year": "Past year",
+ "Remove document filter": "Remove document filter",
"Any status": "Any status",
"Search Results": "Search Results",
"Remove search": "Remove search",