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

(feat) add a custom banner for trainings

This commit is contained in:
Karen 2023-01-26 09:48:37 +01:00 committed by Sylvain
parent 06a93391a2
commit 0778616345
13 changed files with 356 additions and 204 deletions

View File

@ -13,7 +13,7 @@ import { User } from '../../models/user';
import { EditorialBlock } from '../editorial-block/editorial-block';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
import { SettingValue, machineBannerSettings } from '../../models/setting';
import { SettingValue, machinesSettings } from '../../models/setting';
declare const Application: IApplication;
@ -46,16 +46,7 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
const [banner, setBanner] = useState<Record<string, SettingValue>>({});
// fetch Banner text and button from API
const fetchBanner = async () => {
SettingAPI.query(machineBannerSettings)
.then(settings => {
setBanner({ ...SettingLib.bulkMapToObject(settings) });
})
.catch(onError);
};
// retrieve the full list of machines on component mount
// retrieve the full list of machines and the machines Banner on component mount
useEffect(() => {
MachineAPI.index()
.then(data => setAllMachines(data))
@ -63,7 +54,11 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
MachineCategoryAPI.index()
.then(data => setMachineCategories(data))
.catch(e => onError(e));
fetchBanner();
SettingAPI.query(machinesSettings)
.then(settings => {
setBanner({ ...SettingLib.bulkMapToObject(settings) });
})
.catch(onError);
}, []);
// filter the machines shown when the full list was retrieved

View File

