Adding Panel to Add Event Types in Admin Dashboard of Open Event Frontend

This blog will illustrate how to add a new section to admin dashboard of Open Event Frontend which allows admin to add event types. For this we need modals to display a form by which we can edit or add a new event type and we need to create a new route admin/content/events. To create a new route we use ember CLI command:

ember g route admin/content/events

The primary end point of Open Event API with which we are concerned with for creating a new event type or topic is:

GET/POST/DELETE        /v1/event-types

The model concerned with event types is:

 name : attr('string'),
 slug : attr('string'),

 events: hasMany('event')

 

This model is very basic and contains only name and slug and a relationship to event model. Next we want to fetch the existing event types and display them in table. We write queries which fetches data in event-type model in the route file admin/content/events.js.

import Route from '@ember/routing/route';
export default Route.extend({
 titleToken() {
   return this.get('l10n').t('Social Links');
 },
 async model() {
   return {
     'eventTopics': await this.get('store').query('event-topic', {}),
     'eventTypes': await this.get('store').query('event-type', {})
   };
 }
});

 

This will fetch the data in our model. Next we need to display this data in a template for which we define a table that will display each event type.

<button class="ui blue button {{if device.isMobile 'fluid'}}" {{action 'openNewEventTypeModal'}}>{{t 'Add New Event Type'}}</button> 

      <table class="ui celled table">
         <tbody>
           {{#each model.eventTypes as |eventType|}}
             <tr>
               <td>
                 {{eventType.name}}
               </td>
             </tr>
           {{/each}}
         </tbody>
       </table>

 

We have two buttons that are used to edit or delete a event type. Both buttons open up a modal to achieve this functionality. We also have a “Add new Event Type” button at the top. This buttons opens up a modal and sends out a action to its controller when user successfully fills up the name of the event type. Let us take a look at the code of our controller that saves/deletes our event type to server.

addEventType() {
     this.set('isLoading', true);
     this.get('eventType').save()
       .then(() => {
        // Success message
       })
       .catch(()=> {
        //failure message
       })
       .finally(() => {
         this.set('isLoading', false);
       });
   }

 

deleteEventType(eventType) {
     this.set('isLoading', true);
     eventType.destroyRecord()
       .then(() => {
        // Success
       })
       .catch(()=> {
        //failure
       })
       .finally(() => {
         this.set('isLoading', false);
       });
   }

 

In addNewEventType() function we take the data from the form and save the model, which eventually sends POST request to save the new Event Type on server. This returns a JavaScript promise and we handle it via then and catch. It goes to then block if promise resolves and goes to catch is promise rejects/fails.

Similarly in delete function we take the eventType which is passed as model of event-type object and call destroyRecord() function which eventually sends out a DELETE request to server and data gets deleted. Here also we handle the response via resolve and reject depicted with then and catch respectively.

Resources

Continue Reading

Adding an Edit Route for Access Codes in Open Event Frontend

This blog post will illustrate how to add edit route to access code to allow organizers editing an access code. Editing access codes deals with the following API on Open Event Server.

PATCH          /v1/access-codes/{id}

First of all we need to add a route to allow editing of access codes. Our route will be /events/{event_id}/tickets/access-codes/edit/{access_code_id}. To generate a new route we need to run the command

ember g route events/view/tickets/access-codes/edit

This will generate new routes files and templates. Also this will add this route in router.js. In router.js we need to specify what we are passing as a parameter to route. For this we specify access_code_id to let ember know that this parameter will be passed with the URL. This is done so as to know which access code to update. Our route should look something like this /events/{event_id}/tickets/access-codes/edit/{access_code_id}. Final router.js file for access-codes part is given below:

//  app/router.js
 
this.route('access-codes', function() {
   this.route('list', { path: '/:access_status' });
   this.route('create');
   this.route('edit', { path: '/edit/:access_code_id' });
});

 

Next we need to pass model (data) to our edit route template. In our model we will be looking for a particular access code with an id and event tickets. After we get our event tickets, we then look for the event tickets which are already present in that access code. This is done so as to check the tickets in the template which are already present in that access code. So for this in afterModel hook we loop over all event tickets and whichever ticket is included in the access code tickets array we mark its isChecked property as true. This helps us to mark those tickets as checked in multiple select checkboxes of template.

// app/routes/events/view/tickets/access-codes/edit.js

model(params) {
   return RSVP.hash({
     accessCode : this.store.findRecord('access-code', params.access_code_id, {}),
     tickets    : this.modelFor('events.view').query('tickets', {})
   });
 },

 async afterModel(model) {
   let tickets = await model.accessCode.tickets;
   let allTickets = model.tickets;
   allTickets.forEach(ticket => {
     if (tickets.includes(ticket)) {
       ticket.set('isChecked', true);
     } else {
       ticket.set('isChecked', false);
     }
   });

 

The information about multiple select checkboxes in frontend has been discussed in this blog post. Now after we are done setting our edit route we need to redirect user to edit page when he/she clicks on the edit button in access code list. For this we define the necessary actions in template which will be triggered when user clicks on the icon. Code for the edit icon in access code list is given below.

// templates/components/ui-table/cell/events/view/tickets/access-codes/cell-actions.hbs

class="ui vertical compact basic buttons"> {{#ui-popup content=(t 'Edit') click=(action editAccessCode record.id) class='ui icon button' position='left center'}} class="edit icon"> {{/ui-popup}}

 

The editAccessCode action looks something like this.

// controller/events/view/tickets/access-codes/edit.js

editAccessCode(id) {
     this.transitionToRoute('events.view.tickets.access-codes.edit', id);
}

 

After clicking on the edit icon user is redirected to edit route where he/she can edit access code choosen. We use the same component that we chose for creating access code. To know more about create access code template component refer to this blog. Finally when saving the edited access code we call save action. This action is defined in the controllers. The action looks something like this.

// controllers/events/view/tickets/access-codes/edit.js

export default Controller.extend({
 actions: {
   save(accessCode) {
     accessCode.save()
       .then(() => {
         this.get('notify').success(this.get('l10n').t('Access code has been successfully updated.'));
         this.transitionToRoute('events.view.tickets.access-codes');
       })
       .catch(() => {
         this.get('notify').error(this.get('l10n').t('An unexpected error has occured. Discount code cannot be created.'));
       });
   }
 }
});

 

After the access code is saved. We redirect user back to access code list.

Resources

Continue Reading

Implementing dynamic forms to edit a speaker using Custom Forms in Open Event Frontend

Open Event Frontend allows the organizer of an event to customise the sessions and speakers form using the custom form API. While creating the event the organiser can select the form fields which he wants to place in the sessions and speakers form. This blog will illustrate how a form to edit the details of a speaker is created. Only those fields are included which were included by the person who created the event during the sessions and speakers creation section.

The first step is to retrieve the fields of the form. Each event has custom form fields which can be enabled on the sessions-speakers page, where the organiser can include/exclude the fields for speakers & session forms.

A query is written in the javascript file of the route admin/speakers/edit to retrieve the required details and create a form. The second query helps to determine the speaker id and include the model of speaker and the attribute values of the speaker with that specific id.

import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Route.extend(AuthenticatedRouteMixin, {
 titleToken(model) {
   var speakerName = model.get('name');
   return this.get('l10n').t('Edit Speaker-'.concat(speakerName));
 },
 async model(params) {
   const eventDetails = this.modelFor('events.view');
   return {
     event : eventDetails,
     form  : await eventDetails.query('customForms', {
       'page[size]' : 50,
       sort         : 'id'
     }),
     speaker: await this.get('store').findRecord('speaker', params.speaker_id)
   };
 }
});

In the frontend we call the form of session and speakers. With the speaker-id being passed from the route, a form is created with the values entered by the user during the speaker creation and the other attributes marked included in the session-speakers wizard.

{{forms/session-speaker-form fields=model.form data=model save=(action 'save') isSpeaker=true includeSpeaker=true isSessionSpeaker=true isLoading=isLoading}}

Finally whenever user edits a speaker and clicks on the save button patch endpoint of the speakers API is called and the new details are saved.

Resources

  • Official Ember Model Table docs: http://onechiporenko.github.io/ember-models-table/v.1
  • Official Ember Data documentation: https://github.com/emberjs/data
Continue Reading

Adding the Edit Session route in Open Event Frontend

This blog article will illustrate how to edit a Session in Open Event Frontend, for which a nested route /sessions/edit/<session_id> had to be created. Further an Edit Session Form was created on that route to use the patch end-point of the sessions API of Open Event API.

To get started with it, we simply use the ember CLI to generate the routes

$  ember generate route events/view/sessions/edit

Now router.js file is changed to add the session_id. The router.js file should be looking like this now.

this.route(‘sessions’, function() {
this.route(‘list’, { path: ‘/:session_status’ });
this.route(‘create’);
this.route(‘edit’, { path: ‘/edit/:session_id’ });
});

This means that our routes and sub routes are in place. We are using ember CLI to generate these routes, that means, the template files for them generate automatically. Now these routes exist and we need to write the data in the templates of these routes which will get displayed to the end user.

The routes are nested, the content of the parent route can be made available to all the children routes via the outlet in ember js.

Next, we go to the template file of sessions/edit route which is at templates/events/view/sessions/edit.hbs which displays an Edit Session form.

<form class=”ui form {{if isLoading ‘loading’}}” {{action ‘submit’ on=’submit’}}>


{{input type=’text’ id=’title’ value=data.title}}

{{widgets/forms/rich-text-editor value=data.shortAbstract name=’shortAbstract’}}

{{widgets/forms/rich-text-editor value=data.comments name=’comments’}}

{{input type=’text’ value=data.slidesUrl}}

<button type=”submit” class=”ui teal submit button update-changes”>
{{t ‘Save Session’}}
</button>
</form>

After the editing of different attributes is done Save Session button is clicked for which the action is defined in the controller /controllers/events/view/sessions/edit.js. The patch endpoint was called and the details of the attributes changes were saved.

save() {
this.set(‘isLoading’, true);
this.get(‘model’).save()
.then(() => {
this.get(‘notify’).success(this.get(‘l10n’).t(‘Session details have been saved’));
this.transitionToRoute(‘events.view.sessions’);
})
.catch(() => {
this.get(‘notify’).error(this.get(‘l10n’).t(‘Oops something went wrong. Please try again’));
})
.finally(() => {
this.set(‘isLoading’, false);
});
}
}

Thus after editing the attributes of sessions in the Edit Session form and clicking on the Save Session button the session details are edited.

Resources

  • EmberJS Controllers: https://guides.emberjs.com/v2.12.0/controllers
  • Open Event Server, API docs: https://open-event-api.herokuapp.com
Continue Reading

Implementing Speaker Table on Open Event Frontend by Integrating Speakers API

This blog article will illustrate how the Speakers API is integrated in Open Event Frontend, which allows for the speakers and their associated data to be displayed in the speakers table. Also, it illustrates how the basic semantic UI table is converted into an ember model table to accommodate the fetched data from the API. Our discussion primarily will involve the app/routes/events/view/speakers  route to illustrate the process.

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

GET /v1/events/{event_identifier}/speakers

First we need to formulate the filters options while making the API call. There are several tabs present which display all the sessions in a particular state i.e. pending, rejected, accepted and all. Thus the formulated filter options stored in the variable filterOptions are as follows:

let filterOptions = [];
if (params.speakers_sessions_state === 'pending') {
filterOptions = [
{
name : 'state',
op : 'eq',
val : 'pending'
}
];
} else if (params.speakers_sessions_state === 'accepted') {
filterOptions = [
{
name : 'state',
op : 'eq',
val : 'accepted'
}
];
} else if (params.speakers_sessions_state === 'rejected') {
filterOptions = [
{
name : 'state',
op : 'eq',
val : 'rejected'
}
];
} else {
filterOptions = [];
}

Next we need to formulate the required query to actually make the API call. It is important to note here that there is a column for sessions of the speaker in  the speaker table. Sessions are a related field for a user and hence we will make use of the include clause while generating the query, Similarly we will make use of the filter clause to pass on the filter options we generated above in filterOptions.

Since the speakers are being fetched for a particular event, and we are in that event’s dashboard, the speakers in question have a hasMany relationship with the aforementioned event. Thus we can make use of the model already fetched in the view route. So the final query call will be as follows:

 return this.modelFor('events.view').query('speakers', {
include      : 'sessions,event',
filter       : filterOptions,
'page[size]' : 10
});

The page[size] option in the query is for implementing pagination, it ensures that at max 10 speakers are returned at once.

Next, we need to convert the existing table to an ember model table. For this the first step is to delete the entire original table along with the dummy data in it. The events-table ember table component will be re-used to form the base structure of the table as follows:

{{events/events-table columns=columns data=model
useNumericPagination=true
showGlobalFilter=true
showPageSize=true
}}

Final steps of conversion table to ember table involve defining the columns of the table. They will be defined in the usual manner, with mandatory title and name attributes. In case the display requirements of the data are in mere simple text, calling a template for displaying the text is not required, however for more complex natured values in the columns, it is advisable to make use of the component, and the technical logic can be handled in the component template itself. For instance, one such field on the speakers table is sessions which are related records and were included especially in the query call. They are not directly accessible by their field names. Thus they must be referred as a child of the record object.

{{#each record.sessions as |session|}}
       {{session.title}}
{{/each}}

Similarly the template for the actions column will have to be created as it requires complex logic to implement actions on those buttons. After defining all the columns in the controller, the final columns are as follows:

columns: [
{
propertyName : 'name',
title       : 'Name'
},
{
propertyName : 'email',
title       : 'Email'
},
{
propertyName : 'mobile',
title       : 'Phone'
},
{
propertyName   : 'sessions',
title         : 'Submitted Sessions',
template       : 'components/ui-table/cell/events/view/speakers/cell-simple-sessions',
disableSorting : true
},
{
propertyName : '',
title       : 'Actions',
template     : 'components/ui-table/cell/events/view/speakers/cell-buttons'
}
]

After performing all these steps, the static table which was holding dummy data has been converted into an ember table and thus features like inbuilt pagination, searching etc. can be used.

Resources

 

Continue Reading

How Errors from Server Side are Handled On Open Event Frontend

This blog article will illustrate how the various error or status codes are handled  in  Open Event Frontend, and how the appropriate response is generated corresponding to those error codes. Open Event Frontend, relies on Open Event Server for all server operations. Open Event Server exposes  a well documented JSON:API Spec Compliant REST API. The clients using the api primarily interact with it using GET, POST , PATCH and DELETE requests. And thus for each request the API returns corresponding data as response along with it’s status code.

For instance whenever the app opens, for the landing page, all the events are fetched by making a GET request to the end point v1/events. If the request is successful and events data is returned, the status code is 200 which stands for OK in the http standard set by IANA.

Fig 1: Screenshot of google chrome developer consoles’ networking tab while making a request.

Since Open Event server is compliant with JSON:API Spec, to quote it’s official documentation, “Error objects MUST be returned as an array keyed by errors in the top level of a JSON API document.” Thus whenever there is an error, or the request is unsuccessful due to a variety of reasons, the server has a predefined format to convey the information to the front end.

The process is illustrated by the reset password form on open event frontend. When a user forgets his password, he/she has the option to reset it, using his email address. Thus the form just takes in the email address of the user and makes a POST request to the reset-password API endpoint of the server.

  • Once the request is made there are 3 possibilities (check references for error code significance):
    The request is successful and a status code of 200 is returned.
  • The email address user entered doesn’t exists and no record is found in the database. 422 status code should be returned.
  • The server is down, or the request is invalid (something unexpected has occurred). In all such scenarios error code 404 should be returned.

this.get('loader')
         .post('auth/reset-password', payload)
         .then(() => {
           this.set('successMessage', this.l10n.t('Please go to the link sent to your     

           email to reset your password'));
         })
         .catch(reason => {
           if (reason && reason.hasOwnProperty('errors') && reason.errors[0].status

               === 422) {
             this.set('errorMessage', this.l10n.t('No account is registered with this

                      email address.'));
           } else {
             this.set('errorMessage', this.l10n.t('An unexpected error occurred.'));
           }
         })
         .finally(()=> {
           this.set('isLoading', false);
         }
         );
Figure 2 : The reset password UI

Thus as mentioned in the JSON:API docs, the errors property is expected to contain the status code and error message(optional) , which ember handles via the the catch block. The catch block is executed whenever the response from the request is not successful. The contents of the response are present in the reason property. If the status of the error is 422, the corresponding message is stored inside the errorMessage property of the component which is further used to display the alert by rendering an error block on the forgot password form.

In case there is no error, the errorMessage is undefined, and the error block is not rendered at all. In case of any other unexpected error, the standard text is displayed by initialising the errorMessage property to it.

Resources

Continue Reading

How the Form Mixin Enhances Validations in Open Event Frontend

This blog article will illustrate how the various validations come together in  Open Event Frontend, in a standard format, strongly reducing the code redundancy in declaring validations. Open Event Frontend, offers high flexibility in terms of validation options, and all are stored in a convenient object notation format as follows:

getValidationRules() {
return {
  inline : true,
  delay  : false,
  on     : 'blur',
  fields : {
    identification: {
      identifier : 'email',
      rules      : [
        {
          type   : 'empty',
          prompt : this.l10n.t('Please enter your email ID')
        },
        {
          type   : 'email',
          prompt : this.l10n.t('Please enter a valid email ID')
        }
      ]
    },
    password: {
      identifier : 'password',
      rules      : [
        {
          type   : 'empty',
          prompt : this.l10n.t('Please enter your password')
        }
      ]
    }
  }
};
}

Thus the validations of a form are stored as objects, where the  identifier attribute determines which field to apply the validation conditions to. The rules array contains all the rules to be applied to the determined object. Within rules, the type represents the kind of validation, whereas the prompt attribute determines what message shall be displayed in case there is a violation of the validation rule.

 

NOTE: In case an identifier is not specified, then the name of the rule object is itself considered as the identifier.

These validations are in turn implemented by the FormMixin. The various relevant sections of the mixin will be discussed in detail. Please check references for complete source code of the mixin.

getForm() {
return this.get($form);
}

The getForm function returns the calling component’s entire form object on which the validations are to be applied.

onValid(callback) {
this.getForm().form('validate form');
if (this.getForm().form('is valid')) {
  callback();
}
}

The onValid function serves as the boundary level function, and is used to specify what should happen when the validation is successful. It expects a function object as i’s argument. It makes use of the getForm() function to retrieve the form and using semantic UI, determines if all the validations have been successful. In case they have, it calls the callback method passed down to it as the argument.

Next transitioning into the actual implementation, certain behavioral traits of the validations are controlled by the two global variables autoScrollToErrors and autoScrollSpeed. The former is a boolean, which is set to true if the application is designed in such a way that once the user presses the submit button and a validation fails, the browser scrolls to the first field whose validation failed with a speed specified by the latter.

Next, comes the heart of the mixin. Since it needs to be continuously determined if a field’s validation has failed, the entire logic of checking is placed inside a debounce call which is supported by ember to continuously execute a function with a gap of a certain specified time (400 ms in this case). Since the validity can be checked only after all the fields have rendered, the debounce call is placed inside a didRender call. The didRender call ensures that any logic inside it is executed only after all the elements of the relevant component have been rendered and are a part of the DOM.

didRender() {
. . .
debounce(this, () => {
  const defaultFormRules = {
    onFailure: formErrors => {
      if (this.autoScrollToErrors) {
        // Scroll to the first error message
        if (formErrors.length > 0) {
          $('html,body').animate({
            scrollTop: this.$(`div:contains('${formErrors[0]}')`).offset().top
          }, this.autoScrollSpeed);
        }
      }
    }
  };
}, 400); . . .}

 

onFailure is an ES6 syntax function, which takes all the form errors as its arguments. And in case the autoScrollToErrors global variable is set to true, it checks if there are any errors or the length of the formErrors array is more than 0. In case there are errors, it makes a call to animate callback, and scrolls to the very first field which has an error with the speed defined by the previously discussed global autoScrollSpeed . The fact that the very first field is selected for scrolling off to, is ensured by the index 0 specified (formErrors[0]). Thus role of the mixin is to continuously check for any validations and in case there are, scroll to them.

Resources

Continue Reading

How App Social Links are specified in Open Event Frontend

This blog article will illustrate how the various social links are specified in the the footer of Open Event Frontend, using the settings API. Open Event Frontend, offers high flexibility to the admins regarding the settings of the App, and hence the media links are not hard coded, and can be changed easily via the admin settings panel.

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

GET /v1/settings

The model for settings has the following fields which concern the social links.

 googleUrl              : attr('string'),
 githubUrl              : attr('string'),
 twitterUrl             : attr('string')

Next we define them as segmented URL(s) so that they can make use of the link input widget.

segmentedTwitterUrl    : computedSegmentedLink.bind(this)('twitterUrl'),
 segmentedGoogleUrl     : computedSegmentedLink.bind(this)('googleUrl'),
 segmentedGithubUrl     : computedSegmentedLink.bind(this)('githubUrl'),

Now it is required for us to fetch the data from the API, by making the corresponding call to the API. Since the footer is present in every single page of the app, it is necessary that we make the call from the application route itself. Hence we add the following to the application route modal.

socialLinks: this.get('store').queryRecord('setting', {
})

Next we need to iterate over these social links, and add them to the footer as per their availability.So we will do so by first passing the model to the footer component, and then iterating over it in footer.hbs

{{footer-main socialLinks=model.socialLinks footerPages=footerPages}}


And thus we have passed the socialLinks portion of the model, under the alias socialLinks.Next, we iterate over them and each time check, if the link exists before rendering it.

<div class="three wide column">
     <div class="ui inverted link list">
       <strong class="item">{{t 'Connect with us'}}</strong>
       {{#if socialLinks.supportUrl}}
         <a class="item" href="{{socialLinks.supportUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="info icon"></i> {{t 'Support'}}
         </a>
       {{/if}}
       {{#if socialLinks.facebookUrl}}
         <a class="item" href="{{socialLinks.facebookUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="facebook f icon"></i> {{t 'Facebook'}}
         </a>
       {{/if}}
       {{#if socialLinks.youtubeUrl}}
         <a class="item" href="{{socialLinks.youtubeUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="youtube icon"></i> {{t 'Youtube'}}
         </a>
       {{/if}}
       {{#if socialLinks.googleUrl}}
         <a class="item" href="{{socialLinks.googleUrl}}" target="_blank" rel="noopener noreferrer">
           <i class="google plus icon"></i> {{t 'Google +'}}
         </a>
       {{/if}}
     </div>
   </div>

Thus all the links in the app are easily manageable, from the admin settings menu, without the need of hard coding them. This approach also, makes it easy to preserve the configuration in a central location.

Resources

Continue Reading

Keeping Order of tickets in Event Wizard in Sync with API on Open Event Frontend

This blog article will illustrate how the various tickets are stored and displayed in order the event organiser decides  on  Open Event Frontend and also, how they are kept in sync with the backend.

First we will take a look at how the user is able to control the order of the tickets using the ticket widget.

{{#each tickets as |ticket index|}}
  {{widgets/forms/ticket-input ticket=ticket
  timezone=data.event.timezone
  canMoveUp=(not-eq index 0)
  canMoveDown=(not-eq ticket.position (dec
  data.event.tickets.length))
  moveTicketUp=(action 'moveTicket' ticket 'up')
  moveTicketDown=(action 'moveTicket' ticket 'down')
  removeTicket=(confirm 'Are you sure you  wish to delete this 
  ticket ?' (action 'removeTicket' ticket))}}
{{/each}}

The canMoveUp and canMoveDown are dynamic properties and are dependent upon the current positions of the tickets in the tickets array.  These properties define whether the up or down arraow or both should be visible alongside the ticket to trigger the moveTicket action.

There is an attribute called position in the ticket model which is responsible for storing the position of the ticket on the backend. Hence it is necessary that the list of the ticket available should always be ordered by position. However, it should be kept in mind, that even if the position attribute of the tickers is changed, it will not actually change the indices of the ticket records in the array fetched from the API. And since we want the ticker order in sync with the backend, i.e. user shouldn’t have to refresh to see the changes in ticket order, we are going to return the tickets via a computed function which sorts them in the required order.

tickets: computed('data.event.tickets.@each.isDeleted', 'data.event.tickets.@each.position', function() {
   return this.get('data.event.tickets').sortBy('position').filterBy('isDeleted', false);
 })

The sortBy method ensures that the tickets are always ordered and this computed property thus watches the position of each of the tickets to look out for any changes. Now we can finally define the moveTicket action to enable modification of position for tickets.

moveTicket(ticket, direction) {
     const index = ticket.get('position');
     const otherTicket = this.get('data.event.tickets').find(otherTicket => otherTicket.get('position') === (direction === 'up' ? (index - 1) : (index + 1)));
     otherTicket.set('position', index);
     ticket.set('position', direction === 'up' ? (index - 1) : (index + 1));
   }

The moveTicket action takes two arguments, ticket and direction. It temporarily stores the position of the current ticket and the position of the ticket which needs to be swapped with the current ticket.Based on the direction the positions are swapped. Since the position of each of the tickets is being watched by the tickets computed array, the change in order becomes apparent immediately.

Now when the User will trigger the save request, the positions of each of the tickets will be updated via a PATCH or POST (if the ticket is new) request.

Also, the positions of all the tickets maybe affected while adding a new ticket or deleting an existing one. In case of a new ticket, the position of the new ticket should be initialised while creating it and it should be below all the other tickets.

addTicket(type, position) {
     const salesStartDateTime = moment();
     const salesEndDateTime = this.get('data.event.startsAt');
     this.get('data.event.tickets').pushObject(this.store.createRecord('ticket', {
       type,
       position,
       salesStartsAt : salesStartDateTime,
       salesEndsAt   : salesEndDateTime
     }));
   }

Deleting a ticket requires updating positions of all the tickets below the deleted ticket. All of the positions need to be shifted one place up.

removeTicket(deleteTicket) {
     const index = deleteTicket.get('position');
     this.get('data.event.tickets').forEach(ticket => {
       if (ticket.get('position') > index) {
         ticket.set('position', ticket.get('position') - 1);
       }
     });
     deleteTicket.deleteRecord();
   }

The tickets whose position is to be updated are filtered by comparison of their position from the position of the deleted ticket.

Resources

Continue Reading

Implementing Roles API on Open Event Frontend to Create Roles Using an External Modal

This blog article will illustrate how the roles are created via the external model  on the admin permissions page in Open Event Frontend, using the roles API. Our discussion primarily will involve the admin/permissions/index route to illustrate the process.The primary end point of Open Event API with which we are concerned with for fetching the permissions  for a user is

POST /v1/roles

First we need to create a model for the user-permissions, which will have the fields corresponding to the api, so we proceed with the ember CLI command:

ember g model role

Next we define the model according to the requirements. The model needs to extend the base model class, and has only two fields one for the title and one for the actual name of the role.

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';

export default ModelBase.extend({
 name           : attr('string'),
 titleName      : attr('string')
 });

Next we need to modify the existing modal to incorporate the API and creation of roles in it. It is very important to note here that using createRecord as the model will result in a major flaw. If createRecord is used and the user tries to create multiple roles, other than the first POST request all the subsequent requests will be PATCH requests and will keep on modifying the same role. To avoid this, a new record needs to be created every time the user clicks on Add Role.  We slightly modify the modal component call to pass in the name and titleName to it.

{{modals/add-system-role-modal  isOpen=isAddSystemRoleModalOpen
                                isLoading=isLoading
                                name=name
                                titleName=titleName
                                addSystemRole=(action 'addSystemRole')}}

Upon entering the details of the roles and successful validation of the form, if the user clicks the Add Role button of the modal, the action addSystemRole will be triggered. We will write the entire logic for the same in the respective controller of the route.

addSystemRole() {
     this.set('isLoading', true);
     this.get('store').createRecord('role', {
       name      : this.get('name'),
       titleName : this.get('titleName')
     }).save()
       .then(() => {
         this.set('isLoading', false);
         this.notify.success(this.l10n.t('User permissions have 
         been saved successfully.'));
         this.set('isAddSystemRoleModalOpen', false);
         this.setProperties({
           name          : null,
           roleTitleName : null
         });
       })
       .catch(()=> {
         this.set('isLoading', false);
         this.notify.error(this.l10n.t('An unexpected error has occurred.
         User permissions not saved.'));
       });
   },

At first the isLoading property is made true.This adds the semantic UI class loading to the the form,  and so the form goes in the loading state, Next, a record is created of the type role  and it’s properties are made equal to the corresponding values entered by the user.

Then save() is called, which subsequently makes a POST request to the server. If the request is successful the modal is closed by setting the isAddSystemRoleModalOpen property to false. Also, the fields of the modal are cleared for a  better user experience in case multiple roles need to be added one after the other.

In cases when  there is an error during the processing of the request the catch() block executes. And the modal is not closed. Neither are the fields cleared.

Resources

Continue Reading
  • 1
  • 2
Close Menu
%d bloggers like this: