Enhance folder creation workflow

Enhance folder creation workflow

<template>
    <app-layout profile="standard">

Content Home

Current folder ID {{id}}

Current folder Path {{folder.path}}

            <app-row>
                <app-cell :span="12">
                    <app-row>
                        <app-cell :span="2" v-if="folder.parentId" key="has-parent">
                            <app-row align="center">
                                <app-icon size="big" theme="dark" symbol="arrow-alt-circle-up" />
                            </app-row>
                            <app-row align="center">
                                <router-link :to="folder.parentId">
                                    Parent Folder
                                </router-link>
                            </app-row>
                        </app-cell>
                    </app-row>
                </app-cell>
            </app-row>
            <app-row v-if="folder.content.folders.length != 0" key="has-subfolders">
                <app-cell tag="h4" :span="12">List of subfolders</app-cell>
                <app-cell :span="12">
                    <app-row>
                        <app-cell :span="2" v-for="subfolder in folder.content.folders" :key="subfolder.id">
                            <app-row align="center">
                                <app-icon size="big" theme="dark" symbol="folder" />
                            </app-row>
                            <app-row align="center">
                                <router-link :to="subfolder.id">
                                    {{subfolder.name}}
                                </router-link>
                            </app-row>
                        </app-cell>
                    </app-row>
                </app-cell>
            </app-row>
            <app-row v-else key="has-subfolders">
                No subfolfers in this directory
            </app-row>
            <app-button @click="showNewFolderDialog" floating>
                <app-icon symbol="plus" solid />
            </app-button>
            <app-dialog
                    ref="dialogBox"
                    title="New folder">
                <app-dialog-content>
                    <app-text-input
                            id="folder-name"
                            required="required"
                            label="Folder's name"
                            v-model="newFolderName"/>
                </app-dialog-content>
                <app-dialog-actions>
                    <button
                            type="button"
                            class="mdl-button"
                            @click="create">
                        Create
                    </button>
                    <button
                            type="button"
                            class="mdl-button"
                            @click="hideNewFolderDialog">
                        Cancel
                    </button>
                </app-dialog-actions>
            </app-dialog>

    </app-layout>
</template>


Use icon for parent folder

Use icon for parent folder

<template>
    <app-layout profile="standard">

Content Home

Current folder ID {{id}}

Current folder Path {{folder.path}}

            <app-row>
                <app-cell :span="12">
                    <app-row>
                        <app-cell :span="2" v-if="folder.parentId" key="has-parent">
                            <app-row align="center">
                                <app-icon size="big" theme="dark" symbol="present_to_all"/>
                            </app-row>
                            <app-row align="center">
                                <router-link :to="folder.parentId">
                                    Parent Folder
                                </router-link>
                            </app-row>
                        </app-cell>
                    </app-row>
                </app-cell>
            </app-row>
            <app-row v-if="folder.content.folders.length != 0" key="has-subfolders">
                <app-cell tag="h4" :span="12">List of subfolders</app-cell>
                <app-cell :span="12">
                    <app-row>
                        <app-cell :span="2" v-for="subfolder in folder.content.folders" :key="subfolder.id">
                            <app-row align="center">
                                <app-icon size="big" theme="dark" symbol="folder_open" />
                            </app-row>
                            <app-row align="center">
                                <router-link :to="subfolder.id">
                                    {{subfolder.name}}
                                </router-link>
                            </app-row>
                        </app-cell>
                    </app-row>
                </app-cell>
            </app-row>
            <app-row v-else key="has-subfolders">
                No subfolfers in this directory
            </app-row>
            <app-button
                    text="New folder"
                    @click="showNewFolderDialog"/>
            <app-dialog
                    ref="dialogBox"
                    title="New folder">
                <app-dialog-content>
                    <app-text-input
                            id="folder-name"
                            required="required"
                            label="Folder's name"
                            v-model="newFolderName"/>
                </app-dialog-content>
                <app-dialog-actions>
                    <button
                            type="button"
                            class="mdl-button"
                            @click="create">
                        Create
                    </button>
                    <button
                            type="button"
                            class="mdl-button"
                            @click="hideNewFolderDialog">
                        Cancel
                    </button>
                </app-dialog-actions>
            </app-dialog>

    </app-layout>
</template>

<template>
    <app-layout profile="standard">

Content Home

Current folder ID {{id}}

Current folder Path {{folder.path}}

                <router-link :to="folder.parentId">Parent Directory</router-link>

            <app-row v-if="folder.content.folders.length != 0" key="has-subfolders">
                <app-cell tag="h4" :span="12">List of subfolders</app-cell>
                <app-cell :span="12">
                    <app-row>
                        <app-cell :span="2" v-for="subfolder in folder.content.folders" :key="subfolder.id">
                            <app-row align="center">
                                <app-icon size="big" theme="dark" symbol="folder_open" />
                            </app-row>
                            <app-row align="center">
                                <router-link :to="subfolder.id">
                                    {{subfolder.name}}
                                </router-link>
                            </app-row>
                        </app-cell>
                    </app-row>
                </app-cell>
            </app-row>
            <app-row v-else key="has-subfolders">
                No subfolfers in this directory
            </app-row>
            <app-button
                    text="New folder"
                    @click="showNewFolderDialog"/>
            <app-dialog
                    ref="dialogBox"
                    title="New folder">
                <app-dialog-content>
                    <app-text-input
                            id="folder-name"
                            required="required"
                            label="Folder's name"
                            v-model="newFolderName"/>
                </app-dialog-content>
                <app-dialog-actions>
                    <button
                            type="button"
                            class="mdl-button"
                            @click="create">
                        Create
                    </button>
                    <button
                            type="button"
                            class="mdl-button"
                            @click="hideNewFolderDialog">
                        Cancel
                    </button>
                </app-dialog-actions>
            </app-dialog>

    </app-layout>
</template>


Fix equalizer CSS

Fix equalizer CSS

<template>

            <button v-for="preset in presets" @click.prevent="loadPreset(preset)">{{ preset.name }}</button>

                {{ band.label }}
                <input type="range" min="-20" max="20" step="0.01" :value="band.filter.gain.value" orient="vertical"
                    @input="changeGain(band.filter, $event)">

</template>


Cleanup unused styles + strings from enroll-page.

<template>

    <confirm-enrollment-modal
      v-if="showConfirmEnrollmentModal"
      :className="className"
      :classId="classId"
      :selectedUsers="selectedUsers"
    />

      <k-filter-textbox
        :placeholder="$tr('searchForUser')"
        v-model.trim="filterInput"
        @input="pageNum = 1"
      />

    <user-table
      v-model="selectedUsers"
      :users="visibleFilteredUsers"
      :title="$tr('userTableLabel')"
      :selectable="true"
      :selectAllLabel="$tr('selectAllOnPage')"
      :userCheckboxLabel="$tr('selectUser')"
      :emptyMessage="$tr('noUsersMatch')"
    />

        {{ $tr('pagination', { visibleStartRange, visibleEndRange, numFilteredUsers }) }}

        <ui-icon-button
          type="primary"
          :icon="isRtl? 'chevron_right' : 'chevron_left'"
          :ariaLabel="$tr('previousResults')"
          :disabled="pageNum === 1"
          size="small"
          @click="goToPage(pageNum - 1)"
        />
        <ui-icon-button
          type="primary"
          :icon="isRtl? 'chevron_left' : 'chevron_right'"
          :ariaLabel="$tr('nextResults')"
          :disabled="pageNum === numPages"
          size="small"
          @click="goToPage(pageNum + 1)"
        />

    <k-button
      :text="$tr('confirmSelectionButtonLabel')"
      :primary="true"
      @click="openConfirmEnrollmentModal"
      :disabled="selectedUsers.length === 0"
    />

</template>

<template>

    <confirm-enrollment-modal
      v-if="showConfirmEnrollmentModal"
      :className="className"
      :classId="classId"
      :selectedUsers="selectedUsers"
    />

{{ $tr('selectLearners', { className }) }}

{{ $tr('showingAllUnassigned') }}

      <k-filter-textbox
        class="filter"
        :class="{ 'invisible' : showSelectedUsers }"
        :placeholder="$tr('searchForUser')"
        v-model.trim="filterInput"
        @input="pageNum = 1"
      />

    <user-table
      v-model="selectedUsers"
      :users="visibleFilteredUsers"
      :title="$tr('userTableLabel')"
      :selectable="true"
      :selectAllLabel="$tr('selectAllOnPage')"
      :userCheckboxLabel="$tr('selectUser')"
      :emptyMessage="$tr('noUsersMatch')"
    />

        {{ $tr('pagination', { visibleStartRange, visibleEndRange, numFilteredUsers }) }}

        <ui-icon-button
          type="primary"
          :icon="isRtl? 'chevron_right' : 'chevron_left'"
          :ariaLabel="$tr('previousResults')"
          :disabled="pageNum === 1"
          size="small"
          @click="goToPage(pageNum - 1)"
        />
        <ui-icon-button
          type="primary"
          :icon="isRtl? 'chevron_left' : 'chevron_right'"
          :ariaLabel="$tr('nextResults')"
          :disabled="pageNum === numPages"
          size="small"
          @click="goToPage(pageNum + 1)"
        />

    <k-button
      :text="$tr('enrollSelectedUsers')"
      :primary="true"
      @click="openConfirmEnrollmentModal"
      :disabled="selectedUsers.length === 0"
    />

</template>


#311 增加leader图标

#311 增加leader图标

<template>

分片情况
                    <el-button type="text" @click="getJobSharding"></el-button>

            <el-table stripe border :data="jobShardings" style="width: 100%">
                <el-table-column prop="executorName" label="Executor" min-width="110px">
                    <template slot-scope="scope">

                        <el-tooltip content="leader" placement="top" v-if="scope.row.leader">

                        </el-tooltip>
                        {{scope.row.executorName}}
                    </template>
                </el-table-column>
                <el-table-column prop="ip" label="IP"></el-table-column>
                <el-table-column label="分片项">
                    <template slot-scope="scope">
                        <el-tag class="sharding-tag" type="primary" v-for="item in $array.strToArray(scope.row.sharding)" :key="item">{{item}}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="version" label="作业版本"></el-table-column>
            </el-table>

</template>

<template>

分片情况
                    <el-button type="text" @click="getJobSharding"></el-button>

            <el-table stripe border :data="jobShardings" style="width: 100%">
                <el-table-column prop="executorName" label="Executor" min-width="100px">
                    <template slot-scope="scope">

                        {{scope.row.executorName}}
                    </template>
                </el-table-column>
                <el-table-column prop="ip" label="IP"></el-table-column>
                <el-table-column label="分片项">
                    <template slot-scope="scope">
                        <el-tag class="sharding-tag" type="primary" v-for="item in $array.strToArray(scope.row.sharding)" :key="item">{{item}}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="version" label="作业版本"></el-table-column>
            </el-table>

