mirror of
https://github.com/Arcadia-Solutions/arcadia.git
synced 2025-12-21 09:19:33 -06:00
feat: edit artist on frontend, resolves #363
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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#"
|
||||
|
||||
33
frontend/src/api-schema/schema.d.ts
vendored
33
frontend/src/api-schema/schema.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
99
frontend/src/components/artist/EditArtistDialog.vue
Normal file
99
frontend/src/components/artist/EditArtistDialog.vue
Normal 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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user