diff --git a/react-admin/src/App.tsx b/react-admin/src/App.tsx index ae93d0e..f5bc181 100644 --- a/react-admin/src/App.tsx +++ b/react-admin/src/App.tsx @@ -29,6 +29,7 @@ import {ModelCreatePage} from "./pages/models/ModelCreatePage"; import { ModelEditPage } from "./pages/models/ModelEditPage"; import {ComponentTablePage} from "./pages/component/ComponentTablePage"; import {CollectionTable} from "./pages/collection/CollectionTable"; +import {CollectionEdit} from "./pages/collection/CollectionEdit"; function App() { return ( @@ -56,6 +57,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/react-admin/src/locales/en.json b/react-admin/src/locales/en.json index 77fac85..488950c 100644 --- a/react-admin/src/locales/en.json +++ b/react-admin/src/locales/en.json @@ -93,6 +93,7 @@ "page_information": "Page Information", "model": "Model", "model_information": "Model Information", + "collection_information": "Collection Information", "components": "Components", "field_type": "Field type", "upload_asset": "Upload asset", diff --git a/react-admin/src/pages/collection/CollectionEdit.tsx b/react-admin/src/pages/collection/CollectionEdit.tsx new file mode 100644 index 0000000..47bfc68 --- /dev/null +++ b/react-admin/src/pages/collection/CollectionEdit.tsx @@ -0,0 +1,135 @@ +import InputField from "../../components/InputField"; +import {Link, useParams} from "react-router-dom"; +import {useTranslation} from "react-i18next"; +import {useState} from "react"; +import {useForm} from "react-hook-form"; +import {PutCollectionIdentifierType} from "../../types/collection/PutCollectionIdentifierType"; +import {joiResolver} from "@hookform/resolvers/joi"; +import EditableCollectionType from "../../types/collection/EditableCollectionType"; +import {useGetCollection} from "./hooks/useGetCollection"; +import {useCollectionPutSchema} from "./schemas/CollectionPutSchema"; +import {useCollectionEditSchema} from "./schemas/CollectionEditSchema"; +import {usePutCollectionIdentifier} from "./hooks/usePutCollectionIdentifier"; +import {useUpdateCollection} from "./hooks/useUpdateCollection"; + +export const CollectionEdit = (() => { + const params = useParams(); + const collection_id = params.collection_id ?? '' + const {mutate} = useUpdateCollection(collection_id); + const [t] = useTranslation("global") + const {data} = useGetCollection(collection_id) + const [isEditableIdentifier, setIsEditableIdentifier] = useState(true) + const values = data?.data.data + + + const { + register: putCollectionRegister, + getValues: getCollectionIdentifierValue + } = useForm({ + resolver: joiResolver(useCollectionPutSchema(), {allowUnknown: true}), + values: { + identifier: data?.data.data.identifier + } + }); + + const { + register, + handleSubmit, + formState: {errors}, + } = useForm({ + resolver: joiResolver(useCollectionEditSchema(), {allowUnknown: true}), + values + }) + + const {mutate: putCollectionIdentifierMutate} = usePutCollectionIdentifier(collection_id) + + + const editableIdentifierOnClick = (() => { + setIsEditableIdentifier(false) + }) + const saveIdentifierOnClick = (() => { + putCollectionIdentifierMutate({identifier: getCollectionIdentifierValue('identifier')}) + setIsEditableIdentifier(true) + console.log("teststes") + }) + + const cancelIdentifierOnClick = (() => { + setIsEditableIdentifier(true) + }) + + const submitHandler = ((data: EditableCollectionType) => { + mutate(data) + }) + return ( + <> + + + + + {t("collection_information")} + + + + + + + + + + {isEditableIdentifier ? ( + <> + + {t("edit_identifier")} + + > + ) : ( + <> + + {t('save')} + + + {t('cancel')} + + > + )} + + + + + + {t("save")} + + + {t("cancel")} + + + + + + + > + ) +}) \ No newline at end of file diff --git a/react-admin/src/pages/collection/hooks/useGetCollection.ts b/react-admin/src/pages/collection/hooks/useGetCollection.ts new file mode 100644 index 0000000..0945e13 --- /dev/null +++ b/react-admin/src/pages/collection/hooks/useGetCollection.ts @@ -0,0 +1,23 @@ +import {useQuery} from '@tanstack/react-query' +import { useAxios } from '../../../hooks/useAxios' +import _ from 'lodash' +import {useNavigate} from 'react-router-dom' + +export const useGetCollection = (collection_id: string) => { + const client = useAxios() + const redirect = useNavigate() + + return useQuery({ + queryKey: ['collection', collection_id], + queryFn: (async () => { + try { + return await client.get("/collection/" + collection_id) + } catch (error) { + if (_.get(error, 'response.status') === 401) { + localStorage.removeItem('AUTH_TOKEN') + redirect("/admin/login") + } + } + }) + }) +} \ No newline at end of file diff --git a/react-admin/src/pages/collection/hooks/usePutCollectionIdentifier.ts b/react-admin/src/pages/collection/hooks/usePutCollectionIdentifier.ts new file mode 100644 index 0000000..a7a6bf8 --- /dev/null +++ b/react-admin/src/pages/collection/hooks/usePutCollectionIdentifier.ts @@ -0,0 +1,21 @@ +import {useMutation} from '@tanstack/react-query' +import { useAxios } from '../../../hooks/useAxios' +import _ from 'lodash' +import {useNavigate} from 'react-router-dom' +import {PutCollectionIdentifierType} from "../../../types/collection/PutCollectionIdentifierType"; + +export const usePutCollectionIdentifier = (collection_id: string) => { + const client = useAxios(); + const redirect = useNavigate(); + return useMutation({ + mutationFn: async (data: PutCollectionIdentifierType) => { + const url = '/put-collection-identifier/' + collection_id; + return await client.put(url , JSON.stringify(data)); + }, + onSuccess: (res) => { + if (_.get(res, 'data.status') === true) { + redirect("/admin/collection-edit/" + collection_id) + } + } + }) +} \ No newline at end of file diff --git a/react-admin/src/pages/collection/hooks/useStoreCollection.ts b/react-admin/src/pages/collection/hooks/useStoreCollection.ts new file mode 100644 index 0000000..8c4ac8b --- /dev/null +++ b/react-admin/src/pages/collection/hooks/useStoreCollection.ts @@ -0,0 +1,20 @@ +import {useMutation} from '@tanstack/react-query' +import { useAxios } from '../../../hooks/useAxios' +import _ from 'lodash' +import {useNavigate} from 'react-router-dom' +import {CreatableModelType} from "../../../types/model/CreatableModelType"; + +export const useStoreCollection = () => { + const client = useAxios(); + const redirect = useNavigate(); + return useMutation({ + mutationFn: async (data: CreatableModelType) => { + return await client.post('/model', JSON.stringify(data)); + }, + onSuccess: (res) => { + if (_.get(res, 'data.status') === true) { + redirect("/admin/model") + } + } + }) +} \ No newline at end of file diff --git a/react-admin/src/pages/collection/hooks/useUpdateCollection.ts b/react-admin/src/pages/collection/hooks/useUpdateCollection.ts new file mode 100644 index 0000000..3abb6cd --- /dev/null +++ b/react-admin/src/pages/collection/hooks/useUpdateCollection.ts @@ -0,0 +1,21 @@ +import {useMutation} from '@tanstack/react-query' +import { useAxios } from '../../../hooks/useAxios' +import _ from 'lodash' +import {useNavigate} from 'react-router-dom' +import IEditableModel from "../../../types/model/IEditableModel"; + +export const useUpdateCollection = (role_id: string) => { + const client = useAxios(); + const redirect = useNavigate(); + return useMutation({ + mutationFn: async (data: IEditableModel) => { + const url = '/collection/' + role_id; + return await client.put(url , JSON.stringify(data)); + }, + onSuccess: (res) => { + if (_.get(res, 'data.status') === true) { + redirect("/admin/collection") + } + } + }) +} \ No newline at end of file diff --git a/react-admin/src/pages/collection/schemas/CollectionCreateSchema.ts b/react-admin/src/pages/collection/schemas/CollectionCreateSchema.ts new file mode 100644 index 0000000..2eb1ea8 --- /dev/null +++ b/react-admin/src/pages/collection/schemas/CollectionCreateSchema.ts @@ -0,0 +1,15 @@ +import Joi from 'joi'; +import {useTranslation} from "react-i18next"; + +export const useCollectionCreateSchema = (() => { + + const [t] = useTranslation("global") + return Joi.object({ + name : Joi.string().required().messages({ + 'string.empty': t("empty_message", {attribute: t("name")}), + }), + identifier : Joi.string().required().messages({ + 'string.empty': t("empty_message", {attribute: t("identifier")}), + }) + }); +}) diff --git a/react-admin/src/pages/collection/schemas/CollectionEditSchema.ts b/react-admin/src/pages/collection/schemas/CollectionEditSchema.ts new file mode 100644 index 0000000..765e1f3 --- /dev/null +++ b/react-admin/src/pages/collection/schemas/CollectionEditSchema.ts @@ -0,0 +1,12 @@ +import Joi from 'joi'; +import {useTranslation} from "react-i18next"; + +export const useCollectionEditSchema = (() => { + + const [t] = useTranslation("global") + return Joi.object({ + name : Joi.string().required().messages({ + 'string.empty': t("empty_message", {attribute: t("name")}), + }) + }); +}) diff --git a/react-admin/src/pages/collection/schemas/CollectionPutSchema.ts b/react-admin/src/pages/collection/schemas/CollectionPutSchema.ts new file mode 100644 index 0000000..b19d940 --- /dev/null +++ b/react-admin/src/pages/collection/schemas/CollectionPutSchema.ts @@ -0,0 +1,12 @@ +import Joi from 'joi'; +import {useTranslation} from "react-i18next"; + +export const useCollectionPutSchema = (() => { + + const [t] = useTranslation("global") + return Joi.object({ + identifier : Joi.string().required().messages({ + 'string.empty': t("empty_message", {attribute: t("identifier")}), + }) + }); +}) diff --git a/react-admin/src/types/collection/CreatableCollectionType.ts b/react-admin/src/types/collection/CreatableCollectionType.ts new file mode 100644 index 0000000..22df65e --- /dev/null +++ b/react-admin/src/types/collection/CreatableCollectionType.ts @@ -0,0 +1,4 @@ +export type CreatableCollectionType = { + name: string; + identifier: string; +} \ No newline at end of file diff --git a/react-admin/src/types/collection/EditableCollectionType.ts b/react-admin/src/types/collection/EditableCollectionType.ts new file mode 100644 index 0000000..ae4b288 --- /dev/null +++ b/react-admin/src/types/collection/EditableCollectionType.ts @@ -0,0 +1,4 @@ +export default interface EditableCollectionType { + id: string; + name: string; +} \ No newline at end of file diff --git a/react-admin/src/types/collection/PutCollectionIdentifierType.ts b/react-admin/src/types/collection/PutCollectionIdentifierType.ts new file mode 100644 index 0000000..c2a365e --- /dev/null +++ b/react-admin/src/types/collection/PutCollectionIdentifierType.ts @@ -0,0 +1,3 @@ +export type PutCollectionIdentifierType = { + identifier: String; +} \ No newline at end of file diff --git a/src/api/handlers/collection/fetch_collection_api_handler.rs b/src/api/handlers/collection/fetch_collection_api_handler.rs index 2cf20e2..e492264 100644 --- a/src/api/handlers/collection/fetch_collection_api_handler.rs +++ b/src/api/handlers/collection/fetch_collection_api_handler.rs @@ -1,4 +1,3 @@ -use crate::api::handlers::model::fetch_model_api_handler::FetchModelResponse; use crate::avored_state::AvoRedState; use crate::error::{Error, Result}; use crate::models::collection_model::CollectionModel; diff --git a/src/api/handlers/collection/mod.rs b/src/api/handlers/collection/mod.rs index 42b7d0b..88e1461 100644 --- a/src/api/handlers/collection/mod.rs +++ b/src/api/handlers/collection/mod.rs @@ -3,3 +3,4 @@ pub mod fetch_collection_api_handler; pub mod request; pub mod store_collection_api_handler; pub mod update_collection_api_handler; +pub mod put_collection_identifier_api_handler; diff --git a/src/api/handlers/collection/put_collection_identifier_api_handler.rs b/src/api/handlers/collection/put_collection_identifier_api_handler.rs new file mode 100644 index 0000000..028c307 --- /dev/null +++ b/src/api/handlers/collection/put_collection_identifier_api_handler.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; +use axum::{Extension, Json}; +use axum::extract::{Path, State}; +use crate::api::handlers::collection::request::put_collection_identifier_request::PutCollectionRequest; +use crate::avored_state::AvoRedState; +use crate::error::{Error, Result}; +use crate::models::collection_model::{CollectionModel, PutCollectionIdentifierModel}; +use crate::models::token_claim_model::LoggedInUser; +use crate::models::validation_error::ErrorResponse; +use crate::responses::ApiResponse; + +pub async fn put_collection_identifier_api_handler( + Path(collection_id): Path, + Extension(logged_in_user): Extension, + state: State>, + Json(payload): Json, +) -> Result>> { + println!("->> {:<12} - put_collection_identifier_api_handler", "HANDLER"); + + let has_permission_bool = state + .admin_user_service + .has_permission(logged_in_user.clone(), String::from("collection_edit")) + .await?; + if !has_permission_bool { + return Err(Error::Forbidden); + } + + let error_messages = payload.validate(state.clone()).await?; + + if !error_messages.is_empty() { + let error_response = ErrorResponse { + status: false, + errors: error_messages, + }; + + return Err(Error::BadRequest(error_response)); + } + + let put_collection_identifier = PutCollectionIdentifierModel { + id: collection_id, + identifier: payload.identifier, + logged_in_username: logged_in_user.email, + }; + let updated_collection = state + .collection_service + .update_collection_identifier(&state.db, put_collection_identifier) + .await?; + + + let api_response = ApiResponse { + status: true, + data: updated_collection, + }; + + Ok(Json(api_response)) +} \ No newline at end of file diff --git a/src/api/handlers/collection/request/mod.rs b/src/api/handlers/collection/request/mod.rs index 447d048..161c8d4 100644 --- a/src/api/handlers/collection/request/mod.rs +++ b/src/api/handlers/collection/request/mod.rs @@ -1,3 +1,4 @@ pub mod collection_table_request; pub mod store_collection_request; pub mod update_collection_request; +pub mod put_collection_identifier_request; diff --git a/src/api/handlers/collection/request/put_collection_identifier_request.rs b/src/api/handlers/collection/request/put_collection_identifier_request.rs new file mode 100644 index 0000000..88450c3 --- /dev/null +++ b/src/api/handlers/collection/request/put_collection_identifier_request.rs @@ -0,0 +1,44 @@ +use crate::avored_state::AvoRedState; +use crate::models::validation_error::{ErrorMessage, Validate}; +use axum::extract::State; +use rust_i18n::t; +use serde::Deserialize; +use std::sync::Arc; + +#[derive(Deserialize, Debug, Clone, Default)] +pub struct PutCollectionRequest { + pub identifier: String, +} + +impl PutCollectionRequest { + pub async fn validate( + &self, + state: State>, + ) -> crate::error::Result> { + let mut errors: Vec = vec![]; + + if !self.identifier.required()? { + let error_message = ErrorMessage { + key: String::from("identifier"), + message: t!("validation_required", attribute = t!("identifier")).to_string(), + }; + + errors.push(error_message); + } + + let collection_count = state + .collection_service + .count_of_identifier(&state.db, self.identifier.clone()) + .await?; + + if collection_count.total > 0 { + let error_message = ErrorMessage { + key: String::from("identifier"), + message: t!("validation_count", attribute = t!("identifier")).to_string(), + }; + errors.push(error_message); + } + + Ok(errors) + } +} diff --git a/src/api/handlers/collection/update_collection_api_handler.rs b/src/api/handlers/collection/update_collection_api_handler.rs index 05c9c10..78e9da7 100644 --- a/src/api/handlers/collection/update_collection_api_handler.rs +++ b/src/api/handlers/collection/update_collection_api_handler.rs @@ -36,7 +36,7 @@ pub async fn update_collection_api_handler( return Err(Error::BadRequest(error_response)); } - let creatable_model = UpdatableCollection { + let creatable_collection = UpdatableCollection { name: payload.name, id: collection_id, logged_in_username: logged_in_user.email, @@ -44,7 +44,7 @@ pub async fn update_collection_api_handler( let updated_model = state .collection_service - .update_collection(&state.db, creatable_model) + .update_collection(&state.db, creatable_collection) .await?; let response = ApiResponse { status: true, diff --git a/src/api/rest_api_routes.rs b/src/api/rest_api_routes.rs index ff3e256..7fbd088 100644 --- a/src/api/rest_api_routes.rs +++ b/src/api/rest_api_routes.rs @@ -66,6 +66,7 @@ use axum::{middleware, routing::get, Extension, Router}; use juniper::{EmptyMutation, EmptySubscription}; use std::sync::Arc; use tower_http::cors::CorsLayer; +use crate::api::handlers::collection::put_collection_identifier_api_handler::put_collection_identifier_api_handler; pub fn rest_api_routes(state: Arc) -> Router { Router::new() @@ -153,6 +154,14 @@ fn admin_api_routes(state: Arc) -> Router { "/api/collection/:collection_id", get(fetch_collection_api_handler), ) + .route( + "/api/collection/:collection_id", + put(update_collection_api_handler), + ) + .route( + "/api/put-collection-identifier/:collection_id", + put(put_collection_identifier_api_handler), + ) .route("/api/model", get(model_table_api_handler)) .route("/api/model", post(store_model_api_handler)) .route("/api/model/:model_id", put(update_model_api_handler)) diff --git a/src/models/collection_model.rs b/src/models/collection_model.rs index 33be196..a016f41 100644 --- a/src/models/collection_model.rs +++ b/src/models/collection_model.rs @@ -34,6 +34,14 @@ pub struct UpdatableCollection { pub logged_in_username: String, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PutCollectionIdentifierModel { + pub id: String, + pub identifier: String, + pub logged_in_username: String, +} + + impl TryFrom for CollectionModel { type Error = Error; fn try_from(val: Object) -> crate::error::Result { diff --git a/src/repositories/collection_repository.rs b/src/repositories/collection_repository.rs index 859cba6..4a2405a 100644 --- a/src/repositories/collection_repository.rs +++ b/src/repositories/collection_repository.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use super::into_iter_objects; use crate::error::{Error, Result}; -use crate::models::collection_model::{CollectionModel, CreatableCollection, UpdatableCollection}; +use crate::models::collection_model::{CollectionModel, CreatableCollection, PutCollectionIdentifierModel, UpdatableCollection}; use crate::models::ModelCount; use crate::PER_PAGE; use surrealdb::dbs::Session; @@ -98,59 +98,59 @@ impl CollectionRepository { } // // - // pub async fn update_model_identifier( - // &self, - // datastore: &Datastore, - // database_session: &Session, - // put_model_identifier_model: PutCollectionIdentifierCollection - // ) -> Result { - // let sql = "UPDATE type::thing($table, $id) - // SET - // identifier = $identifier, - // updated_at = $updated_at, - // updated_by = $updated_by - // ; - // "; - // - // let vars: BTreeMap = [ - // ("identifier".into(), put_model_identifier_model.identifier.into()), - // ("table".into(), "models".into()), - // ("updated_at".into(), Datetime::default().into()), - // ("updated_by".into(), put_model_identifier_model.logged_in_username.into()), - // ("id".into(), put_model_identifier_model.id.into()) - // ].into(); - // let responses = datastore.execute(sql, database_session, Some(vars)).await?; - // - // let result_object_option = into_iter_objects(responses)?.next(); - // let result_object = match result_object_option { - // Some(object) => object, - // None => Err(Error::Generic("no record found".to_string())), - // }; - // let updated_model: Result = result_object?.try_into(); - // - // updated_model - // } - - // pub async fn count_of_identifier( - // &self, - // datastore: &Datastore, - // database_session: &Session, - // identifier: String - // ) -> Result { - // let sql = "SELECT count(identifier=$identifier) FROM models GROUP ALL"; - // - // let vars: BTreeMap = [("identifier".into(), identifier.into())].into(); - // let responses = datastore.execute(sql, database_session, Some(vars)).await?; - // - // let result_object_option = into_iter_objects(responses)?.next(); - // let result_object = match result_object_option { - // Some(object) => object, - // None => Err(Error::Generic("no record found".to_string())), - // }; - // let model_count: Result = result_object?.try_into(); - // - // model_count - // } + pub async fn update_collection_identifier( + &self, + datastore: &Datastore, + database_session: &Session, + put_model_identifier_model: PutCollectionIdentifierModel + ) -> Result { + let sql = "UPDATE type::thing($table, $id) + SET + identifier = $identifier, + updated_at = $updated_at, + updated_by = $updated_by + ; + "; + + let vars: BTreeMap = [ + ("identifier".into(), put_model_identifier_model.identifier.into()), + ("table".into(), "collections".into()), + ("updated_at".into(), Datetime::default().into()), + ("updated_by".into(), put_model_identifier_model.logged_in_username.into()), + ("id".into(), put_model_identifier_model.id.into()) + ].into(); + let responses = datastore.execute(sql, database_session, Some(vars)).await?; + + let result_object_option = into_iter_objects(responses)?.next(); + let result_object = match result_object_option { + Some(object) => object, + None => Err(Error::Generic("no record found".to_string())), + }; + let updated_model: Result = result_object?.try_into(); + + updated_model + } + + pub async fn count_of_identifier( + &self, + datastore: &Datastore, + database_session: &Session, + identifier: String + ) -> Result { + let sql = "SELECT count(identifier=$identifier) FROM collections GROUP ALL"; + + let vars: BTreeMap = [("identifier".into(), identifier.into())].into(); + let responses = datastore.execute(sql, database_session, Some(vars)).await?; + + let result_object_option = into_iter_objects(responses)?.next(); + let result_object = match result_object_option { + Some(object) => object, + None => Err(Error::Generic("no record found".to_string())), + }; + let model_count: Result = result_object?.try_into(); + + model_count + } pub async fn update_collection( &self, diff --git a/src/services/collection_service.rs b/src/services/collection_service.rs index b5fb2c3..d7d93ae 100644 --- a/src/services/collection_service.rs +++ b/src/services/collection_service.rs @@ -1,8 +1,6 @@ use crate::error::Result; -use crate::models::collection_model::{ - CollectionModel, CollectionPagination, CreatableCollection, UpdatableCollection, -}; -use crate::models::Pagination; +use crate::models::collection_model::{CollectionModel, CollectionPagination, CreatableCollection, PutCollectionIdentifierModel, UpdatableCollection}; +use crate::models::{ModelCount, Pagination}; use crate::providers::avored_database_provider::DB; use crate::repositories::collection_repository::CollectionRepository; use crate::PER_PAGE; @@ -11,6 +9,7 @@ pub struct CollectionService { collection_repository: CollectionRepository, } + impl CollectionService { pub fn new(collection_repository: CollectionRepository) -> Result { Ok(CollectionService { @@ -99,25 +98,25 @@ impl CollectionService { .await } - // pub async fn update_collection_identifier( - // &self, - // (datastore, database_session): &DB, - // put_collection_identifier_collection: PutCollectionIdentifierCollection - // ) -> Result { - // self.collection_repository - // .update_collection_identifier(datastore, database_session, put_collection_identifier_collection) - // .await - // } + pub async fn update_collection_identifier( + &self, + (datastore, database_session): &DB, + put_collection_identifier_collection: PutCollectionIdentifierModel + ) -> Result { + self.collection_repository + .update_collection_identifier(datastore, database_session, put_collection_identifier_collection) + .await + } - // pub async fn count_of_identifier( - // &self, - // (datastore, database_session): &DB, - // identifier: String, - // ) -> Result { - // self.collection_repository - // .count_of_identifier(datastore, database_session, identifier) - // .await - // } + pub async fn count_of_identifier( + &self, + (datastore, database_session): &DB, + identifier: String, + ) -> Result { + self.collection_repository + .count_of_identifier(datastore, database_session, identifier) + .await + } pub async fn update_collection( &self,