diff --git a/app/actions/definitions/collections.tsx b/app/actions/definitions/collections.tsx index 573b5f1a2..d8b5c8b66 100644 --- a/app/actions/definitions/collections.tsx +++ b/app/actions/definitions/collections.tsx @@ -3,6 +3,7 @@ import { EditIcon, PadlockIcon, PlusIcon, + SearchIcon, StarredIcon, TrashIcon, UnstarredIcon, @@ -20,6 +21,7 @@ import { createAction } from "~/actions"; import { CollectionSection } from "~/actions/sections"; import { setPersistedState } from "~/hooks/usePersistedState"; import history from "~/utils/history"; +import { searchPath } from "~/utils/routeHelpers"; const ColorCollectionIcon = ({ collection }: { collection: Collection }) => ( @@ -111,6 +113,17 @@ export const editCollectionPermissions = createAction({ }, }); +export const searchInCollection = createAction({ + name: ({ t }) => t("Search in collection"), + analyticsName: "Search collection", + section: CollectionSection, + icon: , + visible: ({ activeCollectionId }) => !!activeCollectionId, + perform: ({ activeCollectionId }) => { + history.push(searchPath(undefined, { collectionId: activeCollectionId })); + }, +}); + export const starCollection = createAction({ name: ({ t }) => t("Star"), analyticsName: "Star collection", diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx index 9a4aceec1..581977462 100644 --- a/app/actions/definitions/documents.tsx +++ b/app/actions/definitions/documents.tsx @@ -606,6 +606,17 @@ export const pinDocument = createAction({ children: [pinDocumentToCollection, pinDocumentToHome], }); +export const searchInDocument = createAction({ + name: ({ t }) => t("Search in document"), + analyticsName: "Search document", + section: DocumentSection, + icon: , + visible: ({ activeDocumentId }) => !!activeDocumentId, + perform: ({ activeDocumentId }) => { + history.push(searchPath(undefined, { documentId: activeDocumentId })); + }, +}); + export const printDocument = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? t("Print") : t("Print document"), @@ -613,7 +624,7 @@ export const printDocument = createAction({ section: DocumentSection, icon: , visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print), - perform: async () => { + perform: () => { queueMicrotask(window.print); }, }); diff --git a/app/components/FilterOptions.tsx b/app/components/FilterOptions.tsx index 428226df0..537ba1bf9 100644 --- a/app/components/FilterOptions.tsx +++ b/app/components/FilterOptions.tsx @@ -45,7 +45,7 @@ const FilterOptions = ({ : ""; return ( - +
{(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",