feat(docs): allow users to play with Flex Grid Playground in Fullscreen (#11776)

<template>

      <q-btn
        color="secondary"
        @click="toggleFullscreen"
        :icon="$q.fullscreen.isActive ? 'fullscreen_exit' : 'fullscreen'"
        :label="$q.fullscreen.isActive ? 'Exit Fullscreen' : 'Try playground in Fullscreen'"
      />

Parent Properties (container)

        <q-select color="blue-12" v-model="group.containerGroup" :options="containerOptions" label="Container" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.directionGroup" :options="directionOptions" label="Direction" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.wrapGroup" :options="wrapOptions" label="Wrap" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.justifyGroup" :options="justifyOptions" label="Justify Content" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.itemsGroup" :options="itemsOptions" label="Align Items" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.contentGroup" :options="contentOptions" label="Align Content" emit-value map-options dense options-dense />

Container Classes

    <q-input filled v-model="classes" dense readonly class="q-py-sm">
      <template #append>
        <copy-button :text="classes" />
      </template>
    </q-input>

      Results (children: {{ group.children.length }}/10)

    <q-btn class="float-right" round dense flat :icon="mdiShareVariant" @click="share">
      <q-tooltip>{{ copied ? 'Copied to clipboard' : 'Share URL' }}</q-tooltip>
    </q-btn>
    <q-btn class="float-right" round dense flat :icon="fabCodepen" @click="editInCodepen">
      <q-tooltip>Edit in Codepen</q-tooltip>
    </q-btn>

    <q-btn class="float-right" label="Add Child" :icon="mdiPlus" dense flat :disabled="group.children.length >= 10" @click="addChild" />

        <child v-for="(child, index) in group.children" :key="index"
          :child="child"
          :ref="el => { childRef[index] = el }"
          :index="index"
          :selected-index="selectedIndex"
          @delete="onDelete"
          @change="onChange"
        />

Child Classes

    <q-input filled v-model="group.childClasses" dense readonly class="q-py-sm">
      <template #append>
        <copy-button :text="group.childClasses" />
      </template>
    </q-input>

Child Styles

    <q-input filled v-model="group.childStyles" dense readonly class="q-py-sm">
      <template #append>
        <copy-button :text="group.childStyles" />
      </template>
    </q-input>

    <doc-codepen ref="codepenRef" title="Flex example" slugifiedTitle="flex-example" />

</template>

<template>

Parent Properties (container)

        <q-select color="blue-12" v-model="group.containerGroup" :options="containerOptions" label="Container" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.directionGroup" :options="directionOptions" label="Direction" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.wrapGroup" :options="wrapOptions" label="Wrap" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.justifyGroup" :options="justifyOptions" label="Justify Content" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.itemsGroup" :options="itemsOptions" label="Align Items" emit-value map-options dense options-dense />

        <q-select color="blue-12" v-model="group.contentGroup" :options="contentOptions" label="Align Content" emit-value map-options dense options-dense />

Container Classes

    <q-input filled v-model="classes" dense readonly class="q-py-sm">
      <template #append>
        <copy-button :text="classes" />
      </template>
    </q-input>

      Results (children: {{ group.children.length }}/10)

    <q-btn class="float-right" round dense flat :icon="mdiShareVariant" @click="share">
      <q-tooltip>{{ copied ? 'Copied to clipboard' : 'Share URL' }}</q-tooltip>
    </q-btn>
    <q-btn class="float-right" round dense flat :icon="fabCodepen" @click="editInCodepen">
      <q-tooltip>Edit in Codepen</q-tooltip>
    </q-btn>

    <q-btn class="float-right" label="Add Child" :icon="mdiPlus" dense flat :disabled="group.children.length >= 10" @click="addChild" />

        <child v-for="(child, index) in group.children" :key="index"
          :child="child"
          :ref="el => { childRef[index] = el }"
          :index="index"
          :selected-index="selectedIndex"
          @delete="onDelete"
          @change="onChange"
        />

Child Classes

    <q-input filled v-model="group.childClasses" dense readonly class="q-py-sm">
      <template #append>
        <copy-button :text="group.childClasses" />
      </template>
    </q-input>

Child Styles

    <q-input filled v-model="group.childStyles" dense readonly class="q-py-sm">
      <template #append>
        <copy-button :text="group.childStyles" />
      </template>
    </q-input>

    <doc-codepen ref="codepenRef" title="Flex example" slugifiedTitle="flex-example" />

</template>


chore: update ButtonGroup demo

chore: update ButtonGroup demo

<template>

<veui-button-group>

    <veui-checkbox
      v-model="disabled"
      ui="small"
    >
      禁用全部
    </veui-checkbox>

    <veui-checkbox
      v-model="items[2].disabled"
      ui="small"
    >
      禁用「删除」
    </veui-checkbox>

    <veui-radio-group
      v-model="size"
      ui="small"
      :items="sizes"
    />

    <veui-button-group
      :ui="`primary ${size}`"
      :items="items"
      :disabled="disabled"
    />

    <veui-button-group
      :ui="size"
      :items="items"
      :disabled="disabled"
    />

    <veui-button-group
      :ui="`alt ${size}`"
      :items="items"
      :disabled="disabled"
    />

</template>

<template>

<veui-button-group>

    <veui-checkbox v-model="disabled">
      禁用
    </veui-checkbox>

    <veui-button-group
      ui="primary large"
      :items="items1"
      :disabled="disabled"
    />

    <veui-button-group
      :items="items2"
      :disabled="disabled"
    />

    <veui-button-group
      ui="alt"
      :items="items3"
      :disabled="disabled"
    />

    <veui-button-group
      ui="alt tiny"
      :items="items4"
      :disabled="disabled"
    />

    <veui-button-group
      ui="alt micro"
      :items="items4"
      :disabled="disabled"
    />

</template>


在 overlay 真正显示的时候，才进行相关操作 (#312)

在 overlay 真正显示的时候，才进行相关操作 (#312)


Use consistent icons

Use consistent icons

<template>
    <v-list>
        <v-list-item
            v-for="inventory in inventories"
            :class="{ 'is-deleted': Boolean(inventory._deleted) }"
            :key="inventory.id"
            @click="onInventoryClicked(inventory)">

{{ getValues(inventory) }}

                {{ getQuantity(inventory) }}

        </v-list-item>
    </v-list>
</template>


put $tr string on single line

put $tr string on single line

<template>

{{ $tr('channelList') }}

          {{ $tr('channels') }}
          {{ $tr('lastActive') }}

            <mat-svg category="action" name="view_module" />
            <router-link :to="reportLink(channel.id)">{{ channel.title }}</router-link>

            {{ lastActiveText(channel) }}

</template>


replace border with shadow

replace border with shadow

<template v-if="ready">

    <ui-alert v-if="itemError" :dismissible="false" type="error">
      {{ $tr('itemError') }}
    </ui-alert>

      <content-renderer
        ref="contentRenderer"
        class="content-renderer"
        :id="content.id"
        :kind="content.kind"
        :files="content.files"
        :contentId="content.content_id"
        :channelId="channelId"
        :available="content.available"
        :extraFields="content.extra_fields"
        :assessment="true"
        :itemId="itemId"
        :initSession="initSession"
        @answerGiven="answerGiven"
        @hintTaken="hintTaken"
        @sessionInitialized="sessionInitialized"
        @itemError="handleItemError"
        @startTracking="startTracking"
        @stopTracking="stopTracking"
        @updateProgress="updateProgress"/>

      <transition mode="out-in">
        <k-button
          :text="$tr('check')"
          :primary="checkButtonIsPrimary"
          :raised="true"
          v-if="!complete"
          @click="checkAnswer"
          class="question-btn"
          :class="{shaking: shake}"
          :disabled="checkingAnswer"
        />
        <k-button
          :text="$tr('next')"
          :primary="true"
          :raised="true"
          v-else
          @click="nextQuestion"
          class="question-btn"
        />
      </transition>
      <slot/>

        {{ $tr('goal', {count: totalCorrectRequiredM}) }}

      <exercise-attempts
        class="attemptprogress"
        :waitingForAttempt="firstAttemptAtQuestion"
        :numSpaces="attemptsWindowN"
        :log="recentAttempts"
      />

          {{ $tr('tryAgain') }}

          {{ $tr('correct') }}

</template>


update loading state

update loading state

<template>

  <KPageContainer>

{{ $tr('pageHeader') }}

    <Init
      v-if="status === CSVImportStatuses.NOT_STARTED"
      @cancel="done"
      @next="preview"
    />
    <Preview
      v-else-if="showPreviewState"
      @cancel="done"
    />
    <template v-else-if="showLoaderState">

        <KCircularLoader class="loader" /> Loading...

    </template>

  </KPageContainer>

</template>

<template>

  <KPageContainer>

    <h1>{{ $tr('pageHeader') }}</h1>

    <Init
      v-if="status === CSVImportStatuses.NOT_STARTED"
      @cancel="done"
      @next="preview"
    />
    <Preview
      v-else-if="showPreviewState"
      @cancel="done"
    />
    <template v-else-if="showLoaderState">
      <KCircularLoader style="margin: 32px" />
    </template>

  </KPageContainer>

</template>

<script>

  import { mapState, mapActions } from 'vuex';
  import { CSVImportStatuses } from '../../constants';
  import Init from './Init';
  import Preview from './Preview';

  export default {
    name: 'ImportCsvPage',
    metaInfo() {
      return {
        title: this.$tr('pageHeader'),
      };
    },
    components: {
      Init,
      Preview,
    },
    computed: {
      ...mapState('importCSV', ['status']),
      CSVImportStatuses: () => CSVImportStatuses,
      showPreviewState() {
        return [
          CSVImportStatuses.VALIDATED,
          CSVImportStatuses.ERRORS,
          CSVImportStatuses.FINISHED,
        ].includes(this.status);
      },
      showLoaderState() {
        return [CSVImportStatuses.VALIDATING, CSVImportStatuses.SAVING].includes(this.status);
      },
    },
    watch: {
      pollForTasks(val) {
        return val ? this.startTaskPolling() : this.stopTaskPolling();
      },
    },
    mounted() {
      this.$store.commit('importCSV/RESET_STATE');
      this.refreshTaskList();
      this.startTaskPolling();
    },
    destroyed() {
      this.stopTaskPolling();
    },
    methods: {
      ...mapActions('importCSV', ['startValidating', 'refreshTaskList']),
      preview(file, deleteUsers) {
        this.startValidating({ deleteUsers: deleteUsers, file: file });
      },
      done() {
        this.$router.push(this.$router.getRoute('DATA_PAGE'));
      },
      startTaskPolling() {
        if (!this.intervalId) {
          this.intervalId = setInterval(this.refreshTaskList, 1000);
        }
      },
      stopTaskPolling() {
        if (this.intervalId) {
          this.intervalId = clearInterval(this.intervalId);
        }
      },
    },
    $trs: {
      pageHeader: 'Import users',
    },
  };

</script>

<style lang="scss" scoped>

  .caution {
    font-weight: bold;
    color: red;
  }

</style>


Fix Web IDE editor resizing on sidebar reset size

<script>
  export default {
    props: {
      startSize: {
        type: Number,
        required: true,
      },
      side: {
        type: String,
        required: true,
      },
      minSize: {
        type: Number,
        required: false,
        default: 0,
      },
      maxSize: {
        type: Number,
        required: false,
        default: Number.MAX_VALUE,
      },
      enabled: {
        type: Boolean,
        required: false,
        default: true,
      },
    },
    data() {
      return {
        size: this.startSize,
      };
    },
    computed: {
      className() {
        return `drag-${this.side}`;
      },
      cursorStyle() {
        if (this.enabled) {
          return { cursor: 'ew-resize' };
        }
        return {};
      },
    },
    methods: {
      resetSize(e) {
        e.preventDefault();
        this.$emit('resize-start', this.size);

        this.size = this.startSize;
        this.$emit('update:size', this.size);

        // End resizing on next tick so that listeners can react to DOM changes
        this.$nextTick(() => {
          this.$emit('resize-end', this.size);
        });
      },
      startDrag(e) {
        if (this.enabled) {
          e.preventDefault();
          this.startPos = e.clientX;
          this.currentStartSize = this.size;
          document.addEventListener('mousemove', this.drag);
          document.addEventListener('mouseup', this.endDrag, { once: true });
          this.$emit('resize-start', this.size);
        }
      },
      drag(e) {
        e.preventDefault();
        let moved = e.clientX - this.startPos;
        if (this.side === 'left') moved = -moved;
        let newSize = this.currentStartSize + moved;
        if (newSize < this.minSize) {
          newSize = this.minSize;
        } else if (newSize > this.maxSize) {
          newSize = this.maxSize;
        }
        this.size = newSize;

        this.$emit('update:size', newSize);
      },
      endDrag(e) {
        e.preventDefault();
        document.removeEventListener('mousemove', this.drag);
        this.$emit('resize-end', this.size);
      },
    },
  };
</script>

<template>
  <div
    :class="className"
    :style="cursorStyle"
    class="drag-handle"
    @mousedown="startDrag"
    @dblclick="resetSize"
  ></div>
</template>

<script>
  export default {
    props: {
      startSize: {
        type: Number,
        required: true,
      },
      side: {
        type: String,
        required: true,
      },
      minSize: {
        type: Number,
        required: false,
        default: 0,
      },
      maxSize: {
        type: Number,
        required: false,
        default: Number.MAX_VALUE,
      },
      enabled: {
        type: Boolean,
        required: false,
        default: true,
      },
    },
    data() {
      return {
        size: this.startSize,
      };
    },
    computed: {
      className() {
        return `drag-${this.side}`;
      },
      cursorStyle() {
        if (this.enabled) {
          return { cursor: 'ew-resize' };
        }
        return {};
      },
    },
    methods: {
      resetSize(e) {
        e.preventDefault();
        this.size = this.startSize;
        this.$emit('update:size', this.size);
      },
      startDrag(e) {
        if (this.enabled) {
          e.preventDefault();
          this.startPos = e.clientX;
          this.currentStartSize = this.size;
          document.addEventListener('mousemove', this.drag);
          document.addEventListener('mouseup', this.endDrag, { once: true });
          this.$emit('resize-start', this.size);
        }
      },
      drag(e) {
        e.preventDefault();
        let moved = e.clientX - this.startPos;
        if (this.side === 'left') moved = -moved;
        let newSize = this.currentStartSize + moved;
        if (newSize < this.minSize) {
          newSize = this.minSize;
        } else if (newSize > this.maxSize) {
          newSize = this.maxSize;
        }
        this.size = newSize;

        this.$emit('update:size', newSize);
      },
      endDrag(e) {
        e.preventDefault();
        document.removeEventListener('mousemove', this.drag);
        this.$emit('resize-end', this.size);
      },
    },
  };
</script>

<template>
  <div
    :class="className"
    :style="cursorStyle"
    class="drag-handle"
    @mousedown="startDrag"
    @dblclick="resetSize"
  ></div>
</template>


:art:

:art:

<template>

		<router-link to="/">%fa:check%%i18n:@done%</router-link>

%i18n:@add-widget%

				<select v-model="widgetAdderSelected">
					<option value="profile">%i18n:common.widgets.profile%</option>
					<option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
					<option value="calendar">%i18n:common.widgets.calendar%</option>
					<option value="timemachine">%i18n:common.widgets.timemachine%</option>
					<option value="activity">%i18n:common.widgets.activity%</option>
					<option value="rss">%i18n:common.widgets.rss%</option>
					<option value="trends">%i18n:common.widgets.trends%</option>
					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option>
					<option value="slideshow">%i18n:common.widgets.slideshow%</option>
					<option value="version">%i18n:common.widgets.version%</option>
					<option value="broadcast">%i18n:common.widgets.broadcast%</option>
					<option value="notifications">%i18n:common.widgets.notifications%</option>
					<option value="users">%i18n:common.widgets.users%</option>
					<option value="polls">%i18n:common.widgets.polls%</option>
					<option value="post-form">%i18n:common.widgets.post-form%</option>
					<option value="messaging">%i18n:common.widgets.messaging%</option>
					<option value="memo">%i18n:common.widgets.memo%</option>
					<option value="hashtags">%i18n:common.widgets.hashtags%</option>
					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option>
					<option value="server">%i18n:common.widgets.server%</option>
					<option value="donation">%i18n:common.widgets.donation%</option>
					<option value="nav">%i18n:common.widgets.nav%</option>
					<option value="tips">%i18n:common.widgets.tips%</option>
				</select>
				<button @click="addWidget">%i18n:@add%</button>

				<x-draggable v-model="trash" :options="{ group: 'x' }" @add="onTrash"></x-draggable>

%i18n:common.trash%

		<template v-if="customize">
			<x-draggable v-for="place in ['left', 'right']"
				:list="widgets[place]"
				:class="place"
				:data-place="place"
				:options="{ group: 'x', animation: 150 }"
				@sort="onWidgetSort"
				:key="place"
			>

					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="desktop"/>

			</x-draggable>

				%i18n:common.customization-tips.title%

					<mk-post-form v-if="$store.state.settings.showPostFormOnTopOfTl"/>
					<mk-timeline ref="tl" @loaded="onTlLoaded"/>

		</template>
		<template v-else>

				<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" @chosen="warp" platform="desktop"/>

				<mk-post-form class="form" v-if="$store.state.settings.showPostFormOnTopOfTl"/>
				<mk-timeline class="tl" ref="tl" @loaded="onTlLoaded" v-if="mode == 'timeline'"/>

		</template>

</template>


beautify user component

beautify user component

<template>

                    Name
                    E-Mail-Address
                    Roles
                    Added
                    Updated
                    Options

                        {{ user.name }}

                        {{ user.email }}

                        <multiselect
                            label="display_name"
                            track-by="id"
                            v-model="user.roles"
                            v-validate=""
                            :closeOnSelect="false"
                            :hideSelected="true"
                            :multiple="true"
                            :name="'roles_'+user.id"
                            :options="roles">
                        </multiselect>

                        {{ user.created_at }}

                        {{ user.updated_at }}

                                     Save

                                     Send Reset-Mail

                                     Delete

        <button type="button" class="btn btn-success" v-on:click="showNewUserModal">
             Add New User
        </button>

        <modal name="new-user-modal" height="auto" :scrollable="true">

Add new User

                    <button type="button" class="close" aria-label="Close" v-on:click="hideNewUserModal">
                        ×
                    </button>

                    <form name="newUserForm" class="form-horizontal" role="form" v-on:submit.prevent="onAddUser(newUser)">

                                Username:

                                <input class="form-control" type="text" id="name" v-model="newUser.name" required />

                                E-Mail Address:

                                <input class="form-control" type="email" id="display_name" v-model="newUser.email" required />

                                Password:

                                <input class="form-control" type="password" id="description" v-model="newUser.password" required />

                        <button type="submit" class="btn btn-success">
                             Add
                        </button>
                    </form>

                    <button type="button" class="btn btn-danger"     v-on:click="hideNewUserModal">
                         Cancel
                    </button>

        </modal>

        <modal name="confirm-delete-user-modal" height="auto" :scrollable="true">

Delete User {{ selectedUser.name }}

                    <button type="button" class="close" aria-label="Close" v-on:click="hideDeleteUserModal">
                        ×
                    </button>

                    Do you really want to delete User {{ selectedUser.name }} ({{ selectedUser.email }})?

                    <button type="button" class="btn btn-success" v-on:click="deleteUser(selectedUser.id)">
                         Delete
                    </button>
                    <button type="button" class="btn btn-danger" v-on:click="hideDeleteUserModal">
                         Cancel
                    </button>

        </modal>

</template>

<template>

                    Name
                    E-Mail-Address
                    Roles
                    Added
                    Updated
                    Options

                        {{ user.name }}

                        {{ user.email }}

                        <multiselect v-model="user.roles" :options="roles" label="display_name" :multiple="true" :hideSelected="true" :closeOnSelect="false" track-by="id" :name="'roles_'+user.id" v-validate=""></multiselect>

                        {{ user.created_at }}

                        {{ user.updated_at }}

                                     Save

                                     Send Reset-Mail

                                     Delete

        <button type="button" class="btn btn-success" v-on:click="showNewUserModal">
             Add New User
        </button>

        <modal name="new-user-modal" height="auto" :scrollable="true">

Add new User

                    <button type="button" class="close" aria-label="Close" v-on:click="hideNewUserModal">
                        ×
                    </button>

                    <form name="newUserForm" class="form-horizontal" role="form" v-on:submit.prevent="onAddUser(newUser)">

                                Username:

                                <input class="form-control" type="text" id="name" v-model="newUser.name" required />

                                E-Mail Address:

                                <input class="form-control" type="email" id="display_name" v-model="newUser.email" required />

                                Password:

                                <input class="form-control" type="password" id="description" v-model="newUser.password" required />

                        <button type="submit" class="btn btn-success">
                             Add
                        </button>
                    </form>

                    <button type="button" class="btn btn-danger"     v-on:click="hideNewUserModal">
                         Cancel
                    </button>

        </modal>

        <modal name="confirm-delete-user-modal" height="auto" :scrollable="true">

Delete User {{ selectedUser.name }}

                    <button type="button" class="close" aria-label="Close" v-on:click="hideDeleteUserModal">
                        ×
                    </button>

                    Do you really want to delete User {{ selectedUser.name }} ({{ selectedUser.email }})?

                    <button type="button" class="btn btn-success" v-on:click="deleteUser(selectedUser.id)">
                         Delete
                    </button>
                    <button type="button" class="btn btn-danger" v-on:click="hideDeleteUserModal">
                         Cancel
                    </button>

        </modal>

</template>


Added basic permissions check to see if the current user should see this - ux help would be welcome...

<template>

			<el-card>

						{{ this.site.title }} menu
						<el-tag
							:type="status === 'draft' ? 'primary' : 'success'"
							class="menu-status"
						>
							{{ status }} {{ isUnsaved ? '(unsaved)' : '' }}
						</el-tag>
						<el-tooltip v-if="lastPublishedDate" :content="datePublished" placement="bottom">

								Last published {{ timeElapsedSincePublish }}

						</el-tooltip>

						<el-button type="primary" @click="saveMenu">Save</el-button>
						<el-button type="primary" :plain="true" class="el-button--icon" @click="previewSite">
							Preview
							<icon name="newwindow" aria-hidden="true" width="12" height="12" class="ico" />
						</el-button>
						<el-button type="success" @click="publishMenu">Publish...</el-button>

				<draggable
					v-if="menu.length"
					v-model="menu"
					@change="updateErrors"
					:options="{
						handle: '.menu-editor-menu__item__drag-handle',
						chosenClass: 'menu-editor__menu-item--dragging'
					}"
				>

							<icon name="arrow" />

						<menu-item-field
							:item="item"
							:index="index"
							itemKey="text"
							name="Link text"
							placeholder="Homepage"
							:errors="errors"
							:validate="validateMenuItem"
						/>

						<menu-item-field
							:item="item"
							:index="index"
							itemKey="url"
							name="Location"
							placeholder="https://kent.ac.uk"
							:errors="errors"
							:validate="validateMenuItem"
						/>

							<el-button @click="removeMenuItem(index)" type="default" size="small">
								<icon name="delete" width="14" height="14" />
							</el-button>

				</draggable>

					No menu items

					<el-button @click="addMenuItem" class="u-flex-auto-left">Add link</el-button>

			</el-card>

			<site-page-links :on-click="addMenuItem" :site-pages="sitePages" />

	<el-alert
		title="You cannot edit the menu for this site"
		type="error"
		description="You do not have permission to edit the menu for this site. Please contact the site owner."
		:closable="false"
		show-icon
	>
  </el-alert>

</template>

<template>

			<el-card>

						{{ this.site.title }} menu
						<el-tag
							:type="status === 'draft' ? 'primary' : 'success'"
							class="menu-status"
						>
							{{ status }} {{ isUnsaved ? '(unsaved)' : '' }}
						</el-tag>
						<el-tooltip v-if="lastPublishedDate" :content="datePublished" placement="bottom">

								Last published {{ timeElapsedSincePublish }}

						</el-tooltip>

						<el-button type="primary" @click="saveMenu">Save</el-button>
						<el-button type="primary" :plain="true" class="el-button--icon" @click="previewSite">
							Preview
							<icon name="newwindow" aria-hidden="true" width="12" height="12" class="ico" />
						</el-button>
						<el-button type="success" @click="publishMenu">Publish...</el-button>

				<draggable
					v-if="menu.length"
					v-model="menu"
					@change="updateErrors"
					:options="{
						handle: '.menu-editor-menu__item__drag-handle',
						chosenClass: 'menu-editor__menu-item--dragging'
					}"
				>

							<icon name="arrow" />

						<menu-item-field
							:item="item"
							:index="index"
							itemKey="text"
							name="Link text"
							placeholder="Homepage"
							:errors="errors"
							:validate="validateMenuItem"
						/>

						<menu-item-field
							:item="item"
							:index="index"
							itemKey="url"
							name="Location"
							placeholder="https://kent.ac.uk"
							:errors="errors"
							:validate="validateMenuItem"
						/>

							<el-button @click="removeMenuItem(index)" type="default" size="small">
								<icon name="delete" width="14" height="14" />
							</el-button>

				</draggable>

					No menu items

					<el-button @click="addMenuItem" class="u-flex-auto-left">Add link</el-button>

			</el-card>

			<site-page-links :on-click="addMenuItem" :site-pages="sitePages" />

</template>


Add rest of navbar buttons

Add rest of navbar buttons

<template>

      <button class="navbar-buttons">

      </button>

        <input type="file" id="file-input">

      <button class="navbar-buttons">

      </button>
      <button class="navbar-buttons">

      </button>
      <button class="navbar-buttons">

      </button>

      <button class="navbar-dropdown">
         New
      </button>

        New File
        Duplicate File

      <button class="navbar-dropdown">
        Manage
      </button>

        Test
        Test

      <button class="navbar-buttons">

      </button>
      <button class="navbar-buttons">

      </button>
      <button class="navbar-buttons">

      </button>

      <button class="navbar-buttons">

      </button>

</template>

<template>

      <button class="navbar-buttons">

      </button>

</template>


fix #634

fix #634

<template lang='jade'>
.grd.contain
  .grd-row
    .grd-row-col-3-6.mx1
      h3 Market
      market-picker.contain(v-on:market='updateMarketConfig')
    .grd-row-col-3-6.mx1
      range-creator(v-on:range='updateRange')
</template>

<script>

import marketPicker from '../../global/configbuilder/marketpicker.vue'
import rangeCreator from '../../global/configbuilder/rangecreator.vue'
import _ from 'lodash'

export default {
  data: () => {
    return {
      market: {},
      range: {}
    }
  },
  components: {
    marketPicker,
    rangeCreator
  },
  computed: {
    config: function() {
      let config = {};
      Object.assign(
        config,
        this.market,
        {
          importer: {
            daterange: this.range
          }
        },
        {
          candleWriter: { enabled: true }
        }
      );

      return config;
    }
  },
  methods: {
    updateMarketConfig: function(mc) {
      this.market = mc;
      this.emitConfig();
    },
    updateRange: function(range) {
      this.range = range;
      this.emitConfig();
    },
    emitConfig: function() {
      this.$emit('config', this.config);
    }
  }
}
</script>

<style>
</style>

<template lang='jade'>
.grd.contain
  .grd-row
    .grd-row-col-3-6.mx1
      h3 Market
      market-picker.contain(v-on:market='updateMarketConfig')
    .grd-row-col-3-6.mx1
      range-creator(v-on:range='updateRange')
</template>

<script>

import marketPicker from '../../global/configbuilder/marketpicker.vue'
import rangeCreator from '../../global/configbuilder/rangecreator.vue'
import _ from 'lodash'

export default {
  data: () => {
    return {
      market: {},
      range: {}
    }
  },
  components: {
    marketPicker,
    rangeCreator
  },
  computed: {
    config: function() {
      let config = {};
      Object.assign(
        config,
        this.market,
        {
          importer: {
            daterange: this.range
          }
        }
      );

      return config;
    }
  },
  methods: {
    updateMarketConfig: function(mc) {
      this.market = mc;
      this.emitConfig();
    },
    updateRange: function(range) {
      this.range = range;
      this.emitConfig();
    },
    emitConfig: function() {
      this.$emit('config', this.config);
    }
  }
}
</script>

<style>
</style>


Full working pagination buttons

Full working pagination buttons

<template>

            Ergebnisse pro Seite:
            <select v-model="paginationValue">
                <option
                    v-for="(step, index) in paginationSteps"
                    v-bind:key="index"
                    v-bind:value="step"
                    >
                        {{ step }}
                </option>
            </select>

            Selected: {{ pagination }}

            <template v-if="!isFirstSegment">

1

...

            </template>

                {{ page }}

            <template v-if="!isLastSegment">

...

{{ lastPage }}

            </template>

</template>

<template>

            Ergebnisse pro Seite:
            <select v-model="pagination">
                <option
                    v-for="(step, index) in paginationSteps"
                    v-bind:key="index" v-bind:value="step">
                        {{ step }}
                </option>
            </select>

1

...

{{ currentPage }}

...

{{ lastPage }}

</template>


Move last checkbox to component

Move last checkbox to component

Signed-off-by: Joas Schilling <ab43a7c9cb5b2380afc4ddf8b3e2583169b39a02@schilljs.com>

<template>

		<input ref="signaling_server"
			type="text"
			name="signaling_server"
			placeholder="wss://signaling.example.org"
			:value="server"
			:disabled="loading"
			:aria-label="t('spreed', 'High-performance backend URL')"
			@input="updateServer">

		<NcCheckboxRadioSwitch :checked="verify"
			@update:checked="updateVerify">
			{{ t('spreed', 'Validate SSL certificate') }}
		</NcCheckboxRadioSwitch>

		<NcButton v-show="!loading"
			type="tertiary-no-background"
			:aria-label="t('spreed', 'Delete this server')"
			@click="removeServer">
			<template #icon>
				<Delete :size="20" />
			</template>
		</NcButton>

				<NcLoadingIcon v-if="!checked" :size="20" />
				<AlertCircle v-else-if="errorMessage" :size="20" :fill-color="'#E9322D'" />
				<Check v-else :size="20" :fill-color="'#46BA61'" />

				{{ connectionState }}

</template>

<template>

		<input ref="signaling_server"
			type="text"
			name="signaling_server"
			placeholder="wss://signaling.example.org"
			:value="server"
			:disabled="loading"
			:aria-label="t('spreed', 'High-performance backend URL')"
			@input="updateServer">
		<input :id="'verify' + index"
			type="checkbox"
			:name="'verify' + index"
			class="checkbox verify"
			:checked="verify"
			@change="updateVerify">
		{{ t('spreed', 'Validate SSL certificate') }}

		<NcButton v-show="!loading"
			type="tertiary-no-background"
			:aria-label="t('spreed', 'Delete this server')"
			@click="removeServer">
			<template #icon>
				<Delete :size="20" />
			</template>
		</NcButton>

				<NcLoadingIcon v-if="!checked" :size="20" />
				<AlertCircle v-else-if="errorMessage" :size="20" :fill-color="'#E9322D'" />
				<Check v-else :size="20" :fill-color="'#46BA61'" />

				{{ connectionState }}

</template>


Handle disabled actions button in vue lib

Signed-off-by: Marco Ambrosini <3397c7e210a9d1588196cfebb12b2c6b133f6934@pm.me>

<template>

		<input id="file-upload"
			ref="fileUploadInput"
			multiple
			type="file"
			tabindex="-1"
			aria-hidden="true"
			class="hidden-visually"
			@change="handleFileInput">

			<form
				class="new-message-form"
				@submit.prevent>

					<Actions
						ref="uploadMenu"
						:disabled="disabled"
						default-icon="icon-clip-add-file"
						:aria-label="t('spreed', 'Share files to the conversation')"
						:aria-haspopup="true">
						<ActionButton
							v-if="canUploadFiles"
							:close-after-click="true"
							icon="icon-upload"
							@click.prevent="clickImportInput">
							{{ t('spreed', 'Upload new files') }}
						</ActionButton>
						<ActionButton
							v-if="canShareFiles"
							:close-after-click="true"
							icon="icon-folder"
							@click.prevent="handleFileShare">
							{{ t('spreed', 'Share from Files') }}
						</ActionButton>
					</Actions>

					<EmojiPicker @select="addEmoji">
						<button
							type="button"
							:disabled="disabled"
							class="nc-button nc-button__main"
							:aria-label="t('spreed', 'Add emoji')"
							:aria-haspopup="true">
							<EmoticonOutline
								:size="16"
								decorative
								title="" />
						</button>
					</EmojiPicker>

						<Quote
							:is-new-message-form-quote="true"
							:parent-id="messageToBeReplied.id"
							v-bind="messageToBeReplied" />

					<AdvancedInput
						ref="advancedInput"
						v-model="text"
						:token="token"
						:active-input="!disabled"
						:placeholder-text="placeholderText"
						:aria-label="placeholderText"
						@update:contentEditable="contentEditableToParsed"
						@submit="handleSubmit"
						@files-pasted="handlePastedFiles" />

				<button
					:disabled="disabled"
					type="submit"
					:aria-label="t('spreed', 'Send message')"
					class="nc-button nc-button__main"
					@click.prevent="handleSubmit">
					<Send
						title=""
						:size="16"
						decorative />
				</button>
			</form>

</template>

<template>

		<input id="file-upload"
			ref="fileUploadInput"
			multiple
			type="file"
			tabindex="-1"
			aria-hidden="true"
			class="hidden-visually"
			@change="handleFileInput">

			<form
				class="new-message-form"
				@submit.prevent>

					<Actions
						ref="uploadMenu"
						default-icon="icon-clip-add-file"
						:aria-label="t('spreed', 'Share files to the conversation')"
						:aria-haspopup="true">
						<ActionButton
							v-if="canUploadFiles"
							:close-after-click="true"
							icon="icon-upload"
							@click.prevent="clickImportInput">
							{{ t('spreed', 'Upload new files') }}
						</ActionButton>
						<ActionButton
							v-if="canShareFiles"
							:close-after-click="true"
							icon="icon-folder"
							@click.prevent="handleFileShare">
							{{ t('spreed', 'Share from Files') }}
						</ActionButton>
					</Actions>

					<EmojiPicker @select="addEmoji">
						<button
							type="button"
							:disabled="disabled"
							class="nc-button nc-button__main"
							:aria-label="t('spreed', 'Add emoji')"
							:aria-haspopup="true">
							<EmoticonOutline
								:size="16"
								decorative
								title="" />
						</button>
					</EmojiPicker>

						<Quote
							:is-new-message-form-quote="true"
							:parent-id="messageToBeReplied.id"
							v-bind="messageToBeReplied" />

					<AdvancedInput
						ref="advancedInput"
						v-model="text"
						:token="token"
						:active-input="!disabled"
						:placeholder-text="placeholderText"
						:aria-label="placeholderText"
						@update:contentEditable="contentEditableToParsed"
						@submit="handleSubmit"
						@files-pasted="handlePastedFiles" />

				<button
					:disabled="disabled"
					type="submit"
					:aria-label="t('spreed', 'Send message')"
					class="nc-button nc-button__main"
					@click.prevent="handleSubmit">
					<Send
						title=""
						:size="16"
						decorative />
				</button>
			</form>

</template>


RandomPicture: white text color

RandomPicture: white text color

<template>
</template>

<script>
export default {
  props: {
    name: { required: true },
    seed: { default: 2 },
    size: { required: true },
  },
  mounted () {
    this.$el.appendChild(this.box)
  },
  watch: {
    box (box, prevBox) {
      this.$el.replaceChild(box, prevBox)
    },
  },
  computed: {
    initials () {
      if (!this.name) return
      let parts = this.name.split(' ').map(s => s[0])
      if (parts.length > 2) {
        parts = [parts[0], parts[parts.length - 1]]
      }
      return parts.join('').toUpperCase()
    },
    box () {
      function pseudoRandom (seed) {
        let random = Math.sin(seed) * 10000
        random -= Math.floor(random)
        return random
      }

      let seed = pseudoRandom(this.seed)
      let size = this.size

      function getRandomRange (min, max, add = 1000) {
        return Math.floor(pseudoRandom(seed * add) * (max - min) + min)
      }

      const svgns = 'http://www.w3.org/2000/svg'
      const box = document.createElementNS(svgns, 'svg')

      const rows = 3
      const columns = 3
      const blockSize = Math.floor(size / 2)
      const rotate = blockSize * rows / 2

      box.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
      box.setAttribute('width', size)
      box.setAttribute('height', size)
      box.setAttribute('viewbox', '0 0 100 100')
      box.setAttribute('class', 'box')

      const g = document.createElementNS(svgns, 'g')
      g.setAttribute(
        'transform',
        `translate(${-(rows * blockSize - size) / 2} ${-((rows * blockSize - size) / 2)}) ` +
        `rotate(${90 * seed} ${rotate} ${rotate})`,
      )

      for (let i = 0; i < columns; i++) {
        // noprotect
        for (let j = 0; j < rows; j++) {
          let rect = document.createElementNS(svgns, 'rect')
          rect.setAttribute('width', blockSize)
          rect.setAttribute('height', blockSize)
          rect.setAttribute('fill', 'rgba(' +
            getRandomRange(100, 255, (i + 1) * (j + 1) * 1) + ',' +
            getRandomRange(100, 255, (i + 1) * (j + 1) * 2) + ',' +
            getRandomRange(100, 255, (i + 1) * (j + 1) * 3) + ',1)',
          )
          rect.setAttribute('x', i * blockSize)
          rect.setAttribute('y', j * blockSize)

          g.appendChild(rect)
        }
      }
      box.appendChild(g)

      let overlay = document.createElementNS(svgns, 'rect')
      overlay.setAttribute('width', size)
      overlay.setAttribute('height', size)
      overlay.setAttribute('fill', 'rgba(' +
        getRandomRange(100, 255, 1) + ',' +
        getRandomRange(100, 255, 2) + ',' +
        getRandomRange(100, 255, 3) + ',0.5)',
      )
      overlay.setAttribute('x', 0)
      overlay.setAttribute('y', 0)
      box.appendChild(overlay)

      if (this.initials) {
        let initials = document.createTextNode(this.initials)
        let textOverlay = document.createElementNS(svgns, 'text')
        textOverlay.setAttribute('width', size)
        textOverlay.setAttribute('height', size)
        textOverlay.setAttribute('fill', 'rgba(255,255,255,1)')
        textOverlay.setAttribute('font-size', blockSize)
        textOverlay.setAttribute('text-anchor', 'middle')
        textOverlay.setAttribute('x', blockSize)
        textOverlay.setAttribute('y', blockSize * 1.3)
        textOverlay.appendChild(initials)
        box.appendChild(textOverlay)
      }

      return box
    },
  },
}
</script>

<style scoped lang='stylus'>
</style>

<template>
</template>

<script>
export default {
  props: {
    name: { required: true },
    seed: { default: 2 },
    size: { required: true },
  },
  mounted () {
    this.$el.appendChild(this.box)
  },
  watch: {
    box (box, prevBox) {
      this.$el.replaceChild(box, prevBox)
    },
  },
  computed: {
    initials () {
      if (!this.name) return
      let parts = this.name.split(' ').map(s => s[0])
      if (parts.length > 2) {
        parts = [parts[0], parts[parts.length - 1]]
      }
      return parts.join('').toUpperCase()
    },
    box () {
      function pseudoRandom (seed) {
        let random = Math.sin(seed) * 10000
        random -= Math.floor(random)
        return random
      }

      let seed = pseudoRandom(this.seed)
      let size = this.size

      function getRandomRange (min, max, add = 1000) {
        return Math.floor(pseudoRandom(seed * add) * (max - min) + min)
      }

      const svgns = 'http://www.w3.org/2000/svg'
      const box = document.createElementNS(svgns, 'svg')

      const rows = 3
      const columns = 3
      const blockSize = Math.floor(size / 2)
      const rotate = blockSize * rows / 2

      box.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
      box.setAttribute('width', size)
      box.setAttribute('height', size)
      box.setAttribute('viewbox', '0 0 100 100')
      box.setAttribute('class', 'box')

      const g = document.createElementNS(svgns, 'g')
      g.setAttribute(
        'transform',
        `translate(${-(rows * blockSize - size) / 2} ${-((rows * blockSize - size) / 2)}) ` +
        `rotate(${90 * seed} ${rotate} ${rotate})`,
      )

      for (let i = 0; i < columns; i++) {
        // noprotect
        for (let j = 0; j < rows; j++) {
          let rect = document.createElementNS(svgns, 'rect')
          rect.setAttribute('width', blockSize)
          rect.setAttribute('height', blockSize)
          rect.setAttribute('fill', 'rgba(' +
            getRandomRange(100, 255, (i + 1) * (j + 1) * 1) + ',' +
            getRandomRange(100, 255, (i + 1) * (j + 1) * 2) + ',' +
            getRandomRange(100, 255, (i + 1) * (j + 1) * 3) + ',1)',
          )
          rect.setAttribute('x', i * blockSize)
          rect.setAttribute('y', j * blockSize)

          g.appendChild(rect)
        }
      }
      box.appendChild(g)

      let overlay = document.createElementNS(svgns, 'rect')
      overlay.setAttribute('width', size)
      overlay.setAttribute('height', size)
      overlay.setAttribute('fill', 'rgba(' +
        getRandomRange(100, 255, 1) + ',' +
        getRandomRange(100, 255, 2) + ',' +
        getRandomRange(100, 255, 3) + ',0.5)',
      )
      overlay.setAttribute('x', 0)
      overlay.setAttribute('y', 0)
      box.appendChild(overlay)

      if (this.initials) {
        let initials = document.createTextNode(this.initials)
        let textOverlay = document.createElementNS(svgns, 'text')
        textOverlay.setAttribute('width', size)
        textOverlay.setAttribute('height', size)
        textOverlay.setAttribute('fill', 'rgba(0,0,0,1)')
        textOverlay.setAttribute('font-size', blockSize)
        textOverlay.setAttribute('text-anchor', 'middle')
        textOverlay.setAttribute('x', blockSize)
        textOverlay.setAttribute('y', blockSize * 1.3)
        textOverlay.appendChild(initials)
        box.appendChild(textOverlay)
      }

      return box
    },
  },
}
</script>

<style scoped lang='stylus'>
</style>


add autofocus on board edit

add autofocus on board edit

Signed-off-by: Michael Weimann <1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e@michael-weimann.eu>

<!--
  - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  -
  - @author John Molakvoæ <skjnldsv@protonmail.com>
  -
  - @license GNU AGPL version 3 or any later version
  -
  - This program is free software: you can redistribute it and/or modify
  - it under the terms of the GNU Affero General Public License as
  - published by the Free Software Foundation, either version 3 of the
  - License, or (at your option) any later version.
  -
  - This program is distributed in the hope that it will be useful,
  - but WITHOUT ANY WARRANTY; without even the implied warranty of
  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  - GNU Affero General Public License for more details.
  -
  - You should have received a copy of the GNU Affero General Public License
  - along with this program. If not, see <http://www.gnu.org/licenses/>.
  -
  -->
<template>
	<AppNavigationItem v-if="!editing"
		:title="!deleted ? board.title : undoText"
		:loading="loading"
		:to="routeTo"
		:undo="deleted"
		@undo="unDelete">
		<AppNavigationIconBullet slot="icon" :color="board.color" />

		<AppNavigationCounter v-if="board.acl.length"
			slot="counter"
			class="icon-shared"
			style="opacity: 0.5" />

		<template v-if="!deleted" slot="actions">
			<template v-if="!isDueSubmenuActive">
				<ActionButton
					icon="icon-info"
					:close-after-click="true"
					@click="actionDetails">
					{{ t('deck', 'Board details') }}
				</ActionButton>
				<ActionButton v-if="canManage && !board.archived"
					icon="icon-rename"
					:close-after-click="true"
					@click="actionEdit">
					{{ t('deck', 'Edit board') }}
				</ActionButton>
				<ActionButton v-if="canManage && !board.archived"
					icon="icon-clone"
					:close-after-click="true"
					@click="actionClone">
					{{ t('deck', 'Clone board') }}
				</ActionButton>
				<ActionButton v-if="canManage && board.archived"
					icon="icon-archive"
					:close-after-click="true"
					@click="actionUnarchive">
					{{ t('deck', 'Unarchive board') }}
				</ActionButton>
				<ActionButton v-else-if="canManage && !board.archived"
					icon="icon-archive"
					:close-after-click="true"
					@click="actionArchive">
					{{ t('deck', 'Archive board') }}
				</ActionButton>

				<ActionButton v-if="!board.archived && board.acl.length === 0" :icon="board.settings['notify-due'] === 'off' ? 'icon-sound' : 'icon-sound-off'" @click="board.settings['notify-due'] === 'off' ? updateSetting('notify-due', 'all') : updateSetting('notify-due', 'off')">
					{{ board.settings['notify-due'] === 'off' ? t('deck', 'Turn on due date reminders') : t('deck', 'Turn off due date reminders') }}
				</ActionButton>
			</template>

			<!-- Due date reminder settings -->
			<template v-if="isDueSubmenuActive">
				<ActionButton
					:icon="updateDueSetting ? 'icon-loading-small' : 'icon-view-previous'"
					:disabled="updateDueSetting"
					@click="isDueSubmenuActive=false">
					{{ t('deck', 'Due date reminders') }}
				</ActionButton>

				<ActionButton
					name="notification"
					icon="icon-sound"
					:disabled="updateDueSetting"
					:class="{ 'forced-active': board.settings['notify-due'] === 'all' }"
					@click="updateSetting('notify-due', 'all')">
					{{ t('deck', 'All cards') }}
				</ActionButton>
				<ActionButton
					name="notification"
					icon="icon-user"
					:disabled="updateDueSetting"
					:class="{ 'forced-active': board.settings['notify-due'] === 'assigned' }"
					@click="updateSetting('notify-due', 'assigned')">
					{{ t('deck', 'Assigned cards') }}
				</ActionButton>
				<ActionButton
					name="notification"
					icon="icon-sound-off"
					:disabled="updateDueSetting"
					:class="{ 'forced-active': board.settings['notify-due'] === 'off' }"
					@click="updateSetting('notify-due', 'off')">
					{{ t('deck', 'No notifications') }}
				</ActionButton>
			</template>
			<ActionButton v-else-if="!board.archived && board.acl.length > 0"
				:title="t('deck', 'Due date reminders')"
				:icon="dueDateReminderIcon"
				@click="isDueSubmenuActive=true">
				{{ dueDateReminderText }}
			</ActionButton>

			<ActionButton v-if="canManage && !isDueSubmenuActive"
				icon="icon-delete"
				:close-after-click="true"
				@click="actionDelete">
				{{ t('deck', 'Delete board') }}
			</ActionButton>
		</template>
	</AppNavigationItem>
	<div v-else-if="editing" class="board-edit">
		<ColorPicker class="app-navigation-entry-bullet-wrapper" :value="`#${board.color}`" @input="updateColor">
			<div :style="{ backgroundColor: getColor }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
		</ColorPicker>
		<form @submit.prevent.stop="applyEdit">
			<input v-model="editTitle"
				v-focus
				type="text"
				required>
			<input type="submit" value="" class="icon-confirm">
			<Actions><ActionButton icon="icon-close" @click.stop.prevent="cancelEdit" /></Actions>
		</form>
	</div>
</template>

<script>
import { AppNavigationIconBullet, AppNavigationCounter, AppNavigationItem, ColorPicker, Actions, ActionButton } from '@nextcloud/vue'
import ClickOutside from 'vue-click-outside'

export default {
	name: 'AppNavigationBoard',
	components: {
		AppNavigationIconBullet,
		AppNavigationCounter,
		AppNavigationItem,
		ColorPicker,
		Actions,
		ActionButton,
	},
	directives: {
		ClickOutside,
	},
	inject: [
		'boardApi',
	],
	props: {
		board: {
			type: Object,
			required: true,
		},
	},
	data() {
		return {
			classes: [],
			deleted: false,
			loading: false,
			editing: false,
			menuOpen: false,
			undoTimeoutHandle: null,
			editTitle: '',
			editColor: '',
			isDueSubmenuActive: false,
			updateDueSetting: null,
		}
	},
	computed: {
		getColor() {
			if (this.editColor !== '') {
				return this.editColor
			}
			return this.board.color
		},
		undoText() {
			return t('deck', 'Board {0} deleted', [this.board.title])
		},
		routeTo() {
			return {
				name: 'board',
				params: { id: this.board.id },
			}
		},
		canManage() {
			return this.board.permissions.PERMISSION_MANAGE
		},
		dueDateReminderIcon() {
			if (this.board.settings['notify-due'] === 'all') {
				return 'icon-sound'
			} else if (this.board.settings['notify-due'] === 'assigned') {
				return 'icon-user'
			} else if (this.board.settings['notify-due'] === 'off') {
				return 'icon-sound-off'
			}
			return ''
		},
		dueDateReminderText() {
			if (this.board.settings['notify-due'] === 'all') {
				return t('deck', 'All cards')
			} else if (this.board.settings['notify-due'] === 'assigned') {
				return t('deck', 'Only assigned cards')
			} else if (this.board.settings['notify-due'] === 'off') {
				return t('deck', 'No reminder')
			}
			return ''
		},
	},
	watch: {},
	mounted() {
		// prevent click outside event with popupItem.
		this.popupItem = this.$el
	},
	methods: {
		unDelete() {
			clearTimeout(this.undoTimeoutHandle)
			this.boardApi.unDeleteBoard(this.board)
				.then(() => {
					this.deleted = false
				})
		},
		updateColor(newColor) {
			this.editColor = newColor
		},
		actionEdit() {
			this.editTitle = this.board.title
			this.editColor = '#' + this.board.color
			this.editing = true
		},
		async actionClone() {
			this.loading = true
			try {
				const newBoard = await this.$store.dispatch('cloneBoard', this.board)
				this.loading = false
				this.$router.push({ name: 'board', params: { id: newBoard.id } })
			} catch (e) {
				OC.Notification.showTemporary(t('deck', 'An error occurred'))
				console.error(e)
			}
		},
		actionArchive() {
			this.loading = true
			this.$store.dispatch('archiveBoard', this.board)
		},
		actionUnarchive() {
			this.loading = true
			this.$store.dispatch('unarchiveBoard', this.board)
		},
		actionDelete() {
			OC.dialogs.confirmDestructive(
				t('deck', 'Are you sure you want to delete the board {title}? This will delete all the data of this board.', { title: this.board.title }),
				t('deck', 'Delete the board?'),
				{
					type: OC.dialogs.YES_NO_BUTTONS,
					confirm: t('deck', 'Delete'),
					confirmClasses: 'error',
					cancel: t('deck', 'Cancel'),
				},
				(result) => {
					if (result) {
						this.loading = true
						this.boardApi.deleteBoard(this.board)
							.then(() => {
								this.loading = false
								this.deleted = true
								this.undoTimeoutHandle = setTimeout(() => {
									this.$store.dispatch('removeBoard', this.board)
								}, 7000)
							})
					}
				},
				true
			)
		},
		actionDetails() {
			this.$router.push({ name: 'board.details', params: { id: this.board.id } })
		},
		applyEdit(e) {
			this.editing = false
			if (this.editTitle || this.editColor) {
				this.loading = true
				const copy = JSON.parse(JSON.stringify(this.board))
				copy.title = this.editTitle
				copy.color = (typeof this.editColor.hex !== 'undefined' ? this.editColor.hex : this.editColor).substring(1)
				this.$store.dispatch('updateBoard', copy)
					.then(() => {
						this.loading = false
					})
			}
		},
		cancelEdit(e) {
			this.editing = false
		},
		async updateSetting(key, value) {
			this.updateDueSetting = value
			const setting = {}
			setting['board:' + this.board.id + ':' + key] = value
			await this.$store.dispatch('setConfig', setting)
			this.isDueSubmenuActive = false
			this.updateDueSetting = null
		},
	},
}
</script>

<style lang="scss" scoped>
	.board-edit {
		margin-left: 44px;
		order: 1;
		display: flex;
		height: 44px;

		form {
			display: flex;
			flex-grow: 1;

			input[type='text'] {
				flex-grow: 1;
			}
		}
	}

	.app-navigation-entry-bullet-wrapper {
		width: 44px;
		height: 44px;
		.color0 {
			width: 30px !important;
			margin: 5px;
			margin-left: 7px;
			height: 30px;
			border-radius: 50%;
			background-size: 14px;
		}
	}

	.forced-active {
		box-shadow: inset 4px 0 var(--color-primary-element);
	}
</style>

<!--
  - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  -
  - @author John Molakvoæ <skjnldsv@protonmail.com>
  -
  - @license GNU AGPL version 3 or any later version
  -
  - This program is free software: you can redistribute it and/or modify
  - it under the terms of the GNU Affero General Public License as
  - published by the Free Software Foundation, either version 3 of the
  - License, or (at your option) any later version.
  -
  - This program is distributed in the hope that it will be useful,
  - but WITHOUT ANY WARRANTY; without even the implied warranty of
  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  - GNU Affero General Public License for more details.
  -
  - You should have received a copy of the GNU Affero General Public License
  - along with this program. If not, see <http://www.gnu.org/licenses/>.
  -
  -->
<template>
	<AppNavigationItem v-if="!editing"
		:title="!deleted ? board.title : undoText"
		:loading="loading"
		:to="routeTo"
		:undo="deleted"
		@undo="unDelete">
		<AppNavigationIconBullet slot="icon" :color="board.color" />

		<AppNavigationCounter v-if="board.acl.length"
			slot="counter"
			class="icon-shared"
			style="opacity: 0.5" />

		<template v-if="!deleted" slot="actions">
			<template v-if="!isDueSubmenuActive">
				<ActionButton
					icon="icon-info"
					:close-after-click="true"
					@click="actionDetails">
					{{ t('deck', 'Board details') }}
				</ActionButton>
				<ActionButton v-if="canManage && !board.archived"
					icon="icon-rename"
					:close-after-click="true"
					@click="actionEdit">
					{{ t('deck', 'Edit board') }}
				</ActionButton>
				<ActionButton v-if="canManage && !board.archived"
					icon="icon-clone"
					:close-after-click="true"
					@click="actionClone">
					{{ t('deck', 'Clone board') }}
				</ActionButton>
				<ActionButton v-if="canManage && board.archived"
					icon="icon-archive"
					:close-after-click="true"
					@click="actionUnarchive">
					{{ t('deck', 'Unarchive board') }}
				</ActionButton>
				<ActionButton v-else-if="canManage && !board.archived"
					icon="icon-archive"
					:close-after-click="true"
					@click="actionArchive">
					{{ t('deck', 'Archive board') }}
				</ActionButton>

				<ActionButton v-if="!board.archived && board.acl.length === 0" :icon="board.settings['notify-due'] === 'off' ? 'icon-sound' : 'icon-sound-off'" @click="board.settings['notify-due'] === 'off' ? updateSetting('notify-due', 'all') : updateSetting('notify-due', 'off')">
					{{ board.settings['notify-due'] === 'off' ? t('deck', 'Turn on due date reminders') : t('deck', 'Turn off due date reminders') }}
				</ActionButton>
			</template>

			<!-- Due date reminder settings -->
			<template v-if="isDueSubmenuActive">
				<ActionButton
					:icon="updateDueSetting ? 'icon-loading-small' : 'icon-view-previous'"
					:disabled="updateDueSetting"
					@click="isDueSubmenuActive=false">
					{{ t('deck', 'Due date reminders') }}
				</ActionButton>

				<ActionButton
					name="notification"
					icon="icon-sound"
					:disabled="updateDueSetting"
					:class="{ 'forced-active': board.settings['notify-due'] === 'all' }"
					@click="updateSetting('notify-due', 'all')">
					{{ t('deck', 'All cards') }}
				</ActionButton>
				<ActionButton
					name="notification"
					icon="icon-user"
					:disabled="updateDueSetting"
					:class="{ 'forced-active': board.settings['notify-due'] === 'assigned' }"
					@click="updateSetting('notify-due', 'assigned')">
					{{ t('deck', 'Assigned cards') }}
				</ActionButton>
				<ActionButton
					name="notification"
					icon="icon-sound-off"
					:disabled="updateDueSetting"
					:class="{ 'forced-active': board.settings['notify-due'] === 'off' }"
					@click="updateSetting('notify-due', 'off')">
					{{ t('deck', 'No notifications') }}
				</ActionButton>
			</template>
			<ActionButton v-else-if="!board.archived && board.acl.length > 0"
				:title="t('deck', 'Due date reminders')"
				:icon="dueDateReminderIcon"
				@click="isDueSubmenuActive=true">
				{{ dueDateReminderText }}
			</ActionButton>

			<ActionButton v-if="canManage && !isDueSubmenuActive"
				icon="icon-delete"
				:close-after-click="true"
				@click="actionDelete">
				{{ t('deck', 'Delete board') }}
			</ActionButton>
		</template>
	</AppNavigationItem>
	<div v-else-if="editing" class="board-edit">
		<ColorPicker class="app-navigation-entry-bullet-wrapper" :value="`#${board.color}`" @input="updateColor">
			<div :style="{ backgroundColor: getColor }" class="color0 icon-colorpicker app-navigation-entry-bullet" />
		</ColorPicker>
		<form @submit.prevent.stop="applyEdit">
			<input v-model="editTitle" type="text" required>
			<input type="submit" value="" class="icon-confirm">
			<Actions><ActionButton icon="icon-close" @click.stop.prevent="cancelEdit" /></Actions>
		</form>
	</div>
</template>

<script>
import { AppNavigationIconBullet, AppNavigationCounter, AppNavigationItem, ColorPicker, Actions, ActionButton } from '@nextcloud/vue'
import ClickOutside from 'vue-click-outside'

export default {
	name: 'AppNavigationBoard',
	components: {
		AppNavigationIconBullet,
		AppNavigationCounter,
		AppNavigationItem,
		ColorPicker,
		Actions,
		ActionButton,
	},
	directives: {
		ClickOutside,
	},
	inject: [
		'boardApi',
	],
	props: {
		board: {
			type: Object,
			required: true,
		},
	},
	data() {
		return {
			classes: [],
			deleted: false,
			loading: false,
			editing: false,
			menuOpen: false,
			undoTimeoutHandle: null,
			editTitle: '',
			editColor: '',
			isDueSubmenuActive: false,
			updateDueSetting: null,
		}
	},
	computed: {
		getColor() {
			if (this.editColor !== '') {
				return this.editColor
			}
			return this.board.color
		},
		undoText() {
			return t('deck', 'Board {0} deleted', [this.board.title])
		},
		routeTo() {
			return {
				name: 'board',
				params: { id: this.board.id },
			}
		},
		canManage() {
			return this.board.permissions.PERMISSION_MANAGE
		},
		dueDateReminderIcon() {
			if (this.board.settings['notify-due'] === 'all') {
				return 'icon-sound'
			} else if (this.board.settings['notify-due'] === 'assigned') {
				return 'icon-user'
			} else if (this.board.settings['notify-due'] === 'off') {
				return 'icon-sound-off'
			}
			return ''
		},
		dueDateReminderText() {
			if (this.board.settings['notify-due'] === 'all') {
				return t('deck', 'All cards')
			} else if (this.board.settings['notify-due'] === 'assigned') {
				return t('deck', 'Only assigned cards')
			} else if (this.board.settings['notify-due'] === 'off') {
				return t('deck', 'No reminder')
			}
			return ''
		},
	},
	watch: {},
	mounted() {
		// prevent click outside event with popupItem.
		this.popupItem = this.$el
	},
	methods: {
		unDelete() {
			clearTimeout(this.undoTimeoutHandle)
			this.boardApi.unDeleteBoard(this.board)
				.then(() => {
					this.deleted = false
				})
		},
		updateColor(newColor) {
			this.editColor = newColor
		},
		actionEdit() {
			this.editTitle = this.board.title
			this.editColor = '#' + this.board.color
			this.editing = true
		},
		async actionClone() {
			this.loading = true
			try {
				const newBoard = await this.$store.dispatch('cloneBoard', this.board)
				this.loading = false
				this.$router.push({ name: 'board', params: { id: newBoard.id } })
			} catch (e) {
				OC.Notification.showTemporary(t('deck', 'An error occurred'))
				console.error(e)
			}
		},
		actionArchive() {
			this.loading = true
			this.$store.dispatch('archiveBoard', this.board)
		},
		actionUnarchive() {
			this.loading = true
			this.$store.dispatch('unarchiveBoard', this.board)
		},
		actionDelete() {
			OC.dialogs.confirmDestructive(
				t('deck', 'Are you sure you want to delete the board {title}? This will delete all the data of this board.', { title: this.board.title }),
				t('deck', 'Delete the board?'),
				{
					type: OC.dialogs.YES_NO_BUTTONS,
					confirm: t('deck', 'Delete'),
					confirmClasses: 'error',
					cancel: t('deck', 'Cancel'),
				},
				(result) => {
					if (result) {
						this.loading = true
						this.boardApi.deleteBoard(this.board)
							.then(() => {
								this.loading = false
								this.deleted = true
								this.undoTimeoutHandle = setTimeout(() => {
									this.$store.dispatch('removeBoard', this.board)
								}, 7000)
							})
					}
				},
				true
			)
		},
		actionDetails() {
			this.$router.push({ name: 'board.details', params: { id: this.board.id } })
		},
		applyEdit(e) {
			this.editing = false
			if (this.editTitle || this.editColor) {
				this.loading = true
				const copy = JSON.parse(JSON.stringify(this.board))
				copy.title = this.editTitle
				copy.color = (typeof this.editColor.hex !== 'undefined' ? this.editColor.hex : this.editColor).substring(1)
				this.$store.dispatch('updateBoard', copy)
					.then(() => {
						this.loading = false
					})
			}
		},
		cancelEdit(e) {
			this.editing = false
		},
		async updateSetting(key, value) {
			this.updateDueSetting = value
			const setting = {}
			setting['board:' + this.board.id + ':' + key] = value
			await this.$store.dispatch('setConfig', setting)
			this.isDueSubmenuActive = false
			this.updateDueSetting = null
		},
	},
}
</script>

<style lang="scss" scoped>
	.board-edit {
		margin-left: 44px;
		order: 1;
		display: flex;
		height: 44px;

		form {
			display: flex;
			flex-grow: 1;

			input[type='text'] {
				flex-grow: 1;
			}
		}
	}

	.app-navigation-entry-bullet-wrapper {
		width: 44px;
		height: 44px;
		.color0 {
			width: 30px !important;
			margin: 5px;
			margin-left: 7px;
			height: 30px;
			border-radius: 50%;
			background-size: 14px;
		}
	}

	.forced-active {
		box-shadow: inset 4px 0 var(--color-primary-element);
	}
</style>


Display a message in the "previous Splatfests" dialog when results aren't available yet

<template>
    <Modal @close="$emit('close')" class="is-wide splatfest-history-dialog modal-rounded-box">

{{ $t('splatfest.previous') }}

                        <Dropdown
                            :options="regions"
                            v-model="region"
                            style="margin: 0 -12px" class="font-splatoon2 is-right-tablet"
                            />

                                    <SplatfestBox
                                        :festival="info.festival"
                                        :now="now"
                                        :historyMode="true"
                                        />

                                <SplatfestResultsBox
                                    v-if="info.results"
                                    :festival="info.festival"
                                    :results="info.results"
                                    :historyMode="true"
                                    />

 now" class="has-text-centered font-splatoon2 is-size-5">
                                    {{ info.festival.times.result - now | duration | resultsIn }}

    </Modal>
</template>

<template>
    <Modal @close="$emit('close')" class="is-wide splatfest-history-dialog modal-rounded-box">

{{ $t('splatfest.previous') }}

                        <Dropdown
                            :options="regions"
                            v-model="region"
                            style="margin: 0 -12px" class="font-splatoon2 is-right-tablet"
                            />

                                    <SplatfestBox
                                        :festival="info.festival"
                                        :now="now"
                                        :historyMode="true"
                                        />

                                <SplatfestResultsBox
                                    v-if="info.results"
                                    :festival="info.festival"
                                    :results="info.results"
                                    :historyMode="true"
                                    />

    </Modal>
</template>


Improve grammar in IP sentence

Improve grammar in IP sentence

<template>

        {{ s__('ClusterIntegration|Applications') }}

        <application-row
          id="helm"
          :title="applications.helm.title"
          title-link="https://docs.helm.sh/"
          :status="applications.helm.status"
          :status-reason="applications.helm.statusReason"
          :request-status="applications.helm.requestStatus"
          :request-reason="applications.helm.requestReason"
        >

            {{ s__(`ClusterIntegration|Helm streamlines installing
              and managing Kubernetes applications.
              Tiller runs inside of your Kubernetes Cluster,
              and manages releases of your charts.`) }}

        </application-row>
        <application-row
          :id="ingressId"
          :title="applications.ingress.title"
          title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
          :status="applications.ingress.status"
          :status-reason="applications.ingress.statusReason"
          :request-status="applications.ingress.requestStatus"
          :request-reason="applications.ingress.requestReason"
        >

              {{ s__(`ClusterIntegration|Ingress gives you a way to route
                requests to services based on the request host or path,
                centralizing a number of services into a single entrypoint.`) }}

            <template v-if="ingressInstalled">

                  {{ s__('ClusterIntegration| Ingress IP Address') }}

                  <input
                    type="text"
                    id="ingress-ip-address"
                    class="form-control js-ip-address"
                    :value="ingressExternalIp"
                    readonly
                  />

                    <clipboard-button
                      :text="ingressExternalIp"
                      :title="s__('ClusterIntegration|Copy Ingress IP Address')"
                      css-class="btn btn-default js-clipboard-btn"
                    />

                <input
                  v-else
                  type="text"
                  class="form-control js-ip-address"
                  readonly
                  value="?"
                />

                {{ s__(`ClusterIntegration|The IP address is in
                the process of being assigned. Please check your Kubernetes
                cluster or Quotas on GKE if it takes a long time.`) }}

                  {{ __('More information') }}

                {{ s__(`ClusterIntegration|Point a wildcard DNS to this
                generated IP address in order to access
                your application after it has been deployed.`) }}

                  {{ __('More information') }}

            </template>

        </application-row>
        <application-row
          id="prometheus"
          :title="applications.prometheus.title"
          title-link="https://prometheus.io/docs/introduction/overview/"
          :manage-link="managePrometheusPath"
          :status="applications.prometheus.status"
          :status-reason="applications.prometheus.statusReason"
          :request-status="applications.prometheus.requestStatus"
          :request-reason="applications.prometheus.requestReason"
        >

        </application-row>

</template>

<template>

        {{ s__('ClusterIntegration|Applications') }}

        <application-row
          id="helm"
          :title="applications.helm.title"
          title-link="https://docs.helm.sh/"
          :status="applications.helm.status"
          :status-reason="applications.helm.statusReason"
          :request-status="applications.helm.requestStatus"
          :request-reason="applications.helm.requestReason"
        >

            {{ s__(`ClusterIntegration|Helm streamlines installing
              and managing Kubernetes applications.
              Tiller runs inside of your Kubernetes Cluster,
              and manages releases of your charts.`) }}

        </application-row>
        <application-row
          :id="ingressId"
          :title="applications.ingress.title"
          title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
          :status="applications.ingress.status"
          :status-reason="applications.ingress.statusReason"
          :request-status="applications.ingress.requestStatus"
          :request-reason="applications.ingress.requestReason"
        >

              {{ s__(`ClusterIntegration|Ingress gives you a way to route
                requests to services based on the request host or path,
                centralizing a number of services into a single entrypoint.`) }}

            <template v-if="ingressInstalled">

                  {{ s__('ClusterIntegration| Ingress IP Address') }}

                  <input
                    type="text"
                    id="ingress-ip-address"
                    class="form-control js-ip-address"
                    :value="ingressExternalIp"
                    readonly
                  />

                    <clipboard-button
                      :text="ingressExternalIp"
                      :title="s__('ClusterIntegration|Copy Ingress IP Address')"
                      css-class="btn btn-default js-clipboard-btn"
                    />

                <input
                  v-else
                  type="text"
                  class="form-control js-ip-address"
                  readonly
                  value="?"
                />

                {{ s__(`ClusterIntegration|The IP address is still in
                the process of being assigned, please check your Kubernetes
                cluster or Quotas on GKE if it takes a long time.`) }}

                  {{ __('More information') }}

                {{ s__(`ClusterIntegration|Point a wildcard DNS to this
                generated IP address in order to access
                your application after it has been deployed.`) }}

                  {{ __('More information') }}

            </template>

        </application-row>
        <application-row
          id="prometheus"
          :title="applications.prometheus.title"
          title-link="https://prometheus.io/docs/introduction/overview/"
          :manage-link="managePrometheusPath"
          :status="applications.prometheus.status"
          :status-reason="applications.prometheus.statusReason"
          :request-status="applications.prometheus.requestStatus"
          :request-reason="applications.prometheus.requestReason"
        >

        </application-row>

</template>


Update ViewerManager.vue

Update ViewerManager.vue

<template>

    <transition name="fade">

      <template v-if="isList">

          <ListCard
            v-for="{ id: uid, attributes } in dashboards"
            :key="uid"
            v-bind="attributes"
            @click.native="handleClick(uid)"
          />

      </template>

      <template v-else-if="currentDashboard">
        <Viewer :config="currentDashboard" />
      </template>
    </transition>

       {{ backLabel }}

</template>


Fix mavigation's dropdown css

Fix mavigation's dropdown css

<template>

      <router-link class="brand" tag="li" to="/" exact>

      </router-link>

      <router-link tag="li" to="/" exact>
        Home
      </router-link>

      <router-link tag="li" to="/add">
        Add
      </router-link>

      <router-link tag="li" to="/explore">
        Explore
      </router-link>

        login

        {{currentUser.displayName}}

            logout

</template>


Reorganise open production page

Reorganise open production page

<template>

      <spinner />

 0">

        {{ $t('productions.home.title') }}

          {{ $t('productions.home.create_new') }}

 0">

 4
        }"
      >

          <router-link
            :to="getPath(production)"
          >

                {{ generateAvatar(production) }}

              {{ production.name }}

          </router-link>

        {{ $t('productions.home.welcome') }}

          {{ $t('productions.home.empty') }}

          <button-simple
            class="level-item big-button"
            :text="$t('productions.home.create_new')"
            :is-responsive="false"
            @click="showNewModal"
          />

          {{ $t('productions.home.no_prod_for_client') }}

          {{ $t('productions.home.no_task') }}

    <edit-production-modal
      :active="modals.isNewDisplayed"
      :is-loading="editProduction.isLoading"
      :is-error="editProduction.isError"
      @confirm="confirmEditProduction"
      @cancel="hideNewModal"
    />

