Our pure JavaScript Scheduler component


Post by doonamis »

Hello I'm having problems with the resource columns of the scheduler, the first one is I can't get them to be ordered, I don't see the arrow for the sort, I think is enabled by default but in my case it's not showing

Also, is there a way to have a contextMenu as the eventContextMenu but for the resource cells?

Here is my current config
columns: [
    {
      text: 'Driver / Vehicle Type',
      field: 'name',
      width: 250,
      editor: false,
      htmlEncode: false,
      headerMenuItems: [
        { 
          text: 'My unique header item', 
          icon: 'fa fa-flask', 
          onItem : () => function() {
            console.log('Edit')
          } 
        }
      ],
      renderer: function({ value, record }) {
        if( record.id == 'temporary-row' ) {
          return 'Unassigned'
        }

        return  `
        <div class="react-resource">
          ${record.urlPicture ? `<img class="react-resource__picture" src="${record.urlPicture}" alt="${value}" title="${value}" loading="lazy" />` : '' }
          <div class="grid-resource-vehicle__text">
            ${record.vehicleName ? record.vehicleName : '' }
            <small>${record.vehicleType ? record.vehicleType : '' }</small>
          </div>
        </div>`;
      },
      sortable: function(user1, user2) {
        return user1.name < user2.name ? -1 : 1;
      },
      filterable: {
        filterFn : function({ record, value }) {
          
          const vehicleName = record.vehicleName ? record.vehicleName.toLowerCase() : ''
          const vehicleType = record.vehicleType ? record.vehicleType.toLowerCase() : ''
          const searchValue = value.toLowerCase()
          if( record.id == 'temporary-row' ) {
            return true
          }
          console.log('Search', searchValue, record.name.toLowerCase(), vehicleName, vehicleType)
          return (record.name.toLowerCase().indexOf(searchValue) !== -1) || (vehicleName.indexOf(searchValue) !== -1) || (vehicleType.indexOf(searchValue) !== -1)
        }, 
        filterField : {
          emptyText : 'Filter by driver or vehicle'
        }
      }
    }
  ],
Thanks!

Post by mats »

Please post your full config or ideally upload a full test case that we can inspect.

Post by doonamis »

Hello Mats,

Can you give some email where I can send you the files? I can give you acces to the current development site if you need it

Thanks!

Post by mats »

No need for email, please attach to this thread

Post by doonamis »

Hello mats, sorry I can't attach full files here as it have some credentials and keys stored, I'm sharing both config and scheduler file then:

(url to api calls are hidden)

schedulerConfig.js
import moment from 'moment'
import TaskStore from '~/scheduler-2.2.5/lib/TaskStore'

const tickWidth = 25