</template>


forgot this bit

forgot this bit

<script lang="coffee">
import Records        from '@/shared/services/records'
import ModalService   from '@/shared/services/modal_service'
import EventBus       from '@/shared/services/event_bus'
import utils          from '@/shared/record_store/utils'
import LmoUrlService  from '@/shared/services/lmo_url_service'
import AbilityService from '@/shared/services/ability_service'
import Session from '@/shared/services/session'
import AppConfig      from '@/shared/services/app_config'
import Flash   from '@/shared/services/flash'
import { audiencesFor, audienceValuesFor } from '@/shared/helpers/announcement'
import {each , sortBy, includes, map, pull, uniq, throttle, debounce} from 'lodash'
import { submitForm } from '@/shared/helpers/form'

export default
  props:
    close: Function
    announcement:
      type: Object
      required: true

  data: ->
    query: ''
    recipients: []
    searchResults: []
    upgradeUrl: AppConfig.baseUrl + 'upgrade'
    invitationsRemaining: 1000
    showInvitationsRemaining: false
    subscriptionActive: true
    canInvite: true
    maxMembers: 0
    invitedGroupIds: if @announcement.model.isA('group') then [@announcement.model.id] else []
    historyData: []
    historyLoading: false
    historyOpen: false

  created: ->
    @searchResults = if @invitingToGroup then [] else @announcement.model.members()
    @maxMembers = @announcement.model.group().parentOrSelf().subscriptionMaxMembers || 0
    if @invitingToGroup
      @announcement.model.fetchToken()

    if @announcement.model.group()
      @invitationsRemaining =
        (@announcement.model.group().parentOrSelf().subscriptionMaxMembers || 0) -
        @announcement.model.group().parentOrSelf().orgMembershipsCount

      @showInvitationsRemaining =
        @invitingToGroup &&
        @announcement.model.group().parentOrSelf().subscriptionMaxMembers

      @subscriptionActive = @announcement.model.group().parentOrSelf().subscriptionActive

      @canInvite = @subscriptionActive && (!@announcement.model.group().parentOrSelf().subscriptionMaxMembers || @invitationsRemaining > 0)

  mounted: ->
    @submit = submitForm @, @announcement,
      prepareFn: =>
        @announcement.recipients = @recipients
        @announcement.invitedGroupIds = @invitedGroupIds
      successCallback: (data) =>
        @announcement.membershipsCount = data.memberships.length
        @close()
      flashSuccess: 'announcement.flash.success'
      flashOptions:
        count: => @announcement.membershipsCount

  methods:
    tooManyInvitations: ->
        @showInvitationsRemaining && (@invitationsRemaining < @recipients.length)

    audiences: -> audiencesFor(@announcement.model)

    audienceValues: -> audienceValuesFor(@announcement.model)

    search: debounce (query) ->
      Records.announcements.search(query, @announcement.model).then (data) =>
        if data && data.length == 1 && data[0].id == 'multiple'
          each map(data[0].emails, @buildRecipientFromEmail), @addRecipient
        else
          @searchResults = uniq @recipients.concat utils.parseJSONList(data)
    , 300
    , {trailing: true}

    buildRecipientFromEmail: (email) ->
      id: email
      name: email
      email: email
      emailHash: email
      avatarKind: 'mdi-email-outline'

    copied: (e) ->
      @$copyText(e.text, @$refs.copyContainer.$el)
      Flash.success('common.copied')

    remove: (item) ->
      index = @recipients.indexOf(item)
      if (index >= 0)
        @recipients.splice(index, 1)

    addRecipient: (recipient) ->
      return unless recipient
      return if includes(@recipients, recipient)
      @searchResults.unshift recipient
      @recipients.unshift recipient
      @query = ''

    loadAudience: (kind) ->
      Records.announcements.fetchAudience(@announcement.model, kind).then (data) =>
        each sortBy(utils.parseJSONList(data), (e) => e.name || e.email ), @addRecipient

    resetShareableLink: ->
      @announcement.model.resetToken().then =>
        Flash.success('invitation_form.shareable_link_reset')

    closeHistoryModal: -> @historyOpen = false
    openHistoryModal: ->
      @historyOpen = true
      @historyLoading = true
      Records.announcements.fetchHistoryFor(@announcement.model).then (data) =>
        console.log data
        @historyLoading = false
        @historyData = data

  computed:
    modelKind: -> @announcement.model.constructor.singular
    pollType: -> @announcement.model.pollType
    translatedPollType: -> @announcement.model.poll().translatedPollType() if @announcement.model.isA('poll') or @announcement.model.isA('outcome')
    invitableGroups: ->
      return [] unless @announcement.model.isA('group')
      @announcement.model.subgroups().filter (g) -> AbilityService.canAddMembersToGroup(g)
    canUpdateAnyoneCanParticipate: ->
      @announcement.model.isA('poll') &&
      AbilityService.canAdminister(@announcement.model)

    shareableLink: ->
      if @announcement.model.token
        LmoUrlService.shareableLink(@announcement.model)
      else
        @$t('common.action.loading')

    invitingToGroup: ->
      @announcement.model.isA('group')

  watch:
    query: (q) ->
      @search q if q && q.length > 2
</script>

<template lang="pug">
v-card
  submit-overlay(:value="announcement.processing")
  v-card-title
    h1.headline(v-if="modelKind == 'group'" v-t="{path: 'announcement.send_group', args: {name: announcement.model.name} }")
    h1.headline(v-if="modelKind == 'discussion'" v-t="{path: 'announcement.send_discussion'}")
    h1.headline(v-if="modelKind == 'poll'" v-t="{path: 'announcement.send_poll', args: {type: announcement.model.translatedPollType()}}")
    h1.headline(v-if="modelKind == 'outcome'" v-t="{path: 'announcement.send_outcome'}")
    v-spacer
    dismiss-modal-button(:close="close")
  v-card-text.announcement-form
    p(v-t="{path: 'announcement.send_'+modelKind+'_explain' , args: {type: translatedPollType}}")

    div(v-if="invitingToGroup && !canInvite")
      .announcement-form__invite
        p(v-if="invitationsRemaining < 1" v-html="$t('announcement.form.no_invitations_remaining', {upgradeUrl: upgradeUrl, maxMembers: maxMembers})")
        p(v-if="!subscriptionActive" v-html="$('discussion.subscription_canceled', {upgradeUrl: upgradeUrl})")

    div(v-if="!invitingToGroup || canInvite")
      .announcement-form__invite
        v-autocomplete.announcement-form__input(multiple chips return-object autofocus hide-no-data hide-selected v-model='recipients' @change="query= ''" :search-input.sync="query" item-text='name' item-value="id" item-avatar="avatar_url.large" :placeholder="$t('announcement.form.placeholder')" :items='searchResults')
          template(v-slot:selection='data')
            v-chip.chip--select-multi(:value='data.selected', close, @click:close='remove(data.item)')
              user-avatar.mr-1(:user="data.item" size="small" :no-link="true")
              span {{ data.item.name }}
          template(v-slot:item='data')
            v-list-item-avatar
              user-avatar(:user="data.item" size="small" :no-link="true")
            v-list-item-content.announcement-chip__content
              v-list-item-title(v-html='data.item.name')
        div.text-right(v-if="audiences().length")
          span Quick add:
          space
          span(v-for='(audience, index) in audiences()' :key='audience')
            a.announcement-form__audience(@click='loadAudience(audience)' v-t="{ path: 'announcement.audiences.' + audience, args: audienceValues() }")
            span(v-if="index < audiences().length - 1")
              | ,
              space

      div(v-if="invitableGroups.length")
        span Any other groups?
        div(v-for="group in invitableGroups" :key="group.id")
          v-checkbox(v-model="invitedGroupIds" :label="group.name" :value="group.id" hide-details)

      v-layout(v-if="showInvitationsRemaining")
        v-spacer
        p.caption(v-html="$t('announcement.form.invitations_remaining', {count: invitationsRemaining, upgradeUrl: upgradeUrl })")

  v-card-actions
    v-dialog(v-model="historyOpen")
      template(v-slot:activator="{on}")
        v-btn.text(@click="openHistoryModal()" v-on="on" v-t="common.history")
      v-card
        v-card-title
          h1.headline(v-t="'announcement.' + modelKind + '_notification_history'")
          v-spacer
          dismiss-modal-button(:close="closeHistoryModal")
        v-progress-circular(v-if="historyLoading" indeterminate)
        v-card-text(v-if="!historyLoading")
          p(v-for="event in historyData" :key="event.id")
            span.body-1
              time-ago(:date="event.created_at")
              mid-dot
              span {{event.author_name}}
              space
              span notified {{event.notifications.length}} people
            br
            ul
              li(v-for="notification in event.notifications" :key="notification.id")
                span {{notification.to}}
                space
                span(v-if="notification.viewed") (seen)
                span(v-if="!notification.viewed") (not seen)
    div(v-if="recipients.length")
      p(v-show="invitingToGroup && tooManyInvitations()" v-html="$t('announcement.form.too_many_invitations', {upgradeUrl: upgradeUrl})")
    v-spacer
    v-btn.announcement-form__submit(color="primary" :disabled="!recipients.length || (invitingToGroup && tooManyInvitations())" @click="submit()" v-t="'common.action.send'")
</template>

<style lang="css">
.announcement-form__checkbox {
  margin: 16px 0;
}

.announcement-form__list-item,
.announcement-form__invited {
  padding: 0 !important;
}

.announcement-form__invite {
  margin-bottom: 16px;
}

.announcement-form__shareable-link,
.announcement-form__help {
  margin: 8px 0;
}

.announcement-form__audience {
  height: 42px;
  min-height: 42px;
  margin-bottom: 8px;
  /* // color: $primary-text-color;
  i { opacity: 0.8; } */
}
</style>

<script lang="coffee">
import Records        from '@/shared/services/records'
import ModalService   from '@/shared/services/modal_service'
import EventBus       from '@/shared/services/event_bus'
import utils          from '@/shared/record_store/utils'
import LmoUrlService  from '@/shared/services/lmo_url_service'
import AbilityService from '@/shared/services/ability_service'
import Session from '@/shared/services/session'
import AppConfig      from '@/shared/services/app_config'
import Flash   from '@/shared/services/flash'
import { audiencesFor, audienceValuesFor } from '@/shared/helpers/announcement'
import {each , sortBy, includes, map, pull, uniq, throttle, debounce} from 'lodash'
import { submitForm } from '@/shared/helpers/form'

