Emit the same event when we know that the status might be changed

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

<!--
  - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
  -
  - @author Marco Ambrosini <marcoambrosini@pm.me>
  -
  - @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>
	<div>
		<SearchBox
			v-if="displaySearchBox"
			v-model="searchText"
			:placeholder-text="t('spreed', 'Add participants to the conversation')"
			:is-searching="isSearching"
			@input="handleInput"
			@abort-search="abortSearch" />
		<Caption v-if="isSearching"
			:title="t('spreed', 'Participants')" />
		<CurrentParticipants
			:search-text="searchText"
			:participants-initialised="participantsInitialised" />
		<ParticipantsSearchResults
			v-if="isSearching"
			:search-results="searchResults"
			:contacts-loading="contactsLoading"
			:no-results="noResults"
			@click="addParticipants" />
	</div>
</template>

<script>
import Caption from '../../Caption'
import CurrentParticipants from './CurrentParticipants/CurrentParticipants'
import SearchBox from '../../LeftSidebar/SearchBox/SearchBox'
import debounce from 'debounce'
import { EventBus } from '../../../services/EventBus'
import { CONVERSATION, PARTICIPANT, WEBINAR } from '../../../constants'
import { searchPossibleConversations } from '../../../services/conversationsService'
import {
	addParticipant,
	fetchParticipants,
} from '../../../services/participantsService'
import isInLobby from '../../../mixins/isInLobby'
import { loadState } from '@nextcloud/initial-state'
import SHA1 from 'crypto-js/sha1'
import Hex from 'crypto-js/enc-hex'
import CancelableRequest from '../../../utils/cancelableRequest'
import Axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import ParticipantsSearchResults from './ParticipantsSearchResults/ParticipantsSearchResults'

export default {
	name: 'ParticipantsTab',
	components: {
		CurrentParticipants,
		SearchBox,
		Caption,
		ParticipantsSearchResults,
	},

	mixins: [
		isInLobby,
	],

	props: {
		displaySearchBox: {
			type: Boolean,
			required: true,
		},
	},

	data() {
		return {
			searchText: '',
			searchResults: [],
			contactsLoading: false,
			participantsInitialised: false,
			isCirclesEnabled: loadState('talk', 'circles_enabled'),
			/**
			 * Stores the cancel function for cancelableGetParticipants
			 */
			cancelGetParticipants: () => {},
			fetchingParticipants: false,
		}
	},

	computed: {
		show() {
			return this.$store.getters.getSidebarStatus
		},
		opened() {
			return !!this.token && this.show
		},
		token() {
			return this.$store.getters.getToken()
		},
		conversation() {
			if (this.$store.getters.conversation(this.token)) {
				return this.$store.getters.conversation(this.token)
			}
			return {
				token: '',
				displayName: '',
				isFavorite: false,
				type: CONVERSATION.TYPE.PUBLIC,
				lobbyState: WEBINAR.LOBBY.NONE,
			}
		},
		isSearching() {
			return this.searchText !== ''
		},
		noResults() {
			return this.searchResults === []
		},
	},

	beforeMount() {
		EventBus.$on('routeChange', this.onRouteChange)
		EventBus.$on('joinedConversation', this.onJoinedConversation)

		// FIXME this works only temporary until signaling is fixed to be only on the calls
		// Then we have to search for another solution. Maybe the room list which we update
		// periodically gets a hash of all online sessions?
		EventBus.$on('Signaling::participantListChanged', this.debounceUpdateParticipants)
	},

	beforeDestroy() {
		EventBus.$off('routeChange', this.onRouteChange)
		EventBus.$off('joinedConversation', this.onJoinedConversation)
		EventBus.$off('Signaling::participantListChanged', this.debounceUpdateParticipants)
	},

	methods: {
		/**
		 * If the route changes, the search filter is reset
		 */
		onRouteChange() {
			// Reset participantsInitialised when there is only the current user in the participant list
			this.participantsInitialised = this.$store.getters.participantsList(this.token).length > 1
			this.searchText = ''
		},

		/**
		 * If the conversation has been joined, we get the participants
		 */
		onJoinedConversation() {
			this.$nextTick(() => {
				this.cancelableGetParticipants()
			})
		},

		handleClose() {
			this.$store.dispatch('hideSidebar')
		},
		handleInput() {
			this.contactsLoading = true
			this.searchResults = []
			this.debounceFetchSearchResults()
		},

		debounceFetchSearchResults: debounce(function() {
			if (this.isSearching) {
				this.fetchSearchResults()
			}
		}, 250),

		debounceUpdateParticipants: debounce(function() {
			if (!this.fetchingParticipants) {
				this.cancelableGetParticipants()
			}
		}, 2000),

		async fetchSearchResults() {
			try {
				const response = await searchPossibleConversations(this.searchText, this.token)
				this.searchResults = response.data.ocs.data
				this.contactsLoading = false
			} catch (exception) {
				console.error(exception)
				showError(t('spreed', 'An error occurred while performing the search'))
			}
		},

		/**
		 * Add the selected group/user/circle to the conversation
		 * @param {Object} item The autocomplete suggestion to start a conversation with
		 * @param {string} item.id The ID of the target
		 * @param {string} item.source The source of the target
		 */
		async addParticipants(item) {
			try {
				await addParticipant(this.token, item.id, item.source)
				this.searchText = ''
				this.cancelableGetParticipants()
			} catch (exception) {
				console.debug(exception)
			}
		},

		async cancelableGetParticipants() {
			if (this.token === '' || this.isInLobby) {
				return
			}

			try {
				// The token must be stored in a local variable to ensure that
				// the same token is used after waiting.
				const token = this.token
				// Clear previous requests if there's one pending
				this.cancelGetParticipants('Cancel get participants')
				// Get a new cancelable request function and cancel function pair
				this.fetchingParticipants = true
				const { request, cancel } = CancelableRequest(fetchParticipants)
				this.cancelGetParticipants = cancel
				const participants = await request(token)
				this.$store.dispatch('purgeParticipantsStore', token)

				const hasUserStatuses = !!participants.headers['x-nextcloud-has-user-statuses']
				participants.data.ocs.data.forEach(participant => {
					this.$store.dispatch('addParticipant', {
						token: token,
						participant,
					})
					if (participant.participantType === PARTICIPANT.TYPE.GUEST
						|| participant.participantType === PARTICIPANT.TYPE.GUEST_MODERATOR) {
						this.$store.dispatch('forceGuestName', {
							token: token,
							actorId: Hex.stringify(SHA1(participant.sessionId)),
							actorDisplayName: participant.displayName,
						})
					} else if (hasUserStatuses) {
						emit('user_status:status.updated', {
							status: participant.status,
							message: participant.statusMessage,
							icon: participant.statusIcon,
							clearAt: participant.statusClearAt,
							userId: participant.userId,
						})
					}
				})
				this.participantsInitialised = true
			} catch (exception) {
				if (!Axios.isCancel(exception)) {
					console.error(exception)
					showError(t('spreed', 'An error occurred while fetching the participants'))
				}
			} finally {
				this.fetchingParticipants = false
			}
		},

		// Ends the search operation
		abortSearch() {
			this.searchText = ''
		},
	},
}
</script>

<style scoped>

/** TODO: fix these in the nextcloud-vue library **/

::v-deep .app-sidebar-header__menu {
	top: 6px !important;
	margin-top: 0 !important;
	right: 54px !important;
}

::v-deep .app-sidebar__close {
	top: 6px !important;
	right: 6px !important;
}

</style>

<!--
  - @copyright Copyright (c) 2019 Marco Ambrosini <marcoambrosini@pm.me>
  -
  - @author Marco Ambrosini <marcoambrosini@pm.me>
  -
  - @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>
	<div>
		<SearchBox
			v-if="displaySearchBox"
			v-model="searchText"
			:placeholder-text="t('spreed', 'Add participants to the conversation')"
			:is-searching="isSearching"
			@input="handleInput"
			@abort-search="abortSearch" />
		<Caption v-if="isSearching"
			:title="t('spreed', 'Participants')" />
		<CurrentParticipants
			:search-text="searchText"
			:participants-initialised="participantsInitialised" />
		<ParticipantsSearchResults
			v-if="isSearching"
			:search-results="searchResults"
			:contacts-loading="contactsLoading"
			:no-results="noResults"
			@click="addParticipants" />
	</div>
</template>

<script>
import Caption from '../../Caption'
import CurrentParticipants from './CurrentParticipants/CurrentParticipants'
import SearchBox from '../../LeftSidebar/SearchBox/SearchBox'
import debounce from 'debounce'
import { EventBus } from '../../../services/EventBus'
import { CONVERSATION, PARTICIPANT, WEBINAR } from '../../../constants'
import { searchPossibleConversations } from '../../../services/conversationsService'
import {
	addParticipant,
	fetchParticipants,
} from '../../../services/participantsService'
import isInLobby from '../../../mixins/isInLobby'
import { loadState } from '@nextcloud/initial-state'
import SHA1 from 'crypto-js/sha1'
import Hex from 'crypto-js/enc-hex'
import CancelableRequest from '../../../utils/cancelableRequest'
import Axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import ParticipantsSearchResults from './ParticipantsSearchResults/ParticipantsSearchResults'

