Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions web/components/core/views/all-views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import useSWR from "swr";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CalendarLayout, GanttLayout, KanBanLayout, SpreadsheetLayout } from "components/issues";
import { AppliedFiltersList, CalendarLayout, GanttLayout, KanBanLayout, SpreadsheetLayout } from "components/issues";

export const AllViews: React.FC = observer(() => {
const router = useRouter();
Expand Down Expand Up @@ -34,7 +34,8 @@ export const AllViews: React.FC = observer(() => {
const activeLayout = issueFilterStore.userDisplayFilters.layout;

return (
<div className="relative w-full h-full overflow-auto">
<div className="relative w-full h-full flex flex-col overflow-auto">
<AppliedFiltersList />
{activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
Expand Down
8 changes: 4 additions & 4 deletions web/components/issue-layouts/root.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from "react";
// components
import { LayoutSelection } from "../issues/issue-layouts/header/layout-selection";
import { IssueDropdown } from "../issues/issue-layouts/header/helpers/dropdown";
import { FilterSelection } from "../issues/issue-layouts/header/filters/filters-selection";
import { DisplayFiltersSelection } from "../issues/issue-layouts/header/display-filters";
import { LayoutSelection } from "../issues/issue-layouts/filters/header/layout-selection";
import { IssueDropdown } from "../issues/issue-layouts/filters/header/helpers/dropdown";
import { FilterSelection } from "../issues/issue-layouts/filters/header/filters/filters-selection";
import { DisplayFiltersSelection } from "../issues/issue-layouts/filters/header/display-filters";

import { FilterPreview } from "./filters-preview";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ export const CalendarMonthsDropdown: React.FC = observer(() => {
<button
key={month.shortTitle}
type="button"
className={`text-xs hover:bg-custom-background-80 rounded py-0.5 ${
activeMonthDate.getMonth() === index ? "bg-custom-background-80" : ""
}`}
className="text-xs hover:bg-custom-background-80 rounded py-0.5"
onClick={() => {
const newDate = new Date(activeMonthDate.getFullYear(), index, 1);
handleDateChange(newDate);
Expand Down
6 changes: 4 additions & 2 deletions web/components/issues/issue-layouts/calendar/issue-blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
{(provided, snapshot) => (
<Link href={`/${workspaceSlug?.toString()}/projects/${issue.project}/issues/${issue.id}`}>
<a
className={`h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-200 ${
snapshot.isDragging ? "shadow-custom-shadow-rg bg-custom-background-90" : "bg-custom-background-100"
className={`h-8 w-full shadow-custom-shadow-2xs rounded py-1.5 px-1 flex items-center gap-1.5 border-[0.5px] border-custom-border-100 ${
snapshot.isDragging
? "shadow-custom-shadow-rg bg-custom-background-90"
: "bg-custom-background-100 hover:bg-custom-background-90"
}`}
{...provided.draggableProps}
{...provided.dragHandleProps}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { observer } from "mobx-react-lite";

// icons
import { X } from "lucide-react";
// helpers
import { renderLongDateFormat } from "helpers/date-time.helper";
import { capitalizeFirstLetter } from "helpers/string.helper";
// constants
import { DATE_FILTER_OPTIONS } from "constants/filters";

type Props = {
handleRemove: (val: string) => void;
values: string[];
};

export const AppliedDateFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values } = props;

const getDateLabel = (value: string): string => {
let dateLabel = "";

const dateDetails = DATE_FILTER_OPTIONS.find((d) => d.value === value);

if (dateDetails) dateLabel = dateDetails.name;
else {
const dateParts = value.split(";");

if (dateParts.length === 2) {
const [date, time] = dateParts;

dateLabel = `${capitalizeFirstLetter(time)} ${renderLongDateFormat(date)}`;
}
}

return dateLabel;
};

return (
<div className="flex items-center gap-1 flex-wrap">
{values.map((date) => (
<div key={date} className="text-xs flex items-center gap-1 bg-custom-background-80 p-1 rounded">
<span className="normal-case">{getDateLabel(date)}</span>
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(date)}
>
<X size={10} strokeWidth={2} />
</button>
</div>
))}
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";

// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import {
AppliedDateFilters,
AppliedLabelsFilters,
AppliedMembersFilters,
AppliedPriorityFilters,
AppliedStateFilters,
AppliedStateGroupFilters,
} from "components/issues";
// icons
import { X } from "lucide-react";
// helpers
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
// types
import { IIssueFilterOptions } from "types";

export const AppliedFiltersList: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;

const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore();

const userFilters = issueFilterStore.userFilters;

// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
Object.entries(userFilters).forEach(([key, value]) => {
if (!value) return;

if (Array.isArray(value) && value.length === 0) return;

appliedFilters[key as keyof IIssueFilterOptions] = value;
});

const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId) return;

// remove all values of the key if value is null
if (!value) {
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: null,
},
});
return;
}

// remove the passed value from the key
let newValues = issueFilterStore.userFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value);

issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: newValues,
},
});
};

const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;

const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});

issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: { ...newFilters },
});
};

// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;

return (
<div className="flex items-stretch gap-2 flex-wrap bg-custom-background-100 p-4">
{Object.entries(appliedFilters).map(([key, value]) => {
const filterKey = key as keyof IIssueFilterOptions;

return (
<div
key={filterKey}
className="capitalize py-1 px-2 border border-custom-border-200 rounded-md flex items-center gap-2 flex-wrap"
>
<span className="text-xs text-custom-text-300">{replaceUnderscoreIfSnakeCase(filterKey)}</span>
{(filterKey === "assignees" || filterKey === "created_by" || filterKey === "subscriber") && (
<AppliedMembersFilters
handleRemove={(val) => handleRemoveFilter(filterKey, val)}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
values={value}
/>
)}
{(filterKey === "start_date" || filterKey === "target_date") && (
<AppliedDateFilters handleRemove={(val) => handleRemoveFilter(filterKey, val)} values={value} />
)}
{filterKey === "labels" && (
<AppliedLabelsFilters
handleRemove={(val) => handleRemoveFilter("labels", val)}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
values={value}
/>
)}
{filterKey === "priority" && (
<AppliedPriorityFilters handleRemove={(val) => handleRemoveFilter("priority", val)} values={value} />
)}
{filterKey === "state" && (
<AppliedStateFilters
handleRemove={(val) => handleRemoveFilter("state", val)}
states={projectStore.states?.[projectId?.toString() ?? ""]}
values={value}
/>
)}
{filterKey === "state_group" && (
<AppliedStateGroupFilters handleRemove={(val) => handleRemoveFilter("state_group", val)} values={value} />
)}
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemoveFilter(filterKey, null)}
>
<X size={12} strokeWidth={2} />
</button>
</div>
);
})}
<button
type="button"
onClick={handleClearAllFilters}
className="flex items-center gap-2 text-xs border border-custom-border-200 py-1 px-2 rounded-md text-custom-text-300 hover:text-custom-text-200"
>
Clear all
<X size={12} strokeWidth={2} />
</button>
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from "./date";
export * from "./filters-list";
export * from "./label";
export * from "./members";
export * from "./priority";
export * from "./state";
export * from "./state-group";
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { observer } from "mobx-react-lite";

// icons
import { X } from "lucide-react";
// types
import { IIssueLabels } from "types";

type Props = {
handleRemove: (val: string) => void;
labels: IIssueLabels[] | undefined;
values: string[];
};

export const AppliedLabelsFilters: React.FC<Props> = observer((props) => {
const { handleRemove, labels, values } = props;

return (
<div className="flex items-center gap-1 flex-wrap">
{values.map((labelId) => {
const labelDetails = labels?.find((l) => l.id === labelId);

if (!labelDetails) return null;

return (
<div key={labelId} className="text-xs flex items-center gap-1 bg-custom-background-80 p-1 rounded">
<span
className="h-1.5 w-1.5 rounded-full"
style={{
backgroundColor: labelDetails.color,
}}
/>
<span className="normal-case">{labelDetails.name}</span>
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(labelId)}
>
<X size={10} strokeWidth={2} />
</button>
</div>
);
})}
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { observer } from "mobx-react-lite";

// ui
import { Avatar } from "components/ui";
// icons
import { X } from "lucide-react";
// types
import { IUserLite } from "types";

type Props = {
handleRemove: (val: string) => void;
members: IUserLite[] | undefined;
values: string[];
};

export const AppliedMembersFilters: React.FC<Props> = observer((props) => {
const { handleRemove, members, values } = props;

return (
<div className="flex items-center gap-1 flex-wrap">
{values.map((memberId) => {
const memberDetails = members?.find((m) => m.id === memberId);

if (!memberDetails) return null;

return (
<div key={memberId} className="text-xs flex items-center gap-1 bg-custom-background-80 p-1 rounded">
<Avatar user={memberDetails} height="16px" width="16px" />
<span className="normal-case">{memberDetails.display_name}</span>
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(memberId)}
>
<X size={10} strokeWidth={2} />
</button>
</div>
);
})}
</div>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { observer } from "mobx-react-lite";

// icons
import { PriorityIcon } from "components/icons";
import { X } from "lucide-react";
// types
import { TIssuePriorities } from "types";

type Props = {
handleRemove: (val: string) => void;
values: string[];
};

export const AppliedPriorityFilters: React.FC<Props> = observer((props) => {
const { handleRemove, values } = props;

return (
<div className="flex items-center gap-1 flex-wrap">
{values.map((priority) => (
<div key={priority} className="text-xs flex items-center gap-1 bg-custom-background-80 p-1 rounded">
<PriorityIcon
priority={priority as TIssuePriorities}
className={`!text-xs ${
priority === "urgent"
? "text-red-500"
: priority === "high"
? "text-orange-500"
: priority === "medium"
? "text-yellow-500"
: priority === "low"
? "text-green-500"
: ""
}`}
/>
{priority}
<button
type="button"
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
onClick={() => handleRemove(priority)}
>
<X size={10} strokeWidth={2} />
</button>
</div>
))}
</div>
);
});
Loading