export default
  props:
    close: Function
    announcement:
      type: Object
      required: true

  data: ->
    query: ''
    recipients: []
    searchResults: []
    upgradeUrl: AppConfig.baseUrl + 'upgrade'
    invitationsRemaining: 1000
    showInvitationsRemaining: false
    subscriptionActive: true
    canInvite: true
    maxMembers: 0
    invitedGroupIds: if @announcement.model.isA('group') then [@announcement.model.id] else []
    historyData: []
    historyLoading: false
    historyOpen: false

  created: ->
    @searchResults = if @invitingToGroup then [] else @announcement.model.members()
    @maxMembers = @announcement.model.group().parentOrSelf().subscriptionMaxMembers || 0
    if @invitingToGroup
      @announcement.model.fetchToken()

    if @announcement.model.group()
      @invitationsRemaining =
        (@announcement.model.group().parentOrSelf().subscriptionMaxMembers || 0) -
        @announcement.model.group().parentOrSelf().orgMembershipsCount

      @showInvitationsRemaining =
        @invitingToGroup &&
        @announcement.model.group().parentOrSelf().subscriptionMaxMembers

      @subscriptionActive = @announcement.model.group().parentOrSelf().subscriptionActive

      @canInvite = @subscriptionActive && (!@announcement.model.group().parentOrSelf().subscriptionMaxMembers || @invitationsRemaining > 0)

  mounted: ->
    @submit = submitForm @, @announcement,
      prepareFn: =>
        @announcement.recipients = @recipients
        @announcement.invitedGroupIds = @invitedGroupIds
      successCallback: (data) =>
        @announcement.membershipsCount = data.memberships.length
        @close()
      flashSuccess: 'announcement.flash.success'
      flashOptions:
        count: => @announcement.membershipsCount

  methods:
    tooManyInvitations: ->
        @showInvitationsRemaining && (@invitationsRemaining < @recipients.length)

    audiences: -> audiencesFor(@announcement.model)

    audienceValues: -> audienceValuesFor(@announcement.model)

    search: debounce (query) ->
      Records.announcements.search(query, @announcement.model).then (data) =>
        if data && data.length == 1 && data[0].id == 'multiple'
          each map(data[0].emails, @buildRecipientFromEmail), @addRecipient
        else
          @searchResults = uniq @recipients.concat utils.parseJSONList(data)
    , 300
    , {trailing: true}

    buildRecipientFromEmail: (email) ->
      id: email
      name: email
      email: email
      emailHash: email
      avatarKind: 'mdi-email-outline'

    copied: (e) ->
      @$copyText(e.text, @$refs.copyContainer.$el)
      Flash.success('common.copied')

    remove: (item) ->
      index = @recipients.indexOf(item)
      if (index >= 0)
        @recipients.splice(index, 1)

    addRecipient: (recipient) ->
      return unless recipient
      return if includes(@recipients, recipient)
      @searchResults.unshift recipient
      @recipients.unshift recipient
      @query = ''

    loadAudience: (kind) ->
      Records.announcements.fetchAudience(@announcement.model, kind).then (data) =>
        each sortBy(utils.parseJSONList(data), (e) => e.name || e.email ), @addRecipient

    resetShareableLink: ->
      @announcement.model.resetToken().then =>
        Flash.success('invitation_form.shareable_link_reset')

    closeHistoryModal: -> @historyOpen = false
    openHistoryModal: ->
      @historyOpen = true
      @historyLoading = true
      Records.announcements.fetchHistoryFor(@announcement.model).then (data) =>
        console.log data
        @historyLoading = false
        @historyData = data

  computed:
    modelKind: -> @announcement.model.constructor.singular
    pollType: -> @announcement.model.pollType
    translatedPollType: -> @announcement.model.poll().translatedPollType() if @announcement.model.isA('poll') or @announcement.model.isA('outcome')
    invitableGroups: ->
      return [] unless @announcement.model.isA('group')
      @announcement.model.subgroups().filter (g) -> AbilityService.canAddMembersToGroup(g)
    canUpdateAnyoneCanParticipate: ->
      @announcement.model.isA('poll') &&
      AbilityService.canAdminister(@announcement.model)

    shareableLink: ->
      if @announcement.model.token
        LmoUrlService.shareableLink(@announcement.model)
      else
        @$t('common.action.loading')

    invitingToGroup: ->
      @announcement.model.isA('group')

  watch:
    query: (q) ->
      @search q if q && q.length > 2
</script>

<template lang="pug">
v-card
  submit-overlay(:value="announcement.processing")
  v-card-title
    h1.headline(v-if="modelKind == 'group'" v-t="{path: 'announcement.send_group', args: {name: announcement.model.name} }")
    h1.headline(v-if="modelKind == 'discussion'" v-t="{path: 'announcement.send_discussion'}")
    h1.headline(v-if="modelKind == 'poll'" v-t="{path: 'announcement.send_poll', args: {type: announcement.model.translatedPollType()}}")
    h1.headline(v-if="modelKind == 'outcome'" v-t="{path: 'announcement.send_outcome'}")
    v-spacer
    dismiss-modal-button(:close="close")
  v-card-text.announcement-form
    p(v-t="{path: 'announcement.send_'+modelKind+'_explain' , args: {type: translatedPollType}}")

    div(v-if="invitingToGroup && !canInvite")
      .announcement-form__invite
        p(v-if="invitationsRemaining < 1" v-html="$t('announcement.form.no_invitations_remaining', {upgradeUrl: upgradeUrl, maxMembers: maxMembers})")
        p(v-if="!subscriptionActive" v-html="$('discussion.subscription_canceled', {upgradeUrl: upgradeUrl})")

    div(v-if="!invitingToGroup || canInvite")
      .announcement-form__invite
        v-autocomplete.announcement-form__input(multiple chips return-object autofocus hide-no-data hide-selected v-model='recipients' @change="query= ''" :search-input.sync="query" item-text='name' item-value="id" item-avatar="avatar_url.large" :placeholder="$t('announcement.form.placeholder')" :items='searchResults')
          template(v-slot:selection='data')
            v-chip.chip--select-multi(:value='data.selected', close, @click:close='remove(data.item)')
              user-avatar.mr-1(:user="data.item" size="small" :no-link="true")
              span {{ data.item.name }}
          template(v-slot:item='data')
            v-list-item-avatar
              user-avatar(:user="data.item" size="small" :no-link="true")
            v-list-item-content.announcement-chip__content
              v-list-item-title(v-html='data.item.name')
        div.text-right(v-if="audiences().length")
          span Quick add:
          space
          span(v-for='(audience, index) in audiences()' :key='audience')
            a.announcement-form__audience(@click='loadAudience(audience)' v-t="{ path: 'announcement.audiences.' + audience, args: audienceValues() }")
            span(v-if="index < audiences().length - 1")
              | ,
              space

      div(v-if="invitableGroups.length")
        span Any other groups?
        div(v-for="group in invitableGroups" :key="group.id")
          v-checkbox(v-model="invitedGroupIds" :label="group.name" :value="group.id" hide-details)

      v-layout(v-if="showInvitationsRemaining")
        v-spacer
        p.caption(v-html="$t('announcement.form.invitations_remaining', {count: invitationsRemaining, upgradeUrl: upgradeUrl })")

  v-card-actions
    v-dialog(v-model="historyOpen")
      template(v-slot:activator="{on}")
        v-btn(@click="openHistoryModal()" v-on="on") History
      v-card
        v-card-title
          h1.headline(v-t="'announcement.' + modelKind + '_notification_history'")
          v-spacer
          dismiss-modal-button(:close="closeHistoryModal")
        v-progress-circular(v-if="historyLoading" indeterminate)
        v-card-text(v-if="!historyLoading")
          p(v-for="event in historyData" :key="event.id")
            span.body-1
              time-ago(:date="event.created_at")
              mid-dot
              span {{event.author_name}}
              space
              span notified {{event.notifications.length}} people
            br
            ul
              li(v-for="notification in event.notifications" :key="notification.id")
                span {{notification.to}}
                space
                span(v-if="notification.viewed") (seen)
                span(v-if="!notification.viewed") (not seen)
    div(v-if="recipients.length")
      p(v-show="invitingToGroup && tooManyInvitations()" v-html="$t('announcement.form.too_many_invitations', {upgradeUrl: upgradeUrl})")
    v-spacer
    v-btn.announcement-form__submit(color="primary" :disabled="!recipients.length || (invitingToGroup && tooManyInvitations())" @click="submit()" v-t="'common.action.send'")
</template>

<style lang="css">
.announcement-form__checkbox {
  margin: 16px 0;
}

.announcement-form__list-item,
.announcement-form__invited {
  padding: 0 !important;
}

.announcement-form__invite {
  margin-bottom: 16px;
}

.announcement-form__shareable-link,
.announcement-form__help {
  margin: 8px 0;
}

.announcement-form__audience {
  height: 42px;
  min-height: 42px;
  margin-bottom: 8px;
  /* // color: $primary-text-color;
  i { opacity: 0.8; } */
}
</style>


fix durations select on time poll

fix durations select on time poll

<script lang="coffee">
import AppConfig from '@/shared/services/app_config'
import EventBus  from '@/shared/services/event_bus'
import { addDays, addMinutes, formatDistanceToNowStrict } from 'date-fns'
import { pull } from 'lodash-es'

export default
  props:
    poll: Object
    back: Boolean

  data: ->
    durations:
      [5, 10, 15, 20, 30, 45, 60, 120, 180, 240, null].map (minutes) =>
        if minutes
          {text: formatDistanceToNowStrict(addMinutes(new Date, minutes)), value: minutes}
        else
          {text: @$t('common.all_day'), value: null}
    menu: false

  created: ->
    @poll.customFields.meeting_duration = @poll.customFields.meeting_duration or 60
    if @poll.isNew()
      @poll.canRespondMaybe = true
      @poll.closingAt = addDays(new Date, 3)
      @poll.notifyOnParticipate = true
    # EventBus.listen $scope, 'timeZoneSelected', (e, zone) ->
    #   $scope.poll.customFields.time_zone = zone
</script>

<template lang="pug">
.poll-meeting-form
  poll-common-form-fields(:poll="poll")
  poll-meeting-form-options-field(:poll="poll")
  v-select(v-model="poll.customFields.meeting_duration" :label="$t('poll_meeting_form.meeting_duration')" :items="durations")
  poll-common-closing-at-field(:poll="poll")
  poll-common-settings(:poll="poll")
</template>

<script lang="coffee">
import AppConfig from '@/shared/services/app_config'
import EventBus  from '@/shared/services/event_bus'
import { addDays } from 'date-fns'

import { pull } from 'lodash-es'

