-
{pageResult.title}
+
+
+ {pageResult.title || t("Untitled")}
+
+ {timeAgo(pageResult.updatedAt)}
+
+
{showSpace && pageResult.space && (
diff --git a/apps/client/src/features/search/components/search-spotlight-filters.module.css b/apps/client/src/features/search/components/search-spotlight-filters.module.css
index e8073aab6..3709f2960 100644
--- a/apps/client/src/features/search/components/search-spotlight-filters.module.css
+++ b/apps/client/src/features/search/components/search-spotlight-filters.module.css
@@ -17,3 +17,10 @@
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-6));
}
}
+
+.filterButtonActive {
+ color: var(--mantine-color-blue-light-color);
+ &:hover {
+ color: var(--mantine-color-blue-light-color);
+ }
+}
diff --git a/apps/client/src/features/search/components/search-spotlight-filters.tsx b/apps/client/src/features/search/components/search-spotlight-filters.tsx
index 0b2bcc48c..96a7178ad 100644
--- a/apps/client/src/features/search/components/search-spotlight-filters.tsx
+++ b/apps/client/src/features/search/components/search-spotlight-filters.tsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
+import cx from "clsx";
import {
Button,
Menu,
@@ -11,18 +12,24 @@ import {
import {
IconChevronDown,
IconBuilding,
+ IconPlus,
IconFileDescription,
IconCheck,
+ IconUser,
+ IconTag,
+ IconLetterCase,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { useGetSpacesQuery } from "@/features/space/queries/space-query";
import { SpaceFilterMenu } from "@/features/space/components/space-filter-menu";
+import { CreatorFilterMenu } from "@/features/search/components/creator-filter-menu";
import { RadioMenuItem } from "@/components/ui/radio-menu-item";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import classes from "./search-spotlight-filters.module.css";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
+import { LabelFilterMenu } from "./label-filter-menu";
interface SearchSpotlightFiltersProps {
onFiltersChange?: (filters: any) => void;
@@ -40,9 +47,17 @@ export function SearchSpotlightFilters({
const { t } = useTranslation();
const hasAttachmentIndexing = useHasFeature(Feature.ATTACHMENT_INDEXING);
const [selectedSpaceId, setSelectedSpaceId] = useState(
- spaceId || null,
+ spaceId || null
);
const [contentType, setContentType] = useState("page");
+ const [selectedCreatorId, setSelectedCreatorId] = useState(null);
+ const [selectedCreatorName, setSelectedCreatorName] = useState(
+ null
+ );
+ const [selectedLabelIds, setSelectedLabelIds] = useState([]);
+ const [titleOnly, setTitleOnly] = useState(false);
+ const [openedFilter, setOpenedFilter] = useState(null);
+ const [visibleFilters, setVisibleFilters] = useState([]);
const [workspace] = useAtom(workspaceAtom);
const { data: spacesData } = useGetSpacesQuery({ limit: 100 });
@@ -50,15 +65,6 @@ export function SearchSpotlightFilters({
? spacesData?.items.find((space) => space.id === selectedSpaceId)
: null;
- useEffect(() => {
- if (onFiltersChange) {
- onFiltersChange({
- spaceId: selectedSpaceId,
- contentType,
- });
- }
- }, []);
-
const contentTypeOptions = [
{ value: "page", label: t("Pages") },
{
@@ -68,38 +74,70 @@ export function SearchSpotlightFilters({
},
];
+ useEffect(() => {
+ onFiltersChange?.({
+ spaceId: selectedSpaceId,
+ contentType,
+ creatorId: selectedCreatorId,
+ labelIds: selectedLabelIds,
+ titleOnly,
+ });
+ }, [
+ selectedSpaceId,
+ contentType,
+ selectedCreatorId,
+ selectedLabelIds,
+ titleOnly,
+ onFiltersChange,
+ ]);
+
const handleSpaceSelect = (spaceId: string | null) => {
setSelectedSpaceId(spaceId);
+ };
- if (onFiltersChange) {
- onFiltersChange({
- spaceId: spaceId,
- contentType,
- });
+ const handleCreatorSelect = (user: { id: string; name: string } | null) => {
+ setSelectedCreatorId(user?.id ?? null);
+ setSelectedCreatorName(user?.name ?? null);
+ };
+
+ const handleLabelsSelect = (labelIds: string[]) => {
+ setSelectedLabelIds(labelIds);
+ };
+
+ const handleChangeContentType = (value: string) => {
+ setContentType(value);
+
+ if (value === "attachment") {
+ setSelectedLabelIds([]);
}
};
- const handleFilterChange = (filterType: string, value: any) => {
- let newSelectedSpaceId = selectedSpaceId;
- let newContentType = contentType;
+ const onDemandFilters = [
+ { key: "creator", label: t("Created by"), icon: IconUser, available: true },
+ {
+ key: "labels",
+ label: t("Labels"),
+ icon: IconTag,
+ available: contentType !== "attachment",
+ },
+ ];
- switch (filterType) {
- case "spaceId":
- newSelectedSpaceId = value;
- setSelectedSpaceId(value);
- break;
- case "contentType":
- newContentType = value;
- setContentType(value);
- break;
- }
+ const isFilterVisible = (key: string) => {
+ if (openedFilter === key) return true;
+ if (key === "creator") return !!selectedCreatorId;
+ if (key === "labels")
+ return contentType !== "attachment" && selectedLabelIds.length > 0;
+ return false;
+ };
- if (onFiltersChange) {
- onFiltersChange({
- spaceId: newSelectedSpaceId,
- contentType: newContentType,
- });
- }
+ const orderedVisibleFilters = visibleFilters.filter(isFilterVisible);
+ const addableFilters = onDemandFilters.filter(
+ (filter) => filter.available && !isFilterVisible(filter.key),
+ );
+
+ const revealFilter = (key: string) => {
+ setVisibleFilters((prev) => [...prev.filter((k) => k !== key), key]);
+ setOpenedFilter(key);
};
return (
@@ -122,8 +160,17 @@ export function SearchSpotlightFilters({
color="blue"
labelPosition="left"
styles={{
- root: { display: "flex", alignItems: "center" },
- label: { paddingRight: "8px", fontSize: "13px", fontWeight: 500 },
+ root: {
+ display: "flex",
+ alignItems: "center",
+ flexShrink: 0,
+ },
+ label: {
+ whiteSpace: "nowrap",
+ paddingRight: "8px",
+ fontSize: "13px",
+ fontWeight: 500,
+ },
}}
/>
@@ -181,7 +228,7 @@ export function SearchSpotlightFilters({
onClick={() =>
!option.disabled &&
contentType !== option.value &&
- handleFilterChange("contentType", option.value)
+ handleChangeContentType(option.value)
}
disabled={
option.disabled || (isAiMode && option.value === "attachment")
@@ -195,13 +242,11 @@ export function SearchSpotlightFilters({
{t("Enterprise")}
)}
- {!option.disabled &&
- isAiMode &&
- option.value === "attachment" && (
-
- {t("AI Answers not available for attachments")}
-
- )}
+ {!option.disabled && isAiMode && option.value === "attachment" && (
+
+ {t("AI Answers not available for attachments")}
+
+ )}
{contentType === option.value &&