first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import {MessageDescriptor} from '@common/i18n/message-descriptor';
import {Channel} from '@common/channels/channel';
import {ReactElement} from 'react';
import {SvgIconProps} from '@common/icons/svg-icon';
export interface ChannelContentConfig {
models: Record<
string,
{
label: MessageDescriptor;
sortMethods: string[];
layoutMethods: string[];
autoUpdateMethods?: string[];
}
>;
sortingMethods: Record<
string,
{
label: MessageDescriptor;
contentTypes?: Channel['config']['contentType'][];
}
>;
layoutMethods: Record<
string,
{
label: MessageDescriptor;
icon?: ReactElement<SvgIconProps>;
}
>;
autoUpdateMethods: Record<
string,
{
label: MessageDescriptor;
provider?: string;
value?: {
label: MessageDescriptor;
inputType: 'text' | 'number';
};
}
>;
userSelectableLayouts: string[];
}

View File

@@ -0,0 +1,351 @@
import {useFormContext} from 'react-hook-form';
import {NormalizedModel} from '@common/datatable/filters/normalized-model';
import {Trans} from '@common/i18n/trans';
import {Table} from '@common/ui/tables/table';
import {RowElementProps} from '@common/ui/tables/table-row';
import {useIsTouchDevice} from '@common/utils/hooks/is-touch-device';
import React, {
cloneElement,
ReactElement,
ReactNode,
useContext,
useRef,
useState,
} from 'react';
import {TableContext} from '@common/ui/tables/table-context';
import {DragPreviewRenderer} from '@common/ui/interactions/dnd/use-draggable';
import {
DropPosition,
useSortable,
} from '@common/ui/interactions/dnd/sortable/use-sortable';
import clsx from 'clsx';
import {mergeProps} from '@react-aria/utils';
import {ColumnConfig} from '@common/datatable/column-config';
import {DragHandleIcon} from '@common/icons/material/DragHandle';
import {NameWithAvatar} from '@common/datatable/column-templates/name-with-avatar';
import {IconButton} from '@common/ui/buttons/icon-button';
import {CloseIcon} from '@common/icons/material/Close';
import {DragPreview} from '@common/ui/interactions/dnd/drag-preview';
import {WarningIcon} from '@common/icons/material/Warning';
import {IllustratedMessage} from '@common/ui/images/illustrated-message';
import playlist from '../playlist.svg';
import {SvgImage} from '@common/ui/images/svg-image/svg-image';
import {Link, useParams} from 'react-router-dom';
import {Button} from '@common/ui/buttons/button';
import {RefreshIcon} from '@common/icons/material/Refresh';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {useUpdateChannelContent} from '@common/admin/channels/requests/use-update-channel-content';
import {ChannelContentSearchFieldProps} from '@common/admin/channels/channel-editor/channel-content-search-field';
import {useChannelContent} from '@common/channels/requests/use-channel-content';
import {PaginationControls} from '@common/ui/navigation/pagination-controls';
import {queryClient} from '@common/http/query-client';
import {PaginationResponse} from '@common/http/backend-response/pagination-response';
import {moveItemInNewArray} from '@common/utils/array/move-item-in-new-array';
import {useReorderChannelContent} from '@common/admin/channels/requests/use-reorder-channel-content';
import {useAddToChannel} from '@common/admin/channels/requests/use-add-to-channel';
import {useRemoveFromChannel} from '@common/admin/channels/requests/use-remove-from-channel';
import {ChannelContentItem} from '@common/channels/channel';
const columnConfig: ColumnConfig<NormalizedModel>[] = [
{
key: 'dragHandle',
width: 'w-42 flex-shrink-0',
header: () => <Trans message="Drag handle" />,
hideHeader: true,
body: () => (
<DragHandleIcon className="cursor-pointer text-muted hover:text" />
),
},
{
key: 'name',
header: () => <Trans message="Content item" />,
visibleInMode: 'all',
body: item => {
return (
<NameWithAvatar
image={item.image}
label={
item.model_type === 'channel' ? (
<Link
className="hover:underline"
to={`/admin/channels/${item.id}/edit`}
target="_blank"
>
{item.name}
</Link>
) : (
item.name
)
}
description={item.description}
/>
);
},
},
{
key: 'type',
header: () => <Trans message="Content type" />,
width: 'w-100 flex-shrink-0',
body: item => <span className="capitalize">{item.model_type}</span>,
},
{
key: 'actions',
header: () => <Trans message="Actions" />,
hideHeader: true,
align: 'end',
width: 'w-42 flex-shrink-0',
visibleInMode: 'all',
body: item => <RemoveItemColumn item={item} />,
},
];
interface Props {
searchField: ReactElement<ChannelContentSearchFieldProps>;
title?: ReactNode;
noResultsMessage?: ReactNode;
}
export function ChannelContentEditor({
searchField,
title,
noResultsMessage,
}: Props) {
const {watch, getValues} = useFormContext<UpdateChannelPayload>();
const channel = getValues();
const contentType = watch('config.contentType');
const addToChannel = useAddToChannel();
const query = useChannelContent<ChannelContentItem<NormalizedModel>>(
channel,
{loader: 'editChannelPage', paginate: 'simple'},
{paginate: true},
);
const pagination = query.data!;
// only show delete and drag buttons when channel content is managed manually
const filteredColumns = columnConfig.filter(col => {
return !(
contentType !== 'manual' &&
(col.key === 'actions' || col.key === 'dragHandle')
);
});
return (
<div className="mt-40">
<div className="mb-40">
<h2 className="mb-10 text-2xl">
{title || <Trans message="Channel content" />}
</h2>
<ContentNotEditableWarning />
<UpdateContentButton />
{contentType === 'manual'
? cloneElement<ChannelContentSearchFieldProps>(searchField, {
onResultSelected: result => {
addToChannel.mutate({
channelId: channel.id,
item: result,
});
},
})
: null}
</div>
<PaginationControls
pagination={query.data}
type="simple"
className="mb-24"
/>
<Table
className="mt-24"
columns={filteredColumns}
data={pagination?.data || []}
meta={query.queryKey}
renderRowAs={contentType === 'manual' ? ContentTableRow : undefined}
enableSelection={false}
hideHeaderRow
/>
<PaginationControls
pagination={query.data}
type="simple"
className="mt-24"
scrollToTop
/>
{!pagination.data?.length && contentType === 'manual'
? noResultsMessage || (
<IllustratedMessage
title={<Trans message="Channel is empty" />}
description={
<Trans message="No content is attached to this channel yet." />
}
image={<SvgImage src={playlist} />}
/>
)
: null}
</div>
);
}
function ContentTableRow({
item,
children,
className,
...domProps
}: RowElementProps<NormalizedModel>) {
const isTouchDevice = useIsTouchDevice();
const {data, meta} = useContext(TableContext);
const {getValues} = useFormContext<UpdateChannelPayload>();
const domRef = useRef<HTMLTableRowElement>(null);
const reorderContent = useReorderChannelContent();
const previewRef = useRef<DragPreviewRenderer>(null);
const [dropPosition, setDropPosition] = useState<DropPosition>(null);
const {sortableProps} = useSortable({
ref: domRef,
disabled: isTouchDevice ?? false,
item,
items: data,
type: 'channelContentItem',
preview: previewRef,
strategy: 'line',
onDropPositionChange: position => {
setDropPosition(position);
},
onSortEnd: (oldIndex, newIndex) => {
// do optimistic reorder
const newPagination = queryClient.setQueryData<
PaginationResponse<unknown>
>(meta, pagination => {
if (pagination) {
pagination = {
...pagination,
data: moveItemInNewArray(pagination.data, oldIndex, newIndex),
};
}
return pagination;
});
// reorder on backend
if (newPagination) {
reorderContent.mutate({
channelId: getValues('id'),
modelType: item.model_type,
ids: newPagination.data.map(item => (item as NormalizedModel).id),
});
}
},
});
return (
<div
className={clsx(
className,
dropPosition === 'before' && 'sort-preview-before',
dropPosition === 'after' && 'sort-preview-after',
)}
ref={domRef}
{...mergeProps(sortableProps, domProps)}
>
{children}
{!item.isPlaceholder && <RowDragPreview item={item} ref={previewRef} />}
</div>
);
}
interface RowDragPreviewProps {
item: NormalizedModel;
}
const RowDragPreview = React.forwardRef<
DragPreviewRenderer,
RowDragPreviewProps
>(({item}, ref) => {
return (
<DragPreview ref={ref}>
{() => (
<div className="rounded bg-chip p-8 text-base shadow">{item.name}</div>
)}
</DragPreview>
);
});
interface RemoveItemColumnProps {
item: NormalizedModel;
}
function RemoveItemColumn({item}: RemoveItemColumnProps) {
const removeFromChannel = useRemoveFromChannel();
const {getValues} = useFormContext<UpdateChannelPayload>();
return (
<IconButton
size="md"
className="text-muted"
disabled={removeFromChannel.isPending}
onClick={() => {
removeFromChannel.mutate({
channelId: getValues('id'),
item: item,
});
}}
>
<CloseIcon />
</IconButton>
);
}
function ContentNotEditableWarning() {
const {watch} = useFormContext<UpdateChannelPayload>();
const contentType = watch('config.contentType');
if (contentType === 'manual') {
return null;
}
return (
<div className="mb-20 mt-4 flex items-center gap-8">
<WarningIcon size="xs" />
<div className="text-xs text-muted">
{contentType === 'listAll' ? (
<Trans message="This channel is listing all available content of specified type, and can't be curated manually." />
) : null}
{contentType === 'autoUpdate' ? (
<Trans message="This channel content is set to update automatically and can't be curated manually." />
) : null}
</div>
</div>
);
}
function UpdateContentButton() {
const {slugOrId} = useParams();
const updateContent = useUpdateChannelContent(slugOrId!);
const {setValue, watch, getValues} = useFormContext<UpdateChannelPayload>();
if (watch('config.contentType') !== 'autoUpdate') {
return null;
}
return (
<Button
size="xs"
variant="outline"
color="primary"
startIcon={<RefreshIcon />}
onClick={() => {
updateContent.mutate(
{
channelConfig: (getValues as any)('config'),
},
{
onSuccess: response => {
if (response.channel.content) {
(setValue as any)('content', response.channel.content);
}
},
},
);
}}
disabled={
updateContent.isPending ||
!watch('config.autoUpdateMethod') ||
!watch('id')
}
>
<Trans message="Update content now" />
</Button>
);
}

View File

@@ -0,0 +1,58 @@
import {NormalizedModel} from '@common/datatable/filters/normalized-model';
import {useFormContext} from 'react-hook-form';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {useTrans} from '@common/i18n/use-trans';
import React, {useState} from 'react';
import {useAddableContent} from '@common/admin/channels/requests/use-addable-content';
import {ComboBox} from '@common/ui/forms/combobox/combobox';
import {message} from '@common/i18n/message';
import {SearchIcon} from '@common/icons/material/Search';
import {Item} from '@common/ui/forms/listbox/item';
export interface ChannelContentSearchFieldProps {
onResultSelected?: (result: NormalizedModel) => void;
imgRenderer?: (result: NormalizedModel) => React.ReactNode;
}
export function ChannelContentSearchField({
onResultSelected,
imgRenderer,
}: ChannelContentSearchFieldProps) {
const {watch} = useFormContext<UpdateChannelPayload>();
const contentModel = watch('config.contentModel');
const {trans} = useTrans();
const [query, setQuery] = useState('');
const {isFetching, data} = useAddableContent({
query,
modelType: contentModel,
limit: 20,
});
return (
<ComboBox
isAsync
placeholder={trans(message('Search for content to add...'))}
isLoading={isFetching}
inputValue={query}
onInputValueChange={setQuery}
clearInputOnItemSelection
blurReferenceOnItemSelection
selectionMode="none"
openMenuOnFocus
floatingMaxHeight={670}
startAdornment={<SearchIcon />}
hideEndAdornment
>
{data?.results.map(result => (
<Item
key={result.id}
value={result.id}
onSelected={() => onResultSelected?.(result)}
startIcon={imgRenderer ? imgRenderer(result) : null}
description={result.description}
textLabel={result.name}
>
{result.name}
</Item>
))}
</ComboBox>
);
}

