Add jpeg extension for add preview modal

<template>

        <router-link
          :to="entityPage"
          class="flexrow-item has-text-centered back-link"
        >
          <chevron-left-icon />
        </router-link>

          <router-link :to="taskEntityPath">
            {{ currentTask ? title : 'Loading...'}}
          </router-link>

        <task-type-name
          class="flexrow-item task-type"
          :task-type="currentTaskType"
          :production-id="currentProduction.id"
          v-if="currentTaskType"
        />
        <subscribe-button
          class="flexrow-item action-button"
          :subscribed="isAssigned || isSubscribed"
          @click="toggleSubscribe"
          v-if="!isAssigned"
        />

        {{ $t('tasks.current_status') }}
        <validation-tag
          class="is-medium flexrow-item"
          :task="currentTask"
          :is-static="true"
          v-if="currentTask"
        />
        {{ $t('tasks.fields.assignees') }}:

          <people-avatar
            class="flexrow-item"
            :key="personId"
            :person="personMap[personId]"
            :size="30"
            :font-size="16"
          />

            <add-comment
              :is-loading="loading.addComment"
              :is-error="errors.addComment"
              :user="user"
              :team="currentTeam"
              :task="currentTask"
              :task-status="taskStatusForCurrentUser"
              :attached-file-name="attachedFileName"
              @add-comment="addComment"
              @add-preview="onAddPreviewClicked"
              @file-drop="selectFile"
              v-if="isCommentingAllowed"
            />

 0">
              <comment
                :comment="comment"
                :highlighted="isHighlighted(comment)"
                :key="comment.id"
                :current-user="user"
                :editable="comment.person && user.id === comment.person.id && index === 0"
                v-for="(comment, index) in currentTaskComments"
              />

                {{ $t('tasks.no_comment')}}

          <spinner />

              {{ $t('tasks.preview') }}

              <button
                :class="{
                  button: true,
                  'flexrow-item': true,
                  'is-loading': loading.setPreview
                }"
                @click="setPreview"
                v-if="currentTaskPreviews && currentTaskPreviews.length > 0 && isCurrentUserManager"
              >
                <image-icon class="icon" />

                  {{ $t('tasks.set_preview') }}

              </button>

                {{ $t('tasks.set_preview_error') }}

              {{ $t('tasks.set_preview_done') }}

            <preview-row
              :key="preview.id"
              :preview="preview"
              :preview-path="previewPath(preview.id)"
              :taskId="currentTask ? currentTask.id : ''"
              :selected="preview.id === currentPreviewId"
              v-for="preview in currentTaskPreviews"
            />

              {{ $t('tasks.no_preview')}}

 0 && isMovie"
            >
              <video-player
                :preview="currentPreview"
                :task-type-map="taskTypeMap"
                :entity-preview-files="taskEntityPreviews"
                @annotationchanged="onAnnotationChanged"
                ref="preview-movie"
              />

              <download-icon class="icon" />

                {{ $t('tasks.download_pdf_file', {extension}) }}

            <model-viewer
              :preview-url="currentPreviewPath"
              :preview-dl-path="currentPreviewDlPath"
              v-else-if="currentTaskPreviews && currentTaskPreviews.length > 0 && extension === 'obj'"
            />

            <picture-viewer
              :preview="currentPreview"
              @annotation-changed="onAnnotationChanged"
              @add-preview="onAddExtraPreview"
              @remove-extra-preview="showRemoveExtraPreviewModal"
              ref="preview-picture"
              v-else-if="currentTaskPreviews && currentTaskPreviews.length > 0 && extension === 'png'"
            />

    <add-preview-modal
      ref="add-preview-modal"
      :active="modals.addPreview"
      :is-loading="loading.addPreview"
      :is-error="errors.addPreview"
      :cancel-route="taskPath()"
      :form-data="addPreviewFormData"
      @fileselected="selectFile"
      @confirm="closeAddPreviewModal"
    />

    <add-preview-modal
      ref="add-extra-preview-modal"
      :active="modals.addExtraPreview"
      :is-loading="loading.addExtraPreview"
      :is-error="errors.addExtraPreview"
      :form-data="addExtraPreviewFormData"
      extensions=".png,.jpg,.jpeg"
      @cancel="hideExtraPreviewModal"
      @fileselected="selectFile"
      @confirm="createExtraPreview"
    />

    <add-preview-modal
      ref="change-preview-modal"
      :active="modals.changePreview"
      :is-loading="loading.changePreview"
      :is-error="errors.changePreview"
      :cancel-route="taskPath()"
      :form-data="changePreviewFormData"
      :is-editing="true"
      @fileselected="selectFile"
      @confirm="changePreview"
    />

    <edit-comment-modal
      :active="modals.editComment"
      :is-loading="loading.editComment"
      :is-error="errors.editComment"
      :cancel-route="taskPath()"
      :comment-to-edit="commentToEdit"
      :team="currentTeam"
      @confirm="confirmEditTaskComment"
    />

    <delete-modal
      :active="modals.deleteComment"
      :is-loading="loading.deleteComment"
      :is-error="errors.deleteComment"
      :cancel-route="taskPath()"
      :text="$t('tasks.delete_comment')"
      :error-text="$t('tasks.delete_comment_error')"
      @confirm="confirmDeleteTaskComment"
    />

    <delete-modal
      :active="modals.deleteExtraPreview"
      :is-loading="loading.deleteExtraPreview"
      :is-error="errors.deleteExtraPreview"
      :text="$t('tasks.delete_preview')"
      :error-text="$t('tasks.delete_preview_error')"
      @cancel="hideRemoveExtraPreviewModal"
      @confirm="confirmDeleteTaskPreview"
    />

</template>

<template>

        <router-link
          :to="entityPage"
          class="flexrow-item has-text-centered back-link"
        >
          <chevron-left-icon />
        </router-link>

          <router-link :to="taskEntityPath">
            {{ currentTask ? title : 'Loading...'}}
          </router-link>

        <task-type-name
          class="flexrow-item task-type"
          :task-type="currentTaskType"
          :production-id="currentProduction.id"
          v-if="currentTaskType"
        />
        <subscribe-button
          class="flexrow-item action-button"
          :subscribed="isAssigned || isSubscribed"
          @click="toggleSubscribe"
          v-if="!isAssigned"
        />

        {{ $t('tasks.current_status') }}
        <validation-tag
          class="is-medium flexrow-item"
          :task="currentTask"
          :is-static="true"
          v-if="currentTask"
        />
        {{ $t('tasks.fields.assignees') }}:

          <people-avatar
            class="flexrow-item"
            :key="personId"
            :person="personMap[personId]"
            :size="30"
            :font-size="16"
          />

            <add-comment
              :is-loading="loading.addComment"
              :is-error="errors.addComment"
              :user="user"
              :team="currentTeam"
              :task="currentTask"
              :task-status="taskStatusForCurrentUser"
              :attached-file-name="attachedFileName"
              @add-comment="addComment"
              @add-preview="onAddPreviewClicked"
              @file-drop="selectFile"
              v-if="isCommentingAllowed"
            />

 0">
              <comment
                :comment="comment"
                :highlighted="isHighlighted(comment)"
                :key="comment.id"
                :current-user="user"
                :editable="comment.person && user.id === comment.person.id && index === 0"
                v-for="(comment, index) in currentTaskComments"
              />

                {{ $t('tasks.no_comment')}}

          <spinner />

              {{ $t('tasks.preview') }}

              <button
                :class="{
                  button: true,
                  'flexrow-item': true,
                  'is-loading': loading.setPreview
                }"
                @click="setPreview"
                v-if="currentTaskPreviews && currentTaskPreviews.length > 0 && isCurrentUserManager"
              >
                <image-icon class="icon" />

                  {{ $t('tasks.set_preview') }}

              </button>

                {{ $t('tasks.set_preview_error') }}

              {{ $t('tasks.set_preview_done') }}

            <preview-row
              :key="preview.id"
              :preview="preview"
              :preview-path="previewPath(preview.id)"
              :taskId="currentTask ? currentTask.id : ''"
              :selected="preview.id === currentPreviewId"
              v-for="preview in currentTaskPreviews"
            />

              {{ $t('tasks.no_preview')}}

 0 && isMovie"
            >
              <video-player
                :preview="currentPreview"
                :task-type-map="taskTypeMap"
                :entity-preview-files="taskEntityPreviews"
                @annotationchanged="onAnnotationChanged"
                ref="preview-movie"
              />

              <download-icon class="icon" />

                {{ $t('tasks.download_pdf_file', {extension}) }}

            <model-viewer
              :preview-url="currentPreviewPath"
              :preview-dl-path="currentPreviewDlPath"
              v-else-if="currentTaskPreviews && currentTaskPreviews.length > 0 && extension === 'obj'"
            />

            <picture-viewer
              :preview="currentPreview"
              @annotation-changed="onAnnotationChanged"
              @add-preview="onAddExtraPreview"
              @remove-extra-preview="showRemoveExtraPreviewModal"
              ref="preview-picture"
              v-else-if="currentTaskPreviews && currentTaskPreviews.length > 0 && extension === 'png'"
            />

    <add-preview-modal
      ref="add-preview-modal"
      :active="modals.addPreview"
      :is-loading="loading.addPreview"
      :is-error="errors.addPreview"
      :cancel-route="taskPath()"
      :form-data="addPreviewFormData"
      @fileselected="selectFile"
      @confirm="closeAddPreviewModal"
    />

    <add-preview-modal
      ref="add-extra-preview-modal"
      :active="modals.addExtraPreview"
      :is-loading="loading.addExtraPreview"
      :is-error="errors.addExtraPreview"
      :form-data="addExtraPreviewFormData"
      extensions=".png,.jpg"
      @cancel="hideExtraPreviewModal"
      @fileselected="selectFile"
      @confirm="createExtraPreview"
    />

    <add-preview-modal
      ref="change-preview-modal"
      :active="modals.changePreview"
      :is-loading="loading.changePreview"
      :is-error="errors.changePreview"
      :cancel-route="taskPath()"
      :form-data="changePreviewFormData"
      :is-editing="true"
      @fileselected="selectFile"
      @confirm="changePreview"
    />

    <edit-comment-modal
      :active="modals.editComment"
      :is-loading="loading.editComment"
      :is-error="errors.editComment"
      :cancel-route="taskPath()"
      :comment-to-edit="commentToEdit"
      :team="currentTeam"
      @confirm="confirmEditTaskComment"
    />

    <delete-modal
      :active="modals.deleteComment"
      :is-loading="loading.deleteComment"
      :is-error="errors.deleteComment"
      :cancel-route="taskPath()"
      :text="$t('tasks.delete_comment')"
      :error-text="$t('tasks.delete_comment_error')"
      @confirm="confirmDeleteTaskComment"
    />

    <delete-modal
      :active="modals.deleteExtraPreview"
      :is-loading="loading.deleteExtraPreview"
      :is-error="errors.deleteExtraPreview"
      :text="$t('tasks.delete_preview')"
      :error-text="$t('tasks.delete_preview_error')"
      @cancel="hideRemoveExtraPreviewModal"
      @confirm="confirmDeleteTaskPreview"
    />

</template>


Fixed academic calendar term grouping logic

