Skip to content

Commit

Permalink
Support for filter by parent document (#6850)
Browse files Browse the repository at this point in the history
* Backend support for filter by parent document

* parentDocumentId -> documentId
  • Loading branch information
tommoor committed Apr 26, 2024
1 parent 3f4583c commit 958cf45
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 45 deletions.
13 changes: 13 additions & 0 deletions app/actions/definitions/collections.tsx
Expand Up @@ -3,6 +3,7 @@ import {
EditIcon,
PadlockIcon,
PlusIcon,
SearchIcon,
StarredIcon,
TrashIcon,
UnstarredIcon,
Expand All @@ -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} />
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 12 additions & 1 deletion app/actions/definitions/documents.tsx
Expand Up @@ -606,14 +606,25 @@ 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"),
analyticsName: "Print document",
section: DocumentSection,
icon: <PrintIcon />,
visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print),
perform: async () => {
perform: () => {
queueMicrotask(window.print);
},
});
Expand Down
10 changes: 3 additions & 7 deletions app/components/FilterOptions.tsx
Expand Up @@ -45,7 +45,7 @@ const FilterOptions = ({
: "";

return (
<Wrapper>
<div>
<MenuButton {...menu}>
{(props) => (
<StyledButton {...props} className={className} neutral disclosure>
Expand Down Expand Up @@ -76,7 +76,7 @@ const FilterOptions = ({
</MenuItem>
))}
</ContextMenu>
</Wrapper>
</div>
);
};

Expand All @@ -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;
Expand All @@ -120,8 +120,4 @@ const Icon = styled.div`
height: 18px;
`;

const Wrapper = styled.div`
margin-right: 8px;
`;

export default FilterOptions;
2 changes: 1 addition & 1 deletion app/hooks/useActionContext.ts
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions app/menus/CollectionMenu.tsx
Expand Up @@ -26,6 +26,7 @@ import {
editCollectionPermissions,
starCollection,
unstarCollection,
searchInCollection,
} from "~/actions/definitions/collections";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
Expand Down Expand Up @@ -205,6 +206,7 @@ function CollectionMenu({
onClick: handleExport,
icon: <ExportIcon />,
},
actionToMenuItem(searchInCollection, context),
{
type: "separator",
},
Expand Down
4 changes: 3 additions & 1 deletion app/menus/DocumentMenu.tsx
Expand Up @@ -44,6 +44,7 @@ import {
createNestedDocument,
shareDocument,
copyDocument,
searchInDocument,
} from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext";
import useCurrentUser from "~/hooks/useCurrentUser";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -305,6 +306,7 @@ function DocumentMenu({
actionToMenuItem(downloadDocument, context),
actionToMenuItem(copyDocument, context),
actionToMenuItem(printDocument, context),
actionToMenuItem(searchInDocument, context),
{
type: "separator",
},
Expand Down
89 changes: 56 additions & 33 deletions app/scenes/Search/Search.tsx
Expand Up @@ -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";
Expand All @@ -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(
() => ({
Expand All @@ -73,6 +76,7 @@ function Search(props: Props) {
userId,
dateFilter,
titleFilter,
documentId,
}),
[
query,
Expand All @@ -81,6 +85,7 @@ function Search(props: Props) {
userId,
dateFilter,
titleFilter,
documentId,
]
);

Expand All @@ -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),
Expand All @@ -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[];
Expand Down Expand Up @@ -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 ? (
<>
<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 });
{(query || hasFilters) && (
<Filters>
{document && (
<DocumentFilter
document={document}
onClick={() => {
handleFilterChange({ documentId: undefined });
}}
checked={titleFilter}
/>
</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>
)}
{query ? (
<>
{showEmpty && (
<Fade>
<Centered column>
Expand Down Expand Up @@ -282,7 +304,7 @@ function Search(props: Props) {
/>
</ResultList>
</>
) : (
) : documentId || collectionId ? null : (
<RecentSearches
ref={recentSearchesCompositeRef}
onEscape={handleEscape}
Expand Down Expand Up @@ -323,6 +345,7 @@ const Filters = styled(Flex)`
overflow-y: hidden;
overflow-x: auto;
padding: 8px 0;
gap: 8px;
${hideScrollbars()}
${breakpoint("tablet")`
Expand Down
25 changes: 25 additions & 0 deletions app/scenes/Search/components/DocumentFilter.tsx
@@ -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>
);
}
2 changes: 1 addition & 1 deletion app/types.ts
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/utils/routeHelpers.ts
Expand Up @@ -101,6 +101,7 @@ export function searchPath(
query?: string,
params: {
collectionId?: string;
documentId?: string;
ref?: string;
} = {}
): string {
Expand Down
8 changes: 8 additions & 0 deletions server/models/helpers/SearchHelper.ts
Expand Up @@ -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 */
Expand Down Expand Up @@ -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({
Expand Down

0 comments on commit 958cf45

Please sign in to comment.