Add Edit user Modal in Open Event Frontend

This blog article will illustrate how the UI for the edit user modal is implemented  and Users API has been integrated into it in Open Event Frontend.

The admin can make any user the admin, sales admin or the marketer of the app. In the route admin/users there is an ember table where all the users are listed. In the table there exists a column named ‘Action Buttons’.

When the edit action button is clicked a modal appears on the screen. Them template of the modal is as follows:

class="content">
class="ui form">

class="ui header">{{t 'Provide admin access?'}}

class="grouped inline fields">
class="field"> {{ui-radio name="isAdmin" label="Yes" value=true onChange=(action (mut isAdmin))}} {{ui-radio name="isAdmin" label="No" value=false onChange=(action (mut isAdmin))}}
</div> <h4 class="ui header">{{t 'Custom system roles'}}</h4>
class="field"> {{ui-checkbox label="Sales Admin" onChange=(action (mut checked))}} {{ui-checkbox label="Marketer" onChange=(action (mut checked))}}
<button class="ui teal right floated submit button update-changes"> {{t 'Save'}} </button> </div> </div>

For the API integration the users model is used. The attributes isAdmin, isSalesAdmin, isMarketer from the model are used to send a patch request to the server. The modal has basically to parts. The first part consists of radio buttons through which the super admin has the rights to create a user an admin of the app or to remove his role as the admin. The second part consists of checkboxes through which the user can get the custom system role to be the sales admin or the marketer. A get request is sent to the user’s model in the server and the initial values of the modal are decided.

If the admin changes some value, he clicks on the save button in the modal and a patch request is sent to the server. The save function is written in the modal’s component.

actions: {
saveRole(id) {
this.get('store').findRecord('user', id).then(function(user) {
user.save();
});
this.set('isOpen', false);
},
toggleSalesAdmin(user) {
user.toggleProperty('isSalesAdmin');
},
toggleMarketer(user) {
user.toggleProperty('isMarketer');
},
createAdmin(user, isAdmin) {
user.set('isAdmin', isAdmin);
}
}

Resources

Adding the User Settings Route in Admin User Route Open Event Frontend

This blog article will illustrate how the User settings API has been integrated into the admin users route  Open Event Frontend. The admin can change the contact info of some user, details about the email preferences for different events created by the user and the third party authentication details entered by the user.

To make the settings user link in the user link column of the users table functional new sub routes are added to the app’s user route as follows:

  • /admin/users/<user_id>/settings/contact-info
  • /admin/users/<user_id>/settings/email-preferences
  • /admin/users/<user_id>/settings/applications

The template for the index route which redirects to each of the settings route is:

class="ui grid">
class="row">
class="twelve wide column"> {{#tabbed-navigation}} {{#link-to 'admin.users.view.settings.contact-info' model.user.id class='item'}} {{t 'Contact Info'}} {{/link-to}} {{#link-to 'admin.users.view.settings.email-preferences' model.user.id class='item'}} {{t 'Email-Preferences'}} {{/link-to}} {{#link-to 'admin.users.view.settings.applications' model.user.id class='item'}} {{t 'Applications'}} {{/link-to}} {{/tabbed-navigation}}
</div> {{outlet}} </div>

Interestingly, the routes admin/users/view and admin/users/list are both dynamic and expect a parameter after /users/ hence, the app cannot distinguish between them on it’s own, thus explicit handling of the dynamic parameter of the routes was implemented, differentiating them on the basis of the route’s state as follows:

beforeModel(transition) {
this._super(...arguments);
const userState = transition.params[transition.targetName].users_status;
if (!['all', 'deleted', 'active'].includes(userState)) {
this.replaceWith('admin.users.view', userState);
}
}

Thus if the dynamic portion of the route doesn’t contain the parameters all, deleted or active, then it must be referring to a user’s events or sessions and the route needs to be replaced with the desired events or sessions route accordingly.

The server is queried to fetch the details of a given user like the email,  contact, various events created by the user to get the email and notification preferences. For getting each detail the current users model is returned and the values in the model are returned to the form.

For the contact-info sub route the values like the email and the contact number are fetched and are shown in the form. There is a save button in the form too. The admin can change this information and send a patch request to the server by clicking this button.