export default
  props:
    poll: Object
    back: Boolean

  data: ->
    durations: AppConfig.durations
    menu: false

  created: ->
    @poll.customFields.meeting_duration = @poll.customFields.meeting_duration or 60
    if @poll.isNew()
      @poll.canRespondMaybe = true
      @poll.closingAt = addDays(new Date, 3)
      @poll.notifyOnParticipate = true
    # EventBus.listen $scope, 'timeZoneSelected', (e, zone) ->
    #   $scope.poll.customFields.time_zone = zone
</script>

<template lang="pug">
.poll-meeting-form
  poll-common-form-fields(:poll="poll")
  poll-meeting-form-options-field(:poll="poll")
  v-select(v-model="poll.customFields.meeting_duration" :label="$t('poll_meeting_form.meeting_duration')" :items="durations", item-text="label", item-value="minutes")
  poll-common-closing-at-field(:poll="poll")
  poll-common-settings(:poll="poll")
</template>


No need to assign `undefined`

No need to assign `undefined`

<script>
import PopoverRef from './PopoverRef';
import { childMixin, safeScopedSlotMixin } from '@/utils/mixins';
import { arrayHasItems } from '@/utils/helpers';
import { last, get } from '@/utils/_';

export default {
  name: 'CalendarDay',
  mixins: [childMixin, safeScopedSlotMixin],
  render(h) {
    // Backgrounds layer
    const backgroundsLayer = () =>
      this.hasBackgrounds &&
      h(
        'div',
        {
          class: 'vc-highlights vc-day-layer',
        },
        this.backgrounds.map(({ key, wrapperClass, class: bgClass }) =>
          h(
            'div',
            {
              key,
              class: wrapperClass,
            },
            [
              h('div', {
                class: bgClass,
              }),
            ],
          ),
        ),
      );

    // Content layer
    const contentLayer = () =>
      this.safeScopedSlot('day-content', {
        day: this.day,
        attributes: this.day.attributes,
        attributesMap: this.day.attributesMap,
        dayProps: this.dayContentProps,
        dayEvents: this.dayContentEvents,
      }) ||
      h(
        'span',
        {
          class: this.dayContentClass,
          attrs: { ...this.dayContentProps },
          on: this.dayContentEvents,
          ref: 'content',
        },
        [this.day.label],
      );

    // Popover content wrapper
    const contentWrapperLayer = () => {
      if (!this.hasPopovers) {
        return contentLayer();
      }
      const { visibility, placement, isInteractive } = this.popoverState;
      return h(
        PopoverRef,
        {
          props: {
            id: this.dayPopoverId,
            args: this.dayEvent,
            visibility,
            placement,
            isInteractive,
          },
        },
        [contentLayer()],
      );
    };

    // Dots layer
    const dotsLayer = () =>
      this.hasDots &&
      h(
        'div',
        {
          class: 'vc-day-layer vc-day-box-center-bottom',
        },
        [
          h(
            'div',
            {
              class: 'vc-dots',
            },
            this.dots.map(({ key, class: bgClass }) =>
              h('span', {
                class: bgClass,
                key,
              }),
            ),
          ),
        ],
      );

    // Bars layer
    const barsLayer = () =>
      this.hasBars &&
      h(
        'div',
        {
          class: 'vc-day-layer vc-day-box-center-bottom',
        },
        [
          h(
            'div',
            {
              class: 'vc-bars',
            },
            this.bars.map(({ key, class: bgClass }) =>
              h('span', {
                class: bgClass,
                key,
              }),
            ),
          ),
        ],
      );

    // Root layer
    return h(
      'div',
      {
        class: [
          'vc-day',
          ...this.day.classes,
          { 'vc-day-box-center-center': !this.$scopedSlots['day-content'] },
        ],
      },
      [
        h(
          'div',
          {
            class: ['vc-h-full', { [this.theme.dayNotInMonth]: !this.inMonth }],
          },
          [backgroundsLayer(), contentWrapperLayer(), dotsLayer(), barsLayer()],
        ),
      ],
    );
  },
  inject: ['sharedState'],
  props: {
    day: { type: Object, required: true },
  },
  data() {
    return {
      glyphs: {},
      popoverState: {},
    };
  },
  computed: {
    label() {
      return this.day.label;
    },
    dateTime() {
      return this.day.dateTime;
    },
    inMonth() {
      return this.day.inMonth;
    },
    isDisabled() {
      return this.day.isDisabled;
    },
    backgrounds() {
      return this.glyphs.backgrounds;
    },
    hasBackgrounds() {
      return !!arrayHasItems(this.backgrounds);
    },
    content() {
      return this.glyphs.content;
    },
    dots() {
      return this.glyphs.dots;
    },
    hasDots() {
      return !!arrayHasItems(this.dots);
    },
    bars() {
      return this.glyphs.bars;
    },
    hasBars() {
      return !!arrayHasItems(this.bars);
    },
    popovers() {
      return this.glyphs.popovers;
    },
    hasPopovers() {
      return !!arrayHasItems(this.popovers);
    },
    dayContentClass() {
      const contentClass = get(last(this.content), 'class') || '';
      return `vc-day-content vc-focusable ${
        this.isDisabled ? this.theme.dayContentDisabled : this.theme.dayContent
      } ${this.theme.isDark ? 'vc-is-dark' : ''} ${contentClass}`;
    },
    dayContentProps() {
      let tabindex;
      if (this.day.isFocusable) {
        tabindex = '0';
      } else if (this.day.inMonth) {
        tabindex = '-1';
      }
      return {
        tabindex,
        'aria-label': this.day.ariaLabel,
      };
    },
    dayContentEvents() {
      return {
        click: this.click,
        mouseenter: this.mouseenter,
        mouseleave: this.mouseleave,
        focusin: this.focusin,
        focusout: this.focusout,
        keydown: this.keydown,
      };
    },
    dayEvent() {
      return {
        ...this.day,
        el: this.$refs.content,
        popovers: this.popovers,
      };
    },
  },
  watch: {
    theme() {
      this.refresh();
    },
    popovers() {
      const visibilities = ['click', 'focus', 'hover', 'visible'];
      let placement = '';
      let isInteractive = false;
      let vIdx = -1;
      this.popovers.forEach(p => {
        const vNew = visibilities.indexOf(p.visibility);
        vIdx = vNew > vIdx ? vNew : vIdx;
        placement = placement || p.placement;
        isInteractive = isInteractive || p.isInteractive;
      });
      this.popoverState = {
        visibility: vIdx >= 0 ? visibilities[vIdx] : 'hidden',
        placement: placement || 'bottom',
        isInteractive,
      };
    },
  },
  methods: {
    getDayEvent(origEvent) {
      return {
        ...this.dayEvent,
        event: origEvent,
      };
    },
    click(e) {
      this.$emit('dayclick', this.getDayEvent(e));
    },
    mouseenter(e) {
      this.$emit('daymouseenter', this.getDayEvent(e));
    },
    mouseleave(e) {
      this.$emit('daymouseleave', this.getDayEvent(e));
    },
    focusin(e) {
      this.$emit('dayfocusin', this.getDayEvent(e));
    },
    focusout(e) {
      this.$emit('dayfocusout', this.getDayEvent(e));
    },
    keydown(e) {
      this.$emit('daykeydown', this.getDayEvent(e));
    },
    refresh() {
      if (!this.day.refresh) return;
      this.day.refresh = false;
      const glyphs = {
        backgrounds: [],
        dots: [],
        bars: [],
        popovers: [],
        content: [],
      };
      this.day.attributes = Object.values(this.day.attributesMap || {}).sort(
        (a, b) => a.order - b.order,
      );
      this.day.attributes.forEach(attr => {
        // Add glyphs for each attribute
        const { targetDate } = attr;
        const { isDate, isComplex, startTime, endTime } = targetDate;
        const onStart = startTime === this.dateTime;
        const onEnd = endTime === this.dateTime;
        const onStartAndEnd = onStart && onEnd;
        const onStartOrEnd = onStart || onEnd;
        const dateInfo = {
          isDate,
          isComplex,
          onStart,
          onEnd,
          onStartAndEnd,
          onStartOrEnd,
        };
        this.processHighlight(attr, dateInfo, glyphs);
        this.processContent(attr, dateInfo, glyphs);
        this.processDot(attr, dateInfo, glyphs);
        this.processBar(attr, dateInfo, glyphs);
        this.processPopover(attr, glyphs);
      });
      this.glyphs = glyphs;
    },
    processHighlight(
      { key, highlight },
      { isDate, isComplex, onStart, onEnd, onStartAndEnd },
      { backgrounds, content },
    ) {
      if (!highlight) return;
      const { base, start, end } = highlight;
      if (isDate || isComplex) {
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${start.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: start.contentClass,
        });
      } else if (onStartAndEnd) {
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${start.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: start.contentClass,
        });
      } else if (onStart) {
        backgrounds.push({
          key: `${key}-base`,
          wrapperClass: 'vc-day-layer vc-day-box-right-center',
          class: `vc-highlight vc-highlight-base-start ${base.class}`,
        });
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${start.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: start.contentClass,
        });
      } else if (onEnd) {
        backgrounds.push({
          key: `${key}-base`,
          wrapperClass: 'vc-day-layer vc-day-box-left-center',
          class: `vc-highlight vc-highlight-base-end ${base.class}`,
        });
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${end.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: end.contentClass,
        });
      } else {
        backgrounds.push({
          key: `${key}-middle`,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight vc-highlight-base-middle ${base.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: base.contentClass,
        });
      }
    },
    processContent(
      { key, content },
      { isDate, onStart, onEnd },
      { content: contents },
    ) {
      if (!content) return;
      const { base, start, end } = content;
      if (isDate || onStart) {
        contents.push({
          key,
          class: start.class,
        });
      } else if (onEnd) {
        contents.push({
          key,
          class: end.class,
        });
      } else {
        contents.push({
          key,
          class: base.class,
        });
      }
    },
    processDot({ key, dot }, { isDate, onStart, onEnd }, { dots }) {
      if (!dot) return;
      const { base, start, end } = dot;
      if (isDate || onStart) {
        dots.push({
          key,
          class: `vc-dot ${start.class}`,
        });
      } else if (onEnd) {
        dots.push({
          key,
          class: `vc-dot ${end.class}`,
        });
      } else {
        dots.push({
          key,
          class: `vc-dot ${base.class}`,
        });
      }
    },
    processBar({ key, bar }, { isDate, onStart, onEnd }, { bars }) {
      if (!bar) return;
      const { base, start, end } = bar;
      if (isDate || onStart) {
        bars.push({
          key,
          class: `vc-bar ${start.class}`,
        });
      } else if (onEnd) {
        bars.push({
          key,
          class: `vc-bar ${end.class}`,
        });
      } else {
        bars.push({
          key,
          class: `vc-bar ${base.class}`,
        });
      }
    },
    processPopover(attribute, { popovers }) {
      if (!attribute.popover) return;
      const {
        label,
        labelClass,
        labelStyle,
        hideIndicator,
        visibility,
        placement,
        isInteractive,
      } = attribute.popover;
      popovers.splice(0, 0, {
        key: attribute.key,
        customData: attribute.customData,
        attribute,
        label,
        labelClass,
        labelStyle,
        hideIndicator,
        visibility: visibility || (label ? 'hover' : 'click'),
        placement: placement || 'bottom',
        isInteractive: isInteractive !== undefined ? isInteractive : !label,
      });
    },
  },
};
</script>

