1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

split plan-catageories management into separate components

This commit is contained in:
Sylvain 2021-06-09 09:24:39 +02:00
parent dfd2ac52dd
commit 08ad436351
14 changed files with 406 additions and 184 deletions

View File

@ -0,0 +1,17 @@
import React from 'react';
interface FabAlertProps {
level: 'info' | 'warning' | 'danger',
className?: string,
}
/**
* This component shows a styled text paragraph, useful to display important information messages.
*/
export const FabAlert: React.FC<FabAlertProps> = ({ level, className, children }) => {
return (
<div className={`fab-alert fab-alert--${level} ${className ? className : ''}`}>
{children}
</div>
)
};

View File

@ -20,6 +20,13 @@ export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className,
return !!icon;
}
/**
* Check if the current button has children properties (like some text)
*/
const hasChildren = (): boolean => {
return !!children;
}
/**
* Handle the action of the button
*/
@ -31,7 +38,7 @@ export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className,
return (
<button type={type} form={form} onClick={handleClick} disabled={disabled} className={`fab-button ${className ? className : ''}`}>
{hasIcon() && <span className="fab-button--icon">{icon}</span>}
{hasIcon() && <span className={hasChildren() ? 'fab-button--icon' : 'fab-button--icon-only'}>{icon}</span>}
{children}
</button>
);

View File

@ -1,9 +1,9 @@
import React, { BaseSyntheticEvent } from 'react';
import React, { BaseSyntheticEvent, ReactNode } from 'react';
interface LabelledInputProps {
id: string,
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week',
label: string,
label: string | ReactNode,
value: any,
onChange: (event: BaseSyntheticEvent) => void
}

View File

@ -0,0 +1,123 @@
import React, { BaseSyntheticEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PlanCategoryAPI from '../../api/plan-category';
import { PlanCategory } from '../../models/plan-category';
import { FabButton } from '../base/fab-button';
import { FabModal } from '../base/fab-modal';
import { LabelledInput } from '../base/labelled-input';
import { Loader } from '../base/loader';
import { FabAlert } from '../base/fab-alert';
interface CreatePlanCategoryProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a button.
* When clicked, we show a modal dialog allowing to fill the parameters with a new plan-category.
*/
const CreatePlanCategoryComponent: React.FC<CreatePlanCategoryProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [category, setCategory] = useState<PlanCategory>(null);
// is the creation modal open?
const [isOpen, setIsOpen] = useState<boolean>(false);
/**
* Opens/closes the new plan-category (creation) modal
*/
const toggleModal = (): void => {
setIsOpen(!isOpen);
};
/**
* The creation has been confirmed by the user.
* Push the new plan-category to the API.
*/
const onCreateConfirmed = (): void => {
PlanCategoryAPI.create(category).then(() => {
onSuccess(t('app.admin.create_plan_category.category_created'));
resetCategory();
toggleModal();
}).catch((error) => {
onError(t('app.admin.create_plan_category.unable_to_create') + error);
});
};
/**
* Callback triggered when the user is changing the name of the category in the modal dialog.
* We update the name of the temporary-set plan-category, accordingly.
*/
const onCategoryNameChange = (event: BaseSyntheticEvent) => {
setCategory({...category, name: event.target.value });
};
/**
* Callback triggered when the user is changing the weight of the category in the modal dialog.
* We update the weight of the temporary-set plan-category, accordingly.
*/
const onCategoryWeightChange = (event: BaseSyntheticEvent) => {
setCategory({...category, weight: event.target.value });
};
/**
* Initialize a new plan-category for creation
*/
const initCategoryCreation = () => {
setCategory({ name: '', weight: 0 });
};
/**
* Reinitialize the category to prevent ghost data
*/
const resetCategory = () => {
setCategory(null);
}
return (
<div className="create-plan-category">
<FabButton type='button'
icon={<i className='fa fa-plus' />}
className="add-category"
onClick={toggleModal}>
{t('app.admin.create_plan_category.new_category')}
</FabButton>
<FabModal title={t('app.admin.create_plan_category.new_category')}
className="create-plan-category-modal"
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={t('app.admin.create_plan_category.confirm_create')}
onConfirm={onCreateConfirmed}
onCreation={initCategoryCreation}>
{category && <div>
<label htmlFor="name">{t('app.admin.create_plan_category.name')}</label>
<LabelledInput id="name"
label={<i className="fa fa-tag" />}
type="text"
value={category.name}
onChange={onCategoryNameChange} />
<label htmlFor="weight">{t('app.admin.create_plan_category.significance')}</label>
<LabelledInput id="weight"
type="number"
label={<i className="fa fa-sort-numeric-desc" />}
value={category.weight}
onChange={onCategoryWeightChange} />
</div>}
<FabAlert level="info" className="significance-info">
{t('app.admin.create_plan_category.significance_info')}
</FabAlert>
</FabModal>
</div>
)
};
export const CreatePlanCategory: React.FC<CreatePlanCategoryProps> = ({ onSuccess, onError }) => {
return (
<Loader>
<CreatePlanCategoryComponent onSuccess={onSuccess} onError={onError} />
</Loader>
);
}

