50
common/resources/client/channels/channel.ts
Executable file
50
common/resources/client/channels/channel.ts
Executable file
@@ -0,0 +1,50 @@
|
||||
import {PaginationResponse} from '@common/http/backend-response/pagination-response';
|
||||
import {User} from '@common/auth/user';
|
||||
|
||||
export const CHANNEL_MODEL = 'channel';
|
||||
|
||||
export type ChannelContentItem<T = unknown> = T & {
|
||||
channelable_id?: number;
|
||||
channelable_order?: number;
|
||||
};
|
||||
|
||||
export interface ChannelConfig {
|
||||
autoUpdateMethod?: string;
|
||||
autoUpdateProvider?: string;
|
||||
disablePagination?: boolean;
|
||||
disablePlayback?: boolean;
|
||||
restriction?: string;
|
||||
restrictionModelId?: 'urlParam' | number;
|
||||
contentModel: string;
|
||||
contentType: 'listAll' | 'manual' | 'autoUpdate';
|
||||
contentOrder: string;
|
||||
// layout user selected manually, it's stored in a cookie and set as this
|
||||
// prop in channel controller so there are no mismatches during SSR
|
||||
selectedLayout?: string;
|
||||
layout: string;
|
||||
nestedLayout: string;
|
||||
hideTitle?: boolean;
|
||||
lockSlug?: boolean;
|
||||
preventDeletion?: boolean;
|
||||
actions?: {tooltip: string; icon: string; route: string}[];
|
||||
adminDescription?: string;
|
||||
paginationType?: 'infiniteScroll' | 'lengthAware' | 'simple';
|
||||
}
|
||||
|
||||
export interface Channel<T = ChannelContentItem> {
|
||||
id: number;
|
||||
name: string;
|
||||
internal: boolean;
|
||||
public: boolean;
|
||||
description?: string;
|
||||
type: string;
|
||||
slug: string;
|
||||
config: ChannelConfig;
|
||||
items?: T[];
|
||||
model_type: 'channel';
|
||||
items_count?: number;
|
||||
user?: User;
|
||||
updated_at?: string;
|
||||
restriction?: {id: number; name: string; model_type: string};
|
||||
content?: PaginationResponse<T>;
|
||||
}
|
||||
69
common/resources/client/channels/requests/use-channel-content.ts
Executable file
69
common/resources/client/channels/requests/use-channel-content.ts
Executable file
@@ -0,0 +1,69 @@
|
||||
import {hashKey, keepPreviousData, useQuery} from '@tanstack/react-query';
|
||||
import {apiClient} from '@common/http/query-client';
|
||||
import {Channel, ChannelContentItem} from '@common/channels/channel';
|
||||
import {
|
||||
channelEndpoint,
|
||||
channelQueryKey,
|
||||
} from '@common/channels/requests/use-channel';
|
||||
import {PaginatedBackendResponse} from '@common/http/backend-response/pagination-response';
|
||||
import {useRef} from 'react';
|
||||
import {useChannelQueryParams} from '@common/channels/use-channel-query-params';
|
||||
import {useSearchParams} from 'react-router-dom';
|
||||
|
||||
interface Response<T extends ChannelContentItem = ChannelContentItem>
|
||||
extends PaginatedBackendResponse<T> {}
|
||||
|
||||
interface Options {
|
||||
paginate?: boolean;
|
||||
}
|
||||
|
||||
export function useChannelContent<
|
||||
T extends ChannelContentItem = ChannelContentItem,
|
||||
>(
|
||||
channel: Channel<T>,
|
||||
params?: Record<string, string> | null,
|
||||
options?: Options,
|
||||
) {
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryParams = useChannelQueryParams(channel, params);
|
||||
if (options?.paginate) {
|
||||
queryParams.page = searchParams.get('page') || '1';
|
||||
}
|
||||
const queryKey = channelQueryKey(channel.id, queryParams);
|
||||
const initialQueryKey = useRef(hashKey(queryKey)).current;
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: channelQueryKey(channel.id, queryParams),
|
||||
queryFn: () => fetchChannelContent<T>(channel, queryParams),
|
||||
placeholderData: keepPreviousData,
|
||||
initialData: () => {
|
||||
if (hashKey(queryKey) === initialQueryKey) {
|
||||
return channel.content;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...query,
|
||||
queryKey,
|
||||
};
|
||||
}
|
||||
|
||||
function fetchChannelContent<T extends ChannelContentItem = ChannelContentItem>(
|
||||
channel: Channel<T>,
|
||||
params: any,
|
||||
) {
|
||||
return apiClient
|
||||
.get<Response<T>>(channelEndpoint(channel.id), {
|
||||
params: {
|
||||
...params,
|
||||
paginate:
|
||||
channel.config.paginationType === 'lengthAware'
|
||||
? 'lengthAware'
|
||||
: 'simple',
|
||||
returnContentOnly: 'true',
|
||||
},
|
||||
})
|
||||
.then(response => response.data.pagination);
|
||||
}
|
||||
68
common/resources/client/channels/requests/use-channel.ts
Executable file
68
common/resources/client/channels/requests/use-channel.ts
Executable file
@@ -0,0 +1,68 @@
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import {apiClient} from '@common/http/query-client';
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {useParams} from 'react-router-dom';
|
||||
import {Channel} from '@common/channels/channel';
|
||||
import {useChannelQueryParams} from '@common/channels/use-channel-query-params';
|
||||
import {isSsr} from '@common/utils/dom/is-ssr';
|
||||
import {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';
|
||||
|
||||
export interface GetChannelResponse extends BackendResponse {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
export function useChannel(
|
||||
slugOrId: string | number | undefined,
|
||||
loader: 'channelPage' | 'editChannelPage' | 'editUserListPage',
|
||||
userParams?: Record<string, string | null>,
|
||||
) {
|
||||
const params = useParams();
|
||||
const channelId = slugOrId || params.slugOrId!;
|
||||
const queryParams = useChannelQueryParams(undefined, userParams);
|
||||
return useQuery({
|
||||
// only refetch when channel ID or restriction changes and not query params.
|
||||
// content will be re-fetched in channel content components
|
||||
// on SSR use query params as well, to avoid caching wrong data when query params change
|
||||
queryKey: isSsr()
|
||||
? channelQueryKey(channelId, queryParams)
|
||||
: channelQueryKey(channelId, {restriction: queryParams.restriction}),
|
||||
|
||||
queryFn: () => fetchChannel(channelId, {...queryParams, loader}),
|
||||
initialData: () => {
|
||||
// @ts-ignore
|
||||
const data = getBootstrapData().loaders?.[loader];
|
||||
const isSameChannel =
|
||||
data?.channel.id == channelId || data?.channel.slug == channelId;
|
||||
const isSameRestriction =
|
||||
!queryParams.restriction ||
|
||||
data?.channel.restriction?.name === queryParams.restriction;
|
||||
if (isSameChannel && isSameRestriction) {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function channelQueryKey(
|
||||
slugOrId: number | string,
|
||||
params?: Record<string, string | number | null>,
|
||||
) {
|
||||
const key: any[] = ['channel', `${slugOrId}`];
|
||||
if (params) {
|
||||
key.push(params);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
export function channelEndpoint(slugOrId: number | string) {
|
||||
return `channel/${slugOrId}`;
|
||||
}
|
||||
|
||||
function fetchChannel(
|
||||
slugOrId: number | string,
|
||||
params: Record<string, string | number | undefined | null> = {},
|
||||
): Promise<GetChannelResponse> {
|
||||
return apiClient
|
||||
.get(channelEndpoint(slugOrId), {params})
|
||||
.then(response => response.data);
|
||||
}
|
||||
23
common/resources/client/channels/requests/use-infinite-channel-content.ts
Executable file
23
common/resources/client/channels/requests/use-infinite-channel-content.ts
Executable file
@@ -0,0 +1,23 @@
|
||||
import {useInfiniteData} from '@common/ui/infinite-scroll/use-infinite-data';
|
||||
import {Channel, ChannelContentItem} from '@common/channels/channel';
|
||||
import {
|
||||
channelEndpoint,
|
||||
channelQueryKey,
|
||||
} from '@common/channels/requests/use-channel';
|
||||
import {useChannelQueryParams} from '@common/channels/use-channel-query-params';
|
||||
|
||||
export function useInfiniteChannelContent<
|
||||
T extends ChannelContentItem = ChannelContentItem,
|
||||
>(channel: Channel<T>) {
|
||||
const queryParams = useChannelQueryParams(channel);
|
||||
return useInfiniteData<T>({
|
||||
willSortOrFilter: true,
|
||||
initialPage: channel.content,
|
||||
queryKey: channelQueryKey(channel.id),
|
||||
endpoint: channelEndpoint(channel.id),
|
||||
queryParams: {
|
||||
returnContentOnly: 'true',
|
||||
...queryParams,
|
||||
},
|
||||
});
|
||||
}
|
||||
27
common/resources/client/channels/use-channel-query-params.ts
Executable file
27
common/resources/client/channels/use-channel-query-params.ts
Executable file
@@ -0,0 +1,27 @@
|
||||
import {Channel} from '@common/channels/channel';
|
||||
import {useParams, useSearchParams} from 'react-router-dom';
|
||||
import {useBackendFilterUrlParams} from '@common/datatable/filters/backend-filter-url-params';
|
||||
import {BackendFiltersUrlKey} from '@common/datatable/filters/backend-filters-url-key';
|
||||
|
||||
export function useChannelQueryParams(
|
||||
channel?: Channel,
|
||||
userParams?: Record<string, string | null> | null,
|
||||
): Record<string, string | number | null> {
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const {encodedFilters} = useBackendFilterUrlParams();
|
||||
|
||||
const queryParams = {
|
||||
...userParams,
|
||||
restriction: params.restriction || '',
|
||||
order: searchParams.get('order'),
|
||||
[BackendFiltersUrlKey]: encodedFilters,
|
||||
};
|
||||
|
||||
// always set default channel order to keep query key stable
|
||||
if (!queryParams.order && channel) {
|
||||
queryParams.order = channel.config.contentOrder || 'popularity:desc';
|
||||
}
|
||||
|
||||
return queryParams;
|
||||
}
|
||||
Reference in New Issue
Block a user