This commit is contained in:
commit
33453ecc55
3475 files changed
+850363
No files matched your search
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useOverlay } from '@toss/use-overlay';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
toast,
|
||||
} from '@ufb/react';
|
||||
import { ErrorCode } from '@ufb/shared';
|
||||
|
||||
import {
|
||||
CreateInputTemplate,
|
||||
isObjectEqual,
|
||||
Path,
|
||||
useAllChannels,
|
||||
useOAIMutation,
|
||||
useWarnIfSavedChanges,
|
||||
} from '@/shared';
|
||||
import { isDefaultField } from '@/entities/field';
|
||||
|
||||
import {
|
||||
CREATE_CHANNEL_DEFAULT_INPUT,
|
||||
useCreateChannelStore,
|
||||
} from '../create-channel-model';
|
||||
import { CREATE_CHANNEL_MAIN_STEP_LIST } from '../create-channel-type';
|
||||
import {
|
||||
CREATE_CHANNEL_HELP_TEXT,
|
||||
CREATE_CHANNEL_REQUIRED_STEP,
|
||||
CREATE_CHANNEL_STEPPER_TEXT,
|
||||
} from '../create-channel.constant';
|
||||
|
||||
interface IProps extends React.PropsWithChildren {
|
||||
actionButton?: React.ReactNode;
|
||||
validate?: () => Promise<boolean> | boolean;
|
||||
disableNextBtn?: boolean;
|
||||
isLoading?: boolean;
|
||||
onClickBack?: () => void;
|
||||
}
|
||||
|
||||
const CreateChannelInputTemplate: React.FC<IProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
actionButton,
|
||||
validate,
|
||||
disableNextBtn,
|
||||
isLoading,
|
||||
onClickBack,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentStep, nextStep, prevStep, input, reset, jumpStepByKey } =
|
||||
useCreateChannelStore();
|
||||
|
||||
const router = useRouter();
|
||||
const projectId = Number(router.query.projectId);
|
||||
const overlay = useOverlay();
|
||||
const { refetch } = useAllChannels(projectId);
|
||||
|
||||
const openCreateChannelError = () => {
|
||||
overlay.open(({ isOpen, close }) => (
|
||||
<Dialog open={isOpen} onOpenChange={close}>
|
||||
<DialogContent>
|
||||
<DialogHeader>{t('text.guide')}</DialogHeader>
|
||||
<DialogBody>
|
||||
<DialogDescription>
|
||||
{t('main.create-channel.guide.invalid-channel')}
|
||||
</DialogDescription>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<DialogClose>{t('v2.button.cancel')}</DialogClose>
|
||||
<Button onClick={() => jumpStepByKey('channel-info')}>
|
||||
{t('v2.button.confirm')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
));
|
||||
};
|
||||
|
||||
const { mutate } = useOAIMutation({
|
||||
method: 'post',
|
||||
path: '/api/admin/projects/{projectId}/channels',
|
||||
pathParams: { projectId },
|
||||
queryOptions: {
|
||||
async onSuccess(data) {
|
||||
await refetch();
|
||||
await router.replace({
|
||||
pathname: Path.CREATE_CHANNEL_COMPLETE,
|
||||
query: { projectId, channelId: data?.id },
|
||||
});
|
||||
reset();
|
||||
},
|
||||
onError(error) {
|
||||
if (error.code === ErrorCode.Channel.ChannelAlreadyExists) {
|
||||
openCreateChannelError();
|
||||
} else {
|
||||
toast.error(error.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isDefaultInput = useMemo(
|
||||
() => isObjectEqual(CREATE_CHANNEL_DEFAULT_INPUT, input),
|
||||
[input],
|
||||
);
|
||||
|
||||
useWarnIfSavedChanges(
|
||||
!isDefaultInput,
|
||||
Path.CREATE_CHANNEL_COMPLETE.replace(
|
||||
'[projectId]',
|
||||
router.query.projectId as string,
|
||||
),
|
||||
);
|
||||
|
||||
const onComplete = () => {
|
||||
mutate({
|
||||
...input.channelInfo,
|
||||
fields: input.fields.filter((v) => !isDefaultField(v)),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<CreateInputTemplate
|
||||
currentStepIndex={currentStep.index}
|
||||
lastStep={CREATE_CHANNEL_MAIN_STEP_LIST.length - 1}
|
||||
onNext={nextStep}
|
||||
onPrev={prevStep}
|
||||
title={CREATE_CHANNEL_STEPPER_TEXT[currentStep.key]}
|
||||
actionButton={actionButton}
|
||||
onComplete={onComplete}
|
||||
validate={validate}
|
||||
disableNextBtn={disableNextBtn ?? isLoading}
|
||||
helpText={CREATE_CHANNEL_HELP_TEXT[currentStep.key]}
|
||||
onClickBack={onClickBack}
|
||||
isRequiredStep={CREATE_CHANNEL_REQUIRED_STEP[currentStep.key]}
|
||||
>
|
||||
{children}
|
||||
</CreateInputTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateChannelInputTemplate;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { CreationLayout, CreationStepper, Path } from '@/shared';
|
||||
|
||||
import { useCreateChannelStore } from '../create-channel-model';
|
||||
import { CREATE_CHANNEL_MAIN_STEP_LIST } from '../create-channel-type';
|
||||
import {
|
||||
CREATE_CHANNEL_COMPONENTS,
|
||||
CREATE_CHANNEL_STEPPER_TEXT,
|
||||
} from '../create-channel.constant';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const CreateChannel: React.FC<IProps> = () => {
|
||||
const { currentStep } = useCreateChannelStore();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<CreationLayout
|
||||
title={t('main.create-channel.title')}
|
||||
leftPanel={
|
||||
<CreationStepper
|
||||
steps={CREATE_CHANNEL_MAIN_STEP_LIST}
|
||||
currentStepIndex={currentStep.index}
|
||||
stepTitle={CREATE_CHANNEL_STEPPER_TEXT}
|
||||
/>
|
||||
}
|
||||
leftBottom={
|
||||
<div className="flex justify-end">
|
||||
<Image
|
||||
src={`/assets/images/create-channel-${currentStep.key}.svg`}
|
||||
alt="Create Project"
|
||||
className="align"
|
||||
width={320}
|
||||
height={320}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onClickGoBack={() =>
|
||||
router.push({
|
||||
pathname: Path.PROJECT_MAIN,
|
||||
query: { projectId: router.query.projectId },
|
||||
})
|
||||
}
|
||||
>
|
||||
{CREATE_CHANNEL_COMPONENTS[currentStep.key]}
|
||||
</CreationLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateChannel;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as CreateChannel } from './create-channel.ui';
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { useAllChannels } from '@/shared';
|
||||
import { EMPTY_FUNCTION } from '@/shared/utils/empty-function';
|
||||
import type { ChannelInfo } from '@/entities/channel';
|
||||
import { ChannelInfoForm, channelInfoSchema } from '@/entities/channel';
|
||||
|
||||
import { useCreateChannelStore } from '../create-channel-model';
|
||||
import CreateChannelInputTemplate from './create-channel-input-template.ui';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const InputChannelInfoStep: React.FC<IProps> = () => {
|
||||
const { onChangeInput, input, setEditingStepIndex } = useCreateChannelStore();
|
||||
const router = useRouter();
|
||||
const projectId = Number(router.query.projectId);
|
||||
const { data } = useAllChannels(projectId);
|
||||
|
||||
const methods = useForm<ChannelInfo>({
|
||||
resolver: zodResolver(channelInfoSchema),
|
||||
defaultValues: input.channelInfo,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = methods.watch((values) => {
|
||||
const newValues = channelInfoSchema.safeParse(values);
|
||||
if (!newValues.data) return;
|
||||
onChangeInput('channelInfo', newValues.data);
|
||||
setEditingStepIndex(0);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CreateChannelInputTemplate
|
||||
validate={async () => {
|
||||
const isValid = await methods.trigger();
|
||||
await methods.handleSubmit(EMPTY_FUNCTION)();
|
||||
const isDuplicated = data?.items.some(
|
||||
(v) => v.name === methods.getValues('name'),
|
||||
);
|
||||
if (isDuplicated) {
|
||||
methods.setError('name', { message: 'Duplicated name' });
|
||||
return false;
|
||||
}
|
||||
return isValid;
|
||||
}}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<ChannelInfoForm type="create" />
|
||||
</FormProvider>
|
||||
</CreateChannelInputTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputChannelInfoStep;
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { PreviewFieldTable } from '@/entities/field';
|
||||
|
||||
import { useCreateChannelStore } from '../create-channel-model';
|
||||
import CreateChannelInputTemplate from './create-channel-input-template.ui';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const InputFieldPreviewStep: React.FC<IProps> = () => {
|
||||
const { input, jumpStepByKey } = useCreateChannelStore();
|
||||
|
||||
return (
|
||||
<CreateChannelInputTemplate onClickBack={() => jumpStepByKey('field')}>
|
||||
<PreviewFieldTable
|
||||
fields={input.fields.filter((v) => v.status === 'ACTIVE')}
|
||||
/>
|
||||
</CreateChannelInputTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputFieldPreviewStep;
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useOverlay } from '@toss/use-overlay';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { Icon, ToggleGroup, ToggleGroupItem } from '@ufb/react';
|
||||
|
||||
import { FieldSettingSheet } from '@/entities/field';
|
||||
import type { FieldInfo } from '@/entities/field';
|
||||
import FieldTable from '@/entities/field/ui/field-table.ui';
|
||||
|
||||
import { useCreateChannelStore } from '../create-channel-model';
|
||||
import CreateChannelInputTemplate from './create-channel-input-template.ui';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const InputFieldStep: React.FC<IProps> = () => {
|
||||
const { input, onChangeInput, jumpStepByKey } = useCreateChannelStore();
|
||||
const fields = input.fields;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const overlay = useOverlay();
|
||||
|
||||
const createField = (field: FieldInfo) => {
|
||||
onChangeInput(
|
||||
'fields',
|
||||
input.fields.concat({ ...field, order: fields.length }),
|
||||
);
|
||||
};
|
||||
|
||||
const updateField = (input: { index: number; field: FieldInfo }) => {
|
||||
const { field, index } = input;
|
||||
onChangeInput(
|
||||
'fields',
|
||||
fields.map((v, i) => (i === index ? field : v)),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteField = ({ index }: { index: number }) => {
|
||||
onChangeInput(
|
||||
'fields',
|
||||
input.fields.filter((_, i) => i !== index),
|
||||
);
|
||||
};
|
||||
|
||||
const openFieldFormSheet = (input?: { index: number; field: FieldInfo }) => {
|
||||
overlay.open(({ close, isOpen }) => (
|
||||
<FieldSettingSheet
|
||||
isOpen={isOpen}
|
||||
close={close}
|
||||
onSubmit={(newField) => {
|
||||
if (input) {
|
||||
updateField({ index: input.index, field: newField });
|
||||
return;
|
||||
}
|
||||
createField(newField);
|
||||
}}
|
||||
fieldRows={fields}
|
||||
data={input?.field}
|
||||
onClickDelete={
|
||||
input ?
|
||||
() => {
|
||||
deleteField({ index: input.index });
|
||||
close();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<CreateChannelInputTemplate
|
||||
actionButton={
|
||||
<ToggleGroup type="single" value="">
|
||||
<ToggleGroupItem value="item-1" onClick={() => openFieldFormSheet()}>
|
||||
<Icon name="RiAddLine" />
|
||||
{t('v2.button.name.add', { name: 'Field' })}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="item-2"
|
||||
onClick={() => jumpStepByKey('field-preview')}
|
||||
>
|
||||
<Icon name="RiEyeLine" />
|
||||
{t('v2.text.preview')}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
}
|
||||
>
|
||||
<FieldTable
|
||||
fields={input.fields}
|
||||
onClickRow={(index, field) => openFieldFormSheet({ index, field })}
|
||||
disableFilter
|
||||
/>
|
||||
</CreateChannelInputTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputFieldStep;
|
||||
Reference in new issue
Block a user