View File

@ -0,0 +1,67 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import PlanCategoryAPI from '../../api/plan-category';
import { PlanCategory } from '../../models/plan-category';
import { FabButton } from '../base/fab-button';
import { FabModal } from '../base/fab-modal';
import { Loader } from '../base/loader';
interface DeletePlanCategoryProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
category: PlanCategory,
}
/**
* This component shows a button.
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided plan-category.
*/
const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
const { t } = useTranslation('admin');
const [deletionModal, setDeletionModal] = useState<boolean>(false);
/**
* Opens/closes the deletion modal
*/
const toggleDeletionModal = (): void => {
setDeletionModal(!deletionModal);
};
/**
* The deletion has been confirmed by the user.
* Call the API to trigger the deletion of the temporary set plan-category
*/
const onDeleteConfirmed = (): void => {
PlanCategoryAPI.destroy(category.id).then(() => {
onSuccess(t('app.admin.delete_plan_category.category_deleted'));
}).catch((error) => {
onError(t('app.admin.delete_plan_category.unable_to_delete') + error);
});
toggleDeletionModal();
};
return (
<div className="delete-plan-category">
<FabButton type='button' className="delete-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.delete_plan_category.delete_category')}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.delete_plan_category.confirm_delete')}
onConfirm={onDeleteConfirmed}>
<span>{t('app.admin.delete_plan_category.delete_confirmation')}</span>
</FabModal>
</div>
)
};
export const DeletePlanCategory: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
return (
<Loader>
<DeletePlanCategoryComponent onSuccess={onSuccess} onError={onError} category={category} />
</Loader>
);
}

View File