export default {
  columns: [
    {
      text: 'Driver / Vehicle Type',
      field: 'name',
      width: 200,
      editor: false,
      htmlEncode: false,
      headerMenuItems: [
        { 
          text: 'My unique header item', 
          icon: 'fa fa-flask', 
          onItem : () => function() {
            console.log('Edit')
          } 
        }
      ],
      renderer: function({ value, record }) {
        if( record.id == 'temporary-row' ) {
          return 'Unassigned'
        }

        return  `
        <div class="react-resource">
          ${record.urlPicture ? `<img class="react-resource__picture" src="${record.urlPicture}" alt="${value}" title="${value}" loading="lazy" />` : '' }
          <div class="grid-resource-vehicle__text">
            ${record.vehicleName ? record.vehicleName : '' }
            <small>${record.vehicleType ? record.vehicleType : '' }</small>
          </div>
        </div>`;
      },
      sortable: function(user1, user2) {
        return user1.name < user2.name ? -1 : 1;
      },
      filterable: {
        filterFn : function({ record, value }) {
          
          const vehicleName = record.vehicleName ? record.vehicleName.toLowerCase() : ''
          const vehicleType = record.vehicleType ? record.vehicleType.toLowerCase() : ''
          const searchValue = value.toLowerCase()
          if( record.id == 'temporary-row' ) {
            return true
          }
          console.log('Search', searchValue, record.name.toLowerCase(), vehicleName, vehicleType)
          return (record.name.toLowerCase().indexOf(searchValue) !== -1) || (vehicleName.indexOf(searchValue) !== -1) || (vehicleType.indexOf(searchValue) !== -1)
        }, 
        filterField : {
          emptyText : 'Filter by driver or vehicle'
        }
      }
    }
  ],
  viewPreset: {
    name: 'hourAndDay',
    tickWidth: tickWidth,
    columnLinesFor: 'top',
    timeResolution: { // Dates will be snapped to this resolution
      unit: 'minute', // Valid values are 'millisecond', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'.
      increment: 5
    },
    headerConfig: {
      top: {
        unit: 'd',
        align: 'center',
        dateFormat: 'ddd DD MMM'
      },
      middle: {
        unit: 'h',
        align: 'center',
        dateFormat: 'HH'
      },
      bottom: {
        unit: 'm',
        align: 'left',
        dateFormat: 'mm',
        increment: 15
      }
    }
  },
  features: {
    filterBar  : true,
    stripe: true,
    resourceTimeRanges: true,
    eventContextMenu: {
      processItems({eventRecord, items}) {
        items.editEvent.text = 'Edit ride'
        //console.log(items)

        // Do not show menu for secret events
        
        if (eventRecord.editable === false) {
            return false;
        }
      },
      items: {
        deleteEvent   : false,
        // custom item with inline handler
        unassign: {
          text: 'Unassign',
          icon: 'b-fa b-fa-user-times',
          weight: 200,
          onItem: ({ source, eventRecord, resourceRecord }) => {
            //console.log('Unassign source', source)
            eventRecord.unassign(resourceRecord)
          }
        },
        resetTime: {
          text: 'Reset time & duration',
          icon: 'b-fa b-fa-clock',
          weight: 200,
          onItem: ({ eventRecord }) => {
            //console.log('Unassign source', source)
            eventRecord.startDate = eventRecord.originalStartDate
            eventRecord.duration = eventRecord.originalDuration
          }
        },
        booking: {
          text: 'Booking editor',
          icon: 'b-fa b-fa-book',
          weight: 200,
          onItem: (data) => {
            //alert('Booking')
            //console.log(data)
            //this.$emit('openBookingEditor')
            
          }
        }
      }
    },
    eventEdit: {
      showDeleteButton: false,
      showNameField: false,
      /*startDateConfig: {
        min: '2019-10-15',
        max: '2019-10-18'
      },
      endDateConfig: {
        min: '2019-10-15',
        max: '2019-10-18'
      },*/
      /*extraItems: [
        {
            type        : 'combo',
            ref         : 'roomCombo',
            items       : [],
            name        : 'vehicleId',
            label       : 'Vehicle',
            placeholder : 'Select vehicle...',
            index       : 2,
            //editable    : false,
            clearable   : false,
            //listItemTpl: item => `<div>${item.name} - ${item.vehicleType}</div>`
        }
      ]*/
    },
    eventDrop: {
      validatorFn: function (context) {
        console.log('drop!')
      }
    },
    eventDrag: {
      validatorFn: function (context) {
        const task = context.draggedRecords[0]
        // const newResource = context.newResource
        const startLimit = moment(task.originalStartDate).subtract(15, 'minutes')
        const endLimit = moment(task.originalStartDate).add(60, 'minutes')
        // Ride can only be dragged 15min earlier or 60min later
        const currentTime = moment(context.startDate)

        if (currentTime.isBefore(startLimit)) {
          return {
            valid: false,
            message: "Start time can't be moved more than 15min before"
          }
        }

        if (currentTime.isAfter(endLimit)) {
          return {
            valid: false,
            message: "Start time can't be moved more than 60min after"
          }
        }

        return {
          valid: true
        }
      }
    },
    eventResize: {
      validatorFn: function (context) {
        const task = context.eventRecord
        const startDate = moment(context.startDate)
        const endDate = moment(context.endDate)

        //const originalDuration = task.endDate - task.originalStartDate
        const startLimit = moment(task.originalStartDate).subtract(0, 'minutes')
        const currentTime = moment(context.startDate)

        if (currentTime.isBefore(startLimit)) {
          return {
            valid: false,
            message: "Can't resize from start"
          }
        }
        // Duration can't be larger than the double of the time
        if ((task.originalDuration * 2) < endDate.diff(startDate, 'seconds')) {
          return {
            valid: false,
            message: "The duration of a ride can't be more than the double of original duration"
          }
        }

        return {
          valid: true
        }
      }
    }
  },
  crudManager: {
    resourceStore: {
      // Add some custom fields
      fields: ['driverId', 'vehicleId','vehicleName', 'vehicleType', 'urlPicture', 'latitude', 'longitude']
    },
    eventStore: {
      storeClass: TaskStore
    },
    // autoLoad: true,
    transport: {
      load: {
        requestConfig: {
          // url: null,
          url: '####',
          method: 'GET',
          disableCaching: false,
          autoLoad   : false,
          headers: {
            Authorization: null
          },
          fetchOptions: {
            credentials: 'omit'
          }
        }
      }
    },
    listeners: {
      // Will be called after data is fetched but before it is loaded into stores
      beforeLoadApply({ response }) {
        // console.log('Response!', response)
        //response.events.rows = FakeData.events.rows

        response.resources.rows.unshift({
          id: 'temporary-row',
          name: 'Temporary',
          cls: 'b-grid-row--temporary'
        });

        response.resources.rows.forEach(function (driver) {
          driver.driverId = driver.id
          driver.id = `${driver.id}--${driver.vehicleId}`
        })
        
        // Set necessary fields
        response.events.rows.forEach(function (ride) {
          //console.log('Ride start before', ride.start, ride)
          ride.startDate = moment(ride.start).format('YYYY-MM-DD HH:mm')
          //console.log('Ride start after', ride.startDate)
          //ride.endDate = moment(ride.endDate).format('YYYY-MM-DD hh:mm')
          ride.resourceId = `${ride.driverAssigned}--${ride.vehicleAssigned}`
          ride.originalStartDate = ride.startDate
          ride.originalDuration = ride.duration
          ride.durationUnit = 's'
          //ride.transportTime = 1800

          if(!ride.editable) {
            ride.draggable = false;
            ride.resizable = false;
          }

          if(ride.staged) {
            ride.resourceId = 'temporary-row'
          }

        })

        // console.log('Response after', response.events.rows)
        // this.columns[1].editor.items = response.vehicles.rows
        // Turn "nested event" dates into actual dates, to not have to process them each time during render
        // response.events.rows.forEach(event => refreshAgendaOffsets(event))
        //console.log(response.timeRanges.rows[0])
        /*response.timeRanges.rows.forEach(function (item) {
          item.startDate = moment(item.startDate).format('YYYY-MM-DD hh:mm')
          item.endDate = moment(item.endDate).format('YYYY-MM-DD hh:mm')
        })*/
        //console.log('TimeRanges', response)
      },
      
    }
  },
  listeners: {
    finishCellEdit({ editorContext }) {
      // Update resource record after selecting another vehicle
      if (editorContext.column.field == 'vehicleId') {
        var vehicles = editorContext.editor._inputField._store._data
        const newVehicle = vehicles.find(function (item) {
          return item.data.id === editorContext.value
        })
        editorContext.record.set('vehicleName', newVehicle.name)
        editorContext.record.set('vehicleType', newVehicle.vehicleType)
      }
    }
  },
  onCellClick(context) {
    //alert('hola')
  },
  // eventBodyTemplate is used to render markup inside an event. It is populated using data from eventRenderer()
  eventBodyTemplate: function (event) {
    /*var startTime = moment(event.startDate)
    var originalStartTime = moment(event.originalStartDate)
    var startTimeOffset =  startTime.diff(originalStartTime, 'minutes');*/
    return `
    <div class="event-icon event-icon--transport" style="width:${event.transportWidth}px; left:-${event.transportWidth}px"></div>
    <div class="event-text">
      ${event.ride.name}
      <small>${event.pax}pax</small>
      </div>
    <div class="event-icon event-icon--security" style="width:${event.securityWidth}px"></div>`
  },
  // eventRenderer is here used to translate the dates of nested events into pixels, passed on to the eventBodyTemplate
  eventRenderer({ eventRecord, tplData }) {
    // getCoordinateFromDate gives us a px value in time axis, subtract events left from it to be within the event
    // const dateToPx = date => this.getCoordinateFromDate(date) - tplData.left
    return {
      ride: eventRecord,
      transportWidth: Math.round(eventRecord.approachDuration*0.02777),
      securityWidth: Math.round(eventRecord.coolDownTime*0.02777),
      pax: 4
    }
    /*
    // Calculate coordinates for all nested events and put in an array passed on to eventBodyTemplate
    return (eventRecord.agenda || [eventRecord]).map(nestedEvent => ({
      left: dateToPx(DateHelper.add(eventRecord.startDate, nestedEvent.startOffset)),
      width: dateToPx(DateHelper.add(eventRecord.startDate, nestedEvent.endOffset)),
      name: nestedEvent.name,
      cls: nestedEvent.cls,
      type: nestedEvent.type
    })) */
  },

  // taken from the original example
  onEquipmentStoreLoad({ source: store }) {
    // Save vehicles from store to dropdown editor
    this.columns.get('vehicleId').editor.items = store.getRange()
    this.eventEdit.extraItems[0].items = store.getRange()
    // console.log('Hello from store!', this.columns.get('vehicleId').editor.items)
    // Setup the data for the equipment combo inside the event editor
    // const equipmentCombo = this.features.eventEdit.getEditor().query(item => item.name === 'equipment')
    // equipmentCombo.items = store.getRange()
    // this._equipmentStore = store

    // Since the event bars contain icons for equipment, we need to refresh rows once equipment store is available
    // this.refreshRows()
  }
}