</template>

<template>

      <spinner />

 0">

        {{ $t('productions.home.title') }}

 4
        }"
      >

          <router-link
            :to="getPath(production)"
          >

                {{ generateAvatar(production) }}

              {{ production.name }}

          </router-link>
          <router-link
            class="secondary"
            :to="assetsPath(production)"
          >
            {{ $t('assets.title') }}
          </router-link>
          -
          <router-link
            class="secondary"
            :to="shotsPath(production)"
          >
            {{ $t('shots.title') }}
          </router-link>

          {{ $t('productions.home.create_new') }}

        {{ $t('productions.home.welcome') }}

          {{ $t('productions.home.empty') }}

          <button-simple
            class="level-item big-button"
            :text="$t('productions.home.create_new')"
            :is-responsive="false"
            @click="showNewModal"
          />

          {{ $t('productions.home.no_prod_for_client') }}

          {{ $t('productions.home.no_task') }}

    <edit-production-modal
      :active="modals.isNewDisplayed"
      :is-loading="editProduction.isLoading"
      :is-error="editProduction.isError"
      @confirm="confirmEditProduction"
      @cancel="hideNewModal"
    />

</template>


New tags display

New tags display

<template>

<router-link v-bind:to="'category/' + tag.slug"  class="card text-white bg-dark">{{tag.name}}</router-link>