<style lang="postcss" scoped>
.vc-day {
  position: relative;
  min-height: var(--day-min-height);
  width: 100%;
  height: 100%;
  z-index: 1;
}

.vc-day-layer {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  pointer-events: none;
}

.vc-day-box-center-center {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  transform-origin: 50% 50%;
}

.vc-day-box-left-center {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  height: 100%;
  transform-origin: 0% 50%;
}

.vc-day-box-right-center {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  height: 100%;
  transform-origin: 100% 50%;
}

.vc-day-box-center-bottom {
  display: flex;
  justify-content: center;
  align-items: flex-end;
}

.vc-day-content {
  display: flex;
  justify-content: center;
  align-items: center;
  width: var(--day-content-width);
  height: var(--day-content-height);
  margin: var(--day-content-margin);
  user-select: none;
  &:hover {
    background-color: var(--day-content-bg-color-hover);
    &.vc-is-dark {
      background-color: var(--day-content-dark-bg-color-hover);
    }
  }
  &:focus {
    background-color: var(--day-content-bg-color-focus);
    &.vc-is-dark {
      background-color: var(--day-content-dark-bg-color-focus);
    }
  }
}

.vc-highlights {
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
}

.vc-highlight {
  width: var(--highlight-height);
  height: var(--highlight-height);
  &.vc-highlight-base-start {
    width: 50% !important;
    border-radius: 0 !important;
    border-right-width: 0 !important;
  }
  &.vc-highlight-base-end {
    width: 50% !important;
    border-radius: 0 !important;
    border-left-width: 0 !important;
  }
  &.vc-highlight-base-middle {
    width: 100%;
    border-radius: 0 !important;
    border-left-width: 0 !important;
    border-right-width: 0 !important;
    margin: 0 -1px;
  }
}

.vc-dots {
  display: flex;
  justify-content: center;
  align-items: center;
}

.vc-dot {
  width: var(--dot-diameter);
  height: var(--dot-diameter);
  border-radius: var(--dot-border-radius);
  transition: all var(--day-content-transition-time);
  &:not(:last-child) {
    margin-right: var(--dot-spacing);
  }
}

.vc-bars {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  width: var(--bars-width);
}

.vc-bar {
  flex-grow: 1;
  height: var(--bar-height);
  transition: all var(--day-content-transition-time);
}
</style>

<script>
import PopoverRef from './PopoverRef';
import { childMixin, safeScopedSlotMixin } from '@/utils/mixins';
import { arrayHasItems } from '@/utils/helpers';
import { last, get } from '@/utils/_';

export default {
  name: 'CalendarDay',
  mixins: [childMixin, safeScopedSlotMixin],
  render(h) {
    // Backgrounds layer
    const backgroundsLayer = () =>
      this.hasBackgrounds &&
      h(
        'div',
        {
          class: 'vc-highlights vc-day-layer',
        },
        this.backgrounds.map(({ key, wrapperClass, class: bgClass }) =>
          h(
            'div',
            {
              key,
              class: wrapperClass,
            },
            [
              h('div', {
                class: bgClass,
              }),
            ],
          ),
        ),
      );

    // Content layer
    const contentLayer = () =>
      this.safeScopedSlot('day-content', {
        day: this.day,
        attributes: this.day.attributes,
        attributesMap: this.day.attributesMap,
        dayProps: this.dayContentProps,
        dayEvents: this.dayContentEvents,
      }) ||
      h(
        'span',
        {
          class: this.dayContentClass,
          attrs: { ...this.dayContentProps },
          on: this.dayContentEvents,
          ref: 'content',
        },
        [this.day.label],
      );

    // Popover content wrapper
    const contentWrapperLayer = () => {
      if (!this.hasPopovers) {
        return contentLayer();
      }
      const { visibility, placement, isInteractive } = this.popoverState;
      return h(
        PopoverRef,
        {
          props: {
            id: this.dayPopoverId,
            args: this.dayEvent,
            visibility,
            placement,
            isInteractive,
          },
        },
        [contentLayer()],
      );
    };

    // Dots layer
    const dotsLayer = () =>
      this.hasDots &&
      h(
        'div',
        {
          class: 'vc-day-layer vc-day-box-center-bottom',
        },
        [
          h(
            'div',
            {
              class: 'vc-dots',
            },
            this.dots.map(({ key, class: bgClass }) =>
              h('span', {
                class: bgClass,
                key,
              }),
            ),
          ),
        ],
      );

    // Bars layer
    const barsLayer = () =>
      this.hasBars &&
      h(
        'div',
        {
          class: 'vc-day-layer vc-day-box-center-bottom',
        },
        [
          h(
            'div',
            {
              class: 'vc-bars',
            },
            this.bars.map(({ key, class: bgClass }) =>
              h('span', {
                class: bgClass,
                key,
              }),
            ),
          ),
        ],
      );

    // Root layer
    return h(
      'div',
      {
        class: [
          'vc-day',
          ...this.day.classes,
          { 'vc-day-box-center-center': !this.$scopedSlots['day-content'] },
        ],
      },
      [
        h(
          'div',
          {
            class: ['vc-h-full', { [this.theme.dayNotInMonth]: !this.inMonth }],
          },
          [backgroundsLayer(), contentWrapperLayer(), dotsLayer(), barsLayer()],
        ),
      ],
    );
  },
  inject: ['sharedState'],
  props: {
    day: { type: Object, required: true },
  },
  data() {
    return {
      glyphs: {},
      popoverState: {},
    };
  },
  computed: {
    label() {
      return this.day.label;
    },
    dateTime() {
      return this.day.dateTime;
    },
    inMonth() {
      return this.day.inMonth;
    },
    isDisabled() {
      return this.day.isDisabled;
    },
    backgrounds() {
      return this.glyphs.backgrounds;
    },
    hasBackgrounds() {
      return !!arrayHasItems(this.backgrounds);
    },
    content() {
      return this.glyphs.content;
    },
    dots() {
      return this.glyphs.dots;
    },
    hasDots() {
      return !!arrayHasItems(this.dots);
    },
    bars() {
      return this.glyphs.bars;
    },
    hasBars() {
      return !!arrayHasItems(this.bars);
    },
    popovers() {
      return this.glyphs.popovers;
    },
    hasPopovers() {
      return !!arrayHasItems(this.popovers);
    },
    dayContentClass() {
      const contentClass = get(last(this.content), 'class') || '';
      return `vc-day-content vc-focusable ${
        this.isDisabled ? this.theme.dayContentDisabled : this.theme.dayContent
      } ${this.theme.isDark ? 'vc-is-dark' : ''} ${contentClass}`;
    },
    dayContentProps() {
      let tabindex = undefined;
      if (this.day.isFocusable) {
        tabindex = '0';
      } else if (this.day.inMonth) {
        tabindex = '-1';
      }
      return {
        tabindex,
        'aria-label': this.day.ariaLabel,
      };
    },
    dayContentEvents() {
      return {
        click: this.click,
        mouseenter: this.mouseenter,
        mouseleave: this.mouseleave,
        focusin: this.focusin,
        focusout: this.focusout,
        keydown: this.keydown,
      };
    },
    dayEvent() {
      return {
        ...this.day,
        el: this.$refs.content,
        popovers: this.popovers,
      };
    },
  },
  watch: {
    theme() {
      this.refresh();
    },
    popovers() {
      const visibilities = ['click', 'focus', 'hover', 'visible'];
      let placement = '';
      let isInteractive = false;
      let vIdx = -1;
      this.popovers.forEach(p => {
        const vNew = visibilities.indexOf(p.visibility);
        vIdx = vNew > vIdx ? vNew : vIdx;
        placement = placement || p.placement;
        isInteractive = isInteractive || p.isInteractive;
      });
      this.popoverState = {
        visibility: vIdx >= 0 ? visibilities[vIdx] : 'hidden',
        placement: placement || 'bottom',
        isInteractive,
      };
    },
  },
  methods: {
    getDayEvent(origEvent) {
      return {
        ...this.dayEvent,
        event: origEvent,
      };
    },
    click(e) {
      this.$emit('dayclick', this.getDayEvent(e));
    },
    mouseenter(e) {
      this.$emit('daymouseenter', this.getDayEvent(e));
    },
    mouseleave(e) {
      this.$emit('daymouseleave', this.getDayEvent(e));
    },
    focusin(e) {
      this.$emit('dayfocusin', this.getDayEvent(e));
    },
    focusout(e) {
      this.$emit('dayfocusout', this.getDayEvent(e));
    },
    keydown(e) {
      this.$emit('daykeydown', this.getDayEvent(e));
    },
    refresh() {
      if (!this.day.refresh) return;
      this.day.refresh = false;
      const glyphs = {
        backgrounds: [],
        dots: [],
        bars: [],
        popovers: [],
        content: [],
      };
      this.day.attributes = Object.values(this.day.attributesMap || {}).sort(
        (a, b) => a.order - b.order,
      );
      this.day.attributes.forEach(attr => {
        // Add glyphs for each attribute
        const { targetDate } = attr;
        const { isDate, isComplex, startTime, endTime } = targetDate;
        const onStart = startTime === this.dateTime;
        const onEnd = endTime === this.dateTime;
        const onStartAndEnd = onStart && onEnd;
        const onStartOrEnd = onStart || onEnd;
        const dateInfo = {
          isDate,
          isComplex,
          onStart,
          onEnd,
          onStartAndEnd,
          onStartOrEnd,
        };
        this.processHighlight(attr, dateInfo, glyphs);
        this.processContent(attr, dateInfo, glyphs);
        this.processDot(attr, dateInfo, glyphs);
        this.processBar(attr, dateInfo, glyphs);
        this.processPopover(attr, glyphs);
      });
      this.glyphs = glyphs;
    },
    processHighlight(
      { key, highlight },
      { isDate, isComplex, onStart, onEnd, onStartAndEnd },
      { backgrounds, content },
    ) {
      if (!highlight) return;
      const { base, start, end } = highlight;
      if (isDate || isComplex) {
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${start.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: start.contentClass,
        });
      } else if (onStartAndEnd) {
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${start.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: start.contentClass,
        });
      } else if (onStart) {
        backgrounds.push({
          key: `${key}-base`,
          wrapperClass: 'vc-day-layer vc-day-box-right-center',
          class: `vc-highlight vc-highlight-base-start ${base.class}`,
        });
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${start.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: start.contentClass,
        });
      } else if (onEnd) {
        backgrounds.push({
          key: `${key}-base`,
          wrapperClass: 'vc-day-layer vc-day-box-left-center',
          class: `vc-highlight vc-highlight-base-end ${base.class}`,
        });
        backgrounds.push({
          key,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight ${end.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: end.contentClass,
        });
      } else {
        backgrounds.push({
          key: `${key}-middle`,
          wrapperClass: 'vc-day-layer vc-day-box-center-center',
          class: `vc-highlight vc-highlight-base-middle ${base.class}`,
        });
        content.push({
          key: `${key}-content`,
          class: base.contentClass,
        });
      }
    },
    processContent(
      { key, content },
      { isDate, onStart, onEnd },
      { content: contents },
    ) {
      if (!content) return;
      const { base, start, end } = content;
      if (isDate || onStart) {
        contents.push({
          key,
          class: start.class,
        });
      } else if (onEnd) {
        contents.push({
          key,
          class: end.class,
        });
      } else {
        contents.push({
          key,
          class: base.class,
        });
      }
    },
    processDot({ key, dot }, { isDate, onStart, onEnd }, { dots }) {
      if (!dot) return;
      const { base, start, end } = dot;
      if (isDate || onStart) {
        dots.push({
          key,
          class: `vc-dot ${start.class}`,
        });
      } else if (onEnd) {
        dots.push({
          key,
          class: `vc-dot ${end.class}`,
        });
      } else {
        dots.push({
          key,
          class: `vc-dot ${base.class}`,
        });
      }
    },
    processBar({ key, bar }, { isDate, onStart, onEnd }, { bars }) {
      if (!bar) return;
      const { base, start, end } = bar;
      if (isDate || onStart) {
        bars.push({
          key,
          class: `vc-bar ${start.class}`,
        });
      } else if (onEnd) {
        bars.push({
          key,
          class: `vc-bar ${end.class}`,
        });
      } else {
        bars.push({
          key,
          class: `vc-bar ${base.class}`,
        });
      }
    },
    processPopover(attribute, { popovers }) {
      if (!attribute.popover) return;
      const {
        label,
        labelClass,
        labelStyle,
        hideIndicator,
        visibility,
        placement,
        isInteractive,
      } = attribute.popover;
      popovers.splice(0, 0, {
        key: attribute.key,
        customData: attribute.customData,
        attribute,
        label,
        labelClass,
        labelStyle,
        hideIndicator,
        visibility: visibility || (label ? 'hover' : 'click'),
        placement: placement || 'bottom',
        isInteractive: isInteractive !== undefined ? isInteractive : !label,
      });
    },
  },
};
</script>