scheduler.vue
    <scheduler
      ref="scheduler"
      :readOnly="!edit"
      :barMargin="10"
      :rowHeight="40"
      :autoHeight="true"
      :createEventOnDblClick="false"
      :columns="schedulerConfig.columns"
      :startDate="startDate.toDate()"
      :endDate="endDate.toDate()"
      :eventStore="schedulerConfig.eventStore"
      :viewPreset="schedulerConfig.viewPreset"
      :crudManager="schedulerConfig.crudManager"
      :filterBarFeature="schedulerConfig.features.filterBar"
      :resourceTimeRangesFeature="schedulerConfig.features.resourceTimeRanges"
      :stripeFeature="schedulerConfig.features.stripe"
      :eventContextMenuFeature="schedulerConfig.features.eventContextMenu"
      :eventEditFeature="schedulerConfig.features.eventEdit"
      :eventDragFeature="schedulerConfig.features.eventDrag"
      :eventResizeFeature="schedulerConfig.features.eventResize"
      :afterEventDropFeature="schedulerConfig.features.eventDrop"
      :listeners="schedulerConfig.listeners"
      :eventBodyTemplate="schedulerConfig.eventBodyTemplate"
      :eventRenderer="schedulerConfig.eventRenderer"
      :cellClick="schedulerConfig.onCellClick"
    ></scheduler>

Post by pmiklashevich »

Please create runnable testcase based on one of our examples. Testcase should have minimal code to reproduce the issue. Also please provide steps how to reproduce your issues and what should we pay our attention to. How to ask for help is described here: viewtopic.php?f=35&t=772

Pavlo Miklashevych
Sr. Frontend Developer


Post Reply