</template>

<template>

    <router-link v-for="(tag, index) in tags" v-bind:key="index" v-bind:to="'category/' + tag.slug">{{tag.name}}</router-link>

</template>


fix: image event onLoad

fix: image event onLoad

<template>
  <uni-image v-on="$listeners">
    <div
      ref="content"
      :style="style"
    />
    <v-uni-resize-sensor
      v-if="mode === 'widthFix' || mode === 'heightFix'"
      ref="sensor"
      @resize="_fixSize()"
    />
  </uni-image>
</template>
<script>
function fixNumber (number) {
  // fix: 解决 Chrome 浏览器上某些情况下导致 1px 缝隙的问题
  if (typeof navigator && navigator.vendor === 'Google Inc.' && number > 10) {
    number = Math.round(number / 2) * 2
  }
  return number
}

export default {
  name: 'Image',
  props: {
    src: {
      type: String,
      default: ''
    },
    mode: {
      type: String,
      default: 'scaleToFill'
    },
    // TODO 懒加载
    lazyLoad: {
      type: [Boolean, String],
      default: false
    },
    draggable: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      originalWidth: 0,
      originalHeight: 0,
      originalStyle: { width: '', height: '' },
      contentPath: ''
    }
  },
  computed: {
    ratio () {
      return this.originalWidth && this.originalHeight ? this.originalWidth / this.originalHeight : 0
    },
    style () {
      let size = 'auto'
      let position = ''
      const repeat = 'no-repeat'

      switch (this.mode) {
        case 'aspectFit':
          size = 'contain'
          position = 'center center'
          break
        case 'aspectFill':
          size = 'cover'
          position = 'center center'
          break
        case 'widthFix':
        case 'heightFix':
          size = '100% 100%'
          break
        case 'top':
          position = 'center top'
          break
        case 'bottom':
          position = 'center bottom'
          break
        case 'center':
          position = 'center center'
          break
        case 'left':
          position = 'left center'
          break
        case 'right':
          position = 'right center'
          break
        case 'top left':
          position = 'left top'
          break
        case 'top right':
          position = 'right top'
          break
        case 'bottom left':
          position = 'left bottom'
          break
        case 'bottom right':
          position = 'right bottom'
          break
        default:
          size = '100% 100%'
          position = '0% 0%'
          break
      }

      return {
        'background-image': this.contentPath ? `url("${this.contentPath}")` : 'none',
        'background-position': position,
        'background-size': size,
        'background-repeat': repeat
      }
    }
  },
  watch: {
    src (newValue, oldValue) {
      this._loadImage()
    },
    mode (newValue, oldValue) {
      if (oldValue === 'widthFix' || oldValue === 'heightFix') {
        this._resetSize()
      }
      if (newValue === 'widthFix' || newValue === 'heightFix') {
        this._fixSize()
      }
    },
    contentPath (val) {
      if (!val && this.__img) {
        this.__img.remove()
        delete this.__img
      }
    }
  },
  mounted () {
    this.originalStyle.width = this.$el.style.width || ''
    this.originalStyle.height = this.$el.style.height || ''
    this._loadImage()
  },
  beforeDestroy () {
    this._clearImage()
  },
  methods: {
    _fixSize () {
      if (this.ratio) {
        const $el = this.$el
        if (this.mode === 'widthFix') {
          const width = $el.offsetWidth
          if (width) {
            $el.style.height = fixNumber(width / this.ratio) + 'px'
          }
        } else if (this.mode === 'heightFix') {
          const height = $el.offsetHeight
          if (height) {
            $el.style.width = fixNumber(height * this.ratio) + 'px'
          }
        }
      }
      window.dispatchEvent(new CustomEvent('updateview'))
    },
    _resetSize () {
      this.$el.style.width = this.originalStyle.width
      this.$el.style.height = this.originalStyle.height
    },
    _resetData () {
      this.originalWidth = 0
      this.originalHeight = 0
      this.contentPath = ''
    },
    _loadImage () {
      const realImagePath = this.$getRealPath(this.src)
      if (realImagePath) {
        const img = this._img = this._img || new Image()
        img.onload = $event => {
          this._img = null
          const width = this.originalWidth = img.width
          const height = this.originalHeight = img.height

          this._fixSize()

          this.contentPath = realImagePath
          img.draggable = this.draggable
          if (this.__img) {
            this.__img.remove()
          }
          this.__img = img
          this.$el.appendChild(img)

          this.$trigger('load', $event, {
            width,
            height
          })
        }
        img.onerror = $event => {
          this._img = null
          this._resetData()
          // 与微信小程序保持一致，保留之前样式
          // this._resetSize()
          this.$trigger('error', $event, {
            errMsg: `GET ${this.src} 404 (Not Found)`
          })
        }
        img.src = realImagePath
      } else {
        this._clearImage()
        this._resetData()
        // 与微信小程序保持一致，保留之前样式
        // this._resetSize()
      }
    },
    _clearImage () {
      const img = this._img
      if (img) {
        img.onload = null
        img.onerror = null
        this._img = null
      }
    }
  }
}
</script>
<style>
uni-image {
  width: 320px;
  height: 240px;
  display: inline-block;
  overflow: hidden;
  position: relative;
}

