Add icons :art:

Add icons :art:

<template>

	<ui-card>
		<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>

			<fa :icon="faPaperPlane"/> Deliver
			<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
				<ui-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly>
					Process
					<template #prefix><fa :icon="fasPlayCircle"/></template>
					<template #suffix>jobs/s</template>
				</ui-input>
				<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
					Active
					<template #prefix><fa :icon="farPlayCircle"/></template>
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly>
					Waiting
					<template #prefix><fa :icon="faStopCircle"/></template>
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly>
					Delayed
					<template #prefix><fa :icon="faStopwatch"/></template>
					<template #suffix>jobs</template>
				</ui-input>
			</ui-horizon-group>

			<fa :icon="faInbox"/> Inbox
			<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
				<ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
					Process
					<template #prefix><fa :icon="fasPlayCircle"/></template>
					<template #suffix>jobs/s</template>
				</ui-input>
				<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
					Active
					<template #prefix><fa :icon="farPlayCircle"/></template>
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly>
					Waiting
					<template #prefix><fa :icon="faStopCircle"/></template>
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly>
					Delayed
					<template #prefix><fa :icon="faStopwatch"/></template>
					<template #suffix>jobs</template>
				</ui-input>
			</ui-horizon-group>

			<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>

	</ui-card>

</template>

<template>

	<ui-card>
		<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>

			<fa :icon="faPaperPlane"/> Deliver
			<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
				<ui-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly>
					Process
					<template #suffix>jobs/s</template>
				</ui-input>
				<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
					Active
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly>
					Waiting
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly>
					Delayed
					<template #suffix>jobs</template>
				</ui-input>
			</ui-horizon-group>

			<fa :icon="faInbox"/> Inbox
			<ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
				<ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
					Process
					<template #suffix>jobs/s</template>
				</ui-input>
				<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
					Active
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly>
					Waiting
					<template #suffix>jobs</template>
				</ui-input>
				<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly>
					Delayed
					<template #suffix>jobs</template>
				</ui-input>
			</ui-horizon-group>

			<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>

	</ui-card>

</template>


Fix #543 dropdown toggle programmatic

Fix #543 dropdown toggle programmatic

<template>

            <slot name="trigger"/>

        <transition name="fade">

        </transition>
        <transition name="fade">

                    <slot/>

        </transition>

</template>