export default {
	name: 'ParticipantsTab',
	components: {
		CurrentParticipants,
		SearchBox,
		Caption,
		ParticipantsSearchResults,
	},

	mixins: [
		isInLobby,
	],

	props: {
		displaySearchBox: {
			type: Boolean,
			required: true,
		},
	},

	data() {
		return {
			searchText: '',
			searchResults: [],
			contactsLoading: false,
			participantsInitialised: false,
			isCirclesEnabled: loadState('talk', 'circles_enabled'),
			/**
			 * Stores the cancel function for cancelableGetParticipants
			 */
			cancelGetParticipants: () => {},
			fetchingParticipants: false,
		}
	},

	computed: {
		show() {
			return this.$store.getters.getSidebarStatus
		},
		opened() {
			return !!this.token && this.show
		},
		token() {
			return this.$store.getters.getToken()
		},
		conversation() {
			if (this.$store.getters.conversation(this.token)) {
				return this.$store.getters.conversation(this.token)
			}
			return {
				token: '',
				displayName: '',
				isFavorite: false,
				type: CONVERSATION.TYPE.PUBLIC,
				lobbyState: WEBINAR.LOBBY.NONE,
			}
		},
		isSearching() {
			return this.searchText !== ''
		},
		noResults() {
			return this.searchResults === []
		},
	},

	beforeMount() {
		EventBus.$on('routeChange', this.onRouteChange)
		EventBus.$on('joinedConversation', this.onJoinedConversation)

		// FIXME this works only temporary until signaling is fixed to be only on the calls
		// Then we have to search for another solution. Maybe the room list which we update
		// periodically gets a hash of all online sessions?
		EventBus.$on('Signaling::participantListChanged', this.debounceUpdateParticipants)
	},

	beforeDestroy() {
		EventBus.$off('routeChange', this.onRouteChange)
		EventBus.$off('joinedConversation', this.onJoinedConversation)
		EventBus.$off('Signaling::participantListChanged', this.debounceUpdateParticipants)
	},

	methods: {
		/**
		 * If the route changes, the search filter is reset
		 */
		onRouteChange() {
			// Reset participantsInitialised when there is only the current user in the participant list
			this.participantsInitialised = this.$store.getters.participantsList(this.token).length > 1
			this.searchText = ''
		},

		/**
		 * If the conversation has been joined, we get the participants
		 */
		onJoinedConversation() {
			this.$nextTick(() => {
				this.cancelableGetParticipants()
			})
		},

		handleClose() {
			this.$store.dispatch('hideSidebar')
		},
		handleInput() {
			this.contactsLoading = true
			this.searchResults = []
			this.debounceFetchSearchResults()
		},

		debounceFetchSearchResults: debounce(function() {
			if (this.isSearching) {
				this.fetchSearchResults()
			}
		}, 250),

		debounceUpdateParticipants: debounce(function() {
			if (!this.fetchingParticipants) {
				this.cancelableGetParticipants()
			}
		}, 2000),

		async fetchSearchResults() {
			try {
				const response = await searchPossibleConversations(this.searchText, this.token)
				this.searchResults = response.data.ocs.data
				this.contactsLoading = false
			} catch (exception) {
				console.error(exception)
				showError(t('spreed', 'An error occurred while performing the search'))
			}
		},

		/**
		 * Add the selected group/user/circle to the conversation
		 * @param {Object} item The autocomplete suggestion to start a conversation with
		 * @param {string} item.id The ID of the target
		 * @param {string} item.source The source of the target
		 */
		async addParticipants(item) {
			try {
				await addParticipant(this.token, item.id, item.source)
				this.searchText = ''
				this.cancelableGetParticipants()
			} catch (exception) {
				console.debug(exception)
			}
		},

		async cancelableGetParticipants() {
			if (this.token === '' || this.isInLobby) {
				return
			}

			try {
				// The token must be stored in a local variable to ensure that
				// the same token is used after waiting.
				const token = this.token
				// Clear previous requests if there's one pending
				this.cancelGetParticipants('Cancel get participants')
				// Get a new cancelable request function and cancel function pair
				this.fetchingParticipants = true
				const { request, cancel } = CancelableRequest(fetchParticipants)
				this.cancelGetParticipants = cancel
				const participants = await request(token)
				this.$store.dispatch('purgeParticipantsStore', token)
				participants.data.ocs.data.forEach(participant => {
					this.$store.dispatch('addParticipant', {
						token: token,
						participant,
					})
					if (participant.participantType === PARTICIPANT.TYPE.GUEST
						|| participant.participantType === PARTICIPANT.TYPE.GUEST_MODERATOR) {
						this.$store.dispatch('forceGuestName', {
							token: token,
							actorId: Hex.stringify(SHA1(participant.sessionId)),
							actorDisplayName: participant.displayName,
						})
					}
				})
				this.participantsInitialised = true
			} catch (exception) {
				if (!Axios.isCancel(exception)) {
					console.error(exception)
					showError(t('spreed', 'An error occurred while fetching the participants'))
				}
			} finally {
				this.fetchingParticipants = false
			}
		},

		// Ends the search operation
		abortSearch() {
			this.searchText = ''
		},
	},
}
</script>

<style scoped>

/** TODO: fix these in the nextcloud-vue library **/

::v-deep .app-sidebar-header__menu {
	top: 6px !important;
	margin-top: 0 !important;
	right: 54px !important;
}

::v-deep .app-sidebar__close {
	top: 6px !important;
	right: 6px !important;
}

</style>


Fixed #244 -- Vue.js - checkbox with same value on different pages be checked

<template>
    <form :class="css.checkbox.root">

                <input type="checkbox" :name="question.name" :value="item.value" v-model="selectedValues" :id="question.inputId + '_' + item.value" :disabled="!isEditMode" />
                {{item.text}}

        <survey-other-choice v-show="question.hasOther && isOtherSelected" :class="css.radiogroup.other" :question="question" :isEditMode="isEditMode" :css="css"/>
    </form>
</template>


Fix joint course student display

Fix joint course student display

<template>

      Table of Student Information

        <template #cell(email)="data">

            {{ data.value.email }}

        </template>

</template>


border fix

border fix

<template>

    <input
      v-model="amount"
      ref="amount"
      type="number"
      step="0.01"
      placeholder="0.00"
      :class="amountClass"
      min="0"
      :max="maxAmount"
      @change="isAmountInvalid = false"
    />
    <input
      v-model="address"
      type="text"
      :placeholder="$t('send_gulden.enter_gulden_address')"
      :class="addressClass"
      @keydown="isAddressInvalid = false"
    />
    <input
      v-model="password"
      type="password"
      v-show="walletPassword === null"
      :placeholder="$t('common.enter_your_password')"
      :class="passwordClass"
      @keydown="onPasswordKeydown"
    />

      <button @click="clearInput" class="clear" :disabled="disableClearButton">
        {{ $t("buttons.clear") }}
      </button>
      <button
        @click="showConfirmation"
        class="send"
        :disabled="disableSendButton"
      >
        {{ $t("buttons.send") }}
      </button>

</template>