uni-image[hidden] {
  display: none;
}

uni-image>div {
  width: 100%;
  height: 100%;
}

uni-image>img {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
}

uni-image>.uni-image-will-change {
  will-change: transform;
}
</style>

<template>
  <uni-image v-on="$listeners">
    <div
      ref="content"
      :style="style"
    />
    <v-uni-resize-sensor
      v-if="mode === 'widthFix' || mode === 'heightFix'"
      ref="sensor"
      @resize="_fixSize()"
    />
  </uni-image>
</template>
<script>
function fixNumber (number) {
  // fix: 解决 Chrome 浏览器上某些情况下导致 1px 缝隙的问题
  if (typeof navigator && navigator.vendor === 'Google Inc.' && number > 10) {
    number = Math.round(number / 2) * 2
  }
  return number
}

export default {
  name: 'Image',
  props: {
    src: {
      type: String,
      default: ''
    },
    mode: {
      type: String,
      default: 'scaleToFill'
    },
    // TODO 懒加载
    lazyLoad: {
      type: [Boolean, String],
      default: false
    },
    draggable: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      originalWidth: 0,
      originalHeight: 0,
      originalStyle: { width: '', height: '' },
      contentPath: ''
    }
  },
  computed: {
    ratio () {
      return this.originalWidth && this.originalHeight ? this.originalWidth / this.originalHeight : 0
    },
    style () {
      let size = 'auto'
      let position = ''
      const repeat = 'no-repeat'

      switch (this.mode) {
        case 'aspectFit':
          size = 'contain'
          position = 'center center'
          break
        case 'aspectFill':
          size = 'cover'
          position = 'center center'
          break
        case 'widthFix':
        case 'heightFix':
          size = '100% 100%'
          break
        case 'top':
          position = 'center top'
          break
        case 'bottom':
          position = 'center bottom'
          break
        case 'center':
          position = 'center center'
          break
        case 'left':
          position = 'left center'
          break
        case 'right':
          position = 'right center'
          break
        case 'top left':
          position = 'left top'
          break
        case 'top right':
          position = 'right top'
          break
        case 'bottom left':
          position = 'left bottom'
          break
        case 'bottom right':
          position = 'right bottom'
          break
        default:
          size = '100% 100%'
          position = '0% 0%'
          break
      }

      return {
        'background-image': this.contentPath ? `url("${this.contentPath}")` : 'none',
        'background-position': position,
        'background-size': size,
        'background-repeat': repeat
      }
    }
  },
  watch: {
    src (newValue, oldValue) {
      this._loadImage()
    },
    mode (newValue, oldValue) {
      if (oldValue === 'widthFix' || oldValue === 'heightFix') {
        this._resetSize()
      }
      if (newValue === 'widthFix' || newValue === 'heightFix') {
        this._fixSize()
      }
    },
    contentPath (val) {
      if (!val && this.__img) {
        this.__img.remove()
        delete this.__img
      }
    }
  },
  mounted () {
    this.originalStyle.width = this.$el.style.width || ''
    this.originalStyle.height = this.$el.style.height || ''
    this._loadImage()
  },
  beforeDestroy () {
    this._clearImage()
  },
  methods: {
    _fixSize () {
      if (this.ratio) {
        const $el = this.$el
        if (this.mode === 'widthFix') {
          const width = $el.offsetWidth
          if (width) {
            $el.style.height = fixNumber(width / this.ratio) + 'px'
          }
        } else if (this.mode === 'heightFix') {
          const height = $el.offsetHeight
          if (height) {
            $el.style.width = fixNumber(height * this.ratio) + 'px'
          }
        }
      }
      window.dispatchEvent(new CustomEvent('updateview'))
    },
    _resetSize () {
      this.$el.style.width = this.originalStyle.width
      this.$el.style.height = this.originalStyle.height
    },
    _resetData () {
      this.originalWidth = 0
      this.originalHeight = 0
      this.contentPath = ''
    },
    _loadImage () {
      const realImagePath = this.$getRealPath(this.src)
      if (realImagePath) {
        const img = this._img = this._img || new Image()
        img.onload = $event => {
          this._img = null
          this.originalWidth = img.width
          this.originalHeight = img.height

          this._fixSize()

          this.contentPath = realImagePath
          img.draggable = this.draggable
          if (this.__img) {
            this.__img.remove()
          }
          this.__img = img
          this.$el.appendChild(img)

          this.$trigger('load', $event, {
            width: img.width,
            height: img.height
          })
        }
        img.onerror = $event => {
          this._img = null
          this._resetData()
          // 与微信小程序保持一致，保留之前样式
          // this._resetSize()
          this.$trigger('error', $event, {
            errMsg: `GET ${this.src} 404 (Not Found)`
          })
        }
        img.src = realImagePath
      } else {
        this._clearImage()
        this._resetData()
        // 与微信小程序保持一致，保留之前样式
        // this._resetSize()
      }
    },
    _clearImage () {
      const img = this._img
      if (img) {
        img.onload = null
        img.onerror = null
        this._img = null
      }
    }
  }
}
</script>
<style>
uni-image {
  width: 320px;
  height: 240px;
  display: inline-block;
  overflow: hidden;
  position: relative;
}

