feat: sets selection modal

This commit is contained in:
Nevo David 2025-06-09 19:23:51 +07:00
parent 35625b281b
commit ae1ab39ed4
9 changed files with 151 additions and 114 deletions

View File

@ -61,17 +61,21 @@ 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[];
setId?: string;
set?: CreatePostDto;
addEditSets?: (data: any) => void;
reopenModal: () => void;
mutate: () => void;
@ -95,7 +99,9 @@ export const AddEditModal: FC<{
padding,
customClose,
addEditSets,
set,
} = props;
const [customer, setCustomer] = useState('');
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
@ -105,17 +111,24 @@ export const AddEditModal: FC<{
// selected integrations to allow edit
const [selectedIntegrations, setSelectedIntegrations] = useStateCallback<
Integrations[]
>([]);
>(
set
? ints.filter(
(f) =>
uniq(set.posts.flatMap((p) => p.integration.id)).indexOf(f.id) > -1
)
: []
);
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
@ -134,6 +147,12 @@ export const AddEditModal: FC<{
>(
onlyValues
? onlyValues
: set
? set?.posts?.[0].value || [
{
content: '',
},
]
: [
{
content: '',
@ -467,18 +486,24 @@ export const AddEditModal: FC<{
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,
@ -578,7 +603,7 @@ export const AddEditModal: FC<{
}, [data, postFor, selectedIntegrations]);
useClickOutside(askClose);
return (
<>
<SetContext.Provider value={{ set }}>
{user?.tier?.ai && (
<CopilotPopup
hitEscapeToClose={false}
@ -658,7 +683,7 @@ Here are the things you can do:
<PickPlatforms
toolTip={true}
integrations={integrations.filter((f) => !f.disabled)}
selectedIntegrations={[]}
selectedIntegrations={set ? selectedIntegrations : []}
singleSelect={false}
onChange={setSelectedIntegrations}
isMain={true}
@ -938,6 +963,7 @@ Here are the things you can do:
{!!selectedIntegrations.length && (
<div className="flex-1 flex flex-col p-[16px] pt-0">
<ProvidersOptions
hideEditOnlyThis={!!set}
allIntegrations={props.allIntegrations || []}
integrations={selectedIntegrations}
editorValue={value}
@ -947,6 +973,6 @@ Here are the things you can do:
)}
</div>
</div>
</>
</SetContext.Provider>
);
});

View File

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

View File

@ -336,6 +336,7 @@ export const CalendarColumn: FC<{
changeDate,
display,
reloadCalendarView,
sets,
} = useCalendar();
const toaster = useToaster();
const modal = useModals();
@ -547,8 +548,13 @@ export const CalendarColumn: FC<{
},
[integrations]
);
const addModal = useCallback(async () => {
const signature = await (await fetch('/signatures/default')).json();
const set = !sets.length ? undefined : await new Promise(() => {
});
modal.openModal({
closeOnClickOutside: false,
closeOnEscape: false,
@ -583,7 +589,7 @@ export const CalendarColumn: FC<{
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({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,12 +7,7 @@ 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';
@ -20,6 +15,48 @@ import { useT } from '@gitroom/react/translation/get.transation.service.client';
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
import dayjs from 'dayjs';
const SaveSetModal: FC<{
postData: any;
initialValue?: string;
onSave: (name: string) => void;
onCancel: () => void;
}> = ({ postData, onSave, onCancel, initialValue }) => {
const [name, setName] = useState(initialValue);
const t = useT();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name.trim()) {
onSave(name.trim());
}
};
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div>
<Input
label="Set Name"
translationKey="label_set_name"
name="setName"
value={name}
disableForm={true}
onChange={(e) => setName(e.target.value)}
placeholder="Enter a name for this set"
autoFocus
/>
</div>
<div className="flex gap-2 justify-end">
<Button type="button" secondary onClick={onCancel}>
{t('cancel', 'Cancel')}
</Button>
<Button type="submit" disabled={!name.trim()}>
{t('save', 'Save')}
</Button>
</div>
</form>
);
};
export const Sets: FC = () => {
const fetch = useFetch();
const user = useUser();
@ -41,7 +78,7 @@ export const Sets: FC = () => {
const { data, mutate } = useSWR('sets', list);
const addSet = useCallback(
(data?: any) => () => {
(params?: { id?: string; name?: string; content?: string }) => () => {
modal.openModal({
closeOnClickOutside: false,
closeOnEscape: false,
@ -54,8 +91,40 @@ export const Sets: FC = () => {
allIntegrations={integrations.map((p: any) => ({
...p,
}))}
{...(params?.id ? { set: JSON.parse(params.content) } : {})}
addEditSets={(data) => {
console.log('save', data);
modal.openModal({
title: 'Save as Set',
classNames: {
modal: 'bg-sixth text-textColor',
title: 'text-textColor',
},
children: (
<SaveSetModal
initialValue={params?.name || ''}
postData={data}
onSave={async (name: string) => {
try {
await fetch('/sets', {
method: 'POST',
body: JSON.stringify({
...(params?.id ? { id: params.id } : {}),
name,
content: JSON.stringify(data),
}),
});
modal.closeAll();
mutate();
toaster.show('Set saved successfully', 'success');
} catch (error) {
toaster.show('Failed to save set', 'warning');
}
}}
onCancel={() => modal.closeAll()}
/>
),
size: 'md',
});
}}
reopenModal={() => {}}
mutate={() => {}}
@ -130,90 +199,3 @@ export const Sets: FC = () => {
</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>
);
};