Merge pull request #811 from gitroomhq/feat/sets

Sets feature
This commit is contained in:
Nevo David 2025-06-09 20:02:57 +07:00 committed by GitHub
commit abb1e4155c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 920 additions and 445 deletions

View File

@ -33,6 +33,7 @@ import { SignatureController } from '@gitroom/backend/api/routes/signature.contr
import { AutopostController } from '@gitroom/backend/api/routes/autopost.controller';
import { McpService } from '@gitroom/nestjs-libraries/mcp/mcp.service';
import { McpController } from '@gitroom/backend/api/routes/mcp.controller';
import { SetsController } from '@gitroom/backend/api/routes/sets.controller';
const authenticatedController = [
UsersController,
@ -50,6 +51,7 @@ const authenticatedController = [
WebhookController,
SignatureController,
AutopostController,
SetsController,
];
@Module({
imports: [UploadModule],

View File

@ -0,0 +1,57 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { Organization } from '@prisma/client';
import { ApiTags } from '@nestjs/swagger';
import { SetsService } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.service';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import {
AuthorizationActions,
Sections,
} from '@gitroom/backend/services/auth/permissions/permissions.service';
import {
UpdateSetsDto,
SetsDto,
} from '@gitroom/nestjs-libraries/dtos/sets/sets.dto';
@ApiTags('Sets')
@Controller('/sets')
export class SetsController {
constructor(private _setsService: SetsService) {}
@Get('/')
async getSets(@GetOrgFromRequest() org: Organization) {
return this._setsService.getSets(org.id);
}
@Post('/')
async createASet(
@GetOrgFromRequest() org: Organization,
@Body() body: SetsDto
) {
return this._setsService.createSet(org.id, body);
}
@Put('/')
async updateSet(
@GetOrgFromRequest() org: Organization,
@Body() body: UpdateSetsDto
) {
return this._setsService.createSet(org.id, body);
}
@Delete('/:id')
async deleteSet(
@GetOrgFromRequest() org: Organization,
@Param('id') id: string
) {
return this._setsService.deleteSet(org.id, id);
}
}

View File

@ -61,16 +61,22 @@ import { TagsComponent } from './tags.component';
import { RepeatComponent } from '@gitroom/frontend/components/launches/repeat.component';
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import { uniq } from 'lodash';
import { SetContext } from '@gitroom/frontend/components/launches/set.context';
function countCharacters(text: string, type: string): number {
if (type !== 'x') {
return text.length;
}
return weightedLength(text);
}
export const AddEditModal: FC<{
date: dayjs.Dayjs;
integrations: Integrations[];
allIntegrations?: Integrations[];
set?: CreatePostDto;
addEditSets?: (data: any) => void;
reopenModal: () => void;
mutate: () => void;
padding?: string;
@ -92,7 +98,10 @@ export const AddEditModal: FC<{
onlyValues,
padding,
customClose,
addEditSets,
set,
} = props;
const [customer, setCustomer] = useState('');
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
@ -103,21 +112,44 @@ export const AddEditModal: FC<{
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback<
Integrations[]
>([]);
const integrations = useMemo(() => {
if (!customer) {
return ints;
}
const list = ints.filter((f) => f?.customer?.id === customer);
if (list.length === 1) {
if (list.length === 1 && !set) {
setSelectedIntegrations([list[0]]);
}
return list;
}, [customer, ints]);
}, [customer, ints, set]);
const [dateState, setDateState] = useState(date);
// hook to open a new modal
const modal = useModals();
const selectIntegrationsDefault = useMemo(() => {
if (!set) {
return [];
}
const keepReference: Integrations[] = [];
const neededIntegrations = uniq(set.posts.flatMap((p) => p.integration.id));
for (const i of ints) {
if (neededIntegrations.indexOf(i.id) > -1) {
keepReference.push(i);
}
}
return keepReference;
}, [set]);
useEffect(() => {
if (set?.posts) {
setSelectedIntegrations(selectIntegrationsDefault);
}
}, [selectIntegrationsDefault]);
// value of each editor
const [value, setValue] = useState<
Array<{
@ -131,6 +163,12 @@ export const AddEditModal: FC<{
>(
onlyValues
? onlyValues
: set
? set?.posts?.[0]?.value || [
{
content: '',
},
]
: [
{
content: '',
@ -436,41 +474,52 @@ export const AddEditModal: FC<{
'Yes, shortlink it!'
);
setLoading(true);
await fetch('/posts', {
method: 'POST',
body: JSON.stringify({
...(postFor
? {
order: postFor.id,
}
: {}),
type,
inter,
tags,
shortLink,
date: dateState.utc().format('YYYY-MM-DDTHH:mm:ss'),
posts: allKeys.map((p) => ({
...p,
value: p.value.map((a) => ({
...a,
content: a.content.slice(0, p.maximumCharacters || 1000000),
})),
const data = {
...(postFor
? {
order: postFor.id,
}
: {}),
type,
inter,
tags,
shortLink,
date: dateState.utc().format('YYYY-MM-DDTHH:mm:ss'),
posts: allKeys.map((p) => ({
...p,
value: p.value.map((a) => ({
...a,
content: a.content.slice(0, p.maximumCharacters || 1000000),
})),
}),
});
})),
};
addEditSets
? addEditSets(data)
: await fetch('/posts', {
method: 'POST',
body: JSON.stringify(data),
});
existingData.group = makeId(10);
mutate();
toaster.show(
!existingData.integration
? 'Added successfully'
: 'Updated successfully'
);
if (!addEditSets) {
mutate();
toaster.show(
!existingData.integration
? 'Added successfully'
: 'Updated successfully'
);
}
if (customClose) {
setTimeout(() => {
customClose();
}, 2000);
}
modal.closeAll();
if (!addEditSets) {
modal.closeAll();
}
},
[
inter,
@ -481,6 +530,7 @@ export const AddEditModal: FC<{
existingData,
selectedIntegrations,
tags,
addEditSets,
]
);
const uppy = useUppyUploader({
@ -569,7 +619,7 @@ export const AddEditModal: FC<{
}, [data, postFor, selectedIntegrations]);
useClickOutside(askClose);
return (
<>
<SetContext.Provider value={{ set }}>
{user?.tier?.ai && (
<CopilotPopup
hitEscapeToClose={false}
@ -649,7 +699,7 @@ Here are the things you can do:
<PickPlatforms
toolTip={true}
integrations={integrations.filter((f) => !f.disabled)}
selectedIntegrations={[]}
selectedIntegrations={selectIntegrationsDefault}
singleSelect={false}
onChange={setSelectedIntegrations}
isMain={true}
@ -811,70 +861,84 @@ Here are the things you can do:
{t('delete_post', 'Delete Post')}
</Button>
)}
<Button
onClick={schedule('draft')}
className="rounded-[4px] border-2 border-customColor21"
secondary={true}
disabled={selectedIntegrations.length === 0}
>
{t('save_as_draft', 'Save as draft')}
</Button>
<Button
onClick={schedule('schedule')}
className="rounded-[4px] relative group"
disabled={
selectedIntegrations.length === 0 ||
loading ||
!canSendForPublication
}
>
<div className="flex justify-center items-center gap-[5px] h-full">
<div className="h-full flex items-center text-white">
{!canSendForPublication
? t('not_matching_order', 'Not matching order')
: postFor
? t('submit_for_order', 'Submit for order')
: !existingData.integration
? selectedIntegrations.length === 0
? t(
'select_channels_from_circles',
'Select channels from the circles above'
)
: t('add_to_calendar', 'Add to calendar')
: // @ts-ignore
existingData?.posts?.[0]?.state === 'DRAFT'
? t('schedule', 'Schedule')
: t('update', 'Update')}
</div>
{!postFor && (
<div className="h-full flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M15.0233 7.14804L9.39828 12.773C9.34604 12.8253 9.284 12.8668 9.21572 12.8951C9.14743 12.9234 9.07423 12.938 9.00031 12.938C8.92639 12.938 8.8532 12.9234 8.78491 12.8951C8.71662 12.8668 8.65458 12.8253 8.60234 12.773L2.97734 7.14804C2.8718 7.04249 2.8125 6.89934 2.8125 6.75007C2.8125 6.6008 2.8718 6.45765 2.97734 6.3521C3.08289 6.24655 3.22605 6.18726 3.37531 6.18726C3.52458 6.18726 3.66773 6.24655 3.77328 6.3521L9.00031 11.5798L14.2273 6.3521C14.2796 6.29984 14.3417 6.25838 14.4099 6.2301C14.4782 6.20181 14.5514 6.18726 14.6253 6.18726C14.6992 6.18726 14.7724 6.20181 14.8407 6.2301C14.909 6.25838 14.971 6.29984 15.0233 6.3521C15.0755 6.40436 15.117 6.46641 15.1453 6.53469C15.1736 6.60297 15.1881 6.67616 15.1881 6.75007C15.1881 6.82398 15.1736 6.89716 15.1453 6.96545C15.117 7.03373 15.0755 7.09578 15.0233 7.14804Z"
fill="white"
/>
</svg>
<div
onClick={postNow}
className={clsx(
'hidden group-hover:flex hover:flex flex-col justify-center absolute start-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder',
loading &&
'cursor-not-allowed pointer-events-none opacity-50'
)}
>
{t('post_now', 'Post now')}
</div>
{addEditSets && (
<Button
onClick={schedule('draft')}
className="rounded-[4px] relative group"
>
{t('save_set', 'Save Set')}
</Button>
)}
{!addEditSets && (
<Button
onClick={schedule('draft')}
className="rounded-[4px] border-2 border-customColor21"
secondary={true}
disabled={selectedIntegrations.length === 0}
>
{t('save_as_draft', 'Save as draft')}
</Button>
)}
{!addEditSets && (
<Button
onClick={schedule('schedule')}
className="rounded-[4px] relative group"
disabled={
selectedIntegrations.length === 0 ||
loading ||
!canSendForPublication
}
>
<div className="flex justify-center items-center gap-[5px] h-full">
<div className="h-full flex items-center text-white">
{!canSendForPublication
? t('not_matching_order', 'Not matching order')
: postFor
? t('submit_for_order', 'Submit for order')
: !existingData.integration
? selectedIntegrations.length === 0
? t(
'select_channels_from_circles',
'Select channels from the circles above'
)
: t('add_to_calendar', 'Add to calendar')
: // @ts-ignore
existingData?.posts?.[0]?.state === 'DRAFT'
? t('schedule', 'Schedule')
: t('update', 'Update')}
</div>
)}
</div>
</Button>
{!postFor && (
<div className="h-full flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M15.0233 7.14804L9.39828 12.773C9.34604 12.8253 9.284 12.8668 9.21572 12.8951C9.14743 12.9234 9.07423 12.938 9.00031 12.938C8.92639 12.938 8.8532 12.9234 8.78491 12.8951C8.71662 12.8668 8.65458 12.8253 8.60234 12.773L2.97734 7.14804C2.8718 7.04249 2.8125 6.89934 2.8125 6.75007C2.8125 6.6008 2.8718 6.45765 2.97734 6.3521C3.08289 6.24655 3.22605 6.18726 3.37531 6.18726C3.52458 6.18726 3.66773 6.24655 3.77328 6.3521L9.00031 11.5798L14.2273 6.3521C14.2796 6.29984 14.3417 6.25838 14.4099 6.2301C14.4782 6.20181 14.5514 6.18726 14.6253 6.18726C14.6992 6.18726 14.7724 6.20181 14.8407 6.2301C14.909 6.25838 14.971 6.29984 15.0233 6.3521C15.0755 6.40436 15.117 6.46641 15.1453 6.53469C15.1736 6.60297 15.1881 6.67616 15.1881 6.75007C15.1881 6.82398 15.1736 6.89716 15.1453 6.96545C15.117 7.03373 15.0755 7.09578 15.0233 7.14804Z"
fill="white"
/>
</svg>
<div
onClick={postNow}
className={clsx(
'hidden group-hover:flex hover:flex flex-col justify-center absolute start-0 top-[100%] w-full h-[40px] bg-customColor22 border border-tableBorder',
loading &&
'cursor-not-allowed pointer-events-none opacity-50'
)}
>
{t('post_now', 'Post now')}
</div>
</div>
)}
</div>
</Button>
)}
</Submitted>
</div>
</div>
@ -915,6 +979,7 @@ Here are the things you can do:
{!!selectedIntegrations.length && (
<div className="flex-1 flex flex-col p-[16px] pt-0">
<ProvidersOptions
hideEditOnlyThis={!!set}
allIntegrations={props.allIntegrations || []}
integrations={selectedIntegrations}
editorValue={value}
@ -924,6 +989,6 @@ Here are the things you can do:
)}
</div>
</div>
</>
</SetContext.Provider>
);
});

View File

@ -27,6 +27,7 @@ export const CalendarContext = createContext({
currentYear: dayjs().year(),
currentMonth: dayjs().month(),
customer: null as string | null,
sets: [] as { name: string; id: string; content: string[] }[],
comments: [] as Array<{
date: string;
total: number;
@ -142,6 +143,13 @@ export const CalendarWeekProvider: FC<{
refreshWhenHidden: false,
revalidateOnFocus: false,
});
const setList = useCallback(async () => {
return (await fetch('/sets')).json();
}, []);
const { data: sets, mutate } = useSWR('sets', setList);
const setFiltersWrapper = useCallback(
(filters: {
currentDay: 0 | 1 | 2 | 3 | 4 | 5 | 6;
@ -202,6 +210,7 @@ export const CalendarWeekProvider: FC<{
setFilters: setFiltersWrapper,
changeDate,
comments,
sets: sets || [],
}}
>
{children}

View File

@ -336,6 +336,7 @@ export const CalendarColumn: FC<{
changeDate,
display,
reloadCalendarView,
sets,
} = useCalendar();
const toaster = useToaster();
const modal = useModals();
@ -547,8 +548,39 @@ export const CalendarColumn: FC<{
},
[integrations]
);
const addModal = useCallback(async () => {
const signature = await (await fetch('/signatures/default')).json();
const set: any = !sets.length
? undefined
: await new Promise((resolve) => {
modal.openModal({
title: t('select_set', 'Select a Set'),
closeOnClickOutside: true,
closeOnEscape: true,
withCloseButton: true,
onClose: () => resolve('exit'),
classNames: {
modal: 'bg-secondary text-textColor',
},
children: (
<SetSelectionModal
sets={sets}
onSelect={(selectedSet) => {
resolve(selectedSet);
modal.closeAll();
}}
onContinueWithoutSet={() => {
resolve(undefined);
modal.closeAll();
}}
/>
),
});
});
if (set === 'exit') return;
modal.openModal({
closeOnClickOutside: false,
closeOnEscape: false,
@ -577,13 +609,14 @@ export const CalendarColumn: FC<{
date={
randomHour ? getDate.hour(Math.floor(Math.random() * 24)) : getDate
}
{...set?.content ? {set: JSON.parse(set.content)} : {}}
reopenModal={() => ({})}
/>
),
size: '80%',
// title: `Adding posts for ${getDate.format('DD/MM/YYYY HH:mm')}`,
});
}, [integrations, getDate]);
}, [integrations, getDate, sets]);
const openStatistics = useCallback(
(id: string) => () => {
modal.openModal({
@ -933,3 +966,45 @@ export const Statistics = () => {
</svg>
);
};
const SetSelectionModal: FC<{
sets: any[];
onSelect: (set: any) => void;
onContinueWithoutSet: () => void;
}> = ({ sets, onSelect, onContinueWithoutSet }) => {
const t = useT();
return (
<div className="flex flex-col gap-4 p-4">
<div className="text-lg font-medium">
{t('choose_set_or_continue', 'Choose a set or continue without one')}
</div>
<div className="flex flex-col gap-2 max-h-60 overflow-y-auto">
{sets.map((set) => (
<div
key={set.id}
onClick={() => onSelect(set)}
className="p-3 border border-tableBorder rounded-lg cursor-pointer hover:bg-customColor31 transition-colors"
>
<div className="font-medium">{set.name}</div>
{set.description && (
<div className="text-sm text-gray-400 mt-1">
{set.description}
</div>
)}
</div>
))}
</div>
<div className="flex gap-2 pt-2 border-t border-tableBorder">
<button
onClick={onContinueWithoutSet}
className="flex-1 px-4 py-2 bg-customColor31 text-textColor rounded-lg hover:bg-customColor23 transition-colors"
>
{t('continue_without_set', 'Continue without set')}
</button>
</div>
</div>
);
};

View File

@ -86,7 +86,7 @@ export const PickPlatforms: FC<{
);
return;
}
if (selectedAccounts.includes(integration)) {
if (selectedAccounts.some((account) => account.id === integration.id)) {
const changedIntegrations = selectedAccounts.filter(
({ id }) => id !== integration.id
);

View File

@ -57,7 +57,6 @@ export const useValues = (
criteriaMode: 'all',
});
console.log(form.formState.errors);
const getValues = useMemo(() => {
return () => ({
...form.getValues(),

View File

@ -6,6 +6,7 @@ import { ShowAllProviders } from '@gitroom/frontend/components/launches/provider
import dayjs from 'dayjs';
import { useStateCallback } from '@gitroom/react/helpers/use.state.callback';
export const ProvidersOptions: FC<{
hideEditOnlyThis: boolean;
integrations: Integrations[];
allIntegrations: Integrations[];
editorValue: Array<{
@ -14,7 +15,7 @@ export const ProvidersOptions: FC<{
}>;
date: dayjs.Dayjs;
}> = (props) => {
const { integrations, editorValue, date } = props;
const { integrations, editorValue, date, hideEditOnlyThis } = props;
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback([
integrations[0],
]);
@ -42,6 +43,7 @@ export const ProvidersOptions: FC<{
}}
>
<ShowAllProviders
hideEditOnlyThis={hideEditOnlyThis}
value={editorValue}
integrations={integrations}
selectedProvider={selectedIntegrations?.[0]}

View File

@ -45,6 +45,7 @@ import useSWR from 'swr';
import { InternalChannels } from '@gitroom/frontend/components/launches/internal.channels';
import { MergePost } from '@gitroom/frontend/components/launches/merge.post';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { useSet } from '@gitroom/frontend/components/launches/set.context';
// Simple component to change back to settings on after changing tab
export const SetTab: FC<{
@ -110,6 +111,7 @@ export const withProvider = function <T extends object>(
}>;
hideMenu?: boolean;
show: boolean;
hideEditOnlyThis?: boolean;
}) => {
const existingData = useExistingData();
const t = useT();
@ -170,9 +172,13 @@ export const withProvider = function <T extends object>(
}
);
const set = useSet();
// this is a smart function, it updates the global value without updating the states (too heavy) and set the settings validation
const form = useValues(
existingData.settings,
set?.set
? set?.set?.posts?.find((p) => p?.integration?.id === props?.id)?.settings
: existingData.settings,
props.id,
props.identifier,
editInPlace ? InPlaceValue : props.value,
@ -451,7 +457,7 @@ export const withProvider = function <T extends object>(
</Button>
</div>
)}
{!existingData.integration && (
{!existingData.integration && !props.hideEditOnlyThis && (
<div className="flex-1 flex">
<Button
className="text-white rounded-[4px] flex-1 !bg-red-700 overflow-hidden whitespace-nowrap"

View File

@ -122,13 +122,14 @@ export const Providers = [
];
export const ShowAllProviders: FC<{
integrations: Integrations[];
hideEditOnlyThis: boolean;
value: Array<{
content: string;
id?: string;
}>;
selectedProvider?: Integrations;
}> = (props) => {
const { integrations, value, selectedProvider } = props;
const { integrations, value, selectedProvider, hideEditOnlyThis } = props;
return (
<>
{integrations.map((integration) => {
@ -145,6 +146,7 @@ export const ShowAllProviders: FC<{
}
return (
<ProviderComponent
hideEditOnlyThis={hideEditOnlyThis}
key={integration.id}
{...integration}
value={value}

View File

@ -0,0 +1,5 @@
import { createContext, useContext } from 'react';
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
export const SetContext = createContext<{set?: CreatePostDto}>({});
export const useSet = () => useContext(SetContext);

View File

@ -21,6 +21,7 @@ import { useVariables } from '@gitroom/react/helpers/variable.context';
import { PublicComponent } from '@gitroom/frontend/components/public-api/public.component';
import Link from 'next/link';
import { Webhooks } from '@gitroom/frontend/components/webhooks/webhooks';
import { Sets } from '@gitroom/frontend/components/sets/sets';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { Tabs } from '@mantine/core';
import { SignaturesComponent } from '@gitroom/frontend/components/settings/signatures.component';
@ -84,10 +85,7 @@ export const SettingsPopup: FC<{
if (user?.tier?.autoPost) {
return 'autopost';
}
if (user?.tier?.public_api && isGeneral) {
return 'api';
}
return 'signatures';
return 'sets';
}, [user?.tier, isGeneral]);
const t = useT();
@ -131,6 +129,7 @@ export const SettingsPopup: FC<{
{t('auto_post', 'Auto Post')}
</Tabs.Tab>
)}
<Tabs.Tab value="sets">{t('sets', 'Sets')}</Tabs.Tab>
{user?.tier.current !== 'FREE' && (
<Tabs.Tab value="signatures">
{t('signatures', 'Signatures')}
@ -141,88 +140,6 @@ export const SettingsPopup: FC<{
)}
</Tabs.List>
{/* <Tabs.Panel value="profile" pt="md">
<div className="flex flex-col gap-[4px]">
<div className="text-[20px] font-[500]">Profile</div>
<div className="text-[14px] text-customColor18 font-[400]">
Add profile information
</div>
</div>
<div className="rounded-[4px] border border-customColor6 p-[24px] flex flex-col">
<div className="flex justify-between items-center">
<div className="w-[455px]">
<Input label="Full Name" translationKey="label_full_name" name="fullname" />
</div>
<div className="flex gap-[8px] mb-[10px]">
<div className="w-[48px] h-[48px] rounded-full bg-customColor38">
{!!picture?.path && (
<img
src={picture?.path}
alt="profile"
className="w-full h-full rounded-full"
/>
)}
</div>
<div className="flex flex-col gap-[2px]">
<div className="text-[14px]">Profile Picture</div>
<div className="flex gap-[8px]">
<button
className="h-[24px] w-[120px] bg-forth rounded-[4px] flex justify-center gap-[4px] items-center cursor-pointer"
type="button"
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
>
<path
d="M12.25 8.3126V11.3751C12.25 11.6072 12.1578 11.8297 11.9937 11.9938C11.8296 12.1579 11.6071 12.2501 11.375 12.2501H2.625C2.39294 12.2501 2.17038 12.1579 2.00628 11.9938C1.84219 11.8297 1.75 11.6072 1.75 11.3751V8.3126C1.75 8.19657 1.79609 8.08529 1.87814 8.00324C1.96019 7.92119 2.07147 7.8751 2.1875 7.8751C2.30353 7.8751 2.41481 7.92119 2.49686 8.00324C2.57891 8.08529 2.625 8.19657 2.625 8.3126V11.3751H11.375V8.3126C11.375 8.19657 11.4211 8.08529 11.5031 8.00324C11.5852 7.92119 11.6965 7.8751 11.8125 7.8751C11.9285 7.8751 12.0398 7.92119 12.1219 8.00324C12.2039 8.08529 12.25 8.19657 12.25 8.3126ZM5.12203 4.68463L6.5625 3.24362V8.3126C6.5625 8.42863 6.60859 8.53991 6.69064 8.62196C6.77269 8.70401 6.88397 8.7501 7 8.7501C7.11603 8.7501 7.22731 8.70401 7.30936 8.62196C7.39141 8.53991 7.4375 8.42863 7.4375 8.3126V3.24362L8.87797 4.68463C8.96006 4.76672 9.0714 4.81284 9.1875 4.81284C9.3036 4.81284 9.41494 4.76672 9.49703 4.68463C9.57912 4.60254 9.62524 4.4912 9.62524 4.3751C9.62524 4.259 9.57912 4.14766 9.49703 4.06557L7.30953 1.87807C7.2689 1.83739 7.22065 1.80512 7.16754 1.78311C7.11442 1.76109 7.05749 1.74976 7 1.74976C6.94251 1.74976 6.88558 1.76109 6.83246 1.78311C6.77935 1.80512 6.7311 1.83739 6.69047 1.87807L4.50297 4.06557C4.42088 4.14766 4.37476 4.259 4.37476 4.3751C4.37476 4.4912 4.42088 4.60254 4.50297 4.68463C4.58506 4.76672 4.6964 4.81284 4.8125 4.81284C4.9286 4.81284 5.03994 4.76672 5.12203 4.68463Z"
fill="white"
/>
</svg>
</div>
<div
className="text-[12px] text-white"
onClick={openMedia}
>
Upload image
</div>
</button>
<button
className="h-[24px] w-[88px] rounded-[4px] border-2 border-customColor21 hover:text-red-600 flex justify-center items-center gap-[4px]"
type="button"
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
>
<path
d="M11.8125 2.625H9.625V2.1875C9.625 1.8394 9.48672 1.50556 9.24058 1.25942C8.99444 1.01328 8.6606 0.875 8.3125 0.875H5.6875C5.3394 0.875 5.00556 1.01328 4.75942 1.25942C4.51328 1.50556 4.375 1.8394 4.375 2.1875V2.625H2.1875C2.07147 2.625 1.96019 2.67109 1.87814 2.75314C1.79609 2.83519 1.75 2.94647 1.75 3.0625C1.75 3.17853 1.79609 3.28981 1.87814 3.37186C1.96019 3.45391 2.07147 3.5 2.1875 3.5H2.625V11.375C2.625 11.6071 2.71719 11.8296 2.88128 11.9937C3.04538 12.1578 3.26794 12.25 3.5 12.25H10.5C10.7321 12.25 10.9546 12.1578 11.1187 11.9937C11.2828 11.8296 11.375 11.6071 11.375 11.375V3.5H11.8125C11.9285 3.5 12.0398 3.45391 12.1219 3.37186C12.2039 3.28981 12.25 3.17853 12.25 3.0625C12.25 2.94647 12.2039 2.83519 12.1219 2.75314C12.0398 2.67109 11.9285 2.625 11.8125 2.625ZM5.25 2.1875C5.25 2.07147 5.29609 1.96019 5.37814 1.87814C5.46019 1.79609 5.57147 1.75 5.6875 1.75H8.3125C8.42853 1.75 8.53981 1.79609 8.62186 1.87814C8.70391 1.96019 8.75 2.07147 8.75 2.1875V2.625H5.25V2.1875ZM10.5 11.375H3.5V3.5H10.5V11.375ZM6.125 5.6875V9.1875C6.125 9.30353 6.07891 9.41481 5.99686 9.49686C5.91481 9.57891 5.80353 9.625 5.6875 9.625C5.57147 9.625 5.46019 9.57891 5.37814 9.49686C5.29609 9.41481 5.25 9.30353 5.25 9.1875V5.6875C5.25 5.57147 5.29609 5.46019 5.37814 5.37814C5.46019 5.29609 5.57147 5.25 5.6875 5.25C5.80353 5.25 5.91481 5.29609 5.99686 5.37814C6.07891 5.46019 6.125 5.57147 6.125 5.6875ZM8.75 5.6875V9.1875C8.75 9.30353 8.70391 9.41481 8.62186 9.49686C8.53981 9.57891 8.42853 9.625 8.3125 9.625C8.19647 9.625 8.08519 9.57891 8.00314 9.49686C7.92109 9.41481 7.875 9.30353 7.875 9.1875V5.6875C7.875 5.57147 7.92109 5.46019 8.00314 5.37814C8.08519 5.29609 8.19647 5.25 8.3125 5.25C8.42853 5.25 8.53981 5.29609 8.62186 5.37814C8.70391 5.46019 8.75 5.57147 8.75 5.6875Z"
fill="currentColor"
/>
</svg>
</div>
<div className="text-[12px] " onClick={remove}>
Remove
</div>
</button>
</div>
</div>
</div>
</div>
<div>
<Textarea label="Bio" translationKey="label_bio" name="bio" className="resize-none" />
</div>
</div>
</Tabs.Panel> */}
{!!user?.tier?.team_members && isGeneral && (
<Tabs.Panel value="teams" pt="md">
<TeamsComponent />
@ -241,6 +158,12 @@ export const SettingsPopup: FC<{
</Tabs.Panel>
)}
{user?.tier.current !== 'FREE' && (
<Tabs.Panel value="sets" pt="md">
<Sets />
</Tabs.Panel>
)}
{user?.tier.current !== 'FREE' && (
<Tabs.Panel value="signatures" pt="md">
<SignaturesComponent />

View File

@ -0,0 +1,201 @@
'use client';
import 'reflect-metadata';
import React, { FC, Fragment, useCallback, useMemo, useState } from 'react';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import useSWR from 'swr';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { Button } from '@gitroom/react/form/button';
import { useModals } from '@mantine/modals';
import { Input } from '@gitroom/react/form/input';
import { useToaster } from '@gitroom/react/toaster/toaster';
import clsx from 'clsx';
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import dayjs from 'dayjs';
const SaveSetModal: FC<{
postData: any;
initialValue?: string;
onSave: (name: string) => void;
onCancel: () => void;
}> = ({ postData, onSave, onCancel, initialValue }) => {
const [name, setName] = useState(initialValue);
const t = useT();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name.trim()) {
onSave(name.trim());
}
};
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<Input
label="Set Name"
translationKey="label_set_name"
name="setName"
value={name}
disableForm={true}
onChange={(e) => setName(e.target.value)}
placeholder="Enter a name for this set"
autoFocus
/>
</div>
<div className="flex gap-2 justify-end">
<Button type="button" secondary onClick={onCancel}>
{t('cancel', 'Cancel')}
</Button>
<Button type="submit" disabled={!name.trim()}>
{t('save', 'Save')}
</Button>
</div>
</form>
);
};
export const Sets: FC = () => {
const fetch = useFetch();
const user = useUser();
const modal = useModals();
const toaster = useToaster();
const load = useCallback(async (path: string) => {
return (await (await fetch(path)).json()).integrations;
}, []);
const { isLoading, data: integrations } = useSWR('/integrations/list', load, {
fallbackData: [],
});
const list = useCallback(async () => {
return (await fetch('/sets')).json();
}, []);
const { data, mutate } = useSWR('sets', list);
const addSet = useCallback(
(params?: { id?: string; name?: string; content?: string }) => () => {
modal.openModal({
closeOnClickOutside: false,
closeOnEscape: false,
withCloseButton: false,
classNames: {
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
},
children: (
<AddEditModal
allIntegrations={integrations.map((p: any) => ({
...p,
}))}
{...(params?.id ? { set: JSON.parse(params.content) } : {})}
addEditSets={(data) => {
modal.openModal({
title: 'Save as Set',
classNames: {
modal: 'bg-sixth text-textColor',
title: 'text-textColor',
},
children: (
<SaveSetModal
initialValue={params?.name || ''}
postData={data}
onSave={async (name: string) => {
try {
await fetch('/sets', {
method: 'POST',
body: JSON.stringify({
...(params?.id ? { id: params.id } : {}),
name,
content: JSON.stringify(data),
}),
});
modal.closeAll();
mutate();
toaster.show('Set saved successfully', 'success');
} catch (error) {
toaster.show('Failed to save set', 'warning');
}
}}
onCancel={() => modal.closeAll()}
/>
),
size: 'md',
});
}}
reopenModal={() => {}}
mutate={() => {}}
integrations={integrations}
date={dayjs()}
/>
),
size: '80%',
title: ``,
});
},
[integrations]
);
const deleteSet = useCallback(
(data: any) => async () => {
if (await deleteDialog(`Are you sure you want to delete ${data.name}?`)) {
await fetch(`/sets/${data.id}`, {
method: 'DELETE',
});
mutate();
toaster.show('Set deleted successfully', 'success');
}
},
[]
);
const t = useT();
return (
<div className="flex flex-col">
<h3 className="text-[20px]">Sets ({data?.length || 0})</h3>
<div className="text-customColor18 mt-[4px]">
Manage your content sets for easy reuse across posts.
</div>
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
<div className="flex flex-col w-full">
{!!data?.length && (
<div className="grid grid-cols-[2fr,1fr,1fr] w-full gap-y-[10px]">
<div>{t('name', 'Name')}</div>
<div>{t('edit', 'Edit')}</div>
<div>{t('delete', 'Delete')}</div>
{data?.map((p: any) => (
<Fragment key={p.id}>
<div className="flex flex-col justify-center">{p.name}</div>
<div className="flex flex-col justify-center">
<div>
<Button onClick={addSet(p)}>{t('edit', 'Edit')}</Button>
</div>
</div>
<div className="flex flex-col justify-center">
<div>
<Button onClick={deleteSet(p)}>
{t('delete', 'Delete')}
</Button>
</div>
</div>
</Fragment>
))}
</div>
)}
<div>
<Button
onClick={addSet()}
className={clsx((data?.length || 0) > 0 && 'my-[16px]')}
>
Add a set
</Button>
</div>
</div>
</div>
</div>
);
};

View File

@ -35,6 +35,8 @@ import { SignatureRepository } from '@gitroom/nestjs-libraries/database/prisma/s
import { SignatureService } from '@gitroom/nestjs-libraries/database/prisma/signatures/signature.service';
import { AutopostRepository } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.repository';
import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autopost/autopost.service';
import { SetsService } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.service';
import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.repository';
@Global()
@Module({
@ -78,6 +80,8 @@ import { AutopostService } from '@gitroom/nestjs-libraries/database/prisma/autop
EmailService,
TrackService,
ShortLinkService,
SetsService,
SetsRepository,
],
get exports() {
return this.providers;

View File

@ -23,8 +23,8 @@ model Organization {
github GitHub[]
subscription Subscription?
Integration Integration[]
post Post[] @relation("organization")
submittedPost Post[] @relation("submittedForOrg")
post Post[] @relation("organization")
submittedPost Post[] @relation("submittedForOrg")
allowTrial Boolean @default(false)
Comments Comments[]
notifications Notifications[]
@ -37,18 +37,19 @@ model Organization {
tags Tags[]
signatures Signatures[]
autoPost AutoPost[]
sets Sets[]
}
model Tags {
id String @id @default(uuid())
name String
color String
orgId String
id String @id @default(uuid())
name String
color String
orgId String
organization Organization @relation(fields: [orgId], references: [id])
posts TagsPosts[]
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts TagsPosts[]
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([orgId])
@@index([deletedAt])
@ -56,9 +57,9 @@ model Tags {
model TagsPosts {
postId String
post Post @relation(fields: [postId], references: [id])
post Post @relation(fields: [postId], references: [id])
tagId String
tag Tags @relation(fields: [tagId], references: [id])
tag Tags @relation(fields: [tagId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -67,39 +68,39 @@ model TagsPosts {
}
model User {
id String @id @default(uuid())
email String
password String?
providerName Provider
name String?
lastName String?
isSuperAdmin Boolean @default(false)
bio String?
audience Int @default(0)
pictureId String?
picture Media? @relation(fields: [pictureId], references: [id])
providerId String?
organizations UserOrganization[]
timezone Int
comments Comments[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastReadNotifications DateTime @default(now())
inviteId String?
activated Boolean @default(true)
items ItemUser[]
marketplace Boolean @default(true)
account String?
connectedAccount Boolean @default(false)
groupBuyer MessagesGroup[] @relation("groupBuyer")
groupSeller MessagesGroup[] @relation("groupSeller")
orderBuyer Orders[] @relation("orderBuyer")
orderSeller Orders[] @relation("orderSeller")
payoutProblems PayoutProblems[]
lastOnline DateTime @default(now())
agencies SocialMediaAgency[]
ip String?
agent String?
id String @id @default(uuid())
email String
password String?
providerName Provider
name String?
lastName String?
isSuperAdmin Boolean @default(false)
bio String?
audience Int @default(0)
pictureId String?
picture Media? @relation(fields: [pictureId], references: [id])
providerId String?
organizations UserOrganization[]
timezone Int
comments Comments[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastReadNotifications DateTime @default(now())
inviteId String?
activated Boolean @default(true)
items ItemUser[]
marketplace Boolean @default(true)
account String?
connectedAccount Boolean @default(false)
groupBuyer MessagesGroup[] @relation("groupBuyer")
groupSeller MessagesGroup[] @relation("groupSeller")
orderBuyer Orders[] @relation("orderBuyer")
orderSeller Orders[] @relation("orderSeller")
payoutProblems PayoutProblems[]
lastOnline DateTime @default(now())
agencies SocialMediaAgency[]
ip String?
agent String?
@@unique([email, providerName])
@@index([lastReadNotifications])
@ -110,12 +111,12 @@ model User {
}
model UsedCodes {
id String @id @default(uuid())
code String
orgId String
id String @id @default(uuid())
code String
orgId String
organization Organization @relation(fields: [orgId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([code])
}
@ -164,16 +165,16 @@ model Trending {
}
model TrendingLog {
id String @id @default(uuid())
language String?
date DateTime
id String @id @default(uuid())
language String?
date DateTime
}
model ItemUser {
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id])
userId String
key String
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id])
userId String
key String
@@index([userId])
@@index([key])
@ -195,13 +196,13 @@ model Star {
}
model Media {
id String @id @default(uuid())
id String @id @default(uuid())
name String
path String
organization Organization @relation(fields: [organizationId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userPicture User[]
agencies SocialMediaAgency[]
deletedAt DateTime?
@ -210,31 +211,31 @@ model Media {
}
model SocialMediaAgency {
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id])
userId String @unique()
name String
logoId String?
logo Media? @relation(fields: [logoId], references: [id])
website String?
slug String?
id String @id @default(uuid())
user User @relation(fields: [userId], references: [id])
userId String @unique()
name String
logoId String?
logo Media? @relation(fields: [logoId], references: [id])
website String?
slug String?
facebook String?
instagram String?
twitter String?
linkedIn String?
youtube String?
tiktok String?
otherSocialMedia String?
facebook String?
instagram String?
twitter String?
linkedIn String?
youtube String?
tiktok String?
otherSocialMedia String?
shortDescription String
description String
niches SocialMediaAgencyNiche[]
approved Boolean @default(false)
shortDescription String
description String
niches SocialMediaAgencyNiche[]
approved Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([userId])
@@index([deletedAt])
@ -242,85 +243,85 @@ model SocialMediaAgency {
}
model SocialMediaAgencyNiche {
agencyId String
agency SocialMediaAgency @relation(fields: [agencyId], references: [id])
niche String
agencyId String
agency SocialMediaAgency @relation(fields: [agencyId], references: [id])
niche String
@@id([agencyId, niche])
}
model Credits {
id String @id @default(uuid())
id String @id @default(uuid())
organization Organization @relation(fields: [organizationId], references: [id])
organizationId String
credits Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([createdAt])
}
model Subscription {
id String @id @default(cuid())
organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id])
subscriptionTier SubscriptionTier
identifier String?
cancelAt DateTime?
period Period
totalChannels Int
isLifetime Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
id String @id @default(cuid())
organizationId String @unique
organization Organization @relation(fields: [organizationId], references: [id])
subscriptionTier SubscriptionTier
identifier String?
cancelAt DateTime?
period Period
totalChannels Int
isLifetime Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([organizationId])
@@index([deletedAt])
}
model Customer {
id String @id @default(uuid())
name String
orgId String
organization Organization @relation(fields: [orgId], references: [id])
id String @id @default(uuid())
name String
orgId String
organization Organization @relation(fields: [orgId], references: [id])
integrations Integration[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@unique([orgId, name, deletedAt])
}
model Integration {
id String @id @default(cuid())
id String @id @default(cuid())
internalId String
organizationId String
name String
organization Organization @relation(fields: [organizationId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
picture String?
providerIdentifier String
type String
token String
disabled Boolean @default(false)
disabled Boolean @default(false)
tokenExpiration DateTime?
refreshToken String?
posts Post[]
profile String?
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime? @updatedAt
orderItems OrderItems[]
inBetweenSteps Boolean @default(false)
refreshNeeded Boolean @default(false)
postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]")
inBetweenSteps Boolean @default(false)
refreshNeeded Boolean @default(false)
postingTimes String @default("[{\"time\":120}, {\"time\":400}, {\"time\":700}]")
customInstanceDetails String?
customerId String?
customer Customer? @relation(fields: [customerId], references: [id])
customer Customer? @relation(fields: [customerId], references: [id])
plugs Plugs[]
exisingPlugData ExisingPlugData[]
rootInternalId String?
additionalSettings String? @default("[]")
additionalSettings String? @default("[]")
webhooks IntegrationsWebhooks[]
@@index([rootInternalId])
@ -331,14 +332,14 @@ model Integration {
}
model Signatures {
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
content String
autoAdd Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
content String
autoAdd Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([createdAt])
@@index([organizationId])
@ -346,17 +347,17 @@ model Signatures {
}
model Comments {
id String @id @default(uuid())
content String
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
postId String
post Post @relation(fields: [postId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
id String @id @default(uuid())
content String
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
postId String
post Post @relation(fields: [postId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([createdAt])
@@index([organizationId])
@ -366,39 +367,39 @@ model Comments {
}
model Post {
id String @id @default(cuid())
state State @default(QUEUE)
publishDate DateTime
organizationId String
integrationId String
content String
group String
organization Organization @relation("organization", fields: [organizationId], references: [id])
integration Integration @relation(fields: [integrationId], references: [id])
title String?
description String?
parentPostId String?
releaseId String?
releaseURL String?
settings String?
parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id])
childrenPost Post[] @relation("parentPostId")
image String?
submittedForOrderId String?
submittedForOrder Orders? @relation(fields: [submittedForOrderId], references: [id])
id String @id @default(cuid())
state State @default(QUEUE)
publishDate DateTime
organizationId String
integrationId String
content String
group String
organization Organization @relation("organization", fields: [organizationId], references: [id])
integration Integration @relation(fields: [integrationId], references: [id])
title String?
description String?
parentPostId String?
releaseId String?
releaseURL String?
settings String?
parentPost Post? @relation("parentPostId", fields: [parentPostId], references: [id])
childrenPost Post[] @relation("parentPostId")
image String?
submittedForOrderId String?
submittedForOrder Orders? @relation(fields: [submittedForOrderId], references: [id])
submittedForOrganizationId String?
submittedForOrganization Organization? @relation("submittedForOrg", fields: [submittedForOrganizationId], references: [id])
approvedSubmitForOrder APPROVED_SUBMIT_FOR_ORDER @default(NO)
lastMessageId String?
lastMessage Messages? @relation(fields: [lastMessageId], references: [id])
intervalInDays Int?
payoutProblems PayoutProblems[]
comments Comments[]
tags TagsPosts[]
error String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
submittedForOrganization Organization? @relation("submittedForOrg", fields: [submittedForOrganizationId], references: [id])
approvedSubmitForOrder APPROVED_SUBMIT_FOR_ORDER @default(NO)
lastMessageId String?
lastMessage Messages? @relation(fields: [lastMessageId], references: [id])
intervalInDays Int?
payoutProblems PayoutProblems[]
comments Comments[]
tags TagsPosts[]
error String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([group])
@@index([deletedAt])
@ -417,14 +418,14 @@ model Post {
}
model Notifications {
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
content String
link String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
content String
link String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([createdAt])
@@index([organizationId])
@ -432,17 +433,17 @@ model Notifications {
}
model MessagesGroup {
id String @id @default(uuid())
id String @id @default(uuid())
buyerOrganizationId String
buyerOrganization Organization @relation(fields: [buyerOrganizationId], references: [id])
buyerId String
buyer User @relation("groupBuyer", fields: [buyerId], references: [id])
buyer User @relation("groupBuyer", fields: [buyerId], references: [id])
sellerId String
seller User @relation("groupSeller", fields: [sellerId], references: [id])
seller User @relation("groupSeller", fields: [sellerId], references: [id])
messages Messages[]
orders Orders[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([buyerId, sellerId])
@@index([createdAt])
@ -451,34 +452,34 @@ model MessagesGroup {
}
model PayoutProblems {
id String @id @default(uuid())
status String
orderId String
order Orders @relation(fields: [orderId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
postId String?
post Post? @relation(fields: [postId], references: [id])
amount Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
status String
orderId String
order Orders @relation(fields: [orderId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
postId String?
post Post? @relation(fields: [postId], references: [id])
amount Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Orders {
id String @id @default(uuid())
buyerId String
sellerId String
posts Post[]
buyer User @relation("orderBuyer", fields: [buyerId], references: [id])
seller User @relation("orderSeller", fields: [sellerId], references: [id])
status OrderStatus
ordersItems OrderItems[]
messageGroupId String
messageGroup MessagesGroup @relation(fields: [messageGroupId], references: [id])
captureId String?
payoutProblems PayoutProblems[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
buyerId String
sellerId String
posts Post[]
buyer User @relation("orderBuyer", fields: [buyerId], references: [id])
seller User @relation("orderSeller", fields: [sellerId], references: [id])
status OrderStatus
ordersItems OrderItems[]
messageGroupId String
messageGroup MessagesGroup @relation(fields: [messageGroupId], references: [id])
captureId String?
payoutProblems PayoutProblems[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([buyerId])
@@index([sellerId])
@ -488,29 +489,29 @@ model Orders {
}
model OrderItems {
id String @id @default(uuid())
orderId String
order Orders @relation(fields: [orderId], references: [id])
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
quantity Int
price Int
id String @id @default(uuid())
orderId String
order Orders @relation(fields: [orderId], references: [id])
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
quantity Int
price Int
@@index([orderId])
@@index([integrationId])
}
model Messages {
id String @id @default(uuid())
from From
content String?
groupId String
group MessagesGroup @relation(fields: [groupId], references: [id])
special String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
id String @id @default(uuid())
from From
content String?
groupId String
group MessagesGroup @relation(fields: [groupId], references: [id])
special String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([groupId])
@@index([createdAt])
@ -518,44 +519,44 @@ model Messages {
}
model Plugs {
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
plugFunction String
data String
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
activated Boolean @default(true)
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
plugFunction String
data String
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
activated Boolean @default(true)
@@unique([plugFunction, integrationId])
@@index([organizationId])
}
model ExisingPlugData {
id String @id @default(uuid())
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
methodName String
value String
id String @id @default(uuid())
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
methodName String
value String
@@unique([integrationId, methodName, value])
}
model PopularPosts {
id String @id @default(uuid())
category String
topic String
content String
hook String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
category String
topic String
content String
hook String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model IntegrationsWebhooks {
integrationId String
integration Integration @relation(fields: [integrationId], references: [id])
webhookId String
webhook Webhooks @relation(fields: [webhookId], references: [id])
webhook Webhooks @relation(fields: [webhookId], references: [id])
@@unique([integrationId, webhookId])
@@id([integrationId, webhookId])
@ -564,22 +565,22 @@ model IntegrationsWebhooks {
}
model Webhooks {
id String @id @default(uuid())
name String
id String @id @default(uuid())
name String
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
integrations IntegrationsWebhooks[]
url String
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id])
integrations IntegrationsWebhooks[]
url String
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@index([deletedAt])
}
model AutoPost {
id String @id @default(uuid())
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
title String
@ -593,12 +594,24 @@ model AutoPost {
generateContent Boolean
integrations String
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([deletedAt])
}
model Sets {
id String @id @default(uuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
name String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
}
enum OrderStatus {
PENDING
ACCEPTED

View File

@ -0,0 +1,58 @@
import { PrismaRepository } from '@gitroom/nestjs-libraries/database/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { SetsDto } from '@gitroom/nestjs-libraries/dtos/sets/sets.dto';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class SetsRepository {
constructor(private _sets: PrismaRepository<'sets'>) {}
getTotal(orgId: string) {
return this._sets.model.sets.count({
where: {
organizationId: orgId,
},
});
}
getSets(orgId: string) {
return this._sets.model.sets.findMany({
where: {
organizationId: orgId,
},
orderBy: {
createdAt: 'desc',
},
});
}
deleteSet(orgId: string, id: string) {
return this._sets.model.sets.delete({
where: {
id,
organizationId: orgId,
},
});
}
async createSet(orgId: string, body: SetsDto) {
const { id } = await this._sets.model.sets.upsert({
where: {
id: body.id || uuidv4(),
organizationId: orgId,
},
create: {
id: body.id || uuidv4(),
organizationId: orgId,
name: body.name,
content: body.content,
},
update: {
name: body.name,
content: body.content,
},
});
return { id };
}
}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { SetsRepository } from '@gitroom/nestjs-libraries/database/prisma/sets/sets.repository';
import { SetsDto } from '@gitroom/nestjs-libraries/dtos/sets/sets.dto';
@Injectable()
export class SetsService {
constructor(private _setsRepository: SetsRepository) {}
getTotal(orgId: string) {
return this._setsRepository.getTotal(orgId);
}
getSets(orgId: string) {
return this._setsRepository.getSets(orgId);
}
createSet(orgId: string, body: SetsDto) {
return this._setsRepository.createSet(orgId, body);
}
deleteSet(orgId: string, id: string) {
return this._setsRepository.deleteSet(orgId, id);
}
}

View File

@ -0,0 +1,29 @@
import { IsDefined, IsOptional, IsString } from 'class-validator';
export class SetsDto {
@IsOptional()
@IsString()
id?: string;
@IsString()
@IsDefined()
name: string;
@IsString()
@IsDefined()
content: string;
}
export class UpdateSetsDto {
@IsString()
@IsDefined()
id: string;
@IsString()
@IsDefined()
name: string;
@IsString()
@IsDefined()
content: string;
}

View File

@ -483,5 +483,6 @@
"start_7_days_free_trial": "Start 7 days free trial",
"change_language": "Change Language",
"that_a_wrap": "That's a wrap!\n\nIf you enjoyed this thread:\n\n1. Follow me @{{username}} for more of these\n2. RT the tweet below to share this thread with your audience\n",
"post_as_images_carousel": "Post as images carousel"
"post_as_images_carousel": "Post as images carousel",
"save_set": "Save Set"
}