uni-image[hidden] {
  display: none;
}

uni-image>div {
  width: 100%;
  height: 100%;
}

uni-image>img {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
}

uni-image>.uni-image-will-change {
  will-change: transform;
}
</style>


Removed up vote button.

Removed up vote button.

<template xmlns="http://www.w3.org/1999/html">

 0">

{{$t('pages:detailCommitment.questions.title')}}

{{$t('common:question')}}
                {{question.question}}

{{question.description}}

</template>

<template xmlns="http://www.w3.org/1999/html">

 0">

{{$t('pages:detailCommitment.questions.title')}}

{{$t('common:question')}}
                {{question.question}}

{{question.description}}

                    <up-vote-menu :answer-id="question.commitmentAnswerId"
                                  :is-admin="question.isCreatedByUser" :up-voted-by-user="question.isUpVotedByUser"
                                  :number-of-up-votes="question.upVotes"
                                  @up-voted="upVote" @down-voted="downVote"
                                  :custom-text-has-up-voted="$t('pages:detailCommitment.menu.upVote.hasUpVoted',
                                  {question: `<span class='primary-title'>${question.question}</span>`})"
                                  :custom-text-has-not-up-voted="$t('pages:detailCommitment.menu.upVote.hasNotUpVoted',
                                  {question: `<span class='primary-title'>${question.question}</span>`})"
                                  :custom-text-is-admin="$t('pages:detailCommitment.menu.upVote.isAdmin',
                                  {question: `<span class='primary-title'>${question.question}</span>`})">

                            <v-icon medium class="action-icon">mdi-thumb-up</v-icon>
                            {{question.upVotes}}

                    </up-vote-menu>