<style lang="postcss" scoped>
.vc-day {
  position: relative;
  min-height: var(--day-min-height);
  width: 100%;
  height: 100%;
  z-index: 1;
}

.vc-day-layer {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  pointer-events: none;
}

.vc-day-box-center-center {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  transform-origin: 50% 50%;
}

.vc-day-box-left-center {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  height: 100%;
  transform-origin: 0% 50%;
}

.vc-day-box-right-center {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  height: 100%;
  transform-origin: 100% 50%;
}

.vc-day-box-center-bottom {
  display: flex;
  justify-content: center;
  align-items: flex-end;
}

.vc-day-content {
  display: flex;
  justify-content: center;
  align-items: center;
  width: var(--day-content-width);
  height: var(--day-content-height);
  margin: var(--day-content-margin);
  user-select: none;
  &:hover {
    background-color: var(--day-content-bg-color-hover);
    &.vc-is-dark {
      background-color: var(--day-content-dark-bg-color-hover);
    }
  }
  &:focus {
    background-color: var(--day-content-bg-color-focus);
    &.vc-is-dark {
      background-color: var(--day-content-dark-bg-color-focus);
    }
  }
}

.vc-highlights {
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
}

.vc-highlight {
  width: var(--highlight-height);
  height: var(--highlight-height);
  &.vc-highlight-base-start {
    width: 50% !important;
    border-radius: 0 !important;
    border-right-width: 0 !important;
  }
  &.vc-highlight-base-end {
    width: 50% !important;
    border-radius: 0 !important;
    border-left-width: 0 !important;
  }
  &.vc-highlight-base-middle {
    width: 100%;
    border-radius: 0 !important;
    border-left-width: 0 !important;
    border-right-width: 0 !important;
    margin: 0 -1px;
  }
}

.vc-dots {
  display: flex;
  justify-content: center;
  align-items: center;
}

.vc-dot {
  width: var(--dot-diameter);
  height: var(--dot-diameter);
  border-radius: var(--dot-border-radius);
  transition: all var(--day-content-transition-time);
  &:not(:last-child) {
    margin-right: var(--dot-spacing);
  }
}

.vc-bars {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  width: var(--bars-width);
}

.vc-bar {
  flex-grow: 1;
  height: var(--bar-height);
  transition: all var(--day-content-transition-time);
}
</style>


fixed validation when non-required fields are once again blank

<template>
  <component :is="type" :form.sync="form" :field="field" :model="model" :to="field.templateOptions"></component>
</template>

<script>
 const Vue = require('vue');
 import Util, {getTypes, setError, parseValidationString} from '../util';
 export default {
   props: ['form', 'model', 'field', 'to'],
   computed: {
     type:function(){
       return 'formly_'+this.field.type;
     }
   },
   methods: {
     validate:function(){

       //first check if we need to create a field
       if ( !this.form.$errors[this.field.key] ) this.$set(this.form.$errors, this.field.key, {});
       if ( !this.field.templateOptions ) this.$set(this.field, 'templateOptions', {});

       //check for required fields. This whole setting,unsetting thing seems kind of wrong though..
       //there might be a more 'vue-ey' way to do this...
       if ( this.field.required ){
         if ( !this.form.$errors[this.field.key].required ) this.$set(this.form.$errors[ this.field.key ], 'required', true);
         setError(this.form, this.field.key, 'required', !this.model[ this.field.key ]) ;
       }

       //if we've got nothing left then return
       if ( !this.field.validators ) return;

       //set these for the validators so we don't have to use 'this' in them
       let model = this.model;
       let field = this.field;

       Object.keys(this.field.validators).forEach((validKey) => {
         if ( !this.form.$errors[this.field.key][validKey] ) this.$set(this.form.$errors[ this.field.key ], validKey, false);
         if ( !this.field.required && !this.model[ this.field.key ] ) {
	   setError(this.form, this.field.key, validKey, false);
	   return;
	 }

         let validator = this.field.validators[validKey];
         let validatorMessage = false;

         if ( typeof validator === 'object' ){
           if ( !( 'message' in validator ) ){
             console.error( "Looks like you've set a validator object without setting a message. If you don't need to explicity set the message just define the validator as either an expression or a function. Refer to the docs for more info");
           } else {
             validatorMessage = validator.message;
             validator = validator.expression;
           }
         }

         let label = ( 'templateOptions' in this.field ) && ( 'label' in this.field.templateOptions ) ? this.field.templateOptions.label : '';
         validatorMessage = parseValidationString( validKey, validatorMessage, label, model[ this.field.key ] );

         let valid = false;
         if ( typeof validator === 'function' ){
	   //set the asynchronous flag so that we know it's going
	   let asyncKey = '$async_'+validKey;
	   this.$set(this.form.$errors[ this.field.key ], asyncKey, true);

           // setup for async validation
           validator(field, model, (asyncValid = false, asyncValidatorMessage = validatorMessage) => {
             // whenever validation is done via a function we will assume it's asynchronous and will require next() to be called
             // this way it doesn't matter if it's async or not, next() should always be called
             setError(this.form, this.field.key, validKey, !asyncValid, asyncValidatorMessage);
	     this.$set(this.form.$errors[ this.field.key ], asyncKey, false);
           });
         } else {
           let res = new Function('model', 'field', 'return '+validator+';' );
           valid = !res.call({}, model, field);
           setError(this.form, this.field.key, validKey, valid, validatorMessage);
         }

       });
     }
   },
   components: getTypes(),
   created(){
     this.validate();
     this.$watch('model.'+this.field.key, (val) =>{
       let valid = this.validate();
     });
   },
   mounted(){
     if ( !this.field.wrapper ) return;

     //create a temporary element
     let wrapper = document.createElement('DIV');
     //populate it with the wrapper string
     wrapper.innerHTML = this.field.wrapper;
     //get the parent
     let parent = this.$el.parentNode;
     //insert the wrapper before this element
     parent.insertBefore(wrapper, this.$el);
     //append the element to the new wrapper
     wrapper.firstChild.appendChild( this.$el );
     //move the new wrapper back to where it should be
     parent.insertBefore(wrapper.firstChild, wrapper);
     //remove the temporary element
     parent.removeChild(wrapper);
   }
 }
</script>

<template>
  <component :is="type" :form.sync="form" :field="field" :model="model" :to="field.templateOptions"></component>
</template>

