feat: ask for shortlinking

This commit is contained in:
Nevo David 2026-01-19 16:33:24 +07:00
parent d8b8b3d629
commit 5bad88daa9
8 changed files with 226 additions and 29 deletions

View File

@ -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
);
}
}

View File

@ -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<AddEditModalProps> = (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<AddEditModalProps> = (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<AddEditModalProps> = (props) => {
}
}
},
[ref, repeater, tags, date, addEditSets, dummy]
[ref, repeater, tags, date, addEditSets, dummy, shortlinkPreferenceData]
);
return (

View File

@ -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 = () => {
<h3 className="text-[20px]">{t('global_settings', 'Global Settings')}</h3>
<MetricComponent />
<EmailNotificationsComponent />
<ShortlinkPreferenceComponent />
</div>
);
};

View File

@ -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<ShortlinkPreferenceResponse>('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<ShortLinkPreference>('ASK');
// Sync local state with fetched data
useEffect(() => {
if (data?.shortlink) {
setLocalValue(data.shortlink);
}
}, [data]);
const handleChange = useCallback(
async (event: React.ChangeEvent<HTMLSelectElement>) => {
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 (
<div className="my-[16px] mt-[16px] bg-sixth border-fifth border rounded-[4px] p-[24px]">
<div className="animate-pulse">{t('loading', 'Loading...')}</div>
</div>
);
}
return (
<div className="my-[16px] mt-[16px] bg-sixth border-fifth border rounded-[4px] p-[24px] flex flex-col gap-[24px]">
<div className="mt-[4px]">
{t('shortlink_settings', 'Shortlink Settings')}
</div>
<div className="flex items-center justify-between gap-[24px]">
<div className="flex flex-col flex-1">
<div className="text-[14px]">
{t('shortlink_preference', 'Shortlink Preference')}
</div>
<div className="text-[12px] text-customColor18">
{t(
'shortlink_preference_description',
'Control how URLs in your posts are handled. Shortlinks provide click statistics.'
)}
</div>
</div>
<div className="w-[200px]">
<Select
name="shortlink"
label=""
disableForm={true}
hideErrors={true}
value={localValue}
onChange={handleChange}
>
<option value="ASK">
{t('shortlink_ask', 'Ask every time')}
</option>
<option value="YES">
{t('shortlink_yes', 'Always shortlink')}
</option>
<option value="NO">
{t('shortlink_no', 'Never shortlink')}
</option>
</Select>
</div>
</div>
</div>
);
};
export default ShortlinkPreferenceComponent;

View File

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

View File

@ -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
);
}
}

View File

@ -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
}

View File

@ -0,0 +1,8 @@
import { IsEnum } from 'class-validator';
import { ShortLinkPreference } from '@prisma/client';
export class ShortlinkPreferenceDto {
@IsEnum(ShortLinkPreference)
shortlink: ShortLinkPreference;
}