View File

@@ -0,0 +1,49 @@
import {TabList} from '@common/ui/tabs/tab-list';
import {Tab} from '@common/ui/tabs/tab';
import {Trans} from '@common/i18n/trans';
import {TabPanel, TabPanels} from '@common/ui/tabs/tab-panels';
import {Tabs} from '@common/ui/tabs/tabs';
import React, {Fragment, ReactNode} from 'react';
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
interface Props {
children: ReactNode;
}
export function ChannelEditorTabs({children}: Props) {
return (
<Tabs isLazy>
<TabList>
<Tab>
<Trans message="Details" />
</Tab>
<Tab>
<Trans message="SEO" />
</Tab>
</TabList>
<TabPanels className="pt-20">
<TabPanel>{children}</TabPanel>
<TabPanel>
<SeoFields />
</TabPanel>
</TabPanels>
</Tabs>
);
}
function SeoFields() {
return (
<Fragment>
<FormTextField
name="config.seoTitle"
label={<Trans message="SEO title" />}
className="mb-24"
/>
<FormTextField
name="config.seoDescription"
label={<Trans message="SEO description" />}
inputElementType="textarea"
rows={6}
/>
</Fragment>
);
}

View File

@@ -0,0 +1,57 @@
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
import {Trans} from '@common/i18n/trans';
import React, {Fragment} from 'react';
import {useFormContext} from 'react-hook-form';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {SlugEditor} from '@common/ui/slug-editor';
import {useTrans} from '@common/i18n/use-trans';
import {message} from '@common/i18n/message';
import clsx from 'clsx';
interface Props {
className?: string;
autoFocus?: boolean;
}
export function ChannelNameField({className, autoFocus}: Props) {
return (
<Fragment>
<FormTextField
name="name"
label={<Trans message="Title" />}
required
autoFocus={autoFocus}
className={clsx('mb-10', className)}
/>
<FormSlugField />
</Fragment>
);
}
function FormSlugField() {
const {watch, setValue} = useFormContext<UpdateChannelPayload>();
const value = watch('slug');
const name = watch('name');
const disableSlugEditing = watch('config.lockSlug');
const restriction = watch('config.restriction');
const restrictionId = watch('config.restrictionModelId');
const {trans} = useTrans();
return (
<SlugEditor
hideButton={disableSlugEditing}
placeholder={name}
suffix={
restriction && restrictionId === 'urlParam'
? trans(message(':restriction_name', {values: {restriction}}))
: undefined
}
className="text-sm"
pattern="[A-Za-z0-9_-]+"
minLength={3}
maxLength={20}
value={value}
onChange={newSlug => {
setValue('slug', newSlug);
}}
/>
);
}

View File

@@ -0,0 +1,28 @@
import {FormSelect, Option} from '@common/ui/forms/select/select';
import {Trans} from '@common/i18n/trans';
import {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';
interface Props {
config: ChannelContentConfig;
className?: string;
}
export function ChannelPaginationTypeField({className}: Props) {
return (
<FormSelect
className={className}
selectionMode="single"
name="config.paginationType"
label={<Trans message="Pagination type" />}
>
<Option value="infiniteScroll">
<Trans message="Infinite scroll" />
</Option>
<Option value="lengthAware">
<Trans message="List of page buttons" />
</Option>
<Option value="simple">
<Trans message="Next/previous page buttons only" />
</Option>
</FormSelect>
);
}

View File

@@ -0,0 +1,79 @@
import {useFormContext} from 'react-hook-form';
import {FormSelect, Option} from '@common/ui/forms/select/select';
import {Trans} from '@common/i18n/trans';
import {InfoDialogTrigger} from '@common/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger';
import {Fragment, ReactNode} from 'react';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
import clsx from 'clsx';
import {ChannelsDocsLink} from '@common/admin/channels/channels-docs-link';
interface Props {
children?: ReactNode;
config: ChannelContentConfig;
className?: string;
}
export function ContentAutoUpdateField({children, config, className}: Props) {
const {watch, setValue} = useFormContext<UpdateChannelPayload>();
const modelConfig = config.models[watch('config.contentModel')];
const selectedMethodConfig =
config.autoUpdateMethods[watch('config.autoUpdateMethod')!];
if (
watch('config.contentType') !== 'autoUpdate' ||
!modelConfig.autoUpdateMethods?.length
) {
return null;
}
return (
<div className={clsx('items-end gap-14 md:flex', className)}>
<FormSelect
required
className="flex-auto"
selectionMode="single"
name="config.autoUpdateMethod"
onSelectionChange={value => {
if (config.autoUpdateMethods[value].provider) {
setValue(
'config.autoUpdateProvider',
config.autoUpdateMethods[value].provider,
);
}
}}
label={
<Fragment>
<Trans message="Auto update method" />
<InfoDialogTrigger
body={
<Fragment>
<div className="mb-20">
<Trans message="This option will automatically update channel content every 24 hours using the specified method." />
</div>
<ChannelsDocsLink hash="automatically-update-content-with-specified-method" />
</Fragment>
}
/>
</Fragment>
}
>
{modelConfig.autoUpdateMethods.map(method => (
<Option value={method} key={method}>
<Trans {...config.autoUpdateMethods[method].label} />
</Option>
))}
</FormSelect>
{selectedMethodConfig?.value ? (
<FormTextField
name="config.autoUpdateValue"
required
className="flex-auto"
label={<Trans {...selectedMethodConfig?.value.label} />}
type={selectedMethodConfig?.value.inputType}
/>
) : null}
{children}
</div>
);
}

View File

@@ -0,0 +1,60 @@
import {useFormContext} from 'react-hook-form';
import {FormSelect, Option} from '@common/ui/forms/select/select';
import {Trans} from '@common/i18n/trans';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {ReactNode} from 'react';
import {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';
import clsx from 'clsx';
interface Props {
config: ChannelContentConfig;
className?: string;
}
export function ContentLayoutFields({config, className}: Props) {
return (
<div className={clsx('items-end gap-14 md:flex', className)}>
<LayoutField
config={config}
name="config.layout"
label={<Trans message="Layout" />}
/>
<LayoutField
config={config}
name="config.nestedLayout"
label={<Trans message="Layout when nested" />}
/>
</div>
);
}
interface LayoutFieldProps extends Props {
name: string;
label: ReactNode;
}
function LayoutField({config, name, label}: LayoutFieldProps) {
const {watch} = useFormContext<UpdateChannelPayload>();
const contentModel = watch('config.contentModel');
const modelConfig = config.models[contentModel];
if (!modelConfig.layoutMethods?.length) {
return null;
}
return (
<FormSelect
className="w-full flex-auto"
selectionMode="single"
name={name}
label={label}
>
{modelConfig.layoutMethods.map(method => {
const label = config.layoutMethods[method].label;
return (
<Option key={method} value={method}>
<Trans {...label} />
</Option>
);
})}
</FormSelect>
);
}

View File

@@ -0,0 +1,46 @@
import {useFormContext} from 'react-hook-form';
import {FormSelect, Option} from '@common/ui/forms/select/select';
import {Trans} from '@common/i18n/trans';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import React from 'react';
import {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';
interface Props {
config: ChannelContentConfig;
className?: string;
exclude?: string[];
}
export function ContentModelField({config, className, exclude}: Props) {
const {setValue, getValues} = useFormContext<UpdateChannelPayload>();
return (
<FormSelect
className={className}
selectionMode="single"
name="config.contentModel"
label={<Trans message="Type of content" />}
onSelectionChange={newValue => {
const modelConfig = config.models[newValue];
if (
getValues('config.contentType') === 'autoUpdate' &&
!modelConfig.autoUpdateMethods?.length
) {
(setValue as any)('config.contentType', 'manual');
}
setValue('config.autoUpdateMethod', modelConfig.autoUpdateMethods?.[0]);
setValue(
'config.contentOrder',
modelConfig.sortMethods[0] || 'channelables.order:asc',
);
setValue('config.layout', modelConfig.layoutMethods[0]);
}}
>
{Object.entries(config.models)
.filter(([model]) => !exclude?.includes(model))
.map(([model, {label}]) => (
<Option value={model} key={model}>
<Trans {...label} />
</Option>
))}
</FormSelect>
);
}

View File

@@ -0,0 +1,39 @@
import {useFormContext} from 'react-hook-form';
import {FormSelect, Option} from '@common/ui/forms/select/select';
import {Trans} from '@common/i18n/trans';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';
interface Props {
config: ChannelContentConfig;
className?: string;
}
export function ContentOrderField({config, className}: Props) {
const {watch} = useFormContext<UpdateChannelPayload>();
const contentType = watch('config.contentType');
const modelConfig = config.models[watch('config.contentModel')];
const sortMethods = [...modelConfig.sortMethods, 'channelables.order:asc'];
return (
<FormSelect
className={className}
selectionMode="single"
name="config.contentOrder"
label={<Trans message="How to order content" />}
>
{sortMethods.map(method => {
const sortConfig = config.sortingMethods[method];
if (
!sortConfig.contentTypes ||
sortConfig.contentTypes.includes(contentType)
) {
return (
<Option value={method} key={method}>
<Trans {...sortConfig.label} />
</Option>
);
}
})}
</FormSelect>
);
}

View File

@@ -0,0 +1,54 @@
import {useFormContext} from 'react-hook-form';
import {FormSelect, Option} from '@common/ui/forms/select/select';
import {Trans} from '@common/i18n/trans';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';
interface Props {
config: ChannelContentConfig;
className?: string;
}
export function ContentTypeField({config, className}: Props) {
const {setValue} = useFormContext<UpdateChannelPayload>();
return (
<FormSelect
className={className}
selectionMode="single"
name="config.contentType"
label={<Trans message="Content" />}
onSelectionChange={newValue => {
// if content type is "auto update" select first model that
// can be auto updated, otherwise select first available model
let model = Object.entries(config.models)[0];
if (newValue === 'autoUpdate') {
const newModel = Object.entries(config.models).find(
([, modelConfig]) => modelConfig.autoUpdateMethods?.length,
);
if (newModel) {
model = newModel;
}
}
const [modelName, modelConfig] = model;
setValue('config.contentModel', modelName);
setValue('config.restrictionModelId', undefined);
setValue(
'config.autoUpdateMethod',
newValue === 'autoUpdate' ? modelConfig.autoUpdateMethods?.[0] : '',
);
setValue('config.contentOrder', modelConfig.sortMethods[0]);
(setValue as any)('config.restriction', null);
}}
>
<Option value="listAll">
<Trans message="List all content of specified type" />
</Option>
<Option value="manual">
<Trans message="Manage content manually" />
</Option>
<Option value="autoUpdate">
<Trans message="Automatically update content with specified method" />
</Option>
</FormSelect>
);
}

View File

@@ -0,0 +1,39 @@
import {useForm} from 'react-hook-form';
import React, {ReactNode} from 'react';
import {CrupdateResourceLayout} from '@common/admin/crupdate-resource-layout';
import {Trans} from '@common/i18n/trans';
import {EMPTY_PAGINATION_RESPONSE} from '@common/http/backend-response/pagination-response';
import {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';
import {useCreateChannel} from '@common/admin/channels/requests/use-create-channel';
interface Props {
defaultValues?: Partial<UpdateChannelPayload['config']>;
children: ReactNode;
}
export function CreateChannelPageLayout({defaultValues, children}: Props) {
const form = useForm<UpdateChannelPayload>({
defaultValues: {
content: EMPTY_PAGINATION_RESPONSE.pagination,
config: {
contentType: 'listAll',
contentOrder: 'created_at:desc',
nestedLayout: 'carousel',
...defaultValues,
},
},
});
const createChannel = useCreateChannel(form);
return (
<CrupdateResourceLayout
form={form}
onSubmit={values => {
createChannel.mutate(values);
}}
title={<Trans message="Add new channel" />}
isLoading={createChannel.isPending}
>
{children}
</CrupdateResourceLayout>
);
}

View File

@@ -0,0 +1,51 @@
import {useForm} from 'react-hook-form';
import React, {ReactNode} from 'react';
import {CrupdateResourceLayout} from '@common/admin/crupdate-resource-layout';
import {Trans} from '@common/i18n/trans';
import {PageStatus} from '@common/http/page-status';
import {useChannel} from '@common/channels/requests/use-channel';
import {Channel} from '@common/channels/channel';
import {
UpdateChannelPayload,
useUpdateChannel,
} from '@common/admin/channels/requests/use-update-channel';
interface Props {
children: ReactNode;
}
export function EditChannelPageLayout({children}: Props) {
const query = useChannel(undefined, 'editChannelPage');
if (query.data) {
return <PageContent channel={query.data.channel}>{children}</PageContent>;
}
return <PageStatus query={query} loaderIsScreen={false} />;
}
interface PageContentProps {
channel: Channel;
children: ReactNode;
}
function PageContent({channel, children}: PageContentProps) {
const form = useForm<UpdateChannelPayload>({
// @ts-ignore
defaultValues: {
...channel,
},
});
const updateChannel = useUpdateChannel(form);
return (
<CrupdateResourceLayout
form={form}
onSubmit={values => {
updateChannel.mutate(values);
}}
title={
<Trans message="Edit “:name“ channel" values={{name: channel.name}} />
}
isLoading={updateChannel.isPending}
>
{children}
</CrupdateResourceLayout>
);
}