<script>
 const Vue = require('vue');
 import Util, {getTypes, setError, parseValidationString} from '../util';
 export default {
   props: ['form', 'model', 'field', 'to'],
   computed: {
     type:function(){
       return 'formly_'+this.field.type;
     }
   },
   methods: {
     validate:function(){

       //first check if we need to create a field
       if ( !this.form.$errors[this.field.key] ) this.$set(this.form.$errors, this.field.key, {});
       if ( !this.field.templateOptions ) this.$set(this.field, 'templateOptions', {});

       //check for required fields. This whole setting,unsetting thing seems kind of wrong though..
       //there might be a more 'vue-ey' way to do this...
       if ( this.field.required ){
         if ( !this.form.$errors[this.field.key].required ) this.$set(this.form.$errors[ this.field.key ], 'required', true);
         setError(this.form, this.field.key, 'required', !this.model[ this.field.key ]) ;
       }

       //if we've got nothing left then return
       if ( !this.field.validators ) return;

       //set these for the validators so we don't have to use 'this' in them
       let model = this.model;
       let field = this.field;

       Object.keys(this.field.validators).forEach((validKey) => {
         if ( !this.form.$errors[this.field.key][validKey] ) this.$set(this.form.$errors[ this.field.key ], validKey, false);
         if ( !this.field.required && !this.model[ this.field.key ] ) return;

         let validator = this.field.validators[validKey];
         let validatorMessage = false;

         if ( typeof validator === 'object' ){
           if ( !( 'message' in validator ) ){
             console.error( "Looks like you've set a validator object without setting a message. If you don't need to explicity set the message just define the validator as either an expression or a function. Refer to the docs for more info");
           } else {
             validatorMessage = validator.message;
             validator = validator.expression;
           }
         }

         let label = ( 'templateOptions' in this.field ) && ( 'label' in this.field.templateOptions ) ? this.field.templateOptions.label : '';
         validatorMessage = parseValidationString( validKey, validatorMessage, label, model[ this.field.key ] );

         let valid = false;
         if ( typeof validator === 'function' ){
	   //set the asynchronous flag so that we know it's going
	   let asyncKey = '$async_'+validKey;
	   this.$set(this.form.$errors[ this.field.key ], asyncKey, true);

           // setup for async validation
           validator(field, model, (asyncValid = false, asyncValidatorMessage = validatorMessage) => {
             // whenever validation is done via a function we will assume it's asynchronous and will require next() to be called
             // this way it doesn't matter if it's async or not, next() should always be called
             setError(this.form, this.field.key, validKey, !asyncValid, asyncValidatorMessage);
	     this.$set(this.form.$errors[ this.field.key ], asyncKey, false);
           });
         } else {
           let res = new Function('model', 'field', 'return '+validator+';' );
           valid = !res.call({}, model, field);
           setError(this.form, this.field.key, validKey, valid, validatorMessage);
         }

       });
     }
   },
   components: getTypes(),
   created(){
     this.validate();
     this.$watch('model.'+this.field.key, (val) =>{
       let valid = this.validate();
     });
   },
   mounted(){
     if ( !this.field.wrapper ) return;

     //create a temporary element
     let wrapper = document.createElement('DIV');
     //populate it with the wrapper string
     wrapper.innerHTML = this.field.wrapper;
     //get the parent
     let parent = this.$el.parentNode;
     //insert the wrapper before this element
     parent.insertBefore(wrapper, this.$el);
     //append the element to the new wrapper
     wrapper.firstChild.appendChild( this.$el );
     //move the new wrapper back to where it should be
     parent.insertBefore(wrapper.firstChild, wrapper);
     //remove the temporary element
     parent.removeChild(wrapper);
   }
 }
</script>


added resize directive

added resize directive

<script>
  import Detachable from '../../mixins/detachable'
  import Toggleable from '../../mixins/toggleable'
  import Activator from './mixins/activator'
  import Generators from './mixins/generators'
  import Position from './mixins/position'
  import Utils from './mixins/utils'
  import Keyable from './mixins/keyable'

  import ClickOutside from '../../directives/click-outside'

  export default {
    name: 'v-menu',

    mixins: [
      Activator,
      Detachable,
      Generators,
      Keyable,
      Position,
      Utils,
      Toggleable
    ],

    directives: {
      ClickOutside
    },

    data () {
      return {
        autoIndex: null,
        defaultOffset: 8,
        dimensions: {
          activator: {
            top: 0, left: 0,
            bottom: 0, right: 0,
            width: 0, height: 0,
            offsetTop: 0, scrollHeight: 0
          },
          content: {
            top: 0, left: 0,
            bottom: 0, right: 0,
            width: 0, height: 0,
            offsetTop: 0, scrollHeight: 0
          },
          list: null,
          selected: null
        },
        direction: { vert: 'bottom', horiz: 'right' },
        isContentActive: false,
        isBooted: false,
        maxHeightAutoDefault: '200px',
        resizeTimeout: {},
        startIndex: 3,
        stopIndex: 0,
        tileLength: 0,
        window: {},
        absoluteX: 0,
        absoluteY: 0,
        insideContent: false,
        hasJustFocused: false,
        focusedTimeout: {}
      }
    },

    props: {
      top: Boolean,
      left: Boolean,
      bottom: Boolean,
      right: Boolean,
      fullWidth: Boolean,
      auto: Boolean,
      offsetX: Boolean,
      offsetY: Boolean,
      disabled: Boolean,
      maxHeight: {
        default: 'auto'
      },
      nudgeTop: {
        type: Number,
        default: 0
      },
      nudgeBottom: {
        type: Number,
        default: 0
      },
      nudgeLeft: {
        type: Number,
        default: 0
      },
      nudgeRight: {
        type: Number,
        default: 0
      },
      nudgeWidth: {
        type: Number,
        default: 0
      },
      openOnClick: {
        type: Boolean,
        default: true
      },
      openOnHover: {
        type: Boolean,
        default: false
      },
      lazy: Boolean,
      closeOnClick: {
        type: Boolean,
        default: true
      },
      closeOnContentClick: {
        type: Boolean,
        default: true
      },
      activator: {
        default: null
      },
      origin: {
        type: String,
        default: 'top left'
      },
      transition: {
        type: String,
        default: 'menu-transition'
      },
      positionX: {
        type: Number,
        default: null
      },
      positionY: {
        type: Number,
        default: null
      },
      positionAbsolutely: {
        type: Boolean,
        default: false
      },
      maxWidth: [Number, String],
      minWidth: [Number, String]
    },

    computed: {
      calculatedMinWidth () {
        const minWidth = parseInt(this.minWidth) ||
          this.dimensions.activator.width + this.nudgeWidth + (this.auto ? 16 : 0)

        if (!this.maxWidth) return minWidth

        const maxWidth = parseInt(this.maxWidth)

        return maxWidth < minWidth ? maxWidth : minWidth
      },
      styles () {
        return {
          maxHeight: this.auto ? '200px' : isNaN(this.maxHeight) ? this.maxHeight : `${this.maxHeight}px`,
          minWidth: `${this.calculatedMinWidth}px`,
          maxWidth: `${parseInt(this.maxWidth)}px`,
          top: `${this.calcYOverflow(this.calcTop())}px`,
          left: `${this.calcXOverflow(this.calcLeft())}px`
        }
      },
      hasActivator () {
        return !!this.$slots.activator || this.activator
      }
    },

    watch: {
      activator (newActivator, oldActivator) {
        this.removeActivatorEvents(oldActivator)
        this.addActivatorEvents(newActivator)
      },
      disabled (val) {
        val && this.deactivate()
      },
      hasJustFocused (val) {
        if (!val) return

        clearTimeout(this.focusedTimeout)
        this.focusedTimeout = setTimeout(() => (this.hasJustFocused = false), 100)
      },
      isActive (val) {
        if (this.disabled) return

        val && this.activate() || this.deactivate()
      },
      windowResizeHandler () {
        this.isBooted = false
      }
    },

    methods: {
      activate () {
        if (typeof window === 'undefined') return
        this.isBooted = true
        this.insideContent = true
        this.getTiles()
        this.updateDimensions()
        requestAnimationFrame(this.startTransition)
      },
      deactivate () {
        this.isContentActive = false
      },
      onResize () {
        clearTimeout(this.resizeTimeout)
        if (!this.isActive) return
        this.resizeTimeout = setTimeout(this.updateDimensions, 200)
      },
      startTransition () {
        requestAnimationFrame(() => (this.isContentActive = true))
        requestAnimationFrame(this.calculateScroll)
      }
    },

    render (h) {
      const directives = !this.openOnHover ? [{
        name: 'click-outside',
        value: () => this.closeOnClick
      }] : []

      directives.push({
        name: 'resize',
        value: this.onResize
      })

      const data = {
        'class': 'menu',
        style: {
          display: this.fullWidth ? 'block' : 'inline-block'
        },
        directives,
        on: {
          keydown: e => {
            if (e.keyCode === 27) this.isActive = false
            else this.changeListIndex(e)
          }
        }
      }

      return h('div', data, [
        this.genActivator(),
        this.genTransition()
      ])
    }
  }
</script>

<style lang="stylus" src="../../stylus/components/_menus.styl"></style>

<script>
  import Detachable from '../../mixins/detachable'
  import Toggleable from '../../mixins/toggleable'
  import Activator from './mixins/activator'
  import Generators from './mixins/generators'
  import Position from './mixins/position'
  import Utils from './mixins/utils'
  import Keyable from './mixins/keyable'

  import ClickOutside from '../../directives/click-outside'

  export default {
    name: 'v-menu',

    mixins: [
      Activator,
      Detachable,
      Generators,
      Keyable,
      Position,
      Utils,
      Toggleable
    ],

    directives: {
      ClickOutside
    },

    data () {
      return {
        autoIndex: null,
        defaultOffset: 8,
        dimensions: {
          activator: {
            top: 0, left: 0,
            bottom: 0, right: 0,
            width: 0, height: 0,
            offsetTop: 0, scrollHeight: 0
          },
          content: {
            top: 0, left: 0,
            bottom: 0, right: 0,
            width: 0, height: 0,
            offsetTop: 0, scrollHeight: 0
          },
          list: null,
          selected: null
        },
        direction: { vert: 'bottom', horiz: 'right' },
        isContentActive: false,
        isBooted: false,
        maxHeightAutoDefault: '200px',
        resizeTimeout: {},
        startIndex: 3,
        stopIndex: 0,
        tileLength: 0,
        window: {},
        absoluteX: 0,
        absoluteY: 0,
        insideContent: false,
        hasJustFocused: false,
        focusedTimeout: {}
      }
    },

    props: {
      top: Boolean,
      left: Boolean,
      bottom: Boolean,
      right: Boolean,
      fullWidth: Boolean,
      auto: Boolean,
      offsetX: Boolean,
      offsetY: Boolean,
      disabled: Boolean,
      maxHeight: {
        default: 'auto'
      },
      nudgeTop: {
        type: Number,
        default: 0
      },
      nudgeBottom: {
        type: Number,
        default: 0
      },
      nudgeLeft: {
        type: Number,
        default: 0
      },
      nudgeRight: {
        type: Number,
        default: 0
      },
      nudgeWidth: {
        type: Number,
        default: 0
      },
      openOnClick: {
        type: Boolean,
        default: true
      },
      openOnHover: {
        type: Boolean,
        default: false
      },
      lazy: Boolean,
      closeOnClick: {
        type: Boolean,
        default: true
      },
      closeOnContentClick: {
        type: Boolean,
        default: true
      },
      activator: {
        default: null
      },
      origin: {
        type: String,
        default: 'top left'
      },
      transition: {
        type: String,
        default: 'menu-transition'
      },
      positionX: {
        type: Number,
        default: null
      },
      positionY: {
        type: Number,
        default: null
      },
      positionAbsolutely: {
        type: Boolean,
        default: false
      },
      maxWidth: [Number, String],
      minWidth: [Number, String]
    },

    computed: {
      calculatedMinWidth () {
        const minWidth = parseInt(this.minWidth) ||
          this.dimensions.activator.width + this.nudgeWidth + (this.auto ? 16 : 0)

        if (!this.maxWidth) return minWidth

        const maxWidth = parseInt(this.maxWidth)

        return maxWidth < minWidth ? maxWidth : minWidth
      },
      styles () {
        return {
          maxHeight: this.auto ? '200px' : isNaN(this.maxHeight) ? this.maxHeight : `${this.maxHeight}px`,
          minWidth: `${this.calculatedMinWidth}px`,
          maxWidth: `${parseInt(this.maxWidth)}px`,
          top: `${this.calcYOverflow(this.calcTop())}px`,
          left: `${this.calcXOverflow(this.calcLeft())}px`
        }
      },
      hasActivator () {
        return !!this.$slots.activator || this.activator
      }
    },

    watch: {
      activator (newActivator, oldActivator) {
        this.removeActivatorEvents(oldActivator)
        this.addActivatorEvents(newActivator)
      },
      disabled (val) {
        val && this.deactivate()
      },
      hasJustFocused (val) {
        if (!val) return

        clearTimeout(this.focusedTimeout)
        this.focusedTimeout = setTimeout(() => (this.hasJustFocused = false), 100)
      },
      isActive (val) {
        if (this.disabled) return

        val && this.activate() || this.deactivate()
      },
      windowResizeHandler () {
        this.isBooted = false
      }
    },

    mounted () {
      window.addEventListener('resize', this.onResize, { passive: true })
    },

    beforeDestroy () {
      window.removeEventListener('resize', this.onResize, { passive: true })
      window.removeEventListener('resize', this.windowResizeHandler)
    },

    methods: {
      activate () {
        if (typeof window === 'undefined') return
        this.isBooted = true
        this.insideContent = true
        this.getTiles()
        this.updateDimensions()
        requestAnimationFrame(this.startTransition)
      },
      deactivate () {
        this.isContentActive = false
      },
      onResize () {
        clearTimeout(this.resizeTimeout)
        if (!this.isActive) return
        this.resizeTimeout = setTimeout(this.updateDimensions, 200)
      },
      startTransition () {
        requestAnimationFrame(() => (this.isContentActive = true))
        requestAnimationFrame(this.calculateScroll)
      }
    },

    render (h) {
      const directives = !this.openOnHover ? [{
        name: 'click-outside',
        value: () => this.closeOnClick
      }] : []

      const data = {
        'class': 'menu',
        style: {
          display: this.fullWidth ? 'block' : 'inline-block'
        },
        directives,
        on: {
          keydown: e => {
            if (e.keyCode === 27) this.isActive = false
            else this.changeListIndex(e)
          }
        }
      }

      return h('div', data, [
        this.genActivator(),
        this.genTransition()
      ])
    }
  }
