mirror of
https://github.com/Arcadia-Solutions/arcadia.git
synced 2025-12-20 00:39:34 -06:00
feat: support query parameters to go to a specific page in the forum:
- `post_id` - `page`
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use arcadia_storage::models::collage::SearchCollagesQuery;
|
||||
use arcadia_storage::models::series::SearchSeriesQuery;
|
||||
use arcadia_storage::models::{collage::SearchCollagesQuery, forum::GetForumThreadPostsQuery};
|
||||
use utoipa::{
|
||||
openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
|
||||
Modify, OpenApi,
|
||||
@@ -92,7 +92,8 @@ use crate::handlers::{
|
||||
GetUserApplicationsQuery,
|
||||
SearchTorrentRequestsQuery,
|
||||
SearchCollagesQuery,
|
||||
SearchSeriesQuery
|
||||
SearchSeriesQuery,
|
||||
GetForumThreadPostsQuery
|
||||
),)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -8,13 +8,6 @@ use arcadia_storage::{models::forum::ForumThreadEnriched, redis::RedisPoolInterf
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct GetForumThreadQuery {
|
||||
pub title: String,
|
||||
pub offset: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct GetForumThreadQueryId {
|
||||
pub id: i64,
|
||||
|
||||
@@ -5,25 +5,12 @@ use actix_web::{
|
||||
};
|
||||
use arcadia_common::error::Result;
|
||||
use arcadia_storage::{
|
||||
models::{common::PaginatedResults, forum::ForumPostHierarchy},
|
||||
models::{
|
||||
common::PaginatedResults,
|
||||
forum::{ForumPostHierarchy, GetForumThreadPostsQuery},
|
||||
},
|
||||
redis::RedisPoolInterface,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct GetForumThreadQuery {
|
||||
pub title: String,
|
||||
pub offset: Option<i64>,
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams)]
|
||||
pub struct GetForumThreadPostsQuery {
|
||||
pub thread_id: i64,
|
||||
pub page: u32,
|
||||
pub page_size: u32,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -41,10 +28,7 @@ pub async fn exec<R: RedisPoolInterface + 'static>(
|
||||
) -> Result<HttpResponse> {
|
||||
//TODO: restrict access to some sub_categories based on forbidden_classes
|
||||
|
||||
let thread = arc
|
||||
.pool
|
||||
.find_forum_thread_posts(query.thread_id, query.page, query.page_size)
|
||||
.await?;
|
||||
let thread = arc.pool.find_forum_thread_posts(query.into_inner()).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(thread))
|
||||
}
|
||||
|
||||
23
backend/storage/.sqlx/query-0a8b1361933b00d93f861dee93762b059f94b0ca603ad4201a9a96143eaed957.json
generated
Normal file
23
backend/storage/.sqlx/query-0a8b1361933b00d93f861dee93762b059f94b0ca603ad4201a9a96143eaed957.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT COUNT(*)::BIGINT FROM forum_posts\n WHERE forum_thread_id = $1 AND id < $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0a8b1361933b00d93f861dee93762b059f94b0ca603ad4201a9a96143eaed957"
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::{DateTime, Local};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::prelude::FromRow;
|
||||
use utoipa::ToSchema;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use super::user::{UserLite, UserLiteAvatar};
|
||||
|
||||
@@ -160,3 +160,11 @@ pub struct ForumPostAndThreadName {
|
||||
pub sticky: bool,
|
||||
pub forum_thread_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, IntoParams, ToSchema)]
|
||||
pub struct GetForumThreadPostsQuery {
|
||||
pub thread_id: i64,
|
||||
pub page: Option<u32>,
|
||||
pub page_size: u32,
|
||||
pub post_id: Option<i64>,
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use crate::{
|
||||
common::PaginatedResults,
|
||||
forum::{
|
||||
ForumPost, ForumPostAndThreadName, ForumPostHierarchy, ForumThread,
|
||||
ForumThreadEnriched, UserCreatedForumPost, UserCreatedForumThread,
|
||||
ForumThreadEnriched, GetForumThreadPostsQuery, UserCreatedForumPost,
|
||||
UserCreatedForumThread,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -305,11 +306,30 @@ impl ConnectionPool {
|
||||
|
||||
pub async fn find_forum_thread_posts(
|
||||
&self,
|
||||
forum_thread_id: i64,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
form: GetForumThreadPostsQuery,
|
||||
) -> Result<PaginatedResults<ForumPostHierarchy>> {
|
||||
let offset = (page - 1) * page_size;
|
||||
let page_size = form.page_size as i64;
|
||||
let mut current_page = form.page.unwrap_or(1);
|
||||
|
||||
let offset = if let Some(post_id) = form.post_id {
|
||||
let position = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*)::BIGINT FROM forum_posts
|
||||
WHERE forum_thread_id = $1 AND id < $2
|
||||
"#,
|
||||
form.thread_id,
|
||||
post_id
|
||||
)
|
||||
.fetch_one(self.borrow())
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
// i64 ceil division is unstable as of now
|
||||
current_page = ((position + 1) as u64).div_ceil(form.page_size as u64) as u32;
|
||||
((position / page_size) * page_size) as i64
|
||||
} else {
|
||||
((form.page.unwrap_or(1) - 1) as i64) * page_size
|
||||
};
|
||||
|
||||
let forum_thread_data = sqlx::query!(
|
||||
r#"
|
||||
@@ -354,9 +374,9 @@ impl ConnectionPool {
|
||||
LIMIT $3
|
||||
) p;
|
||||
"#,
|
||||
forum_thread_id,
|
||||
offset as i64,
|
||||
page_size as i64
|
||||
form.thread_id,
|
||||
offset,
|
||||
page_size
|
||||
)
|
||||
.fetch_one(self.borrow())
|
||||
.await
|
||||
@@ -372,8 +392,8 @@ impl ConnectionPool {
|
||||
|
||||
let paginated_results = PaginatedResults {
|
||||
results: posts,
|
||||
page,
|
||||
page_size,
|
||||
page: current_page,
|
||||
page_size: form.page_size,
|
||||
total_items: forum_thread_data.total_items.unwrap_or(0),
|
||||
};
|
||||
|
||||
|
||||
17
frontend/src/api-schema/schema.d.ts
vendored
17
frontend/src/api-schema/schema.d.ts
vendored
@@ -315,7 +315,7 @@ export interface paths {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["Get forum thread"];
|
||||
get: operations["Get forum thread's posts"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
@@ -1415,6 +1415,16 @@ export interface components {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
GetForumThreadPostsQuery: {
|
||||
/** Format: int32 */
|
||||
page?: number | null;
|
||||
/** Format: int32 */
|
||||
page_size: number;
|
||||
/** Format: int64 */
|
||||
post_id?: number | null;
|
||||
/** Format: int64 */
|
||||
thread_id: number;
|
||||
};
|
||||
GetUserApplicationsQuery: {
|
||||
/** Format: int64 */
|
||||
limit?: number | null;
|
||||
@@ -3163,12 +3173,13 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
"Get forum thread": {
|
||||
"Get forum thread's posts": {
|
||||
parameters: {
|
||||
query: {
|
||||
thread_id: number;
|
||||
page: number;
|
||||
page?: number | null;
|
||||
page_size: number;
|
||||
post_id?: number | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -23,10 +23,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps, ref } from 'vue'
|
||||
import PaginationSelector from './PaginationSelector.vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps<{
|
||||
totalItems: number
|
||||
pageSize: number
|
||||
initialPage?: number | null
|
||||
totalPages: number
|
||||
}>()
|
||||
|
||||
const currentPage = ref(1)
|
||||
@@ -40,10 +46,8 @@ const emit = defineEmits<{
|
||||
changePage: [Pagination]
|
||||
}>()
|
||||
|
||||
const totalPages = computed(() => Math.ceil(props.totalItems / props.pageSize))
|
||||
|
||||
const pageRanges = computed(() => {
|
||||
const total = totalPages.value
|
||||
const total = props.totalPages
|
||||
const maxVisible = 15
|
||||
let startPage = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
|
||||
let endPage = startPage + maxVisible - 1
|
||||
@@ -62,10 +66,24 @@ const pageRanges = computed(() => {
|
||||
})
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
if (page < 1 || page > totalPages.value) return
|
||||
if (page < 1 || page > props.totalPages) return
|
||||
currentPage.value = page
|
||||
router.push({
|
||||
// path: route.path,
|
||||
query: { page: page },
|
||||
})
|
||||
emit('changePage', { page, pageSize: props.pageSize })
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
goToPage,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.initialPage) {
|
||||
currentPage.value = props.initialPage
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.pagination {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<template>
|
||||
<ContentContainer class="comment-container">
|
||||
<ContentContainer class="comment-container" :id="`post-${comment.id}`">
|
||||
<div style="float: right">
|
||||
<RouterLink
|
||||
:to="{
|
||||
query: { post_id: comment.id },
|
||||
hash: `#post-${comment.id}`,
|
||||
}"
|
||||
>
|
||||
<i class="pi pi-link" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="comment">
|
||||
<div class="user">
|
||||
<img class="avatar" :src="comment.created_by.avatar ?? '/default_user_avatar.jpg'" :alt="comment.created_by.username + '\'s avatar'" />
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
"new_post": "New post",
|
||||
"new_thread": "New thread",
|
||||
"thread_name": "Thread name",
|
||||
"discuss": "Discuss"
|
||||
"discuss": "Discuss",
|
||||
"go_to_last_page_to_reply": "Go to last page to reply"
|
||||
},
|
||||
"user": {
|
||||
"username": "Username",
|
||||
|
||||
@@ -31,8 +31,16 @@ export const getForumThread = async (forumThreadId: number): Promise<ForumThread
|
||||
|
||||
export type PaginatedResults_ForumPostHierarchy = components['schemas']['PaginatedResults_ForumPostHierarchy']
|
||||
|
||||
export const getForumThreadPosts = async (forumThreadId: number, page: number, page_size: number): Promise<PaginatedResults_ForumPostHierarchy> => {
|
||||
return (await api.get<PaginatedResults_ForumPostHierarchy>(`/forum/thread/posts?thread_id=${forumThreadId}&page=${page}&page_size=${page_size}`)).data
|
||||
export type GetForumThreadPostsQuery = components['schemas']['GetForumThreadPostsQuery']
|
||||
|
||||
export const getForumThreadPosts = async (query: GetForumThreadPostsQuery): Promise<PaginatedResults_ForumPostHierarchy> => {
|
||||
return (
|
||||
await api.get<PaginatedResults_ForumPostHierarchy>(
|
||||
`/forum/thread/posts?thread_id=${query.thread_id}&page_size=${query.page_size}` +
|
||||
(query.page !== null ? `&page=${query.page}` : '') +
|
||||
(query.post_id !== null ? `&post_id=${query.post_id}` : ''),
|
||||
)
|
||||
).data
|
||||
}
|
||||
|
||||
export type UserCreatedForumPost = components['schemas']['UserCreatedForumPost']
|
||||
|
||||
@@ -356,3 +356,13 @@ export const isAttributeUsed = (attribute: keyof Torrent | keyof TorrentRequest,
|
||||
return true
|
||||
}
|
||||
}
|
||||
export const scrollToHash = () => {
|
||||
;(function h() {
|
||||
const e = document.querySelector(location.hash)
|
||||
if (e) {
|
||||
e.scrollIntoView({ behavior: 'smooth' })
|
||||
} else {
|
||||
setTimeout(h, 100)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
<RouterLink :to="`/forum/sub-category/${forumThread.forum_sub_category_id}`">{{ forumThread.forum_sub_category_name }}</RouterLink> >
|
||||
{{ forumThread.name }}
|
||||
</div>
|
||||
<PaginatedResults :totalItems="totalPosts" @change-page="fetchForumThreadPosts($event.page, $event.pageSize)" :page-size="pageSize">
|
||||
<PaginatedResults
|
||||
v-if="forumThreadPosts.length > 0"
|
||||
:totalPages
|
||||
:initialPage
|
||||
:totalItems="totalPosts"
|
||||
@change-page="fetchForumThreadPosts($event.page, $event.pageSize, null)"
|
||||
:page-size="pageSize"
|
||||
>
|
||||
<GeneralComment v-for="post in forumThreadPosts" :key="post.id" :comment="post" />
|
||||
</PaginatedResults>
|
||||
<Form v-slot="$form" :initialValues="newPost" :resolver @submit="onFormSubmit" validateOnSubmit :validateOnValueUpdate="false">
|
||||
@@ -17,7 +24,15 @@
|
||||
:label="t('forum.new_post')"
|
||||
>
|
||||
<template #buttons>
|
||||
<Button type="submit" label="Post" icon="pi pi-send" :loading="sendingPost" class="post-button" />
|
||||
<Button
|
||||
type="submit"
|
||||
label="Post"
|
||||
icon="pi pi-send"
|
||||
:loading="sendingPost"
|
||||
class="post-button"
|
||||
:disabled="currentPage !== totalPages"
|
||||
v-tooltip.top="currentPage !== totalPages ? t('forum.go_to_last_page_to_reply') : ''"
|
||||
/>
|
||||
</template>
|
||||
</BBCodeEditor>
|
||||
<Message v-if="$form.content?.invalid" severity="error" size="small" variant="simple">
|
||||
@@ -48,6 +63,9 @@ import { Form } from '@primevue/forms'
|
||||
import { Button } from 'primevue'
|
||||
import BBCodeEditor from '@/components/community/BBCodeEditor.vue'
|
||||
import PaginatedResults from '@/components/PaginatedResults.vue'
|
||||
import { nextTick } from 'vue'
|
||||
import { scrollToHash } from '@/services/helpers'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
@@ -56,6 +74,9 @@ const forumThread = ref<null | ForumThreadEnriched>(null)
|
||||
const forumThreadPosts = ref<ForumPostHierarchy[]>([])
|
||||
const totalPosts = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const totalPages = computed(() => Math.ceil(totalPosts.value / pageSize.value))
|
||||
const currentPage = ref(1)
|
||||
let initialPage: number | null = null
|
||||
const newPost = ref<UserCreatedForumPost>({
|
||||
content: '',
|
||||
forum_thread_id: 0,
|
||||
@@ -64,14 +85,35 @@ const sendingPost = ref(false)
|
||||
const bbcodeEditorEmptyInput = ref(false)
|
||||
const siteName = import.meta.env.VITE_SITE_NAME
|
||||
|
||||
const fetchForumThreadPosts = async (page: number, page_size: number) => {
|
||||
const paginatedPosts = await getForumThreadPosts(parseInt(route.params.id as string), page, page_size)
|
||||
const fetchForumThreadPosts = async (page: number | null, page_size: number, post_id: number | null) => {
|
||||
const paginatedPosts = await getForumThreadPosts({
|
||||
thread_id: parseInt(route.params.id as string),
|
||||
page,
|
||||
page_size,
|
||||
post_id,
|
||||
})
|
||||
forumThreadPosts.value = paginatedPosts.results
|
||||
totalPosts.value = paginatedPosts.total_items
|
||||
await nextTick()
|
||||
if (post_id !== null) {
|
||||
initialPage = paginatedPosts.page
|
||||
scrollToHash()
|
||||
}
|
||||
currentPage.value = paginatedPosts.page
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
;[forumThread.value] = await Promise.all([getForumThread(+route.params.id!), fetchForumThreadPosts(1, pageSize.value)])
|
||||
let page: number | null = 1
|
||||
if (route.query.page) {
|
||||
page = parseInt(route.query.page as string)
|
||||
initialPage = page
|
||||
} else if (route.query.post_id) {
|
||||
page = null
|
||||
}
|
||||
;[forumThread.value] = await Promise.all([
|
||||
getForumThread(+route.params.id!),
|
||||
fetchForumThreadPosts(page, pageSize.value, route.query.post_id ? parseInt(route.query.post_id as string) : null),
|
||||
])
|
||||
|
||||
document.title = forumThread.value ? `${forumThread.value.name} - ${siteName}` : `Forum thread - ${siteName}`
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user