Support for filter by parent document (#6850)

* Backend support for filter by parent document

* parentDocumentId -> documentId
This commit is contained in:
Tom Moor
2024-04-25 22:44:15 -04:00
committed by GitHub
parent 3f4583ce72
commit 958cf45d74
14 changed files with 145 additions and 45 deletions

View File

@@ -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 }) => (
<DynamicCollectionIcon 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: <SearchIcon />,
visible: ({ activeCollectionId }) => !!activeCollectionId,
perform: ({ activeCollectionId }) => {
history.push(searchPath(undefined, { collectionId: activeCollectionId }));
},
});
export const starCollection = createAction({
name: ({ t }) => t("Star"),
analyticsName: "Star collection",

View File

@@ -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: <SearchIcon />,
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: <PrintIcon />,
visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print),
perform: async () => {
perform: () => {
queueMicrotask(window.print);
},
});

View File

@@ -45,7 +45,7 @@ const FilterOptions = ({
: "";
return (
<Wrapper>
<div>
<MenuButton {...menu}>
{(props) => (
<StyledButton {...props} className={className} neutral disclosure>
@@ -76,7 +76,7 @@ const FilterOptions = ({
</MenuItem>
))}
</ContextMenu>
</Wrapper>
</div>
);
};
@@ -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;

View File

@@ -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,

View File

@@ -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: <ExportIcon />,
},
actionToMenuItem(searchInCollection, context),
{
type: "separator",
},

View File

@@ -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",
},

View File

@@ -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) {
<SearchInput
key={query ? "search" : "recent"}
ref={searchInputRef}
placeholder={`${t("Search")}`}
placeholder={`${
documentId
? t("Search in document")
: collectionId
? t("Search in collection")
: t("Search")
}`}
onKeyDown={handleKeyDown}
defaultValue={query}
/>
{(query || hasFilters) && (
<Filters>
{document && (
<DocumentFilter
document={document}
onClick={() => {
handleFilterChange({ documentId: undefined });
}}
/>
)}
<DocumentTypeFilter
statusFilter={statusFilter}
onSelect={({ statusFilter }) =>
handleFilterChange({ statusFilter })
}
/>
<CollectionFilter
collectionId={collectionId}
onSelect={(collectionId) => handleFilterChange({ collectionId })}
/>
<UserFilter
userId={userId}
onSelect={(userId) => handleFilterChange({ userId })}
/>
<DateFilter
dateFilter={dateFilter}
onSelect={(dateFilter) => handleFilterChange({ dateFilter })}
/>
<SearchTitlesFilter
width={26}
height={14}
label={t("Search titles only")}
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
handleFilterChange({ titleFilter: ev.target.checked });
}}
checked={titleFilter}
/>
</Filters>
)}
{query ? (
<>
<Filters>
<DocumentTypeFilter
statusFilter={statusFilter}
onSelect={({ statusFilter }) =>
handleFilterChange({ statusFilter })
}
/>
<CollectionFilter
collectionId={collectionId}
onSelect={(collectionId) =>
handleFilterChange({ collectionId })
}
/>
<UserFilter
userId={userId}
onSelect={(userId) => handleFilterChange({ userId })}
/>
<DateFilter
dateFilter={dateFilter}
onSelect={(dateFilter) => handleFilterChange({ dateFilter })}
/>
<SearchTitlesFilter
width={26}
height={14}
label={t("Search titles only")}
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
handleFilterChange({ titleFilter: ev.target.checked });
}}
checked={titleFilter}
/>
</Filters>
{showEmpty && (
<Fade>
<Centered column>
@@ -282,7 +304,7 @@ function Search(props: Props) {
/>
</ResultList>
</>
) : (
) : documentId || collectionId ? null : (
<RecentSearches
ref={recentSearchesCompositeRef}
onEscape={handleEscape}
@@ -323,6 +345,7 @@ const Filters = styled(Flex)`
overflow-y: hidden;
overflow-x: auto;
padding: 8px 0;
gap: 8px;
${hideScrollbars()}
${breakpoint("tablet")`

View File

@@ -0,0 +1,25 @@
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import Document from "~/models/Document";
import { StyledButton } from "~/components/FilterOptions";
import Tooltip from "~/components/Tooltip";
type Props = {
document: Document;
onClick: React.MouseEventHandler;
};
export function DocumentFilter(props: Props) {
const { t } = useTranslation();
return (
<div>
<Tooltip content={t("Remove document filter")} delay={350}>
<StyledButton onClick={props.onClick} icon={<CloseIcon />} neutral>
{props.document.title}
</StyledButton>
</Tooltip>
</div>
);
}

View File

@@ -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;

View File

@@ -101,6 +101,7 @@ export function searchPath(
query?: string,
params: {
collectionId?: string;
documentId?: string;
ref?: string;
} = {}
): string {

View File

@@ -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({

View File

@@ -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,

View File

@@ -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
*

View File

@@ -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",