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 {