@ -0,0 +1,108 @@
import React, { BaseSyntheticEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PlanCategoryAPI from '../../api/plan-category';
import { PlanCategory } from '../../models/plan-category';
import { FabButton } from '../base/fab-button';
import { FabModal } from '../base/fab-modal';
import { LabelledInput } from '../base/labelled-input';
import { Loader } from '../base/loader';
import { FabAlert } from '../base/fab-alert';
interface EditPlanCategoryProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
category: PlanCategory
}
/**
* This component shows an edit button.
* When clicked, we show a modal dialog allowing to edit the parameters of the provided plan-category.
*/
const EditPlanCategoryComponent: React.FC<EditPlanCategoryProps> = ({ onSuccess, onError, category }) => {
const { t } = useTranslation('admin');
// is the edition modal open?
const [editionModal, setEditionModal] = useState<boolean>(false);
// when editing, we store the category here, until the edition is over
const [tempCategory, setTempCategory] = useState<PlanCategory>(category);
/**
* Opens/closes the edition modal
*/
const toggleEditionModal = (): void => {
setEditionModal(!editionModal);
};
/**
* The edit has been confirmed by the user.
* Call the API to trigger the update of the temporary set plan-category
*/
const onEditConfirmed = (): void => {
PlanCategoryAPI.update(tempCategory).then((updatedCategory) => {
onSuccess(t('app.admin.edit_plan_category.category_updated'));
setTempCategory(updatedCategory);
toggleEditionModal();
}).catch((error) => {
onError(t('app.admin.edit_plan_category.unable_to_update') + error);
});
};
/**
* Callback triggered when the user is changing the name of the category in the modal dialog.
* We update the name of the temporary-set plan-category, accordingly.
*/
const onCategoryNameChange = (event: BaseSyntheticEvent) => {
setTempCategory({...tempCategory, name: event.target.value });
};
/**
* Callback triggered when the user is changing the weight of the category in the modal dialog.
* We update the weight of the temporary-set plan-category, accordingly.
*/
const onCategoryWeightChange = (event: BaseSyntheticEvent) => {
setTempCategory({...tempCategory, weight: event.target.value });
};
return (
<div className="edit-plan-category">
<FabButton type='button' className="edit-button" icon={<i className="fa fa-edit" />} onClick={toggleEditionModal} />
<FabModal title={t('app.admin.edit_plan_category.edit_category')}
isOpen={editionModal}
toggleModal={toggleEditionModal}
className="edit-plan-category-modal"
closeButton={true}
confirmButton={t('app.admin.edit_plan_category.confirm_edition')}
onConfirm={onEditConfirmed}>
{tempCategory && <div>
<label htmlFor="category-name">{t('app.admin.edit_plan_category.name')}</label>
<LabelledInput id="category-name"
type="text"
label={<i className="fa fa-tag" />}
value={tempCategory.name}
onChange={onCategoryNameChange} />
<label htmlFor="category-weight">{t('app.admin.edit_plan_category.significance')}</label>
<LabelledInput id="category-weight"
type="number"
label={<i className="fa fa-sort-numeric-desc" />}
value={tempCategory.weight}
onChange={onCategoryWeightChange} />
</div>}
<FabAlert level="info" className="significance-info">
{t('app.admin.edit_plan_category.significance_info')}
</FabAlert>
</FabModal>
</div>
)
};
export const EditPlanCategory: React.FC<EditPlanCategoryProps> = ({ onSuccess, onError, category }) => {
return (
<Loader>
<EditPlanCategoryComponent onSuccess={onSuccess} onError={onError} category={category} />
</Loader>
);
}

View File

