Skip to content

Commit

Permalink
Merge pull request #211 from cabcookie:improve-search
Browse files Browse the repository at this point in the history
Einfacher Suchen (Projekte/Personen)
  • Loading branch information
cabcookie authored Oct 12, 2024
2 parents 8a368ca + e6003df commit 93f6876
Show file tree
Hide file tree
Showing 14 changed files with 434 additions and 99 deletions.
8 changes: 7 additions & 1 deletion components/accounts/AccountDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Editor } from "@tiptap/core";
import { filter, flow, map } from "lodash/fp";
import { FC } from "react";
import CrmLink from "../crm/CrmLink";
import { ProjectFilterProvider } from "../projects/useProjectFilter";
import { SearchProvider } from "../search/useSearch";
import DefaultAccordionItem from "../ui-elements/accordion/DefaultAccordionItem";
import { debouncedUpdateAccountDetails } from "../ui-elements/account-details/account-updates-helpers";
import NotesWriter from "../ui-elements/notes-writer/NotesWriter";
Expand Down Expand Up @@ -129,7 +131,11 @@ const AccountDetails: FC<AccountDetailsProps> = ({
)(projects)}
isVisible={!!showProjects}
>
<ProjectList accountId={account.id} />
<SearchProvider>
<ProjectFilterProvider accountId={account.id}>
<ProjectList />
</ProjectFilterProvider>
</SearchProvider>
</DefaultAccordionItem>

<AccountPeople accountId={account.id} isVisible={!!showContacts} />
Expand Down
44 changes: 7 additions & 37 deletions components/accounts/ProjectList.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,23 @@
import { useAccountsContext } from "@/api/ContextAccounts";
import { Project, useProjectsContext } from "@/api/ContextProjects";
import { filterAndSortProjects } from "@/helpers/projects";
import { flow, identity, map, times } from "lodash/fp";
import { FC, useEffect, useState } from "react";
import { FC } from "react";
import ApiLoadingError from "../layouts/ApiLoadingError";
import ProjectAccordionItem from "../projects/ProjectAccordionItem";
import { useProjectFilter } from "../projects/useProjectFilter";
import LoadingAccordionItem from "../ui-elements/accordion/LoadingAccordionItem";
import { Accordion } from "../ui/accordion";

const PROJECT_FILTERS = ["WIP", "On Hold", "Done"] as const;
export type ProjectFilters = (typeof PROJECT_FILTERS)[number];
export const isValidProjectFilter = (
filter: string
): filter is ProjectFilters =>
PROJECT_FILTERS.includes(filter as ProjectFilters);

