hotfix: explicitly disable sorting when filtering complex-lists

<docs>
  # `complex-list`

  An array of objects with arbitrary properties. Each property may have any inputs a field is allowed to have, including custom inputs. Complex-list is similar to [Angular's _transcluded directives_](https://nulogy.com/who-we-are/company-blog/articles/transclusion-in-angular/) or [Advanced Custom Fields' _repeater field_](https://www.advancedcustomfields.com/add-ons/repeater-field/), in that each item in the list is treated like a separate field. Like fields, items must also have `_label`, but may not have `_placeholder`.

  ### Complex List Arguments

  * **props** an array of objects, representing the fields in each item. Each item should have a name, defined by `prop: 'name'`, as well as `_label` and the input that item uses.
  * **collapse** a property that should be used as the title for items. If `collapse` is set, all but the current item will be collapsed, only displaying its title. This is useful for lists with lots of complicated items.
  * **filder** boolean determining if the items may be filtered. If `true`, will add a search box at the top of the list.

  ### Complex List Usage

  * When a complex-list is empty, it will display a `add` button to add the initial item
  * Items can be added by clicking the `add` button
  * When a complex-list is _not_ empty, the focused item will have actions it, with `add` and `remove` buttons
  * Items can be removed by clicking the `remove` button
  * Items can be reordered by clicking the up and down carets next to their location
  * If complex-list is filterable, typing in the search box will match all text inputs in the list items

  ```yaml
  links:
    _has:
      input: complex-list
      collapse: title
      filter: true
      props:
        -
          prop: url
          _label: URL
          _has:
            input: text
            type: url
        -
          prop: title
          _label: Title
          _has: text
  ```

  > #### info::Note
  >
  > Complex lists don't have any of the common shared arguments, and don't display a field label.
</docs>

<style lang="sass">
  @import '../styleguide/animations';

  .complex-list {
    margin: 0;
    width: 100%;

    > .ui-textbox {
      margin-bottom: 10px;
    }
  }

  .hide-show-enter,
  .hide-show-leave-to {
    opacity: 0;
  }

  .hide-show-enter-to,
  .hide-show-leave {
    opacity: 1;
  }

  .hide-show-enter-active,
  .hide-show-leave-active {
    transition: opacity $standard-time $standard-curve;
  }
</style>

<template>
  <transition mode="out-in" name="hide-show" @after-enter="onResize">
    <div class="complex-list" v-if="items.length" v-click-outside="unselect">
      <ui-textbox v-if="isFilterable" type="text" label="Filter List"
        v-model.trim="query"
        :autofocus="true"
        :floatingLabel="true"></ui-textbox>
      <transition-group mode="out-in" name="hide-show" tag="div" class="complex-list-items" @after-enter="onListResize">
        <item v-for="(item, index) in matches"
          :index="index"
          :total="items.length"
          :originalItems="items"
          :name="name + '.' + index"
          :data="item"
          :schema="args"
          :key="index"
          :isFiltered="isFiltered"
          :currentItem="currentItem"
          :addItem="addItem"
          :removeItem="removeItem"
          :moveItem="moveItem"
          @current="onCurrentChange">
        </item>
      </transition-group>
    </div>
    <ui-button v-else buttonType="button" color="accent" icon="add" @click.stop.prevent="addItem(-1)">Add Items</ui-button>
  </transition>
</template>

<script>
  import _ from 'lodash';
  import Fuse from 'fuse.js';
  import item from './complex-list-item.vue';
  import UiButton from 'keen/UiButton';
  import UiTextbox from 'keen/UiTextbox';
  import { UPDATE_FORMDATA } from '../lib/forms/mutationTypes';

  /**
   * recursively build keys to filter by
   * @param  {object} schema
   * @param  {string} [path] used when recursing
   * @return {array}
   */

  function buildKeys(schema, path = '') {
    let keys = [];

    _.each(_.get(schema, '_has.props', []), (prop) => {
      const input = _.isString(prop._has) ? prop._has : _.get(prop, '_has.input'),
        key = path ? `${path}.${prop.prop}` : prop.prop;

      if (_.includes(['text', 'wysiwyg'], input)) {
        // text prop, add it to the keys
        keys.push(key);
      } else if (input === 'select' && !_.get(prop, '_has.multiple')) {
        // single select
        keys.push(key);
      } else if (input === 'complex-list') {
        keys.concat(buildKeys(prop, prop.prop));
      }
    });

    return keys;
  }

  /**
   * filter content by query
   * @param  {array} items
   * @param  {string} query
   * @param  {array} keys
   * @return {array}
   */
  function filterContent(items, query, keys) {
    const options = {
        shouldSort: false,
        tokenize: true,
        findAllMatches: true,
        threshold: 0.3,
        location: 0,
        distance: 100,
        maxPatternLength: 32,
        minMatchCharLength: 1,
        keys: keys
      },
      fuse = new Fuse(items, options);

    return fuse.search(query);
  }

  export default {
    props: ['name', 'data', 'schema', 'args'],
    data() {
      return {
        currentItem: _.isArray(this.data) ? this.data.length - 1 : 0,
        query: ''
      };
    },
    computed: {
      items() {
        return _.isArray(this.data) ? this.data : []; // _index is the original index, used when filtered
      },
      keys() {
        return this.isFilterable ? buildKeys(this.schema) : [];
      },
      matches() {
        return this.query.length && this.isFilterable ? filterContent(this.items, this.query, this.keys) : this.items;
      },
      isFilterable() {
        return _.get(this.schema, '_has.filter');
      },
      isFiltered() {
        return this.query.length > 0;
      }
    },
    methods: {
      updateFormData(items) {
        this.$store.commit(UPDATE_FORMDATA, { path: this.name, data: items });
      },
      onListResize() {
        this.$emit('resize'); // potentially resize the form (after transitioning elements)
      },
      onResize(el) {
        if (el && el.classList.contains('complex-list')) {
          this.$emit('resize'); // resize the form because we've displayed the list
        }
      },
      addItem(index) {
        const props = _.map(_.get(this.args, 'props', []), (item) => item.prop),
          newObj = _.reduce(props, (obj, prop) => {
            obj[prop] = null;
            return obj;
          }, {});

        let items = _.cloneDeep(this.items);

        items.splice(index + 1, 0, newObj); // add new item after the specified index
        this.currentItem = index + 1; // select new item
        this.updateFormData(items); // save the data
      },
      removeItem(index) {
        let items = _.cloneDeep(this.items);

        items.splice(index, 1); // remove item at the specified index
        this.updateFormData(items);
      },
      moveItem(index, direction) {
        let items = _.cloneDeep(this.items),
          item = _.head(items.splice(index, 1));

        if (direction === 'up') {
          // up one
          items.splice(index - 1, 0, item);
          this.currentItem = index - 1;
        } else {
          // down one
          items.splice(index + 1, 0, item);
          this.currentItem = index + 1;
        }
        this.updateFormData(items); // save the data
      },
      unselect() {
        this.currentItem = null;
      },
      onCurrentChange(index) {
        this.currentItem = index;
      }
    },
    components: {
      item,
      UiButton,
      UiTextbox
    }
  };
</script>

<docs>
  # `complex-list`

  An array of objects with arbitrary properties. Each property may have any inputs a field is allowed to have, including custom inputs. Complex-list is similar to [Angular's _transcluded directives_](https://nulogy.com/who-we-are/company-blog/articles/transclusion-in-angular/) or [Advanced Custom Fields' _repeater field_](https://www.advancedcustomfields.com/add-ons/repeater-field/), in that each item in the list is treated like a separate field. Like fields, items must also have `_label`, but may not have `_placeholder`.

  ### Complex List Arguments

  * **props** an array of objects, representing the fields in each item. Each item should have a name, defined by `prop: 'name'`, as well as `_label` and the input that item uses.
  * **collapse** a property that should be used as the title for items. If `collapse` is set, all but the current item will be collapsed, only displaying its title. This is useful for lists with lots of complicated items.
  * **filder** boolean determining if the items may be filtered. If `true`, will add a search box at the top of the list.

  ### Complex List Usage

  * When a complex-list is empty, it will display a `add` button to add the initial item
  * Items can be added by clicking the `add` button
  * When a complex-list is _not_ empty, the focused item will have actions it, with `add` and `remove` buttons
  * Items can be removed by clicking the `remove` button
  * Items can be reordered by clicking the up and down carets next to their location
  * If complex-list is filterable, typing in the search box will match all text inputs in the list items

  ```yaml
  links:
    _has:
      input: complex-list
      collapse: title
      filter: true
      props:
        -
          prop: url
          _label: URL
          _has:
            input: text
            type: url
        -
          prop: title
          _label: Title
          _has: text
  ```

  > #### info::Note
  >
  > Complex lists don't have any of the common shared arguments, and don't display a field label.
</docs>

<style lang="sass">
  @import '../styleguide/animations';

  .complex-list {
    margin: 0;
    width: 100%;

    > .ui-textbox {
      margin-bottom: 10px;
    }
  }

  .hide-show-enter,
  .hide-show-leave-to {
    opacity: 0;
  }

  .hide-show-enter-to,
  .hide-show-leave {
    opacity: 1;
  }

  .hide-show-enter-active,
  .hide-show-leave-active {
    transition: opacity $standard-time $standard-curve;
  }
</style>

<template>
  <transition mode="out-in" name="hide-show" @after-enter="onResize">
    <div class="complex-list" v-if="items.length" v-click-outside="unselect">
      <ui-textbox v-if="isFilterable" type="text" label="Filter List"
        v-model.trim="query"
        :autofocus="true"
        :floatingLabel="true"></ui-textbox>
      <transition-group mode="out-in" name="hide-show" tag="div" class="complex-list-items" @after-enter="onListResize">
        <item v-for="(item, index) in matches"
          :index="index"
          :total="items.length"
          :originalItems="items"
          :name="name + '.' + index"
          :data="item"
          :schema="args"
          :key="index"
          :isFiltered="isFiltered"
          :currentItem="currentItem"
          :addItem="addItem"
          :removeItem="removeItem"
          :moveItem="moveItem"
          @current="onCurrentChange">
        </item>
      </transition-group>
    </div>
    <ui-button v-else buttonType="button" color="accent" icon="add" @click.stop.prevent="addItem(-1)">Add Items</ui-button>
  </transition>
</template>

<script>
  import _ from 'lodash';
  import Fuse from 'fuse.js';
  import item from './complex-list-item.vue';
  import UiButton from 'keen/UiButton';
  import UiTextbox from 'keen/UiTextbox';
  import { UPDATE_FORMDATA } from '../lib/forms/mutationTypes';

  /**
   * recursively build keys to filter by
   * @param  {object} schema
   * @param  {string} [path] used when recursing
   * @return {array}
   */

  function buildKeys(schema, path = '') {
    let keys = [];

    _.each(_.get(schema, '_has.props', []), (prop) => {
      const input = _.isString(prop._has) ? prop._has : _.get(prop, '_has.input'),
        key = path ? `${path}.${prop.prop}` : prop.prop;

      if (_.includes(['text', 'wysiwyg'], input)) {
        // text prop, add it to the keys
        keys.push(key);
      } else if (input === 'select' && !_.get(prop, '_has.multiple')) {
        // single select
        keys.push(key);
      } else if (input === 'complex-list') {
        keys.concat(buildKeys(prop, prop.prop));
      }
    });

    return keys;
  }

  /**
   * filter content by query
   * @param  {array} items
   * @param  {string} query
   * @param  {array} keys
   * @return {array}
   */
  function filterContent(items, query, keys) {
    const options = {
        tokenize: true,
        findAllMatches: true,
        threshold: 0.3,
        location: 0,
        distance: 100,
        maxPatternLength: 32,
        minMatchCharLength: 1,
        keys: keys
      },
      fuse = new Fuse(items, options);

    return fuse.search(query);
  }

  export default {
    props: ['name', 'data', 'schema', 'args'],
    data() {
      return {
        currentItem: _.isArray(this.data) ? this.data.length - 1 : 0,
        query: ''
      };
    },
    computed: {
      items() {
        return _.isArray(this.data) ? this.data : []; // _index is the original index, used when filtered
      },
      keys() {
        return this.isFilterable ? buildKeys(this.schema) : [];
      },
      matches() {
        return this.query.length && this.isFilterable ? filterContent(this.items, this.query, this.keys) : this.items;
      },
      isFilterable() {
        return _.get(this.schema, '_has.filter');
      },
      isFiltered() {
        return this.query.length > 0;
      }
    },
    methods: {
      updateFormData(items) {
        this.$store.commit(UPDATE_FORMDATA, { path: this.name, data: items });
      },
      onListResize() {
        this.$emit('resize'); // potentially resize the form (after transitioning elements)
      },
      onResize(el) {
        if (el && el.classList.contains('complex-list')) {
          this.$emit('resize'); // resize the form because we've displayed the list
        }
      },
      addItem(index) {
        const props = _.map(_.get(this.args, 'props', []), (item) => item.prop),
          newObj = _.reduce(props, (obj, prop) => {
            obj[prop] = null;
            return obj;
          }, {});

        let items = _.cloneDeep(this.items);

        items.splice(index + 1, 0, newObj); // add new item after the specified index
        this.currentItem = index + 1; // select new item
        this.updateFormData(items); // save the data
      },
      removeItem(index) {
        let items = _.cloneDeep(this.items);

        items.splice(index, 1); // remove item at the specified index
        this.updateFormData(items);
      },
      moveItem(index, direction) {
        let items = _.cloneDeep(this.items),
          item = _.head(items.splice(index, 1));

        if (direction === 'up') {
          // up one
          items.splice(index - 1, 0, item);
          this.currentItem = index - 1;
        } else {
          // down one
          items.splice(index + 1, 0, item);
          this.currentItem = index + 1;
        }
        this.updateFormData(items); // save the data
      },
      unselect() {
        this.currentItem = null;
      },
      onCurrentChange(index) {
        this.currentItem = index;
      }
    },
    components: {
      item,
      UiButton,
      UiTextbox
    }
  };
</script>


fix sam's issue with file extension. it now only accepts .lfr or .mint files

<template>
  <v-container
    id="dashboard"
    fluid
    tag="section"
  >
    <v-row>

      <v-col
        cols="12"
      >

          Workspaces

      </v-col>
      <v-col
        cols="12"
        lg="3"
        v-for="(workspace, key, i) in workspaces"
        :key="i"
      >
        <base-material-workspace-chart-card
          :id="workspace._id"
          hover-reveal
          color="info"
          type="Line"
        >
          <template v-slot:reveal-actions>
            <v-tooltip bottom>
              <template v-slot:activator="{ attrs, on }">
                <v-btn
                  v-bind="attrs"
                  color="info"
                  icon
                  v-on="on"
                >
                  <v-icon
                    color="info"
                  >
                    mdi-refresh
                  </v-icon>
                </v-btn>
              </template>

              Refresh
            </v-tooltip>

            <v-tooltip bottom>
              <template v-slot:activator="{ attrs, on }">
                <v-btn
                  v-bind="attrs"
                  light
                  icon
                  v-on="on"
                  v-on:click="selectworkspace(workspace._id)"
                >
                  <v-icon>mdi-view-split-vertical</v-icon>
                </v-btn>
              </template>

              View Files
            </v-tooltip>

            <v-tooltip bottom>
              <template v-slot:activator="{ attrs, on }">
                <v-btn
                  v-bind="attrs"
                  light
                  icon
                  v-on="on"
                  v-on:click="deleteworkspace(workspace._id)"
                >
                  <v-icon color="error">mdi-delete</v-icon>
                </v-btn>
              </template>

              Delete Workspace
            </v-tooltip>
          </template>

            {{workspace.name}}

          <template v-slot:actions>
            <v-icon
              class="mr-1"
              small
            >
              mdi-clock-outline
            </v-icon>
            Last Update: {{formattimestamp(workspace.updated_at)}}
          </template>
        </base-material-workspace-chart-card>
      </v-col>

        <v-col
            cols="12"
            sm="3"
            lg="3"
            align="center"
        >

            <v-tooltip top>
                <template v-slot:activator="{ on }">
                <v-btn
                    v-on="on"
                    class="newbutton"
                    color="success"
                    fab
                    x-large
                    dark
                    @click="newworkspacedialog = true"
                    >
                    <v-icon>mdi-plus</v-icon>
                </v-btn>
                </template>
                Create New Workspace
            </v-tooltip>
                <v-dialog
                    v-model="newworkspacedialog"
                    max-width="300px"
                    >
                    <v-card>
                    <v-card-title>
                        Create New Workspace
                    </v-card-title>
                    <v-card-text>
                        <v-text-field
                            v-model="newworkspacename"
                            label="Workspace Name"
                            ></v-text-field>
                    </v-card-text>
                    <v-card-actions>
                        <v-spacer></v-spacer>
                        <v-btn
                            color="success"
                            text
                            @click="createworkspace()"
                            >
                            Create
                        </v-btn>

                        <v-btn
                            color="primary"
                            text
                            @click="newworkspacedialog = false"
                            >
                            Cancel
                        </v-btn>

                    </v-card-actions>
                    </v-card>
                </v-dialog>

        </v-col>

    </v-row>
            <v-row
                v-if="files.length > 0"
                >
                <v-col
                    cols="12"

                >

                    Files

                </v-col>
                <v-col
                    cols="12"
                    sm="3"
                    lg="3"
                    v-for="(file, i) in files" :key="i"
                >
                    <base-material-workspace-stats-card
                        color="info"
                        icon="mdi-file"
                        title="File Type:"
                        :value="file.ext"
                        :name="file.name"
                        sub-icon="mdi-clockwise-outline"
                        :sub-text=" 'Modified: ' + formattimestamp(file.updated_at)"
                        :id="file.id"
                        :workspaceid="selectedworkspace._id"
                        v-on:onFileDeleted="refreshFiles"
                    />
                </v-col>
                <v-col
                    cols="12"
                    sm="3"
                    lg="3"
                    v-if="files.length > 0"
                    align="center"
                >

                    <v-tooltip top>
                        <template v-slot:activator="{ on }">
                        <v-btn
                            v-on="on"
                            class="newbutton"
                            color="success"
                            fab
                            x-large
                            dark
                            @click="newfiledialog = true"
                            >
                            <v-icon>mdi-plus</v-icon>
                        </v-btn>
                        </template>
                        Create New File
                    </v-tooltip>
                                    <v-dialog
                    v-model="newfiledialog"
                    max-width="300px"
                    >
                    <v-card>
                    <v-card-title>
                        Create New File
                    </v-card-title>
                    <v-card-text>
                      Please add a file extension name at the end of the filename: .lfr or .mint
                    </v-card-text>
                    <v-card-text>
                        <v-text-field
                            v-model="newfilename"
                            label="File Name"
                            :rules="rules"
                            hide-details="auto"
                            ></v-text-field>
                    </v-card-text>
                    <v-card-actions>
                        <v-spacer></v-spacer>
                        <v-btn
                            color="success"
                            text
                            @click="createfile()"
                            >
                            Create
                        </v-btn>

                        <v-btn
                            color="primary"
                            text
                            @click="newfiledialog = false"
                            >
                            Cancel
                        </v-btn>

                    </v-card-actions>
                    </v-card>
                </v-dialog>

        </v-col>
    </v-row>
  </v-container>
</template>

<template>
  <v-container
    id="dashboard"
    fluid
    tag="section"
  >
    <v-row>

      <v-col
        cols="12"
      >

          Workspaces

      </v-col>
      <v-col
        cols="12"
        lg="3"
        v-for="(workspace, key, i) in workspaces"
        :key="i"
      >
        <base-material-workspace-chart-card
          :id="workspace._id"
          hover-reveal
          color="info"
          type="Line"
        >
          <template v-slot:reveal-actions>
            <v-tooltip bottom>
              <template v-slot:activator="{ attrs, on }">
                <v-btn
                  v-bind="attrs"
                  color="info"
                  icon
                  v-on="on"
                >
                  <v-icon
                    color="info"
                  >
                    mdi-refresh
                  </v-icon>
                </v-btn>
              </template>

              Refresh
            </v-tooltip>

            <v-tooltip bottom>
              <template v-slot:activator="{ attrs, on }">
                <v-btn
                  v-bind="attrs"
                  light
                  icon
                  v-on="on"
                  v-on:click="selectworkspace(workspace._id)"
                >
                  <v-icon>mdi-view-split-vertical</v-icon>
                </v-btn>
              </template>

              View Files
            </v-tooltip>

            <v-tooltip bottom>
              <template v-slot:activator="{ attrs, on }">
                <v-btn
                  v-bind="attrs"
                  light
                  icon
                  v-on="on"
                  v-on:click="deleteworkspace(workspace._id)"
                >
                  <v-icon color="error">mdi-delete</v-icon>
                </v-btn>
              </template>

              Delete Workspace
            </v-tooltip>
          </template>

            {{workspace.name}}

          <template v-slot:actions>
            <v-icon
              class="mr-1"
              small
            >
              mdi-clock-outline
            </v-icon>
            Last Update: {{formattimestamp(workspace.updated_at)}}
          </template>
        </base-material-workspace-chart-card>
      </v-col>

        <v-col
            cols="12"
            sm="3"
            lg="3"
            align="center"
        >

            <v-tooltip top>
                <template v-slot:activator="{ on }">
                <v-btn
                    v-on="on"
                    class="newbutton"
                    color="success"
                    fab
                    x-large
                    dark
                    @click="newworkspacedialog = true"
                    >
                    <v-icon>mdi-plus</v-icon>
                </v-btn>
                </template>
                Create New Workspace
            </v-tooltip>
                <v-dialog
                    v-model="newworkspacedialog"
                    max-width="300px"
                    >
                    <v-card>
                    <v-card-title>
                        Create New Workspace
                    </v-card-title>
                    <v-card-text>
                        <v-text-field
                            v-model="newworkspacename"
                            label="Workspace Name"
                            ></v-text-field>
                    </v-card-text>
                    <v-card-actions>
                        <v-spacer></v-spacer>
                        <v-btn
                            color="success"
                            text
                            @click="createworkspace()"
                            >
                            Create
                        </v-btn>

                        <v-btn
                            color="primary"
                            text
                            @click="newworkspacedialog = false"
                            >
                            Cancel
                        </v-btn>

                    </v-card-actions>
                    </v-card>
                </v-dialog>

        </v-col>

    </v-row>
            <v-row
                v-if="files.length > 0"
                >
                <v-col
                    cols="12"

                >

                    Files

                </v-col>
                <v-col
                    cols="12"
                    sm="3"
                    lg="3"
                    v-for="(file, i) in files" :key="i"
                >
                    <base-material-workspace-stats-card
                        color="info"
                        icon="mdi-file"
                        title="File Type:"
                        :value="file.ext"
                        :name="file.name"
                        sub-icon="mdi-clockwise-outline"
                        :sub-text=" 'Modified: ' + formattimestamp(file.updated_at)"
                        :id="file.id"
                        :workspaceid="selectedworkspace._id"
                        v-on:onFileDeleted="refreshFiles"
                    />
                </v-col>
                <v-col
                    cols="12"
                    sm="3"
                    lg="3"
                    v-if="files.length > 0"
                    align="center"
                >

                    <v-tooltip top>
                        <template v-slot:activator="{ on }">
                        <v-btn
                            v-on="on"
                            class="newbutton"
                            color="success"
                            fab
                            x-large
                            dark
                            @click="newfiledialog = true"
                            >
                            <v-icon>mdi-plus</v-icon>
                        </v-btn>
                        </template>
                        Create New File
                    </v-tooltip>
                                    <v-dialog
                    v-model="newfiledialog"
                    max-width="300px"
                    >
                    <v-card>
                    <v-card-title>
                        Create New File
                    </v-card-title>
                    <v-card-text>
                        <v-text-field
                            v-model="newfilename"
                            label="File Name"
                            ></v-text-field>
                    </v-card-text>
                    <v-card-actions>
                        <v-spacer></v-spacer>
                        <v-btn
                            color="success"
                            text
                            @click="createfile()"
                            >
                            Create
                        </v-btn>

                        <v-btn
                            color="primary"
                            text
                            @click="newfiledialog = false"
                            >
                            Cancel
                        </v-btn>

                    </v-card-actions>
                    </v-card>
                </v-dialog>

        </v-col>
    </v-row>
  </v-container>
</template>


Fall back if the framework's configuration doesn't exist CA-433

<template>

        <RightAside v-if="showRightAside" />

            <FrameworkEditorToolbar
                @showExportModal="onOpenExportModal"
                @changeProperties="changeProperties" />

                        <Component
                            :is="dynamicThingComponent"
                            :id="'scroll-' + framework.shortId().split('/').pop()"
                            :obj="framework"
                            :repo="repo"
                            :parentNotEditable="queryParams.view==='true'"
                            :profile="frameworkProfile"
                            @deleteObject="deleteObject"
                            @removeObject="removeObject"
                            @editNodeEvent="onEditNode()"
                            @doneEditingNodeEvent="onDoneEditingNode()"
                            :properties="properties">

                                    {{ framework.competency.length }} item

                                 1">
                                    {{ framework.competency.length }} items

                                    Last modified {{ lastModified }}

                                    Created {{ $moment(framework['schema:dateCreated']).fromNow() }}

                                    Approved

                                Published

                        </Component>

                        <Hierarchy
                            :container="framework"
                            containerType="Framework"
                            containerTypeGet="EcFramework"
                            containerNodeProperty="competency"
                            containerEdgeProperty="relation"
                            nodeType="EcCompetency"
                            edgeType="EcAlignment"
                            edgeRelationProperty="relationType"
                            edgeRelationLiteral="narrows"
                            edgeSourceProperty="source"
                            edgeTargetProperty="target"
                            :viewOnly="queryParams.view === 'true'"
                            :repo="repo"
                            :exportOptions="competencyExportOptions"
                            :highlightList="highlightCompetency"
                            :profile="competencyProfile"
                            @deleteObject="deleteObject"
                            @editMultipleEvent="onEditMultiple"
                            @removeObject="removeObject"
                            @exportObject="exportObject"
                            @searchThings="handleSearch($event)"
                            @selectButtonClick="onSelectButtonClick"
                            :properties="properties"
                            @selectedArray="selectedArrayEvent"
                            @doneLoadingNodes="preloadRelations" />

</template>


clean

clean

<template>

1

Network

              ({{ selectedNetwork.type.name }} - {{ selectedNetwork.service }})

Cancel

No

Icon

{{ key }}

                  {{ net.service }}

2

Address

{{ $t('accessWallet.hdDerivationPath') }}

                  {{ val.label }}

                    {{ $t('accessWallet.customPaths') }}

{{ val.label }}

                    {{ $t('accessWallet.addCustomPath') }}

              {{
                $t('accessWallet.invalidPathDesc', { path: invalidPath.path })
              }}

              {{ getPathLabel(selectedPath) }} ({{ selectedPath }})

              Alias
              <input
                id="customPathLabel"
                v-model="customPath.label"
                placeholder="my custom path"
              />
              Path
              <input
                id="customPathInput"
                v-model="customPath.path"
                placeholder="m/44'/1'/0'/0"
              />
              <button class="submit-button cancel" @click="showCustomPathInput">
                {{ $t('common.cancel') }}
              </button>
              <button class="submit-button submit" @click="addCustomPath">
                {{ $t('accessWallet.addCustomPath') }}
              </button>

{{ $t('accessWallet.interactAddr') }}

{{ $t('accessWallet.id') }}

{{ $t('common.address') }}

{{ $t('common.balance') }}

                  <blockie
                    :address="account.account.getChecksumAddressString()"
                    :size="8"
                    :scale="16"
                    width="30px"
                    height="30px"
                  />

                  {{ account.account.getChecksumAddressString() | concatAddr }}

{{ convertBalance(account.balance) }}

                    <input
                      :id="'address' + account.index"
                      type="checkbox"
                      @click="unselectAllAddresses"
                    />

              < {{ $t('common.previous') }}
              {{ $t('common.next') }} >

              {{ $t('accessWallet.acceptTerms') }}
              <router-link to="/terms-and-conditions"
                >{{ $t('common.terms') }}.</router-link
              >
              <input
                ref="accessMyWalletBtn"
                type="checkbox"
                @click="accessMyWalletBtnDisabled = !accessMyWalletBtnDisabled"
              />

            {{ $t('common.accessMyWallet') }}

          <customer-support />

</template>


Remove the notification bar

Remove the notification bar

<template>

        I code in Python/Django/Flask and lately I've been doing javascript also. I'm not a
        design-guy so I'm usually assigned to do backend stuffs.

I'm in continuous
        loop of learning and experimenting new technologies ergo my ever-presence on many tech
        conferences, hackathons, and codecamps.

I do blogs too. I write about tech,
        hackathon, experiences, events, anything random that comes to mind.

Recent Posts

              <router-link class="router-title"
                           :to="{ name: 'Page', params: { slug: post.slug }}">
                {{ post.title }}
              </router-link>

</template>

<template>

      Site is under construction. Please visit
      zorexsalvo.com.

        I code in Python/Django/Flask and lately I've been doing javascript also. I'm not a
        design-guy so I'm usually assigned to do backend stuffs.

I'm in continuous
        loop of learning and experimenting new technologies ergo my ever-presence on many tech
        conferences, hackathons, and codecamps.

I do blogs too. I write about tech,
        hackathon, experiences, events, anything random that comes to mind.

Recent Posts

              <router-link class="router-title"
                           :to="{ name: 'Page', params: { slug: post.slug }}">
                {{ post.title }}
              </router-link>

</template>


dont show subscription card on subgroup

dont show subscription card on subgroup

<template lang="pug">
v-card.my-6(v-if="canSee" outlined)
  v-card-title
    v-icon.mr-2(color="primary") mdi-rocket
    h1.headline(v-t="'group_page.options.subscription_status'")
    v-btn.ml-2(icon small href="https://help.loomio.org/en/subscriptions/" target="_blank" :title="$t('common.help')")
      v-icon(color="accent") mdi-help-circle-outline
  div.px-6
    v-simple-table(dense)
      template(v-slot:default)
        tbody
          tr(v-for="(val, key) in tableData" :key="key" v-if="val")
            td(v-t="'subscription_status.' + key")
            td(v-html="val")
  v-card-actions
    v-spacer
    v-btn(color="primary" href="/upgrade" target="_blank") Upgrade
</template>


Added turns to the user profile table

Added turns to the user profile table

<template>

                    {{ user.username }}

{{ user.level }} at <a  :href="`/programming-competition-leaderboard?organization=${user.organization_id}`">{{ user.organization }}

                      From <a :href="`/programming-competition-leaderboard?country=${user.country_code}`">{{user.location}}

 0">Bots in
                        <template v-for="(lang, index) in botLang">
                             0" class="hl"><a  :href="`/programming-competition-leaderboard?language=${lang}`">{{lang}}<span v-if="(index+1) < botLang.length">,
                        </template>

 {{ user.rank ? `rank ${user.rank}` : "No Rank" }}, {{ user.tier || "Salt" }} tier

Points

{{ Math.round(user.score * 100) / 100 }}

Bots

{{ user.num_submissions }}

Games

{{ user.num_games }}

<a :href="`/programming-competition-leaderboard?show_user=${user.user_id}`">View on leaderboard

                    Game Videos Feed

                    <img :src="`${baseUrl}/assets/images/temp/game_video.png`" class="icon-">

Nothing to show

Complete your first game and view replays
here

                                Played On
                                Opponents
                                Map Size
                                Watch
                                Won
                                Turns

                                    <time :datetime="game.time_played"
                                          :title="game.time_played">
                                        {{ game.time_played | moment("calendar") }}
                                    </time>

                                            {{ player.rank }}

                                {{ game.map_width }}x{{ game.map_height }}

                                        {{ game.game_id }}

                                  {{ game.players[user.user_id].rank === 1 ? 'Won' : '' }}

                                  {{ game.turns_total }}

                        <button
                            type="button"
                            class="btn"
                            :disabled="page === 0"
                            v-on:click="prev_page">Prev</button>
                        <button
                            type="button"
                            class="btn"
                            v-on:click="next_page">Next</button>

                    Nemeses

                    <img :src="`${baseUrl}/assets/images/temp/game_video.png`" class="icon-">

No nemeses yet

Submit your first bot to uncover your nemeses
here

 0">

                                    Nemesis
                                    Games
                                    Win %
                                    Loss %

                                       Nemesis
                                        Games
                                        Win %
                                        Loss %

                                                    {{nemesis.id}}

                                            {{nemesis.total}}

                                            {{nemesis.wins}}

                                            {{nemesis.losses}}

                    Your Hackathons

                    <img :src="`${baseUrl}/assets/images/temp/event.png`" class="icon-">

Your haven't join any event

Start joining a Hackathon by adding code to
your profile

                            <a :href="`${baseUrl}/hackathon-and-events`" class="ha-button">Join a Hackathon

                <form v-if="is_my_page" class="profile-section-right-form">

                        <input type="text" placeholder="Hackathon signup code" ref="hackathon_signup_code">
                        <button class="btn" v-on:click="join_hackathon">Join Hackathon</button>

                </form>

 0">

                                    Hackathons
                                    Location
                                    Status

                                        Hackathons
                                        Location
                                        Status

                                        {{hackathon.title}}
                                        {{hackathon.location}}
                                        {{hackathon.status.charAt(0).toUpperCase() + hackathon.status.slice(1)}}
                                        View Hackathon

                    Your Errors

                                    Id
                                    Reason
                                    Date
                                    Log File
                                    Game File

                                        Id
                                        Reason
                                        Date
                                        Log File
                                        Game File

                                        {{game.game_id}}
                                        Time out
                                        <time :datetime="game.time_played"
                                                  :title="game.time_played">
                                                {{ game.time_played | moment("calendar") }}
                                            </time>
                                        Download Log
                                        Download Game

</template>

<template>

                    {{ user.username }}

{{ user.level }} at <a  :href="`/programming-competition-leaderboard?organization=${user.organization_id}`">{{ user.organization }}

                      From <a :href="`/programming-competition-leaderboard?country=${user.country_code}`">{{user.location}}

 0">Bots in
                        <template v-for="(lang, index) in botLang">
                             0" class="hl"><a  :href="`/programming-competition-leaderboard?language=${lang}`">{{lang}}<span v-if="(index+1) < botLang.length">,
                        </template>

 {{ user.rank ? `rank ${user.rank}` : "No Rank" }}, {{ user.tier || "Salt" }} tier

Points

{{ Math.round(user.score * 100) / 100 }}

Bots

{{ user.num_submissions }}

Games

{{ user.num_games }}

<a :href="`/programming-competition-leaderboard?show_user=${user.user_id}`">View on leaderboard

                    Game Videos Feed

                    <img :src="`${baseUrl}/assets/images/temp/game_video.png`" class="icon-">

Nothing to show

Complete your first game and view replays
here

                                Played On
                                Opponents
                                Map Size
                                Watch
                                Won

                                    <time :datetime="game.time_played"
                                          :title="game.time_played">
                                        {{ game.time_played | moment("calendar") }}
                                    </time>

                                            {{ player.rank }}

                                {{ game.map_width }}x{{ game.map_height }}

                                        {{ game.game_id }}

                                  {{ game.players[user.user_id].rank === 1 ? 'Won' : '' }}

                        <button
                            type="button"
                            class="btn"
                            :disabled="page === 0"
                            v-on:click="prev_page">Prev</button>
                        <button
                            type="button"
                            class="btn"
                            v-on:click="next_page">Next</button>

                    Nemeses

                    <img :src="`${baseUrl}/assets/images/temp/game_video.png`" class="icon-">

No nemeses yet

Submit your first bot to uncover your nemeses
here

 0">

                                    Nemesis
                                    Games
                                    Win %
                                    Loss %

                                       Nemesis
                                        Games
                                        Win %
                                        Loss %

                                                    {{nemesis.id}}

                                            {{nemesis.total}}

                                            {{nemesis.wins}}

                                            {{nemesis.losses}}

                    Your Hackathons

                    <img :src="`${baseUrl}/assets/images/temp/event.png`" class="icon-">

Your haven't join any event

Start joining a Hackathon by adding code to
your profile

                            <a :href="`${baseUrl}/hackathon-and-events`" class="ha-button">Join a Hackathon

                <form v-if="is_my_page" class="profile-section-right-form">

                        <input type="text" placeholder="Hackathon signup code" ref="hackathon_signup_code">
                        <button class="btn" v-on:click="join_hackathon">Join Hackathon</button>

                </form>

 0">

                                    Hackathons
                                    Location
                                    Status

                                        Hackathons
                                        Location
                                        Status

                                        {{hackathon.title}}
                                        {{hackathon.location}}
                                        {{hackathon.status.charAt(0).toUpperCase() + hackathon.status.slice(1)}}
                                        View Hackathon

                    Your Errors

                                    Id
                                    Reason
                                    Date
                                    Log File
                                    Game File

                                        Id
                                        Reason
                                        Date
                                        Log File
                                        Game File

                                        {{game.game_id}}
                                        Time out
                                        <time :datetime="game.time_played"
                                                  :title="game.time_played">
                                                {{ game.time_played | moment("calendar") }}
                                            </time>
                                        Download Log
                                        Download Game

</template>


UI: .query type compatible

UI: .query type compatible

<template>

    <form class="ui form" method="GET" v-bind:class="{loading:loading}" v-on:submit.prevent v-on:keyup.enter="submit">

          {{$L('job name')}}
          <input type="text" v-model="names" :placeholder="$L('multiple names can separated by commas')">

          {{$L('job ID')}}
          <input type="text" v-model="ids" :placeholder="$L('multiple IDs can separated by commas')">

        {{$L('node')}}
        <input v-if="$store.getters.showWithHostname" type="text" v-model="hostnames" :placeholder="$L('multiple Hostnames can separated by commas')">
        <input v-else type="text" v-model="ips" :placeholder="$L('multiple IPs can separated by commas')">

          {{$L('starting date')}}
          <input type="date" v-model="begin">

          {{$L('end date')}}
          <input type="date" v-model="end">

            <input type="checkbox" class="hidden" v-model="latest">
            {{$L('latest result of each job on each node')}}

            <input type="checkbox" class="hidden" v-model="failedOnly">
            {{$L('failure only')}}

        <button class="fluid ui button" type="button" v-on:click="submit">{{$L('submit query')}}</button>

    </form>

 0">

          {{$L('job name')}}
          {{$L('executing node')}}
          {{$L('executing user')}}
          {{$L('executing time')}}
          {{$L('executing result')}}

          <router-link class="item" :to="'/job/edit/'+log.jobGroup+'/'+log.jobId">{{log.name}}</router-link>
          {{$store.getters.hostshows(log.node)}}
          {{log.user}}
           {{formatTime(log)}}

            <router-link :to="'/log/'+log.id">{{$L(log.success ? 'successed' : 'failed')}}</router-link> |

    <Pager v-if="list && list.length>0" :total="total" :maxBtn="5"/>
    <ExecuteJob ref="executeJobModal"/>

</template>


Replace fileManager update API call with createHistory

<template>
  <div>
    <userInputs
      v-on:changeViewMode="changeViewMode"
      v-on:execute="executeAction"
    ></userInputs>
    <documents
      v-on:changeFolder="changeFolder"
          :view-mode="viewMode"
          :docs="docs"
    ></documents>
  </div>
</template>

<script>
import fileManager from 'src/logic/filemanager';
import documents from './Documents';
import userInputs from './UserInputs';

export default {
  components: {
    documents,
    userInputs,
  },
  data() {
    return {
      viewMode: 'listView',
      docs: {},
      history: null
    };
  },
  watch: {
  },
  methods: {
    changeFolder(newFolder) {
      if (newFolder.id === 0) {
        this.$router.push({ path: '' });
      } else {
        this.$router.push({ path: '', query: { folder: newFolder.id } });
      }
      this.docs = newFolder;
      this.history.update(this.docs);
    },
    changeViewMode(viewMode) {
      this.viewMode = viewMode;
    },
    executeAction(action, doc) {
      switch (action) {
        case 'createFolder': {
          this.docs.createFolder('Folder');
          break;
        }
        case 'createFile': {
          this.docs.createFile('File');
          break;
        }
        case 'history back': {
          this.docs = this.history.previous();
          break;
        }
        case 'history forward': {
          this.docs = this.history.next();
          break;
        }
        case 'download': {
          this.downloadDoc(doc);
          break;
        }
        case 'delete': {
          console.log(doc);
          doc.remove();
          break;
        }
        default: break;
      }
    },
    downloadDoc(doc) {
      console.log(doc, 'doc');
      doc.load()
      .then((docContent) => {
        this.download(doc, docContent);
      });
    },
    download(doc, docContent) {
      const element = document.createElement('a');
      const href = 'data:text/plain;charset=utf-8,';
      const content = encodeURIComponent(docContent);
      element.setAttribute('href', href+content, docContent);
      const fileName = doc.name;
      element.setAttribute('download', fileName);

      element.style.display = 'none';
      document.body.appendChild(element);

      element.click();

      document.body.removeChild(element);
    }
  },
  mounted() {
    const ROOT_FOLDER_ID = 0;
    this.docs = fileManager.getFolderByID(ROOT_FOLDER_ID);
    this.history = fileManager.createHistory(this.docs);
  }
};

</script>

<style scoped lang="scss">
@import 'src/assets/styles/variables.scss';

</style>

<template>
  <div>
    <userInputs
      v-on:changeViewMode="changeViewMode"
      v-on:execute="executeAction"
    ></userInputs>
    <documents
      v-on:changeFolder="changeFolder"
          :view-mode="viewMode"
          :docs="docs"
    ></documents>
  </div>
</template>

<script>
import fileManager from 'src/logic/filemanager';
import documents from './Documents';
import userInputs from './UserInputs';

export default {
  components: {
    documents,
    userInputs,
  },
  data() {
    return {
      viewMode: 'listView',
      docs: {}
    };
  },
  watch: {
  },
  methods: {
    changeFolder(newFolder) {
      if (newFolder.id === 0) {
        this.$router.push({ path: '' });
      } else {
        this.$router.push({ path: '', query: { folder: newFolder.id } });
      }
      this.docs = newFolder;
      fileManager.update(this.docs);
    },
    changeViewMode(viewMode) {
      this.viewMode = viewMode;
    },
    executeAction(action, doc) {
      switch (action) {
        case 'createFolder': {
          this.docs.createFolder('Folder');
          break;
        }
        case 'createFile': {
          this.docs.createFile('File');
          break;
        }
        case 'history back': {
          this.docs = fileManager.previous();
          break;
        }
        case 'history forward': {
          this.docs = fileManager.next();
          break;
        }
        case 'download': {
          this.downloadDoc(doc);
          break;
        }
        case 'delete': {
          doc.remove();
          break;
        }
        default: break;
      }
    },
    downloadDoc(doc) {
      console.log(doc, 'doc');
      doc.load()
      .then((docContent) => {
        this.download(doc, docContent);
      });
    },
    download(doc, docContent) {
      const element = document.createElement('a');
      const href = 'data:text/plain;charset=utf-8,';
      const content = encodeURIComponent(docContent);
      element.setAttribute('href', href+content, docContent);
      const fileName = doc.name;
      element.setAttribute('download', fileName);

      element.style.display = 'none';
      document.body.appendChild(element);

      element.click();

      document.body.removeChild(element);
    }
  },
  mounted() {
    const ROOT_FOLDER_ID = 0;
    this.docs = fileManager.getFolderByID(ROOT_FOLDER_ID);
    fileManager.update(this.docs);
  }
};

</script>

<style scoped lang="scss">
@import 'src/assets/styles/variables.scss';

</style>


UI fix: Don't show assigned course instance if no members (#528)

<script>
  /** Given a class id, generates and populates the stats for the class component */
  import { mapGetters } from 'vuex'
  import ClassComponent from '../ClassComponent'

  export default {
    components: {
      ClassComponent
    },

    props: {
      classroomState: {
        type: Object,
        required: true
      }
    },

    computed: {
      ...mapGetters({
        levelSessionsMapForClassroom: 'levelSessions/getSessionsMapForClassroom',
        sortedCourses: 'courses/sorted',
        getCourseInstancesForClass: 'courseInstances/getCourseInstancesForClass'
      }),

      levelSessionsMapByUser () {
        return this.levelSessionsMapForClassroom(this.classroomState._id) || {}
      },

      classroomCreationDate () {
        return moment(parseInt(this.classroomState._id.substring(0, 8), 16) * 1000).format('MMMM Do, YYYY')
      },

      classroomStatsFromClassroom () {
        return {
          id: this.classroomState._id,
          name: this.classroomState.name,
          language: this.classroomState.aceConfig.language || 'python',
          numberOfStudents: this.classroomState.members.length || 0,
          classroomCreated: this.classroomCreationDate,
          archived: this.classroomState.archived
        }
      },

      // Maps the course Id to the levels associated.
      courseLevelsMap () {
        const map = new Map()
        const courseInstanceCourses = new Set()
        const courseInstances = this.getCourseInstancesForClass(this.classroomState.ownerID, this.classroomState._id)

        for (const { courseID, members } of courseInstances) {
          // We don't want to show course instances if there aren't any students assigned.
          if (!Array.isArray(members) || members.length === 0) {
            continue
          }
          courseInstanceCourses.add(courseID)
        }

        for (const course of this.classroomState.courses) {
          if (!courseInstanceCourses.has(course._id)) {
            continue
          }
          map.set(course._id, { levels: course.levels })
        }

        return map
      },
      /**
       * TODO: Migrate this to be a background stats calculation.
       * Returns an array of chapter stats objects with the following shape:
      {
        name: String
        assigned: Integer - number of members,
        progress: Float between 0 and 1.
      }
      */
      chapterStatsAdapter () {
        return this.sortedCourses
          .filter((course) => me.hasCampaignAccess(course))
          .map((course) => {
            // Splits off the "Chapter 1" part of the name
            // Expects the course name to have 'Chapter <int>:' structure.
            const splitName = course.name.split(':')
            let name = course.name
            if (splitName.length > 1) {
              name = splitName[0]
            }

            const result = {
              name,
              assigned: false,
              progress: 0
            }

            // If we have assigned this course then calculate the progress.
            if (this.courseLevelsMap.has(course._id)) {
              result.assigned = true
              const levels = this.courseLevelsMap.get(course._id).levels
              const levelSetInCourse = new Set(levels.map((l) => l.original))

              let progress = 0
              // Fallback to 1 to prevent division by 0 error in an empty class.
              const totalProgress = this.classroomState.members.length * levels.length || 1

              for (const memberId of this.classroomState.members) {
                for (const [levelOriginal, sessionData] of Object.entries(this.levelSessionsMapByUser[memberId] || [])) {
                  if (!levelSetInCourse.has(levelOriginal)) {
                    continue
                  }

                  if (sessionData.state.complete) {
                    progress += 1
                  }
                }
              }

              result.progress = progress / totalProgress
            }

            return result
          })
      }
    }
  }
</script>

<template>
  <ClassComponent
    :classroom-stats="classroomStatsFromClassroom"
    :chapter-stats="chapterStatsAdapter"
    @clickTeacherArchiveModalButton="$emit('clickTeacherArchiveModalButton')"
  />
</template>

<script>
  /** Given a class id, generates and populates the stats for the class component */
  import { mapGetters } from 'vuex'
  import ClassComponent from '../ClassComponent'

  export default {
    components: {
      ClassComponent
    },

    props: {
      classroomState: {
        type: Object,
        required: true
      }
    },

    computed: {
      ...mapGetters({
        levelSessionsMapForClassroom: 'levelSessions/getSessionsMapForClassroom',
        sortedCourses: 'courses/sorted',
        getCourseInstancesForClass: 'courseInstances/getCourseInstancesForClass'
      }),

      levelSessionsMapByUser () {
        return this.levelSessionsMapForClassroom(this.classroomState._id) || {}
      },

      classroomCreationDate () {
        return moment(parseInt(this.classroomState._id.substring(0, 8), 16) * 1000).format('MMMM Do, YYYY')
      },

      classroomStatsFromClassroom () {
        return {
          id: this.classroomState._id,
          name: this.classroomState.name,
          language: this.classroomState.aceConfig.language || 'python',
          numberOfStudents: this.classroomState.members.length || 0,
          classroomCreated: this.classroomCreationDate,
          archived: this.classroomState.archived
        }
      },

      // Maps the course Id to the levels associated.
      courseLevelsMap () {
        const map = new Map()
        const courseInstanceCourses = new Set()
        const courseInstances = this.getCourseInstancesForClass(this.classroomState.ownerID, this.classroomState._id)

        for (const { courseID } of courseInstances) {
          courseInstanceCourses.add(courseID)
        }

        for (const course of this.classroomState.courses) {
          if (!courseInstanceCourses.has(course._id)) {
            continue
          }
          map.set(course._id, { levels: course.levels })
        }

        return map
      },
      /**
       * TODO: Migrate this to be a background stats calculation.
       * Returns an array of chapter stats objects with the following shape:
      {
        name: String
        assigned: Integer - number of members,
        progress: Float between 0 and 1.
      }
      */
      chapterStatsAdapter () {
        return this.sortedCourses
          .filter((course) => me.hasCampaignAccess(course))
          .map((course) => {
            // Splits off the "Chapter 1" part of the name
            // Expects the course name to have 'Chapter <int>:' structure.
            const splitName = course.name.split(':')
            let name = course.name
            if (splitName.length > 1) {
              name = splitName[0]
            }

            const result = {
              name,
              assigned: false,
              progress: 0
            }

            // If we have assigned this course then calculate the progress.
            if (this.courseLevelsMap.has(course._id)) {
              result.assigned = true
              const levels = this.courseLevelsMap.get(course._id).levels
              const levelSetInCourse = new Set(levels.map((l) => l.original))

              let progress = 0
              // Fallback to 1 to prevent division by 0 error in an empty class.
              const totalProgress = this.classroomState.members.length * levels.length || 1

              for (const memberId of this.classroomState.members) {
                for (const [levelOriginal, sessionData] of Object.entries(this.levelSessionsMapByUser[memberId] || [])) {
                  if (!levelSetInCourse.has(levelOriginal)) {
                    continue
                  }

                  if (sessionData.state.complete) {
                    progress += 1
                  }
                }
              }

              result.progress = progress / totalProgress
            }

            return result
          })
      }
    }
  }
</script>

<template>
  <ClassComponent
    :classroom-stats="classroomStatsFromClassroom"
    :chapter-stats="chapterStatsAdapter"
    @clickTeacherArchiveModalButton="$emit('clickTeacherArchiveModalButton')"
  />
</template>


Lower Update-Interval for Presence

Lower Update-Interval for Presence

<template>

{{ person.status_text }}

</template>


Fixed a potential undefined dereference on ReaderView page load.

Refs #78.

<template>
	<v-container>
		<AppTasks v-if="!loading" v-model="appTasksMenu">
			<AppTask
				:label="$t('quickTasks.edit.reader')"
				:to="`/readers/${reader.id}/edit`"
				icon="mdi-account dem-overlay-edit"
			/>
			<AppTask
				:label="$t('quickTasks.configure.reader')"
				:to="`/readers/${reader.id}/configuration`"
				icon="mdi-cog"
			/>
			<AppTask
				v-if="mayDelete"
				:label="$t('quickTasks.delete.reader')"
				:confirm="$t('quickTasks.delete.reader.confirm')"
				icon="mdi-account dem-overlay-delete"
				@cancel="appTasksMenu = false"
				@confirm="deleteType"
			/>
		</AppTasks>

		<SectionCard :loading="loading">
			<h1 class="display-1">
				<LetterIcon :letter="letter" :color="reader.colour" />
				{{ reader.identifyingName }}
			</h1>
			<v-btn
				:to="`/readers/${reader.id}/favourites`"
				color="accent" class="my-4 me-4" small outlined
			>
				{{ $t('page.Reader.actions.favourites') }}
			</v-btn>
			<v-btn
				:to="`/readers/${reader.id}/readingList`"
				color="accent" class="my-4" small outlined
			>
				{{ $t('page.Reader.actions.readingList') }}
			</v-btn>
		</SectionCard>
	</v-container>
</template>

<script>
import AppTask from '@/components/AppTask'
import AppTasks from '@/components/AppTasks'
import LetterIcon from '@/components/LetterIcon'
import SectionCard from '@/components/SectionCard'
import { deleteStub } from '@/helpers/actions'
import modelViewMixin from '@/mixins/model-view'
import readerService from '@/services/reader-service'

export default {
	name: 'ReaderView',

	components: {
		AppTask,
		AppTasks,
		LetterIcon,
		SectionCard
	},

	mixins: [modelViewMixin],

	metaInfo() {
		return {
			title: this.reader.identifyingName
		}
	},

	data() {
		return {
			reader: {},
			mayDelete: false,
			appTasksMenu: false
		}
	},

	computed: {
		letter() {
			return this.reader.name?.charAt(0)
		}
	},

	methods: {
		async fetchData() {
			const readerP = readerService.findById(this.parsedId)
			this.mayDelete = await readerService.mayDeleteReader()
			this.reader = await readerP // Resolve calls in parallel
		},

		deleteType() {
			deleteStub(this,
				() => readerService.deleteModel(this.reader.id),
				'quickTasks.delete.reader.confirm.done',
				'ReaderIndex')
		}
	}
}
</script>

<template>
	<v-container>
		<AppTasks v-if="!loading" v-model="appTasksMenu">
			<AppTask
				:label="$t('quickTasks.edit.reader')"
				:to="`/readers/${reader.id}/edit`"
				icon="mdi-account dem-overlay-edit"
			/>
			<AppTask
				:label="$t('quickTasks.configure.reader')"
				:to="`/readers/${reader.id}/configuration`"
				icon="mdi-cog"
			/>
			<AppTask
				v-if="mayDelete"
				:label="$t('quickTasks.delete.reader')"
				:confirm="$t('quickTasks.delete.reader.confirm')"
				icon="mdi-account dem-overlay-delete"
				@cancel="appTasksMenu = false"
				@confirm="deleteType"
			/>
		</AppTasks>

		<SectionCard :loading="loading">
			<h1 class="display-1">
				<LetterIcon :letter="reader.name.charAt(0)" :color="reader.colour" />
				{{ reader.identifyingName }}
			</h1>
			<v-btn
				:to="`/readers/${reader.id}/favourites`"
				color="accent" class="my-4 me-4" small outlined
			>
				{{ $t('page.Reader.actions.favourites') }}
			</v-btn>
			<v-btn
				:to="`/readers/${reader.id}/readingList`"
				color="accent" class="my-4" small outlined
			>
				{{ $t('page.Reader.actions.readingList') }}
			</v-btn>
		</SectionCard>
	</v-container>
</template>

<script>
import AppTask from '@/components/AppTask'
import AppTasks from '@/components/AppTasks'
import LetterIcon from '@/components/LetterIcon'
import SectionCard from '@/components/SectionCard'
import { deleteStub } from '@/helpers/actions'
import modelViewMixin from '@/mixins/model-view'
import readerService from '@/services/reader-service'

export default {
	name: 'ReaderView',

	components: {
		AppTask,
		AppTasks,
		LetterIcon,
		SectionCard
	},

	mixins: [modelViewMixin],

	metaInfo() {
		return {
			title: this.reader.identifyingName
		}
	},

	data() {
		return {
			reader: {},
			mayDelete: false,
			appTasksMenu: false
		}
	},

	methods: {
		async fetchData() {
			const readerP = readerService.findById(this.parsedId)
			this.mayDelete = await readerService.mayDeleteReader()
			this.reader = await readerP // Resolve calls in parallel
		},

		deleteType() {
			deleteStub(this,
				() => readerService.deleteModel(this.reader.id),
				'quickTasks.delete.reader.confirm.done',
				'ReaderIndex')
		}
	}
}
</script>


devop: 🔧 added edgecase, comment

devop: 🔧 added edgecase, comment

<template>
  <div class="wallet-main">
    <the-wallet-side-menu />
    <v-main>
      <v-container class="pa-2 pa-md-3 mb-14" fluid>
        <the-wallet-header />
        <module-confirmation />
        <wallet-promo-pop-up />
        <router-view />
        <wallet-promo-snackbar />
      </v-container>
    </v-main>
    <the-wallet-footer />
  </div>
</template>

<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { toBN, toHex } from 'web3-utils';
import TheWalletSideMenu from './components-wallet/TheWalletSideMenu';
import TheWalletHeader from './components-wallet/TheWalletHeader';
import TheWalletFooter from './components-wallet/TheWalletFooter';
import WalletPromoPopUp from './components-wallet/WalletPromoPopUp';
import ModuleConfirmation from '@/modules/confirmation/ModuleConfirmation';
import handlerWallet from '@/core/mixins/handlerWallet.mixin';
import nodeList from '@/utils/networks';
import { ERROR, Toast, WARNING } from '@/modules/toast/handler/handlerToast';
import { Web3Wallet } from '@/modules/access-wallet/common';
import Web3 from 'web3';
import { ROUTES_HOME } from '@/core/configs/configRoutes';
import WalletPromoSnackbar from '@/views/components-wallet/WalletPromoSnackbar';
export default {
  components: {
    TheWalletSideMenu,
    TheWalletHeader,
    TheWalletFooter,
    WalletPromoPopUp,
    ModuleConfirmation,
    WalletPromoSnackbar
  },
  mixins: [handlerWallet],
  computed: {
    ...mapState('wallet', ['address', 'web3', 'identifier']),
    ...mapState('global', ['online', 'gasPriceType', 'baseGasPrice']),
    ...mapGetters('global', [
      'network',
      'gasPrice',
      'isEIP1559SupportedNetwork'
    ]),
    ...mapState('external', ['coinGeckoTokens']),
    ...mapGetters('wallet', ['balanceInWei'])
  },
  watch: {
    address() {
      if (!this.address) {
        this.$router.push({ name: ROUTES_HOME.HOME.NAME });
      }
    },
    network() {
      this.web3.eth.clearSubscriptions();
      this.matchNetwork();
    },
    web3() {
      this.subscribeToBlockNumber();
      this.setTokensAndBalance();
    },
    coinGeckoTokens() {
      this.setTokenAndEthBalance();
    }
  },
  mounted() {
    if (this.online) {
      this.setup();
      this.findAndSetNetwork();
      this.web3Listeners();
    }
  },
  beforeDestroy() {
    if (window.ethereum) {
      window.ethereum.removeListener('chainChanged', this.findAndSetNetwork);
      window.ethereum.removeListener('accountsChanged', this.setWeb3Account);
    }
  },
  destroyed() {
    this.web3.eth.clearSubscriptions();
  },
  methods: {
    ...mapActions('wallet', [
      'setBlockNumber',
      'setTokens',
      'setWallet',
      'setWeb3Instance'
    ]),
    ...mapActions('global', [
      'setNetwork',
      'setBaseFeePerGas',
      'updateGasPrice'
    ]),
    ...mapActions('external', ['setCoinGeckoTokens', 'setTokenAndEthBalance']),
    setup() {
      this.setTokensAndBalance();
      this.subscribeToBlockNumber();
    },
    setTokensAndBalance() {
      if (this.coinGeckoTokens?.get) {
        this.setTokenAndEthBalance();
      } else {
        this.setTokens([]);
      }
    },
    checkAndSetBaseFee(baseFee) {
      if (baseFee) {
        this.setBaseFeePerGas(toBN(baseFee));
      } else {
        this.setBaseFeePerGas(toBN('0'));
      }
      this.updateGasPrice();
    },
    subscribeToBlockNumber() {
      this.web3.eth.getBlockNumber().then(bNumber => {
        this.setBlockNumber(bNumber);
        this.web3.eth.getBlock(bNumber).then(block => {
          if (block) {
            this.checkAndSetBaseFee(block.baseFeePerGas);
          }
          this.web3.eth
            .subscribe('newBlockHeaders')
            .on('data', res => {
              if (this.isEIP1559SupportedNetwork && res.baseFeePerGas) {
                this.checkAndSetBaseFee(toBN(res.baseFeePerGas));
              }
              this.setBlockNumber(res.number);
            })
            .on('error', err => {
              Toast(
                err.message === 'Load failed'
                  ? 'eth_subscribe is not supported. Please make sure your provider supports eth_subscribe'
                  : err,
                {},
                ERROR
              );
            });
        });
      });
    },
    /**
     * Checks Metamask chainID on load, switches current network if it doesn't match
     * and setup listeners for metamask changes
     */
    web3Listeners() {
      if (window.ethereum.on) {
        window.ethereum.on('chainChanged', this.findAndSetNetwork);
        window.ethereum.on('accountsChanged', this.setWeb3Account);
      } else {
        Toast(
          'Something is wrong with Metamask. (window.ethereum.on is not a function).  Please refresh the page and reload Metamask.',
          {},
          ERROR
        );
      }
    },
    /**
     * Attempts to switch metamask network to current mew network.
     * Informs user of pending or unknown networks.
     */
    async matchNetwork() {
      const { ethereum } = window;
      const {
        type: { chainID }
      } = this.network;
      if (ethereum) {
        const data = { chainId: toHex(chainID) };
        try {
          await ethereum.request({
            method: 'wallet_switchEthereumChain',
            params: [data]
          });
        } catch (er) {
          const { message } = er;
          let toastMsg = '';
          let toastLink = {};
          if (message) {
            if (message.includes('pending')) {
              toastMsg =
                'There is a pending request to MetaMask, make your selection before continuing';
            } else if (message.includes('adding')) {
              toastMsg =
                "It seems like you don't have this network setup in MetaMask,";
              toastLink = {
                title: 'please go here to add the network',
                url: 'https://chainlist.org/'
              };
            } else if (message.includes('rejected')) return;
            else {
              toastMsg =
                'There was a problem processing your request to MetaMask';
            }
          }
          Toast(toastMsg, toastLink, ERROR, 5000);
        }
      }
    },
    async findAndSetNetwork() {
      const networkId = await window.ethereum.request({
        method: 'net_version'
      });
      const foundNetwork = Object.values(nodeList).find(item => {
        if (toBN(networkId).toNumber() === item[0].type.chainID) return item;
      });
      if (window.ethereum.isMetaMask) {
        this.setNetwork(foundNetwork[0]).then(() => {
          this.setWeb3Instance(new Web3(window.ethereum));
        });
      } else {
        Toast(
          "Can't find matching nodes for selected MetaMask node! MetaMask may not function properly. Please select a supported node",
          {},
          WARNING
        );
      }
    },
    setWeb3Account(acc) {
      const web3 = new Web3(window.ethereum);
      const wallet = new Web3Wallet(acc[0]);
      this.setWallet([wallet, web3.currentProvider]);
    }
  }
};
</script>

<style lang="scss" scoped>
.box-shadow {
  box-shadow: 0 0 15px var(--v-greyMedium-base) !important;
}
.wallet-main {
  background-color: var(--v-greyLight-base);
  height: 100%;
}
</style>

<template>
  <div class="wallet-main">
    <the-wallet-side-menu />
    <v-main>
      <v-container class="pa-2 pa-md-3 mb-14" fluid>
        <the-wallet-header />
        <module-confirmation />
        <wallet-promo-pop-up />
        <router-view />
        <wallet-promo-snackbar />
      </v-container>
    </v-main>
    <the-wallet-footer />
  </div>
</template>

<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { toBN, toHex } from 'web3-utils';
import TheWalletSideMenu from './components-wallet/TheWalletSideMenu';
import TheWalletHeader from './components-wallet/TheWalletHeader';
import TheWalletFooter from './components-wallet/TheWalletFooter';
import WalletPromoPopUp from './components-wallet/WalletPromoPopUp';
import ModuleConfirmation from '@/modules/confirmation/ModuleConfirmation';
import handlerWallet from '@/core/mixins/handlerWallet.mixin';
import nodeList from '@/utils/networks';
import { ERROR, Toast, WARNING } from '@/modules/toast/handler/handlerToast';
import { Web3Wallet } from '@/modules/access-wallet/common';
import Web3 from 'web3';
import { ROUTES_HOME } from '@/core/configs/configRoutes';
import WalletPromoSnackbar from '@/views/components-wallet/WalletPromoSnackbar';
export default {
  components: {
    TheWalletSideMenu,
    TheWalletHeader,
    TheWalletFooter,
    WalletPromoPopUp,
    ModuleConfirmation,
    WalletPromoSnackbar
  },
  mixins: [handlerWallet],
  computed: {
    ...mapState('wallet', ['address', 'web3', 'identifier']),
    ...mapState('global', ['online', 'gasPriceType', 'baseGasPrice']),
    ...mapGetters('global', [
      'network',
      'gasPrice',
      'isEIP1559SupportedNetwork'
    ]),
    ...mapState('external', ['coinGeckoTokens']),
    ...mapGetters('wallet', ['balanceInWei'])
  },
  watch: {
    address() {
      if (!this.address) {
        this.$router.push({ name: ROUTES_HOME.HOME.NAME });
      }
    },
    network() {
      this.web3.eth.clearSubscriptions();
      this.matchNetwork();
    },
    web3() {
      this.subscribeToBlockNumber();
      this.setTokensAndBalance();
    },
    coinGeckoTokens() {
      this.setTokenAndEthBalance();
    }
  },
  mounted() {
    if (this.online) {
      this.setup();
      this.findAndSetNetwork();
      this.web3Listeners();
    }
  },
  beforeDestroy() {
    if (window.ethereum) {
      window.ethereum.removeListener('chainChanged', this.findAndSetNetwork);
      window.ethereum.removeListener('accountsChanged', this.setWeb3Account);
    }
  },
  destroyed() {
    this.web3.eth.clearSubscriptions();
  },
  methods: {
    ...mapActions('wallet', [
      'setBlockNumber',
      'setTokens',
      'setWallet',
      'setWeb3Instance'
    ]),
    ...mapActions('global', [
      'setNetwork',
      'setBaseFeePerGas',
      'updateGasPrice'
    ]),
    ...mapActions('external', ['setCoinGeckoTokens', 'setTokenAndEthBalance']),
    setup() {
      this.setTokensAndBalance();
      this.subscribeToBlockNumber();
    },
    setTokensAndBalance() {
      if (this.coinGeckoTokens?.get) {
        this.setTokenAndEthBalance();
      } else {
        this.setTokens([]);
      }
    },
    checkAndSetBaseFee(baseFee) {
      if (baseFee) {
        this.setBaseFeePerGas(toBN(baseFee));
      } else {
        this.setBaseFeePerGas(toBN('0'));
      }
      this.updateGasPrice();
    },
    subscribeToBlockNumber() {
      this.web3.eth.getBlockNumber().then(bNumber => {
        this.setBlockNumber(bNumber);
        this.web3.eth.getBlock(bNumber).then(block => {
          if (block) {
            this.checkAndSetBaseFee(block.baseFeePerGas);
          }
          this.web3.eth
            .subscribe('newBlockHeaders')
            .on('data', res => {
              if (this.isEIP1559SupportedNetwork && res.baseFeePerGas) {
                this.checkAndSetBaseFee(toBN(res.baseFeePerGas));
              }
              this.setBlockNumber(res.number);
            })
            .on('error', err => {
              Toast(
                err.message === 'Load failed'
                  ? 'eth_subscribe is not supported. Please make sure your provider supports eth_subscribe'
                  : err,
                {},
                ERROR
              );
            });
        });
      });
    },
    /**
     * Checks Metamask chainID on load, switches current network if it doesn't match
     * and setup listeners for metamask changes
     */
    web3Listeners() {
      if (window.ethereum.on) {
        window.ethereum.on('chainChanged', this.findAndSetNetwork);
        window.ethereum.on('accountsChanged', this.setWeb3Account);
      } else {
        Toast(
          'Something is wrong with Metamask. (window.ethereum.on is not a function).  Please refresh the page and reload Metamask.',
          {},
          ERROR
        );
      }
    },
    async matchNetwork() {
      const { ethereum } = window;
      const {
        type: { chainID }
      } = this.network;
      if (ethereum) {
        const data = { chainId: toHex(chainID) };
        try {
          await ethereum.request({
            method: 'wallet_switchEthereumChain',
            params: [data]
          });
        } catch (er) {
          const { message } = er;
          let toastMsg = '';
          let toastLink = {};
          if (message) {
            if (message.includes('pending')) {
              toastMsg =
                'There is a pending request to MetaMask, make your selection before continuing';
            } else if (message.includes('adding')) {
              toastMsg =
                "It seems like you don't have this network setup in MetaMask,";
              toastLink = {
                title: 'please go here to add the network',
                url: 'https://chainlist.org/'
              };
            } else {
              toastMsg =
                'There was a problem processing your request to MetaMask';
            }
          }
          Toast(toastMsg, toastLink, ERROR, 5000);
        }
      }
    },
    async findAndSetNetwork() {
      const networkId = await window.ethereum.request({
        method: 'net_version'
      });
      const foundNetwork = Object.values(nodeList).find(item => {
        if (toBN(networkId).toNumber() === item[0].type.chainID) return item;
      });
      if (window.ethereum.isMetaMask) {
        this.setNetwork(foundNetwork[0]).then(() => {
          this.setWeb3Instance(new Web3(window.ethereum));
        });
      } else {
        Toast(
          "Can't find matching nodes for selected MetaMask node! MetaMask may not function properly. Please select a supported node",
          {},
          WARNING
        );
      }
    },
    setWeb3Account(acc) {
      const web3 = new Web3(window.ethereum);
      const wallet = new Web3Wallet(acc[0]);
      this.setWallet([wallet, web3.currentProvider]);
    }
  }
};
</script>

<style lang="scss" scoped>
.box-shadow {
  box-shadow: 0 0 15px var(--v-greyMedium-base) !important;
}
.wallet-main {
  background-color: var(--v-greyLight-base);
  height: 100%;
}
</style>


beverages

beverages

<template>

        {{ this.button.order }}

           🏢 Coworking 👩🚀 Space Offers

                Place for your work in downtown

                High-speed Internet ~ 250/20📱

                Lot of Electric Sockets ⚡🔌

                External Monitors - shared 🖥

                Standing desks

                Meeting room 🤝 (soon for rent)

                Private offices (WiP - ask)

                A/C ❄

                Coding women have 50% off 👩🏽💻

                Bitcoin & Litecoin accepted

            🍻 Beverages 🥤

                🍵 Tea

                🍹 Softdrinks ❄

                    Club-Mate

                    Flora-power

                    Fritz-kola

                    Mana Roots

                    Pragomost

                    Thomas Henry Mate-Mate

                ☕ <s>Specialty Coffee</s>

        <input v-model="subEmail" class="input is-medium is-white" type="email" placeholder="Enter your email">

      {{ this.button.subscribe }}
      {{ this.button.subscribe }}
      {{ this.button.login }}
      {{ this.button.login }}

    <Offer></Offer>

Our story

                We started as first Slovak hackerspace back in June 2010, this is our 4th place as we went through expansion and held more and biggers meetups.

                In September 2017 we considered to keep up our 💸 cashflow & our existence and started thinking about daily utilization. We came up with idea to open coworking in our hackerspace Progressbar in Bratislava.

                If you think we are doing great thing

                    you can support us at donate.progressbar.sk

                     signup for our coworking in Bratislava

                    <router-link to="/sponsors">be our proud sponsor</router-link>

                👋🏼 You are welcome 🤗

Our coworkers are ~

                Blockchain startups

                Decentralization technology specialists

                Cryptocurrency techies & fanatics

                Backend & Frontend developers

                Machine learning developers

                Hardware tinkers

                Opensource developers

                Cyberpunks & Cypherpunks

Tech Cowork Progressbar in Bratislava

Tech Cowork Progressbar in Bratislava where we accept Bitcoin and Litecoin for coworking. Daypass starts from {{ this.config.orderPrices.day }}€

  <cookie-law transitionName="fade"
   position="bottom"
   theme="dark-lime"
   buttonText="I understand"></cookie-law>

</template>

<template>

        {{ this.button.order }}

           🏢 Coworking 👩🚀 Space Offers

                Place for your work in downtown

                High-speed Internet ~ 250/20📱

                Lot of Electric Sockets ⚡🔌

                External Monitors - shared 🖥

                Standing desks

                Meeting room 🤝 (soon for rent)

                Private offices (WiP - ask)

                A/C ❄

                Coding women have 50% off 👩🏽💻

                Bitcoin & Litecoin accepted

            Drink 🥤 bar

                ☕ Specialty Coffee

                🍵 Tea

                🍹 Softdrinks ❄

                    Club-Mate

                    Flora-power

                    Mana Roots

                    Pragomost

                    Fritz-kola

                    Thomas Henry Mate-Mate

        <input v-model="subEmail" class="input is-medium is-white" type="email" placeholder="Enter your email">

      {{ this.button.subscribe }}
      {{ this.button.subscribe }}
      {{ this.button.login }}
      {{ this.button.login }}

    <Offer></Offer>

Our story

                We started as first Slovak hackerspace back in June 2010, this is our 4th place as we went through expansion and held more and biggers meetups.

                In September 2017 we considered to keep up our 💸 cashflow & our existence and started thinking about daily utilization. We came up with idea to open coworking in our hackerspace Progressbar in Bratislava.

                If you think we are doing great thing

                    you can support us at donate.progressbar.sk

                     signup for our coworking in Bratislava

                    <router-link to="/sponsors">be our proud sponsor</router-link>

                👋🏼 You are welcome 🤗

Our coworkers are ~

                Blockchain startups

                Decentralization technology specialists

                Cryptocurrency techies & fanatics

                Backend & Frontend developers

                Machine learning developers

                Hardware tinkers

                Opensource developers

                Cyberpunks & Cypherpunks

Tech Cowork Progressbar in Bratislava

Tech Cowork Progressbar in Bratislava where we accept Bitcoin and Litecoin for coworking. Daypass starts from {{ this.config.orderPrices.day }}€

  <cookie-law transitionName="fade"
   position="bottom"
   theme="dark-lime"
   buttonText="I understand"></cookie-law>

</template>