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, }; };