<template>

    <template v-for="(eventTerm, i) in splitByTerm(events)">
      <uw-card v-if="!eventTerm.termBreak" :key="i" loaded>
        <template #card-heading>

            {{ eventTerm.quarter }} {{ eventTerm.year }}

        </template>
        <template #card-body>

{{ formatDateRange(event.start, event.end) }}

                {{ event.summary }}

        </template>
      </uw-card>

          {{ eventTerm.events[0].summary }}

          {{ formatDateRange(eventTerm.events[0].start, eventTerm.events[0].end) }}

    </template>

</template>


Display request link only if request is present

<template>

            Anfrage: {{ request.title }}

            An: {{ currentObject.publicbody_name }}

          Zur Anfrage

          <button
              @click.prevent="next"
              class="btn btn-secondary mb-2"
              v-if="objectListIndex < maxIndex"
            >
              Überspringen
          </button>
          <button
              @click.prevent="next"
              class="btn btn-light mb-2"
              v-if="objectListIndex == maxIndex"
              disabled
            >
              Überspringen
          </button>

          Kein weiteren Anfragen mehr! Vielen Dank!

          Loading...

          <campaign-questionair-message
          v-for="message in this.filteredMessages()"
          :key="message.id"
          :request="request"
          :message="message"
          :config="config"
          ></campaign-questionair-message>

Fragen:

          <form>

              {{ answer.question }}*
              <select v-if="answer.options.length > 1" select class="form-control" v-model="answer.answer">
                <option v-for="(option, index) in answer.options" :key=index :selected="index === 0">{{ option }}</option>
              </select>
              <input v-if="answer.options.length === 1" :class="answer.error ? 'form-control is-invalid' : 'form-control'" type="text" v-model="answer.answer" />
              {{ answer.helptext }}

Bitte beantworten Sie diese Frage.

            <button
              type="submit"
              @click.prevent="submitAnswersAndNext"
              class="btn btn-primary pull-right"
              v-if="objectListIndex < maxIndex"
            >
              Absenden und weiter
            </button>
            <button
              type="submit"
              @click.prevent="submitAnswers"
              class="btn btn-light mb-2 mr-2 pull-right"
            >
              Absenden
            </button>
          </form>

          Antworten erfolgreich gespeichert!

</template>


removed third argument I decided not to include

<template>
  <transition-group
    name="list" tag="div"
    class="apos-notifications" :class="themeClass"
  >
    <AposNotification
      v-for="notification in notifications"
      :key="notification._id"
      :notification="notification"
      @close="dismiss"
    />
  </transition-group>
</template>

<script>
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';

export default {
  name: 'TheAposNotifications',
  mixins: [ AposThemeMixin ],
  data () {
    return {
      notifications: [],
      dismissed: []
    };
  },
  async mounted() {
    apos.notify = async function(message, options) {

      if (options.dismiss === true) {
        options.dismiss = 5;
      }

      // Send it to the server, which will send it back to us via
      // the same long polling mechanism that allows it to reach
      // other tabs, and allows server-sent notifications to
      // reach us

      await apos.http.post(apos.notification.action, {
        body: {
          message,
          interpolate: options.interpolate || {},
          type: options.type,
          icon: options.icon,
          dismiss: options.dismiss,
          buttons: options.buttons,
          localize: options.localize
        }
      });
    };

    await this.poll();
  },
  methods: {
    async dismiss(notificationId) {
      await apos.http.patch(`${apos.notification.action}/${notificationId}`, {
        body: {
          dismissed: true
        }
      });
      this.notifications = this.notifications.reduce((acc, cur) => {
        if (cur._id !== notificationId) {
          acc.push(cur);
        }
        return acc;
      }, []);
    },
    async poll() {
      try {
        if (document.visibilityState === 'hidden') {
          // Wait for tab to become visible
          setTimeout(this.poll, 5000);
        } else {
          const allNotifications = [ ...this.notifications, ...this.dismissed ];
          const latestTimestamp = allNotifications
            .map(notification => notification.updatedAt)
            .sort()
            .reverse()[0];

          const seenIds = allNotifications
            .filter(notification => notification.updatedAt === latestTimestamp)
            .map(notification => notification._id);

          const { notifications, dismissed } = await apos.http.get(apos.notification.action, {
            ...(latestTimestamp && {
              qs: {
                modifiedOnOrSince: latestTimestamp,
                seenIds
              }
            })
          });

          this.notifications = [ ...this.notifications, ...(notifications || []) ];
          this.dismissed = [ ...this.dismissed, ...(dismissed || []) ];

          if (dismissed.length) {
            this.notifications = this.notifications.filter(notification => {
              return !dismissed.some(element => notification._id === element._id);
            });
          }
          // Long polling, we should reconnect promptly, the server
          // is responsible for keeping that request open for a reasonable
          // amount of time if there are no new messages, not us
          setTimeout(this.poll, 50);
        }
      } catch (err) {
        console.error(err);
        setTimeout(this.poll, 5000);
      }
    }
  }
};
</script>

<style lang="scss" scoped>
  .apos-notifications {
    z-index: $z-index-notifications;
    pointer-events: none;
    position: fixed;
    bottom: 20px;
    display: flex;
    width: 100%;
    flex-direction: column;
    align-items: center;
  }

  .list-enter-active,
  .list-leave-active {
    @include apos-transition();
  }

  .list-enter,
  .list-leave-to {
    opacity: 0;
    transform: translateY(5px);
  }

</style>

<template>
  <transition-group
    name="list" tag="div"
    class="apos-notifications" :class="themeClass"
  >
    <AposNotification
      v-for="notification in notifications"
      :key="notification._id"
      :notification="notification"
      @close="dismiss"
    />
  </transition-group>
</template>

<script>
import AposThemeMixin from 'Modules/@apostrophecms/ui/mixins/AposThemeMixin';

export default {
  name: 'TheAposNotifications',
  mixins: [ AposThemeMixin ],
  data () {
    return {
      notifications: [],
      dismissed: []
    };
  },
  async mounted() {
    apos.notify = async function(message, options, interpolate) {

      if (options.dismiss === true) {
        options.dismiss = 5;
      }

      // Send it to the server, which will send it back to us via
      // the same long polling mechanism that allows it to reach
      // other tabs, and allows server-sent notifications to
      // reach us

      await apos.http.post(apos.notification.action, {
        body: {
          message,
          interpolate: interpolate || options.interpolate || {},
          type: options.type,
          icon: options.icon,
          dismiss: options.dismiss,
          buttons: options.buttons,
          localize: options.localize
        }
      });
    };

    await this.poll();
  },
  methods: {
    async dismiss(notificationId) {
      await apos.http.patch(`${apos.notification.action}/${notificationId}`, {
        body: {
          dismissed: true
        }
      });
      this.notifications = this.notifications.reduce((acc, cur) => {
        if (cur._id !== notificationId) {
          acc.push(cur);
        }
        return acc;
      }, []);
    },
    async poll() {
      try {
        if (document.visibilityState === 'hidden') {
          // Wait for tab to become visible
          setTimeout(this.poll, 5000);
        } else {
          const allNotifications = [ ...this.notifications, ...this.dismissed ];
          const latestTimestamp = allNotifications
            .map(notification => notification.updatedAt)
            .sort()
            .reverse()[0];

          const seenIds = allNotifications
            .filter(notification => notification.updatedAt === latestTimestamp)
            .map(notification => notification._id);

          const { notifications, dismissed } = await apos.http.get(apos.notification.action, {
            ...(latestTimestamp && {
              qs: {
                modifiedOnOrSince: latestTimestamp,
                seenIds
              }
            })
          });

          this.notifications = [ ...this.notifications, ...(notifications || []) ];
          this.dismissed = [ ...this.dismissed, ...(dismissed || []) ];

          if (dismissed.length) {
            this.notifications = this.notifications.filter(notification => {
              return !dismissed.some(element => notification._id === element._id);
            });
          }
          // Long polling, we should reconnect promptly, the server
          // is responsible for keeping that request open for a reasonable
          // amount of time if there are no new messages, not us
          setTimeout(this.poll, 50);
        }
      } catch (err) {
        console.error(err);
        setTimeout(this.poll, 5000);
      }
    }
  }
};
</script>

<style lang="scss" scoped>
  .apos-notifications {
    z-index: $z-index-notifications;
    pointer-events: none;
    position: fixed;
    bottom: 20px;
    display: flex;
    width: 100%;
    flex-direction: column;
    align-items: center;
  }

  .list-enter-active,
  .list-leave-active {
    @include apos-transition();
  }

  .list-enter,
  .list-leave-to {
    opacity: 0;
    transform: translateY(5px);
  }

</style>


:bug: bug fix

:bug: bug fix

<template lang="pug">
.sidebar
  section
    h1.logo
      a(href="/chase/")
        img(src="../assets/logo.png" alt="Chase")

  section.menu(v-if="login.accessToken")
    .menu-title Recent {{ catalogCount }}
    .link-item
      router-link(:to="{ name: 'Inbox' }")
        clickable
          i.ion-ios-filing-outline
          span Inbox
        .cnt {{ filteredCatalog('Inbox').length }}
    .link-item
      router-link(:to="{ name: 'Favorite' }")
        clickable
          i.ion-ios-star-outline
          span Favorite
        .cnt {{ filteredCatalog('Favorite').length }}
    .link-item(v-for="sce in myScenesTags")
      router-link(:to="{ name: 'Tag', params: { tag: sce.tag } }")
        clickable
          i.ion-ios-arrow-right
          span {{ sce.label }}
        .cnt {{ filteredCatalog('Tag', sce.tag).length }}

  section.tags
    template(v-for="tag in recentTags")
      .link-item.font-smaller
        router-link(:to="{ name: 'Tag', params: { tag } }")
          clickable
            i.ion-ios-pricetags-outline
            span {{ tag }}

  section
    .misc
      .link-item.font-smaller
        a(href="https://getpocket.com/" target="_blank")
          span
            i.ion-ios-world-outline
            span Pocket
    .config
      .link-item.font-smaller
        router-link(:to="{ name: 'Config' }")
          span
            i.ion-ios-settings
            span Config
    .userinfo
      template(v-if="login.accessToken")
        span.username {{ login.username }}
        clickable.disconnect(@click.native="logout")
          i.ion-ios-close-empty
</template>

<script>
import { mapState, mapGetters } from 'vuex';
import Clickable from '@/components/Clickable';

export default {
  name: 'Sidebar',
  components: {
    Clickable,
  },
  computed: {
    ...mapState({
      login: 'login',
      mytags: 'mytags',
      myscenes: 'myscenes',
    }),
    ...mapGetters([
      'catalogCount',
      'filteredCatalog',
      'recentTags',
      'myScenesTags',
    ]),
  },
  mounted() {
    const ph = this.$cookie.get('phase');
    if (!ph) {
      this.$router.push('welcome');
    } else {
      this.$store.dispatch('actByPhase', this.$cookie);
    }
  },
  methods: {
    getAccessToken() {
      this.$store.dispatch('getAccessToken', this.$cookie);
    },
    showScenesEditor() {
      this.sceneEditing = true;
    },
    logout() {
      this.$store.dispatch('logout', this.$cookie);
      this.$router.push('welcome');
    },
  },
};
</script>

<style lang="stylus" scoped>
.sidebar
  position fixed
  width inherit
  height 100vh
  display flex
  flex-direction column
  justify-content space-between
  padding 15px
  font-size 0.75rem

