diff --git a/apps/backend/src/api/api.module.ts b/apps/backend/src/api/api.module.ts index 54b8382a..aef9a805 100644 --- a/apps/backend/src/api/api.module.ts +++ b/apps/backend/src/api/api.module.ts @@ -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], diff --git a/apps/backend/src/api/routes/sets.controller.ts b/apps/backend/src/api/routes/sets.controller.ts new file mode 100644 index 00000000..df10a344 --- /dev/null +++ b/apps/backend/src/api/routes/sets.controller.ts @@ -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); + } +} \ No newline at end of file diff --git a/apps/frontend/src/components/launches/add.edit.model.tsx b/apps/frontend/src/components/launches/add.edit.model.tsx index 002a055e..1440d9ef 100644 --- a/apps/frontend/src/components/launches/add.edit.model.tsx +++ b/apps/frontend/src/components/launches/add.edit.model.tsx @@ -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 ( - <> + {user?.tier?.ai && ( !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')} )} - - + )} + + {!addEditSets && ( + + )} + + {!addEditSets && ( + + {!postFor && ( +
+ + + +
+ {t('post_now', 'Post now')} +
+
+ )} + + + )} @@ -915,6 +979,7 @@ Here are the things you can do: {!!selectedIntegrations.length && (
- +
); }); diff --git a/apps/frontend/src/components/launches/calendar.context.tsx b/apps/frontend/src/components/launches/calendar.context.tsx index 445a9c37..f054d22b 100644 --- a/apps/frontend/src/components/launches/calendar.context.tsx +++ b/apps/frontend/src/components/launches/calendar.context.tsx @@ -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} diff --git a/apps/frontend/src/components/launches/calendar.tsx b/apps/frontend/src/components/launches/calendar.tsx index 2b6e4e15..3b40cf8e 100644 --- a/apps/frontend/src/components/launches/calendar.tsx +++ b/apps/frontend/src/components/launches/calendar.tsx @@ -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: ( + { + 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 = () => { ); }; + +const SetSelectionModal: FC<{ + sets: any[]; + onSelect: (set: any) => void; + onContinueWithoutSet: () => void; +}> = ({ sets, onSelect, onContinueWithoutSet }) => { + const t = useT(); + + return ( +
+
+ {t('choose_set_or_continue', 'Choose a set or continue without one')} +
+ +
+ {sets.map((set) => ( +
onSelect(set)} + className="p-3 border border-tableBorder rounded-lg cursor-pointer hover:bg-customColor31 transition-colors" + > +
{set.name}
+ {set.description && ( +
+ {set.description} +
+ )} +
+ ))} +
+ +
+ +
+
+ ); +}; diff --git a/apps/frontend/src/components/launches/helpers/pick.platform.component.tsx b/apps/frontend/src/components/launches/helpers/pick.platform.component.tsx index c749b929..141c1aca 100644 --- a/apps/frontend/src/components/launches/helpers/pick.platform.component.tsx +++ b/apps/frontend/src/components/launches/helpers/pick.platform.component.tsx @@ -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 ); diff --git a/apps/frontend/src/components/launches/helpers/use.values.ts b/apps/frontend/src/components/launches/helpers/use.values.ts index efb834ff..ec8ebf02 100644 --- a/apps/frontend/src/components/launches/helpers/use.values.ts +++ b/apps/frontend/src/components/launches/helpers/use.values.ts @@ -57,7 +57,6 @@ export const useValues = ( criteriaMode: 'all', }); - console.log(form.formState.errors); const getValues = useMemo(() => { return () => ({ ...form.getValues(), diff --git a/apps/frontend/src/components/launches/providers.options.tsx b/apps/frontend/src/components/launches/providers.options.tsx index 9393148d..8d4bc337 100644 --- a/apps/frontend/src/components/launches/providers.options.tsx +++ b/apps/frontend/src/components/launches/providers.options.tsx @@ -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<{ }} > ( }>; hideMenu?: boolean; show: boolean; + hideEditOnlyThis?: boolean; }) => { const existingData = useExistingData(); const t = useT(); @@ -170,9 +172,13 @@ export const withProvider = function ( } ); + 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 ( )} - {!existingData.integration && ( + {!existingData.integration && !props.hideEditOnlyThis && (
- -
- - - -
-