 updateContactInfo() {
this.set('isLoading', true);
let currentUser = this.get('model.user');
currentUser.save()
.then(() => {
this.get('notify').success(this.get('l10n').t('Your Contact Info has been updated'));
})
.catch(() => {
this.get('notify').error(this.get('l10n').t('An unexpected error occurred'));
})
.finally(() => {
this.set('isLoading', false);
});
}

For the email-preferences sub route the model has attributes like sessionSchedule, nextEvent etc.and the admin has the access to change the email-notifications for any event  created by any user. The client side has checkboxes to show the data to the user. The states of the checkboxes are determined by the data that we receive from the API. We also let the admin change the preferences of the email-notifications so that he can customise the notifications and keep the ones he wants some user to receive.

{{settings/email-preferences-section preferences=model}}

The sub route for email preferences:

export default Route.extend(AuthenticatedRouteMixin, {
titleToken() {
return this.get('l10n').t('Email Preferences');
},
model() {
const currentUser = this.modelFor('admin.users.view');
return currentUser.query('emailNotifications', { include: 'event' });
}
});

So, the admin has the access to change the information and the email notifications of a user.

Resources

Events API Integration on Admin User Route Open Event Frontend

This blog article will illustrate how the Events API has been integrated into the admin users route  Open Event Frontend, as well as how the action buttons are added to view, edit or delete the events of any user in the list by the admin.

To make the events user link in the user link column of the users table functional a new sub route is added to the app’s user route as follows:

this.route('users', function() {
     this.route('view', { path: '/:user_id' }, function() {
       this.route('events', function() {
         this.route('list', { path: '/:event_status' });
       });
     });

The newly added route further contains a dynamic sub route called list. This nested route fulfills the requirement of filtering the various events of a given user according to their states. Interestingly, the routes admin/users/view and admin/users/list are both dynamic and expect a parameter after /users/ hence, the app cannot distinguish between them on it’s own, thus explicit handling of the dynamic parameter of the routes was implemented, differentiating them on the basis of the route’s state as follows:

beforeModel(transition) {
this._super(...arguments);
const userState = transition.params[transition.targetName].users_status;
if (!['all', 'deleted', 'active'].includes(userState)) {
this.replaceWith('admin.users.view', userState);
}
}

Thus if the dynamic portion of the route doesn’t contain the parameters all, deleted or active, then it must be referring to a user’s events or sessions and the route needs to be replaced with the desired events or sessions route accordingly.

The server is queried to fetch the events of a given user by making use of the hasMany relationship a user has with his sessions. They are loaded in the route admin/users/view/events/list.js

model() {
const userDetails = this.modelFor('admin.users.view');
return this.store.findRecord('user', userDetails.id, {
include: 'events'
});

After fetching the the events from the server, a proper ember table is called in the template file of this route, and all the actions like viewing and editing an event are declared in the template.

{{events/events-table
columns=columns data=model.events
useNumericPagination=true
moveToDetails=(action 'moveToDetails')
editEvent=(action 'editEvent')
openDeleteEventModal=(action 'openDeleteEventModal')
}}

In the controller the columns of the table for events are defined and all the actions are defined.

moveToDetails(id) {
this.transitionToRoute('events.view', id);
},
editEvent(id) {
this.transitionToRoute('events.view.edit.basic-details', id);
},
deleteEvent() {
this.set('isLoading', true);
this.store.findRecord('event', this.get('eventId'), { backgroundReload: false }).then(function(event) {
event.destroyRecord();
})

So, the admin can view the list of the events of a particular user and send a patch or delete request for any event.

Resources

Adding a list view for the Sessions Public Page in Open Event Frontend

This blog article will describe how the sessions are listed in the public pages of an event in Open Event Frontend, which allows the user to view all the sessions of an event. The sessions are filtered as per date. The primary end point of Open Event API with which we are concerned with for fetching the the users details is GET /v1/events/{event_identifier}/sessions

The route of the public page fetches all the sessions of a particular events and filters them as per the criteria selected by the user. The user can view the sessions of a particular day, week or month. The user can also view the list of all the sessions. The query written in the route is:

async model(params) {
   const eventDetails = this.modelFor('public');
   let sessions =  null;
   if (params.session_status === 'today') {
     sessions = await this.get('store').query('session', {
       filter: [
         {
           and: [
             {
               name : 'event',
               op : 'has',
               val : {
                 name : 'identifier',
                 op : 'eq',
                 val : eventDetails.id
               }
             },
             {
               name : 'starts-at',
               op : 'ge',
               val : moment().startOf('day').toISOString()
             },
             {
               name : 'starts-at',
               op : 'lt',
               val : moment().endOf('day').toISOString()
             }
           ]
         }
       ]
     });
   } else {
     sessions = await this.get('store').query('session', {
       filter: [
         {
           name : 'event',
           op : 'has',
           val : {
             name : 'identifier',
             op : 'eq',
             val : eventDetails.id
           }
         }
       ]
     });
   }
   return {
     event  : eventDetails,
     session : sessions
   };
 }

The view route is located at app/e/{event_identifier}/sessions/all. This route will show all the sessions of the selected event. Similarly /week will show the sessions of a week and /month will show the sessions of a month.Four joint buttons are used in the UI of the public page to redirect to these routes.

To list the sessions ember component of session cards is used to include a session in a card with the details of the session like the time, abstract etc and also the session’s track and the details of the speakers like the name, information and social media accounts. In the template of the route this component is called and used in the UI within an ember component. In case there are no sessions that exist between a given time period, a helper text is displayed stating “No sessions exist for the given period”.

class="ui buttons"> {{#link-to 'public.sessions.list' model.event.id 'all' class="ui button"}}{{t 'All'}}{{/link-to}} {{#link-to 'public.sessions.list' model.event.id 'today' class="ui button"}}{{t 'Today'}}{{/link-to}} {{#link-to 'public.sessions.list' model.event.id 'week' class="ui button"}}{{t 'Week'}}{{/link-to}} {{#link-to 'public.sessions.list' model.event.id 'month' class="ui button"}}{{t 'Month'}}{{/link-to}}
class="ui raised very padded text container segment"> {{#each model.session as |session|}} {{public/session-item session=session}} {{else}}
class="ui disabled header">{{t 'No Sessions exist for this time period'}}
{{/each}} </div>

Resources

Add Routes to Add and Edit multiple Sessions in the CFS section of Open Event Frontend

This blog article will describe how the users can add multiple session proposals and edit them through the Call for Speakers modal in Open Event Frontend. The logged in user first adds himself as the speaker through the modal then he can add multiple sessions. The user will be added as the speaker for all the sessions he adds through the CFS modal.

To submit the sessions the user first has to add himself as a speaker of that event in the route:
e/{event_identifier}/cfs

After the user registers himself as the speaker of that event whose Call for Speakers is open he can add multiple sessions and also edit those sessions.

When Add Session Details button is clicked the user gets redirected to a form with the route

e/{event_identifier}/cfs/new-session.

async model() {
const eventDetails = this.modelFor('public');
return {
  event : eventDetails,
  forms : await eventDetails.query('customForms', {
    sort        : 'id',
    'page[size]' : 50
  }),
  session: await this.get('store').createRecord('session', {
    event   : eventDetails,
    creator : this.get('authManager.currentUser')
  }),
  tracks       : await eventDetails.query('tracks', {}),
  sessionTypes : await eventDetails.query('sessionTypes', {})
}

On this route there is a session form where the user can add details like title, short abstract, comments, track etc. Once he clicks on the save button after entering the details post request is sent to the server and that session is added to the list of sessions of that event and the user is added as the speaker of that session.

class="ui container"> {{#if speaker.id}} {{forms/session-speaker-form fields=model.forms data=model isLoading=isLoading save=(action 'save' speaker) isSession=true includeSession=true}} {{/if}}

The user can add another session or edit the sessions previously entered by him. When Edit session is clicked the user gets redirected to the route e/{event_identifier}/cfs/edit/{session_id}

async model(params) {
const eventDetails = this.modelFor('public');
return {
event : eventDetails,
forms : await eventDetails.query('customForms', {
sort        : 'id',
'page[size]' : 50
}),
session: await this.get('store').findRecord('session', params.session_id, {
include: 'session-type,track'
})
};
}

On this route the user can change the details of the session he had entered before. On clicking save a patch request is sent to the server and the new details are saved.

Resources

Filtering and Session API Integration on Admin User Route Open Event Frontend

This blog article will illustrate how the Session API has been integrated into the admin users route  Open Event Frontend, as well as how it’s now possible to filter active and deleted users using the new filters implemented.

To make the sessions buttons on the users table functional a new sub route is added to the app’s user route as follows:

this.route('users', function() {
     this.route('view', { path: '/:user_id' }, function() {
       this.route('sessions', function() {
         this.route('list', { path: '/:session_status' });
       });
     });
     this.route('list', { path: '/:users_status' });

The newly added route further contains a dynamic sub route called list. This nested route fulfills the requirement of filtering the various sessions of a given user according to their states. Interestingly, the routes admin/users/view and admin/users/list are both dynamic and expect a parameter after /users/ hence, the app cannot distinguish between them on it’s own, thus explicit handling of the dynamic parameter of the routes was implemented, differentiating them on the basis of the route’s state as follows:

beforeModel(transition) {
this._super(...arguments);
const userState = transition.params[transition.targetName].users_status;
if (!['all', 'deleted', 'active'].includes(userState)) {
this.replaceWith('admin.users.view', userState);
}
}

Thus if the dynamic portion of the route doesn’t contain the parameters all, deleted or active, then it must be referring to a user’s sessions and the route needs to be replaced with the desired sessions route accordingly. Also, the template admin/users.hbs needs to be changed to display the navigation bar  only when required. It is efficiently handled by an IF condition as follows:

{{#if (and (not-includes session.currentRouteName ‘admin.users.user’) (not-includes session.currentRouteName ‘admin.users.view.sessions.list’))}}

The server is queried to fetch the sessions of a given user by making use of the hasMany relationship a user has with his sessions. They are loaded in the route admin/users/view/sessions/list.js

model() {
const userDetails = this.modelFor('admin.users.view');
return this.store.findRecord('user', userDetails.id, {
include: 'sessions'
});

After fetching the the sessions from the server, the existing session-card  component is reused in the route’s template to display the sessions.

{{#if model.sessions}}
{{#each model.sessions as |session|}}
{{session-card session=session}}

{{/each}}
{{else}}

{{t ‘No session proposals found for the events’}}

{{/if}}
</div>
</div>

Also, in the admin/users route the filtering of deleted users was not functional. Thus  the property deleted-at of the users model which stores the timestamp of the deletion of a user was utilised. deleted-at is null for a user which is active. Hence the active and deleted users can be filtered as :

if (params.users_status === 'active') {
filterOptions = [
{
name : 'deleted-at',
op : 'eq',
val  : null
}
];
} else if (params.users_status === 'deleted') {
filterOptions = [
{
name : 'deleted-at',
op : 'ne',
val  : null
}
];
}
return this.get('store').query('user', {
get_trashed  : true,
filter       : filterOptions,
'page[size]' : 10
});

It’s important to pass the get_trashed parameter as true in the query as the the deleted user records are actually soft deleted records and will be fetched only when explicitly queried for.

Resources

Implementing Event Image Size and Speaker Image Size APIs in Open Event Frontend

This blog article will illustrate how the Image Sizes APIs concerning event and speaker images are integrated in  Open Event Frontend, which allows for dynamic configurations of storing speaker and event images. The primary end points of Open Event API with which we are concerned with for fetching the event and speaker image sizes are

GET /v1/event-image-sizes

And

GET /v1/speaker-image-sizes

These endpoints are accessible only to a user with has administrator privileges as the customisation of image sizes is possible only on the admin dashboard. The image sizes are independent in regards to relationships and don’t have any related fields.

The model for the admin image settings route is defined as follows:

export default ModelBase.extend(CustomPrimaryKeyMixin, {
 thumbnailSizeQuality     : attr('number'),
 type                     : attr('string'),
 smallSizeWidthHeight     : attr('number'),
 smallSizeQuality         : attr('number'),
 iconSizeQuality          : attr('number'),
 iconSizeWidthHeight      : attr('number'),
 thumbnailSizeWidthHeight : attr('number')
});

The form which allows user to select image sizes, is in a separate component, and initially both the speaker and event image sizes are passed onto the component as a part of the entire model, so they can be separated later as per the requirement.

{{forms/admin/settings/images-form image=model save=’saveImages’ isLoading=isLoading}}

Most of the fields specify the units in which the numerical input concerning the image dimensions will be interpreted by the server and standard min and max validations are applied to the fields to ensure genuine and legitimate values can pass through the frontend.

<h3 class=”ui header”>{{t ‘Large Size’}}</h3>


{{input type=’number’ name=’large_width’ value=image.eventImageSize.fullWidth min=1}}

{{input type=’number’ name=’large_height’ value=image.eventImageSize.fullHeight min=1}}

{{input type=’number’ name=’large_quality’ value=image.eventImageSize.fullQuality min=1}}

</div>

{{ui-checkbox label=(t ‘Standard aspect ratio is 13:5. Tick to maintain aspect ratio.’) class=’checkbox’ name=’large_ratio’ checked=image.eventImageSize.fullAspect onChange=(action (mut image.eventImageSize.fullAspect))}}

{{t ‘Standard Size of the available area is 1300px X 500px’}}
<p>{{t ‘Found in :’}}</p>

{{t ‘Background Header Image in Public Event Page’}}

</div>

Furthermore, to ensure a user does not accidentally change the values inside the form, an action is triggered while transitioning away from the route which rollbacks any unsaved changes to the image sizes.

actions: {
willTransition() {
this.get('controller.model').forEach(image => {
image.rollbackAttributes();
});
}
}

Resources

Creating the View Route for Users in Open Event Frontend

This blog article will describe how the users view user route is created in Open Event Frontend, which allows the admin to view a user whenever view action button in the user table is clicked.

The primary end point of Open Event API with which we are concerned with for fetching the the users details is

GET /v1/users/{user_id}

The complete user information on the view route is displayed, which includes the name, email, details about the user. All these are the attributes of the model user. Thus the model for the route is defined as follows:

model(params) {
return this.store.findRecord('user', params.session_id);

The view route is located at app/admin/users/<user_id> and the parent route, app/users has another sub route within it called list. The list route shows all, active, deleted users. This list has a column of action buttons.

This list can only be accessed by the admin. Whenever the view button in the Actions column is clicked the admin gets redirected to the view users route.

actions: {
    moveToUserDetails(id) {
      this.transitionToRoute('admin.users.view', id);
    }
}

The user profile form is:

{{widgets/forms/image-upload
imageUrl=user.avatarUrl
needsCropper=true
label=(t ‘User Image’)
id=’user_image’
icon=’photo’
hint=(t ‘Select User Image’)
maxSizeInKb=10000
aspectRatio=(array 1 1)
helpText=(t ‘We recommend using at least a 1000x1000px (1:1 ratio) image’)}}


{{input type=’text’ id=’name’ value=user.firstName}}

{{input type=’text’ id=’last_name’ value=user.lastName}}

{{widgets/forms/rich-text-editor id=’details’ value=user.details}}

The view route shows the following information about the user: Name, Family name, Email, Image of the user, Details of the user. Thus the admin can view all the users registered in the application.

Resources

Adding Sessions and Events Statistics in the Admin Dashboard in Open Event Frontend

This blog article will illustrate how the admin statistics API for events and sessions is integrated and how the values of different types of sessions and events is added to the admin dashboard in   Open Event Frontend.

The primary end point of Open Event API with which we are concerned with for fetching the statistics are

GET /v1/admin/statistics/events

GET /v1/admin/statistics/events

import Route from '@ember/routing/route';

export default Route.extend({
 async model() {
   return {
     events: await this.get('store').queryRecord('admin-statistics-event', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     })
sessions: await this.get('store').queryRecord('admin-statistics-session', {
       filter: {
         name : 'id',
         op   : 'eq',
         val  : 1
       }
     })
   };
 }
});

The route file helps to fetch the total count of each type of session and event through the queries written in the code. queryRecord is used instead of query because a single record is expected to be returned by the API. The view route is /admin.

The model needs to extend the base model class and all the attributes of the model will be number since the all the data obtained via these models from the API will be numerical statistics.

For Events:

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';export default ModelBase.extend({
draft     : attr('number'),
published : attr('number'),
past      : attr('number')
});

For Sessions:

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';export default ModelBase.extend({
confirmed : attr('number'),
accepted  : attr('number'),
submitted : attr('number'),
draft     : attr('number'),
rejected  : attr('number'),
pending   : attr('number')
});

Once we retrieve the values of the attributes from the queries written in the route file we display the values of pending, rejected, accepted sessions and published, draft, past events.

class="label"> {{t 'Accepted'}}
class="value">
class="ui teal label"> {{model.sessions.accepted}}
</div> </div>
class="ui small statistic">
class="label"> {{t 'Draft'}}
class="value">
class="ui yellow label"> {{model.sessions.pending}}
</div> </div>
class="ui small statistic">
class="label"> {{t 'Rejected'}}
class="value">
class="ui red label"> {{model.sessions.rejected}}

Resources

Creating the View Route for Sessions in Open Event Frontend

This blog article will illustrate how the creation of the view route for sessions is done and how the sessions API is integrated with it on Open Event Frontend, which allows for the sessions and their associated data to be displayed. Also, it illustrates how the template for my-sessions is modified to make it possible to reuse it for the view route with desired changes.

The primary end point of Open Event API with which we are concerned with for fetching the the session details is

GET /v1/sessions/{session_identifier}

For displaying the complete session information on the view route, the session type,  speakers and session track are also required. All of these extra attributes have a relationship with a given session and hence can be fetched in a single query. Thus the model for the route is defined as follows:

model(params) {
return this.store.findRecord(‘session’, params.session_id, {
include: ‘session-type,speakers,track’
});

The view route is located at app/routes/my-sessions/view and the parent route, app/routes/my-sessions has another sub route within it called list. The list route shows upcoming and past sessions to the user based on the params passed to it. Thus a navigation is required to alternate between those two routes. However, this navigation should not be present in the view route. Thus the template my-sessions.hbs is modified as follows:

{{#if (and (not-includes session.currentRouteName ‘my-sessions.view’))}}
<h1 class=”ui header”>{{t ‘My Sessions’}}</h1>

{{#tabbed-navigation}}
{{#link-to ‘my-sessions.list’ ‘upcoming’ class=’item’}}
{{t ‘Upcomming Sessions’}}
{{/link-to}}
{{#link-to ‘my-sessions.list’ ‘past’ class=’item’}}
{{t ‘Past Sessions’}}
{{/link-to}}
{{/tabbed-navigation}}

</div>
{{outlet}}
</div>
{{else}}
{{outlet}}
{{/if}}

The session.currentRouteName property allows conditional rendering of the navigation component.

Finally the template for the view route is created:

   

{{#if (eq model.state ‘accepted’)}}

{{t ‘Accepted’}}

{{else if (eq model.state ‘pending’)}}

{{t ‘Pending’}}

{{else if (eq model.state ‘rejected’)}}

{{t ‘Rejected’}}

{{/if}}
</div>

{{t ‘From ‘}}{{moment-format model.startAt ‘ddd, MMM DD HH:mm A’}}{{t ‘ to ‘}}{{moment-format model.endsAt ‘ddd, MMM DD HH:mm A’}}

</div>

{{#if model.shortAbstract}}
<p> <i>{{model.shortAbstract}}</i> </p>
{{/if}}
{{#if model.sessionType.name}}
<h3 class=”ui left aligned header”>Session Type</h3>
<p>{{model.sessionType.name}}</p>
{{/if}}
{{#if model.track.name}}
<h3 class=”ui left aligned header”>Track</h3>
<p>{{model.track.name}}</p>
{{/if}}
{{#if model.slidesUrl}}
<h3 class=”ui left aligned header”>Slide</h3>
<a href=”{{model.slidesUrl}}”>{{t ‘Download’}}</a>
{{/if}}
</div>

Based on the state property of the session, the label displaying the status is appropriately coloured wherein green, yellow and red colours denote accepted, pending and rejected sessions respectively. Since most of the fields of the session model are optional in nature, all of the them are subjected to conditional checks for existence.

Resources