feat: add unresolve staff PM functionality (#426)

Closes #356
This commit is contained in:
NathanJ60
2025-12-10 18:36:44 +01:00
committed by GitHub
parent 13ef6057e2
commit 5ed51cf826
9 changed files with 202 additions and 145 deletions

View File

@@ -103,6 +103,7 @@ use crate::handlers::user_applications::get_user_applications::GetUserApplicatio
crate::handlers::staff_pms::get_staff_pm::exec,
crate::handlers::staff_pms::list_staff_pms::exec,
crate::handlers::staff_pms::resolve_staff_pm::exec,
crate::handlers::staff_pms::unresolve_staff_pm::exec,
crate::handlers::collages::create_collage::exec,
crate::handlers::collages::create_collage_entries::exec,
crate::handlers::collages::get_collage::exec,

View File

@@ -3,6 +3,7 @@ pub mod create_staff_pm_message;
pub mod get_staff_pm;
pub mod list_staff_pms;
pub mod resolve_staff_pm;
pub mod unresolve_staff_pm;
use actix_web::web::{get, post, put, resource, ServiceConfig};
use arcadia_storage::redis::RedisPoolInterface;
@@ -16,4 +17,5 @@ pub fn config<R: RedisPoolInterface + 'static>(cfg: &mut ServiceConfig) {
cfg.service(resource("/messages").route(post().to(self::create_staff_pm_message::exec::<R>)));
cfg.service(resource("/{id}").route(get().to(self::get_staff_pm::exec::<R>)));
cfg.service(resource("/{id}/resolve").route(put().to(self::resolve_staff_pm::exec::<R>)));
cfg.service(resource("/{id}/unresolve").route(put().to(self::unresolve_staff_pm::exec::<R>)));
}

View File

@@ -0,0 +1,31 @@
use crate::{middlewares::auth_middleware::Authdata, Arcadia};
use actix_web::{
web::{Data, Path},
HttpResponse,
};
use arcadia_common::error::{Error, Result};
use arcadia_storage::{models::user::UserClass, redis::RedisPoolInterface};
#[utoipa::path(
put,
operation_id = "Unresolve staff PM",
tag = "StaffPM",
path = "/api/staff-pms/{id}/unresolve",
params(("id" = i64, Path, description = "Staff PM id")),
security(("http" = ["Bearer"])) ,
responses((status = 200, description = "Unresolved staff PM", body = arcadia_storage::models::staff_pm::StaffPm))
)]
pub async fn exec<R: RedisPoolInterface + 'static>(
arc: Data<Arcadia<R>>,
user: Authdata,
id: Path<i64>,
) -> Result<HttpResponse> {
if user.class != UserClass::Staff {
return Err(Error::InsufficientPrivileges);
}
let updated = arc
.pool
.unresolve_staff_pm(id.into_inner(), user.sub)
.await?;
Ok(HttpResponse::Ok().json(updated))
}

View File

@@ -1,144 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT title_group_id AS \"id!\", title_group_name AS \"name!\", title_group_covers AS \"covers!\",\n title_group_category AS \"category!: _\", title_group_content_type AS \"content_type!: _\", title_group_tag_names AS \"tags!\",\n title_group_original_release_date AS \"original_release_date!\", title_group_platform AS \"platform!: _\",\n '[]'::jsonb AS \"edition_groups!: _\",\n '[]'::jsonb AS \"affiliated_artists!: _\"\n\n FROM title_group_hierarchy_lite tgh\n\n WHERE ($4::BOOLEAN IS NULL OR tgh.torrent_staff_checked = $4)\n AND ($5::BOOLEAN IS NULL OR tgh.torrent_reported = $5)\n AND (\n $7::INT IS NULL OR\n -- don't return torrents created as anonymous\n -- unless the requesting user is the uploader\n (tgh.torrent_created_by_id = $7 AND (\n tgh.torrent_created_by_id = $8 OR\n NOT tgh.torrent_uploaded_as_anonymous)\n )\n )\n AND (\n $9::BIGINT IS NULL OR\n EXISTS (SELECT 1 FROM affiliated_artists aa WHERE aa.title_group_id = tgh.title_group_id AND aa.artist_id = $9)\n )\n -- name filter (partial match) or external link match or series name match\n AND (\n $10::TEXT IS NULL OR\n tgh.title_group_name ILIKE '%' || $10 || '%' ESCAPE '\\' OR\n tgh.title_group_series_name ILIKE '%' || $10 || '%' ESCAPE '\\'\n )\n AND ($11::TEXT IS NULL OR $11 = ANY(tgh.title_group_external_links))\n AND ($12::BOOLEAN IS TRUE OR tgh.torrent_id IS NOT NULL)\n\n GROUP BY title_group_id, title_group_name, title_group_covers, title_group_category,\n title_group_content_type, title_group_tag_names, title_group_original_release_date, title_group_platform\n\n ORDER BY\n CASE WHEN $1 = 'title_group_original_release_date' AND $6 = 'asc' THEN title_group_original_release_date END ASC,\n CASE WHEN $1 = 'title_group_original_release_date' AND $6 = 'desc' THEN title_group_original_release_date END DESC,\n CASE WHEN $1 = 'torrent_size' AND $6 = 'asc' THEN MAX(torrent_size) END ASC,\n CASE WHEN $1 = 'torrent_size' AND $6 = 'desc' THEN MAX(torrent_size) END DESC,\n CASE WHEN $1 = 'torrent_created_at' AND $6 = 'asc' THEN MAX(torrent_created_at) END ASC,\n CASE WHEN $1 = 'torrent_created_at' AND $6 = 'desc' THEN MAX(torrent_created_at) END DESC,\n title_group_original_release_date ASC\n\n LIMIT $2 OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "name!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "covers!",
"type_info": "TextArray"
},
{
"ordinal": 3,
"name": "category!: _",
"type_info": {
"Custom": {
"name": "title_group_category_enum",
"kind": {
"Enum": [
"Ep",
"Album",
"Single",
"Soundtrack",
"Anthology",
"Compilation",
"Remix",
"Bootleg",
"Mixtape",
"ConcertRecording",
"DjMix",
"FeatureFilm",
"ShortFilm",
"Game",
"Program",
"Illustrated",
"Periodical",
"Book",
"Article",
"Manual",
"Other"
]
}
}
}
},
{
"ordinal": 4,
"name": "content_type!: _",
"type_info": {
"Custom": {
"name": "content_type_enum",
"kind": {
"Enum": [
"movie",
"video",
"tv_show",
"music",
"podcast",
"software",
"book",
"collection"
]
}
}
}
},
{
"ordinal": 5,
"name": "tags!",
"type_info": "VarcharArray"
},
{
"ordinal": 6,
"name": "original_release_date!",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "platform!: _",
"type_info": {
"Custom": {
"name": "platform_enum",
"kind": {
"Enum": [
"Linux",
"MacOS",
"Windows",
"Xbox"
]
}
}
}
},
{
"ordinal": 8,
"name": "edition_groups!: _",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "affiliated_artists!: _",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8",
"Bool",
"Bool",
"Text",
"Int4",
"Int4",
"Int8",
"Text",
"Text",
"Bool"
]
},
"nullable": [
true,
true,
true,
true,
true,
true,
true,
true,
null,
null
]
},
"hash": "3a104da12b9d05a2af0eff0085dbbecdee013c49ba2985d63910b036831b718e"
}

View File

@@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\t\tUPDATE staff_pms\n\t\t\t\tSET resolved = FALSE\n\t\t\t\tWHERE id = $1\n\t\t\t\tRETURNING *\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "subject",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_by_id",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "resolved",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "f4438a4333a4291efeb8634199872c0a4679483ba059d07bc6cfbc61389eac36"
}

View File

@@ -78,6 +78,28 @@ impl ConnectionPool {
Ok(updated)
}
pub async fn unresolve_staff_pm(
&self,
staff_pm_id: i64,
_current_user_id: i32,
) -> Result<StaffPm> {
let updated = sqlx::query_as!(
StaffPm,
r#"
UPDATE staff_pms
SET resolved = FALSE
WHERE id = $1
RETURNING *
"#,
staff_pm_id,
)
.fetch_one(self.borrow())
.await
.map_err(Error::CouldNotCreateConversation)?;
Ok(updated)
}
pub async fn list_staff_pms(&self, current_user_id: i32, is_staff: bool) -> Result<Value> {
let row = sqlx::query_unchecked!(
r#"

View File

@@ -356,6 +356,8 @@
"resolve": "Resolve",
"resolved": "Resolved",
"resolved_successfully": "Staff PM resolved!",
"unresolve": "Unresolve",
"unresolved_successfully": "Staff PM unresolved!",
"new": "New staff PM",
"subject": "Subject",
"message": "Message | Messages"

View File

@@ -6631,6 +6631,43 @@ export const StaffPMApiAxiosParamCreator = function (configuration?: Configurati
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {number} id Staff PM id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unresolveStaffPM: async (id: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('unresolveStaffPM', 'id', id)
const localVarPath = `/api/staff-pms/{id}/unresolve`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication http required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -6708,6 +6745,18 @@ export const StaffPMApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['StaffPMApi.resolveStaffPM']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {number} id Staff PM id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async unresolveStaffPM(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StaffPm>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.unresolveStaffPM(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['StaffPMApi.unresolveStaffPM']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
}
};
@@ -6761,6 +6810,15 @@ export const StaffPMApiFactory = function (configuration?: Configuration, basePa
resolveStaffPM(id: number, options?: RawAxiosRequestConfig): AxiosPromise<StaffPm> {
return localVarFp.resolveStaffPM(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} id Staff PM id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unresolveStaffPM(id: number, options?: RawAxiosRequestConfig): AxiosPromise<StaffPm> {
return localVarFp.unresolveStaffPM(id, options).then((request) => request(axios, basePath));
},
};
};
@@ -6816,6 +6874,16 @@ export class StaffPMApi extends BaseAPI {
public resolveStaffPM(id: number, options?: RawAxiosRequestConfig) {
return StaffPMApiFp(this.configuration).resolveStaffPM(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {number} id Staff PM id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public unresolveStaffPM(id: number, options?: RawAxiosRequestConfig) {
return StaffPMApiFp(this.configuration).unresolveStaffPM(id, options).then((request) => request(this.axios, this.basePath));
}
}
@@ -6853,6 +6921,12 @@ export const resolveStaffPM = async (id: number, options?: RawAxiosRequestConfig
};
export const unresolveStaffPM = async (id: number, options?: RawAxiosRequestConfig): Promise<StaffPm> => {
const response = await staffPMApi.unresolveStaffPM(id, options);
return response.data;
};
/**
* SubscriptionApi - axios parameter creator
*/

View File

@@ -11,6 +11,7 @@
>
<template #buttons>
<Button v-if="!staffPm.resolved" :label="t('staff_pm.resolve')" icon="pi pi-check" :loading="resolvingPm" @click="resolvePm" />
<Button v-else :label="t('staff_pm.unresolve')" icon="pi pi-replay" :loading="unresolvingPm" @click="unresolvePm" />
<Button type="submit" :label="t('general.send')" icon="pi pi-send" :loading="sendingMessage" />
</template>
</BBCodeEditor>
@@ -29,7 +30,14 @@ import { Button } from 'primevue'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { showToast } from '@/main'
import { createStaffPMMessage, getStaffPM, resolveStaffPM, type StaffPmHierarchy, type UserCreatedStaffPmMessage } from '@/services/api-schema'
import {
createStaffPMMessage,
getStaffPM,
resolveStaffPM,
unresolveStaffPM,
type StaffPmHierarchy,
type UserCreatedStaffPmMessage,
} from '@/services/api-schema'
const route = useRoute()
const { t } = useI18n()
@@ -38,6 +46,7 @@ const userStore = useUserStore()
const staffPm = ref<StaffPmHierarchy>()
const sendingMessage = ref(false)
const resolvingPm = ref(false)
const unresolvingPm = ref(false)
const newMessage = ref<UserCreatedStaffPmMessage>({
content: '',
staff_pm_id: 0,
@@ -66,6 +75,20 @@ const resolvePm = async () => {
})
}
const unresolvePm = async () => {
unresolvingPm.value = true
unresolveStaffPM(parseInt(route.params.id as string))
.then(() => {
if (staffPm.value) {
staffPm.value.resolved = false
}
showToast('', t('staff_pm.unresolved_successfully'), 'success', 3000, true, 'tr')
})
.finally(() => {
unresolvingPm.value = false
})
}
const sendMessage = async () => {
sendingMessage.value = true
newMessage.value.staff_pm_id = parseInt(route.params.id as string)