Fix ChromeLayout maximize screen button for Safari (#176)

<template>

            {{ title }}

      <slot />

</template>


Update form-file.vue

Update form-file.vue

<template>

        <input type="file"
               :id="id"
               class="custom-file-input"
               @change="onFileChange"
               :multiple="multiple"
               :webkitdirectory="directory"
               ref="input"
        >

</template>


[Collapse] Remove the height style property instead of setting it to zero, this fixes issues when having frequent window width changes.

<template>
    <transition @enter="enter" @leave="leave" name="collapse">
        <div :class="classObject" v-show="show">
            <slot></slot>
        </div>
    </transition>
</template>

<style scoped>
    .collapse-enter-active, .collapse-leave-active {
        transition: all .35s ease;
        overflow: hidden;
    }

    .collapse-enter, .collapse-leave-to {
        height: 0;
    }
</style>

<script>
    export default {

        data() {
            return {
                show: false
            };
        },

        computed: {
            classObject() {
                return {
                    'navbar-collapse': this.isNav,
                    show: this.show
                };
            }
        },

        props: {
            isNav: {
                type: Boolean,
                default: false
            },
            id: {
                type: String,
                required: true
            }
        },

        methods: {
            toggle() {
                this.show = !this.show;
            },
            enter(el) {
                let height = 0;
                Array.prototype.forEach.call(el.children, c => {
                    height += parseInt(getComputedStyle(c).height, 10);
                });
                el.style.height = `${height}px`;
            },
            leave(el) {
                el.style.height = null;
            }
        },

        created() {
            this.$root.$on('collapse::toggle', target => {
                if (target !== this.id) {
                    return;
                }
                this.toggle();
            });
        }
    };

</script>

<template>
    <transition @enter="enter" @leave="leave" name="collapse">
        <div :class="classObject" v-show="show">
            <slot></slot>
        </div>
    </transition>
</template>

<style scoped>
    .collapse-enter-active, .collapse-leave-active {
        transition: all .35s ease;
        overflow: hidden;
    }

    .collapse-enter, .collapse-leave-to {
        height: 0;
    }
</style>

<script>
    export default {

        data() {
            return {
                show: false
            };
        },

        computed: {
            classObject() {
                return {
                    'navbar-collapse': this.isNav,
                    show: this.show
                };
            }
        },

        props: {
            isNav: {
                type: Boolean,
                default: false
            },
            id: {
                type: String,
                required: true
            }
        },

        methods: {
            toggle() {
                this.show = !this.show;
            },
            enter(el) {
                let height = 0;
                Array.prototype.forEach.call(el.children, c => {
                    height += parseInt(getComputedStyle(c).height, 10);
                });
                el.style.height = `${height}px`;
            },
            leave(el) {
                el.style.height = 0;
            }
        },

        created() {
            this.$root.$on('collapse::toggle', target => {
                if (target !== this.id) {
                    return;
                }
                this.toggle();
            });
        }
    };

</script>


fix(dropdown): hover/focus shading for active items

<template>

            <slot name="button-content"><slot name="text">{{text}}</slot></slot>

            {{toggleText}}

            <slot></slot>

</template>


Clean up

Clean up

<template>

	<transition name="nav-back">

	</transition>

	<transition name="nav">

				<button class="item _button account" @click="openAccountMenu">
					<MkAvatar :user="$store.state.i" class="avatar"/><MkAcct class="text" :user="$store.state.i"/>
				</button>
				<MkA class="item index" active-class="active" to="/" exact v-else>
					<Fa :icon="faHome" fixed-width/>{{ $t('timeline') }}
				</MkA>
				<template v-for="item in menu">

					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to">
						<Fa :icon="menuDef[item].icon" fixed-width/>{{ $t(menuDef[item].title) }}
						<Fa :icon="faCircle"/>
					</component>
				</template>

				<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.state.i.isAdmin || $store.state.i.isModerator" @click="oepnInstanceMenu">
					<Fa :icon="faServer" fixed-width/>{{ $t('instance') }}
				</button>
				<button class="item _button" @click="more">
					<Fa :icon="faEllipsisH" fixed-width/>{{ $t('more') }}
					<Fa :icon="faCircle"/>
				</button>
				<MkA class="item" active-class="active" to="/settings">
					<Fa :icon="faCog" fixed-width/>{{ $t('settings') }}
				</MkA>

	</transition>

</template>

<template>

	<transition name="nav-back">

	</transition>

	<transition name="nav">

				<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
					<MkAvatar :user="$store.state.i" class="avatar"/><MkAcct class="text" :user="$store.state.i"/>
				</button>
				<button class="item _button index active" @click="top()" v-if="$route.name === 'index'">
					<Fa :icon="faHome" fixed-width/>{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}
				</button>
				<MkA class="item index" active-class="active" to="/" exact v-else>
					<Fa :icon="faHome" fixed-width/>{{ $store.getters.isSignedIn ? $t('timeline') : $t('home') }}
				</MkA>
				<template v-for="item in menu">

					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to">
						<Fa :icon="menuDef[item].icon" fixed-width/>{{ $t(menuDef[item].title) }}
						<Fa :icon="faCircle"/>
					</component>
				</template>

				<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu">
					<Fa :icon="faServer" fixed-width/>{{ $t('instance') }}
				</button>
				<button class="item _button" @click="more">
					<Fa :icon="faEllipsisH" fixed-width/>{{ $t('more') }}
					<Fa :icon="faCircle"/>
				</button>
				<MkA class="item" active-class="active" to="/settings">
					<Fa :icon="faCog" fixed-width/>{{ $t('settings') }}
				</MkA>

	</transition>

</template>


preserve class of first and last item on infinity

<template lang="pug">
  div(:class="$style.container", v-touch="{methods: true}")
    ol.list-unstyled(:class="[$style.list, {[$style.transition]: transition}]",
    :style="{transform}", ref="list", @transitionend="transitionEnd")
      li(v-if="lastItem", :class="Array.from(lastItem.classList)", v-html="lastItem.innerHTML")
      slot
      li(v-if="firstItem", :class="Array.from(firstItem.classList)", v-html="firstItem.innerHTML")
</template>
<script>
  import {intervalVal} from 'utils'

  export default {
    name: 'HiSwiper',
    props: {
      infinity: {
        type: Boolean,
        default: false
      },
      threshold: {
        type: Number,
        default: 20
      }
    },
    data() {
      return {
        translateX: 0,
        translateStart: 0,
        transition: false,
        index: 0,
        width: 0,
        size: 0,
        firstItem: null,
        lastItem: null
      }
    },
    computed: {
      transform() {
        return `translate3d(${this.translateX}px,0,0)`
      },
      indexTranslateX() {
        return -this.index * this.width
      }
    },
    mounted() {
      const {infinity} = this
      const {list} = this.$refs
      this.index = +infinity
      this.width = list.clientWidth
      this.resetTranslateX()

      const {children} = list

      const size = children.length

      this.size = infinity ? size + 2 : size

      if (!infinity) return

      this.firstItem = children[0]
      this.lastItem = children[size - 1]
    },
    methods: {
      resetTranslateX() {
        this.translateX = this.indexTranslateX
      },
      moveStart() {
        this.transition = false
        this.translateStart = this.translateX
      },
      moving(e) {
        this.translateX = this.translateStart + e.changedX
      },
      moveEnd() {
        const {indexTranslateX, threshold, translateX} = this

        if (indexTranslateX === translateX) return

        let {index} = this

        if (indexTranslateX - threshold > translateX) {
          index++
        } else if (indexTranslateX + threshold < translateX) {
          index--
        }

        this.index = intervalVal(0, this.size - 1, index)
        this.transition = true
        this.resetTranslateX()
      },
      transitionEnd() {
        this.transition = false

        if (!this.infinity) return

        let {index, size} = this

        if (!index) {
          index = size - 2
        } else if (index === size - 1) {
          index = 1
        }

        this.index = index
        this.resetTranslateX()
      }
    }
  }
</script>
<style lang="stylus" module>
  .container
    max-width 100%
    overflow hidden

  .list
    font-size 0
    white-space nowrap

    &.transition
      transition transform .3s

    > li
      display inline-block
      width 100%
</style>

<template lang="pug">
  div(:class="$style.container", v-touch="{methods: true}")
    ol.list-unstyled(:class="[$style.list, {[$style.transition]: transition}]",
    :style="{transform}", ref="list", @transitionend="transitionEnd")
      li(v-if="firstItem", v-html="firstItem")
      slot
      li(v-if="lastItem", v-html="lastItem")
</template>
<script>
  import {intervalVal} from 'utils'

  export default {
    name: 'HiSwiper',
    props: {
      infinity: {
        type: Boolean,
        default: false
      },
      threshold: {
        type: Number,
        default: 20
      }
    },
    data() {
      return {
        translateX: 0,
        translateStart: 0,
        transition: false,
        index: 0,
        width: 0,
        size: 0,
        firstItem: null,
        lastItem: null
      }
    },
    computed: {
      transform() {
        return `translate3d(${this.translateX}px,0,0)`
      },
      indexTranslateX() {
        return -this.index * this.width
      }
    },
    mounted() {
      const {infinity} = this
      const {list} = this.$refs
      this.index = +infinity
      this.width = list.clientWidth
      this.resetTranslateX()

      const {children} = list

      const size = children.length

      this.size = infinity ? size + 2 : size

      if (!infinity) return

      this.firstItem = children[size - 1].innerHTML
      this.lastItem = children[0].innerHTML
    },
    methods: {
      resetTranslateX() {
        this.translateX = this.indexTranslateX
      },
      moveStart() {
        this.transition = false
        this.translateStart = this.translateX
      },
      moving(e) {
        this.translateX = this.translateStart + e.changedX
      },
      moveEnd() {
        const {indexTranslateX, threshold, translateX} = this

        if (indexTranslateX === translateX) return

        let {index} = this

        if (indexTranslateX - threshold > translateX) {
          index++
        } else if (indexTranslateX + threshold < translateX) {
          index--
        }

        this.index = intervalVal(0, this.size - 1, index)
        this.transition = true
        this.resetTranslateX()
      },
      transitionEnd() {
        this.transition = false

        if (!this.infinity) return

        let {index, size} = this

        if (!index) {
          index = size - 2
        } else if (index === size - 1) {
          index = 1
        }

        this.index = index
        this.resetTranslateX()
      }
    }
  }
</script>
<style lang="stylus" module>
  .container
    max-width 100%
    overflow hidden

  .list
    font-size 0
    white-space nowrap

    &.transition
      transition transform .3s

    > li
      display inline-block
      width 100%
</style>


update footer

update footer

<template>

karrot {{ release.name }}

made with  by foodsaving worldwide

contribute on  GitHub

join our official group on  Facebook

        karrot@foodsaving.world

</template>

<template>

karrot {{ release.name }}

made with  by yunity

contribute on  GitHub

join our official group on  Facebook

        fstool@yunity.org

</template>


Hide capstone share modal for teachers (#80)

<template>
  <base-modal
    v-if="!goToNextDirectly && !showCharCx"
    class="victory-modal"
  >
    <template #header>

         {{ contentType }} {{ $t("common.complete") }}
         {{ contentName }}

    </template>

    <template #body>

         {{ $t("play_level.learning_goals") }}:
          {{ learningGoals }}

         {{ $t("play_level.share_your_project") }}
         {{ $t("play_level.keep_editing_your_project") }}
        <input
          ref="share-text-box"
          readonly="true"
          type="text"
          class="ozaria-secondary-button share-text-box"
          :value="shareURL"
        >

          <button
            class="copy-url-button ozaria-button ozaria-primary-button"
            @click="copyUrl"
          >
            {{ $t("play_level.copy_url") }}
          </button>

    </template>

    <template #footer>

        <button
          v-if="shareModal"
          class="continue-editing-button ozaria-button ozaria-secondary-button"
          data-dismiss="modal"
          @click="continueEditingButtonClick"
        >
          {{ $t("common.continue_editing") }}
        </button>
        <button
          v-else
          class="replay-button ozaria-button ozaria-secondary-button"
          data-dismiss="modal"
          @click="replayButtonClick"
        >
          {{ $t("common.replay") }}
        </button>
        <button
          class="next-button ozaria-button ozaria-primary-button"
          @click="nextButtonClick"
        >
          {{ $t("common.next") }}
        </button>

    </template>
  </base-modal>
  <modal-char-customization
    v-else-if="showCharCx"
    class="char-cx-modal"
    @saved="onCharCxSaved"
  />
</template>


Focus editor after image insert

Focus editor after image insert

<template>

        <button class="bold small" @click="bolden" title="Bold">B</button>
        <button class="italic small" @click="pizzaparty" title="Italic">I</button>
        <button class="small" @click="toggleYoutubeEmbed" title="YouTube">YT</button>
        <button class="small" @click="insertPictureGrid" title="Picture Grid">PG</button>
        <input type="checkbox" v-model="autoscroll">autoscroll

        <button class="help bold small" @click="toggleClippy">?</button>

    <textarea
      id="markdown-editor"
      name="markdown-editor"
      v-model="markdown"
      @input="change"
      ref="md"
      @keydown="keydown"
      @scroll="scroll"
    ></textarea>

Tue dr iFrame Embed Code hie drii:

      <input type="text" v-model="youtubeEmbed" id="youtube-embed" >

        <button class="small" @click="toggleYoutubeEmbed">Doch nid</button>
        <button class="small" @click="youtubeInsert">Guet</button>

</template>


Person don't change when clicking on other avatar

In person page you cannot navigate on another person page because the route
don't lead to a new page.

<template>

        <people-avatar
          :person="person"
          :size="80"
          :font-size="30"
          :is-link="false"
        ></people-avatar>

        <page-title :text="person ? person.name : ''"></page-title>

    <page-subtitle :text="$t('people.running_tasks')"></page-subtitle>
    <search-field
      ref="person-tasks-search-field"
      @change="onSearchChange"
    >
    </search-field>
    <todos-list
      :entries="displayedPersonTasks"
      :is-loading="isTasksLoading"
      :is-error="isTasksLoadingError"
    ></todos-list>

</template>


Fix spacing in select component

Fix spacing in select component

<template>

    <input class='input' type='text' readonly
      :placeholder='label'
      :value='text'
      v-bind='attributes'
      @focus='toggle(true)'
      @blur='toggle(false)'>
    {{ label }}

arrow_drop_down

        {{ option.label }}

</template>


update to 1.0.0

update to 1.0.0

<template>

      <slot></slot>

</template>

<template>

      <slot></slot>

</template>


docs: add flex-shrink to ad container

docs: add flex-shrink to ad container

<template>
  <v-flex id="ad__container" shrink>
    <v-system-bar
      v-if="viewport === 'xs' && shouldShowAd"
      v-show="$vuetify.breakpoint.xsOnly"
      :app="$vuetify.breakpoint.xsOnly"
      class="white"
      height="40"
    >
      <div :key="`${path}-xs`" :id="barId" />
    </v-system-bar>

    <v-bottom-nav
      v-else-if="viewport === 'sm' && shouldShowAd"
      :value="$vuetify.breakpoint.smOnly"
      app
      height="48"
    >
      <div :key="`${path}-sm`" :id="botId" />
    </v-bottom-nav>

    <v-navigation-drawer
      v-else-if="viewport === 'md' && shouldShowAd"
      :value="$vuetify.breakpoint.mdAndUp"
      app
      class="transparent pa-3"
      clipped
      floating
      right
      stateless
      width="192"
    >
      <v-layout
        column
        fill-height
      >
        <v-flex shrink mb-3>
          <no-ssr>
            <core-table-of-contents
              :offset="85"
              :threshold="50"
            />
          </no-ssr>
        </v-flex>

        <v-flex
          xs12
          text-xs-center
        >

Diamond Sponsors

                <v-img
                  :src="`https://cdn.vuetifyjs.com/images/${diamond.logo}`"
                  :alt="diamond.Name"
                  contain
                  height="30"
                />

          <misc-sponsor-btn small />
        </v-flex>

        <v-flex
          :key="`${path}-md`"
          :id="navId"
          mt-auto
          style="margin-bottom: 72px;"
        />
      </v-layout>
    </v-navigation-drawer>
  </v-flex>
</template>

<template>
  <v-flex id="ad__container">
    <v-system-bar
      v-if="viewport === 'xs' && shouldShowAd"
      v-show="$vuetify.breakpoint.xsOnly"
      :app="$vuetify.breakpoint.xsOnly"
      class="white"
      height="40"
    >
      <div :key="`${path}-xs`" :id="barId" />
    </v-system-bar>

    <v-bottom-nav
      v-else-if="viewport === 'sm' && shouldShowAd"
      :value="$vuetify.breakpoint.smOnly"
      app
      height="48"
    >
      <div :key="`${path}-sm`" :id="botId" />
    </v-bottom-nav>

    <v-navigation-drawer
      v-else-if="viewport === 'md' && shouldShowAd"
      :value="$vuetify.breakpoint.mdAndUp"
      app
      class="transparent pa-3"
      clipped
      floating
      right
      stateless
      width="192"
    >
      <v-layout
        column
        fill-height
      >
        <v-flex shrink mb-3>
          <no-ssr>
            <core-table-of-contents
              :offset="85"
              :threshold="50"
            />
          </no-ssr>
        </v-flex>

        <v-flex
          xs12
          text-xs-center
        >

Diamond Sponsors

                <v-img
                  :src="`https://cdn.vuetifyjs.com/images/${diamond.logo}`"
                  :alt="diamond.Name"
                  contain
                  height="30"
                />

          <misc-sponsor-btn small />
        </v-flex>

        <v-flex
          :key="`${path}-md`"
          :id="navId"
          mt-auto
          style="margin-bottom: 72px;"
        />
      </v-layout>
    </v-navigation-drawer>
  </v-flex>
</template>


update

update

<template>

    <transition :name="maskTransition">

    </transition>
    <transition :name="dialogTransition">

        <slot></slot>

    </transition>

</template>


fix cleaning after parsing next update time

<template lang="jade">

span {{updateInfo}}

</template>

<script>
import ms from 'pretty-ms';

export default {
  data() {
    return {
      updateInfo: 'aktualizowanie',
    };
  },

  ready() {
    this.$parent.$parent.api.basic
      .on('nextUpdate', time => this.nextUpdate(time));
  },

  methods: {
    nextUpdate(time) {
      if (time) {
        this.thread ? clearInterval(this.thread) : null;

        this.thread = setInterval(_ => this.parse(--time), 1000);
      }

      return time;
    },

    parse(time) {
      if (time == 260) {
        this.updateInfo = `aktualizacja za 4:20`;
        return time;
      }

      this.updateInfo = time > 0
        ? `aktualizacja za ${ ms(time * 1000) }`
        : `aktualizowanie w trakcie ${time ? `(${ms(-time * 1000)})` : ''}`;
    },
  },
};

</script>

<template lang="jade">

span {{updateInfo}}

</template>

<script>
import ms from 'pretty-ms';

export default {
  data() {
    return {
      updateInfo: 'aktualizowanie',
    };
  },

  ready() {
    this.$parent.$parent.api.basic
      .on('nextUpdate', time => this.nextUpdate(time));
  },

  methods: {
    nextUpdate(time) {
      if (time) {
        this.thread ? clearTimeout(this.thread) : null;

        this.thread = setInterval(_ => this.parse(--time), 1000);
      }

      return time;
    },

    parse(time) {
      if (time == 260) {
        this.updateInfo = `aktualizacja za 4:20`;
        return time;
      }

      this.updateInfo = time > 0
        ? `aktualizacja za ${ ms(time * 1000) }`
        : `aktualizowanie w trakcie ${time ? `(${ms(-time * 1000)})` : ''}`;
    },
  },
};

</script>


Show add user to trust circle button correct on mobile.

<template>

            <v-checkbox v-model="contact.isSelected" color="primary" class="import-checkbox"
                        :disabled="contact.alreadySentInvitation || contact.notAllowedToSentInvitation">

{{getUserLabel}}

                        {{$t('dialog:invite.alreadySent')}}

                        {{$t('dialog:invite.notAllowedToSent')}}

            </v-checkbox>

{{contact.name}}

{{contact.email}}

                    <v-tooltip top v-if="contact.isTrustUser">
                        <v-btn color="primary" class="user-action-button" slot="activator"
                               :loading="loading" :disabled="loading"
                               @click="removeUserFromTrustCircle">
                            <v-icon left>mdi-check</v-icon>
                            {{$t('common:trustCircle')}}
                        </v-btn>
                        {{$t('common:removeFromTrustCircle')}}
                    </v-tooltip>
                    <v-tooltip top v-else>
                        <v-btn color="primary" class="user-action-button" slot="activator"
                               :loading="loading" :disabled="loading"
                               @click="addUserToTrustCircle">
                            <v-icon left>mdi-account-plus</v-icon>
                            {{$t('common:trustCircle')}}
                        </v-btn>
                        {{$t('common:addToTrustCircle')}}
                    </v-tooltip>

</template>


Just use text input on New Rule Form

Just use text input on New Rule Form

Previously, we were using the editablename pattern and widget on both
the edit rule and new rule page. As we pretty mush always want to set
the name on the new rule, we now use a text input here instead

Resolves: #601

Signed-off-by: Ryan Lerch <e809e25f3c554b2b195ccd768cd9a485288f896f@redhat.com>

<script setup lang="ts">
import { useAddRuleMutation } from "@/api/rules";
import type { GenerationRule, PostError, Rule } from "@/api/types";
import { useToastStore } from "@/stores/toast";
import { CCol, CRow } from "@coreui/bootstrap-vue";
import type { FormKitNode } from "@formkit/core";
import { FormKit } from "@formkit/vue";
import type { AxiosError } from "axios";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import GenerationRuleList from "./rule-edit/generation-rule/GenerationRuleList.vue";
import TrackingRule from "./rule-edit/tracking-rule/TrackingRule.vue";

const toastStore = useToastStore();
const router = useRouter();

const { mutateAsync } = useAddRuleMutation();

const trackingRuleName = ref("");
const value = ref("New Rule 1");

const generationRulesCount = ref(0);
const formReady = computed(
  () => trackingRuleName.value !== "" && generationRulesCount.value > 0
);

const handleSubmit = async (data: Rule, form: FormKitNode | undefined) => {
  console.log("Will submit the new rule:", data);
  if (!form) {
    throw Error("No form node?");
  }
  try {
    const response = await mutateAsync(data);
    // Success!
    toastStore.addToast({
      color: "success",
      title: "Rule created",
      content: `Rule "${response.name}" has been successfully created.`,
    });
    router.push("/rules");
  } catch (err) {
    const error = err as AxiosError<PostError>;
    console.log("Got error response from server:", error);
    if (!error.response) {
      return;
    }
    form.setErrors(
      error.response.data.detail.map(
        (e) => `Server error: ${e.loc[-1]}: ${e.msg}`
      )
    );
  }
};

const handleTrackingRuleSelected = (name: string) => {
  trackingRuleName.value = name;
};
const handleGenerationRulesChanged = (rules: GenerationRule[]) => {
  generationRulesCount.value = rules.length;
};
</script>

<template>
  <FormKit type="form" id="new-rule" @submit="handleSubmit" :actions="false">
    <CRow class="mb-2">
      <CCol xs="auto" class="flex-fill">
        <h4>
          <FormKit
            type="text"
            name="name"
            v-model="value"
            input-class="form-control-lg"
          />
        </h4>
      </CCol>
      <CCol xs="auto">
        <FormKit
          type="submit"
          :class="['btn', 'btn-primary', 'form-control-lg']"
          :disabled="!formReady"
        >
          Create Rule
        </FormKit>
      </CCol>
    </CRow>

    <CRow>
      <CCol sm="4" class="border-end">
        <TrackingRule @selected="handleTrackingRuleSelected" />
      </CCol>
      <CCol>
        <GenerationRuleList
          v-if="!!trackingRuleName"
          @change="handleGenerationRulesChanged"
        />
      </CCol>
    </CRow>
  </FormKit>
</template>

<script setup lang="ts">
import { useAddRuleMutation } from "@/api/rules";
import type { GenerationRule, PostError, Rule } from "@/api/types";
import { useToastStore } from "@/stores/toast";
import { CCol, CRow } from "@coreui/bootstrap-vue";
import type { FormKitNode } from "@formkit/core";
import { FormKit } from "@formkit/vue";
import type { AxiosError } from "axios";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import EditableName from "./EditableName.vue";
import GenerationRuleList from "./rule-edit/generation-rule/GenerationRuleList.vue";
import TrackingRule from "./rule-edit/tracking-rule/TrackingRule.vue";

const toastStore = useToastStore();
const router = useRouter();

const { mutateAsync } = useAddRuleMutation();

const trackingRuleName = ref("");
const generationRulesCount = ref(0);
const formReady = computed(
  () => trackingRuleName.value !== "" && generationRulesCount.value > 0
);

const handleSubmit = async (data: Rule, form: FormKitNode | undefined) => {
  console.log("Will submit the new rule:", data);
  if (!form) {
    throw Error("No form node?");
  }
  try {
    const response = await mutateAsync(data);
    // Success!
    toastStore.addToast({
      color: "success",
      title: "Rule created",
      content: `Rule "${response.name}" has been successfully created.`,
    });
    router.push("/rules");
  } catch (err) {
    const error = err as AxiosError<PostError>;
    console.log("Got error response from server:", error);
    if (!error.response) {
      return;
    }
    form.setErrors(
      error.response.data.detail.map(
        (e) => `Server error: ${e.loc[-1]}: ${e.msg}`
      )
    );
  }
};

const handleTrackingRuleSelected = (name: string) => {
  trackingRuleName.value = name;
};
const handleGenerationRulesChanged = (rules: GenerationRule[]) => {
  generationRulesCount.value = rules.length;
};
</script>

<template>
  <FormKit type="form" id="new-rule" @submit="handleSubmit" :actions="false">
    <CRow class="mb-2">
      <CCol xs="auto" class="flex-fill">
        <h4>
          <EditableName
            name="name"
            value="New Rule 1"
            button-class=""
            input-class="form-control-lg"
          />
        </h4>
      </CCol>
      <CCol xs="auto">
        <FormKit
          type="submit"
          :class="['btn', 'btn-primary', 'form-control-lg']"
          :disabled="!formReady"
        >
          Create Rule
        </FormKit>
      </CCol>
    </CRow>

    <CRow>
      <CCol sm="4" class="border-end">
        <TrackingRule @selected="handleTrackingRuleSelected" />
      </CCol>
      <CCol>
        <GenerationRuleList
          v-if="!!trackingRuleName"
          @change="handleGenerationRulesChanged"
        />
      </CCol>
    </CRow>
  </FormKit>
</template>


Fix alignment of cards in Safari

Fix alignment of cards in Safari

<template>

  <router-link :to="link" class="card" :class="{ 'mobile-card': isMobile }">
    <card-thumbnail
      class="thumbnail"
      v-bind="{ thumbnail, progress, kind, isMobile, showContentIcon }"
    />

    <div
      class="text"
      dir="auto"
    >
      <h3 class="title">
        <text-truncator
          :text="title"
          :maxHeight="maxTitleHeight"
        />
      </h3>
      <p
        v-if="subtitle"
        class="subtitle"
      >
        {{ subtitle }}
      </p>
      <coach-content-label
        class="coach-content-label"
        :value="numCoachContents"
        :isTopic="isTopic"
      />
    </div>
  </router-link>

</template>

<script>

  import { validateLinkObject, validateContentNodeKind } from 'kolibri.utils.validators';
  import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
  import coachContentLabel from 'kolibri.coreVue.components.coachContentLabel';
  import textTruncator from 'kolibri.coreVue.components.textTruncator';
  import cardThumbnail from './card-thumbnail';

  export default {
    components: {
      cardThumbnail,
      coachContentLabel,
      textTruncator,
    },
    props: {
      title: {
        type: String,
        required: true,
      },
      subtitle: {
        type: String,
        required: false,
      },
      thumbnail: {
        type: String,
        required: false,
      },
      kind: {
        type: String,
        required: true,
        validator: validateContentNodeKind,
      },
      showContentIcon: {
        type: Boolean,
        default: true,
      },
      // ContentNode.coach_content will be `0` if not a coach content leaf node,
      // or a topic without coach content. It will be a positive integer if a topic
      // with coach content, and `1` if a coach content leaf node.
      numCoachContents: {
        type: Number,
        default: 0,
      },
      progress: {
        type: Number,
        required: false,
        default: 0.0,
        validator(value) {
          return value >= 0.0 && value <= 1.0;
        },
      },
      link: {
        type: Object,
        required: true,
        validator: validateLinkObject,
      },
      isMobile: {
        type: Boolean,
        default: false,
      },
    },
    computed: {
      isTopic() {
        return this.kind === ContentNodeKinds.TOPIC || this.kind === ContentNodeKinds.CHANNEL;
      },
      maxTitleHeight() {
        // Add room if there is a subtitle, or the coach content icon appears
        if (this.subtitle || this.numCoachContents > 0) {
          return 40;
        }
        return this.isMobile ? 52 : 60;
      },
    },
  };

</script>

<style lang="stylus" scoped>

  @require '~kolibri.styles.definitions'
  @require './card.styl'

  .coach-content-label
    position: absolute
    bottom: 0
    padding: 8px 0

  .card
    text-decoration: none
    display: inline-block
    width: $thumb-width-desktop
    vertical-align: top
    border-radius: 2px
    background-color: $core-bg-light
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
                0 3px 1px -2px rgba(0, 0, 0, 0.2),
                0 1px 5px 0 rgba(0, 0, 0, 0.12)
    transition: box-shadow 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
    &:hover, &:focus
      box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
                  0 3px 14px 2px rgba(0, 0, 0, 0.12),
                  0 5px 5px -3px rgba(0, 0, 0, 0.2)

  .text
    color: $core-text-default
    overflow: hidden
    padding: 16px
    height: 92px
    position: relative

  .title, .subtitle
    margin: 0

  .subtitle
    position: absolute
    bottom: 12px
    left: 16px
    right: 16px
    font-size: 14px
    white-space: nowrap
    overflow: hidden
    text-overflow: ellipsis

  .mobile-card.card
    width: 100%
    height: $thumb-height-mobile

  .mobile-card
    .thumbnail
      position: absolute
    .text
      margin-left: $thumb-width-mobile
    .subtitle
      bottom: 17px

</style>

<template>

  <router-link :to="link" class="card" :class="{ 'mobile-card': isMobile }">
    <card-thumbnail
      class="thumbnail"
      v-bind="{ thumbnail, progress, kind, isMobile, showContentIcon }"
    />

    <div
      class="text"
      dir="auto"
    >
      <h3 class="title">
        <text-truncator
          :text="title"
          :maxHeight="maxTitleHeight"
        />
      </h3>
      <p
        v-if="subtitle"
        class="subtitle"
      >
        {{ subtitle }}
      </p>
      <coach-content-label
        class="coach-content-label"
        :value="numCoachContents"
        :isTopic="isTopic"
      />
    </div>
  </router-link>

</template>

<script>

  import { validateLinkObject, validateContentNodeKind } from 'kolibri.utils.validators';
  import { ContentNodeKinds } from 'kolibri.coreVue.vuex.constants';
  import coachContentLabel from 'kolibri.coreVue.components.coachContentLabel';
  import textTruncator from 'kolibri.coreVue.components.textTruncator';
  import cardThumbnail from './card-thumbnail';

  export default {
    components: {
      cardThumbnail,
      coachContentLabel,
      textTruncator,
    },
    props: {
      title: {
        type: String,
        required: true,
      },
      subtitle: {
        type: String,
        required: false,
      },
      thumbnail: {
        type: String,
        required: false,
      },
      kind: {
        type: String,
        required: true,
        validator: validateContentNodeKind,
      },
      showContentIcon: {
        type: Boolean,
        default: true,
      },
      // ContentNode.coach_content will be `0` if not a coach content leaf node,
      // or a topic without coach content. It will be a positive integer if a topic
      // with coach content, and `1` if a coach content leaf node.
      numCoachContents: {
        type: Number,
        default: 0,
      },
      progress: {
        type: Number,
        required: false,
        default: 0.0,
        validator(value) {
          return value >= 0.0 && value <= 1.0;
        },
      },
      link: {
        type: Object,
        required: true,
        validator: validateLinkObject,
      },
      isMobile: {
        type: Boolean,
        default: false,
      },
    },
    computed: {
      isTopic() {
        return this.kind === ContentNodeKinds.TOPIC || this.kind === ContentNodeKinds.CHANNEL;
      },
      maxTitleHeight() {
        // Add room if there is a subtitle, or the coach content icon appears
        if (this.subtitle || this.numCoachContents > 0) {
          return 40;
        }
        return this.isMobile ? 52 : 60;
      },
    },
  };

</script>

<style lang="stylus" scoped>

  @require '~kolibri.styles.definitions'
  @require './card.styl'

  .coach-content-label
    position: absolute
    bottom: 0
    padding: 8px 0

  .card
    text-decoration: none
    display: inline-block
    width: $thumb-width-desktop
    border-radius: 2px
    background-color: $core-bg-light
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
                0 3px 1px -2px rgba(0, 0, 0, 0.2),
                0 1px 5px 0 rgba(0, 0, 0, 0.12)
    transition: box-shadow 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)
    &:hover, &:focus
      box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14),
                  0 3px 14px 2px rgba(0, 0, 0, 0.12),
                  0 5px 5px -3px rgba(0, 0, 0, 0.2)

  .text
    color: $core-text-default
    overflow: hidden
    padding: 16px
    height: 92px
    position: relative

  .title, .subtitle
    margin: 0

  .subtitle
    position: absolute
    bottom: 12px
    left: 16px
    right: 16px
    font-size: 14px
    white-space: nowrap
    overflow: hidden
    text-overflow: ellipsis

  .mobile-card.card
    width: 100%
    height: $thumb-height-mobile

  .mobile-card
    .thumbnail
      position: absolute
    .text
      margin-left: $thumb-width-mobile
    .subtitle
      bottom: 17px

</style>


revert change from https://github.com/learningequality/kolibri/commit/44ebfbd3f5607c0b2ae407b2414b059c4aa93ea4#commitcomment-18748813

<template>

        All Users

       ( {{ visibleUsers.length }} )

      Filter User Type
      <select v-model="roleFilter" id="type-filter" name="type-filter">
        <option selected value="all"> All Users </option>
        <option value="admin"> Admins </option>
        <option value="learner"> Learners </option>
      </select>

        <user-create-modal></user-create-modal>

        <svg class="icon" src="../icons/search.svg" role="presentation" aria-hidden="true"></svg>
        <input
          aria-label="Search for a user..."
          type="search"
          v-model="searchFilter"
          placeholder="Search for a user...">

Users

           Full Name
           Username
           Edit

            {{user.full_name}}

              {{role.kind | capitalize}}

            {{user.username}}

            <user-edit-modal
              :userid="user.id"
              :roles="user.roles"
              :username="user.username"
              :fullname="user.full_name">
            </user-edit-modal>

{{ $tr('noUsersExist') }}

{{ $tr('allUsersFilteredOut') }}

</template>

<template>

        All Users

       ( {{ visibleUsers.length }} )

      Filter User Type
      <select v-model="roleFilter" id="type-filter" name="type-filter">
        <option selected value="all"> All Users </option>
        <option value="admin"> Admins </option>
        <option value="learner"> Learners </option>
      </select>

        <svg class="icon" src="../icons/search.svg" role="presentation" aria-hidden="true"></svg>
        <input
          aria-label="Search for a user..."
          type="search"
          v-model="searchFilter"
          placeholder="Search for a user...">

        <user-create-modal></user-create-modal>

Users

           Full Name
           Username
           Edit

            {{user.full_name}}

              {{role.kind | capitalize}}

            {{user.username}}

            <user-edit-modal
              :userid="user.id"
              :roles="user.roles"
              :username="user.username"
              :fullname="user.full_name">
            </user-edit-modal>

{{ $tr('noUsersExist') }}

{{ $tr('allUsersFilteredOut') }}

</template>


Fix bug

Fix bug

<template>

	<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`">
		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
	</router-link>

			<router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link>
			@{{ getAcct(note.user) }}
			<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
				<mk-time :time="note.createdAt"/>
			</router-link>

			<mk-sub-note-content class="text" :note="note"/>

</template>

<template>

	<router-link class="avatar-anchor" :to="`/@${acct}`">
		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
	</router-link>

			<router-link class="name" :to="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
			@{{ acct }}
			<router-link class="created-at" :to="`/@${acct}/${note.id}`">
				<mk-time :time="note.createdAt"/>
			</router-link>

			<mk-sub-note-content class="text" :note="note"/>

</template>


fix img url

fix img url

<template>

image决策用图

    <foxgis-search :placeholder="'搜索'"></foxgis-search>
    <mdl-button raised colored v-mdl-ripple-effect @click="uploadClick">上传决策用图</mdl-button>
    <input type="file" multiple style="display:none" id="file" accept=".png,.jpg,.jpeg,.tif,.tiff">

    <mdl-progress indeterminate id='upload-progress' ></mdl-progress>
    正在上传···

      主题：
      社会
      经济
      人口
      旅游
      农业
      交通
      新闻用图
      决策用图

      地区：
      {{ upload.location }}

      年份：
      {{ upload.year }}

  <div class="card" v-for='u in pageConfig.page_item_num' v-if="((pageConfig.current_page-1)*pageConfig.page_item_num+$index) < displayUploads.length" track-by="$index">

      <input type="text" v-model="displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].name" @change="uploadNameChange($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)"/>
      <mdl-anchor-button accent raised v-mdl-ripple-effect @click="showPreview($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)">预览</mdl-anchor-button>

      标签:

        {{ tag }}
        ×

      <input type="text" maxlength="10" @change="addTag($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)">

        制图地区:<input type="text" value="{{ displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].location }}" @change="editLocation($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)"/>
        制图时间:<input type="text" value="{{ displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].year }}" @change="editTime($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)"/>
        文件大小:{{ calculation(displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].size) }}
        文件格式:{{ displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].format }}

        <mdl-anchor-button colored v-mdl-ripple-effect @click="deleteUpload(displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].upload_id)">删除</mdl-anchor-button>
        <mdl-anchor-button colored v-mdl-ripple-effect @click="downloadUpload(displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].upload_id)">下载</mdl-anchor-button>

0?true:false">

 1">navigate_before

{{ pageConfig.first_page + page }}

 1)&&(pageConfig.current_page < parseInt(total_items/pageConfig.page_item_num)+1)">navigate_next

  <foxgis-dialog id="delete-dialog" class='modal' :dialog="dialogcontent" @dialog-action="deleteAction"></foxgis-dialog>

  <foxgis-loading id="create-loading" class='modal'></foxgis-loading>

</template>

<template>

image决策用图

    <foxgis-search :placeholder="'搜索'"></foxgis-search>
    <mdl-button raised colored v-mdl-ripple-effect @click="uploadClick">上传决策用图</mdl-button>
    <input type="file" multiple style="display:none" id="file" accept=".png,.jpg,.jpeg,.tif,.tiff">

      主题：
      社会
      经济
      人口
      旅游
      农业
      交通
      新闻用图
      决策用图

      地区：
      {{ upload.location }}

      年份：
      {{ upload.year }}

  <div class="card" v-for='u in pageConfig.page_item_num' v-if="((pageConfig.current_page-1)*pageConfig.page_item_num+$index) < displayUploads.length" track-by="$index">

      <input type="text" v-model="displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].name" @change="uploadNameChange($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)"/>
      <mdl-anchor-button accent raised v-mdl-ripple-effect @click="showPreview($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)">预览</mdl-anchor-button>

      标签:

        {{ tag }}
        ×

      <input type="text" maxlength="10" @change="addTag($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)">

        制图地区:<input type="text" value="{{ displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].location }}" @change="editLocation($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)"/>
        制图时间:<input type="text" value="{{ displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].year }}" @change="editTime($event, (pageConfig.current_page-1)*pageConfig.page_item_num+$index)"/>
        文件大小:{{ calculation(displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].size) }}
        文件格式:{{ displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].format }}

        <mdl-anchor-button colored v-mdl-ripple-effect @click="deleteUpload(displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].upload_id)">删除</mdl-anchor-button>
        <mdl-anchor-button colored v-mdl-ripple-effect @click="downloadUpload(displayUploads[(pageConfig.current_page-1)*pageConfig.page_item_num+$index].upload_id)">下载</mdl-anchor-button>

0?true:false">

 1">navigate_before

{{ pageConfig.first_page + page }}

 1)&&(pageConfig.current_page < parseInt(total_items/pageConfig.page_item_num)+1)">navigate_next

  <foxgis-dialog id="delete-dialog" class='modal' :dialog="dialogcontent" @dialog-action="deleteAction"></foxgis-dialog>

  <foxgis-loading id="create-loading" class='modal'></foxgis-loading>

</template>


rm console.log

rm console.log

<template>
  <input :value="value" :name="name" type="hidden" />
</template>

<script>
import connectedElement from './mixins/connectedElement';

export default {
  name: 'HiddenInput',

  mixins: [connectedElement],
};
</script>

<template>
  <input :value="value" :name="name" type="hidden" />
</template>

<script>
import connectedElement from './mixins/connectedElement';

// eslint-disable-next-line no-console
console.log(process.env.AUTH0_DOMAIN);

export default {
  name: 'HiddenInput',

  mixins: [connectedElement],
};
</script>


update runcollapse styling

update runcollapse styling

<template>

Run #{{ run.id }}

          {{ run.language }}

            {{ run.state }} <pulse-loader v-if="run.state == 'Judging'" :loading="true" size="4px" color="#fff" />

          Submission
          Test Run

Code

        <Editor v-model="run.source_code"
                :init-text="run.source_code"
                :id="'run-editor-' + run.id"
                :read-only="true"
                :lang="run.language"
                :minLines="5"
                :maxLines="30" />

Input

{{ run.run_input }}

Output

{{ truncate(run.run_output, 500) }}

</template>


FEAT: set height to images if full prop is true


Add hint to file previews not supported by the viewer

Like done on links, " ↗" is now added to the name of file previews to
hint that the file will be opened in a different tab/window.

Signed-off-by: Daniel Calviño Sánchez <bd7629e975cc0e00ea7cda4ab6a120f2ee42ac1b@gmail.com>


Header click go back to the site

Header click go back to the site

<template>

      {{$root.site.title}}

            Toggle navigation

            <notification-menu></notification-menu>
            <add-menu></add-menu>
            <user-menu></user-menu>

</template>


Image: Fix preview dose not show when preview list not contain src issue (#18975) (#19130)

<template>

    <slot v-if="loading" name="placeholder">

    </slot>
    <slot v-else-if="error" name="error">

{{ t('el.image.error') }}

    </slot>

    <template v-if="preview">
      <image-viewer :z-index="zIndex" :initial-index="imageIndex" v-show="showViewer" :on-close="closeViewer" :url-list="previewSrcList"/>
    </template>

</template>


and dont display twice

and dont display twice

<script lang="coffee">
import { fieldFromTemplate } from '@/shared/helpers/poll'
import AppConfig from '@/shared/services/app_config'
import { optionColors, optionImages } from '@/shared/helpers/poll'

export default
  props:
    stance: Object

  data: ->
    optionColors: optionColors()
    optionImages: optionImages()

  computed:
    poll: -> @stance.poll()
    variableScore: -> @poll.hasVariableScore()
    pollType: -> @poll.pollType
    datesAsOptions: -> @poll.datesAsOptions()
    choices: -> @stance.sortedChoices()

  methods:
    emitClick: -> @$emit('click')

    colorFor: (score) ->
      switch score
        when 2 then AppConfig.pollColors.proposal[0]
        when 1 then AppConfig.pollColors.proposal[1]
        when 0 then AppConfig.pollColors.proposal[2]

</script>

<template lang="pug">
.poll-common-stance-choices(v-if="!poll.singleChoice()")
  span.caption(v-if='stance.castAt && stance.totalScore() == 0' v-t="'poll_common_votes_panel.none_of_the_above'" )
  template(v-if="datesAsOptions")
    v-chip.mr-1.my-1(v-if="choice.show" v-for="choice in choices" :key="choice.poll_option_id" outlined :color="colorFor(choice.score)" @click="emitClick")
      poll-meeting-time(:name="choice.pollOption.name")
  template(v-else)
    .poll-common-stance-choice.mt-1(:class="'poll-common-stance-choice--' + pollType" v-if="choice.score > 0 || pollType == 'score'" v-for="choice in choices" :key="choice.id")
      v-icon(small :color="choice.pollOption.color" v-if="!variableScore") mdi-check
      span(:style="{color: choice.pollOption.color}" v-if="variableScore") {{choice.rank || choice.score}}
      span.ml-2.text--secondary
        |{{ choice.pollOption.optionName() }}

</template>


devop: 🔧 updated block information

devop: 🔧 updated block information

<template>

      <v-row class="alert-container-top pt-4 px-3">
        <v-col cols="12" class="d-flex align-center">
          <v-icon :class="['mr-2', alertTitleColor]" size="16px">{{
            alertIcon
          }}</v-icon>

            {{ alertTitle }}

        </v-col>
      </v-row>
      <v-row v-if="isAvailable" class="mt-1 px-3" align="center">

        <v-col cols="6">

Price

        </v-col>
        <v-col cols="6" class="text-end">

              {{ formattedPrice }} {{ network.type.currencyName }}

              {{ formatFiatPrice }}

        </v-col>
      </v-row>

      <v-row v-if="isAvailable" class="mt-1 px-3" align="center">

        <v-col cols="6">

Total

        </v-col>
        <v-col cols="6" class="text-end">

              {{ formattedPrice }} {{ network.type.currencyName }}

              {{ formatFiatPrice }}

        </v-col>
      </v-row>

      <v-row class="px-3 py-4">

        <v-col cols="6">
          <mew-button btn-style="outline" color-theme="primary" has-full-width>

              <v-icon left>mdi-plus-circle-outline</v-icon>

Add to batch

          </mew-button>
        </v-col>
        <v-col cols="6">
          <mew-button
            title="Mint now"
            btn-style="background"
            color-theme="primary"
            has-full-width
          >
          </mew-button>
        </v-col>
      </v-row>

    <mew-alert
      :theme="alertTheme"
      has-white-background
      hide-alert-icon
      hide-close-icon
    >
      <v-row class="align-center justify-start" no-gutters>

        <v-col cols="12" class="d-flex align-center">
          <v-icon :class="['mr-2', alertTitleColor]" size="16px">{{
            alertIcon
          }}</v-icon>

            {{ alertTitle }}

        </v-col>

        <v-col v-if="isNotAvailable" cols="12" class="d-flex align-center mt-3">

Owner:

            {{ ownerFormatted }}

        </v-col>

        <v-col v-if="isNotAvailable" cols="12" class="mt-1">

See if it's available on Rarible

            <v-icon class="ml-2 greenPrimary--text" size="16px"
              >mdi-open-in-new</v-icon
            >

        </v-col>

        <v-col v-if="isAvailable" cols="6" class="mt-1">

            {{ formattedPrice }} {{ network.type.currencyName }}

            {{ formatFiatPrice }}

        </v-col>

        <v-col v-if="isAvailable" cols="6" class="mt-1 d-flex justify-end">
          <mew-button
            v-if="!isPending"
            :title="$vuetify.breakpoint.xs ? 'Mint' : 'Mint Block'"
            :disabled="disableActionBtn"
            :loading="disableAction"
            @click.native="emitMint()"
          />
          <mew-button v-else disabled btn-style="light">

              <v-progress-circular
                indeterminate
                color="primary"
                class="ma-auto"
                size="16"
                width="2"
              ></v-progress-circular>

Pending

          </mew-button>
        </v-col>

        <v-col v-if="isOwned" cols="12" sm="6" class="mt-4 pr-sm-2 mb-sm-2">
          <mew-button
            v-if="!isPending"
            has-full-width
            title="Send ETH Block"
            btn-style="outline"
            :disabled="disableActionBtn"
            :loading="disableAction"
            @click.native="emitOpenSend()"
          />
          <mew-button v-else disabled btn-style="light">

              <v-progress-circular
                indeterminate
                color="primary"
                class="ma-auto"
                size="16"
                width="2"
              ></v-progress-circular>

Pending

          </mew-button>
        </v-col>

        <v-col
          v-if="isOwned"
          cols="12"
          sm="6"
          class="mt-5 mb-2 mt-sm-4 pl-sm-2"
          :order="$vuetify.breakpoint.xs ? 'last' : ''"
        >
          <mew-button
            v-if="!isPending"
            has-full-width
            :btn-link="raribleLink"
            @click.native="trackToRarible"
          >
            <v-row class="align-center justify-center">

List for sale

              <v-icon class="ml-2 white--text" size="16px"
                >mdi-open-in-new</v-icon
              >
            </v-row>
          </mew-button>
        </v-col>

        <v-col
          v-if="!hasEnoughEth && (isOwned || isAvailable)"
          cols="12"
          :class="[
            'd-flex align-center mt-1',
            isOwned ? 'justify-start' : 'justify-end'
          ]"
        >

            {{ notEnoughMessage }}

                Buy more {{ network.type.name }}.

                <mew-tooltip
                    class="d-inline-block"
                    :text="estimatedFeesTooltip"
                />

              <mew-tooltip
                  class="buy-more-link d-inline-block"
                  :text="estimatedFeesTooltip"
              />

        </v-col>

        <v-col v-if="isReserved" cols="12" class="d-flex align-center mt-3">

            ETH Blocks 1-10 are reserved for the Ethereum founders

        </v-col>
      </v-row>
    </mew-alert>

</template>

<template>
  <mew-alert
    :theme="alertTheme"
    has-white-background
    hide-alert-icon
    hide-close-icon
  >
    <v-row class="align-center justify-start" no-gutters>

      <v-col cols="12" class="d-flex align-center">
        <v-icon :class="['mr-2', alertTitleColor]" size="16px">{{
          alertIcon
        }}</v-icon>

          {{ alertTitle }}

      </v-col>

      <v-col v-if="isNotAvailable" cols="12" class="d-flex align-center mt-3">

Owner:

          {{ ownerFormatted }}

      </v-col>

      <v-col v-if="isNotAvailable" cols="12" class="mt-1">

See if it's available on Rarible

          <v-icon class="ml-2 greenPrimary--text" size="16px"
            >mdi-open-in-new</v-icon
          >

      </v-col>

      <v-col v-if="isAvailable" cols="6" class="mt-1">

          {{ formattedPrice }} {{ network.type.currencyName }}

          {{ formatFiatPrice }}

      </v-col>

      <v-col v-if="isAvailable" cols="6" class="mt-1 d-flex justify-end">
        <mew-button
          v-if="!isPending"
          :title="$vuetify.breakpoint.xs ? 'Mint' : 'Mint Block'"
          :disabled="disableActionBtn"
          :loading="disableAction"
          @click.native="emitMint()"
        />
        <mew-button v-else disabled btn-style="light">

            <v-progress-circular
              indeterminate
              color="primary"
              class="ma-auto"
              size="16"
              width="2"
            ></v-progress-circular>

Pending

        </mew-button>
      </v-col>

      <v-col v-if="isOwned" cols="12" sm="6" class="mt-4 pr-sm-2 mb-sm-2">
        <mew-button
          v-if="!isPending"
          has-full-width
          title="Send ETH Block"
          btn-style="outline"
          :disabled="disableActionBtn"
          :loading="disableAction"
          @click.native="emitOpenSend()"
        />
        <mew-button v-else disabled btn-style="light">

            <v-progress-circular
              indeterminate
              color="primary"
              class="ma-auto"
              size="16"
              width="2"
            ></v-progress-circular>

Pending

        </mew-button>
      </v-col>

      <v-col
        v-if="isOwned"
        cols="12"
        sm="6"
        class="mt-5 mb-2 mt-sm-4 pl-sm-2"
        :order="$vuetify.breakpoint.xs ? 'last' : ''"
      >
        <mew-button
          v-if="!isPending"
          has-full-width
          :btn-link="raribleLink"
          @click.native="trackToRarible"
        >
          <v-row class="align-center justify-center">

List for sale

            <v-icon class="ml-2 white--text" size="16px"
              >mdi-open-in-new</v-icon
            >
          </v-row>
        </mew-button>
      </v-col>

      <v-col
        v-if="!hasEnoughEth && (isOwned || isAvailable)"
        cols="12"
        :class="[
          'd-flex align-center mt-1',
          isOwned ? 'justify-start' : 'justify-end'
        ]"
      >

          {{ notEnoughMessage }}

              Buy more {{ network.type.name }}.

              <mew-tooltip
                  class="d-inline-block"
                  :text="estimatedFeesTooltip"
              />

            <mew-tooltip
                class="buy-more-link d-inline-block"
                :text="estimatedFeesTooltip"
            />

      </v-col>

      <v-col v-if="isReserved" cols="12" class="d-flex align-center mt-3">

          ETH Blocks 1-10 are reserved for the Ethereum founders

      </v-col>
    </v-row>
  </mew-alert>
</template>


Removed only few answers.

Removed only few answers.

<template>

            <user-menu :menu-title="creatorTitle" :user-image="creator.userImagePreview"
                       :user-name="creator.name" :user-id="creator.userId" :user-slug="creator.slug"
                       :is-trust-user="creator.isTrustUser" :is-logged-in-user="creator.isLoggedInUser"
                       @add-trust-circle="(userId) => $emit('add-trust-circle', userId)"
                       @remove-trust-circle="(userId) => $emit('remove-trust-circle', userId)">

            </user-menu>

            <v-icon medium>
                mdi-menu-right
            </v-icon>

            <user-menu :menu-title="userTitle" :user-image="user.userImagePreview"
                       :user-name="user.name" :user-id="user.userId" :user-slug="user.slug"
                       :is-trust-user="user.isTrustUser" :is-logged-in-user="user.isLoggedInUser"
                       @add-trust-circle="(userId) => $emit('add-trust-circle', userId)"
                       @remove-trust-circle="(userId) => $emit('remove-trust-circle', userId)">

            </user-menu>

            <watches-menu :user-name="user.name" :user-id="user.userId" :watched-id="questionId"
                          :is-watching-action="true"
                          watched-id-name="questionId" :user-slug="user.slug" :is-logged-in-user="user.isLoggedInUser"
                          :is-admin="creator.isLoggedInUser" :watched-by-user="isWatchedByUser"
                          :number-of-watches="numberOfWatches" menu-translation="watchesQuestion"
                          api-get-user-command="question/watches" api-watch="user/question/watch"
                          @add-watch="(id) => $emit('add-watch', id)"
                          @remove-watch="(id) => $emit('remove-watch', id)"
                          @watch-menu-closed="(data) => $emit('watch-menu-closed', data)">

                    <v-icon medium class="action-icon">mdi-star</v-icon>
                    {{numberOfWatches}}

            </watches-menu>

            <v-tooltip bottom>

                    <v-icon medium class="tooltip-icon no-answers" v-if="numberOfAnswers === 0">mdi-comment-alert
                    </v-icon>
                    <v-icon medium class="tooltip-icon" v-else>mdi-comment</v-icon>
                    {{numberOfAnswers}}

                {{$t('pages:feeds.menu.questions.noAnswers')}}
                {{$t('pages:feeds.menu.questions.numberOfAnswers', {count: numberOfAnswers})}}
            </v-tooltip>

            <watches-menu :user-id="user.userId" :watched-id="questionId" watched-id-name="questionId"
                          :is-watching-action="false"
                          :user-slug="user.slug" :is-logged-in-user="true" :is-admin="user.isLoggedInUser"
                          :watched-by-user="isWatchedByUser" :number-of-watches="numberOfWatches"
                          menu-translation="watchesQuestion" api-get-user-command="question/watches"
                          api-watch="user/question/watch"
                          @add-watch="(id) => $emit('add-watch', id)"
                          @remove-watch="(id) => $emit('remove-watch', id)"
                          @watch-menu-closed="(data) => $emit('watch-menu-closed', data)">

                    <v-icon medium class="action-icon">mdi-star</v-icon>
                    {{numberOfWatches}}

            </watches-menu>

</template>

<template>

            <user-menu :menu-title="creatorTitle" :user-image="creator.userImagePreview"
                       :user-name="creator.name" :user-id="creator.userId" :user-slug="creator.slug"
                       :is-trust-user="creator.isTrustUser" :is-logged-in-user="creator.isLoggedInUser"
                       @add-trust-circle="(userId) => $emit('add-trust-circle', userId)"
                       @remove-trust-circle="(userId) => $emit('remove-trust-circle', userId)">

            </user-menu>

            <v-icon medium>
                mdi-menu-right
            </v-icon>

            <user-menu :menu-title="userTitle" :user-image="user.userImagePreview"
                       :user-name="user.name" :user-id="user.userId" :user-slug="user.slug"
                       :is-trust-user="user.isTrustUser" :is-logged-in-user="user.isLoggedInUser"
                       @add-trust-circle="(userId) => $emit('add-trust-circle', userId)"
                       @remove-trust-circle="(userId) => $emit('remove-trust-circle', userId)">

            </user-menu>

            <watches-menu :user-name="user.name" :user-id="user.userId" :watched-id="questionId"
                          :is-watching-action="true"
                          watched-id-name="questionId" :user-slug="user.slug" :is-logged-in-user="user.isLoggedInUser"
                          :is-admin="creator.isLoggedInUser" :watched-by-user="isWatchedByUser"
                          :number-of-watches="numberOfWatches" menu-translation="watchesQuestion"
                          api-get-user-command="question/watches" api-watch="user/question/watch"
                          @add-watch="(id) => $emit('add-watch', id)"
                          @remove-watch="(id) => $emit('remove-watch', id)"
                          @watch-menu-closed="(data) => $emit('watch-menu-closed', data)">

                    <v-icon medium class="action-icon">mdi-star</v-icon>
                    {{numberOfWatches}}

            </watches-menu>

            <v-tooltip bottom>

                    <v-icon medium class="tooltip-icon no-answers" v-if="numberOfAnswers === 0">mdi-comment-alert
                    </v-icon>
                    <v-icon medium class="tooltip-icon only-few-answers"
                            v-else-if="numberOfAnswers > 0 && numberOfAnswers <= 4">
                        mdi-comment-alert
                    </v-icon>
                    <v-icon medium class="tooltip-icon" v-else>mdi-comment</v-icon>
                    {{numberOfAnswers}}

                {{$t('pages:feeds.menu.questions.noAnswers')}}
                <span v-else-if="numberOfAnswers <= 4">
                    {{$t('pages:feeds.menu.questions.fewAnswers', {count: numberOfAnswers})}}
                {{$t('pages:feeds.menu.questions.numberOfAnswers', {count: numberOfAnswers})}}
            </v-tooltip>

            <watches-menu :user-id="user.userId" :watched-id="questionId" watched-id-name="questionId"
                          :is-watching-action="false"
                          :user-slug="user.slug" :is-logged-in-user="true" :is-admin="user.isLoggedInUser"
                          :watched-by-user="isWatchedByUser" :number-of-watches="numberOfWatches"
                          menu-translation="watchesQuestion" api-get-user-command="question/watches"
                          api-watch="user/question/watch"
                          @add-watch="(id) => $emit('add-watch', id)"
                          @remove-watch="(id) => $emit('remove-watch', id)"
                          @watch-menu-closed="(data) => $emit('watch-menu-closed', data)">

                    <v-icon medium class="action-icon">mdi-star</v-icon>
                    {{numberOfWatches}}

            </watches-menu>

</template>


Migrate ControlPlayer.vue

Migrate ControlPlayer.vue

<template>

-

{{player.shots + 1}}

{{player.shots}}

{{reason}}

{{player.displayName}}

{{n+1}}

{{n+1}}

+

</template>


Add class

Add class

<template>

    <colgroup>
      <col width="40">
      <col>
      <col v-if="can_edit" width="300">
    </colgroup>

      {{ department.pk }}
      <input class="form-control" :class="{updated: department.updated}" v-model="department.title" :disabled="!can_edit"/>

      <input type="text" class="form-control" placeholder="Название" v-model="create.title" autofocus>
      <input type="submit" class="btn btn-primary-nb form-control" value="Добавить">

</template>

<template>

    <colgroup>
      <col width="40">
      <col>
      <col v-if="can_edit" width="300">
    </colgroup>

      {{ department.pk }}
      <input class="form-control" v-model="department.title" :disabled="!can_edit"/>

      <input type="text" class="form-control" placeholder="Название" v-model="create.title" autofocus>
      <input type="submit" class="btn btn-primary-nb form-control" value="Добавить">

</template>


autoformatting

autoformatting

<template>

          UK Lottery Data

          <button class="button is-static">FROM</button>

          <Datepicker v-model="params.start" v-on:input="adjustFieldData" :disabled="flags.dates" input-class="input has-addons"></Datepicker>

          <button class="button is-static">TO</button>

          <Datepicker v-model="params.end" v-on:input="adjustFieldData" :disabled="flags.dates" input-class="input has-addons"></Datepicker>

          <option disabled value="">Please select one</option>
          <option value="0">All Sets</option>
          <option v-for="s in sets" :value="s">Set: {{ s }}</option>

          <option disabled value="">Please select one</option>
          <option value="all">All Machines</option>
          <option v-for="m in machines" :value="m">Machine: {{ m }}</option>

          <button class="button" @click="runQuery">Submit</button>

        <template scope="props">

            {{ props.row.label}}

            {{ props.row.num[0] }}

            {{ props.row.num[1] }}

            {{ props.row.num[2] }}

            {{ props.row.num[3] }}

            {{ props.row.num[4] }}

            {{ props.row.num[5] }}

            {{ props.row.num[6] }}

        </template>

Frequency Distribution

      <BarChart :chart-data="charts.freqDist.data" :options="charts.freqDist.options" :height="200"></BarChart>

Results Over Time

      <LineChart :chart-data="charts.timeSeries.data" :options="charts.timeSeries.options" :height="200"></LineChart>

© N.Boughton, 2017

            source code

</template>


make header sticky in csv-table component

<template>

                        {{ header }}

                        {{ column }}

</template>


Confirm dialog agree button colored

Confirm dialog agree button colored

<!-- Universal confirmation dialog, just pass text and function in Props -->
<template>
  <v-dialog
    :value="value"
    :attach="attach"
    max-width="500px"
    @input="close"
  >
    <slot slot="activator" />

    <v-card>
      <v-card-text v-if="$slots.default">
        <slot />
      </v-card-text>
      <v-card-text v-else>
        {{ fullText || `Are you sure you want to ${text} ?` }}
      </v-card-text>
      <v-divider />
      <v-card-actions>
        <v-spacer />
        <v-btn
          flat
          :title="cancelBtnText"
          color="primary"
          @click.native="cancel"
        >
          {{ cancelBtnText }}
        </v-btn>
        <v-btn
          :title="confirmBtnText"
          color="info"
          @click.native="confirm"
        >
          {{ confirmBtnText }}
        </v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Prop } from 'vue-property-decorator'

@Component
export default class ConfirmDialog extends Vue {
  @Prop(String) text!: string
  @Prop(String) fullText!: string
  @Prop(Boolean) value!: boolean
  @Prop(String) attach!: string
  @Prop({ default: 'Continue' }) confirmBtnText!: string
  @Prop({ default: 'Cancel' }) cancelBtnText!: string

  close () {
    this.$emit('input', false)
    this.$emit('canceled')
  }
  confirm () {
    this.$emit('confirmed')
    this.close()
  }
  cancel () {
    this.$emit('canceled')
    this.close()
  }
}
</script>

<!-- Universal confirmation dialog, just pass text and function in Props -->
<template>
  <v-dialog
    :value="value"
    :attach="attach"
    max-width="500px"
    @input="close"
  >
    <slot slot="activator" />

    <v-card>
      <v-card-text v-if="$slots.default">
        <slot />
      </v-card-text>
      <v-card-text v-else>
        {{ fullText || `Are you sure you want to ${text} ?` }}
      </v-card-text>
      <v-divider />
      <v-card-actions>
        <v-spacer />
        <v-btn
          flat
          :title="cancelBtnText"
          color="primary"
          @click.native="cancel"
        >
          {{ cancelBtnText }}
        </v-btn>
        <v-btn
          flat
          :title="confirmBtnText"
          color="primary"
          @click.native="confirm"
        >
          {{ confirmBtnText }}
        </v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'
import { Prop } from 'vue-property-decorator'

@Component
export default class ConfirmDialog extends Vue {
  @Prop(String) text!: string
  @Prop(String) fullText!: string
  @Prop(Boolean) value!: boolean
  @Prop(String) attach!: string
  @Prop({ default: 'Continue' }) confirmBtnText!: string
  @Prop({ default: 'Cancel' }) cancelBtnText!: string

  close () {
    this.$emit('input', false)
    this.$emit('canceled')
  }
  confirm () {
    this.$emit('confirmed')
    this.close()
  }
  cancel () {
    this.$emit('canceled')
    this.close()
  }
}
</script>


fix: preview case sensitive file extension

<template>

      <button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
        close
      </button>

        {{ this.name }}

      <preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
      <button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
        more_vert
      </button>

        <rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
        <delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
        <download-button :disabled="loading" v-if="user.perm.download"></download-button>
        <info-button :disabled="loading"></info-button>

    <button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
      chevron_left
    </button>
    <button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
      chevron_right
    </button>

    <template v-if="!loading">

        <ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
        <audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
        <video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
          <track
            kind="captions"
            v-for="(sub, index) in subtitles"
            :key="index"
            :src="sub"
            :label="'Subtitle ' + index" :default="index === 0">
          Sorry, your browser doesn't support embedded videos,
          but don't worry, you can download it
          and watch it with your favorite video player!
        </video>
        <object v-else-if="req.extension.toLowerCase() == '.pdf'" class="pdf" :data="raw"></object>

{{ $t('buttons.download') }} file_download

    </template>

</template>

<template>

      <button @click="back" class="action" :title="$t('files.closePreview')" :aria-label="$t('files.closePreview')" id="close">
        close
      </button>

        {{ this.name }}

      <preview-size-button v-if="isResizeEnabled && this.req.type === 'image'" @change-size="toggleSize" v-bind:size="fullSize" :disabled="loading"></preview-size-button>
      <button @click="openMore" id="more" :aria-label="$t('buttons.more')" :title="$t('buttons.more')" class="action">
        more_vert
      </button>

        <rename-button :disabled="loading" v-if="user.perm.rename"></rename-button>
        <delete-button :disabled="loading" v-if="user.perm.delete"></delete-button>
        <download-button :disabled="loading" v-if="user.perm.download"></download-button>
        <info-button :disabled="loading"></info-button>

    <button class="action" @click="prev" v-show="hasPrevious" :aria-label="$t('buttons.previous')" :title="$t('buttons.previous')">
      chevron_left
    </button>
    <button class="action" @click="next" v-show="hasNext" :aria-label="$t('buttons.next')" :title="$t('buttons.next')">
      chevron_right
    </button>

    <template v-if="!loading">

        <ExtendedImage v-if="req.type == 'image'" :src="raw"></ExtendedImage>
        <audio v-else-if="req.type == 'audio'" :src="raw" autoplay controls></audio>
        <video v-else-if="req.type == 'video'" :src="raw" autoplay controls>
          <track
            kind="captions"
            v-for="(sub, index) in subtitles"
            :key="index"
            :src="sub"
            :label="'Subtitle ' + index" :default="index === 0">
          Sorry, your browser doesn't support embedded videos,
          but don't worry, you can download it
          and watch it with your favorite video player!
        </video>
        <object v-else-if="req.extension == '.pdf'" class="pdf" :data="raw"></object>

{{ $t('buttons.download') }} file_download

    </template>

</template>


Refactor string identifiers #3404

Refactor string identifiers #3404

<template>

 0">

                        {{ $t('firefly.profile_authorized_apps') }}

{{ $t('firefly.profile_authorized_clients') }}

                            {{ $t('firefly.name') }}
                            {{ $t('firefly.profile_scopes') }}

                                {{ token.client.name }}

                                     0">
                                        {{ token.scopes.join(', ') }}

                                    {{ $t('firefly.profile_revoke') }}

</template>

<template>

 0">

                        {{ $t('profile.authorized_apps') }}

{{ $t('profile.authorized_clients') }}

                            {{ $t('firefly.name') }}
                            {{ $t('profile.scopes') }}

                                {{ token.client.name }}

                                     0">
                                        {{ token.scopes.join(', ') }}

                                    {{ $t('profile.revoke') }}

</template>


modify component

modify component

<template>

        <router-link :to="'/product/' + info.id" class="product-main">

{{ info.name }}

¥ {{ info.cost }}

加入购物车

        </router-link>

</template>

<template>

        <router-link :to="/product/" + info.id class="product-main">

{{ info.name }}

¥ {{ info.cost }}

加入购物车

        </router-link>

</template>


Rival Rating

Rival Rating

<template>

Manchester City Barclay Premier League Point

Available Player

Rating Comparision

Rival Rating

</template>

<template>

Manchester City Barclay Premier League Point

Available Player

Rating Comparision

</template>


updated pool message

updated pool message

<template>

Draft Pool

Is A Hero or Ability in AD?

          Find out exactly which heroes and which abilities are active.
          Below is a list of heroes and abilities that make up the draft pool the active ones are in colour while the disabled ones are greyed out.

            Cluckles Says

Only 3 Heroes left out! We feel bad for you Morphling, Invoker and Earth Spirit...

            <template v-for="hero in computedItems">

                <template v-for="ability in hero.abilities">

                </template>

            </template>

    <hgv-loader :color="'#ffc107'"></hgv-loader>

</template>

<template>

Draft Pool

Is A Hero or Ability in AD?

          Find out exactly which heroes and which abilities are active.
          Below is a list of heroes and abilities that make up the draft pool the active ones are in colour while the disabled ones are greyed out.

            Cluckles Says

With all the new additions it gives hope that one day all heroes will be included!

            <template v-for="hero in computedItems">

                <template v-for="ability in hero.abilities">

                </template>

            </template>

    <hgv-loader :color="'#ffc107'"></hgv-loader>

</template>


Update src/components/Room.vue

Update src/components/Room.vue

update room.vue

<template>

    <v-card>
      <v-list two-line subheader>
        <v-subheader>Messages</v-subheader>
        <v-list-tile v-for="(thread, index) in Threads" :key="index">
        <v-list-tile-avatar  color="grey lighten-4">

        </v-list-tile-avatar>
          <v-list-tile-content>
            <v-list-tile-title>{{ thread.message }}</v-list-tile-title>
          </v-list-tile-content>
          <v-list-tile-action v-if="thread.uid === user.uid">
            <v-icon color="black" @click="remove(thread['.key'])">delete</v-icon>
          </v-list-tile-action>
        </v-list-tile>
      </v-list>
      <v-container>
        <v-textarea label="Message" v-model="Thread.message" @keyup.enter="newMessage()"></v-textarea>
        <v-btn @click="newMessage()">Send Message!</v-btn>
      </v-container>
    </v-card>

        <v-progress-circular :size="200" :width="7" indeterminate color="amber"></v-progress-circular>

</template>