</template>


Dangling comma

Dangling comma

<template>

        Announcements

 0" id="message_banner_location">

            {{ message }}

        New here?

          See MyUW at a glance

</template>


Implement deleting content nodes in layout mode

<template>
  <v-card outlined class="mt-3">
    <v-card-title hide-actions class="pa-0 pr-sm-2">
      <v-toolbar dense flat>
        <v-menu bottom
                right
                offset-y>
          <template #activator="{ on, attrs }">
            <v-btn icon v-bind="attrs" v-on="on">
              <v-icon>
                {{ currentIcon }}
              </v-icon>
            </v-btn>
          </template>
          <v-container class="grey lighten-5">
            <v-row v-for="(row, idx) in allowedIcons" :key="idx" no-gutters>
              <v-col v-for="(col, jdx) in row" :key="jdx">
                <v-btn icon
                       tile
                       :outlined="currentIcon === col"
                       class="ma-1"
                       @click="currentIcon = col">
                  <v-icon>{{ col }}</v-icon>
                </v-btn>
              </v-col>
            </v-row>
          </v-container>
        </v-menu>

        <div
          v-if="editInstanceName"
          style="flex: 1;"
          @click.stop
          @keyup.prevent>
          <api-text-field
            dense
            autofocus
            :auto-save="false"
            :uri="contentNode._meta.self"
            fieldname="instanceName"
            @finished="editInstanceName = false" />
        </div>
        <div v-else style="flex: 1;">
          <v-toolbar-title>
            {{ instanceOrContentTypeName }}
          </v-toolbar-title>
        </div>

        <v-menu v-if="!layoutMode"
                bottom
                left
                offset-y>
          <template #activator="{ on, attrs }">
            <v-btn icon v-bind="attrs" v-on="on">
              <v-icon>mdi-dots-vertical</v-icon>
            </v-btn>
          </template>
          <v-list>
            <v-list-item @click="toggleEditInstanceName">
              <v-list-item-icon>
                <v-icon>mdi-pencil</v-icon>
              </v-list-item-icon>
              <v-list-item-title>
                {{ $tc('components.activity.contentNode.editName') }}
              </v-list-item-title>
            </v-list-item>
          </v-list>
        </v-menu>
        <dialog-entity-delete
          v-else
          :entity="contentNode">
          <template #activator="{ on }">
            <v-btn icon
                   small
                   color="error"
                   class="float-right"
                   v-on="on">
              <v-icon>mdi-trash-can-outline</v-icon>
            </v-btn>
          </template>
        </dialog-entity-delete>
      </v-toolbar>
    </v-card-title>
    <v-card-text>
      <slot />
    </v-card-text>
  </v-card>