@ -1,13 +1,13 @@
import React, { BaseSyntheticEvent, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PlanCategoryAPI from '../../api/plan-category';
import { PlanCategory } from '../../models/plan-category';
import { FabButton } from '../base/fab-button';
import { FabModal } from '../base/fab-modal';
import { LabelledInput } from '../base/labelled-input';
import { react2angular } from 'react2angular';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { CreatePlanCategory } from './create-plan-category';
import { EditPlanCategory } from './edit-plan-category';
import { DeletePlanCategory } from './delete-plan-category';
declare var Application: IApplication;
@ -25,14 +25,6 @@ export const PlanCategoriesList: React.FC<PlanCategoriesListProps> = ({ onSucces
// list of all categories
const [categories, setCategories] = useState<Array<PlanCategory>>(null);
// when editing or deleting a category, it will be stored here until the edition is over
const [tempCategory, setTempCategory] = useState<PlanCategory>(null);
// is the creation modal open?
const [creationModal, setCreationModal] = useState<boolean>(false);
// is the edition modal open?
const [editionModal, setEditionModal] = useState<boolean>(false);
// is the deletion modal open?
const [deletionModal, setDeletionModal] = useState<boolean>(false);
// load the categories list on component mount
useEffect(() => {
@ -40,123 +32,14 @@ export const PlanCategoriesList: React.FC<PlanCategoriesListProps> = ({ onSucces
}, []);
/**
* Opens/closes the new plan-category (creation) modal
* The creation/edition/deletion was successful.
* Show the provided message and refresh the list
*/
const toggleCreationModal = (): void => {
setCreationModal(!creationModal);
const handleSuccess = (message: string): void => {
onSuccess(message);
refreshCategories();
};
/**
* Opens/closes the edition modal
*/
const toggleEditionModal = (): void => {
setEditionModal(!editionModal);
};
/**
* Opens/closes the deletion modal
*/
const toggleDeletionModal = (): void => {
setDeletionModal(!deletionModal);
};
/**
* Triggered when the edit-category button is pushed.
* Set the provided category to the currently edited category then open the edition modal.
*/
const handleEditCategory = (category: PlanCategory) => {
return () => {
setTempCategory(category);
toggleEditionModal();
};
};
/**
* Triggered when the delete-category button is pushed.
* Set the provided category to the currently deleted category then open the deletion modal.
*/
const handleDeleteCategory = (category: PlanCategory) => {
return () => {
setTempCategory(category);
toggleDeletionModal();
};
};
/**
* The creation has been confirmed by the user.
* Push the new plan-category to the API.
*/
const onCreateConfirmed = (): void => {
PlanCategoryAPI.create(tempCategory).then(() => {
onSuccess(t('app.admin.plan_categories_list.category_created'));
resetTempCategory();
toggleCreationModal();
refreshCategories();
}).catch((error) => {
onError(t('app.admin.plan_categories_list.unable_to_create') + error);
});
};
/**
* The deletion has been confirmed by the user.
* Call the API to trigger the deletion of the temporary set plan-category
*/
const onDeleteConfirmed = (): void => {
PlanCategoryAPI.destroy(tempCategory.id).then(() => {
onSuccess(t('app.admin.plan_categories_list.category_deleted'));
refreshCategories();
}).catch((error) => {
onError(t('app.admin.plan_categories_list.unable_to_delete') + error);
});
resetTempCategory();
toggleDeletionModal();
};
/**
* The edit has been confirmed by the user.
* Call the API to trigger the update of the temporary set plan-category
*/
const onEditConfirmed = (): void => {
PlanCategoryAPI.update(tempCategory).then(() => {
onSuccess(t('app.admin.plan_categories_list.category_updated'));
resetTempCategory();
refreshCategories();
toggleEditionModal();
}).catch((error) => {
onError(t('app.admin.plan_categories_list.unable_to_update') + error);
});
};
/**
* Callback triggered when the user is changing the name of the category in the modal dialog.
* We update the name of the temporary-set plan-category, accordingly.
*/
const onCategoryNameChange = (event: BaseSyntheticEvent) => {
setTempCategory({...tempCategory, name: event.target.value });
};
/**
* Callback triggered when the user is changing the weight of the category in the modal dialog.
* We update the weight of the temporary-set plan-category, accordingly.
*/
const onCategoryWeightChange = (event: BaseSyntheticEvent) => {
setTempCategory({...tempCategory, weight: event.target.value });
};
/**
* Initialize a new plan-category for creation
*/
const initCategoryCreation = () => {
setTempCategory({ name: '', weight: 0 });
};
/**
* Reinitialize the temporary category to prevent ghost data
*/
const resetTempCategory = () => {
setTempCategory(null);
}
/**
* Refresh the list of categories
*/
@ -168,72 +51,28 @@ export const PlanCategoriesList: React.FC<PlanCategoriesListProps> = ({ onSucces
return (
<div className="plan-categories-list">
<FabButton type='button'
icon={<i className='fa fa-plus' />}
className="add-category"
onClick={toggleCreationModal}>
{t('app.admin.plan_categories_list.new_category')}
</FabButton>
<CreatePlanCategory onSuccess={handleSuccess}
onError={onError} />
<h3>{t('app.admin.plan_categories_list.categories_list')}</h3>
<table className="categories-table">
<thead>
<tr>
<th style={{ width: '66%' }}>{t('app.admin.plan_categories_list.name')}</th>
<th>{t('app.admin.plan_categories_list.significance')} <i className="fa fa-sort-numeric-desc" /></th>
</tr>
</thead>
<tbody>
{categories && categories.map(c =>
<tr key={c.id}>
<td className="category-name">{c.name}</td>
<td className="category-weight">{c.weight}</td>
<td className="category-actions">
<FabButton type='button' className="edit-button" icon={<i className="fa fa-edit" />} onClick={handleEditCategory(c)} />
<FabButton type='button' className="delete-button" icon={<i className="fa fa-trash" />} onClick={handleDeleteCategory(c)} />
<EditPlanCategory onSuccess={handleSuccess} onError={onError} category={c} />
<DeletePlanCategory onSuccess={handleSuccess} onError={onError} category={c} />
</td>
</tr>)}
</tbody>
</table>
<FabModal title={t('app.admin.plan_categories_list.new_category')}
isOpen={creationModal}
toggleModal={toggleCreationModal}
closeButton={true}
confirmButton={t('app.admin.plan_categories_list.confirm_create')}
onConfirm={onCreateConfirmed}
onCreation={initCategoryCreation}>
{tempCategory && <div>
<LabelledInput id="name"
label={t('app.admin.plan_categories_list.name')}
type="text"
value={tempCategory.name}
onChange={onCategoryNameChange} />
<LabelledInput id="weight"
type="number"
label={t('app.admin.plan_categories_list.significance')}
value={tempCategory.weight}
onChange={onCategoryWeightChange} />
</div>}
</FabModal>
<FabModal title={t('app.admin.plan_categories_list.delete_category')}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.plan_categories_list.confirm_delete')}
onConfirm={onDeleteConfirmed}>
<span>{t('app.admin.plan_categories_list.delete_confirmation')}</span>
</FabModal>
<FabModal title={t('app.admin.plan_categories_list.edit_category')}
isOpen={editionModal}
toggleModal={toggleEditionModal}
closeButton={true}
confirmButton={t('app.admin.plan_categories_list.confirm_edition')}
onConfirm={onEditConfirmed}>
{tempCategory && <div>
<LabelledInput id="category-name"
type="text"
label={t('app.admin.plan_categories_list.name')}
value={tempCategory.name}
onChange={onCategoryNameChange} />
<LabelledInput id="category-weight"
type="number"
label={t('app.admin.plan_categories_list.significance')}
value={tempCategory.weight}
onChange={onCategoryWeightChange} />
</div>}
</FabModal>
</div>
)
};

View File

@ -24,6 +24,7 @@
@import "modules/fab-modal";
@import "modules/fab-input";
@import "modules/fab-button";
@import "modules/fab-alert";
@import "modules/payment-schedule-summary";
@import "modules/wallet-info";
@import "modules/stripe-modal";
@ -44,5 +45,8 @@
@import "modules/stripe-update-card-modal";
@import "modules/payzen-update-card-modal";
@import "modules/plan-categories-list";
@import "modules/create-plan-category";
@import "modules/edit-plan-category";
@import "modules/delete-plan-category";
@import "app.responsive";

View File

@ -0,0 +1,7 @@
.create-plan-category {}
.create-plan-category-modal {
.significance-info {
margin-top: 10px;
}
}

View File

@ -0,0 +1,3 @@
.delete-plan-category {
display: inline;
}

View File

@ -0,0 +1,10 @@
.edit-plan-category {
display: inline;
margin-right: 5px;
}
.edit-plan-category-modal {
.significance-info {
margin-top: 10px;
}
}

View File

@ -0,0 +1,24 @@
.fab-alert {
padding: 15px;
margin-bottom: 24px;
border: 1px solid transparent;
border-radius: 4px;
&--info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
&--warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
&--danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
}

View File

@ -46,4 +46,6 @@
&--icon {
margin-right: 0.5em;
}
&--icon-only {}
}

View File

@ -1292,19 +1292,30 @@ en:
manage_plans_categories: "Manage plans' categories"
plan_categories_list:
categories_list: "List of the plan's categories"
name: "Name"
significance: "Significance"
create_plan_category:
new_category: "New category"
name: "Name"
significance: "Significance"
significance_info: "Categories will be shown ordered by signifiance. The higher you set the significance, the first the category will be shown."
confirm_create: "Create the category"
category_created: "The new category was successfully created"
unable_to_create: "Unable to create the category: "
edit_plan_category:
edit_category: "Edit the category"
name: "Name"
significance: "Significance"
confirm_edition: "Validate"
category_updated: "The category was successfully updated"
unable_to_update: "Unable to update the category: "
significance_info: "Categories will be shown ordered by signifiance. The higher you set the significance, the first the category will be shown."
delete_plan_category:
delete_category: "Delete a category"
confirm_delete: "Delete"
delete_confirmation: "Are you sure you want to delete this category? If you do, the plans associated with this category won't be sorted anymore."
category_deleted: "The category was successfully deleted"
unable_to_delete: "Unable to delete the category: "
#feature tour
tour:
conclusion: