Merge branch 'main' into patch-1
This commit is contained in:
commit
54fba79340
|
|
@ -264,7 +264,8 @@ export class IntegrationsController {
|
|||
const load = await integrationProvider[body.name](
|
||||
getIntegration.token,
|
||||
body.data,
|
||||
getIntegration.internalId
|
||||
getIntegration.internalId,
|
||||
getIntegration
|
||||
);
|
||||
|
||||
return load;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,7 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
ClipboardEventHandler, FC, Fragment, MouseEventHandler, useCallback, useEffect, useMemo, useRef, ClipboardEvent, useState, memo
|
||||
ClipboardEventHandler,
|
||||
FC,
|
||||
Fragment,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
ClipboardEvent,
|
||||
useState,
|
||||
memo,
|
||||
} from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
|
|
@ -50,6 +60,7 @@ import { useClickOutside } from '@gitroom/frontend/components/layout/click.outsi
|
|||
import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { DropFiles } from '@gitroom/frontend/components/layout/drop.files';
|
||||
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
|
||||
|
||||
function countCharacters(text: string, type: string): number {
|
||||
if (type !== 'x') {
|
||||
|
|
@ -94,10 +105,6 @@ export const AddEditModal: FC<{
|
|||
return list;
|
||||
}, [customer, ints]);
|
||||
|
||||
const totalCustomers = useMemo(() => {
|
||||
return uniqBy(ints, (i) => i?.customer?.id).length;
|
||||
}, [ints]);
|
||||
|
||||
const [dateState, setDateState] = useState(date);
|
||||
|
||||
// hook to open a new modal
|
||||
|
|
@ -519,28 +526,13 @@ export const AddEditModal: FC<{
|
|||
information={data}
|
||||
onChange={setPostFor}
|
||||
/>
|
||||
{totalCustomers > 1 && (
|
||||
<Select
|
||||
hideErrors={true}
|
||||
label=""
|
||||
name="customer"
|
||||
value={customer}
|
||||
onChange={(e) => {
|
||||
setCustomer(e.target.value);
|
||||
setSelectedIntegrations([]);
|
||||
}}
|
||||
disableForm={true}
|
||||
>
|
||||
<option value="">Selected Customer</option>
|
||||
{uniqBy(ints, (u) => u?.customer?.name)
|
||||
.filter((f) => f.customer?.name)
|
||||
.map((p) => (
|
||||
<option key={p.customer?.id} value={p.customer?.id}>
|
||||
Customer: {p.customer?.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<SelectCustomer
|
||||
integrations={ints}
|
||||
onChange={(val) => {
|
||||
setCustomer(val);
|
||||
setSelectedIntegrations([]);
|
||||
}}
|
||||
/>
|
||||
<DatePicker onChange={setDateState} date={dateState} />
|
||||
{!selectedIntegrations.length && (
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -28,8 +28,9 @@ export const CalendarContext = createContext({
|
|||
currentWeek: dayjs().week(),
|
||||
currentYear: dayjs().year(),
|
||||
currentMonth: dayjs().month(),
|
||||
customer: null as string | null,
|
||||
comments: [] as Array<{ date: string; total: number }>,
|
||||
integrations: [] as (Integrations & {refreshNeeded?: boolean})[],
|
||||
integrations: [] as (Integrations & { refreshNeeded?: boolean })[],
|
||||
trendings: [] as string[],
|
||||
posts: [] as Array<Post & { integration: Integration }>,
|
||||
reloadCalendarView: () => {
|
||||
|
|
@ -42,6 +43,7 @@ export const CalendarContext = createContext({
|
|||
currentDay: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
currentMonth: number;
|
||||
display: 'week' | 'month' | 'day';
|
||||
customer: string | null;
|
||||
}) => {
|
||||
/** empty **/
|
||||
},
|
||||
|
|
@ -66,7 +68,7 @@ export interface Integrations {
|
|||
customer?: {
|
||||
name?: string;
|
||||
id?: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getWeekNumber(date: Date) {
|
||||
|
|
@ -94,6 +96,7 @@ export const CalendarWeekProvider: FC<{
|
|||
const fetch = useFetch();
|
||||
const [internalData, setInternalData] = useState([] as any[]);
|
||||
const [trendings] = useState<string[]>([]);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const display = searchParams.get('display') || 'week';
|
||||
|
|
@ -110,6 +113,7 @@ export const CalendarWeekProvider: FC<{
|
|||
currentWeek: +(searchParams.get('week') || getWeekNumber(new Date())),
|
||||
currentMonth: +(searchParams.get('month') || dayjs().month()),
|
||||
currentYear: +(searchParams.get('year') || dayjs().year()),
|
||||
customer: (searchParams.get('customer') as string) || null,
|
||||
display,
|
||||
});
|
||||
|
||||
|
|
@ -120,6 +124,7 @@ export const CalendarWeekProvider: FC<{
|
|||
week: filters.currentWeek.toString(),
|
||||
month: (filters.currentMonth + 1).toString(),
|
||||
year: filters.currentYear.toString(),
|
||||
customer: filters?.customer?.toString() || '',
|
||||
}).toString();
|
||||
}, [filters, display]);
|
||||
|
||||
|
|
@ -142,6 +147,7 @@ export const CalendarWeekProvider: FC<{
|
|||
currentYear: number;
|
||||
currentMonth: number;
|
||||
display: 'week' | 'month' | 'day';
|
||||
customer: string | null;
|
||||
}) => {
|
||||
setFilters(filters);
|
||||
setInternalData([]);
|
||||
|
|
@ -152,6 +158,7 @@ export const CalendarWeekProvider: FC<{
|
|||
`month=${filters.currentMonth}`,
|
||||
`year=${filters.currentYear}`,
|
||||
`display=${filters.display}`,
|
||||
filters.customer ? `customer=${filters.customer}` : ``,
|
||||
].filter((f) => f);
|
||||
window.history.replaceState(null, '', `/launches?${path.join('&')}`);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
|||
import dayjs from 'dayjs';
|
||||
import { useCallback } from 'react';
|
||||
import { isUSCitizen } from './helpers/isuscitizen.utils';
|
||||
import { SelectCustomer } from '@gitroom/frontend/components/launches/select.customer';
|
||||
|
||||
export const Filters = () => {
|
||||
const week = useCalendar();
|
||||
|
|
@ -13,30 +14,30 @@ export const Filters = () => {
|
|||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
: week.display === 'week'
|
||||
? dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.startOf('isoWeek')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.isoWeek(week.currentWeek)
|
||||
.endOf('isoWeek')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||
: dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.startOf('month')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY') +
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY') +
|
||||
' - ' +
|
||||
dayjs()
|
||||
.year(week.currentYear)
|
||||
.month(week.currentMonth)
|
||||
.endOf('month')
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' :'DD/MM/YYYY');
|
||||
.format(isUSCitizen() ? 'MM/DD/YYYY' : 'DD/MM/YYYY');
|
||||
|
||||
const setDay = useCallback(() => {
|
||||
week.setFilters({
|
||||
|
|
@ -45,6 +46,7 @@ export const Filters = () => {
|
|||
currentYear: dayjs().year(),
|
||||
currentMonth: dayjs().month(),
|
||||
display: 'day',
|
||||
customer: week.customer,
|
||||
});
|
||||
}, [week]);
|
||||
|
||||
|
|
@ -55,6 +57,7 @@ export const Filters = () => {
|
|||
currentYear: dayjs().year(),
|
||||
currentMonth: dayjs().month(),
|
||||
display: 'week',
|
||||
customer: week.customer,
|
||||
});
|
||||
}, [week]);
|
||||
|
||||
|
|
@ -65,9 +68,24 @@ export const Filters = () => {
|
|||
currentWeek: dayjs().isoWeek(),
|
||||
currentYear: dayjs().year(),
|
||||
display: 'month',
|
||||
customer: week.customer,
|
||||
});
|
||||
}, [week]);
|
||||
|
||||
const setCustomer = useCallback(
|
||||
(customer: string) => {
|
||||
week.setFilters({
|
||||
currentDay: week.currentDay,
|
||||
currentMonth: week.currentMonth,
|
||||
currentWeek: week.currentWeek,
|
||||
currentYear: week.currentYear,
|
||||
display: week.display as any,
|
||||
customer: customer,
|
||||
});
|
||||
},
|
||||
[week]
|
||||
);
|
||||
|
||||
const next = useCallback(() => {
|
||||
const increaseDay = week.display === 'day';
|
||||
const increaseWeek =
|
||||
|
|
@ -77,6 +95,7 @@ export const Filters = () => {
|
|||
week.display === 'month' || (increaseWeek && week.currentWeek === 52);
|
||||
|
||||
week.setFilters({
|
||||
customer: week.customer,
|
||||
currentDay: (!increaseDay
|
||||
? 0
|
||||
: week.currentDay === 6
|
||||
|
|
@ -116,6 +135,7 @@ export const Filters = () => {
|
|||
week.display === 'month' || (decreaseWeek && week.currentWeek === 1);
|
||||
|
||||
week.setFilters({
|
||||
customer: week.customer,
|
||||
currentDay: (!decreaseDay
|
||||
? 0
|
||||
: week.currentDay === 0
|
||||
|
|
@ -147,77 +167,82 @@ export const Filters = () => {
|
|||
]);
|
||||
return (
|
||||
<div className="text-textColor flex flex-col md:flex-row gap-[8px] items-center select-none">
|
||||
<div className = "flex flex-grow flex-row">
|
||||
<div onClick={previous} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.1644 15.5866C13.3405 15.7628 13.4395 16.0016 13.4395 16.2507C13.4395 16.4998 13.3405 16.7387 13.1644 16.9148C12.9883 17.0909 12.7494 17.1898 12.5003 17.1898C12.2513 17.1898 12.0124 17.0909 11.8363 16.9148L5.58629 10.6648C5.49889 10.5777 5.42954 10.4742 5.38222 10.3602C5.3349 10.2463 5.31055 10.1241 5.31055 10.0007C5.31055 9.87732 5.3349 9.75515 5.38222 9.64119C5.42954 9.52724 5.49889 9.42375 5.58629 9.33665L11.8363 3.08665C12.0124 2.91053 12.2513 2.81158 12.5003 2.81158C12.7494 2.81158 12.9883 2.91053 13.1644 3.08665C13.3405 3.26277 13.4395 3.50164 13.4395 3.75071C13.4395 3.99978 13.3405 4.23865 13.1644 4.41477L7.57925 9.99993L13.1644 15.5866Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="w-[80px] text-center">
|
||||
{week.display === 'day'
|
||||
? `${dayjs()
|
||||
.month(week.currentMonth)
|
||||
.week(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('dddd')}`
|
||||
: week.display === 'week'
|
||||
? `Week ${week.currentWeek}`
|
||||
: `${dayjs().month(week.currentMonth).format('MMMM')}`}
|
||||
</div>
|
||||
<div onClick={next} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M14.4137 10.6633L8.16374 16.9133C7.98761 17.0894 7.74874 17.1884 7.49967 17.1884C7.2506 17.1884 7.01173 17.0894 6.83561 16.9133C6.65949 16.7372 6.56055 16.4983 6.56055 16.2492C6.56055 16.0002 6.65949 15.7613 6.83561 15.5852L12.4223 10L6.83717 4.41331C6.74997 4.3261 6.68079 4.22257 6.6336 4.10863C6.5864 3.99469 6.56211 3.87257 6.56211 3.74925C6.56211 3.62592 6.5864 3.5038 6.6336 3.38986C6.68079 3.27592 6.74997 3.17239 6.83717 3.08518C6.92438 2.99798 7.02791 2.9288 7.14185 2.88161C7.25579 2.83441 7.37791 2.81012 7.50124 2.81012C7.62456 2.81012 7.74668 2.83441 7.86062 2.88161C7.97456 2.9288 8.07809 2.99798 8.1653 3.08518L14.4153 9.33518C14.5026 9.42238 14.5718 9.52596 14.619 9.63997C14.6662 9.75398 14.6904 9.87618 14.6903 9.99957C14.6901 10.123 14.6656 10.2451 14.6182 10.359C14.5707 10.4729 14.5012 10.5763 14.4137 10.6633Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">{betweenDates}</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'day' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setDay}
|
||||
>
|
||||
Day
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'week' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setWeek}
|
||||
>
|
||||
Week
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'month' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setMonth}
|
||||
>
|
||||
Month
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-row">
|
||||
<div onClick={previous} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M13.1644 15.5866C13.3405 15.7628 13.4395 16.0016 13.4395 16.2507C13.4395 16.4998 13.3405 16.7387 13.1644 16.9148C12.9883 17.0909 12.7494 17.1898 12.5003 17.1898C12.2513 17.1898 12.0124 17.0909 11.8363 16.9148L5.58629 10.6648C5.49889 10.5777 5.42954 10.4742 5.38222 10.3602C5.3349 10.2463 5.31055 10.1241 5.31055 10.0007C5.31055 9.87732 5.3349 9.75515 5.38222 9.64119C5.42954 9.52724 5.49889 9.42375 5.58629 9.33665L11.8363 3.08665C12.0124 2.91053 12.2513 2.81158 12.5003 2.81158C12.7494 2.81158 12.9883 2.91053 13.1644 3.08665C13.3405 3.26277 13.4395 3.50164 13.4395 3.75071C13.4395 3.99978 13.3405 4.23865 13.1644 4.41477L7.57925 9.99993L13.1644 15.5866Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="w-[80px] text-center">
|
||||
{week.display === 'day'
|
||||
? `${dayjs()
|
||||
.month(week.currentMonth)
|
||||
.week(week.currentWeek)
|
||||
.day(week.currentDay)
|
||||
.format('dddd')}`
|
||||
: week.display === 'week'
|
||||
? `Week ${week.currentWeek}`
|
||||
: `${dayjs().month(week.currentMonth).format('MMMM')}`}
|
||||
</div>
|
||||
<div onClick={next} className="cursor-pointer">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M14.4137 10.6633L8.16374 16.9133C7.98761 17.0894 7.74874 17.1884 7.49967 17.1884C7.2506 17.1884 7.01173 17.0894 6.83561 16.9133C6.65949 16.7372 6.56055 16.4983 6.56055 16.2492C6.56055 16.0002 6.65949 15.7613 6.83561 15.5852L12.4223 10L6.83717 4.41331C6.74997 4.3261 6.68079 4.22257 6.6336 4.10863C6.5864 3.99469 6.56211 3.87257 6.56211 3.74925C6.56211 3.62592 6.5864 3.5038 6.6336 3.38986C6.68079 3.27592 6.74997 3.17239 6.83717 3.08518C6.92438 2.99798 7.02791 2.9288 7.14185 2.88161C7.25579 2.83441 7.37791 2.81012 7.50124 2.81012C7.62456 2.81012 7.74668 2.83441 7.86062 2.88161C7.97456 2.9288 8.07809 2.99798 8.1653 3.08518L14.4153 9.33518C14.5026 9.42238 14.5718 9.52596 14.619 9.63997C14.6662 9.75398 14.6904 9.87618 14.6903 9.99957C14.6901 10.123 14.6656 10.2451 14.6182 10.359C14.5707 10.4729 14.5012 10.5763 14.4137 10.6633Z"
|
||||
fill="#E9E9F1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">{betweenDates}</div>
|
||||
</div>
|
||||
<SelectCustomer
|
||||
customer={week.customer as string}
|
||||
onChange={(customer: string) => setCustomer(customer)}
|
||||
integrations={week.integrations}
|
||||
/>
|
||||
<div className="flex flex-row">
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'day' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setDay}
|
||||
>
|
||||
Day
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'week' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setWeek}
|
||||
>
|
||||
Week
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'border border-tableBorder p-[10px] cursor-pointer',
|
||||
week.display === 'month' && 'bg-tableBorder'
|
||||
)}
|
||||
onClick={setMonth}
|
||||
>
|
||||
Month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -527,7 +527,7 @@ export const withProvider = function <T extends object>(
|
|||
{(showTab === 0 || showTab === 2) && (
|
||||
<div className={clsx('mt-[20px]', showTab !== 2 && 'hidden')}>
|
||||
<Component values={editInPlace ? InPlaceValue : props.value} />
|
||||
{data?.internalPlugs?.length && (
|
||||
{!!data?.internalPlugs?.length && (
|
||||
<InternalChannels plugs={data?.internalPlugs} />
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import { FC, useCallback } from 'react';
|
||||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { useFieldArray } from 'react-hook-form';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { Subreddit } from './subreddit';
|
||||
import { LemmySettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/lemmy.dto';
|
||||
|
||||
const LemmySettings: FC = () => {
|
||||
const { register, control } = useSettings();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control, // control props comes from useForm (optional: if you are using FormContext)
|
||||
name: 'subreddit', // unique name for your Field Array
|
||||
});
|
||||
|
||||
const addField = useCallback(() => {
|
||||
append({});
|
||||
}, [fields, append]);
|
||||
|
||||
const deleteField = useCallback(
|
||||
(index: number) => async () => {
|
||||
if (
|
||||
!(await deleteDialog('Are you sure you want to delete this Subreddit?'))
|
||||
)
|
||||
return;
|
||||
remove(index);
|
||||
},
|
||||
[fields, remove]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-[20px] mb-[20px]">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-col relative">
|
||||
<div
|
||||
onClick={deleteField(index)}
|
||||
className="absolute -left-[10px] justify-center items-center flex -top-[10px] w-[20px] h-[20px] bg-red-600 rounded-full text-textColor"
|
||||
>
|
||||
x
|
||||
</div>
|
||||
<Subreddit {...register(`subreddit.${index}.value`)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={addField}>Add Community</Button>
|
||||
{fields.length === 0 && (
|
||||
<div className="text-red-500 text-[12px] mt-[10px]">
|
||||
Please add at least one Subreddit
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProvider(
|
||||
LemmySettings,
|
||||
undefined,
|
||||
LemmySettingsDto,
|
||||
async (items) => {
|
||||
const [firstItems] = items;
|
||||
|
||||
if (
|
||||
firstItems.length &&
|
||||
firstItems[0].path.indexOf('png') === -1 &&
|
||||
firstItems[0].path.indexOf('jpg') === -1 &&
|
||||
firstItems[0].path.indexOf('jpef') === -1 &&
|
||||
firstItems[0].path.indexOf('gif') === -1
|
||||
) {
|
||||
return 'You can set only one picture for a cover';
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
10000
|
||||
);
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
import { FC, FormEvent, useCallback, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
|
||||
export const Subreddit: FC<{
|
||||
onChange: (event: {
|
||||
target: {
|
||||
name: string;
|
||||
value: {
|
||||
id: string;
|
||||
subreddit: string;
|
||||
title: string;
|
||||
name: string;
|
||||
url: string;
|
||||
body: string;
|
||||
media: any[];
|
||||
};
|
||||
};
|
||||
}) => void;
|
||||
name: string;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
|
||||
const state = useSettings();
|
||||
const split = name.split('.');
|
||||
const [loading, setLoading] = useState(false);
|
||||
// @ts-ignore
|
||||
const errors = state?.formState?.errors?.[split?.[0]]?.[split?.[1]]?.value;
|
||||
|
||||
const [results, setResults] = useState([]);
|
||||
const func = useCustomProviderFunction();
|
||||
const value = useWatch({ name });
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const setResult = (result: { id: string; name: string }) => async () => {
|
||||
setLoading(true);
|
||||
setSearchValue('');
|
||||
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
id: String(result.id),
|
||||
subreddit: result.name,
|
||||
title: '',
|
||||
name: '',
|
||||
url: '',
|
||||
body: '',
|
||||
media: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const setTitle = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
title: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const setURL = useCallback(
|
||||
(e: any) => {
|
||||
onChange({
|
||||
target: {
|
||||
name,
|
||||
value: {
|
||||
...value,
|
||||
url: e.target.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const search = useDebouncedCallback(
|
||||
useCallback(async (e: FormEvent<HTMLInputElement>) => {
|
||||
// @ts-ignore
|
||||
setResults([]);
|
||||
// @ts-ignore
|
||||
if (!e.target.value) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const results = await func.get('subreddits', { word: e.target.value });
|
||||
// @ts-ignore
|
||||
setResults(results);
|
||||
}, []),
|
||||
500
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-primary p-[20px]">
|
||||
{value?.subreddit ? (
|
||||
<>
|
||||
<Input
|
||||
error={errors?.subreddit?.message}
|
||||
disableForm={true}
|
||||
value={value.subreddit}
|
||||
readOnly={true}
|
||||
label="Community"
|
||||
name="subreddit"
|
||||
/>
|
||||
<Input
|
||||
error={errors?.title?.message}
|
||||
value={value.title}
|
||||
disableForm={true}
|
||||
label="Title"
|
||||
name="title"
|
||||
onChange={setTitle}
|
||||
/>
|
||||
<Input
|
||||
error={errors?.url?.message}
|
||||
value={value.url}
|
||||
label="URL"
|
||||
name="url"
|
||||
disableForm={true}
|
||||
onChange={setURL}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Community"
|
||||
name="search"
|
||||
label="Search Community"
|
||||
readOnly={loading}
|
||||
value={searchValue}
|
||||
error={errors?.message}
|
||||
disableForm={true}
|
||||
onInput={async (e) => {
|
||||
// @ts-ignore
|
||||
setSearchValue(e.target.value);
|
||||
await search(e);
|
||||
}}
|
||||
/>
|
||||
{!!results.length && !loading && (
|
||||
<div className="z-[400] w-full absolute bg-input -mt-[20px] outline-none border-fifth border cursor-pointer">
|
||||
{results.map((r: { id: string; name: string }) => (
|
||||
<div
|
||||
onClick={setResult(r)}
|
||||
key={r.id}
|
||||
className="px-[16px] py-[5px] hover:bg-secondary"
|
||||
>
|
||||
{r.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -17,6 +17,7 @@ import DiscordProvider from '@gitroom/frontend/components/launches/providers/dis
|
|||
import SlackProvider from '@gitroom/frontend/components/launches/providers/slack/slack.provider';
|
||||
import MastodonProvider from '@gitroom/frontend/components/launches/providers/mastodon/mastodon.provider';
|
||||
import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider';
|
||||
import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy/lemmy.provider';
|
||||
|
||||
export const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
|
|
@ -37,6 +38,7 @@ export const Providers = [
|
|||
{identifier: 'slack', component: SlackProvider},
|
||||
{identifier: 'mastodon', component: MastodonProvider},
|
||||
{identifier: 'bluesky', component: BlueskyProvider},
|
||||
{identifier: 'lemmy', component: LemmyProvider},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
import { Select } from '@gitroom/react/form/select';
|
||||
import { uniqBy } from 'lodash';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { Integrations } from '@gitroom/frontend/components/launches/calendar.context';
|
||||
|
||||
export const SelectCustomer: FC<{
|
||||
onChange: (value: string) => void;
|
||||
integrations: Integrations[];
|
||||
customer?: string;
|
||||
}> = (props) => {
|
||||
const { onChange, integrations, customer: currentCustomer } = props;
|
||||
const [customer, setCustomer] = useState(currentCustomer || '');
|
||||
|
||||
const totalCustomers = useMemo(() => {
|
||||
return uniqBy(integrations, (i) => i?.customer?.id).length;
|
||||
}, [integrations]);
|
||||
|
||||
if (totalCustomers <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
hideErrors={true}
|
||||
label=""
|
||||
name="customer"
|
||||
value={customer}
|
||||
onChange={(e) => {
|
||||
setCustomer(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
disableForm={true}
|
||||
>
|
||||
<option value="">Selected Customer</option>
|
||||
{uniqBy(integrations, (u) => u?.customer?.name)
|
||||
.filter((f) => f.customer?.name)
|
||||
.map((p) => (
|
||||
<option key={p.customer?.id} value={p.customer?.id}>
|
||||
Customer: {p.customer?.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -110,6 +110,11 @@ export class PostsRepository {
|
|||
},
|
||||
deletedAt: null,
|
||||
parentPostId: null,
|
||||
...query.customer ? {
|
||||
integration: {
|
||||
customerId: query.customer,
|
||||
}
|
||||
}: {},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ model Integration {
|
|||
@@index([rootInternalId])
|
||||
@@index([updatedAt])
|
||||
@@index([deletedAt])
|
||||
@@index([customerId])
|
||||
@@unique([organizationId, internalId])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { DribbbleDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-sett
|
|||
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
|
||||
import { DiscordDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/discord.dto';
|
||||
import { SlackDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/slack.dto';
|
||||
import { LemmySettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/lemmy.dto';
|
||||
|
||||
export class EmptySettings {}
|
||||
export class Integration {
|
||||
|
|
@ -66,6 +67,7 @@ export class Post {
|
|||
{ value: MediumSettingsDto, name: 'medium' },
|
||||
{ value: HashnodeSettingsDto, name: 'hashnode' },
|
||||
{ value: RedditSettingsDto, name: 'reddit' },
|
||||
{ value: LemmySettingsDto, name: 'lemmy' },
|
||||
{ value: YoutubeSettingsDto, name: 'youtube' },
|
||||
{ value: PinterestSettingsDto, name: 'pinterest' },
|
||||
{ value: DribbbleDto, name: 'dribbble' },
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsDefined,
|
||||
IsIn,
|
||||
IsNumber,
|
||||
Max,
|
||||
Min,
|
||||
IsDefined, IsIn, IsNumber, IsOptional, IsString, Max, Min
|
||||
} from 'class-validator';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
|
@ -36,4 +32,8 @@ export class GetPostsDto {
|
|||
@Max(dayjs().add(10, 'year').year())
|
||||
@Min(2022)
|
||||
year: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
customer: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
ArrayMinSize,
|
||||
IsDefined,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class LemmySettingsDtoInner {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsDefined()
|
||||
subreddit: string;
|
||||
|
||||
@IsString()
|
||||
@IsDefined()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@IsDefined()
|
||||
title: string;
|
||||
|
||||
@ValidateIf((o) => o.url)
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class LemmySettingsValueDto {
|
||||
@Type(() => LemmySettingsDtoInner)
|
||||
@IsDefined()
|
||||
@ValidateNested()
|
||||
value: LemmySettingsDtoInner;
|
||||
}
|
||||
|
||||
export class LemmySettingsDto {
|
||||
@Type(() => LemmySettingsValueDto)
|
||||
@ValidateNested({ each: true })
|
||||
@ArrayMinSize(1)
|
||||
subreddit: LemmySettingsValueDto[];
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import { DiscordProvider } from '@gitroom/nestjs-libraries/integrations/social/d
|
|||
import { SlackProvider } from '@gitroom/nestjs-libraries/integrations/social/slack.provider';
|
||||
import { MastodonProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.provider';
|
||||
import { BlueskyProvider } from '@gitroom/nestjs-libraries/integrations/social/bluesky.provider';
|
||||
import { LemmyProvider } from '@gitroom/nestjs-libraries/integrations/social/lemmy.provider';
|
||||
// import { MastodonCustomProvider } from '@gitroom/nestjs-libraries/integrations/social/mastodon.custom.provider';
|
||||
|
||||
const socialIntegrationList: SocialProvider[] = [
|
||||
|
|
@ -39,6 +40,7 @@ const socialIntegrationList: SocialProvider[] = [
|
|||
new SlackProvider(),
|
||||
new MastodonProvider(),
|
||||
new BlueskyProvider(),
|
||||
new LemmyProvider(),
|
||||
// new MastodonCustomProvider(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
import { LemmySettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/lemmy.dto';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
export class LemmyProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'lemmy';
|
||||
name = 'Lemmy';
|
||||
isBetweenSteps = false;
|
||||
scopes = [];
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
{
|
||||
key: 'service',
|
||||
label: 'Service',
|
||||
defaultValue: 'https://lemmy.world',
|
||||
validation: `/^https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)$/`,
|
||||
type: 'text' as const,
|
||||
},
|
||||
{
|
||||
key: 'identifier',
|
||||
label: 'Identifier',
|
||||
validation: `/^.{3,}$/`,
|
||||
type: 'text' as const,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password',
|
||||
validation: `/^.{3,}$/`,
|
||||
type: 'password' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
return {
|
||||
url: '',
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const body = JSON.parse(Buffer.from(params.code, 'base64').toString());
|
||||
|
||||
const load = await fetch(body.service + '/api/v3/user/login', {
|
||||
body: JSON.stringify({
|
||||
username_or_email: body.identifier,
|
||||
password: body.password,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (load.status === 401) {
|
||||
return 'Invalid credentials';
|
||||
}
|
||||
|
||||
const { jwt } = await load.json();
|
||||
|
||||
try {
|
||||
const user = await (
|
||||
await fetch(body.service + `/api/v3/user?username=${body.identifier}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
refreshToken: jwt!,
|
||||
expiresIn: dayjs().add(100, 'years').unix() - dayjs().unix(),
|
||||
accessToken: jwt!,
|
||||
id: String(user.person_view.person.id),
|
||||
name:
|
||||
user.person_view.person.display_name ||
|
||||
user.person_view.person.name ||
|
||||
'',
|
||||
picture: user.person_view.person.avatar || '',
|
||||
username: body.identifier || '',
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 'Invalid credentials';
|
||||
}
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails<LemmySettingsDto>[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const [firstPost, ...restPosts] = postDetails;
|
||||
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
|
||||
const { jwt } = await (
|
||||
await fetch(body.service + '/api/v3/user/login', {
|
||||
body: JSON.stringify({
|
||||
username_or_email: body.identifier,
|
||||
password: body.password,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
const valueArray: PostResponse[] = [];
|
||||
|
||||
for (const lemmy of firstPost.settings.subreddit) {
|
||||
const { post_view, ...all } = await (
|
||||
await fetch(body.service + '/api/v3/post', {
|
||||
body: JSON.stringify({
|
||||
community_id: +lemmy.value.id,
|
||||
name: lemmy.value.title,
|
||||
body: firstPost.message,
|
||||
...(lemmy.value.url ? { url: lemmy.value.url } : {}),
|
||||
...(firstPost.media?.length
|
||||
? { custom_thumbnail: firstPost.media[0].url }
|
||||
: {}),
|
||||
nsfw: false,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
valueArray.push({
|
||||
postId: post_view.post.id,
|
||||
releaseURL: body.service + '/post/' + post_view.post.id,
|
||||
id: firstPost.id,
|
||||
status: 'published',
|
||||
});
|
||||
|
||||
for (const comment of restPosts) {
|
||||
const { comment_view } = await (
|
||||
await fetch(body.service + '/api/v3/comment', {
|
||||
body: JSON.stringify({
|
||||
post_id: post_view.post.id,
|
||||
content: comment.message,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
valueArray.push({
|
||||
postId: comment_view.post.id,
|
||||
releaseURL: body.service + '/comment/' + comment_view.comment.id,
|
||||
id: comment.id,
|
||||
status: 'published',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(groupBy(valueArray, (p) => p.id)).map((p) => ({
|
||||
id: p[0].id,
|
||||
postId: p.map((p) => String(p.postId)).join(','),
|
||||
releaseURL: p.map((p) => p.releaseURL).join(','),
|
||||
status: 'published',
|
||||
}));
|
||||
}
|
||||
|
||||
async subreddits(
|
||||
accessToken: string,
|
||||
data: any,
|
||||
id: string,
|
||||
integration: Integration
|
||||
) {
|
||||
const body = JSON.parse(
|
||||
AuthService.fixedDecryption(integration.customInstanceDetails!)
|
||||
);
|
||||
|
||||
const { jwt } = await (
|
||||
await fetch(body.service + '/api/v3/user/login', {
|
||||
body: JSON.stringify({
|
||||
username_or_email: body.identifier,
|
||||
password: body.password,
|
||||
}),
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
const { communities } = await (
|
||||
await fetch(
|
||||
body.service +
|
||||
`/api/v3/search?type_=Communities&sort=Active&q=${data.word}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return communities.map((p: any) => ({
|
||||
title: p.community.title,
|
||||
name: p.community.title,
|
||||
id: p.community.id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue