feat: edit artist on frontend, resolves #363

This commit is contained in:
FrenchGithubUser
2025-11-29 15:54:16 +01:00
parent aea3b2ebaf
commit cb7e175381
11 changed files with 202 additions and 12 deletions

View File

@@ -34,6 +34,7 @@ use crate::handlers::{
crate::handlers::home::get_home::exec,
crate::handlers::artists::get_artist_publications::exec,
crate::handlers::artists::create_artists::exec,
crate::handlers::artists::edit_artist::exec,
crate::handlers::affiliated_artists::create_affiliated_artists::exec,
crate::handlers::affiliated_artists::remove_affiliated_artists::exec,
crate::handlers::torrents::download_dottorrent_file::exec,

View File

@@ -3,7 +3,7 @@ use actix_web::{web::Data, web::Json, HttpResponse};
use arcadia_common::error::{Error, Result};
use arcadia_storage::models::artist::Artist;
use arcadia_storage::models::user::UserClass;
use arcadia_storage::{models::artist::UserEditedArtist, redis::RedisPoolInterface};
use arcadia_storage::{models::artist::EditedArtist, redis::RedisPoolInterface};
const GRACE_PERIOD_IN_DAYS: i64 = 7;
@@ -20,7 +20,7 @@ const GRACE_PERIOD_IN_DAYS: i64 = 7;
)
)]
pub async fn exec<R: RedisPoolInterface + 'static>(
form: Json<UserEditedArtist>,
form: Json<EditedArtist>,
arc: Data<Arcadia<R>>,
user: Authdata,
) -> Result<HttpResponse> {

View File

@@ -5,7 +5,7 @@ use crate::common::TestUser;
use actix_web::http::StatusCode;
use actix_web::test;
use arcadia_storage::connection_pool::ConnectionPool;
use arcadia_storage::models::artist::{Artist, UserEditedArtist};
use arcadia_storage::models::artist::{Artist, EditedArtist};
use common::auth_header;
use common::create_test_app_and_login;
use mocks::mock_redis::MockRedisPool;
@@ -21,7 +21,7 @@ async fn test_staff_can_edit_artist(pool: PgPool) {
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), 100, 100, TestUser::Staff).await;
let req_body = UserEditedArtist{
let req_body = EditedArtist{
id: 1,
name: "Beatles, The".into(),
description: "They are actually called 'The Beatles', but we decided to be weird with articles.".into(),

View File

@@ -36,7 +36,7 @@ pub struct UserCreatedArtist {
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct UserEditedArtist {
pub struct EditedArtist {
pub id: i64,
pub name: String,
pub description: String,

View File

@@ -1,4 +1,4 @@
use crate::models::artist::UserEditedArtist;
use crate::models::artist::EditedArtist;
use crate::models::common::OrderByDirection;
use crate::{
connection_pool::ConnectionPool,
@@ -207,7 +207,7 @@ impl ConnectionPool {
.map_err(Error::CouldNotFindArtist)
}
pub async fn update_artist_data(&self, updated_artist: &UserEditedArtist) -> Result<Artist> {
pub async fn update_artist_data(&self, updated_artist: &EditedArtist) -> Result<Artist> {
sqlx::query_as!(
Artist,
r#"

View File

@@ -28,7 +28,7 @@ export interface paths {
cookie?: never;
};
get: operations["Get artist publications"];
put?: never;
put: operations["Edit artist"];
post: operations["Create artist"];
delete?: never;
options?: never;
@@ -1255,6 +1255,13 @@ export interface components {
/** Format: int32 */
id: number;
};
EditedArtist: {
description: string;
/** Format: int64 */
id: number;
name: string;
pictures: string[];
};
EditedTitleGroup: {
category?: null | components["schemas"]["TitleGroupCategory"];
content_type: components["schemas"]["ContentType"];
@@ -3058,6 +3065,30 @@ export interface operations {
};
};
};
"Edit artist": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["EditedArtist"];
};
};
responses: {
/** @description Successfully edited the artist */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Artist"];
};
};
};
};
"Create artist": {
parameters: {
query?: never;

View File

@@ -1,14 +1,65 @@
<template>
<div class="name">{{ artist.name }}</div>
<div class="artist-header">
<div class="name">{{ artist.name }}</div>
<div class="actions">
<i
class="pi pi-pen-to-square"
v-if="userStore.class === 'staff' || artist.created_by_id === userStore.id"
v-tooltip.top="t('artist.edit')"
@click="editArtist"
/>
<i class="pi pi-bell" v-tooltip.top="'Not implemented yet'" />
<i class="pi pi-bookmark" v-tooltip.top="'Not implemented yet'" />
</div>
</div>
<Dialog closeOnEscape modal :header="t('artist.edit')" v-model:visible="editArtistDialogVisible">
<EditArtistDialog v-if="artistBeingEdited" :initialArtist="artistBeingEdited" @done="artistEdited" />
</Dialog>
</template>
<script setup lang="ts">
import { type Artist } from '@/services/api/artistService'
import { type Artist, type EditedArtist } from '@/services/api/artistService'
import { useUserStore } from '@/stores/user'
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
import Dialog from 'primevue/dialog'
import EditArtistDialog from './EditArtistDialog.vue'
defineProps<{
const { t } = useI18n()
const userStore = useUserStore()
const props = defineProps<{
artist: Artist
}>()
const emit = defineEmits<{
artistEdited: [Artist]
}>()
const editArtistDialogVisible = ref(false)
const artistBeingEdited = ref<EditedArtist | null>(null)
const editArtist = () => {
artistBeingEdited.value = props.artist
editArtistDialogVisible.value = true
}
const artistEdited = (artist: Artist) => {
editArtistDialogVisible.value = false
emit('artistEdited', artist)
}
</script>
<style scoped>
.artist-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.actions {
i {
margin: 0 3px;
cursor: pointer;
}
}
.name {
font-weight: bold;
font-size: 2em;

View File

@@ -0,0 +1,99 @@
<template>
<div class="edit-artist">
<FloatLabel style="margin-bottom: 30px">
<InputText name="name" v-model="editedArtist.name" />
<label for="name">{{ t('artist.name') }}</label>
</FloatLabel>
<BBCodeEditor
:initialValue="initialArtist.description"
:label="t('general.description')"
@valueChange="(val: string) => (editedArtist.description = val)"
/>
<div class="pictures input-list">
<label>{{ t('general.pictures') }}</label>
<div v-for="(_picture, index) in editedArtist.pictures" :key="index">
<InputText size="small" v-model="editedArtist.pictures[index]" />
<Button v-if="index == 0" @click="addPicture" icon="pi pi-plus" size="small" />
<Button v-if="index != 0 || editedArtist.pictures.length > 1" @click="removePicture(index)" icon="pi pi-minus" size="small" />
</div>
</div>
<div class="wrapper-center">
<Button :label="t('general.validate')" size="small" :loading="loading" @click="sendEdits()" />
</div>
</div>
</template>
<script setup lang="ts">
import { editArtist, type Artist, type EditedArtist } from '@/services/api/artistService'
import { FloatLabel, InputText } from 'primevue'
import Button from 'primevue/button'
import { ref, onMounted, toRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import BBCodeEditor from '../community/BBCodeEditor.vue'
const { t } = useI18n()
const props = defineProps<{
initialArtist: EditedArtist
}>()
const editedArtist = ref<EditedArtist>({
id: 0,
name: '',
description: '',
pictures: [],
})
const loading = ref(false)
const emit = defineEmits<{
done: [Artist]
}>()
const addPicture = () => {
editedArtist.value.pictures.push('')
}
const removePicture = (index: number) => {
editedArtist.value.pictures.splice(index, 1)
}
const sendEdits = () => {
loading.value = true
editedArtist.value.pictures = editedArtist.value.pictures.filter((picture) => picture.trim() !== '')
editArtist(editedArtist.value).then((newArtist) => {
loading.value = false
emit('done', newArtist)
})
}
onMounted(() => {
editedArtist.value = structuredClone(toRaw(props.initialArtist))
if (editedArtist.value.pictures.length === 0) {
editedArtist.value.pictures = ['']
}
})
</script>
<style scoped>
.edit-artist {
width: 50vw;
}
.pictures {
margin-top: 20px;
margin-bottom: 20px;
}
.input-list {
label {
display: block;
margin-bottom: 10px;
}
div {
display: flex;
gap: 5px;
margin-bottom: 5px;
input {
flex: 1;
}
}
}
</style>

View File

@@ -15,6 +15,7 @@
"cover": "Cover | Covers",
"external_link": "External link | External links",
"screenshots": "Screenshots",
"pictures": "Pictures",
"name": "Name",
"title": "Title",
"category": "Category",
@@ -76,6 +77,7 @@
"new_artist": "New artist",
"existing_artist": "Existing artist",
"no_affiliated_artist_registered": "No affiliated artist registered",
"edit": "Edit artist",
"role": {
"role": "Role | Roles",
"main": "Main",

View File

@@ -15,10 +15,16 @@ export type UserCreatedAffiliatedArtist = components['schemas']['UserCreatedAffi
export type UserCreatedArtist = components['schemas']['UserCreatedArtist']
export type EditedArtist = components['schemas']['EditedArtist']
export const getArtist = async (id: number): Promise<ArtistAndTitleGroupsLite> => {
return (await api.get<ArtistAndTitleGroupsLite>('/artists?id=' + id)).data
}
export const editArtist = async (artist: EditedArtist): Promise<Artist> => {
return (await api.put<Artist>('/artists', artist)).data
}
export const createArtists = async (artists: UserCreatedArtist[]): Promise<Artist[]> => {
return (await api.post<Artist[]>('/artists', artists)).data
}

View File

@@ -9,7 +9,7 @@
>
<div class="main">
<ArtistFullHeader :artist v-if="userStore.settings.site_appearance.item_detail_layout == 'header'" />
<ArtistSlimHeader v-else class="slim-header" :artist />
<ArtistSlimHeader v-else class="slim-header" :artist @artistEdited="artist = $event" />
<ContentContainer v-if="title_group_preview_mode == 'cover-only'">
<div class="title-groups">
<TitleGroupPreviewCoverOnly v-for="title_group in title_groups" :key="title_group.id" :titleGroup="title_group" />