diff --git a/graphql/resolvers/Filter.ts b/graphql/resolvers/Filter.ts
index 53a2fd948..d50599166 100644
--- a/graphql/resolvers/Filter.ts
+++ b/graphql/resolvers/Filter.ts
@@ -1,7 +1,8 @@
import { arg, nonNull } from 'nexus';
import { ObjectDefinitionBlock } from 'nexus/dist/core';
-import { Filter, FilterCreateInput, FilterInput } from '../types';
+import { Filter, FilterCreateInput, FilterInput, Activity, SubscriptionToggleInput } from '../types';
+import { connectionMap } from '../queries/connections';
export const query = (t: ObjectDefinitionBlock<'Query'>) => {
t.field('filter', {
@@ -12,15 +13,22 @@ export const query = (t: ObjectDefinitionBlock<'Query'>) => {
resolve: async (_, { data }, { db, activity }) => {
if (!activity) return null;
- try {
- return db.filter.findUnique({
- where: {
- id: data.id,
- },
- });
- } catch (error) {
- throw Error(`${error}`);
- }
+ const filter = await db.filter.findUnique({
+ where: {
+ id: data.id,
+ },
+ include: {
+ stargizers: true,
+ },
+ });
+
+ if (!filter) return null;
+
+ return {
+ ...filter,
+ _isOwner: filter?.activityId === activity.id,
+ _isStarred: filter?.stargizers?.some((stargizer) => stargizer?.id === activity.id),
+ };
},
});
};
@@ -47,6 +55,37 @@ export const mutation = (t: ObjectDefinitionBlock<'Mutation'>) => {
},
});
+ t.field('toggleFilterStargizer', {
+ type: Activity,
+ args: {
+ data: nonNull(arg({ type: SubscriptionToggleInput })),
+ },
+ resolve: async (_, { data: { id, direction } }, { db, activity }) => {
+ if (!activity) return null;
+
+ const connection = { id };
+
+ try {
+ return db.activity.update({
+ where: { id: activity.id },
+ data: {
+ filterStargizers: { [connectionMap[String(direction)]]: connection },
+ },
+ });
+
+ // await mailServer.sendMail({
+ // from: `"Fred Foo 👻" <${process.env.MAIL_USER}>`,
+ // to: 'bar@example.com, baz@example.com',
+ // subject: 'Hello ✔',
+ // text: `new post '${title}'`,
+ // html: `new post ${title}`,
+ // });
+ } catch (error) {
+ throw Error(`${error}`);
+ }
+ },
+ });
+
t.field('deleteFilter', {
type: Filter,
args: {
diff --git a/graphql/schema.graphql b/graphql/schema.graphql
index e422180d4..be50158b0 100644
--- a/graphql/schema.graphql
+++ b/graphql/schema.graphql
@@ -67,6 +67,8 @@ input EstimateInput {
}
type Filter {
+ _isOwner: Boolean
+ _isStarred: Boolean
activity: Activity
activityId: String!
createdAt: DateTime!
@@ -231,6 +233,7 @@ type Mutation {
deleteComment(data: CommentDeleteInput!): Comment
deleteFilter(data: FilterInput!): Filter
deleteProject(data: ProjectDelete!): Project
+ toggleFilterStargizer(data: SubscriptionToggleInput!): Activity
toggleGoalArchive(data: GoalArchiveInput!): Activity
toggleGoalDependency(data: GoalDependencyToggleInput!): Goal
toggleGoalStargizer(data: SubscriptionToggleInput!): Activity
diff --git a/graphql/types.ts b/graphql/types.ts
index 7252869ee..a897ef49a 100644
--- a/graphql/types.ts
+++ b/graphql/types.ts
@@ -271,6 +271,10 @@ export const Filter = objectType({
t.list.field('stargizers', { type: Activity });
t.field(FilterModel.createdAt);
t.field(FilterModel.updatedAt);
+
+ // calculated fields
+ t.boolean('_isStarred');
+ t.boolean('_isOwner');
},
});
diff --git a/src/components/FiltersPanel/FiltersPanel.tsx b/src/components/FiltersPanel/FiltersPanel.tsx
index 18b583215..9fcf9b505 100644
--- a/src/components/FiltersPanel/FiltersPanel.tsx
+++ b/src/components/FiltersPanel/FiltersPanel.tsx
@@ -256,13 +256,14 @@ export const FiltersPanel: React.FC = ({
/>
))}
- {Boolean(queryString) && !currentPreset && (
+ {((Boolean(queryString) && !currentPreset) ||
+ (currentPreset && !currentPreset._isOwner && !currentPreset._isStarred)) && (
)}
- {currentPreset && (
+ {currentPreset && (currentPreset._isOwner || currentPreset._isStarred) && (
diff --git a/src/components/NotificationsHub/NotificationsHub.i18n/en.json b/src/components/NotificationsHub/NotificationsHub.i18n/en.json
index bc5ec7068..5ecd5586d 100644
--- a/src/components/NotificationsHub/NotificationsHub.i18n/en.json
+++ b/src/components/NotificationsHub/NotificationsHub.i18n/en.json
@@ -3,5 +3,8 @@
"Voila! Saved successfully 🎉! Use and share it with teammates 😉": "Voila! Saved successfully 🎉! Use and share it with teammates 😉",
"Something went wrong 😿": "Something went wrong 😿",
"We are deleting your filter...": "We are deleting your filter...",
- "Deleted successfully 🎉!": "Deleted successfully 🎉!"
+ "Deleted successfully 🎉!": "Deleted successfully 🎉!",
+ "We are calling owner...": "We are calling owner...",
+ "So sad! We will miss you": "So sad! We will miss you",
+ "Voila! You are stargizer now 🎉": "Voila! You are stargizer now 🎉"
}
diff --git a/src/components/NotificationsHub/NotificationsHub.i18n/ru.json b/src/components/NotificationsHub/NotificationsHub.i18n/ru.json
index 0656ddde4..f617ce8f9 100644
--- a/src/components/NotificationsHub/NotificationsHub.i18n/ru.json
+++ b/src/components/NotificationsHub/NotificationsHub.i18n/ru.json
@@ -3,5 +3,8 @@
"Voila! Saved successfully 🎉! Use and share it with teammates 😉": "Ура! Используй сам и делись с другими членами команды 😉",
"Something went wrong 😿": "Кажется что-то пошло не по плану 😿. Мы уже в курсе и чиним проблему. Прости 🙏",
"We are deleting your filter...": "Удаляем твой фильтр...",
- "Deleted successfully 🎉!": "Удалили 🎉!"
+ "Deleted successfully 🎉!": "Удалили 🎉!",
+ "We are calling owner...": "Звоним владельцу...",
+ "So sad! We will miss you": "Как грустно! Мы будем скучать",
+ "Voila! You are stargizer now 🎉": "Ура! Добавили в избранное 🎉"
}
diff --git a/src/components/pages/GoalsPage/GoalsPage.tsx b/src/components/pages/GoalsPage/GoalsPage.tsx
index a851114b8..298ee3328 100644
--- a/src/components/pages/GoalsPage/GoalsPage.tsx
+++ b/src/components/pages/GoalsPage/GoalsPage.tsx
@@ -8,14 +8,15 @@ import { nullable, Button } from '@taskany/bricks';
import { Filter, Goal, GoalsMetaOutput, Project } from '../../../../graphql/@generated/genql';
import { createFetcher, refreshInterval } from '../../../utils/createFetcher';
import { declareSsrProps, ExternalPageProps } from '../../../utils/declareSsrProps';
+import { ModalEvent, dispatchModalEvent } from '../../../utils/dispatchModal';
+import { createFilterKeys } from '../../../utils/hotkeys';
+import { parseFilterValues, useUrlFilterParams } from '../../../hooks/useUrlFilterParams';
+import { useFilterResource } from '../../../hooks/useFilterResource';
import { Priority } from '../../../types/priority';
import { Page, PageContent } from '../../Page';
import { CommonHeader } from '../../CommonHeader';
import { FiltersPanel } from '../../FiltersPanel/FiltersPanel';
-import { parseFilterValues, useUrlFilterParams } from '../../../hooks/useUrlFilterParams';
import { GoalsGroup, GoalsGroupProjectTitle } from '../../GoalsGroup';
-import { ModalEvent, dispatchModalEvent } from '../../../utils/dispatchModal';
-import { createFilterKeys } from '../../../utils/hotkeys';
import { PageTitle } from '../../PageTitle';
import { tr } from './GoalsPage.i18n';
@@ -157,23 +158,29 @@ const filterFetcher = createFetcher((_, id = '') => ({
description: true,
mode: true,
params: true,
+ _isOwner: true,
+ _isStarred: true,
},
],
}));
export const getServerSideProps = declareSsrProps(
async ({ user, query }) => {
- const { filter: preset } = query.filter ? await filterFetcher(user, query.filter) : { filter: null };
+ const presetData = query.filter ? await filterFetcher(user, query.filter) : { filter: null };
return {
- preset,
fallback: {
[unstable_serialize(query)]: await fetcher(
user,
...Object.values(
- parseFilterValues(preset ? Object.fromEntries(new URLSearchParams(preset.params)) : query),
+ parseFilterValues(
+ presetData.filter
+ ? Object.fromEntries(new URLSearchParams(presetData.filter.params))
+ : query,
+ ),
),
),
+ [unstable_serialize(query.filter)]: presetData,
},
};
},
@@ -182,9 +189,18 @@ export const getServerSideProps = declareSsrProps(
},
);
-export const GoalsPage = ({ user, ssrTime, locale, fallback, preset }: ExternalPageProps) => {
+export const GoalsPage = ({ user, ssrTime, locale, fallback }: ExternalPageProps) => {
const router = useRouter();
const [preview, setPreview] = useState(null);
+ const { toggleFilterStar } = useFilterResource();
+
+ const { data: presetData, mutate: presetMutate } = useSWR(
+ unstable_serialize(router.query.filter),
+ (f: string) => filterFetcher(user, f),
+ {
+ fallback,
+ },
+ );
const {
currentPreset,
@@ -201,7 +217,7 @@ export const GoalsPage = ({ user, ssrTime, locale, fallback, preset }: ExternalP
resetQueryState,
setPreset,
} = useUrlFilterParams({
- preset,
+ preset: presetData?.filter,
});
const { data, isLoading } = useSWR(
@@ -260,11 +276,21 @@ export const GoalsPage = ({ user, ssrTime, locale, fallback, preset }: ExternalP
const selectedGoalResolver = useCallback((id: string) => id === preview?.id, [preview]);
- const onFilterStar = useCallback(() => {
- currentPreset
- ? dispatchModalEvent(ModalEvent.FilterDeleteModal)()
- : dispatchModalEvent(ModalEvent.FilterCreateModal)();
- }, [currentPreset]);
+ const onFilterStar = useCallback(async () => {
+ if (currentPreset) {
+ if (currentPreset._isOwner) {
+ dispatchModalEvent(ModalEvent.FilterDeleteModal)();
+ } else {
+ await toggleFilterStar({
+ id: currentPreset.id,
+ direction: !currentPreset._isStarred,
+ });
+ await presetMutate();
+ }
+ } else {
+ dispatchModalEvent(ModalEvent.FilterCreateModal)();
+ }
+ }, [currentPreset, toggleFilterStar, presetMutate]);
const onFilterCreated = useCallback(
(data: Partial) => {
diff --git a/src/components/pages/ProjectPage/ProjectPage.tsx b/src/components/pages/ProjectPage/ProjectPage.tsx
index bdf578f35..82a4d6dea 100644
--- a/src/components/pages/ProjectPage/ProjectPage.tsx
+++ b/src/components/pages/ProjectPage/ProjectPage.tsx
@@ -12,6 +12,7 @@ import { FiltersPanel } from '../../FiltersPanel/FiltersPanel';
import { ModalEvent, dispatchModalEvent } from '../../../utils/dispatchModal';
import { parseFilterValues, useUrlFilterParams } from '../../../hooks/useUrlFilterParams';
import { useLocalStorage } from '../../../hooks/useLocalStorage';
+import { useFilterResource } from '../../../hooks/useFilterResource';
import { useWillUnmount } from '../../../hooks/useWillUnmount';
import { ProjectPageLayout } from '../../ProjectPageLayout/ProjectPageLayout';
import { Page, PageContent } from '../../Page';
@@ -224,27 +225,31 @@ const filterFetcher = createFetcher((_, id = '') => ({
description: true,
mode: true,
params: true,
+ _isOwner: true,
+ _isStarred: true,
},
],
}));
export const getServerSideProps = declareSsrProps(
async ({ user, params: { id }, query }) => {
- const { filter: preset } = query.filter ? await filterFetcher(user, query.filter) : { filter: null };
+ const presetData = query.filter ? await filterFetcher(user, query.filter) : { filter: null };
const ssrData = await fetcher(
user,
id,
...Object.values(
- parseFilterValues(preset ? Object.fromEntries(new URLSearchParams(preset.params)) : query),
+ parseFilterValues(
+ presetData.filter ? Object.fromEntries(new URLSearchParams(presetData.filter.params)) : query,
+ ),
),
);
return ssrData.project
? {
- preset,
fallback: {
[unstable_serialize(query)]: ssrData,
+ [unstable_serialize(query.filter)]: presetData,
},
}
: {
@@ -256,10 +261,19 @@ export const getServerSideProps = declareSsrProps(
},
);
-export const ProjectPage = ({ user, locale, ssrTime, fallback, preset, params: { id } }: ExternalPageProps) => {
+export const ProjectPage = ({ user, locale, ssrTime, fallback, params: { id } }: ExternalPageProps) => {
const nextRouter = useNextRouter();
const [preview, setPreview] = useState(null);
const [, setCurrentProjectCache] = useLocalStorage('currentProjectCache', null);
+ const { toggleFilterStar } = useFilterResource();
+
+ const { data: presetData, mutate: presetMutate } = useSWR(
+ unstable_serialize(nextRouter.query.filter),
+ (f: string) => filterFetcher(user, f),
+ {
+ fallback,
+ },
+ );
const {
currentPreset,
@@ -276,7 +290,7 @@ export const ProjectPage = ({ user, locale, ssrTime, fallback, preset, params: {
resetQueryState,
setPreset,
} = useUrlFilterParams({
- preset,
+ preset: presetData?.filter,
});
const { data, isLoading } = useSWR(
@@ -352,11 +366,21 @@ export const ProjectPage = ({ user, locale, ssrTime, fallback, preset, params: {
setCurrentProjectCache(null);
});
- const onFilterStar = useCallback(() => {
- currentPreset
- ? dispatchModalEvent(ModalEvent.FilterDeleteModal)()
- : dispatchModalEvent(ModalEvent.FilterCreateModal)();
- }, [currentPreset]);
+ const onFilterStar = useCallback(async () => {
+ if (currentPreset) {
+ if (currentPreset._isOwner) {
+ dispatchModalEvent(ModalEvent.FilterDeleteModal)();
+ } else {
+ await toggleFilterStar({
+ id: currentPreset.id,
+ direction: !currentPreset._isStarred,
+ });
+ await presetMutate();
+ }
+ } else {
+ dispatchModalEvent(ModalEvent.FilterCreateModal)();
+ }
+ }, [currentPreset, toggleFilterStar, presetMutate]);
const onFilterCreated = useCallback(
(data: Partial) => {
diff --git a/src/hooks/useFilterResource.ts b/src/hooks/useFilterResource.ts
index 20a6c0392..874665fc3 100644
--- a/src/hooks/useFilterResource.ts
+++ b/src/hooks/useFilterResource.ts
@@ -1,7 +1,7 @@
import { gql } from '../utils/gql';
import { notifyPromise } from '../utils/notifyPromise';
import { CreateFormType } from '../schema/filter';
-import { FilterInput } from '../../graphql/@generated/genql';
+import { FilterInput, SubscriptionToggleInput } from '../../graphql/@generated/genql';
export const useFilterResource = () => {
const createFilter = (data: CreateFormType) =>
@@ -23,6 +23,25 @@ export const useFilterResource = () => {
},
);
+ const toggleFilterStar = (data: SubscriptionToggleInput) =>
+ notifyPromise(
+ gql.mutation({
+ toggleFilterStargizer: [
+ {
+ data,
+ },
+ {
+ id: true,
+ },
+ ],
+ }),
+ {
+ onPending: 'We are calling owner...',
+ onSuccess: data.direction ? 'Voila! You are stargizer now 🎉' : 'So sad! We will miss you',
+ onError: 'Something went wrong 😿',
+ },
+ );
+
const deleteFilter = (data: FilterInput) =>
notifyPromise(
gql.mutation({
@@ -44,6 +63,7 @@ export const useFilterResource = () => {
return {
createFilter,
+ toggleFilterStar,
deleteFilter,
};
};