diff --git a/src/renderer/__mocks__/state-mocks.ts b/src/renderer/__mocks__/state-mocks.ts
index 295c420a8..a05dae93d 100644
--- a/src/renderer/__mocks__/state-mocks.ts
+++ b/src/renderer/__mocks__/state-mocks.ts
@@ -111,6 +111,8 @@ const mockFilters: FilterSettingsState = {
filterUserTypes: [],
filterIncludeHandles: [],
filterExcludeHandles: [],
+ filterIncludeOrganizations: [],
+ filterExcludeOrganizations: [],
filterSubjectTypes: [],
filterStates: [],
filterReasons: [],
diff --git a/src/renderer/components/filters/OrganizationFilter.test.tsx b/src/renderer/components/filters/OrganizationFilter.test.tsx
new file mode 100644
index 000000000..ab0aff667
--- /dev/null
+++ b/src/renderer/components/filters/OrganizationFilter.test.tsx
@@ -0,0 +1,130 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { mockSettings } from '../../__mocks__/state-mocks';
+import { AppContext } from '../../context/App';
+import type { SettingsState } from '../../types';
+import { OrganizationFilter } from './OrganizationFilter';
+
+const mockUpdateFilter = jest.fn();
+
+describe('components/filters/OrganizationFilter.tsx', () => {
+ beforeEach(() => {
+ mockUpdateFilter.mockReset();
+ });
+
+ it('should render itself & its children', () => {
+ const props = {
+ updateFilter: mockUpdateFilter,
+ settings: mockSettings,
+ };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText('Organizations')).toBeInTheDocument();
+ expect(screen.getByText('Include:')).toBeInTheDocument();
+ expect(screen.getByText('Exclude:')).toBeInTheDocument();
+ });
+
+ describe('Include organizations', () => {
+ it('should handle organization includes', async () => {
+ const props = {
+ updateFilter: mockUpdateFilter,
+ settings: mockSettings,
+ };
+
+ render(
+
+
+ ,
+ );
+
+ await userEvent.type(
+ screen.getByTitle('Include organizations'),
+ 'microsoft{enter}',
+ );
+
+ expect(mockUpdateFilter).toHaveBeenCalledWith(
+ 'filterIncludeOrganizations',
+ 'microsoft',
+ true,
+ );
+ });
+
+ it('should not allow duplicate include organizations', async () => {
+ const props = {
+ updateFilter: mockUpdateFilter,
+ settings: {
+ ...mockSettings,
+ filterIncludeOrganizations: ['microsoft'],
+ } as SettingsState,
+ };
+
+ render(
+
+
+ ,
+ );
+
+ await userEvent.type(
+ screen.getByTitle('Include organizations'),
+ 'microsoft{enter}',
+ );
+
+ expect(mockUpdateFilter).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('Exclude organizations', () => {
+ it('should handle organization excludes', async () => {
+ const props = {
+ updateFilter: mockUpdateFilter,
+ settings: mockSettings,
+ };
+
+ render(
+
+
+ ,
+ );
+
+ await userEvent.type(
+ screen.getByTitle('Exclude organizations'),
+ 'github{enter}',
+ );
+
+ expect(mockUpdateFilter).toHaveBeenCalledWith(
+ 'filterExcludeOrganizations',
+ 'github',
+ true,
+ );
+ });
+
+ it('should not allow duplicate exclude organizations', async () => {
+ const props = {
+ updateFilter: mockUpdateFilter,
+ settings: {
+ ...mockSettings,
+ filterExcludeOrganizations: ['github'],
+ } as SettingsState,
+ };
+
+ render(
+
+
+ ,
+ );
+
+ await userEvent.type(
+ screen.getByTitle('Exclude organizations'),
+ 'github{enter}',
+ );
+
+ expect(mockUpdateFilter).toHaveBeenCalledTimes(0);
+ });
+ });
+});
diff --git a/src/renderer/components/filters/OrganizationFilter.tsx b/src/renderer/components/filters/OrganizationFilter.tsx
new file mode 100644
index 000000000..369087f06
--- /dev/null
+++ b/src/renderer/components/filters/OrganizationFilter.tsx
@@ -0,0 +1,206 @@
+import { type FC, useContext, useEffect, useState } from 'react';
+
+import {
+ CheckCircleFillIcon,
+ NoEntryFillIcon,
+ OrganizationIcon,
+} from '@primer/octicons-react';
+import { Box, Stack, Text, TextInputWithTokens } from '@primer/react';
+
+import { AppContext } from '../../context/App';
+import { IconColor, type Organization } from '../../types';
+import {
+ hasExcludeOrganizationFilters,
+ hasIncludeOrganizationFilters,
+} from '../../utils/notifications/filters/organizations';
+import { Tooltip } from '../fields/Tooltip';
+import { Title } from '../primitives/Title';
+
+type InputToken = {
+ id: number;
+ text: string;
+};
+
+const tokenEvents = ['Enter', 'Tab', ' ', ','];
+
+export const OrganizationFilter: FC = () => {
+ const { updateFilter, settings } = useContext(AppContext);
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies: we only want to run this effect on organization filter changes
+ useEffect(() => {
+ if (!hasIncludeOrganizationFilters(settings)) {
+ setIncludeOrganizations([]);
+ }
+
+ if (!hasExcludeOrganizationFilters(settings)) {
+ setExcludeOrganizations([]);
+ }
+ }, [
+ settings.filterIncludeOrganizations,
+ settings.filterExcludeOrganizations,
+ ]);
+
+ const mapValuesToTokens = (values: string[]): InputToken[] => {
+ return values.map((value, index) => ({
+ id: index,
+ text: value,
+ }));
+ };
+
+ const [includeOrganizations, setIncludeOrganizations] = useState<
+ InputToken[]
+ >(mapValuesToTokens(settings.filterIncludeOrganizations));
+
+ const addIncludeOrganizationsToken = (
+ event:
+ | React.KeyboardEvent
+ | React.FocusEvent,
+ ) => {
+ const value = (event.target as HTMLInputElement).value.trim();
+
+ if (
+ value.length > 0 &&
+ !includeOrganizations.some((v) => v.text === value)
+ ) {
+ setIncludeOrganizations([
+ ...includeOrganizations,
+ { id: includeOrganizations.length, text: value },
+ ]);
+ updateFilter('filterIncludeOrganizations', value as Organization, true);
+
+ (event.target as HTMLInputElement).value = '';
+ }
+ };
+
+ const removeIncludeOrganizationToken = (tokenId: string | number) => {
+ const value =
+ includeOrganizations.find((v) => v.id === tokenId)?.text || '';
+ updateFilter('filterIncludeOrganizations', value as Organization, false);
+
+ setIncludeOrganizations(
+ includeOrganizations.filter((v) => v.id !== tokenId),
+ );
+ };
+
+ const includeOrganizationsKeyDown = (
+ event: React.KeyboardEvent,
+ ) => {
+ if (tokenEvents.includes(event.key)) {
+ addIncludeOrganizationsToken(event);
+ }
+ };
+
+ const [excludeOrganizations, setExcludeOrganizations] = useState<
+ InputToken[]
+ >(mapValuesToTokens(settings.filterExcludeOrganizations));
+
+ const addExcludeOrganizationsToken = (
+ event:
+ | React.KeyboardEvent
+ | React.FocusEvent,
+ ) => {
+ const value = (event.target as HTMLInputElement).value.trim();
+
+ if (
+ value.length > 0 &&
+ !excludeOrganizations.some((v) => v.text === value)
+ ) {
+ setExcludeOrganizations([
+ ...excludeOrganizations,
+ { id: excludeOrganizations.length, text: value },
+ ]);
+ updateFilter('filterExcludeOrganizations', value as Organization, true);
+
+ (event.target as HTMLInputElement).value = '';
+ }
+ };
+
+ const removeExcludeOrganizationToken = (tokenId: string | number) => {
+ const value =
+ excludeOrganizations.find((v) => v.id === tokenId)?.text || '';
+ updateFilter('filterExcludeOrganizations', value as Organization, false);
+
+ setExcludeOrganizations(
+ excludeOrganizations.filter((v) => v.id !== tokenId),
+ );
+ };
+
+ const excludeOrganizationsKeyDown = (
+ event: React.KeyboardEvent,
+ ) => {
+ if (tokenEvents.includes(event.key)) {
+ addExcludeOrganizationsToken(event);
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx
index f1b041d77..2f750c985 100644
--- a/src/renderer/context/App.tsx
+++ b/src/renderer/context/App.tsx
@@ -104,6 +104,8 @@ export const defaultFilters: FilterSettingsState = {
filterUserTypes: [],
filterIncludeHandles: [],
filterExcludeHandles: [],
+ filterIncludeOrganizations: [],
+ filterExcludeOrganizations: [],
filterSubjectTypes: [],
filterStates: [],
filterReasons: [],
@@ -193,6 +195,8 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
settings.filterUserTypes,
settings.filterIncludeHandles,
settings.filterExcludeHandles,
+ settings.filterIncludeOrganizations,
+ settings.filterExcludeOrganizations,
settings.filterReasons,
]);
diff --git a/src/renderer/routes/Filters.tsx b/src/renderer/routes/Filters.tsx
index cce5d8d5e..ebd7c9658 100644
--- a/src/renderer/routes/Filters.tsx
+++ b/src/renderer/routes/Filters.tsx
@@ -3,6 +3,7 @@ import { type FC, useContext } from 'react';
import { FilterIcon, FilterRemoveIcon } from '@primer/octicons-react';
import { Button, Stack, Tooltip } from '@primer/react';
+import { OrganizationFilter } from '../components/filters/OrganizationFilter';
import { ReasonFilter } from '../components/filters/ReasonFilter';
import { StateFilter } from '../components/filters/StateFilter';
import { SubjectTypeFilter } from '../components/filters/SubjectTypeFilter';
@@ -27,6 +28,7 @@ export const FiltersRoute: FC = () => {
+
diff --git a/src/renderer/routes/__snapshots__/Filters.test.tsx.snap b/src/renderer/routes/__snapshots__/Filters.test.tsx.snap
index 672797719..dae095aab 100644
--- a/src/renderer/routes/__snapshots__/Filters.test.tsx.snap
+++ b/src/renderer/routes/__snapshots__/Filters.test.tsx.snap
@@ -511,6 +511,230 @@ exports[`renderer/routes/Filters.tsx General should render itself & its children
+