type ProjectsByAccount = {
accountId: string;
filter?: never;
};
type ProjectsByFilter = {
accountId?: never;
filter: ProjectFilters;
};
type ProjectListProps = (ProjectsByAccount | ProjectsByFilter) & {
type ProjectListProps = {
allowPushToNextDay?: boolean;
};

const ProjectList: FC<ProjectListProps> = ({
accountId,
filter: projectFilter,
allowPushToNextDay,
}) => {
const { projects, loadingProjects, errorProjects } = useProjectsContext();
const { accounts } = useAccountsContext();
const [filteredProjects, setFilteredProjects] = useState<Project[]>([]);

useEffect(() => {
if (!projects) return setFilteredProjects([]);
setFilteredProjects(
filterAndSortProjects(projects, accountId, projectFilter, accounts)
);
}, [accountId, accounts, projectFilter, projects]);
const ProjectList: FC<ProjectListProps> = ({ allowPushToNextDay }) => {
const { projects, loadingProjects, errorProjects } = useProjectFilter();

return (
<div className="space-y-4">
<ApiLoadingError title="Loading projects failed" error={errorProjects} />

{!loadingProjects && filteredProjects.length === 0 && (
{!loadingProjects && projects.length === 0 && (
<div className="text-muted-foreground text-sm font-semibold">
No projects
</div>
Expand All @@ -67,7 +37,7 @@ const ProjectList: FC<ProjectListProps> = ({
))
)(10)}

{filteredProjects.map((project) => (
{projects.map((project) => (
<ProjectAccordionItem
key={project.id}
project={project}
Expand Down
34 changes: 34 additions & 0 deletions components/meetings/create-one-on-one-meeting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useContextContext } from "@/contexts/ContextContext";
import { FC } from "react";
import PeopleSelector from "../ui-elements/selectors/people-selector";
import { Label } from "../ui/label";
import { CreateMeetingProps } from "./useMeetingFilter";

type CreateOneOnOneMeetingProps = {
createMeeting: (props: CreateMeetingProps) => void;
};

const CreateOneOnOneMeeting: FC<CreateOneOnOneMeetingProps> = ({
createMeeting,
}) => {
const { context } = useContextContext();

return (
<div>
<Label className="font-semibold">Create 1:1 Meeting:</Label>
<PeopleSelector
placeholder="Select person for 1:1…"
value=""
onChange={(personId) =>
createMeeting({
topic: "",
participantId: personId ?? undefined,
context,
})
}
/>
</div>
);
};

export default CreateOneOnOneMeeting;
34 changes: 30 additions & 4 deletions components/meetings/useMeetingFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import useMeetings, { Meeting } from "@/api/useMeetings";
import { useContextContext } from "@/contexts/ContextContext";
import usePeople from "@/api/usePeople";
import { Context, useContextContext } from "@/contexts/ContextContext";
import { createMeetingName } from "@/helpers/meetings";
import { filter, flow, map, uniq } from "lodash/fp";
import {
ComponentType,
Expand All @@ -11,10 +13,16 @@ import {
} from "react";
import useMeetingPagination from "./useMeetingPagination";

export type CreateMeetingProps = {
topic: string;
context?: Context;
participantId?: string;
};

interface MeetingFilterType {
meetings: ReturnType<typeof useMeetings>["meetings"] | undefined;
meetingDates: string[];
createMeeting: ReturnType<typeof useMeetings>["createMeeting"];
createMeeting: (props: CreateMeetingProps) => Promise<string | undefined>;
selectedFilter: TMeetingFilters;
availableFilters: TMeetingFilters[];
onSelectFilter: (selectedFilter: string) => void;
Expand Down Expand Up @@ -59,7 +67,7 @@ const MeetingFilterProvider: FC<MeetingFilterProviderProps> = ({
const { fromDate, toDate, handleNextClick, handlePrevClick } =
useMeetingPagination();

const { meetings, createMeeting } = useMeetings({
const { meetings, createMeeting, createMeetingParticipant } = useMeetings({
context,
startDate: fromDate,
});
Expand All @@ -68,6 +76,8 @@ const MeetingFilterProvider: FC<MeetingFilterProviderProps> = ({
const [meetingFilter, setMeetingFilter] = useState<TMeetingFilters>("All");
const [filtered, setFiltered] = useState<Meeting[] | undefined>(undefined);

const { people } = usePeople();

useEffect(() => {
if (!meetings) return setFiltered(undefined);
if (meetingFilter === "All") return setFiltered(meetings);
Expand All @@ -86,12 +96,28 @@ const MeetingFilterProvider: FC<MeetingFilterProviderProps> = ({
setMeetingFilter(newFilter);
};

const createMeetingAndParticipant = async ({
topic,
context,
participantId,
}: CreateMeetingProps) => {
const meetingName = !participantId
? topic
: createMeetingName({ participantId, people });
if (!meetingName) return;
const meetingId = await createMeeting(meetingName, context);
if (!meetingId) return;
if (!participantId) return meetingId;
await createMeetingParticipant(meetingId, participantId);
return meetingId;
};

return (
<MeetingFilter.Provider
value={{
meetings: filtered,
meetingDates,
createMeeting,
createMeeting: createMeetingAndParticipant,
selectedFilter: meetingFilter,
availableFilters: [...MEETING_FILTERS],
onSelectFilter: onFilterChange,
Expand Down
30 changes: 15 additions & 15 deletions components/navigation-menu/CreateOneOnOneMeeting.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useMeetings from "@/api/useMeetings";
import usePeople from "@/api/usePeople";
import { useContextContext } from "@/contexts/ContextContext";
import { first, flow, identity, split } from "lodash/fp";
import { createMeetingName } from "@/helpers/meetings";
import { FC } from "react";
import SearchableDataGroup from "./SearchableDataGroup";

Expand All @@ -13,26 +14,25 @@ type CreateOneOnOneMeetingProps = {
}[];
};

const getFirstName = flow(identity<string>, split(" "), first);

const CreateOneOnOneMeeting: FC<CreateOneOnOneMeetingProps> = ({
metaPressed,
items,
}) => {
const { context } = useContextContext();
const { createMeeting, createMeetingParticipant } = useMeetings({ context });
const { people } = usePeople();

const handleCreate =
(personId: string, personName: string, accountNames: string | undefined) =>
async () => {
const meetingName = `Meet ${getFirstName(personName)}${
!accountNames ? "" : `/${getFirstName(accountNames)}`
}`;
const meetingId = await createMeeting(meetingName, context);
if (!meetingId) return;
await createMeetingParticipant(meetingId, personId);
return meetingId;
};
const handleCreate = (personId: string) => async () => {
const meetingName = createMeetingName({
people,
participantId: personId,
});
if (!meetingName) return;
const meetingId = await createMeeting(meetingName, context);
if (!meetingId) return;
await createMeetingParticipant(meetingId, personId);
return meetingId;
};

return (
<SearchableDataGroup
Expand All @@ -42,7 +42,7 @@ const CreateOneOnOneMeeting: FC<CreateOneOnOneMeetingProps> = ({
id,
value: `…with ${name}${!accountNames ? "" : ` (${accountNames})`}`,
link: "/meetings",
processFn: handleCreate(id, name, accountNames),
processFn: handleCreate(id),
}))}
/>
);
Expand Down
31 changes: 31 additions & 0 deletions components/projects/project-filter-btn-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cn } from "@/lib/utils";
import { FC } from "react";
import ButtonGroup from "../ui-elements/btn-group/btn-group";
import {
PROJECT_FILTERS,
ProjectFilters,
useProjectFilter,
} from "./useProjectFilter";

type ProjectFilterBtnGrpProps = {
className?: string;
};

const ProjectFilterBtnGrp: FC<ProjectFilterBtnGrpProps> = ({ className }) => {
const { isSearchActive, projectFilter, setProjectFilter } =
useProjectFilter();

return (
!isSearchActive && (
<div className={cn(className)}>
<ButtonGroup
values={PROJECT_FILTERS as ProjectFilters[]}
selectedValue={projectFilter}
onSelect={setProjectFilter}
/>
</div>
)
);
};

export default ProjectFilterBtnGrp;
103 changes: 103 additions & 0 deletions components/projects/useProjectFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useAccountsContext } from "@/api/ContextAccounts";
import { Project, useProjectsContext } from "@/api/ContextProjects";
import { filterAndSortProjects } from "@/helpers/projects";
import {
ComponentType,
createContext,
FC,
useContext,
useEffect,
useState,
} from "react";
import { SearchProvider, useSearch } from "../search/useSearch";

export const PROJECT_FILTERS = ["WIP", "On Hold", "Done"];
const PROJECT_FILTERS_CONST = ["WIP", "On Hold", "Done"] as const;
export type ProjectFilters = (typeof PROJECT_FILTERS_CONST)[number];

export const isValidProjectFilter = (
filter: string
): filter is ProjectFilters =>
PROJECT_FILTERS_CONST.includes(filter as ProjectFilters);

interface ProjectFilterType {
projects: Project[];
loadingProjects: ReturnType<typeof useProjectsContext>["loadingProjects"];
errorProjects: ReturnType<typeof useProjectsContext>["errorProjects"];
isSearchActive: boolean;
projectFilter: ProjectFilters;
setProjectFilter: (filter: string) => void;
}

const ProjectFilter = createContext<ProjectFilterType | null>(null);

export const useProjectFilter = () => {
const searchContext = useContext(ProjectFilter);
if (!searchContext)
throw new Error(
"useProjectFilter must be used within ProjectFilterProvider"
);
return searchContext;
};

interface ProjectFilterProviderProps {
children: React.ReactNode;
accountId?: string;
}

export const ProjectFilterProvider: FC<ProjectFilterProviderProps> = ({
children,
accountId,
}) => {
const { projects, loadingProjects, errorProjects } = useProjectsContext();
const { accounts } = useAccountsContext();
const [filteredProjects, setFilteredProjects] = useState<Project[]>([]);
const { searchText, isSearchActive } = useSearch();
const [filter, setFilter] = useState<ProjectFilters>("WIP");

const onFilterChange = (newFilter: string) =>
isValidProjectFilter(newFilter) && setFilter(newFilter);

useEffect(() => {
if (!projects) return setFilteredProjects([]);
setFilteredProjects(
filterAndSortProjects({
projects,
accountId,
projectFilter: filter,
accounts,
searchText,
})
);
}, [accountId, accounts, filter, projects, searchText]);

return (
<ProjectFilter.Provider
value={{
isSearchActive,
setProjectFilter: onFilterChange,
projectFilter: filter,
projects: filteredProjects,
loadingProjects,
errorProjects,
}}
>
{children}
</ProjectFilter.Provider>
);
};

export function withProjectFilter<Props extends object>(
Component: ComponentType<Props>
) {
return function WrappedProvider(componentProps: Props) {
return (
<SearchProvider>
<ProjectFilterProvider>
<Component {...componentProps} />
</ProjectFilterProvider>
;
</SearchProvider>
);
};
}
Loading

0 comments on commit 93f6876

Please sign in to comment.