.flex-aic-jcsb
  display flex
  align-items center
  justify-content space-between

.refresh i
  font-size 1.5rem

.menu
  margin 1rem 0
  .menu-title
    font-size .6rem
    font-style italic
    color #757575
    margin .5em 0

.link-item
  height 2rem
  display flex
  align-items center
  font-size 1rem
  i
    width 1em
    display inline-block
    margin-right .5em
    text-align center
  > a
    flex 1
    height inherit
    display flex
    align-items center
    justify-content space-between
  .cnt
    font-size .75rem

.logo
  display flex
  align-items center
  margin 0
  img
    width 32px
    height 32px

.tags
  .link-item
    height 1.5rem

.font-smaller
  font-size 0.8rem

.userinfo
  display flex
  align-items center
  .username
    margin-right .5em
    font-size .6rem
  .disconnect
    padding 0 .5rem
    font-size 1.25rem
</style>

<template lang="pug">
.sidebar
  section
    h1.logo
      a(href="/chase/")
        img(src="../assets/logo.png" alt="Chase")

  section.menu(v-if="login.accessToken")
    .menu-title Recent {{ catalogCount }}
    .link-item
      router-link(:to="{ name: 'Inbox' }")
        clickable
          i.ion-ios-filing-outline
          span Inbox
        .cnt {{ filteredCatalog('Inbox').length }}
    .link-item
      router-link(:to="{ name: 'Favorite' }")
        clickable
          i.ion-ios-star-outline
          span Favorite
        .cnt {{ filteredCatalog('Favorite').length }}
    .link-item(v-for="sce in myScenesTags")
      router-link(:to="{ name: 'Tag', params: { tag: sce.tag } }")
        clickable
          i.ion-ios-arrow-right
          span {{ sce.label }}
        .cnt {{ filteredCatalog('Tag', sce.tag).length }}

  section.tags
    template(v-for="tag in recentTags")
      .link-item.font-smaller
        router-link(:to="{ name: 'Tag', params: { tag } }")
          clickable
            i.ion-ios-pricetags-outline
            span {{ tag }}

  section
    .misc
      .link-item.font-smaller
        a(href="https://getpocket.com/" target="_blank")
          span
            i.ion-ios-world-outline
            span Pocket
    .config
      .link-item.font-smaller
        router-link(:to="{ name: 'Config' }")
          span
            i.ion-ios-settings
            span Config
    .userinfo
      template(v-if="login.accessToken")
        span.username {{ login.username }}
        clickable.disconnect(@click.native="logout")
          i.ion-ios-close-empty
      template(v-else)
        button(@click="getRequestToken") Connect to Pocket
</template>

<script>
import { mapState, mapGetters } from 'vuex';
import Clickable from '@/components/Clickable';

export default {
  name: 'Sidebar',
  components: {
    Clickable,
  },
  computed: {
    ...mapState({
      login: 'login',
      mytags: 'mytags',
      myscenes: 'myscenes',
    }),
    ...mapGetters([
      'catalogCount',
      'filteredCatalog',
      'recentTags',
      'myScenesTags',
    ]),
  },
  mounted() {
    const ph = this.$cookie.get('phase');
    if (!ph) {
      this.$router.push('welcome');
    } else {
      this.$store.dispatch('actByPhase', this.$cookie);
    }
  },
  methods: {
    getAccessToken() {
      this.$store.dispatch('getAccessToken', this.$cookie);
    },
    showScenesEditor() {
      this.sceneEditing = true;
    },
    logout() {
      this.$store.dispatch('logout', this.$cookie);
      this.$router.push('welcome');
    },
  },
};
</script>

<style lang="stylus" scoped>
.sidebar
  position fixed
  width inherit
  height 100vh
  display flex
  flex-direction column
  justify-content space-between
  padding 15px
  font-size 0.75rem

.flex-aic-jcsb
  display flex
  align-items center
  justify-content space-between

.refresh i
  font-size 1.5rem

.menu
  margin 1rem 0
  .menu-title
    font-size .6rem
    font-style italic
    color #757575
    margin .5em 0

.link-item
  height 2rem
  display flex
  align-items center
  font-size 1rem
  i
    width 1em
    display inline-block
    margin-right .5em
    text-align center
  > a
    flex 1
    height inherit
    display flex
    align-items center
    justify-content space-between
  .cnt
    font-size .75rem

.logo
  display flex
  align-items center
  margin 0
  img
    width 32px
    height 32px

.tags
  .link-item
    height 1.5rem

.font-smaller
  font-size 0.8rem

.userinfo
  display flex
  align-items center
  .username
    margin-right .5em
    font-size .6rem
  .disconnect
    padding 0 .5rem
    font-size 1.25rem
</style>


[Misc] LivedataTopbar cleanup

[Misc] LivedataTopbar cleanup

Remove the unused logic injection.

<template>

      <slot name="left"></slot>

      <slot name="right"></slot>

</template>


Update timeline.vue

Update timeline.vue

<template>

	<mk-friends-maker v-if="alone"/>
	<mk-notes :notes="notes">

			%fa:spinner .pulse%%i18n:common.loading%

			%fa:R comments%
			%i18n:@empty%

		<button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail">
			%i18n:@load-more%
			%i18n:common.loading%<mk-ellipsis/>
		</button>
	</mk-notes>

</template>


Simplify how render cleanup happens for Vega messages (#399)


Update Respawn chair product link

Update Respawn chair product link

<template>

    <league-signup-modal
        v-if="leagueSignupModalOpen"
        @close="leagueSignupModalOpen = false"
        @submit="submitRegistration"
        :first-name="firstName"
        :last-name="lastName"
        :name="name"
        :email="email"
        :birthday="birthday"
        :emails="emails"
        :unsubscribed-from-marketing-emails="unsubscribedFromMarketingEmails"
    >
    </league-signup-modal>

    <clan-creation-modal
        v-if="clanCreationModal"
        :clan="myCreatedClan"
        @close="clanCreationModal = false"
    >
    </clan-creation-modal>

        <clan-selector v-if="!isLoading && Array.isArray(myClans) && myClans.length > 0" :clans="myClans" @change="e => changeClanSelected(e)" :selected="clanIdSelected || clanIdOrSlug" style="margin-bottom: 40px;"/>

Competitive coding has never been so epic

    <SectionFirstCTA v-if="isGlobalPage" :doneRegistering="doneRegistering" :isClanCreator="isClanCreator" :onHandleJoinCTA="onHandleJoinCTA" :championshipActive="championshipActive" class="section-space" />

{{ currentSelectedClanName }}

{{ $t('league.team_derbezt') }}

{{showJoinTeamBtn ? $t('league.invite_link') : $t('league.public_link') }}

        <input readonly :value="clanInviteLink()" />

        {{ $t('league.join_now') }}
        {{ $t('league.edit_team') }}
        {{ $t('league.leave_team') }}
        {{ $t('league.join_team') }}

Deal: 15% off with code HXCOMBAT

          Valid through 2021. Standard shipping options/rates and quantity limits apply. Cannot be combined with other discount code/sale promotions.

        <leaderboard v-if="currentSelectedClan" :title="$t(`league.${championshipArenaSlug.replace(/-/g, '_')}`)" :rankings="selectedClanChampionshipRankings" :playerCount="selectedClanChampionshipLeaderboardPlayerCount" :key="`${clanIdSelected}-score`" :clanId="clanIdSelected" class="leaderboard-component" style="color: black;" />
        <leaderboard v-else :title="$t(`league.${championshipArenaSlug.replace(/-/g, '_')}`)" :rankings="globalChampionshipRankings" :playerCount="globalChampionshipLeaderboardPlayerCount" class="leaderboard-component" />
        {{ $t('league.play_arena_full', { arenaName: $t(`league.${championshipArenaSlug.replace(/-/g, '_')}`), arenaType: $t('league.arena_type_championship'), interpolation: { escapeValue: false } }) }}

Season 1 Final Arena Now Live!

{{ $t('league.season1_announcement_1') }}

{{ $t('league.season1_announcement_2') }}

{{ $t('league.season1_announcement_3') }}

{{ $t('league.season1_prize_1') }}

{{ $t('league.season1_prize_2') }}

{{ $t('league.season1_prize_3') }}

{{ $t('league.season1_prize_4') }}

{{ currentSelectedClanName }} stats

Global stats

      <ChildClanDetailDropdown
        v-if="currentSelectedClanChildDetails.length > 0"
        :label="`Search ${currentSelectedClanName} teams`"
        :childClans="currentSelectedClanChildDetails"
        class="clan-search"
      />
      <InputClanSearch v-if="isGlobalPage" :max-width="510" style="margin: 10px auto"/>

{{ $t('league.ladder_subheader') }}

        <leaderboard v-if="currentSelectedClan" :title="$t(`league.${regularArenaSlug.replace(/-/g, '_')}`)" :rankings="selectedClanRankings" :playerCount="selectedClanLeaderboardPlayerCount" :key="`${clanIdSelected}-score`" :clanId="clanIdSelected" class="leaderboard-component" style="color: black;" />
        <leaderboard v-else :rankings="globalRankings" :title="$t(`league.${regularArenaSlug.replace(/-/g, '_')}`)" :playerCount="globalLeaderboardPlayerCount" class="leaderboard-component" />
        {{ $t('league.play_arena_full', { arenaName: $t(`league.${regularArenaSlug.replace(/-/g, '_')}`), arenaType: $t('league.arena_type_regular'), interpolation: { escapeValue: false } }) }}

        <leaderboard :title="$t('league.codepoints')" :rankings="selectedClanCodePointsRankings" :key="`${clanIdSelected}-codepoints`" :clanId="clanIdSelected" scoreType="codePoints"
          class="leaderboard-component"
          :player-count="codePointsPlayerCount"
        />
        {{ $t('league.earn_codepoints') }}
        {{ $t('league.earn_codepoints') }}

Previous Season

Results from the Infinite Inferno Cup

        {{ $t('league.view_arena_winners', { arenaName: $t(`league.${previousRegularArenaSlug.replace(/-/g, '_')}`), arenaType: $t('league.arena_type_regular'), interpolation: { escapeValue: false } }) }}

        {{ $t('league.view_arena_winners', { arenaName: $t(`league.${previousChampionshipArenaSlug.replace(/-/g, '_')}`), arenaType: $t('league.arena_type_championship'), interpolation: { escapeValue: false } }) }}

    <SectionFirstCTA v-if="!isGlobalPage" :doneRegistering="doneRegistering" :isClanCreator="isClanCreator" :onHandleJoinCTA="onHandleJoinCTA" />

Free to get started

{{ $t('league.free_1') }}

{{ $t('league.free_2') }}

{{ $t('league.free_3') }}

{{ $t('league.free_4') }}

          {{ $t('league.join_now') }}

Compete in the Sorcerer's Blitz for a chance to win!

{{ $t('league.season1_prize_1') }}

{{ $t('league.season1_prize_2') }}

{{ $t('league.season1_prize_hyperx') }}

{{ $t('league.season1_prize_3') }}

{{ $t('league.season1_prize_4') }}

            Prizes will be awarded to players who reach the top of the leaderboard in the Finals arena.  Some prizes are limited to US participants only.
            CodeCombat reserves the right to determine in its sole discretion if a player qualifies and will receive a prize.

        Play Now

How it works

<p class="subheader2 mb-0" v-html="$t('league.how_it_works1', { team: `<span class='esports-aqua'>${this.$t('league.team')}</span>`, interpolation: { escapeValue: false } })">

Complete the training levels and compete in the Season Arena

Compete in the culminating Global Final Arena and push your coding skills to the test

Season arenas

Infinite Inferno Cup

Jan - April 2021

Sorcerer's Blitz

May - Aug 2021

Colossus Clash

Sep - Dec 2021

          {{ $t('league.season_subheading1') }}

          {{ $t('league.season_subheading2') }}

      {{ $t('league.join_now') }}

Are you an educator or esports coach?

{{ $t('league.tagline') }}

                {{ $t('league.end_to_end') }}

                {{ $t('league.path_success') }}

                {{ $t('league.unlimited_potential') }}

      {{ $t('league.edit_team') }}
      {{ $t('league.start_team') }}
      {{ $t('league.join_now') }}

{{ $t('league.features') }}

{{ $t('league.built_in') }}

{{ $t('league.built_in_subheader') }}

{{ $t('league.custom_dev') }}

{{ $t('league.custom_dev_subheader') }}

{{ $t('league.comprehensive_curr') }}

{{ $t('league.comprehensive_curr_subheader') }}

{{ $t('league.roster_management') }}

{{ $t('league.roster_management_subheader') }}

Bring competitive coding to your school

{{ $t('league.share_flyer') }}

          {{ $t('league.download_flyer') }}

</template>


Turn duration into red when it's over estimation

<template>

            {{ $t('tasks.fields.asset_type') }}

            {{ $t('tasks.fields.sequence') }}

            {{ $t('tasks.fields.entity_name') }}

            {{ $t('tasks.fields.task_status') }}

            {{ $t('tasks.fields.assignees') }}

            {{ $t('tasks.fields.frames') }}

            {{ $t('tasks.fields.estimation').substring(0, 3) }}.

            {{ $t('tasks.fields.duration').substring(0, 3) }}.

            {{ $t('tasks.fields.retake_count') }}

            {{ $t('tasks.fields.real_start_date') }}

            {{ $t('tasks.fields.real_end_date') }}

            {{ $t('tasks.fields.last_comment_date') }}

            <entity-thumbnail
              :entity="getEntity(task.entity.id)"
              :width="50"
              :height="33"
            />

            {{ getEntity(task.entity.id).asset_type_name }}

            {{ getEntity(task.entity.id).sequence_name }}

            {{ getEntity(task.entity.id).name }}

          <validation-cell
            class="status unselectable"
            :task-test="task"
            :is-border="false"
            :is-assignees="false"
            :selectable="false"
            :is-static="true"
          />

              <people-avatar
                class="flexrow-item"
                :key="task.id + '-' + personId"
                :person="personMap[personId]"
                :size="30"
                :font-size="17"
                v-for="personId in task.assignees"
              />

            {{ getEntity(task.entity.id).nb_frames }}

            {{ formatEstimation(task.estimation) }}

            {{ formatDuration(task.duration) }}

              •

            {{ formatDate(task.real_start_date) }}

            {{ formatDate(task.real_end_date) }}

            {{ formatDate(task.last_comment_date) }}

    {{ tasks.length }} {{ $tc('tasks.number', tasks.length) }}
    ({{ formatDuration(timeSpent) }}
     {{ $tc('main.days_spent', Math.floor((timeSpent ? timeSpent : 0) / 60) / 8) }}, {{ nbFrames }} {{ $tc('main.nb_frames', nbFrames) }})

</template>


[tasks] Fix task lists for shots

[tasks] Fix task lists for shots

<template>

          <router-link
            class="back-link flexrow-item"
            :to="backPath"
          >
            <chevron-left-icon />
          </router-link>
          <task-type-name
            class="flexrow-item"
            :task-type="currentTaskType"
          />

            <button-simple
              icon="download"
              :title="$t('main.csv.export_file')"
              @click="onExportClick"
              v-if="!isActiveTab('schedule')"
            />

              <router-link :to="tasksPath">
                {{ $t('tasks.tasks')}}
              </router-link>

              <router-link :to="schedulePath">
                {{ $t('schedule.title')}}
              </router-link>

              <router-link :to="estimationPath">
                {{ $t('estimation.title')}}
              </router-link>

            <search-field
              ref="task-search-field"
              :can-save="true"
              @change="onSearchChange"
              @save="saveSearchQuery"
              placeholder="ex: retake chara"
            />

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

              {{ $t('main.start_date') }}

            <datepicker
              wrapper-class="datepicker"
              input-class="date-input input"
              :language="locale"
              :disabled-dates="startDisabledDates"
              :monday-first="true"
              format="yyyy-MM-dd"
              v-model="schedule.selectedStartDate"
            />

              {{ $t('main.end_date') }}

            <datepicker
              wrapper-class="datepicker"
              input-class="date-input input"
              :language="locale"
              :disabled-dates="endDisabledDates"
              :monday-first="true"
              format="yyyy-MM-dd"
              v-model="schedule.selectedEndDate"
            />

            <combobox
              class="flexrow-item"
              :label="$t('tasks.colors.title')"
              :options="schedule.colorOptions"
              locale-key-prefix="tasks.colors."
              v-model="schedule.currentColor"
            />

            <combobox-number
              :label="$t('schedule.zoom_level')"
              :options="schedule.zoomOptions"
              v-model="schedule.zoomLevel"
              v-if="isActiveTab('schedule')"
            />

        <search-query-list
          :queries="searchQueries"
          @changesearch="changeSearch"
          @removesearch="removeSearchQuery"
          v-if="!loading.entities"
        />

      <task-list
        ref="task-list"
        :tasks="tasks"
        :is-assets="isAssets"
        :is-loading="loading.entities"
        :is-error="errors.entities"
        @task-selected="onTaskSelected"
        v-if="isActiveTab('tasks')"
      />

        <schedule
          ref="schedule-widget"
          :start-date="schedule.startDate"
          :end-date="schedule.endDate"
          :hierarchy="schedule.scheduleItems"
          :zoom-level=schedule.zoomLevel
          :height="schedule.scheduleHeight"
          :is-loading="loading.entities"
          :is-estimation-linked="true"
          @item-changed="saveTaskScheduleItem"
          @root-element-expanded="expandPersonElement"
          @estimation-changed="updateEstimation"
        />

        <estimation-helper
          ref="estimation-widget"
          :is-assets="isAssets"
          :tasks="tasks"
          @estimation-changed="updateEstimation"
        />

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

</template>


WIP(data table): implement automated table creation from data fed in

affects: da39a3ee5e6b4b0d3255bfef95601890afd80709@rei/cdr-table

<template>
  <!-- disable lint errors on line length in template -->
  <!-- eslint-disable max-len -->
  <div :class="$style['cdr-table__wrapper']">
    <div :class="$style['cdr-table__scrollable']">
      <table :class="[modifierClass]">
        <!-- @slot table head and/or body placed into data table component -->
        <slot v-if="records.length == 0"/>
        <template v-else>
          <thead v-if="headerCols.length > 0">
            <th
              v-for="header in headerCols"
              :key="header.id">{{ header.value }}
            </th>
          </thead>
          <tbody>
            <tr
              v-for="(recObj, index) in records"
              :key="index">
              <td
                v-for="header in headerCols"
                :key="header.id">
                {{ recObj[header.id] }}
              </td>
            </tr>
          </tbody>
        </template>
      </table>
    </div>
  </div>
</template>

<script>
import modifier from 'mixinsdir/modifier';
import CdrTokens from '@rei/cdr-tokens';
import matchmedia from 'matchmedia-polyfill';
import matchMediaAddListener from 'matchmedia-polyfill/matchMedia.addListener';

/**
 * Cedar 2 compfor for data table
 * @author [REI Software Engineering](https://rei.github.io/rei-cedar/)
 */
export default {
  name: 'CdrTable',
  mixins: [modifier],
  props: {
    records: {
      type: Array,
      required: false,
      default() {
        return [];
      },
    },
    headerCols: {
      type: Array,
      required: false,
      default() {
        return [];
      },
    },
    headerRows: {
      type: Array,
      required: false,
      default() {
        return [];
      },
    },
  },
  computed: {
    baseClass() {
      return 'cdr-table';
    },
  },
  // Determine whether or not table shoud be scrollable
  mounted() {
    window.matchMedia = window.matchMedia || matchmedia;
    window.matchMedia.addListener = window.matchMedia.addListener || matchMediaAddListener;

    const mq = window.matchMedia(`(min-width: ${CdrTokens.breakpointMd})`);

    mq.addListener(this.screenResize);

    this.screenResize(mq);
  },
  methods: {
    screenResize(mediaQuery) {
      const cdrTableScrollable =
        this.$el.getElementsByClassName(this.$style['cdr-table__scrollable'])[0];
      const cdrTable = this.$el.getElementsByTagName('table')[0];
      const cdrTableBody = this.$el.getElementsByTagName('tbody')[0];
      const numberOfCells = cdrTableBody ?
        cdrTableBody.getElementsByTagName('tr')[0].cells.length : 0;

      // For screens medium and larger
      if (mediaQuery.matches) {
        if (numberOfCells > 7) { // more than 7 cells means scrollable
          cdrTableScrollable.classList.add(this.$style['scrolling']); //eslint-disable-line
        } else { // 7 cells or less means non-scrollable
          cdrTableScrollable.classList.remove(this.$style['scrolling']); //eslint-disable-line
        }
      // For xs and sm screens
      } else if (window.matchMedia(`(max-width: ${CdrTokens.breakpointMd}`).matches) {
        // more than 2 cells means scrollable
        if (numberOfCells > 2) {
          cdrTableScrollable.classList.add(this.$style['scrolling']); //eslint-disable-line
          cdrTable.classList.remove(this.$style['single-column']);
        } else { // 2 cells or less means non-scrollable
          cdrTableScrollable.classList.remove(this.$style['scrolling']); //eslint-disable-line
          cdrTable.classList.add(this.$style['single-column']);
        }
      }
    },
  },
};
</script>

<style module>
  @import '../../css/settings/_index.pcss';
  @import './styles/vars/CdrTable.vars.pcss';
  @import './styles/CdrTable.pcss';
</style>

<template>
  <!-- disable lint errors on line length in template -->
  <!-- eslint-disable max-len -->
  <div :class="$style['cdr-table__wrapper']">
    <div :class="$style['cdr-table__scrollable']">
      <table :class="[modifierClass]">
        <!-- @slot table head and/or body placed into data table component -->
        <slot />
      </table>
    </div>
  </div>
</template>

<script>
import modifier from 'mixinsdir/modifier';
import CdrTokens from '@rei/cdr-tokens';
import matchmedia from 'matchmedia-polyfill';
import matchMediaAddListener from 'matchmedia-polyfill/matchMedia.addListener';

/**
 * Cedar 2 compfor for data table
 * @author [REI Software Engineering](https://rei.github.io/rei-cedar/)
 */
export default {
  name: 'CdrTable',
  mixins: [modifier],
  props: {
    records: {
      type: Array,
      required: false,
      default() {
        return [];
      },
    },
    headerCols: {
      type: Array,
      required: false,
      default() {
        return [];
      },
    },
    headerRows: {
      type: Array,
      required: false,
      default() {
        return [];
      },
    },
  },
  computed: {
    baseClass() {
      return 'cdr-table';
    },
  },
  // Determine whether or not table shoud be scrollable
  mounted() {
    window.matchMedia = window.matchMedia || matchmedia;
    window.matchMedia.addListener = window.matchMedia.addListener || matchMediaAddListener;

    const mq = window.matchMedia(`(min-width: ${CdrTokens.breakpointMd})`);

    mq.addListener(this.screenResize);

    this.screenResize(mq);
  },
  methods: {
    screenResize(mediaQuery) {
      const cdrTableScrollable =
        this.$el.getElementsByClassName(this.$style['cdr-table__scrollable'])[0];
      const cdrTable = this.$el.getElementsByTagName('table')[0];
      const cdrTableBody = this.$el.getElementsByTagName('tbody')[0];
      const numberOfCells = cdrTableBody ?
        cdrTableBody.getElementsByTagName('tr')[0].cells.length : 0;

      // For screens medium and larger
      if (mediaQuery.matches) {
        if (numberOfCells > 7) { // more than 7 cells means scrollable
          cdrTableScrollable.classList.add(this.$style['scrolling']); //eslint-disable-line
        } else { // 7 cells or less means non-scrollable
          cdrTableScrollable.classList.remove(this.$style['scrolling']); //eslint-disable-line
        }
      // For xs and sm screens
      } else if (window.matchMedia(`(max-width: ${CdrTokens.breakpointMd}`).matches) {
        // more than 2 cells means scrollable
        if (numberOfCells > 2) {
          cdrTableScrollable.classList.add(this.$style['scrolling']); //eslint-disable-line
          cdrTable.classList.remove(this.$style['single-column']);
        } else { // 2 cells or less means non-scrollable
          cdrTableScrollable.classList.remove(this.$style['scrolling']); //eslint-disable-line
          cdrTable.classList.add(this.$style['single-column']);
        }
      }
    },
  },
};
</script>

<style module>
  @import '../../css/settings/_index.pcss';
  @import './styles/vars/CdrTable.vars.pcss';
  @import './styles/CdrTable.pcss';
</style>


[#134] Standarize breadcrumbs

[#134] Standarize breadcrumbs

<template>

      <breadcrumbs :routes="[{name: 'Homepage', route_link: '/'}]" :active-route="$props.title" />

{{ this.$props.title }}

      <staticpage/>

</template>

<template>

{{ breadcrumbs }}

{{ this.$props.title }}

      <staticpage/>

</template>


Add snackbar messages for visibility status toggle

<template>

  <KPageContainer>
    <KButton
      v-if="!exam.active && !exam.archive"
      :primary="true"
      :text="$tr('openQuizLabel')"
      type="button"
      style="margin-left: 0; margin-top: 1rem; margin-bottom: 0;"
      @click="showConfirmationModal = true"
    />

      <KButton
        :text="$tr('closeQuizLabel')"
        type="submit"
        style="margin-left: 0; margin-top: 1rem; margin-bottom: 0;"
        :appearanceOverrides="cancelStyleOverrides"
        @click="showCancellationModal = true"
      />
      <StatusElapsedTime :date="examDateOpened" actionType="opened" />

        {{ $tr('quizClosedLabel') }}

        <StatusElapsedTime :date="examDateArchived" />

        {{ $tr('reportVisibleLabel') }}

        <KSwitch
          name="toggle-quiz-visibility"
          :checked="exam.active"
          :value="exam.active"
          :label="$tr('yesLabel')"
          @change="handleToggleVisibility"
        />

        {{ coachString('recipientsLabel') }}

        <Recipients
          slot="value"
          :groupNames="groupNames"
          :hasAssignments="exam.assignments.length > 0"
        />

          {{ $tr('averageScoreLabel') }}

            <KIcon
              icon="error"
              :color="$themeTokens.primary"
              style="position: relative; top:4px; left: 4px;"
            />
            <KTooltip
              reference="icon"
              :refs="$refs"
            >
              {{ $tr('visibleToLearnersTooltipMessage') }}
            </KTooltip>

        <Score :value="avgScore" />

        {{ $tr('questionOrderLabel') }}

{{ orderDescriptionString }}

    <QuizStatusModal
      v-if="showConfirmationModal"
      :modalHeader="$tr('openQuizLabel')"
      :modalDetail="$tr('openQuizModalDetail')"
      @cancel="showConfirmationModal = false"
      @submit="handleOpenQuiz"
    />
    <QuizStatusModal
      v-if="showCancellationModal"
      :modalHeader="$tr('closeQuizLabel')"
      :modalDetail="$tr('closeQuizModalDetail')"
      @cancel="showCancellationModal = false"
      @submit="handleCloseQuiz"
    />
  </KPageContainer>

</template>


make secondary buttons darker

make secondary buttons darker

<template>

      <logo class="logo"/>

{{ $tr('kolibri') }}

      <form class="login-form" ref="form" @submit.prevent="signIn">
        <ui-alert
          v-if="invalidCredentials"
          type="error"
          class="alert"
          :dismissible="false"
        >
          {{ $tr('signInError') }}
        </ui-alert>
        <transition name="textbox">
          <k-textbox
            ref="username"
            id="username"
            autocomplete="username"
            :autofocus="true"
            :label="$tr('username')"
            :invalid="usernameIsInvalid"
            :invalidText="usernameIsInvalidText"
            @blur="handleUsernameBlur"
            @input="showDropdown = true"
            @keydown="handleKeyboardNav"
            v-model="username"
          />
        </transition>
        <transition name="list">

            <ui-autocomplete-suggestion
              v-for="(suggestion, i) in suggestions"
              :key="i"
              :suggestion="suggestion"
              :class="{ highlighted: highlightedIndex === i }"
              @click.native="fillUsername(suggestion)"
            />

        </transition>
        <transition name="textbox">
          <k-textbox
            v-if="(!simpleSignIn || (simpleSignIn && (passwordMissing || invalidCredentials)))"
            ref="password"
            id="password"
            type="password"
            autocomplete="current-password"
            :label="$tr('password')"
            :autofocus="simpleSignIn"
            :invalid="passwordIsInvalid"
            :invalidText="passwordIsInvalidText"
            @blur="passwordBlurred = true"
            v-model="password"
          />
        </transition>
        <k-button
          class="login-btn"
          type="submit"
          :text="$tr('signIn')"
          :primary="true"
          :disabled="busy"
        />
      </form>

{{ $tr('noAccount') }}

        <router-link v-if="canSignUp" class="group-btn" :to="signUpPage">
          <k-button :text="$tr('createAccount')" :primary="false"/>
        </router-link>

          <k-button :text="$tr('accessAsGuest')" :primary="false"/>

{{ versionMsg }}

      <language-switcher-footer/>

</template>


dont show translate stance unless locales differ

<script lang="coffee">
import Session        from '@/shared/services/session'
import AbilityService from '@/shared/services/ability_service'
import openModal from '@/shared/helpers/open_modal'

export default
  components:
    ThreadItem: -> import('@/components/thread/item.vue')

  props:
    event: Object

  computed:
    eventable: -> @event.model()
    choiceInHeadline: ->
      @eventable.poll().hasOptionIcons() &&
      @eventable.stanceChoices().length == 1
    canEdit: ->

    componentType:  ->
      if @event.actor()
        'router-link'
      else
        'div'

  created: ->
    @actions =
      edit_stance:
        name: 'poll_common.change_vote'
        icon: 'mdi-pencil'
        canPerform: =>
          @eventable.latest && @eventable.poll().isActive() && @eventable.participant() == Session.user()
        perform: =>
          openModal
            component: 'PollCommonEditVoteModal',
            props:
              stance: @eventable.clone()

      translate_stance:
        icon: 'mdi-translate'
        canPerform: =>
          @eventable.author().locale != Session.user().locale &&
          @eventable.reason && AbilityService.canTranslate(@eventable)
        perform: =>
          @eventable.translate(Session.user().locale)

      show_history:
        name: 'action_dock.edited'
        icon: 'mdi-history'
        canPerform: => @eventable.edited()
        perform: =>
          openModal
            component: 'RevisionHistoryModal'
            props:
              model: @eventable
</script>

<template lang="pug">
thread-item.stance-created(:event="event")
  template(v-slot:actions)
    action-dock(:model="eventable" :actions="actions")
  template(v-if="choiceInHeadline" v-slot:headline)
    component(:is="componentType" :to="event.actor() && urlFor(event.actor())") {{event.actorName()}}
    space
    poll-common-stance-choice(:stance-choice="eventable.stanceChoices()[0]")
  poll-common-stance(:stance="eventable" :reason-only="choiceInHeadline")
  attachment-list(:attachments="eventable.attachments")
</template>

<script lang="coffee">
import Session        from '@/shared/services/session'
import AbilityService from '@/shared/services/ability_service'
import openModal from '@/shared/helpers/open_modal'

export default
  components:
    ThreadItem: -> import('@/components/thread/item.vue')

  props:
    event: Object

  computed:
    eventable: -> @event.model()
    choiceInHeadline: ->
      @eventable.poll().hasOptionIcons() &&
      @eventable.stanceChoices().length == 1
    canEdit: ->

    componentType:  ->
      if @event.actor()
        'router-link'
      else
        'div'

  created: ->
    @actions =
      edit_stance:
        name: 'poll_common.change_vote'
        icon: 'mdi-pencil'
        canPerform: =>
          @eventable.latest && @eventable.poll().isActive() && @eventable.participant() == Session.user()
        perform: =>
          openModal
            component: 'PollCommonEditVoteModal',
            props:
              stance: @eventable.clone()

      translate_stance:
        icon: 'mdi-translate'
        canPerform: =>
          @eventable.reason && AbilityService.canTranslate(@eventable)
        perform: =>
          @eventable.translate(Session.user().locale)

      show_history:
        name: 'action_dock.edited'
        icon: 'mdi-history'
        canPerform: => @eventable.edited()
        perform: =>
          openModal
            component: 'RevisionHistoryModal'
            props:
              model: @eventable
</script>

<template lang="pug">
thread-item.stance-created(:event="event")
  template(v-slot:actions)
    action-dock(:model="eventable" :actions="actions")
  template(v-if="choiceInHeadline" v-slot:headline)
    component(:is="componentType" :to="event.actor() && urlFor(event.actor())") {{event.actorName()}}
    space
    poll-common-stance-choice(:stance-choice="eventable.stanceChoices()[0]")
  poll-common-stance(:stance="eventable" :reason-only="choiceInHeadline")
  attachment-list(:attachments="eventable.attachments")
</template>


docs(NotificationsMenu): remove query date comparison

<template>
  <app-menu
    key="notifications-menu"
    v-model="menu"
    :close-on-content-click="false"
    :max-width="maxWidth"
    bottom
    content-class="overflow-hidden"
    left
  >
    <template #activator="{ attrs, on }">
      <app-tooltip-btn
        path="notifications"
        v-bind="attrs"
        v-on="on"
      >
        <template #icon>
          <v-badge
            :value="unread.length"
            color="red"
            left
            overlap
          >
            <template #badge>
              {{ unread.length }}
            </template>

            <v-icon v-text="`$mdiBell${unread.length === 0 ? 'Outline' : ''}`" />
          </v-badge>
        </template>
      </app-tooltip-btn>
    </template>

    <v-toolbar
      :width="maxWidth"
      class="pr-5"
      flat
      short
    >
      <v-btn
        :disabled="archived ? unread.length < 1 : read.length < 1"
        class="px-2 ml-n1"
        small
        text
        @click="archived = !archived"
      >
        <i18n :path="archived ? 'unread' : 'read'">
          <template #number>
            <strong v-text="archived ? unread.length : read.length" />
          </template>
        </i18n>
      </v-btn>

      <v-spacer />

      <app-tooltip-btn
        v-if="marked"
        :disabled="done"
        :icon="marked.icon"
        :path="`marked-${marked.path}`"
        small
        @click="toggleAll"
      />
    </v-toolbar>

    <v-divider />

    <v-responsive
      class="overflow-y-scroll"
      max-height="320"
    >
      <div
        v-if="done"
        class="py-8 text-center text-subtitle-1"
      >
        <i18n
          path="done"
          tag="p"
        />

        <v-icon
          color="grey lighten-2"
          size="96"
        >
          $mdiVuetify
        </v-icon>
      </div>

      <template v-else>
        <v-list-item
          v-for="({
            created_at_formated: created_at,
            metadata,
            slug,
            title
          }) in filtered"
          :key="slug"
          :ripple="false"
          @click="select(slug)"
        >
          <v-list-item-content>
            <div
              class="grey--text text--darken-1 text-caption font-weight-light text-uppercase"
              v-text="created_at"
            />

            <v-list-item-title
              class="text-subtitle-1"
              v-text="`${metadata.emoji} ${title}`"
            />
          </v-list-item-content>

          <v-list-item-action>
            <v-btn
              :ripple="false"
              icon
              @click.stop.prevent="toggle(slug)"
            >
              <v-icon v-text="marked.icon" />
            </v-btn>
          </v-list-item-action>
        </v-list-item>
      </template>
    </v-responsive>
  </app-menu>
</template>

<script>
  // Utilities
  import { differenceInDays } from 'date-fns'
  import { formatDate } from '@/util/date.js'
  import { get, sync } from 'vuex-pathify'
  import { wait } from '@/util/helpers'
  import bucket from '@/plugins/cosmicjs'

  export default {
    name: 'NotificationsMenu',

    inject: ['theme'],

    data: () => ({
      all: [],
      archived: false,
      icons: {
        read: '$mdiEmailOpen',
        unread: '$mdiEmailMarkAsUnread',
      },
      menu: false,
    }),

    computed: {
      snackbar: sync('snackbar/snackbar'),
      unotifications: sync('user/notifications'),
      hasRecentlyViewed: get('user/hasRecentlyViewed'),
      done () {
        return this.filtered.length === 0
      },
      // What is ultimately iterated
      filtered () {
        return this.archived ? this.read : this.unread
      },
      // Map items to contain a viewed
      // property and format the date
      mapped () {
        return this.all.map(item => {
          const date = new Date(item.created_at)

          return {
            ...item,
            created_at: date.getTime(),
            created_at_formatted: formatDate(date),
            viewed: this.unotifications.includes(item.slug),
          }
        })
      },
      marked () {
        const path = this.archived ? 'unread' : 'read'
        const icon = this.icons[path]

        if (!icon || !path) return null

        return { icon, path }
      },
      maxWidth () {
        return this.$vuetify.breakpoint.mobile ? 296 : 320
      },
      read () {
        return this.mapped.filter(n => n.viewed)
      },
      unread () {
        return this.mapped.filter(n => !n.viewed)
      },
    },

    async mounted () {
      if (!bucket.available) return

      const { objects: notifications } = await bucket.getObjects({
        type: 'notifications',
        props: 'created_at,metadata,slug,title',
        status: 'published',
        limit: 10,
        sort: '-created_at',
      })

      this.all = notifications || []

      if (
        this.hasRecentlyViewed ||
        !this.unread.length
      ) return

      await wait(3000)

      /* eslint-disable-next-line camelcase */
      const { created_at, slug, metadata } = this.unread[0]

      if (differenceInDays(Date.now(), created_at) > 60) return

      this.snackbar = {
        slug,
        ...metadata,
      }
    },

    methods: {
      select (slug) {
        const { metadata } = this.all.find(notification => {
          return notification.slug === slug
        }) || {}

        this.snackbar = {
          slug,
          ...metadata,
        }

        this.menu = false
      },
      toggle (slug) {
        this.unotifications = this.unotifications.includes(slug)
          ? this.unotifications.filter(n => n !== slug)
          : [...this.unotifications, slug]
      },
      toggleAll () {
        this.unotifications = !this.archived
          ? this.notifications.map(i => i.slug)
          : []
      },
    },
  }
</script>

<template>
  <app-menu
    key="notifications-menu"
    v-model="menu"
    :close-on-content-click="false"
    :max-width="maxWidth"
    bottom
    content-class="overflow-hidden"
    left
  >
    <template #activator="{ attrs, on }">
      <app-tooltip-btn
        path="notifications"
        v-bind="attrs"
        v-on="on"
      >
        <template #icon>
          <v-badge
            :value="unread.length"
            color="red"
            left
            overlap
          >
            <template #badge>
              {{ unread.length }}
            </template>

            <v-icon v-text="`$mdiBell${unread.length === 0 ? 'Outline' : ''}`" />
          </v-badge>
        </template>
      </app-tooltip-btn>
    </template>

    <v-toolbar
      :width="maxWidth"
      class="pr-5"
      flat
      short
    >
      <v-btn
        :disabled="archived ? unread.length < 1 : read.length < 1"
        class="px-2 ml-n1"
        small
        text
        @click="archived = !archived"
      >
        <i18n :path="archived ? 'unread' : 'read'">
          <template #number>
            <strong v-text="archived ? unread.length : read.length" />
          </template>
        </i18n>
      </v-btn>

      <v-spacer />

      <app-tooltip-btn
        v-if="marked"
        :disabled="done"
        :icon="marked.icon"
        :path="`marked-${marked.path}`"
        small
        @click="toggleAll"
      />
    </v-toolbar>

    <v-divider />

    <v-responsive
      class="overflow-y-scroll"
      max-height="320"
    >
      <div
        v-if="done"
        class="py-8 text-center text-subtitle-1"
      >
        <i18n
          path="done"
          tag="p"
        />

        <v-icon
          color="grey lighten-2"
          size="96"
        >
          $mdiVuetify
        </v-icon>
      </div>

      <template v-else>
        <v-list-item
          v-for="({ created_at, metadata, slug, title }) in filtered"
          :key="slug"
          :ripple="false"
          @click="select(slug)"
        >
          <v-list-item-content>
            <div
              class="grey--text text--darken-1 text-caption font-weight-light text-uppercase"
              v-text="created_at"
            />

            <v-list-item-title
              class="text-subtitle-1"
              v-text="`${metadata.emoji} ${title}`"
            />
          </v-list-item-content>

          <v-list-item-action>
            <v-btn
              :ripple="false"
              icon
              @click.stop.prevent="toggle(slug)"
            >
              <v-icon v-text="marked.icon" />
            </v-btn>
          </v-list-item-action>
        </v-list-item>
      </template>
    </v-responsive>
  </app-menu>
</template>

<script>
  // Utilities
  import { formatDate } from '@/util/date.js'
  import { get, sync } from 'vuex-pathify'
  import { subDays } from 'date-fns'
  import { wait } from '@/util/helpers'
  import bucket from '@/plugins/cosmicjs'

  export default {
    name: 'NotificationsMenu',

    inject: ['theme'],

    data: () => ({
      all: [],
      archived: false,
      icons: {
        read: '$mdiEmailOpen',
        unread: '$mdiEmailMarkAsUnread',
      },
      menu: false,
    }),

    computed: {
      snackbar: sync('snackbar/snackbar'),
      unotifications: sync('user/notifications'),
      hasRecentlyViewed: get('user/hasRecentlyViewed'),
      done () {
        return this.filtered.length === 0
      },
      // What is ultimately iterated
      filtered () {
        return this.archived ? this.read : this.unread
      },
      // Map items to contain a viewed
      // property and format the date
      mapped () {
        return this.all.map(item => {
          return {
            ...item,
            created_at: formatDate(new Date(item.created_at)),
            viewed: this.unotifications.includes(item.slug),
          }
        })
      },
      marked () {
        const path = this.archived ? 'unread' : 'read'
        const icon = this.icons[path]

        if (!icon || !path) return null

        return { icon, path }
      },
      maxWidth () {
        return this.$vuetify.breakpoint.mobile ? 296 : 320
      },
      read () {
        return this.mapped.filter(n => n.viewed)
      },
      unread () {
        return this.mapped.filter(n => !n.viewed)
      },
    },

    async mounted () {
      if (!bucket.available) return

      const { objects: notifications } = await bucket.getObjects({
        type: 'notifications',
        props: 'created_at,metadata,slug,title',
        status: 'published',
        limit: 10,
        sort: '-created_at',
        query: {
          created_at: {
            $gt: subDays(Date.now(), 60).toISOString().slice(0, 10),
          },
        },
      })

      this.all = notifications || []

      if (
        this.hasRecentlyViewed ||
        !this.unread.length
      ) return

      await wait(3000)

      const { slug, metadata } = this.unread[0]

      this.snackbar = {
        slug,
        ...metadata,
      }
    },

    methods: {
      select (slug) {
        const { metadata } = this.all.find(notification => {
          return notification.slug === slug
        }) || {}

        this.snackbar = {
          slug,
          ...metadata,
        }

        this.menu = false
      },
      toggle (slug) {
        this.unotifications = this.unotifications.includes(slug)
          ? this.unotifications.filter(n => n !== slug)
          : [...this.unotifications, slug]
      },
      toggleAll () {
        this.unotifications = !this.archived
          ? this.notifications.map(i => i.slug)
          : []
      },
    },
  }
</script>


#332 作业立即终止增加二次确认

#332 作业立即终止增加二次确认

<template>

        <Top-bar :domain="domainName" :domain-info="domainInfo"></Top-bar>

                        作业 : {{jobName}}

                    <el-tag :type="statusTag[jobInfo.status]" class="status-tag">运行状态: {{translateStatus[jobInfo.status]}}</el-tag>

                      <el-button size="small" @click="handleActive(true)" v-if="jobInfo.status === 'STOPPING' || jobInfo.status === 'STOPPED'">启用</el-button>
                      <el-button size="small" @click="handleActive(false)" v-if="jobInfo.status === 'READY' || jobInfo.status === 'RUNNING'">禁用</el-button>
                      <el-button size="small" @click="handleOperate('runAtOnce')" v-if="jobInfo.status === 'READY' && jobStatusJudge()">立即执行</el-button>
                      <el-button size="small" @click="handleOperate('stopAtOnce')" v-if="jobInfo.status === 'STOPPING'">立即终止</el-button>
                      <el-button size="small" @click="handleDelete" v-if="jobInfo.status === 'STOPPED' || !jobInfo.enabled">删除</el-button>

              <router-view></router-view>

</template>


pagination & page size

pagination & page size

<template>

用户管理
        用户资料记录

          <router-link to="/"> 总览</router-link>

用户管理

用户资料记录

            <el-button type="primary" @click="onCreate"> 添加用户</el-button>
            <el-button type="danger" @click="onBundleDelete" :disabled="!selectedItems.length">
              删除用户
            </el-button>

            <el-form :inline="true" :model="searchForm">
              <el-form-item label="审批人">
                <el-input v-model="searchForm.user" placeholder="审批人"></el-input>
              </el-form-item>
              <el-form-item label="审批人">
                <el-input v-model="searchForm.user" placeholder="审批人"></el-input>
              </el-form-item>
              <el-form-item label="审批人">
                <el-input v-model="searchForm.user" placeholder="审批人"></el-input>
              </el-form-item>

              <el-form-item label="活动区域">
                <el-select v-model="searchForm.region" placeholder="活动区域">
                  <el-option label="区域一" value="shanghai"></el-option>
                  <el-option label="区域二" value="beijing"></el-option>
                </el-select>
              </el-form-item>
              <el-form-item>
                <el-button type="primary" @click="onSubmitSearch">查询</el-button>
              </el-form-item>
            </el-form>

            <el-row class="tools">
              <el-col :span="4">
                显示
                <el-select v-model="pagination.pageSize" style="width: 80px"
                           @change="handleSizeChange">
                  <el-option
                          v-for="item in pagination.pageSizeList"
                          :key="item.value"
                          :label="item.label"
                          :value="item.value">
                  </el-option>
                </el-select>
                  项结果
              </el-col>
              <el-col :span="12">
                <el-pagination
                        @current-change="handlePageChange"
                        :current-page="pagination.currentPage"
                        :page-size="pagination.pageSize==-1?1:pagination.pageSize"
                        :layout="pagination.pageSize==-1?'total':'total, prev, pager, next, jumper'"
                        :total="pagination.totalCount">
                </el-pagination>
              </el-col>
              <el-col :span="8">
                <el-input class="auto-search" style="width: 200px;float: right;"
                          placeholder="模糊搜索"
                          icon="search"
                          v-model="searchTerm">
                </el-input>
              </el-col>
            </el-row>

            <el-table
                    :data="tableData"
                    border
                    style="width: 100%"
                    max-height="500"
                    :default-sort="{prop: 'date', order: 'descending'}"
                    @sort-change="onSortChange"
                    ref="table"
            >
              <el-table-column
                      fixed
                      type="selection"
                      width="55">
              </el-table-column>
              <el-table-column
                      prop="name"
                      label="Name"
                      sortable="custom"
                      show-overflow-tooltip
                      width="180">
              </el-table-column>
              <el-table-column
                      prop="email"
                      label="Email"
                      sortable="custom"
                      show-overflow-tooltip>
              </el-table-column>
              <el-table-column
                      prop="is_active"
                      label="Is Active"
                      sortable="custom"
                      show-overflow-tooltip>
              </el-table-column>
              <el-table-column
                      prop="last_login_at"
                      label="Last Login"
                      sortable="custom"
                      show-overflow-tooltip>
                <template scope="scope">
                  <el-tag v-if="scope.row.last_login_at">{{ scope.row.last_login_at }}</el-tag>
                  <el-tag type="grey" v-if="scope.row.login_times">{{ scope.row.login_times }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column
                      prop="created_at"
                      label="Created At"
                      sortable="custom"
                      show-overflow-tooltip>
              </el-table-column>
              <el-table-column
                      fixed="right"
                      label="操作"
                      width="100">
                <template scope="scope">
                  <el-button @click="handleClick" type="text" size="small">查看</el-button>
                  <el-button type="text" size="small">编辑</el-button>
                </template>
              </el-table-column>
            </el-table>

          <el-button type="primary" @click="onCreate"> 添加用户</el-button>
          <el-button type="danger" @click="onBundleDelete" :disabled="!selectedItems.length">
            删除用户
          </el-button>

</template>

<template>

用户管理
        用户资料记录

          <router-link to="/"> 总览</router-link>

用户管理

用户资料记录

            <el-button type="primary" @click="onCreate"> 添加用户</el-button>
            <el-button type="danger" @click="onBundleDelete" :disabled="!selectedItems.length">
              删除用户
            </el-button>

            <el-form :inline="true" :model="searchForm">
              <el-form-item label="审批人">
                <el-input v-model="searchForm.user" placeholder="审批人"></el-input>
              </el-form-item>
              <el-form-item label="审批人">
                <el-input v-model="searchForm.user" placeholder="审批人"></el-input>
              </el-form-item>
              <el-form-item label="审批人">
                <el-input v-model="searchForm.user" placeholder="审批人"></el-input>
              </el-form-item>

              <el-form-item label="活动区域">
                <el-select v-model="searchForm.region" placeholder="活动区域">
                  <el-option label="区域一" value="shanghai"></el-option>
                  <el-option label="区域二" value="beijing"></el-option>
                </el-select>
              </el-form-item>
              <el-form-item>
                <el-button type="primary" @click="onSubmitSearch">查询</el-button>
              </el-form-item>
            </el-form>

            <el-row class="tools">
              <el-col :span="4">
                显示
                <el-select v-model="pageSize" style="width: 80px"
                           @change="handleSizeChange">
                  <el-option
                          v-for="item in pageSizeList"
                          :key="item.value"
                          :label="item.label"
                          :value="item.value">
                  </el-option>
                </el-select>
                  项结果
              </el-col>
              <el-col :span="12">
                <el-pagination
                        @current-change="handleCurrentChange"
                        :current-page="currentPage"
                        :page-size="pageSize==-1?1:pageSize"
                        :layout="pageSize==-1?'total':'total, prev, pager, next, jumper'"
                        :total="400">
                </el-pagination>
              </el-col>
              <el-col :span="8">
                <el-input class="auto-search" style="width: 200px;float: right;"
                          placeholder="模糊搜索"
                          icon="search"
                          v-model="searchTerm">
                </el-input>
              </el-col>
            </el-row>

            <el-table
                    :data="tableData"
                    border
                    style="width: 100%"
                    max-height="500"
                    :default-sort="{prop: 'date', order: 'descending'}"
                    ref="table"
            >
              <el-table-column
                      fixed
                      type="selection"
                      width="55">
              </el-table-column>
              <el-table-column
                      prop="name"
                      label="Name"
                      sortable
                      show-overflow-tooltip
                      width="180">
              </el-table-column>
              <el-table-column
                      prop="email"
                      label="Email"
                      sortable
                      show-overflow-tooltip>
              </el-table-column>
              <el-table-column
                      prop="is_active"
                      label="Is Active"
                      sortable
                      show-overflow-tooltip>
              </el-table-column>
              <el-table-column
                      prop="last_login_at"
                      label="Last Login"
                      sortable
                      show-overflow-tooltip>
                <template scope="scope">
                  <el-tag v-if="scope.row.last_login_at">{{ scope.row.last_login_at }}</el-tag>
                  <el-tag type="grey" v-if="scope.row.login_times">{{ scope.row.login_times }}</el-tag>
                </template>
              </el-table-column>
              <el-table-column
                      prop="created_at"
                      label="Created At"
                      sortable
                      show-overflow-tooltip>
              </el-table-column>
              <el-table-column
                      fixed="right"
                      label="操作"
                      width="100">
                <template scope="scope">
                  <el-button @click="handleClick" type="text" size="small">查看</el-button>
                  <el-button type="text" size="small">编辑</el-button>
                </template>
              </el-table-column>
            </el-table>

          <el-button type="primary" @click="onCreate"> 添加用户</el-button>
          <el-button type="danger" @click="onBundleDelete" :disabled="!selectedItems.length">
            删除用户
          </el-button>

</template>


add button to change profile pic

add button to change profile pic

<script lang="coffee">
import AppConfig      from '@/shared/services/app_config'
import Session        from '@/shared/services/session'
import Records        from '@/shared/services/records'
import EventBus       from '@/shared/services/event_bus'
import AbilityService from '@/shared/services/ability_service'
import ModalService   from '@/shared/services/modal_service'
import LmoUrlService  from '@/shared/services/lmo_url_service'
import openModal      from '@/shared/helpers/open_modal'
import UserService    from '@/shared/services/user_service'

import { submitForm }   from '@/shared/helpers/form'
import { hardReload }   from '@/shared/helpers/window'

export default
  data: ->
    isDisabled: false
    user: null
    originalUser: null

  created: ->
    @init()
    EventBus.$emit 'currentComponent', { titleKey: 'profile_page.edit_profile', page: 'profilePage'}
    EventBus.$on 'updateProfile', => @init()
    EventBus.$on 'signedIn', => @init()

  computed:
    showHelpTranslate: -> AppConfig.features.app.help_link
    availableLocales: -> AppConfig.locales
    actions: -> UserService.actions(Session.user(), @)

  methods:
    init: ->
      return unless Session.isSignedIn()
      @watchRecords
        key: Session.user().id
        collections: ['users']
        query: =>
          @originalUser = Session.user()
          @user = @originalUser.clone()
          Session.updateLocale(@user.locale)

          @submit = submitForm @, @user,
            flashSuccess: 'profile_page.messages.updated'
            submitFn: Records.users.updateProfile
            successCallback: @init

    changePicture: ->
      openModal
        component: 'ChangePictureForm'

    changePassword: ->
      @openChangePasswordModal(@user)

    openDeleteUserModal: ->
      @isDeleteUserModalOpen = true

    closeDeleteUserModal: ->
      @isDeleteUserModalOpen = false

</script>
<template lang="pug">
v-content
  v-container.profile-page.max-width-1024
    loading(v-if='!user')
    div(v-if='user')
      v-card
        submit-overlay(:value='user.processing')
        //- v-card-title
        //-   h1.headline(v-t="'profile_page.edit_profile'")
        v-card-text
          v-layout
            v-flex.profile-page__details
              v-layout(:column="$vuetify.breakpoint.xs")
                v-flex
                  v-text-field.profile-page__name-input(:label="$t('profile_page.name_label')" required v-model="user.name")
                  validation-errors(:subject='user', field='name')

                  v-text-field#user-username-field.profile-page__username-input(:label="$t('profile_page.username_label')" required v-model="user.username")
                  validation-errors(:subject='user', field='username')

                  v-text-field#user-email-field.profile-page__email-input(:label="$t('profile_page.email_label')" required='ng-required', v-model='user.email')
                  validation-errors(:subject='user', field='email')

                .profile-page__avatar.d-flex.flex-column.justify-center.align-center.mx-12(@click="changePicture()")
                  user-avatar(:user='originalUser' size='featured' :no-link="true")
                  v-btn(color="accent" @click="changePicture" v-t="'profile_page.change_picture_link'")

              lmo-textarea(:model='user' field="shortBio" :label="$t('profile_page.short_bio_label')" :placeholder="$t('profile_page.short_bio_placeholder')")
              validation-errors(:subject='user', field='shortBio')

              v-text-field#user-location-field.profile-page__location-input(v-model='user.location' :label="$t('profile_page.location_label')" :placeholder="$t('profile_page.location_placeholder')")

              v-select#user-locale-field(:label="$t('profile_page.locale_label')" :items="availableLocales" v-model="user.selectedLocale" item-text="name" item-value="key")
              validation-errors(:subject='user', field='selectedLocale')
              p(v-if='showHelpTranslate')
                a(v-t="'profile_page.help_translate'" href='https://www.loomio.org/g/cpaM3Hsv/loomio-community-translation' target='_blank')
        v-card-actions.profile-page__update-account
          v-spacer
          v-btn.profile-page__update-button(color="primary" @click='submit()' :disabled='isDisabled' v-t="'profile_page.update_profile'")

      v-card.profile-page-card.mt-4
        v-list
          v-list-item(v-for="(action, key) in actions" :key="key" v-if="action.canPerform()" @click="action.perform()" :class="'user-page__' + key")
            v-list-item-icon
              v-icon {{action.icon}}
            v-list-item-title(v-t="action.name")
        //-
        //-   v-btn.profile-page__change-password(color="accent" outlined @click='changePassword()' v-t="'profile_page.change_password_link'")
        //- v-card-text
        //-   h3.lmo-h3(v-t="'profile_page.deactivate_account'")
        //-   v-btn.profile-page__deactivate(outlined color="warning" @click='openConfirmModal(deactivateUserConfirmOpts)', v-t="'profile_page.deactivate_account'")
        //-
        //-   h3.lmo-h3(v-t="'profile_page.delete_account'")
        //-   v-btn.profile-page__delete(outlined color="warning" @click='openConfirmModal(deleteUserConfirmOpts)', v-t="'profile_page.delete_user_link'")

</template>
<style lang="sass">
.profile-page__avatar
  cursor: pointer
</style>

<script lang="coffee">
import AppConfig      from '@/shared/services/app_config'
import Session        from '@/shared/services/session'
import Records        from '@/shared/services/records'
import EventBus       from '@/shared/services/event_bus'
import AbilityService from '@/shared/services/ability_service'
import ModalService   from '@/shared/services/modal_service'
import LmoUrlService  from '@/shared/services/lmo_url_service'
import openModal      from '@/shared/helpers/open_modal'
import UserService    from '@/shared/services/user_service'

import { submitForm }   from '@/shared/helpers/form'
import { hardReload }   from '@/shared/helpers/window'

export default
  data: ->
    isDisabled: false
    user: null
    originalUser: null

  created: ->
    @init()
    EventBus.$emit 'currentComponent', { titleKey: 'profile_page.edit_profile', page: 'profilePage'}
    EventBus.$on 'updateProfile', => @init()
    EventBus.$on 'signedIn', => @init()

  computed:
    showHelpTranslate: -> AppConfig.features.app.help_link
    availableLocales: -> AppConfig.locales
    actions: -> UserService.actions(Session.user(), @)

  methods:
    init: ->
      return unless Session.isSignedIn()
      @watchRecords
        key: Session.user().id
        collections: ['users']
        query: =>
          @originalUser = Session.user()
          @user = @originalUser.clone()
          Session.updateLocale(@user.locale)

          @submit = submitForm @, @user,
            flashSuccess: 'profile_page.messages.updated'
            submitFn: Records.users.updateProfile
            successCallback: @init

    changePicture: ->
      openModal
        component: 'ChangePictureForm'

    changePassword: ->
      @openChangePasswordModal(@user)

    openDeleteUserModal: ->
      @isDeleteUserModalOpen = true

    closeDeleteUserModal: ->
      @isDeleteUserModalOpen = false

</script>
<template lang="pug">
v-content
  v-container.profile-page.max-width-1024
    loading(v-if='!user')
    div(v-if='user')
      v-card
        submit-overlay(:value='user.processing')
        //- v-card-title
        //-   h1.headline(v-t="'profile_page.edit_profile'")
        v-card-text
          v-layout
            v-flex.profile-page__details
              v-layout(:column="$vuetify.breakpoint.xs")
                v-flex
                  v-text-field.profile-page__name-input(:label="$t('profile_page.name_label')" required v-model="user.name")
                  validation-errors(:subject='user', field='name')

                  v-text-field#user-username-field.profile-page__username-input(:label="$t('profile_page.username_label')" required v-model="user.username")
                  validation-errors(:subject='user', field='username')

                  v-text-field#user-email-field.profile-page__email-input(:label="$t('profile_page.email_label')" required='ng-required', v-model='user.email')
                  validation-errors(:subject='user', field='email')

                v-flex.profile-page__avatar(justify-center @click="changePicture()")
                  user-avatar(:user='originalUser' size='featured' :no-link="true")

              lmo-textarea(:model='user' field="shortBio" :label="$t('profile_page.short_bio_label')" :placeholder="$t('profile_page.short_bio_placeholder')")
              validation-errors(:subject='user', field='shortBio')

              v-text-field#user-location-field.profile-page__location-input(v-model='user.location' :label="$t('profile_page.location_label')" :placeholder="$t('profile_page.location_placeholder')")

              v-select#user-locale-field(:label="$t('profile_page.locale_label')" :items="availableLocales" v-model="user.selectedLocale" item-text="name" item-value="key")
              validation-errors(:subject='user', field='selectedLocale')
              p(v-if='showHelpTranslate')
                a(v-t="'profile_page.help_translate'" href='https://www.loomio.org/g/cpaM3Hsv/loomio-community-translation' target='_blank')
        v-card-actions.profile-page__update-account
          v-spacer
          v-btn.profile-page__update-button(color="primary" @click='submit()' :disabled='isDisabled' v-t="'profile_page.update_profile'")

      v-card.profile-page-card.mt-4
        v-list
          v-list-item(v-for="(action, key) in actions" :key="key" v-if="action.canPerform()" @click="action.perform()" :class="'user-page__' + key")
            v-list-item-icon
              v-icon {{action.icon}}
            v-list-item-title(v-t="action.name")
        //-
        //-   v-btn.profile-page__change-password(color="accent" outlined @click='changePassword()' v-t="'profile_page.change_password_link'")
        //- v-card-text
        //-   h3.lmo-h3(v-t="'profile_page.deactivate_account'")
        //-   v-btn.profile-page__deactivate(outlined color="warning" @click='openConfirmModal(deactivateUserConfirmOpts)', v-t="'profile_page.deactivate_account'")
        //-
        //-   h3.lmo-h3(v-t="'profile_page.delete_account'")
        //-   v-btn.profile-page__delete(outlined color="warning" @click='openConfirmModal(deleteUserConfirmOpts)', v-t="'profile_page.delete_user_link'")

</template>
<style lang="sass">
.profile-page__avatar
  cursor: pointer
</style>


Update PaginatedListContainer

Update PaginatedListContainer

<template>

    <KGrid>
      <KGridItem sizes="4, 8, 3">
        <slot name="otherFilter"></slot>
      </KGridItem>
      <KGridItem sizes="4, 8, 9" alignments="left, left, right" class="text-filter">
        <KFilterTextbox
          v-model="filterInput"
          :placeholder="filterPlaceholder"
          @input="pageNum = 1"
        />
      </KGridItem>
    </KGrid>

      <slot v-bind="{items: visibleFilteredItems, filterInput}"></slot>

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

      <UiIconButton
        type="primary"
        :ariaLabel="$tr('previousResults')"
        :disabled="pageNum === 1"
        size="small"
        class="pagination-button"
        @click="changePage(-1)"
      >
        <KIcon icon="back" style="position: relative; top: -1px;" />
      </UiIconButton>
      <UiIconButton
        type="primary"
        :ariaLabel="$tr('nextResults')"
        :disabled="pageNum === 0 || pageNum === numPages"
        size="small"
        class="pagination-button"
        @click="changePage(+1)"
      >
        <KIcon icon="forward" style="position: relative; top: -1px;" />
      </UiIconButton>

</template>

<template>

    <KGrid>
      <KGridItem sizes="3, 3, 3">
        <slot name="otherFilter"></slot>
      </KGridItem>
      <KGridItem sizes="9, 9, 9" align="right" class="text-filter">
        <KFilterTextbox
          v-model.trim="filterInput"
          :placeholder="filterPlaceholder"
          @input="pageNum = 1"
        />
      </KGridItem>
    </KGrid>

      <slot v-bind="{items: visibleFilteredItems, filterInput}"></slot>

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

      <UiIconButton
        type="primary"
        :ariaLabel="$tr('previousResults')"
        :disabled="pageNum === 1"
        size="small"
        class="pagination-button"
        @click="changePage(-1)"
      >
        <KIcon icon="back" style="position: relative; top: -1px;" />
      </UiIconButton>
      <UiIconButton
        type="primary"
        :ariaLabel="$tr('nextResults')"
        :disabled="pageNum === 0 || pageNum === numPages"
        size="small"
        class="pagination-button"
        @click="changePage(+1)"
      >
        <KIcon icon="forward" style="position: relative; top: -1px;" />
      </UiIconButton>

</template>


status: fix css and display status inline with badge

Signed-off-by: Rohit Yadav <902b9eb5dd62819678163bd5e00fbf0cbc7c264b@shapeblue.com>

<template>
  <a-tooltip placement="bottom">
    <template slot="title">
      {{ text }}
    </template>
    <a-badge style="display: inline-flex" :title="text" :status="getBadgeStatus(text)" :text="getText()" />
  </a-tooltip>
</template>

<script>

export default {
  name: 'Status',
  components: {
  },
  props: {
    text: {
      type: String,
      required: true
    },
    displayText: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
    }
  },
  methods: {
    getText () {
      if (this.displayText && this.text) {
        return this.text.charAt(0).toUpperCase() + this.text.slice(1)
      }
      return ''
    },
    getBadgeStatus (state) {
      var status = 'default'
      switch (state) {
        case 'Running':
        case 'Ready':
        case 'Up':
        case 'BackedUp':
        case 'Allocated':
        case 'Implemented':
        case 'Enabled':
        case 'enabled':
        case 'Active':
        case 'Completed':
        case 'Started':
          status = 'success'
          break
        case 'Disabled':
        case 'Down':
        case 'Error':
        case 'Stopped':
          status = 'error'
          break
        case 'Migrating':
        case 'Starting':
        case 'Stopping':
        case 'Scheduled':
          status = 'processing'
          break
        case 'Alert':
        case 'Created':
          status = 'warning'
          break
      }
      return status
    }
  }
}
</script>

<style scoped>
/deep/ .ant-badge-status-dot {
  width: 12px;
  height: 12px;
  margin-top: 5px;
}
</style>

<template>

    <template slot="title">
      {{ text }}
    </template>

      {{ text }}

</template>


fix: Center notifications number in the dropdown

<template>

{{userModel.username}}

{{userModel.email}}

        <a :href="`${ baseUrl }/profile`" class="text is-semibold is-caption is-txtGrey">{{ $t('UserDropdown.settings') }}

        <a :href="`${ baseUrl }/organization`" class="text is-semibold is-caption is-txtGrey">{{ $t('UserDropdown.organizationSettings') }}

        <a :href="`${ baseUrl }/your_apps`" class="text is-semibold is-caption is-txtGrey">{{ $t('UserDropdown.apiKeys') }}

        <a :href="`${ baseUrl }/me`" class="text is-semibold is-caption is-txtGrey" target="_blank">{{ $t('UserDropdown.publicProfile') }}

        <router-link :to="{ name: 'notifications' }" class="text is-semibold is-caption is-txtGrey">{{ $t('UserDropdown.notifications') }}</router-link>
         0" class="notification-number text is-semibold is-small is-txtGrey">{{notificationsCount}}

    <a :href="`${ baseUrl }/logout`" class="navbar-dropdown-bottom">

{{ $t('UserDropdown.logout') }}

</template>


Fixes Pause and Resume in home-song-item. (#388)

Fixes  Issue https://github.com/phanan/koel/issues/387

<template>

      {{ song.title }}

        {{ song.artist.name }} -
        {{ song.playCount | pluralize('play') }}

</template>