feat: sets

This commit is contained in:
Nevo David 2025-06-09 17:49:51 +07:00
parent 3cf3a4b8bb
commit 35625b281b
11 changed files with 779 additions and 426 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

@ -71,6 +71,8 @@ export const AddEditModal: FC<{
date: dayjs.Dayjs;
integrations: Integrations[];
allIntegrations?: Integrations[];
setId?: string;
addEditSets?: (data: any) => void;
reopenModal: () => void;
mutate: () => void;
padding?: string;
@ -92,6 +94,7 @@ export const AddEditModal: FC<{
onlyValues,
padding,
customClose,
addEditSets,
} = props;
const [customer, setCustomer] = useState('');
const [loading, setLoading] = useState(false);
@ -436,28 +439,33 @@ 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(
@ -481,6 +489,7 @@ export const AddEditModal: FC<{
existingData,
selectedIntegrations,
tags,
addEditSets,
]
);
const uppy = useUppyUploader({
@ -811,70 +820,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>

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,219 @@
'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 { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { Input } from '@gitroom/react/form/input';
import { FormProvider, useForm } from 'react-hook-form';
import { object, string } from 'yup';
import { yupResolver } from '@hookform/resolvers/yup';
import { Textarea } from '@gitroom/react/form/textarea';
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';
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(
(data?: any) => () => {
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,
}))}
addEditSets={(data) => {
console.log('save', data);
}}
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>
);
};
const details = object().shape({
name: string().required(),
content: string().required(),
});
export const AddOrEditSet: FC<{
data?: any;
reload: () => void;
}> = (props) => {
const { data, reload } = props;
const fetch = useFetch();
const modal = useModals();
const toast = useToaster();
const form = useForm({
resolver: yupResolver(details),
values: {
name: data?.name || '',
content: data?.content || '',
},
});
const callBack = useCallback(
async (values: any) => {
// TODO: Implement save functionality
console.log('Save set functionality to be implemented', values);
modal.closeAll();
reload();
},
[data]
);
const t = useT();
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(callBack)}>
<div className="relative flex gap-[20px] flex-col flex-1 rounded-[4px] border border-customColor6 bg-sixth p-[16px] pt-0 w-[500px]">
<TopTitle title={data ? 'Edit set' : 'Add set'} />
<button
className="outline-none absolute end-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
type="button"
onClick={modal.closeAll}
>
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
>
<path
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
<div>
<Input
label="Name"
translationKey="label_name"
{...form.register('name')}
/>
<Textarea
label="Content"
translationKey="label_content"
{...form.register('content')}
/>
<div className="flex gap-[10px]">
<Button
type="submit"
className="mt-[24px]"
disabled={!form.formState.isValid}
>
{t('save', 'Save')}
</Button>
</div>
</div>
</div>
</form>
</FormProvider>
);
};

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