diff --git a/apps/backend/src/api/routes/settings.controller.ts b/apps/backend/src/api/routes/settings.controller.ts index c92537b4..b99044bf 100644 --- a/apps/backend/src/api/routes/settings.controller.ts +++ b/apps/backend/src/api/routes/settings.controller.ts @@ -4,6 +4,7 @@ import { Organization } from '@prisma/client'; import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.team.member.dto'; +import { ShortlinkPreferenceDto } from '@gitroom/nestjs-libraries/dtos/settings/shortlink-preference.dto'; import { ApiTags } from '@nestjs/swagger'; import { AuthorizationActions, Sections } from '@gitroom/backend/services/auth/permissions/permission.exception.class'; @@ -46,4 +47,21 @@ export class SettingsController { ) { return this._organizationService.deleteTeamMember(org, id); } + + @Get('/shortlink') + async getShortlinkPreference(@GetOrgFromRequest() org: Organization) { + return this._organizationService.getShortlinkPreference(org.id); + } + + @Post('/shortlink') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async updateShortlinkPreference( + @GetOrgFromRequest() org: Organization, + @Body() body: ShortlinkPreferenceDto + ) { + return this._organizationService.updateShortlinkPreference( + org.id, + body.shortlink + ); + } } diff --git a/apps/frontend/src/components/new-launch/manage.modal.tsx b/apps/frontend/src/components/new-launch/manage.modal.tsx index 202daff9..ca4e5a3c 100644 --- a/apps/frontend/src/components/new-launch/manage.modal.tsx +++ b/apps/frontend/src/components/new-launch/manage.modal.tsx @@ -41,6 +41,7 @@ import { DropdownArrowSmallIcon, } from '@gitroom/frontend/components/ui/icons'; import { useHasScroll } from '@gitroom/frontend/components/ui/is.scroll.hook'; +import { useShortlinkPreference } from '@gitroom/frontend/components/settings/shortlink-preference.component'; function countCharacters(text: string, type: string): number { if (type !== 'x') { @@ -58,6 +59,7 @@ export const ManageModal: FC = (props) => { const toaster = useToaster(); const modal = useModals(); const [showSettings, setShowSettings] = useState(false); + const { data: shortlinkPreferenceData } = useShortlinkPreference(); const { addEditSets, mutate, customClose, dummy } = props; @@ -276,28 +278,38 @@ export const ManageModal: FC = (props) => { } } - const shortLinkUrl = dummy - ? { ask: false } - : await ( - await fetch('/posts/should-shortlink', { - method: 'POST', - body: JSON.stringify({ - messages: checkAllValid.flatMap((p: any) => - p.values.flatMap((a: any) => a.content) - ), - }), - }) - ).json(); + const shortlinkPreference = shortlinkPreferenceData?.shortlink || 'ASK'; - const shortLink = !shortLinkUrl.ask - ? false - : await deleteDialog( - t( - 'shortlink_urls_question', - 'Do you want to shortlink the URLs? it will let you get statistics over clicks' - ), - t('yes_shortlink_it', 'Yes, shortlink it!') - ); + let shortLink = false; + + if (!dummy && shortlinkPreference !== 'NO') { + const shortLinkUrl = await ( + await fetch('/posts/should-shortlink', { + method: 'POST', + body: JSON.stringify({ + messages: checkAllValid.flatMap((p: any) => + p.values.flatMap((a: any) => a.content) + ), + }), + }) + ).json(); + + if (shortLinkUrl.ask) { + if (shortlinkPreference === 'YES') { + // Automatically shortlink without asking + shortLink = true; + } else { + // ASK: Show the dialog + shortLink = await deleteDialog( + t( + 'shortlink_urls_question', + 'Do you want to shortlink the URLs? it will let you get statistics over clicks' + ), + t('yes_shortlink_it', 'Yes, shortlink it!') + ); + } + } + } const group = existingData.group || makeId(10); const data = { @@ -373,7 +385,7 @@ export const ManageModal: FC = (props) => { } } }, - [ref, repeater, tags, date, addEditSets, dummy] + [ref, repeater, tags, date, addEditSets, dummy, shortlinkPreferenceData] ); return ( diff --git a/apps/frontend/src/components/settings/global.settings.tsx b/apps/frontend/src/components/settings/global.settings.tsx index 970a960a..6fc56cd8 100644 --- a/apps/frontend/src/components/settings/global.settings.tsx +++ b/apps/frontend/src/components/settings/global.settings.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import dynamic from 'next/dynamic'; import EmailNotificationsComponent from '@gitroom/frontend/components/settings/email-notifications.component'; +import ShortlinkPreferenceComponent from '@gitroom/frontend/components/settings/shortlink-preference.component'; const MetricComponent = dynamic( () => import('@gitroom/frontend/components/settings/metric.component'), @@ -19,6 +20,7 @@ export const GlobalSettings = () => {

{t('global_settings', 'Global Settings')}