@ -9,7 +9,7 @@ import { FabButton } from '../base/fab-button';
import { EditorialKeys, EditorialBlockForm } from '../editorial-block/editorial-block-form';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
import { SettingName, SettingValue, machineBannerSettings } from '../../models/setting';
import { SettingName, SettingValue, machinesSettings } from '../../models/setting';
declare const Application: IApplication;
@ -47,7 +47,7 @@ export const MachinesSettings: React.FC<MachinesSettingsProps> = ({ onError, onS
/** On component mount, fetch existing Machines Banner Settings from API, and populate form with these values. */
useEffect(() => {
SettingAPI.query(machineBannerSettings)
SettingAPI.query(machinesSettings)
.then(settings => reset(SettingLib.bulkMapToObject(settings)))
.catch(onError);
}, []);

View File

@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';
import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
import { Loader } from '../base/loader';
import { EditorialBlock } from '../editorial-block/editorial-block';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
import { SettingValue, trainingsSettings } from '../../models/setting';
declare const Application: IApplication;
interface TrainingEditorialBlockProps {
onError: (message: string) => void
}
/**
* This component displays to Users the editorial block (banner) associated to trainings.
*/
export const TrainingEditorialBlock: React.FC<TrainingEditorialBlockProps> = ({ onError }) => {
// Store Banner retrieved from API
const [banner, setBanner] = useState<Record<string, SettingValue>>({});
// Retrieve the settings related to the Trainings Banner from the API
useEffect(() => {
SettingAPI.query(trainingsSettings)
.then(settings => {
setBanner({ ...SettingLib.bulkMapToObject(settings) });
})
.catch(onError);
}, []);
return (
<>
{banner.trainings_banner_active &&
<EditorialBlock
text={banner.trainings_banner_text}
cta={banner.trainings_banner_cta_active && banner.trainings_banner_cta_label}
url={banner.trainings_banner_cta_active && banner.trainings_banner_cta_url} />
}
</>
);
};
const TrainingEditorialBlockWrapper: React.FC<TrainingEditorialBlockProps> = (props) => {
return (
<Loader>
<TrainingEditorialBlock {...props} />
</Loader>
);
};
Application.Components.component('trainingEditorialBlock', react2angular(TrainingEditorialBlockWrapper, ['onError']));

View File

@ -9,8 +9,8 @@ import { useForm, SubmitHandler, useWatch } from 'react-hook-form';
import { FormSwitch } from '../form/form-switch';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
import { EditorialBlockForm } from '../editorial-block/editorial-block-form';
import { SettingName, SettingValue, trainingSettings } from '../../models/setting';
import { EditorialKeys, EditorialBlockForm } from '../editorial-block/editorial-block-form';
import { SettingName, SettingValue, trainingsSettings } from '../../models/setting';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
@ -32,8 +32,17 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, o
const isActiveAuthorizationValidity = useWatch({ control, name: 'trainings_authorization_validity' }) as boolean;
const isActiveInvalidationRule = useWatch({ control, name: 'trainings_invalidation_rule' }) as boolean;
/** Link Trainings Banner Settings to generic keys expected by the Editorial Form */
const bannerKeys: Record<EditorialKeys, SettingName> = {
active_text_block: 'trainings_banner_active',
text_block: 'trainings_banner_text',
active_cta: 'trainings_banner_cta_active',
cta_label: 'trainings_banner_cta_label',
cta_url: 'trainings_banner_cta_url'
};
useEffect(() => {
SettingAPI.query(trainingSettings)
SettingAPI.query(trainingsSettings)
.then(settings => {
const data = SettingLib.bulkMapToObject(settings);
reset(data);
@ -63,6 +72,7 @@ export const TrainingsSettings: React.FC<TrainingsSettingsProps> = ({ onError, o
<EditorialBlockForm register={register}
control={control}
formState={formState}
keys={bannerKeys}
info={t('app.admin.trainings_settings.generic_text_block_info')} />
</div>

View File

@ -14,6 +14,10 @@ import type { Machine } from '../../models/machine';
import TrainingAPI from '../../api/training';
import MachineAPI from '../../api/machine';
import { EditDestroyButtons } from '../base/edit-destroy-buttons';
import { EditorialBlock } from '../editorial-block/editorial-block';
import { SettingValue, trainingsSettings } from '../../models/setting';
import SettingAPI from '../../api/setting';
import SettingLib from '../../lib/setting';
declare const Application: IApplication;
@ -31,6 +35,7 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
const [trainings, setTrainings] = useState<Array<Training>>([]);
const [machines, setMachines] = useState<Array<Machine>>([]);
const [filter, setFilter] = useState<boolean>(null);
const [banner, setBanner] = useState<Record<string, SettingValue>>({});
// Styles the React-select component
const customStyles = {
@ -45,7 +50,13 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
})
};
// At component mount, fetch Banner and Machines from API
useEffect(() => {
SettingAPI.query(trainingsSettings)
.then(settings => {
setBanner({ ...SettingLib.bulkMapToObject(settings) });
})
.catch(onError);
MachineAPI.index({ disabled: false })
.then(setMachines)
.catch(onError);
@ -114,6 +125,12 @@ export const Trainings: React.FC<TrainingsProps> = ({ onError, onSuccess }) => {
</div>
</header>
{banner.trainings_banner_active &&
<EditorialBlock
text={banner.trainings_banner_text}
cta={banner.trainings_banner_cta_active && banner.trainings_banner_cta_label}
url={banner.trainings_banner_cta_active && banner.trainings_banner_cta_url} />
}
<div className="trainings-content">
<div className='display'>
<div className='filter'>

View File

@ -17,8 +17,8 @@
/**
* Public listing of the trainings
*/
Application.Controllers.controller('TrainingsController', ['$scope', '$state', 'trainingsPromise',
function ($scope, $state, trainingsPromise) {
Application.Controllers.controller('TrainingsController', ['$scope', '$state', 'trainingsPromise', 'growl',
function ($scope, $state, trainingsPromise, growl) {
// List of trainings
$scope.trainings = trainingsPromise;
@ -31,6 +31,13 @@ Application.Controllers.controller('TrainingsController', ['$scope', '$state', '
* Callback for the 'show' button
*/
$scope.showTraining = function (training) { $state.go('app.public.training_show', { id: training.slug }); };
/**
* Callback triggered by react components
*/
$scope.onError = function (message) {
growl.error(message);
};
}]);
/**

View File

@ -233,7 +233,7 @@ export const storeSettings = [
'store_hidden'
] as const;
export const trainingSettings = [
export const trainingsSettings = [
'trainings_auto_cancel',
'trainings_auto_cancel_threshold',
'trainings_auto_cancel_deadline',
@ -241,9 +241,15 @@ export const trainingSettings = [
'trainings_authorization_validity_duration',
'trainings_invalidation_rule',
'trainings_invalidation_rule_period'
'trainings_auto_cancel_deadline',
'trainings_banner_active',
'trainings_banner_text',
'trainings_banner_cta_active',
'trainings_banner_cta_label',
'trainings_banner_cta_url'
] as const;
export const machineBannerSettings = [
export const machinesSettings = [
'machines_banner_active',
'machines_banner_text',
'machines_banner_cta_active',
@ -277,8 +283,8 @@ export const allSettings = [
...poymentSettings,
...displaySettings,
...storeSettings,
...trainingSettings,
...machineBannerSettings
...trainingsSettings,
...machinesSettings
] as const;
export type SettingName = typeof allSettings[number];

View File

@ -16,7 +16,7 @@
<section class="m-lg">
<editorial-block></editorial-block>
<training-editorial-block on-error="onError"></training-editorial-block>
<div class="row" ng-repeat="training in (trainings.length/3 | array)">

View File

@ -0,0 +1,193 @@
# frozen_string_literal: true
# Helpers methods listing all the settings used in setting.rb
# The following list contains all the settings that can be customized from the Fab-manager's UI.
# A few of them that are system settings, that should not be updated manually (uuid, origin...).
# rubocop:disable Metrics/ModuleLength
module SettingsHelper
# WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName
# - db/seeds.rb (to set the default value)
# - app/policies/setting_policy.rb#public_whitelist (if the setting can be read by anyone)
# - test/fixtures/settings.yml (for backend testing)
# - test/fixtures/history_values.yml (example value for backend testing)
# - test/frontend/__fixtures__/settings.ts (example value for frontend testing)
SETTINGS = %w[
about_title
about_body
about_contacts
privacy_draft
privacy_body
privacy_dpo
twitter_name
home_blogpost
machine_explications_alert
training_explications_alert
training_information_message
subscription_explications_alert
invoice_logo
invoice_reference
invoice_code-active
invoice_code-value
invoice_order-nb
invoice_VAT-active
invoice_VAT-rate
invoice_VAT-rate_Machine
invoice_VAT-rate_Training
invoice_VAT-rate_Space
invoice_VAT-rate_Event
invoice_VAT-rate_Subscription
invoice_VAT-rate_Product
invoice_text
invoice_legals
booking_window_start
booking_window_end
booking_move_enable
booking_move_delay
booking_cancel_enable
booking_cancel_delay
main_color
secondary_color
fablab_name
name_genre
reminder_enable
reminder_delay
event_explications_alert
space_explications_alert
visibility_yearly
visibility_others
reservation_deadline
display_name_enable
machines_sort_by
accounting_sales_journal_code
accounting_payment_card_code
accounting_payment_card_label
accounting_payment_card_journal_code
accounting_payment_wallet_code
accounting_payment_wallet_label
accounting_payment_wallet_journal_code
accounting_payment_other_code
accounting_payment_other_label
accounting_payment_other_journal_code
accounting_wallet_code
accounting_wallet_label
accounting_wallet_journal_code
accounting_VAT_code
accounting_VAT_label
accounting_VAT_journal_code
accounting_subscription_code
accounting_subscription_label
accounting_Machine_code
accounting_Machine_label
accounting_Training_code
accounting_Training_label
accounting_Event_code
accounting_Event_label
accounting_Space_code
accounting_Space_label
accounting_Product_code
accounting_Product_label
hub_last_version
hub_public_key
fab_analytics
link_name
home_content
home_css
origin
uuid
phone_required
tracking_id
book_overlapping_slots
slot_duration
events_in_calendar
spaces_module
plans_module
invoicing_module
facebook_app_id
twitter_analytics
recaptcha_site_key
recaptcha_secret_key
feature_tour_display
email_from
disqus_shortname
allowed_cad_extensions
allowed_cad_mime_types
openlab_app_id
openlab_app_secret
openlab_default
online_payment_module
stripe_public_key
stripe_secret_key
stripe_currency
invoice_prefix
confirmation_required
wallet_module
statistics_module
upcoming_events_shown
payment_schedule_prefix
trainings_module
address_required
accounting_Error_code
accounting_Error_label
payment_gateway
payzen_username
payzen_password
payzen_endpoint
payzen_public_key
payzen_hmac
payzen_currency
public_agenda_module
renew_pack_threshold
pack_only_for_subscription
overlapping_categories
extended_prices_in_same_day
public_registrations
accounting_Pack_code
accounting_Pack_label
facebook
twitter
viadeo
linkedin
instagram
youtube
vimeo
dailymotion
github
echosciences
pinterest
lastfm
flickr
machines_module
user_change_group
user_validation_required
user_validation_required_list
show_username_in_admin_list
store_module
store_withdrawal_instructions
store_hidden
advanced_accounting
external_id
prevent_invoices_zero
invoice_VAT-name
trainings_auto_cancel
trainings_auto_cancel_threshold
trainings_auto_cancel_deadline
trainings_authorization_validity
trainings_authorization_validity_duration
trainings_invalidation_rule
trainings_invalidation_rule_period
machines_banner_active
machines_banner_text
machines_banner_cta_active
machines_banner_cta_label
machines_banner_cta_url
trainings_banner_active
trainings_banner_text
trainings_banner_cta_active
trainings_banner_cta_label
trainings_banner_cta_url
].freeze
end
# rubocop:enable Metrics/ModuleLength

View File

@ -6,186 +6,12 @@
# A full history of the previous values is kept in database with the date and the author of the change
# after_update callback is handled by SettingService
class Setting < ApplicationRecord
include SettingsHelper
has_many :history_values, dependent: :destroy
# The following list contains all the settings that can be customized from the Fab-manager's UI.
# A few of them that are system settings, that should not be updated manually (uuid, origin...).
validates :name, inclusion:
{ in: %w[about_title
about_body
about_contacts
privacy_draft
privacy_body
privacy_dpo
twitter_name
home_blogpost
machine_explications_alert
training_explications_alert
training_information_message
subscription_explications_alert
invoice_logo
invoice_reference
invoice_code-active
invoice_code-value
invoice_order-nb
invoice_VAT-active
invoice_VAT-rate
invoice_VAT-rate_Machine
invoice_VAT-rate_Training
invoice_VAT-rate_Space
invoice_VAT-rate_Event
invoice_VAT-rate_Subscription
invoice_VAT-rate_Product
invoice_text
invoice_legals
booking_window_start
booking_window_end
booking_move_enable
booking_move_delay
booking_cancel_enable
booking_cancel_delay
main_color
secondary_color
fablab_name
name_genre
reminder_enable
reminder_delay
event_explications_alert
space_explications_alert
visibility_yearly
visibility_others
reservation_deadline
display_name_enable
machines_sort_by
accounting_sales_journal_code
accounting_payment_card_code
accounting_payment_card_label
accounting_payment_card_journal_code
accounting_payment_wallet_code
accounting_payment_wallet_label
accounting_payment_wallet_journal_code
accounting_payment_other_code
accounting_payment_other_label
accounting_payment_other_journal_code
accounting_wallet_code
accounting_wallet_label
accounting_wallet_journal_code
accounting_VAT_code
accounting_VAT_label
accounting_VAT_journal_code
accounting_subscription_code
accounting_subscription_label
accounting_Machine_code
accounting_Machine_label
accounting_Training_code
accounting_Training_label
accounting_Event_code
accounting_Event_label
accounting_Space_code
accounting_Space_label
accounting_Product_code
accounting_Product_label
hub_last_version
hub_public_key
fab_analytics
link_name
home_content
home_css
origin
uuid
phone_required
tracking_id
book_overlapping_slots
slot_duration
events_in_calendar
spaces_module
plans_module
invoicing_module
facebook_app_id
twitter_analytics
recaptcha_site_key
recaptcha_secret_key
feature_tour_display
email_from
disqus_shortname
allowed_cad_extensions
allowed_cad_mime_types
openlab_app_id
openlab_app_secret
openlab_default
online_payment_module
stripe_public_key
stripe_secret_key
stripe_currency
invoice_prefix
confirmation_required
wallet_module
statistics_module
upcoming_events_shown
payment_schedule_prefix
trainings_module
address_required
accounting_Error_code
accounting_Error_label
payment_gateway
payzen_username
payzen_password
payzen_endpoint
payzen_public_key
payzen_hmac
payzen_currency
public_agenda_module
renew_pack_threshold
pack_only_for_subscription
overlapping_categories
extended_prices_in_same_day
public_registrations
accounting_Pack_code
accounting_Pack_label
facebook
twitter
viadeo
linkedin
instagram
youtube
vimeo
dailymotion
github
echosciences
pinterest
lastfm
flickr
machines_module
user_change_group
user_validation_required
user_validation_required_list
show_username_in_admin_list
store_module
store_withdrawal_instructions
store_hidden
advanced_accounting
external_id
prevent_invoices_zero
invoice_VAT-name
trainings_auto_cancel
trainings_auto_cancel_threshold
trainings_auto_cancel_deadline
trainings_authorization_validity
trainings_authorization_validity_duration
trainings_invalidation_rule
trainings_invalidation_rule_period
machines_banner_active
machines_banner_text
machines_banner_cta_active
machines_banner_cta_label
machines_banner_cta_url] }
# WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName
# - db/seeds/settings.rb (to set the default value)
# - app/policies/setting_policy.rb#public_whitelist (if the setting can be read by anyone)
# - test/fixtures/settings.yml (for backend testing)
# - test/fixtures/history_values.yml (example value for backend testing)
# - test/frontend/__fixtures__/settings.ts (example value for frontend testing)
# The full list of settings is declared in SettingsHelper
validates :name, inclusion: { in: SETTINGS }
def value
last_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(1).first

View File

@ -43,7 +43,9 @@ class SettingPolicy < ApplicationPolicy
pack_only_for_subscription overlapping_categories public_registrations facebook twitter viadeo linkedin instagram
youtube vimeo dailymotion github echosciences pinterest lastfm flickr machines_module user_change_group
user_validation_required user_validation_required_list store_module store_withdrawal_instructions store_hidden
external_id machines_banner_active machines_banner_text machines_banner_cta_active machines_banner_cta_label machines_banner_cta_url]
external_id machines_banner_active machines_banner_text machines_banner_cta_active machines_banner_cta_label
machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label
trainings_banner_cta_url]
end
##

View File

@ -777,6 +777,36 @@ export const settings: Array<Setting> = [
value: 'https://www.sleede.com/',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Url'
},
{
name: 'trainings_banner_active',
value: 'true',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Custom banner is active'
},
{
name: 'trainings_banner_text',
value: 'Test for Banner Content in Trainings',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Text of the custom banner'
},
{
name: 'trainings_banner_cta_active',
value: 'true',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Custom banner has a button'
},
{
name: 'trainings_banner_cta_label',
value: 'Test for Banner Button in Trainings',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Label'
},
{
name: 'trainings_banner_cta_url',
value: 'https://www.sleede.com/',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Url'
}
];

View File

@ -0,0 +1,14 @@
import { render, screen, waitFor } from '@testing-library/react';
import { TrainingEditorialBlock } from '../../../../app/frontend/src/javascript/components/trainings/training-editorial-block';
// Trainings Editorial Block
describe('Trainings Editorial Block', () => {
test('should render a banner', async () => {
const onError = jest.fn();
render(<TrainingEditorialBlock onError={onError} />);
await waitFor(() => screen.getByText('Test for Banner Content in Trainings'));
await waitFor(() => screen.getByText('Test for Banner Button in Trainings'));
});
});