</template>

<script>

import camelCase from 'lodash/camelCase'
import ApiTextField from '../form/api/ApiTextField'
import DialogEntityDelete from '@/components/dialog/DialogEntityDelete'

export default {
  name: 'CardContentNode',
  components: {
    ApiTextField, DialogEntityDelete
  },
  props: {
    contentNode: { type: Object, required: true },
    layoutMode: { type: Boolean, required: true }
  },
  data () {
    return {
      editInstanceName: false,
      allowedIcons: [
        [
          'mdi-book-open-variant'
        ],
        [
          'mdi-script-text-outline',
          'mdi-timeline-text-outline',
          'mdi-weather-sunny',
          'mdi-weather-lightning-rainy',
          'mdi-gender-female',
          'mdi-gender-male'
        ],
        [
          'mdi-security',
          'mdi-hospital-box-outline'
        ]
      ],
      currentIcon: ''
    }
  },
  computed: {
    instanceOrContentTypeName () {
      if (this.contentNode.instanceName) {
        return this.contentNode.instanceName
      }
      return this.$tc(`contentNode.${camelCase(this.contentNode.contentTypeName)}.name`)
    }
  },
  mounted () {
    this.currentIcon = this.$tc(`contentNode.${camelCase(this.contentNode.contentTypeName)}.icon`)
  },
  methods: {
    toggleEditInstanceName (e) {
      this.editInstanceName = !this.editInstanceName
    }
  }
}
</script>