[qa] Fix syntax error: unexpected token in src/components/lists/EpisodeList.vue with .[

<template>

            {{ $t('shots.fields.episode') }}

            {{ $t('shots.fields.description') }}

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

              <router-link
                class="flexrow-item"
                :to="taskTypePath(columnId)"
                v-if="!isCurrentUserClient"
              >
                {{ taskTypeMap.get(columnId).name }}
              </router-link>

                {{ taskTypeMap.get(columnId).name }}

            {{ $t('episodes.all_episodes') }}

          <stats-cell
            :colors="chartColors('all', 'all')"
            :data="chartData('all', 'all')"
            :frames-data="chartData('all', 'all', 'frames')"
            :countMode="countMode"
            :displayMode="displayMode"
          />

          <stats-cell
            :style="getValidationStyle(columnId)"
            :key="'all-' + columnId"
            :colors="chartColors('all', columnId)"
            :data="chartData('all', columnId)"
            :frames-data="chartData('all', columnId, 'frames')"
            :countMode="countMode"
            :displayMode="displayMode"
            v-for="columnId in validationColumns"
          />

        <template
          v-for="entry in entries"
        >

              <chevron-right-icon v-if="isRetakes && expanded[entry.id] !== true" />
              <chevron-down-icon v-if="isRetakes && expanded[entry.id] === true" />

              {{ entry.name }}

            <description-cell
              class="description"
              :editable="isCurrentUserManager"
              :entry="entry"
              @description-changed="value => onDescriptionChanged(entry, value)"
            />

            <stats-cell
              :colors="chartColors(entry.id, 'all')"
              :data="chartData(entry.id, 'all')"
              :frames-data="chartData(entry.id, 'all', 'frames')"
              :countMode="countMode"
              :displayMode="displayMode"
              v-if="isStats(entry.id, 'all')"
            />

            <stats-cell
              :key="entry.id + columnId"
              :style="getValidationStyle(columnId)"
              :colors="chartColors(entry.id, columnId)"
              :data="chartData(entry.id, columnId)"
              :frames-data="chartData(entry.id, columnId, 'frames')"
              :countMode="countMode"
              :displayMode="displayMode"
              :label="chartLabel(entry.id, columnId)"
              :label-color="chartLabelColor(entry.id, columnId)"
              v-for="columnId in validationColumns"
              v-if="isStats(entry.id, columnId)"
            />

            <row-actions-cell
              :entry="entry"
              @delete-clicked="$emit('delete-clicked', entry)"
              @edit-clicked="$emit('edit-clicked', entry)"
              v-if="isCurrentUserManager"
            />

          <template v-if="expanded[entry.id]">

                - Take {{ takeNumber }}

              <template
                v-for="columnId in validationColumns"
              >
                <stats-cell
                  :key="takeNumber + entry.id + columnId"
                  :style="getValidationStyle(columnId)"
                  :colors="chartColors(entry.id, columnId)"
                  :data="chartTakeData(entry.id, columnId, takeNumber)"
                  :frames-data="chartTakeData(entry.id, columnId, takeNumber, 'frames')"
                  :countMode="countMode"
                  :displayMode="displayMode"
                  v-if="chartRetakeMaxCount(entry.id, columnId) + 1 > takeNumber"
                />

                <stats-cell
                  :key="takeNumber + entry.id + columnId"
                  :style="getValidationStyle(columnId)"
                  :colors="chartColors(entry.id, columnId)"
                  :data="chartData(entry.id, columnId)"
                  :frames-data="chartData(entry.id, columnId, 'frames')"
                  :countMode="countMode"
                  :displayMode="displayMode"
                  v-else-if="
                    isStats(entry.id, columnId) &&
                    chartRetakeMaxCount(entry.id, columnId) + 1 === takeNumber"
                />

              </template>

          </template>
        </template>

{{ $t('episodes.empty_list') }}

{{ $t('episodes.empty_list_client') }}

    {{ displayedEpisodesLength }}
    {{ $tc('episodes.number', displayedEpisodesLength) }}

</template>


ScreenCard: + time on check

ScreenCard: + time on check


Assign ILG when created in QuizEditDetailsPage

<template>

  <CoreBase
    :immersivePage="true"
    immersivePageIcon="close"
    :immersivePagePrimary="false"
    :authorized="$store.getters.userIsAuthorizedForCoach"
    authorizedRole="adminOrCoach"
    :appBarTitle="$tr('appBarTitle')"
    :pageTitle="$tr('pageTitle', { title: quiz.title })"
    :showSubNav="false"
    :immersivePageRoute="previousPageRoute"
  >

    <KPageContainer v-if="!loading && !error">
      <AssignmentDetailsForm
        v-bind="formProps"
        :disabled="disabled"
        :initialIndividualLearners="initialIndividualLearners"
        @cancel="goBackToSummaryPage"
        @submit="handleSaveChanges"
      />
    </KPageContainer>

  </CoreBase>

</template>

<script>

  import { mapGetters, mapState } from 'vuex';
  import { ExamResource } from 'kolibri.resources';
  import { CoachCoreBase } from '../common';
  import { coachStringsMixin } from '../common/commonCoachStrings';
  import AssignmentDetailsModal from './assignments/AssignmentDetailsModal';

  const INDIVIDUAL_LEARNERS_GROUP_KIND = 'individuallearnersgroup';

  export default {
    name: 'QuizEditDetailsPage',
    components: {
      AssignmentDetailsForm: AssignmentDetailsModal,
      CoreBase: CoachCoreBase,
    },
    mixins: [coachStringsMixin],
    props: {},
    data() {
      return {
        quiz: {
          title: '',
          assignments: [],
          active: false,
        },
        loading: true,
        error: false,
        disabled: false,
      };
    },
    computed: {
      ...mapGetters('classSummary', ['groups']),
      ...mapGetters('individualLearners', ['hasIndividualLearnersAssigned']),
      formProps() {
        return {
          assignmentType: 'quiz',
          classId: this.$route.params.classId,
          groups: this.groups,
          initialActive: this.quiz.active,
          initialSelectedCollectionIds: this.initialSelectedCollectionIds,
          initialTitle: this.quiz.title,
          submitErrorMessage: this.$tr('submitErrorMessage'),
        };
      },
      previousPageRoute() {
        let route;
        if (this.$route.name === 'QuizEditDetailsPage') {
          route = 'QuizSummaryPage';
        } else {
          // If coming from Quiz Report
          route = 'ReportsQuizLearnerListPage';
        }
        return this.$router.getRoute(route);
      },
      initialSelectedCollectionIds() {
        let collectionIds = [];
        // Only include the IndividualLearnersGroup in this if it has already
        // had learners assigned to it.
        this.quiz.assignments.forEach(assignment => {
          if (assignment.collection_kind === INDIVIDUAL_LEARNERS_GROUP_KIND) {
            if (this.hasIndividualLearnersAssigned) {
              collectionIds.push(assignment.collection);
            }
          } else {
            collectionIds.push(assignment.collection);
          }
        });
        return collectionIds;
      },
      initialIndividualLearners() {
        return this.$store.state.individualLearners.user_ids;
      },
    },
    beforeRouteEnter(to, from, next) {
      return ExamResource.fetchModel({
        id: to.params.quizId,
      })
        .then(quiz => {
          next(vm => {
            const collection = quiz.assignments.find(
              a => a.collection_kind === INDIVIDUAL_LEARNERS_GROUP_KIND
            );
            if (collection) {
              vm.$store
                .dispatch(
                  'individualLearners/initializeIndividualLearnersGroup',
                  collection.collection
                )
                .then(() => vm.setData(quiz));
            } else {
              // There is no "inidividual learners group" assigned to this quiz, so
              // we will make one. This will also set the newly created individual learners
              // group to the individualLearners vuex state.
              vm.$store
                .dispatch('individualLearners/createIndividualLearnersGroup', {
                  classId: vm.$route.params.classId,
                })
                .then(() => {
                  // Save the Exam with the new assignment
                  ExamResource.saveModel({
                    id: vm.$route.params.quizId,
                    data: {
                      assignments: [
                        { collection: vm.$route.params.classId },
                        { collection: vm.$store.state.individualLearners.id },
                      ],
                    },
                  }).then(() => vm.setData(quiz));
                });
            }
          });
        })
        .catch(error => {
          next(vm => vm.setError(error));
        });
    },
    methods: {
      // @public
      setData(data) {
        this.quiz = data;
        this.loading = false;
        this.$store.dispatch('notLoading');
      },
      // @public
      setError(error) {
        this.$store.dispatch('handleApiError', error);
        this.loading = false;
        this.$store.dispatch('notLoading');
      },
      goBackToSummaryPage() {
        this.$router.push(this.previousPageRoute);
      },
      handleSaveChanges(changes) {
        let promise;

        this.disabled = true;
        if (changes === null) {
          promise = Promise.resolve();
        } else {
          promise = ExamResource.saveModel({
            id: this.$route.params.quizId,
            data: changes,
          });
        }
        return promise
          .then(() => {
            this.goBackToSummaryPage();
          })
          .catch(() => {
            this.disabled = false;
            this.$store.dispatch('createSnackbar', this.$tr('submitErrorMessage'));
          });
      },
    },
    $trs: {
      pageTitle: `Edit quiz details for '{title}'`,
      appBarTitle: `Edit quiz details`,
      submitErrorMessage: 'There was a problem saving your changes',
    },
  };

</script>

<style lang="scss" scoped></style>

<template>

  <CoreBase
    :immersivePage="true"
    immersivePageIcon="close"
    :immersivePagePrimary="false"
    :authorized="$store.getters.userIsAuthorizedForCoach"
    authorizedRole="adminOrCoach"
    :appBarTitle="$tr('appBarTitle')"
    :pageTitle="$tr('pageTitle', { title: quiz.title })"
    :showSubNav="false"
    :immersivePageRoute="previousPageRoute"
  >

    <KPageContainer v-if="!loading && !error">
      <AssignmentDetailsForm
        v-bind="formProps"
        :disabled="disabled"
        :initialIndividualLearners="initialIndividualLearners"
        @cancel="goBackToSummaryPage"
        @submit="handleSaveChanges"
      />
    </KPageContainer>

  </CoreBase>

</template>

<script>

  import { mapGetters } from 'vuex';
  import { ExamResource } from 'kolibri.resources';
  import { CoachCoreBase } from '../common';
  import { coachStringsMixin } from '../common/commonCoachStrings';
  import AssignmentDetailsModal from './assignments/AssignmentDetailsModal';

  const INDIVIDUAL_LEARNERS_GROUP_KIND = 'individuallearnersgroup';

  export default {
    name: 'QuizEditDetailsPage',
    components: {
      AssignmentDetailsForm: AssignmentDetailsModal,
      CoreBase: CoachCoreBase,
    },
    mixins: [coachStringsMixin],
    props: {},
    data() {
      return {
        quiz: {
          title: '',
          assignments: [],
          active: false,
        },
        loading: true,
        error: false,
        disabled: false,
      };
    },
    computed: {
      ...mapGetters('classSummary', ['groups']),
      ...mapGetters('individualLearners', ['hasIndividualLearnersAssigned']),
      formProps() {
        return {
          assignmentType: 'quiz',
          classId: this.$route.params.classId,
          groups: this.groups,
          initialActive: this.quiz.active,
          initialSelectedCollectionIds: this.initialSelectedCollectionIds,
          initialTitle: this.quiz.title,
          submitErrorMessage: this.$tr('submitErrorMessage'),
        };
      },
      previousPageRoute() {
        let route;
        if (this.$route.name === 'QuizEditDetailsPage') {
          route = 'QuizSummaryPage';
        } else {
          // If coming from Quiz Report
          route = 'ReportsQuizLearnerListPage';
        }
        return this.$router.getRoute(route);
      },
      initialSelectedCollectionIds() {
        let collectionIds = [];
        // Only include the IndividualLearnersGroup in this if it has already
        // had learners assigned to it.
        this.quiz.assignments.forEach(assignment => {
          if (assignment.collection_kind === INDIVIDUAL_LEARNERS_GROUP_KIND) {
            if (this.hasIndividualLearnersAssigned) {
              collectionIds.push(assignment.collection);
            }
          } else {
            collectionIds.push(assignment.collection);
          }
        });
        return collectionIds;
      },
      initialIndividualLearners() {
        return this.$store.state.individualLearners.user_ids;
      },
    },
    beforeRouteEnter(to, from, next) {
      return ExamResource.fetchModel({
        id: to.params.quizId,
      })
        .then(quiz => {
          next(vm => {
            const collection = quiz.assignments.find(
              a => a.collection_kind === INDIVIDUAL_LEARNERS_GROUP_KIND
            );
            if (collection) {
              vm.$store
                .dispatch(
                  'individualLearners/initializeIndividualLearnersGroup',
                  collection.collection
                )
                .then(() => vm.setData(quiz));
            } else {
              // There is no "inidividual learners group" assigned to this quiz, so
              // we will make one. This will also set the newly created individual learners
              // group to the individualLearners vuex state.
              vm.$store
                .dispatch('individualLearners/createIndividualLearnersGroup', {
                  classId: vm.$route.params.classId,
                })
                .then(() => vm.setData(quiz));
            }
          });
        })
        .catch(error => {
          next(vm => vm.setError(error));
        });
    },
    methods: {
      // @public
      setData(data) {
        this.quiz = data;
        this.loading = false;
        this.$store.dispatch('notLoading');
      },
      // @public
      setError(error) {
        this.$store.dispatch('handleApiError', error);
        this.loading = false;
        this.$store.dispatch('notLoading');
      },
      goBackToSummaryPage() {
        this.$router.push(this.previousPageRoute);
      },
      handleSaveChanges(changes) {
        let promise;

        this.disabled = true;
        if (changes === null) {
          promise = Promise.resolve();
        } else {
          promise = ExamResource.saveModel({
            id: this.$route.params.quizId,
            data: changes,
          });
        }
        return promise
          .then(() => {
            this.goBackToSummaryPage();
          })
          .catch(() => {
            this.disabled = false;
            this.$store.dispatch('createSnackbar', this.$tr('submitErrorMessage'));
          });
      },
    },
    $trs: {
      pageTitle: `Edit quiz details for '{title}'`,
      appBarTitle: `Edit quiz details`,
      submitErrorMessage: 'There was a problem saving your changes',
    },
  };

</script>

<style lang="scss" scoped></style>


Add dark stripe to the resource card for a topic

<template>

  <div
    v-if="contentNode"
    class="resource-card-outer"
  >
    <div
      v-if="!contentNode.is_leaf"
      class="topic-bar"
      :style="{ backgroundColor: $themeTokens.text }"
    ></div>
    <BaseCard
      v-bind="{ to, title, collectionTitle }"
      class="resource-card"
    >
      <template #topLeft>
        <ContentNodeThumbnail
          :contentNode="contentNode"
          rounded
        />
      </template>
      <template #topRight>
        <LearningActivityLabel :contentNode="contentNode" />
        <KButton
          v-if="contentNode.copies"
          appearance="basic-link"
          class="copies"
          :text="coreString('copies', { num: contentNode.copies.length })"
          @click.prevent="$emit('openCopiesModal', contentNode.copies)"
        />
      </template>

      <template #progress>
        <ProgressBar :contentNode="contentNode" />
      </template>
    </BaseCard>
  </div>

</template>

<script>

  import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
  import ContentNodeThumbnail from '../../thumbnails/ContentNodeThumbnail';
  import ProgressBar from '../../ProgressBar';
  import BaseCard from '../BaseCard';
  import LearningActivityLabel from './LearningActivityLabel';

  export default {
    name: 'ResourceCard',
    components: {
      BaseCard,
      ContentNodeThumbnail,
      LearningActivityLabel,
      ProgressBar,
    },
    mixins: [commonCoreStrings],
    props: {
      contentNode: {
        type: Object,
        required: true,
      },
      /**
       * vue-router link object
       */
      to: {
        type: Object,
        required: true,
      },
      collectionTitle: {
        type: String,
        required: false,
        default: '',
      },
    },
    data() {
      return {
        title: this.contentNode ? this.contentNode.title : '',
      };
    },
  };

</script>

<style lang="scss" scoped>

  .copies {
    float: right;
  }

  .resource-card-outer {
    position: relative;
  }

  .topic-bar {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 10px;
    border-radius: 8px 8px 0 0;
  }

  .resource-card {
    padding-top: 26px;
  }

</style>

<template>

  <BaseCard
    v-if="contentNode"
    v-bind="{ to, title, collectionTitle }"
  >
    <template #topLeft>
      <ContentNodeThumbnail
        :contentNode="contentNode"
        rounded
      />
    </template>
    <template #topRight>
      <LearningActivityLabel :contentNode="contentNode" />
      <KButton
        v-if="contentNode.copies"
        appearance="basic-link"
        class="copies"
        :text="coreString('copies', { num: contentNode.copies.length })"
        @click.prevent="$emit('openCopiesModal', contentNode.copies)"
      />
    </template>

    <template #progress>
      <ProgressBar :contentNode="contentNode" />
    </template>
  </BaseCard>

</template>

<script>

  import commonCoreStrings from 'kolibri.coreVue.mixins.commonCoreStrings';
  import ContentNodeThumbnail from '../../thumbnails/ContentNodeThumbnail';
  import ProgressBar from '../../ProgressBar';
  import BaseCard from '../BaseCard';
  import LearningActivityLabel from './LearningActivityLabel';

  export default {
    name: 'ResourceCard',
    components: {
      BaseCard,
      ContentNodeThumbnail,
      LearningActivityLabel,
      ProgressBar,
    },
    mixins: [commonCoreStrings],
    props: {
      contentNode: {
        type: Object,
        required: true,
      },
      /**
       * vue-router link object
       */
      to: {
        type: Object,
        required: true,
      },
      collectionTitle: {
        type: String,
        required: false,
        default: '',
      },
    },
    data() {
      return {
        title: this.contentNode ? this.contentNode.title : '',
      };
    },
  };

</script>

<style lang="scss" scoped>

  .copies {
    float: right;
  }

</style>


Add responsive class to TaskManager pool view.

<template>

       Pool Status

            Pool ID:

              {{ pool.pool_id }}

            Pool Name:
            {{ pool.pool_name }}

            Platform:
            {{ pool.platform }}

            Size:
            {{ pool.size }}

            CPU:
            {{ pool.cpu }}

            Cycle Time:

              {{ formatDateRelative(new Date(pool.cycle_time * 1000), 0, "") }}

            Max. Run Time:

              {{
                formatDateRelative(new Date(pool.max_run_time * 1000), 0, "")
              }}

Displaying {{ currentEntries }}/{{ totalEntries }} entries.

           1"
            class="bi bi-caret-left-fill"
          >

            Page {{ currentPage }} of {{ totalPages }}.

          <a
            v-on:click="nextPage"
            v-show="currentPage < totalPages"
            data-toggle="tooltip"
            data-placement="top"
            title=""
            class="bi bi-caret-right-fill dimgray"
            data-original-title="Next"
          >

              Task

              Run

              State

              Created

              Started

              Resolved

              Expires

            Status Data

              {{ task.task_id }}

            {{ task.run_id }}

              {{ task.state }}

              {{ task.created | formatDate }}
{{
                task.created | formatDateAgo
              }}

              {{ task.started | formatDate }}
{{
                formatDateRelative(task.created, task.started, "later")
              }}

            N/A

              {{ task.resolved | formatDate }}
{{
                formatDateRelative(
                  task.started !== null ? task.started : task.created,
                  task.resolved,
                  "later"
                )
              }}

            N/A

              {{ task.expires | formatDate }}
{{
                new Date(task.expires) > new Date()
                  ? formatDateRelative(new Date(), task.expires, "from now")
                  : formatDateRelative(task.expires)
              }}

{{ task.status_data }}

            N/A

</template>


fix: groups edit fetch

fix: groups edit fetch

<template lang='pug'>
  v-container(fluid, grid-list-lg)
    v-layout(row wrap)
      v-flex(xs12)
        .admin-header
          v-icon(size='80', color='grey lighten-2') people
          .admin-header-title
            .headline.blue--text.text--darken-2 Edit Group
            .subheading.grey--text {{name}}
          v-spacer
          v-btn(color='indigo', large, outline, to='/groups')
            v-icon arrow_back
          v-dialog(v-model='deleteGroupDialog', max-width='500')
            v-btn(color='red', large, outline, slot='activator')
              v-icon(color='red') delete
            v-card
              .dialog-header.is-red Delete Group?
              v-card-text Are you sure you want to delete group #[strong {{ name }}]? All users will be unassigned from this group.
              v-card-actions
                v-spacer
                v-btn(flat, @click='deleteGroupDialog = false') Cancel
                v-btn(color='red', dark, @click='deleteGroup') Delete
          v-btn(color='primary', large, depressed, @click='updateGroup')
            v-icon(left) check
            span Update Group
        v-card.mt-3
          v-tabs(v-model='tab', :color='$vuetify.dark ? "primary" : "grey lighten-4"', fixed-tabs, :slider-color='$vuetify.dark ? "white" : "primary"', show-arrows)
            v-tab(key='properties') Properties
            v-tab(key='permissions') Permissions
            v-tab(key='rules') Page Rules
            v-tab(key='users') Users

            v-tab-item(key='properties', :transition='false', :reverse-transition='false')
              v-card
                v-card-text
                  v-text-field(
                    outline
                    background-color='grey lighten-3'
                    v-model='name'
                    label='Group Name'
                    counter='255'
                    prepend-icon='people'
                    )
                  v-divider
                  .caption.mt-3.grey--text ID: {{group.id}}

            v-tab-item(key='permissions', :transition='false', :reverse-transition='false')
              v-card

            v-tab-item(key='rules', :transition='false', :reverse-transition='false')
              v-card
                v-card-title.pb-0
                  v-subheader
                    v-icon.mr-2 border_color
                    .subheading Read and Write
                  v-spacer
                  v-btn(flat, outline)
                    v-icon(left) arrow_drop_down
                    | Load Preset
                  v-btn(flat, outline)
                    v-icon(left) vertical_align_bottom
                    | Import Rules
                .pa-3.pl-4
                  criterias
                v-divider.my-0
                v-card-title.pb-0
                  v-subheader
                    v-icon.mr-2 pageview
                    .subheading Read Only
                  v-spacer
                  v-btn(flat, outline)
                    v-icon(left) arrow_drop_down
                    | Load Preset
                  v-btn(flat, outline)
                    v-icon(left) vertical_align_bottom
                    | Import Rules
                .pa-3.pl-4
                  criterias
                v-divider.my-0
                v-card-title.pb-0
                  v-subheader Legend
                .px-4.pb-4
                  .body-1.px-1.py-2 Any number of rules can be used at the same time. However, some rules requires more processing time than others. Rule types are color-coded as followed:
                  .caption
                    v-icon(color='blue') stop
                    span Fast rules. None or insignificant latency introduced to all page loads.
                  .caption
                    v-icon(color='orange') stop
                    span Medium rules. Some latency added to all page loads.
                  .caption
                    v-icon(color='red') stop
                    span Slow rules. May adds noticeable latency to all page loads. Avoid using in multiple rules.

            v-tab-item(key='users', :transition='false', :reverse-transition='false')
              v-card
                v-card-title.pb-0
                  v-btn(color='primary', @click='searchUserDialog = true')
                    v-icon(left) assignment_ind
                    | Assign User
                v-data-table(
                  :items='group.users',
                  :headers='headers',
                  :search='search',
                  :pagination.sync='pagination',
                  :rows-per-page-items='[15]'
                  hide-actions
                )
                  template(slot='items', slot-scope='props')
                    tr(:active='props.selected')
                      td.text-xs-right {{ props.item.id }}
                      td {{ props.item.name }}
                      td {{ props.item.email }}
                      td
                        v-menu(bottom, right, min-width='200')
                          v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz
                          v-list
                            v-list-tile(@click='unassignUser(props.item.id)')
                              v-list-tile-action: v-icon(color='orange') highlight_off
                              v-list-tile-content
                                v-list-tile-title Unassign
                  template(slot='no-data')
                    v-alert.ma-3(icon='warning', :value='true', outline) No users to display.
                .text-xs-center.py-2(v-if='users.length > 15')
                  v-pagination(v-model='pagination.page', :length='pages')

    user-search(v-model='searchUserDialog', @select='assignUser')
</template>

<script>
import _ from 'lodash'

import Criterias from '../common/criterias.vue'
import UserSearch from '../common/user-search.vue'

import groupQuery from 'gql/admin/groups/groups-query-single.gql'
import assignUserMutation from 'gql/admin/groups/groups-mutation-assign.gql'
import deleteGroupMutation from 'gql/admin/groups/groups-mutation-delete.gql'
import unassignUserMutation from 'gql/admin/groups/groups-mutation-unassign.gql'
import updateGroupMutation from 'gql/admin/groups/groups-mutation-update.gql'

export default {
  components: {
    Criterias,
    UserSearch
  },
  data() {
    return {
      group: {
        id: 0,
        name: '',
        users: []
      },
      name: '',
      deleteGroupDialog: false,
      searchUserDialog: false,
      pagination: {},
      users: [],
      headers: [
        { text: 'ID', value: 'id', width: 50, align: 'right' },
        { text: 'Name', value: 'name' },
        { text: 'Email', value: 'email' },
        { text: '', value: 'actions', sortable: false, width: 50 }
      ],
      search: '',
      tab: '1'
    }
  },
  computed: {
    pages () {
      if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null) {
        return 0
      }

      return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
    }
  },
  watch: {
    group(newValue, oldValue) {
      this.name = newValue.name
    }
  },
  methods: {
    async updateGroup() {
      try {
        await this.$apollo.mutate({
          mutation: updateGroupMutation,
          variables: {
            id: this.group.id,
            name: this.name
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-update')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `Group changes have been saved.`,
          icon: 'check'
        })
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    },
    async deleteGroup() {
      this.deleteGroupDialog = false
      try {
        await this.$apollo.mutate({
          mutation: deleteGroupMutation,
          variables: {
            id: this.group.id
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-delete')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `Group ${this.group.name} has been deleted.`,
          icon: 'delete'
        })
        this.$router.replace('/groups')
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    },
    async assignUser(id) {
      try {
        await this.$apollo.mutate({
          mutation: assignUserMutation,
          variables: {
            groupId: this.group.id,
            userId: id
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-assign')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `User has been assigned to ${this.group.name}.`,
          icon: 'assignment_ind'
        })
        this.$apollo.queries.group.refetch()
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    },
    async unassignUser(id) {
      try {
        await this.$apollo.mutate({
          mutation: unassignUserMutation,
          variables: {
            groupId: this.group.id,
            userId: id
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-unassign')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `User has been unassigned from ${this.group.name}.`,
          icon: 'assignment_ind'
        })
        this.$apollo.queries.group.refetch()
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    }
  },
  apollo: {
    group: {
      query: groupQuery,
      variables() {
        return {
          id: _.toSafeInteger(this.$route.params.id)
        }
      },
      fetchPolicy: 'network-only',
      update: (data) => data.groups.single,
      watchLoading (isLoading) {
        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
      }
    }
  }
}
</script>

<style lang='scss'>

</style>

<template lang='pug'>
  v-container(fluid, grid-list-lg)
    v-layout(row wrap)
      v-flex(xs12)
        .admin-header
          v-icon(size='80', color='grey lighten-2') people
          .admin-header-title
            .headline.blue--text.text--darken-2 Edit Group
            .subheading.grey--text {{name}}
          v-spacer
          v-btn(color='indigo', large, outline, to='/groups')
            v-icon arrow_back
          v-dialog(v-model='deleteGroupDialog', max-width='500')
            v-btn(color='red', large, outline, slot='activator')
              v-icon(color='red') delete
            v-card
              .dialog-header.is-red Delete Group?
              v-card-text Are you sure you want to delete group #[strong {{ name }}]? All users will be unassigned from this group.
              v-card-actions
                v-spacer
                v-btn(flat, @click='deleteGroupDialog = false') Cancel
                v-btn(color='red', dark, @click='deleteGroup') Delete
          v-btn(color='primary', large, depressed, @click='updateGroup')
            v-icon(left) check
            span Update Group
        v-card.mt-3
          v-tabs(v-model='tab', :color='$vuetify.dark ? "primary" : "grey lighten-4"', fixed-tabs, :slider-color='$vuetify.dark ? "white" : "primary"', show-arrows)
            v-tab(key='properties') Properties
            v-tab(key='permissions') Permissions
            v-tab(key='rules') Page Rules
            v-tab(key='users') Users

            v-tab-item(key='properties', :transition='false', :reverse-transition='false')
              v-card
                v-card-text
                  v-text-field(
                    outline
                    background-color='grey lighten-3'
                    v-model='name'
                    label='Group Name'
                    counter='255'
                    prepend-icon='people'
                    )
                  v-divider
                  .caption.mt-3.grey--text ID: {{group.id}}

            v-tab-item(key='permissions', :transition='false', :reverse-transition='false')
              v-card

            v-tab-item(key='rules', :transition='false', :reverse-transition='false')
              v-card
                v-card-title.pb-0
                  v-subheader
                    v-icon.mr-2 border_color
                    .subheading Read and Write
                  v-spacer
                  v-btn(flat, outline)
                    v-icon(left) arrow_drop_down
                    | Load Preset
                  v-btn(flat, outline)
                    v-icon(left) vertical_align_bottom
                    | Import Rules
                .pa-3.pl-4
                  criterias
                v-divider.my-0
                v-card-title.pb-0
                  v-subheader
                    v-icon.mr-2 pageview
                    .subheading Read Only
                  v-spacer
                  v-btn(flat, outline)
                    v-icon(left) arrow_drop_down
                    | Load Preset
                  v-btn(flat, outline)
                    v-icon(left) vertical_align_bottom
                    | Import Rules
                .pa-3.pl-4
                  criterias
                v-divider.my-0
                v-card-title.pb-0
                  v-subheader Legend
                .px-4.pb-4
                  .body-1.px-1.py-2 Any number of rules can be used at the same time. However, some rules requires more processing time than others. Rule types are color-coded as followed:
                  .caption
                    v-icon(color='blue') stop
                    span Fast rules. None or insignificant latency introduced to all page loads.
                  .caption
                    v-icon(color='orange') stop
                    span Medium rules. Some latency added to all page loads.
                  .caption
                    v-icon(color='red') stop
                    span Slow rules. May adds noticeable latency to all page loads. Avoid using in multiple rules.

            v-tab-item(key='users', :transition='false', :reverse-transition='false')
              v-card
                v-card-title.pb-0
                  v-btn(color='primary', @click='searchUserDialog = true')
                    v-icon(left) assignment_ind
                    | Assign User
                v-data-table(
                  :items='group.users',
                  :headers='headers',
                  :search='search',
                  :pagination.sync='pagination',
                  :rows-per-page-items='[15]'
                  hide-actions
                )
                  template(slot='items', slot-scope='props')
                    tr(:active='props.selected')
                      td.text-xs-right {{ props.item.id }}
                      td {{ props.item.name }}
                      td {{ props.item.email }}
                      td
                        v-menu(bottom, right, min-width='200')
                          v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz
                          v-list
                            v-list-tile(@click='unassignUser(props.item.id)')
                              v-list-tile-action: v-icon(color='orange') highlight_off
                              v-list-tile-content
                                v-list-tile-title Unassign
                  template(slot='no-data')
                    v-alert.ma-3(icon='warning', :value='true', outline) No users to display.
                .text-xs-center.py-2(v-if='users.length > 15')
                  v-pagination(v-model='pagination.page', :length='pages')

    user-search(v-model='searchUserDialog', @select='assignUser')
</template>

<script>
import Criterias from '../common/criterias.vue'
import UserSearch from '../common/user-search.vue'

import groupQuery from 'gql/admin/groups/groups-query-single.gql'
import assignUserMutation from 'gql/admin/groups/groups-mutation-assign.gql'
import deleteGroupMutation from 'gql/admin/groups/groups-mutation-delete.gql'
import unassignUserMutation from 'gql/admin/groups/groups-mutation-unassign.gql'
import updateGroupMutation from 'gql/admin/groups/groups-mutation-update.gql'

export default {
  components: {
    Criterias,
    UserSearch
  },
  data() {
    return {
      group: {
        id: 0,
        name: '',
        users: []
      },
      name: '',
      deleteGroupDialog: false,
      searchUserDialog: false,
      pagination: {},
      users: [],
      headers: [
        { text: 'ID', value: 'id', width: 50, align: 'right' },
        { text: 'Name', value: 'name' },
        { text: 'Email', value: 'email' },
        { text: '', value: 'actions', sortable: false, width: 50 }
      ],
      search: '',
      tab: '1'
    }
  },
  computed: {
    pages () {
      if (this.pagination.rowsPerPage == null || this.pagination.totalItems == null) {
        return 0
      }

      return Math.ceil(this.pagination.totalItems / this.pagination.rowsPerPage)
    }
  },
  watch: {
    group(newValue, oldValue) {
      this.name = newValue.name
    }
  },
  methods: {
    async updateGroup() {
      try {
        await this.$apollo.mutate({
          mutation: updateGroupMutation,
          variables: {
            id: this.group.id,
            name: this.name
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-update')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `Group changes have been saved.`,
          icon: 'check'
        })
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    },
    async deleteGroup() {
      this.deleteGroupDialog = false
      try {
        await this.$apollo.mutate({
          mutation: deleteGroupMutation,
          variables: {
            id: this.group.id
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-delete')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `Group ${this.group.name} has been deleted.`,
          icon: 'delete'
        })
        this.$router.replace('/groups')
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    },
    async assignUser(id) {
      try {
        await this.$apollo.mutate({
          mutation: assignUserMutation,
          variables: {
            groupId: this.group.id,
            userId: id
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-assign')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `User has been assigned to ${this.group.name}.`,
          icon: 'assignment_ind'
        })
        this.$apollo.queries.group.refetch()
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    },
    async unassignUser(id) {
      try {
        await this.$apollo.mutate({
          mutation: unassignUserMutation,
          variables: {
            groupId: this.group.id,
            userId: id
          },
          watchLoading (isLoading) {
            this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-unassign')
          }
        })
        this.$store.commit('showNotification', {
          style: 'success',
          message: `User has been unassigned from ${this.group.name}.`,
          icon: 'assignment_ind'
        })
        this.$apollo.queries.group.refetch()
      } catch (err) {
        this.$store.commit('showNotification', {
          style: 'red',
          message: err.message,
          icon: 'warning'
        })
      }
    }
  },
  apollo: {
    group: {
      query: groupQuery,
      variables() {
        return {
          id: this.$route.params.id
        }
      },
      fetchPolicy: 'network-only',
      update: (data) => data.groups.single,
      watchLoading (isLoading) {
        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-groups-refresh')
      }
    }
  }
}
</script>

<style lang='scss'>

</style>


Quick CSS fix

Quick CSS fix

<template>

  <button class="action clone-casebook-nodes" data-disable-with="Clone-Node" v-on:click="displayModal()">Clone Nodes</button>
  <Modal v-if="showModal"
            @close="showModal = false">
    <template slot="title">Clone Section To Casebook</template>
    <template slot="body">

Select a casebook to clone {{sectionSource}} to

          <form :action="cb.form_target" method="POST">
            <input type="hidden" name="csrfmiddlewaretoken" :value="csrftoken" />
            <button class="link" type="submit">{{cb.title}}</button>
          </form>

    </template>
  </Modal>

</template>


Fixed sorting on firefox

Fixed sorting on firefox

<template>

        Categorie
        Totaal

        {{stat.name}}
        {{stat.total.toFixed(2)}}

</template>


6927: updated button for allowed groups

6927: updated button for allowed groups

Signed-off-by: Nikola <ef0921d7af2a67b1e5ab56530351d29e92ff52e1@nextcloud.com>

<template>

{{ t('spreed', 'Limit to groups') }}

			{{ t('spreed', 'When at least one group is selected, only people of the listed groups can be part of conversations.') }}

			{{ t('spreed', 'Guests can still join public conversations.') }}

			{{ t('spreed', 'Users that cannot use Talk anymore will still be listed as participants in their previous conversations and also their chat messages will be kept.') }}

			<Multiselect v-model="allowedGroups"
				class="allowed-groups-select"
				:options="groups"
				:placeholder="t('spreed', 'Limit using Talk')"
				:disabled="loading"
				:multiple="true"
				:searchable="true"
				:tag-width="60"
				:loading="loadingGroups"
				:show-no-options="false"
				:close-on-select="false"
				track-by="id"
				label="displayname"
				@search-change="searchGroup" />

			<Button type="primary"
				:disabled="loading"
				@click="saveAllowedGroups">
				{{ saveLabelAllowedGroups }}
			</Button>

{{ t('spreed', 'Limit creating a public and group conversation') }}

			<Multiselect v-model="canStartConversations"
				class="allowed-groups-select"
				:options="groups"
				:placeholder="t('spreed', 'Limit creating conversations')"
				:disabled="loading"
				:multiple="true"
				:searchable="true"
				:tag-width="60"
				:loading="loadingGroups"
				:show-no-options="false"
				:close-on-select="false"
				track-by="id"
				label="displayname"
				@search-change="searchGroup" />

			<Button type="primary"
				:disabled="loading"
				@click="saveStartConversationsGroups">
				{{ saveLabelStartConversations }}
			</Button>

{{ t('spreed', 'Limit starting a call') }}

			<Multiselect id="start_calls"
				v-model="startCalls"
				:options="startCallOptions"
				:placeholder="t('spreed', 'Limit starting calls')"
				label="label"
				track-by="value"
				:disabled="loading || loadingStartCalls"
				@input="saveStartCalls" />

			{{ t('spreed', 'When a call has started, everyone with access to the conversation can join the call.') }}

</template>

<template>

{{ t('spreed', 'Limit to groups') }}

			{{ t('spreed', 'When at least one group is selected, only people of the listed groups can be part of conversations.') }}

			{{ t('spreed', 'Guests can still join public conversations.') }}

			{{ t('spreed', 'Users that cannot use Talk anymore will still be listed as participants in their previous conversations and also their chat messages will be kept.') }}

			<Multiselect v-model="allowedGroups"
				class="allowed-groups-select"
				:options="groups"
				:placeholder="t('spreed', 'Limit using Talk')"
				:disabled="loading"
				:multiple="true"
				:searchable="true"
				:tag-width="60"
				:loading="loadingGroups"
				:show-no-options="false"
				:close-on-select="false"
				track-by="id"
				label="displayname"
				@search-change="searchGroup" />

			<button class="button primary"
				:disabled="loading"
				@click="saveAllowedGroups">
				{{ saveLabelAllowedGroups }}
			</button>

{{ t('spreed', 'Limit creating a public and group conversation') }}

			<Multiselect v-model="canStartConversations"
				class="allowed-groups-select"
				:options="groups"
				:placeholder="t('spreed', 'Limit creating conversations')"
				:disabled="loading"
				:multiple="true"
				:searchable="true"
				:tag-width="60"
				:loading="loadingGroups"
				:show-no-options="false"
				:close-on-select="false"
				track-by="id"
				label="displayname"
				@search-change="searchGroup" />

			<button class="button primary"
				:disabled="loading"
				@click="saveStartConversationsGroups">
				{{ saveLabelStartConversations }}
			</button>

{{ t('spreed', 'Limit starting a call') }}

			<Multiselect id="start_calls"
				v-model="startCalls"
				:options="startCallOptions"
				:placeholder="t('spreed', 'Limit starting calls')"
				label="label"
				track-by="value"
				:disabled="loading || loadingStartCalls"
				@input="saveStartCalls" />

			{{ t('spreed', 'When a call has started, everyone with access to the conversation can join the call.') }}

</template>


:lipstick: (plant) Removed awkward shadow on selected module

<template>
  <plant-alert
    class="module-manager"
    :show="show"
    @close-alert="cancel">

      Manage modules

      <v-touch
        tag="li"
        v-for="(module, index) in updatedModules"
        :class="{ [`type-${module.type}`]: true, active: module.selected }"
        :key="`module-${index}`"
        @tap="onToggleModule(module)">

          <input type="radio"
            :id="`module-${module.type}`"
            :name="`module-${module.type}`"
            :value="module.type"
            :checked="module.selected">

            <feather-check />

        <label class="module-description" :for="`module-${module.type}`">

            <component :is="`feather-${module.meta.icon}`" />
            {{ module.meta.title }}

          {{ module.meta.description }}

      </v-touch>

    <button slot="cancel"
      class="plain"
      @click="cancel">
      Cancel
    </button>

    <button slot="confirm" @click="confirmModuleUpdates">
      Update modules
    </button>
  </plant-alert>
</template>


fix(SPA): 一键注册用户时移除用户名中的空格

fix(SPA): 一键注册用户时移除用户名中的空格

<template>
  <Transition
    enter-active-class="animated bounceInRight"
    leave-active-class="animated bounceOutLeft"
  >

      <CommonHeader>
        {{ $t('auth.one_key') }}
        <RouterLink slot="right" to="/signup">{{ $t('auth.register.name') }}</RouterLink>
      </CommonHeader>

          {{ $t('auth.label.phone') }}

            <input
              id="account"
              v-model="account"
              maxlength="11"
              type="tel"
              :placeholder="$t('auth.placeholder.phone', [11])"
            >

          <svg
            v-show="account.length > 0"
            class="m-style-svg m-svg-def"
            @click="account = ''"
          >
            <use xlink:href="#icon-clean" />
          </svg>

          {{ $t('auth.label.code') }}

            <input
              id="password"
              v-model="code"
              maxlength="6"
              type="number"
              :placeholder="$t('auth.placeholder.code', [4, 6])"
              @keyup.enter="signinByAccount"
            >

          {{ codeText }}

          {{ err | plusMessageFirst }}

          <button
            :disabled="disabled"
            class="m-long-btn m-signin-btn"
            @click="isRegister ? signupByCode() : signinByCode()"
          >
            <CircleLoading v-if="loading" />
            {{ $t('auth.login') }}
          </button>

          {{ $t('auth.use_account') }}

  </Transition>
</template>


devop: 🔧 reset values

devop: 🔧 reset values

<template>

    <mew-stepper class="mx-md-0" :items="steppers" :on-step="step">

      <template v-if="step === 1" #stepperContent1>

STEP 1.

Write down these words

            <v-icon small color="greenPrimary" class="mr-1">mdi-sync</v-icon>

Update

          <v-select
            v-model="phraseSize"
            style="max-width: 150px"
            hide-details
            dense
            item-text="name"
            item-value="value"
            :items="mnemonicOptions"
            label=""
            outlined
          ></v-select>

        <phrase-block class="mb-8 CreateWalletMnemonicTable">
          <mnemonic-phrase-table :data="phrase" />
        </phrase-block>

          <mew-expand-panel
            :has-dividers="true"
            :is-toggle="true"
            :interactive-content="true"
            :panel-items="[{ name: 'Add Extra Word' }]"
          >
            <template #panelBody1>

                <mew-input
                  v-model="extraWord"
                  label="Extra word"
                  placeholder="Extra word"
                />

            </template>
          </mew-expand-panel>

          <mew-button
            title="I wrote them down"
            btn-size="xlarge"
            :has-full-width="false"
            @click.native="updateStep(2)"
          />

        <mew-warning-sheet
          class="mt-5 mb-0"
          title="NOT RECOMMENDED"
          description="This information is sensitive, and these options should only be used in offline settings by experienced crypto users. Your phrase is the key to your wallet. Please make sure to write it down and save it in a secure location. We CAN NOT retrieve or reset your phrase if you lose it."
        />
      </template>

      <template v-if="step === 2" #stepperContent2>

STEP 2.

Verification

          {{ stepTwoText }}

        <v-sheet max-width="600px" class="MnemonicRadioOptions mx-auto">
          <v-radio-group
            v-for="(item, idx) in generatedVerification"
            :key="`${idx}verification`"
            v-model="validateMnemonicValues[getOnlyKey(item)]"
            hide-details
            mandatory
            row
            class="radio-group pa-5"
          >
            <template #label>

                {{ getOnlyKey(item) + 1 }}.

            </template>
            <v-row>
              <v-col
                v-for="(entries, id) in getEntries(item)"
                :key="entries + id"
                class="Options"
                cols="12"
                sm="4"
              >
                <v-radio :label="entries" :value="`${entries}_${id}`"></v-radio>
              </v-col>
            </v-row>
          </v-radio-group>
          <mew-input
            v-if="extraWord && extraWord !== ''"
            v-model="extraWordVerification"
            label="Confirm extra word"
            placeholder="Please confirm your extra word"
            class="mt-10 mb-3"
          />
        </v-sheet>

          <mew-button
            title="Back"
            btn-size="xlarge"
            btn-style="outline"
            class="mx-md-1 my-1"
            @click.native="updateStep(1)"
          />
          <mew-button
            title="Verify"
            btn-size="xlarge"
            :disabled="!canVerify"
            class="CreateMnemonicVerify mx-md-1 my-1"
            @click.native="verify"
          />

        <mew-warning-sheet
          class="mt-5 mb-0"
          title="NOT RECOMMENDED"
          description="This information is sensitive, and these options should only be used in offline settings by experienced crypto users. Your phrase is the key to your wallet. Please make sure to write it down and save it in a secure location. We CAN NOT retrieve or reset your phrase if you lose it."
        />
      </template>

      <template v-if="step === 3" #stepperContent3>

STEP 3.

Well done!

              You are now ready to take advantage of all that Ethereum has to
              offer! Access with mnemonic phrase should only be used in an
              offline setting

            <v-img
              class="d-block d-sm-none mx-auto mt-12 mb-12"
              max-width="170px"
              src="@/assets/images/icons/icon-keystore-mew.png"
            />

              <mew-button
                title="Access Wallet"
                btn-size="xlarge"
                :has-full-width="false"
                class="CreateMnemonicAccessWallet mb-5"
                @click.native="goToAccess"
              />
              <mew-button
                title="Create Another Wallet"
                :has-full-width="false"
                btn-style="transparent"
                @click.native="createAnotherWallet()"
              />

          <v-img
            class="d-none d-sm-block ml-8"
            max-width="250px"
            src="@/assets/images/icons/icon-keystore-mew.png"
          />

      </template>
    </mew-stepper>

</template>


fix: make it possible to input small numbers (e.g. 1e-7) into the input field (#652)

<template>
  <InputField
    :label="label"
    :helper-text="helperText || error"
    :is-dirty="$v.model.$dirty"
    :is-disabled="isDisabled"
    :is-focused="isFocused"
    :is-invalid="isInvalid"
    :warning-text="warningText"
    class="InputCurrency"
  >
    <div
      slot-scope="{ inputClass }"
      :class="inputClass"
      class="flex flex-row"
    >
      <input
        ref="input"
        v-model.trim="model"
        :disabled="isDisabled"
        :name="name"
        autocomplete="off"
        class="InputCurrency__input flex flex-grow bg-transparent text-theme-page-text"
        type="text"
        @blur="onBlur"
        @focus="onFocus"
      >
      <div
        v-if="isMarketEnabled && alternativeCurrency"
        :title="alternativeCurrency"
        class="InputCurrency__alternative-amount flex flex-no-shrink text-grey-dark ml-4"
      >
        {{ alternativeAmount }}
      </div>
    </div>
  </InputField>

</template>

<script>
import { includes, isNumber, isString } from 'lodash'
import { required } from 'vuelidate/lib/validators'
import { MARKET } from '@config'
import store from '@/store'
import InputField from './InputField'

/**
 * Checks if a currency is valid, either as a symbol (€) or code (EUR)
 * @param {String} currency
 * @return {Boolean}
 */
const currencyValidator = currency => {
  const currentNetwork = store.getters['session/network']
  const currencies = [
    currentNetwork.token,
    currentNetwork.subunit,
    currentNetwork.symbol,
    ...Object.keys(MARKET.currencies),
    ...Object.values(MARKET.currencies).map(currency => currency.symbol)
  ]
  return includes(currencies, currency)
}

/**
 * This component uses a String value internally to avoid several problems, such
 * as showing the exponential notation, although it emits a Number always.
 * It also support a `raw` event, that can be used by other components to receive
 * the internal String value.
 */
export default {
  name: 'InputCurrency',

  components: {
    InputField
  },

  model: {
    prop: 'value',
    event: 'input'
  },

  props: {
    alternativeCurrency: {
      type: String,
      required: false,
      default: null,
      validator: currencyValidator
    },
    currency: {
      type: String,
      required: true,
      validator: currencyValidator
    },
    helperText: {
      type: String,
      required: false,
      default: null
    },
    isDisabled: {
      type: Boolean,
      required: false,
      default: false
    },
    label: {
      type: String,
      required: false,
      default () {
        return this.$t('INPUT_CURRENCY.LABEL')
      }
    },
    name: {
      type: String,
      required: false,
      default: 'amount'
    },
    maximumAmount: {
      type: Number,
      required: false,
      default: Math.pow(10, 7)
    },
    maximumError: {
      type: String,
      required: false,
      default: null
    },
    minimumAmount: {
      type: Number,
      required: false,
      default: Math.pow(10, -8)
    },
    minimumError: {
      type: String,
      required: false,
      default: null
    },
    warningText: {
      type: String,
      required: false,
      default: null
    },
    notValidError: {
      type: String,
      required: false,
      default: null
    },
    required: {
      type: Boolean,
      required: false,
      default: false
    },
    value: {
      type: [Number, String],
      required: true
    }
  },

  data: vm => ({
    inputValue: vm.value,
    isFocused: false
  }),

  computed: {
    alternativeAmount () {
      const amount = this.checkAmount(this.inputValue) ? parseFloat(this.inputValue) : 0

      return this.currency_format(amount * this.price, { currency: this.alternativeCurrency })
    },

    error () {
      let error = null

      if (this.$v.model.$dirty) {
        if (this.required && !this.$v.model.isRequired) {
          error = this.$t('INPUT_CURRENCY.ERROR.REQUIRED')
        } else if (!this.$v.model.isNumber) {
          if (this.notValidError) {
            error = this.notValidError
          } else {
            error = this.$t('INPUT_CURRENCY.ERROR.NOT_VALID')
          }
        } else if (!this.$v.model.isLessThanMaximum) {
          if (this.maximumError) {
            error = this.maximumError
          } else {
            const amount = this.currency_format(this.minimumAmount, { currency: this.currency })
            error = this.$t('INPUT_CURRENCY.ERROR.NOT_ENOUGH_AMOUNT', { amount })
          }
        } else if (!this.$v.model.isMoreThanMinimum) {
          if (this.minimumError) {
            error = this.minimumError || error
          } else {
            const amount = this.currency_format(this.maximumAmount, { currency: this.currency })
            error = this.$t('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM', { amount })
          }
        }
      }

      return error
    },

    formattedValue () {
      return this.checkAmount(this.inputValue)
        ? this.currency_format(parseFloat(this.inputValue), { currency: this.currency })
        : this.inputValue
    },

    isInvalid () {
      return this.$v.model.$dirty && !!this.error
    },

    isMarketEnabled () {
      return this.session_network.market.enabled
    },

    model: {
      get () {
        return this.isFocused ? this.inputValue : this.formattedValue
      },
      set (value) {
        if (this.updateInputValue(value)) {
          this.emitInput(value)
        }
      }
    },

    price () {
      return this.$store.getters['market/lastPrice']
    }
  },

  watch: {
    value: {
      handler (val) {
        this.updateInputValue(val)
      },
      immediate: true
    }
  },

  methods: {
    /**
     * Checks that an amount, either a Number or String, is valid
     * @param {(Number|String)} amount
     * @return {Boolean}
     */
    checkAmount (amount) {
      return !!(isNumber(amount) || (isString(amount) && amount.match(/^[0-9]+([,.][0-9]+)?$/)))
    },
    /**
     * Emits the raw input value, as String, and the Number value
     */
    emitInput (value) {
      this.$emit('raw', value)
      this.$emit('input', parseFloat(value) || 0)
    },
    focus () {
      this.$refs.input.focus()
    },
    onBlur () {
      this.isFocused = false
      this.$emit('blur')
    },
    onFocus () {
      this.isFocused = true
      this.$v.model.$touch()
      this.$emit('focus')
    },
    /*
     * Establishes the "internal" value (`inputValue`) of the component
     * @param {(String|Number)} value
     */
    updateInputValue (value) {
      // Ignore empty and not valid values
      if (value && this.checkAmount(value)) {
        let number = Number(value.toString().replace(',', '.'))
        number.toString().includes('-')
          ? this.inputValue = number.toFixed(number.toString().split('-')[1]) // Small numbers will be like 1e-7 so we use the number after '-' for toFixed()
          : this.inputValue = value.toString().replace(',', '.')
        // Inform Vuelidate that the value changed
        this.$v.model.$touch()
        return true
      }
      return false
    }
  },

  validations: {
    model: {
      isNumber (value) {
        return this.inputValue && this.checkAmount(this.inputValue)
      },
      isMoreThanMinimum (value) {
        return parseFloat(this.inputValue) >= this.minimumAmount
      },
      isLessThanMaximum (value) {
        return parseFloat(this.inputValue) <= this.maximumAmount
      },
      isRequired (value) {
        if (this.required) {
          return required(value)
        }
        return true
      }
    }
  }
}
</script>

<style lang="postcss" scoped>
.InputCurrency__input::placeholder {
  @apply .text-transparent
}

.InputCurrency__alternative-amount {
  min-width: 5rem;
  justify-content: flex-end;
}
.InputField--invalid .InputCurrency__alternative-amount {
  @apply .text-red-dark
}
</style>

<template>
  <InputField
    :label="label"
    :helper-text="helperText || error"
    :is-dirty="$v.model.$dirty"
    :is-disabled="isDisabled"
    :is-focused="isFocused"
    :is-invalid="isInvalid"
    :warning-text="warningText"
    class="InputCurrency"
  >
    <div
      slot-scope="{ inputClass }"
      :class="inputClass"
      class="flex flex-row"
    >
      <input
        ref="input"
        v-model.trim="model"
        :disabled="isDisabled"
        :name="name"
        autocomplete="off"
        class="InputCurrency__input flex flex-grow bg-transparent text-theme-page-text"
        type="text"
        @blur="onBlur"
        @focus="onFocus"
      >
      <div
        v-if="isMarketEnabled && alternativeCurrency"
        :title="alternativeCurrency"
        class="InputCurrency__alternative-amount flex flex-no-shrink text-grey-dark ml-4"
      >
        {{ alternativeAmount }}
      </div>
    </div>
  </InputField>

</template>

<script>
import { includes, isNumber, isString } from 'lodash'
import { required } from 'vuelidate/lib/validators'
import { MARKET } from '@config'
import store from '@/store'
import InputField from './InputField'

/**
 * Checks if a currency is valid, either as a symbol (€) or code (EUR)
 * @param {String} currency
 * @return {Boolean}
 */
const currencyValidator = currency => {
  const currentNetwork = store.getters['session/network']
  const currencies = [
    currentNetwork.token,
    currentNetwork.subunit,
    currentNetwork.symbol,
    ...Object.keys(MARKET.currencies),
    ...Object.values(MARKET.currencies).map(currency => currency.symbol)
  ]
  return includes(currencies, currency)
}

/**
 * This component uses a String value internally to avoid several problems, such
 * as showing the exponential notation, although it emits a Number always.
 * It also support a `raw` event, that can be used by other components to receive
 * the internal String value.
 */
export default {
  name: 'InputCurrency',

  components: {
    InputField
  },

  model: {
    prop: 'value',
    event: 'input'
  },

  props: {
    alternativeCurrency: {
      type: String,
      required: false,
      default: null,
      validator: currencyValidator
    },
    currency: {
      type: String,
      required: true,
      validator: currencyValidator
    },
    helperText: {
      type: String,
      required: false,
      default: null
    },
    isDisabled: {
      type: Boolean,
      required: false,
      default: false
    },
    label: {
      type: String,
      required: false,
      default () {
        return this.$t('INPUT_CURRENCY.LABEL')
      }
    },
    name: {
      type: String,
      required: false,
      default: 'amount'
    },
    maximumAmount: {
      type: Number,
      required: false,
      default: Math.pow(10, 7)
    },
    maximumError: {
      type: String,
      required: false,
      default: null
    },
    minimumAmount: {
      type: Number,
      required: false,
      default: Math.pow(10, -8)
    },
    minimumError: {
      type: String,
      required: false,
      default: null
    },
    warningText: {
      type: String,
      required: false,
      default: null
    },
    notValidError: {
      type: String,
      required: false,
      default: null
    },
    required: {
      type: Boolean,
      required: false,
      default: false
    },
    value: {
      type: [Number, String],
      required: true
    }
  },

  data: vm => ({
    inputValue: vm.value,
    isFocused: false
  }),

  computed: {
    alternativeAmount () {
      const amount = this.checkAmount(this.inputValue) ? parseFloat(this.inputValue) : 0

      return this.currency_format(amount * this.price, { currency: this.alternativeCurrency })
    },

    error () {
      let error = null

      if (this.$v.model.$dirty) {
        if (this.required && !this.$v.model.isRequired) {
          error = this.$t('INPUT_CURRENCY.ERROR.REQUIRED')
        } else if (!this.$v.model.isNumber) {
          if (this.notValidError) {
            error = this.notValidError
          } else {
            error = this.$t('INPUT_CURRENCY.ERROR.NOT_VALID')
          }
        } else if (!this.$v.model.isLessThanMaximum) {
          if (this.maximumError) {
            error = this.maximumError
          } else {
            const amount = this.currency_format(this.minimumAmount, { currency: this.currency })
            error = this.$t('INPUT_CURRENCY.ERROR.NOT_ENOUGH_AMOUNT', { amount })
          }
        } else if (!this.$v.model.isMoreThanMinimum) {
          if (this.minimumError) {
            error = this.minimumError || error
          } else {
            const amount = this.currency_format(this.maximumAmount, { currency: this.currency })
            error = this.$t('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM', { amount })
          }
        }
      }

      return error
    },

    formattedValue () {
      return this.checkAmount(this.inputValue)
        ? this.currency_format(parseFloat(this.inputValue), { currency: this.currency })
        : this.inputValue
    },

    isInvalid () {
      return this.$v.model.$dirty && !!this.error
    },

    isMarketEnabled () {
      return this.session_network.market.enabled
    },

    model: {
      get () {
        return this.isFocused ? this.inputValue : this.formattedValue
      },
      set (value) {
        if (this.updateInputValue(value)) {
          this.emitInput(value)
        }
      }
    },

    price () {
      return this.$store.getters['market/lastPrice']
    }
  },

  watch: {
    value: {
      handler (val) {
        this.updateInputValue(val)
      },
      immediate: true
    }
  },

  methods: {
    /**
     * Checks that an amount, either a Number or String, is valid
     * @param {(Number|String)} amount
     * @return {Boolean}
     */
    checkAmount (amount) {
      return !!(isNumber(amount) || (isString(amount) && amount.match(/^[0-9]+([,.][0-9]+)?$/)))
    },
    /**
     * Emits the raw input value, as String, and the Number value
     */
    emitInput (value) {
      this.$emit('raw', value)
      this.$emit('input', parseFloat(value) || 0)
    },
    focus () {
      this.$refs.input.focus()
    },
    onBlur () {
      this.isFocused = false
      this.$emit('blur')
    },
    onFocus () {
      this.isFocused = true
      this.$v.model.$touch()
      this.$emit('focus')
    },
    /*
     * Establishes the "internal" value (`inputValue`) of the component
     * @param {(String|Number)} value
     */
    updateInputValue (value) {
      // Ignore empty and not valid values
      if (value && this.checkAmount(value)) {
        this.inputValue = value.toString().replace(',', '.')
        // Inform Vuelidate that the value changed
        this.$v.model.$touch()
        return true
      }
      return false
    }
  },

  validations: {
    model: {
      isNumber (value) {
        return this.inputValue && this.checkAmount(this.inputValue)
      },
      isMoreThanMinimum (value) {
        return parseFloat(this.inputValue) >= this.minimumAmount
      },
      isLessThanMaximum (value) {
        return parseFloat(this.inputValue) <= this.maximumAmount
      },
      isRequired (value) {
        if (this.required) {
          return required(value)
        }
        return true
      }
    }
  }
}
</script>

<style lang="postcss" scoped>
.InputCurrency__input::placeholder {
  @apply .text-transparent
}

.InputCurrency__alternative-amount {
  min-width: 5rem;
  justify-content: flex-end;
}
.InputField--invalid .InputCurrency__alternative-amount {
  @apply .text-red-dark
}
</style>


Sorting of compared campaigns

Sorting of compared campaigns

<template>

Compared campaigns

                    <model-list-select :list="campaignsNotCompared"
                                       v-model="campaignToAdd"
                                       option-value="id"
                                       option-text="name"
                                       placeholder="Select campaign">
                    </model-list-select>

                    <button @click="addToComparison" class="btn palette-Cyan bg waves-effect">Add to comparison</button>

                <loader :yes="loading"></loader>

                            Campaign
                            Clicks
                            Click-through rate (CTR)
                            Conversions
                            Started payments
                            Finished payments
                            Earned

                                {{ campaign.name }}

                                {{ campaign.stats.click_count.count }}

                                    {{ campaign.stats.ctr | round(2) }}%

                                    {{ campaign.stats.conversions | round(4) }}%

                                {{ campaign.stats.payment_count.count }}

                                {{ campaign.stats.purchase_count.count }}

                                {{ campaign.stats.purchase_sum.sum | round(2) }}€

                                    <button class="btn btn-sm palette-Cyan bg waves-effect"
                                            @click="remove(campaign.deleteUrl)"
                                            title="Remove campaign">

                                    </button>

No campaigns selected

</template>

<template>

Compared campaigns

                    <model-list-select :list="campaignsNotCompared"
                                       v-model="campaignToAdd"
                                       option-value="id"
                                       option-text="name"
                                       placeholder="Select campaign">
                    </model-list-select>

                    <button @click="addToComparison" class="btn palette-Cyan bg waves-effect">Add to comparison</button>

                <loader :yes="loading"></loader>

                            Campaign
                            Clicks
                            Click-through rate (CTR)
                            Conversions
                            Started payments
                            Finished payments
                            Earned

                                {{ campaign.name }}

                                {{ campaign.stats.click_count.count }}

                                    {{ campaign.stats.ctr | round(2) }}%

                                    {{ campaign.stats.conversions | round(4) }}%

                                {{ campaign.stats.payment_count.count }}

                                {{ campaign.stats.purchase_count.count }}

                                {{ campaign.stats.purchase_sum.sum | round(2) }}€

                                    <button class="btn btn-sm palette-Cyan bg waves-effect"
                                            @click="remove(campaign.deleteUrl)"
                                            title="Remove campaign">

                                    </button>

</template>


Add support for ?q query param when loading page

<template>

Tickets

    <ticket-list :tickets="tickets" showColumnPicker="true"></ticket-list>

</template>


frontend: sidebar-user: fix highlight

frontend: sidebar-user: fix highlight

<template>

<partial name="loading-box"></partial>

@{{user.screen_name}}

        <template v-if="user.registered">
          <partial name="loading-box" v-if="!stats"></partial>
          <template v-else>

Received{{stats.reactions_count}}

Average{{average}}

Joined{{stats.since_join}}d ago

          </template>
        </template>

@{{user.screen_name}} は aclog に登録していません

        Best
        Timeline
        Likes
        Stats

      <sidebar-filtering></sidebar-filtering>

</template>