</script>

<style lang="stylus" src="../../stylus/components/_menus.styl"></style>


less console

less console

<template>

Annotations

Tool:

                  {{{ icon(t) }}}

                  {{{ icon(t) }}} {{ t.name }}

Size:
({{ size }})

                  Small
                  Medium
                  <big>Large</big>

Colour:

Ops:

                   Cut
                   Copy
                   Paste

                   Undo
                   Redo

                   Import
                   Export

</template>


Fix name sorting in task page

Fix name sorting in task page

<template>

            <router-link :to="{
              name: 'todos',
            }">
              {{ $t('tasks.current')}}
            </router-link>

            <router-link :to="{
              name: 'todos-tab',
              params: {tab: 'done'}
            }">
              {{ $t('tasks.done') }} ({{ displayedDoneTasks.length }})
            </router-link>

            <router-link :to="{
              name: 'todos-tab',
              params: {tab: 'timesheets'}
            }">
              {{ $t('timesheets.title') }}
            </router-link>

        <search-field
          :class="{
            'search-field': true,
            'flexrow-item': true
          }"
          ref="todos-search-field"
          @change="onSearchChange"
          @save="saveSearchQuery"
          :can-save="true"
          v-if="!isTabActive('done')"
        />

        <combobox
          class="flexrow-item"
          :label="$t('main.sorted_by')"
          :options="sortOptions"
          locale-key-prefix="tasks.fields."
          v-model="currentSort"
        />

        <search-query-list
          :queries="todoSearchQueries"
          @changesearch="changeSearch"
          @removesearch="removeSearchQuery"
        />

      <todos-list
        ref="todo-list"
        :entries="sortedTasks"
        :is-loading="isTodosLoading"
        :is-error="isTodosLoadingError"
        :selection-grid="todoSelectionGrid"
        v-if="isTabActive('todos')"
        @scroll="setTodoListScrollPosition"
      />

      <todos-list
        ref="done-list"
        :entries="displayedDoneTasks"
        :is-loading="isTodosLoading"
        :is-error="isTodosLoadingError"
        :done="true"
        v-if="isTabActive('done')"
      />

      <timesheet-list
        ref="timesheet-list"
        :tasks="loggableTodos"
        :done-tasks="loggableDoneTasks"
        :is-loading="isTodosLoading"
        :is-error="isTodosLoadingError"
        :time-spent-map="timeSpentMap"
        :time-spent-total="timeSpentTotal"
        :hide-done="todosSearchText.length > 0 || loggableDoneTasks.length === 0"
        @date-changed="onDateChanged"
        @time-spent-change="onTimeSpentChange"
        v-if="isTabActive('timesheets')"
      />

    <task-info
      :task="Object.values(selectedTasks)[0]"
    />

</template>


Fix #2469 mantain sort during search

Fix #2469 mantain sort during search

<template>

         sort(column, null, event)"
            @removePriority="(column) => removeSortingPriority(column)"
        />

                <slot name="top-left"/>

                    <b-pagination
                        :icon-pack="iconPack"
                        :total="newDataTotal"
                        :per-page="perPage"
                        :simple="paginationSimple"
                        :size="paginationSize"
                        :current="newCurrentPage"
                        @change="pageChanged"
                        :aria-next-label="ariaNextLabel"
                        :aria-previous-label="ariaPreviousLabel"
                        :aria-page-label="ariaPageLabel"
                        :aria-current-label="ariaCurrentLabel" />

                            <template v-if="headerCheckable">

                            </template>

                        <th
                            v-for="(column, index) in visibleColumns"
                            :key="index"
                            :class="[column.headerClass, {
                                'is-current-sort': !sortMultiple && currentSortColumn === column,
                                'is-sortable': column.sortable,
                                'is-sticky': column.sticky,
                                'is-unselectable': !column.headerSelectable
                            }]"
                            :style="{
                                width: column.width === undefined ? null :
                                (isNaN(column.width) ? column.width : column.width + 'px')
                            }"
                            @click.stop="sort(column, null, $event)">

                                <template v-if="column.$scopedSlots && column.$scopedSlots.header">

                                </template>
                                <template v-else-if="$scopedSlots.header">
                                    <slot
                                        name="header"
                                        :column="column"
                                        :index="index"
                                    />
                                </template>
                                <template v-else>{{ column.label }}</template>

                                <template
                                    v-if="sortMultiple &&
                                        sortMultipleDataComputed &&
                                        sortMultipleDataComputed.length > 0 &&
                                        sortMultipleDataComputed.filter(i =>
                                    i.field === column.field).length > 0">

                                                i.field === column.field)[0].order === 'desc'
                                        }"
                                    />
                                    {{ findIndexOfSortData(column) }}
                                    <button
                                        class="delete is-small multi-sort-cancel-icon"
                                        type="button"
                                        @click.stop="removeSortingPriority(column)"/>
                                </template>

                                <b-icon
                                    v-else-if="column.sortable && !sortMultiple"
                                    :icon="sortIcon"
                                    :pack="iconPack"
                                    both
                                    :size="sortIconSize"
                                    :class="{
                                        'is-desc': !isAsc,
                                        'is-invisible': currentSortColumn !== column
                                    }"
                                />

                            <template v-if="headerCheckable">

                            </template>

                                <template
                                    v-if="column.$scopedSlots && column.$scopedSlots.subheading"
                                >

                                </template>
                                <template v-else-if="$scopedSlots.subheading">
                                    <slot
                                        name="subheading"
                                        :column="column"
                                        :index="index"
                                    />
                                </template>
                                <template v-else>{{ column.subheading }}</template>

                                <template v-if="column.searchable">
                                    <slot
                                        v-if="$scopedSlots.searchable"
                                        name="searchable"
                                        :column="column"
                                        :filters="filters"
                                    />

                                </template>

                    <template v-for="(row, index) in visibleData">
                        <tr
                            :key="customRowKey ? row[customRowKey] : index"
                            :class="[rowClass(row, index), {
                                'is-selected': row === selected,
                                'is-checked': isRowChecked(row),
                            }]"
                            @click="selectRow(row)"
                            @dblclick="$emit('dblclick', row)"
                            @mouseenter="$listeners.mouseenter ? $emit('mouseenter', row) : null"
                            @mouseleave="$listeners.mouseleave ? $emit('mouseleave', row) : null"
                            @contextmenu="$emit('contextmenu', row, $event)"
                            :draggable="draggable"
                            @dragstart="handleDragStart($event, row, index)"
                            @dragend="handleDragEnd($event, row, index)"
                            @drop="handleDrop($event, row, index)"
                            @dragover="handleDragOver($event, row, index)"
                            @dragleave="handleDragLeave($event, row, index)">

                            <slot
                                v-if="$scopedSlots.default"
                                :row="row"
                                :index="index"
                            />
                            <template v-else>
                                <BTableColumn
                                    v-for="column in newColumns"
                                    v-bind="column"
                                    :key="column.customKey || column.label"
                                    internal>

                                    <template v-else>
                                        {{ getValueByPath(row, column.field) }}
                                    </template>
                                </BTableColumn>
                            </template>

                                    <slot
                                        name="detail"
                                        :row="row"
                                        :index="index"/>

                        <slot
                            v-if="isActiveCustomDetailRow(row)"
                            name="detail"
                            :row="row"
                            :index="index"
                        />
                    </template>

                            <slot name="empty"/>

                        <slot name="footer" v-if="hasCustomFooterSlot()"/>

                            <slot name="footer"/>

                <slot name="bottom-left"/>

                    <b-pagination
                        :icon-pack="iconPack"
                        :total="newDataTotal"
                        :per-page="perPage"
                        :simple="paginationSimple"
                        :size="paginationSize"
                        :current="newCurrentPage"
                        @change="pageChanged"
                        :aria-next-label="ariaNextLabel"
                        :aria-previous-label="ariaPreviousLabel"
                        :aria-page-label="ariaPageLabel"
                        :aria-current-label="ariaCurrentLabel" />

</template>


Add console log for debugging

Add console log for debugging

<template>

        <card-header>

                    {{ tab.label }}

        </card-header>
        <card-body>

                <slot></slot>

        </card-body>

</template>


chore(modules): remove icon residue styles

<template>

 m.category === category).length" class="module-category h6 uppercase">
				{{ category }}

				 m.category === category )"
					:key="module.name"
					:href="module.type === 'module' ? '#modules/' + module.module_name : module.link"
					class="border module-box"
				>

								{{ module.label }}

 {{ module.description }}

</template>