<style scoped>
</style>

<template>
  <v-card outlined class="mt-3">
    <v-card-title hide-actions class="pa-0 pr-sm-2">
      <v-toolbar dense flat>
        <v-menu bottom
                right
                offset-y>
          <template #activator="{ on, attrs }">
            <v-btn icon v-bind="attrs" v-on="on">
              <v-icon>
                {{ currentIcon }}
              </v-icon>
            </v-btn>
          </template>
          <v-container class="grey lighten-5">
            <v-row v-for="(row, idx) in allowedIcons" :key="idx" no-gutters>
              <v-col v-for="(col, jdx) in row" :key="jdx">
                <v-btn icon
                       tile
                       :outlined="currentIcon === col"
                       class="ma-1"
                       @click="currentIcon = col">
                  <v-icon>{{ col }}</v-icon>
                </v-btn>
              </v-col>
            </v-row>
          </v-container>
        </v-menu>

        <div
          v-if="editInstanceName"
          style="flex: 1;"
          @click.stop
          @keyup.prevent>
          <api-text-field
            dense
            autofocus
            :auto-save="false"
            :uri="contentNode._meta.self"
            fieldname="instanceName"
            @finished="editInstanceName = false" />
        </div>
        <div v-else style="flex: 1;">
          <v-toolbar-title>
            {{ instanceOrContentTypeName }}
          </v-toolbar-title>
        </div>

        <v-menu v-if="!layoutMode"
                bottom
                left
                offset-y>
          <template #activator="{ on, attrs }">
            <v-btn icon v-bind="attrs" v-on="on">
              <v-icon>mdi-dots-vertical</v-icon>
            </v-btn>
          </template>
          <v-list>
            <v-list-item @click="toggleEditInstanceName">
              <v-list-item-icon>
                <v-icon>mdi-pencil</v-icon>
              </v-list-item-icon>
              <v-list-item-title>
                {{ $tc('components.activity.contentNode.editName') }}
              </v-list-item-title>
            </v-list-item>
          </v-list>
        </v-menu>
      </v-toolbar>
    </v-card-title>
    <v-card-text>
      <slot />
    </v-card-text>
  </v-card>
</template>

<script>

import camelCase from 'lodash/camelCase'
import ApiTextField from '../form/api/ApiTextField'

export default {
  name: 'CardContentNode',
  components: {
    ApiTextField
  },
  props: {
    contentNode: { type: Object, required: true },
    layoutMode: { type: Boolean, required: true }
  },
  data () {
    return {
      editInstanceName: false,
      allowedIcons: [
        [
          'mdi-book-open-variant'
        ],
        [
          'mdi-script-text-outline',
          'mdi-timeline-text-outline',
          'mdi-weather-sunny',
          'mdi-weather-lightning-rainy',
          'mdi-gender-female',
          'mdi-gender-male'
        ],
        [
          'mdi-security',
          'mdi-hospital-box-outline'
        ]
      ],
      currentIcon: ''
    }
  },
  computed: {
    instanceOrContentTypeName () {
      if (this.contentNode.instanceName) {
        return this.contentNode.instanceName
      }
      return this.$tc(`contentNode.${camelCase(this.contentNode.contentTypeName)}.name`)
    }
  },
  mounted () {
    this.currentIcon = this.$tc(`contentNode.${camelCase(this.contentNode.contentTypeName)}.icon`)
  },
  methods: {
    toggleEditInstanceName (e) {
      this.editInstanceName = !this.editInstanceName
    }
  }
}
</script>

<style scoped>
</style>