+ ); }; diff --git a/apps/frontend/src/components/settings/shortlink-preference.component.tsx b/apps/frontend/src/components/settings/shortlink-preference.component.tsx new file mode 100644 index 00000000..fa622695 --- /dev/null +++ b/apps/frontend/src/components/settings/shortlink-preference.component.tsx @@ -0,0 +1,117 @@ +'use client'; + +import React, { useCallback, useEffect, useState } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import useSWR from 'swr'; +import { Select } from '@gitroom/react/form/select'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; + +type ShortLinkPreference = 'ASK' | 'YES' | 'NO'; + +interface ShortlinkPreferenceResponse { + shortlink: ShortLinkPreference; +} + +export const useShortlinkPreference = () => { + const fetch = useFetch(); + + const load = useCallback(async () => { + return (await fetch('/settings/shortlink')).json(); + }, []); + + return useSWR('shortlink-preference', load, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: true, + refreshWhenHidden: false, + refreshWhenOffline: false, + }); +}; + +const ShortlinkPreferenceComponent = () => { + const t = useT(); + const fetch = useFetch(); + const toaster = useToaster(); + const { data, isLoading, mutate } = useShortlinkPreference(); + + const [localValue, setLocalValue] = useState('ASK'); + + // Sync local state with fetched data + useEffect(() => { + if (data?.shortlink) { + setLocalValue(data.shortlink); + } + }, [data]); + + const handleChange = useCallback( + async (event: React.ChangeEvent) => { + const newValue = event.target.value as ShortLinkPreference; + + // Update local state immediately + setLocalValue(newValue); + + await fetch('/settings/shortlink', { + method: 'POST', + body: JSON.stringify({ shortlink: newValue }), + }); + + mutate({ shortlink: newValue }); + toaster.show(t('settings_updated', 'Settings updated'), 'success'); + }, + [fetch, mutate, toaster, t] + ); + + if (isLoading) { + return ( +
+
{t('loading', 'Loading...')}
+
+ ); + } + + return ( +
+
+ {t('shortlink_settings', 'Shortlink Settings')} +
+
+
+
+ {t('shortlink_preference', 'Shortlink Preference')} +
+
+ {t( + 'shortlink_preference_description', + 'Control how URLs in your posts are handled. Shortlinks provide click statistics.' + )} +
+
+
+ +
+
+
+ ); +}; + +export default ShortlinkPreferenceComponent; + diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index 6da496ca..57bae74e 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -1,5 +1,5 @@ import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service'; -import { Role, SubscriptionTier } from '@prisma/client'; +import { Role, ShortLinkPreference, SubscriptionTier } from '@prisma/client'; import { Injectable } from '@nestjs/common'; import { AuthService } from '@gitroom/helpers/auth/auth.service'; import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto'; @@ -329,4 +329,26 @@ export class OrganizationRepository { }, }); } + + getShortlinkPreference(orgId: string) { + return this._organization.model.organization.findUnique({ + where: { + id: orgId, + }, + select: { + shortlink: true, + }, + }); + } + + updateShortlinkPreference(orgId: string, shortlink: ShortLinkPreference) { + return this._organization.model.organization.update({ + where: { + id: orgId, + }, + data: { + shortlink, + }, + }); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts index f523ea8f..c9fca065 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.service.ts @@ -6,7 +6,7 @@ import { AddTeamMemberDto } from '@gitroom/nestjs-libraries/dtos/settings/add.te import { AuthService } from '@gitroom/helpers/auth/auth.service'; import dayjs from 'dayjs'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; -import { Organization } from '@prisma/client'; +import { Organization, ShortLinkPreference } from '@prisma/client'; import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service'; @Injectable() @@ -111,4 +111,15 @@ export class OrganizationService { disable ); } + + getShortlinkPreference(orgId: string) { + return this._organizationRepository.getShortlinkPreference(orgId); + } + + updateShortlinkPreference(orgId: string, shortlink: ShortLinkPreference) { + return this._organizationRepository.updateShortlinkPreference( + orgId, + shortlink + ); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index d3c5d771..32310bb9 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -9,15 +9,16 @@ datasource db { } model Organization { - id String @id @default(uuid()) + id String @id @default(uuid()) name String description String? apiKey String? paymentId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - allowTrial Boolean @default(false) - isTrailing Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + allowTrial Boolean @default(false) + isTrailing Boolean @default(false) + shortlink ShortLinkPreference @default(ASK) autoPost AutoPost[] Comments Comments[] credits Credits[] @@ -879,3 +880,9 @@ enum APPROVED_SUBMIT_FOR_ORDER { WAITING_CONFIRMATION YES } + +enum ShortLinkPreference { + ASK + YES + NO +} diff --git a/libraries/nestjs-libraries/src/dtos/settings/shortlink-preference.dto.ts b/libraries/nestjs-libraries/src/dtos/settings/shortlink-preference.dto.ts new file mode 100644 index 00000000..0cc17941 --- /dev/null +++ b/libraries/nestjs-libraries/src/dtos/settings/shortlink-preference.dto.ts @@ -0,0 +1,8 @@ +import { IsEnum } from 'class-validator'; +import { ShortLinkPreference } from '@prisma/client'; + +export class ShortlinkPreferenceDto { + @IsEnum(ShortLinkPreference) + shortlink: ShortLinkPreference; +} +