From 56964efad317924257685b204c381987a9463ebe Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 1 Jul 2019 12:06:27 +0300
Subject: [PATCH 01/61] feat: config Settings page and service

---
 .../layout/app-sidebar/AppSidebar.vue         |  6 ++++
 .../configSettings/ConfigSettingsPage.vue     | 19 +++++++++++
 src/i18n/en.json                              |  3 +-
 src/router/router.js                          |  5 +++
 src/services/ConfigService                    | 14 ++++++++
 src/services/urlBuilder.ts                    | 32 +++++++++++--------
 6 files changed, 64 insertions(+), 15 deletions(-)
 create mode 100644 src/components/pages/configSettings/ConfigSettingsPage.vue
 create mode 100644 src/services/ConfigService

diff --git a/src/components/layout/app-sidebar/AppSidebar.vue b/src/components/layout/app-sidebar/AppSidebar.vue
index c65fc29..15e49c9 100644
--- a/src/components/layout/app-sidebar/AppSidebar.vue
+++ b/src/components/layout/app-sidebar/AppSidebar.vue
@@ -14,6 +14,12 @@
       >
         <template slot="title">{{ $t('menu.reports') }}</template>
       </sidebar-link>
+      <sidebar-link
+        :to="{ name: 'config' }"
+        icon="vuestic-iconset vuestic-iconset-settings"
+      >
+        <template slot="title">{{ $t('menu.config') }}</template>
+      </sidebar-link>
     </template>
   </va-sidebar>
 </template>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
new file mode 100644
index 0000000..32dda03
--- /dev/null
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -0,0 +1,19 @@
+<template>
+  <va-card class="report-page">
+    ConfigSettings Page
+  </va-card>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from 'vue-property-decorator'
+import { FulfillingBouncingCircleSpinner } from 'epic-spinners'
+
+@Component({
+  components: { FulfillingBouncingCircleSpinner },
+})
+export default class ConfigSettingsPage extends Vue {
+}
+</script>
+
+<style lang="scss">
+</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4b5504e..6e9ab93 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -206,7 +206,8 @@
   },
   "menu": {
     "users": "Users",
-    "reports": "Reports"
+    "reports": "Reports",
+    "config": "Config Settings"
   },
   "messages": {
     "all": "See all messages",
diff --git a/src/router/router.js b/src/router/router.js
index fbb221d..137fcd4 100644
--- a/src/router/router.js
+++ b/src/router/router.js
@@ -38,6 +38,11 @@ export default new Router({
           path: 'report/:id',
           name: 'reportDetails',
           component: () => import('../components/pages/reports/ReportPage.vue')
+        },
+        {
+          path: 'config',
+          name: 'config',
+          component: () => import('../components/pages/configSettings/ConfigSettingsPage.vue')
         }
       ]
     },
diff --git a/src/services/ConfigService b/src/services/ConfigService
new file mode 100644
index 0000000..65a1229
--- /dev/null
+++ b/src/services/ConfigService
@@ -0,0 +1,14 @@
+import {reportsList} from '../data/ReportsList.mock'
+import {report, unresolvedReport} from '../data/Report.mock'
+import executeApiRequest from './executeApiRequest'
+import urlBuilder, {Url} from './urlBuilder'
+
+export class ConfigService {
+  static listConfigSettings () {
+    return executeApiRequest('get', urlBuilder(Url.configSettings, {}), {})
+  }
+
+  static updateConfigSettings () {
+    return executeApiRequest('put', urlBuilder(Url.configSettings, {}), {})
+  }
+}
diff --git a/src/services/urlBuilder.ts b/src/services/urlBuilder.ts
index 133c62e..232cefa 100644
--- a/src/services/urlBuilder.ts
+++ b/src/services/urlBuilder.ts
@@ -29,6 +29,7 @@ export enum Url {
   changeReportState = 'changeReportState',
   respondReport = 'respondReport',
   changeReportedStatus = 'changeReportedStatus',
+  configSettings = 'configSettings',
 }
 
 export enum UserData {
@@ -49,22 +50,25 @@ interface UrlBuilderOptions {
 }
 
 const urls = {
-  [Url.getUsers]: () => 'pleroma/admin/users',
-  [Url.getUser]: (options: UrlBuilderOptions) => `v1/accounts/${options.id}`,
-  [Url.getSimplifiedUser]: (options: UrlBuilderOptions) => `pleroma/admin/users/${options.id}`,
-  [Url.getUserData]: (options: UrlBuilderOptions) => `v1/accounts/${options.id}/${options.dataType}`,
-  [Url.toggleTag]: () => 'pleroma/admin/users/tag/',
-  [Url.toggleUserActivation]: (options: UrlBuilderOptions) => `pleroma/admin/users/${options.id}/activation_status`,
-  [Url.togglePermissionGroup]: (options: UrlBuilderOptions) => `pleroma/admin/users/${options.id}/permission_group/${options.permissionGroup}`,
-  [Url.deleteUser]: (options: UrlBuilderOptions) => `pleroma/admin/user`,
-  [Url.getReports]: (options: UrlBuilderOptions) => `pleroma/admin/reports`,
-  [Url.getReport]: (options: UrlBuilderOptions) => `pleroma/admin/reports/${options.id}`,
-  [Url.changeReportState]: (options: UrlBuilderOptions) => `pleroma/admin/reports/${options.id}`,
-  [Url.respondReport]: (options: UrlBuilderOptions) => `pleroma/admin/reports/${options.id}/respond`,
-  [Url.changeReportedStatus]: (options: UrlBuilderOptions) => `pleroma/admin/statuses/${options.id}`
+  [Url.getUsers]: () => 'users',
+  [Url.getSimplifiedUser]: (options: UrlBuilderOptions) => `users/${options.id}`,
+  [Url.getUser]: (options: UrlBuilderOptions) => `${options.id}`,
+  [Url.getUserData]: (options: UrlBuilderOptions) => `${options.id}/${options.dataType}`,
+  [Url.toggleTag]: () => 'users/tag/',
+  [Url.toggleUserActivation]: (options: UrlBuilderOptions) => `users/${options.id}/activation_status`,
+  [Url.togglePermissionGroup]: (options: UrlBuilderOptions) => `users/${options.id}/permission_group/${options.permissionGroup}`,
+  [Url.deleteUser]: (options: UrlBuilderOptions) => `user`,
+  [Url.getReports]: (options: UrlBuilderOptions) => `reports`,
+  [Url.getReport]: (options: UrlBuilderOptions) => `reports/${options.id}`,
+  [Url.changeReportState]: (options: UrlBuilderOptions) => `reports/${options.id}`,
+  [Url.respondReport]: (options: UrlBuilderOptions) => `reports/${options.id}/respond`,
+  [Url.changeReportedStatus]: (options: UrlBuilderOptions) => `statuses/${options.id}`,
+  [Url.configSettings]: (options: UrlBuilderOptions) => `config`
 }
 
 export default (action: Url, options: UrlBuilderOptions):string => {
   const code = getAll()
-  return `https://${code.instance}/api/${urls[action](options)}`
+  return action === 'getUserData' || action === 'getUser'
+    ? `https://${code.instance}/api/v1/accounts/${urls[action](options)}`
+    : `https://${code.instance}/api/pleroma/admin/${urls[action](options)}`
 }
-- 
GitLab


From 7b67fff1c8ff0ffd5b633a4cd1d07cbc71e998b1 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 1 Jul 2019 14:57:25 +0300
Subject: [PATCH 02/61] feat: upgrade version of va-tabs, va-color-picker,
 va-radio-button, vue-book

---
 package.json                                  |   5 +-
 .../ColorDot.demo.vue                         |   0
 .../ColorDot.vue                              |   0
 .../VaAdvancedColorPicker.demo.vue            |  26 ++
 .../va-color-picker/VaAdvancedColorPicker.vue |  45 +++
 .../va-color-picker/VaColorInput.demo.vue     |  33 +++
 .../va-color-picker/VaColorInput.vue          |  87 ++++++
 .../VaColorPickerInput.demo.vue               |  64 +++++
 .../va-color-picker/VaColorPickerInput.vue    | 111 +++++++
 .../va-color-picker/VaColorSquare.vue         |  27 ++
 .../va-color-picker/VaPaletteCustom.demo.vue  |  32 +++
 .../va-color-picker/VaPaletteCustom.vue       |  67 +++++
 .../VaSimplePalettePicker.demo.vue            |  34 +++
 .../va-color-picker/VaSimplePalettePicker.vue |  61 ++++
 .../VaSliderColorPicker.demo.vue              |  25 ++
 .../va-color-picker/VaSliderColorPicker.vue   |  35 +++
 .../VuesticAdvancedColorPicker.demo.vue       |   0
 .../VuesticAdvancedColorPicker.vue            |   0
 .../VuesticColorDropdown.demo.vue             |   0
 .../VuesticColorDropdown.vue                  |   0
 .../VuesticColorInput.demo.vue                |   0
 .../VuesticColorInput.vue                     |   0
 .../VuesticColorPickerInput.demo.vue          |   0
 .../VuesticColorPickerInput.vue               |   0
 .../VuesticColorSquare.vue                    |   0
 .../VuesticPalletCustom.demo.vue              |   2 +-
 .../VuesticPalletCustom.vue                   |   0
 .../VuesticSimplePalettePicker.demo.vue       |   0
 .../VuesticSimplePalettePicker.vue            |   0
 .../VuesticSliderColorPicker.demo.vue         |   0
 .../VuesticSliderColorPicker.vue              |   0
 .../VuesticTheme.js                           |   0
 .../progress-types/progressMixin.js           |   2 +-
 .../VaRadioButton.demo.vue}                   |  12 +-
 .../VaRadioButton.vue}                        |  43 +--
 .../vuestic-components/va-tabs/VaTab.vue      |  86 ++++--
 .../va-tabs/VaTabs.demo.vue                   | 145 +++++++---
 .../vuestic-components/va-tabs/VaTabs.vue     | 270 ++++++++++--------
 .../va-tabs/__demo__/TabsExample.vue          |  70 +++++
 .../vuestic-components/va-tabs/tabs-docs.md   |  35 +++
 src/vuestic-theme/vuestic-plugin.js           |   2 +-
 .../vuestic-sass/global/color-themes.demo.vue |   2 +-
 .../vuestic-sass/resources/_variables.scss    |   3 +
 yarn.lock                                     |  44 ++-
 44 files changed, 1119 insertions(+), 249 deletions(-)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/ColorDot.demo.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/ColorDot.vue (100%)
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaColorSquare.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.vue
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticAdvancedColorPicker.demo.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticAdvancedColorPicker.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticColorDropdown.demo.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticColorDropdown.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticColorInput.demo.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticColorInput.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticColorPickerInput.demo.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticColorPickerInput.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticColorSquare.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticPalletCustom.demo.vue (84%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticPalletCustom.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticSimplePalettePicker.demo.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticSimplePalettePicker.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticSliderColorPicker.demo.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticSliderColorPicker.vue (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-color-picker => va-color-picker}/VuesticTheme.js (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-radio-button/VuesticRadioButton.demo.vue => va-radio-button/VaRadioButton.demo.vue} (87%)
 rename src/vuestic-theme/vuestic-components/{vuestic-radio-button/VuesticRadioButton.vue => va-radio-button/VaRadioButton.vue} (73%)
 create mode 100644 src/vuestic-theme/vuestic-components/va-tabs/__demo__/TabsExample.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-tabs/tabs-docs.md

diff --git a/package.json b/package.json
index 89c00f8..f8efc40 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
     "v-tooltip": "^2.0.0-rc.30",
     "vee-validate": "2.0.9",
     "vue": "^2.6.6",
-    "vue-book": "0.1.0-alpha.14",
+    "vue-book": "0.1.0-alpha.17",
     "vue-bulma-expanding": "0.0.1",
     "vue-chartjs": "^3.4.0",
     "vue-class-component": "^6.0.0",
@@ -89,9 +89,6 @@
     "typescript": "^3.2.1",
     "vue-template-compiler": "^2.5.21"
   },
-  "gitHooks": {
-    "pre-commit": "lint-staged"
-  },
   "lint-staged": {
     "*.{js,vue}": [
       "vue-cli-service lint",
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/ColorDot.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/ColorDot.demo.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/ColorDot.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/ColorDot.demo.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/ColorDot.vue b/src/vuestic-theme/vuestic-components/va-color-picker/ColorDot.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/ColorDot.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/ColorDot.vue
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.demo.vue
new file mode 100644
index 0000000..f4cb60d
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.demo.vue
@@ -0,0 +1,26 @@
+<template>
+  <div class="demo-container">
+    <div class="demo-container__item">
+      <va-advanced-color-picker v-model="value"/>
+      {{ value }}
+    </div>
+    <div class="demo-container__item">
+      <va-advanced-color-picker v-model="value"/>
+    </div>
+  </div>
+</template>
+
+<script>
+import VaAdvancedColorPicker from './VaAdvancedColorPicker'
+
+export default {
+  components: {
+    VaAdvancedColorPicker,
+  },
+  data () {
+    return {
+      value: '#AAA',
+    }
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.vue
new file mode 100644
index 0000000..5a15ccb
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaAdvancedColorPicker.vue
@@ -0,0 +1,45 @@
+<template>
+  <ChromePicker v-model="valueProxy" class="va-advanced-color-picker"/>
+</template>
+
+<script>
+import { Chrome } from 'vue-color'
+
+export default {
+  name: 'va-advanced-color-picker',
+  components: {
+    ChromePicker: Chrome,
+  },
+  props: {
+    value: {
+      default: '',
+    },
+  },
+  computed: {
+    valueProxy: {
+      get () {
+        return this.value
+      },
+      set (value) {
+        this.$emit('input', value.hex)
+      },
+    },
+  },
+}
+</script>
+
+<style lang="scss">
+.va-advanced-color-picker {
+  .vc-chrome-alpha-wrap {
+    display: none;
+  }
+
+  .vc-chrome-hue-wrap {
+    margin-top: 10px;
+  }
+
+  .vc-chrome-toggle-btn {
+    display: none;
+  }
+}
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.demo.vue
new file mode 100644
index 0000000..993ceda
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.demo.vue
@@ -0,0 +1,33 @@
+<template>
+  <div class="demo-container">
+    <div class="demo-container__item">
+      <va-color-input v-model="value"/>
+    </div>
+    <div class="demo-container__item">
+      <p>Selected</p>
+      <va-color-input v-model="value" selected/>
+    </div>
+    <div class="demo-container__item">
+      <p>Disabled</p>
+      <va-color-input v-model="value" disabled/>
+    </div>
+    <div class="demo-container__item">
+      <img src="https://i.imgur.com/UjiMAZj.png" alt="">
+    </div>
+  </div>
+</template>
+
+<script>
+import VaColorInput from './VaColorInput'
+
+export default {
+  components: {
+    VaColorInput,
+  },
+  data () {
+    return {
+      value: '#aaaaaa',
+    }
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.vue
new file mode 100644
index 0000000..fdf8858
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorInput.vue
@@ -0,0 +1,87 @@
+<template>
+  <div class="va-color-input">
+    <color-dot
+      class="va-color-input__dot flex-center"
+      :selected="selected"
+      :color="value"
+      @click="onClick"
+    />
+    <div class="form-group">
+      <div class="input-group">
+        <input
+          class="va-color-input__input"
+          :disabled="disabled"
+          v-model="valueProxy"
+          :class="{'va-color-input__input__pointer': disabled}"
+          placeholder="input color"
+        >
+        <va-icon name="bar" :style="'width: ' + 9 + 'ch'"/>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import VaAdvancedColorPicker from './VaAdvancedColorPicker'
+import ColorDot from './ColorDot'
+
+export default {
+  name: 'va-color-input',
+  components: {
+    ColorDot,
+    VaAdvancedColorPicker,
+  },
+  props: {
+    value: {
+      default: '',
+    },
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+    selected: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  computed: {
+    valueProxy: {
+      get () {
+        return this.value
+      },
+      set (value) {
+        this.$emit('input', value)
+      },
+    },
+  },
+  methods: {
+    onClick () {
+      this.$emit('click')
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.va-color-input {
+  display: flex;
+
+  .form-group {
+    margin-bottom: 0;
+  }
+
+  &__dot {
+    margin-top: 7px;
+    margin-right: 8px;
+  }
+
+  &__input {
+    width: 9ch;
+
+    &__pointer {
+      cursor: pointer;
+    }
+  }
+}
+
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.demo.vue
new file mode 100644
index 0000000..b7d0eb7
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.demo.vue
@@ -0,0 +1,64 @@
+<template>
+  <VbDemo>
+    <VbCard title="slider mode">
+      <va-color-picker-input
+        v-model="value"
+        mode="slider"
+      >
+        <va-color-square :value="value"/>
+      </va-color-picker-input>
+      {{ value }}
+    </VbCard>
+
+    <VbCard title="advanced mode">
+      <va-color-picker-input
+        v-model="value"
+        mode="advanced"
+      >
+        <va-color-input v-model="value"/>
+      </va-color-picker-input>
+    </VbCard>
+
+    <VbCard title="palette mode">
+      <va-color-picker-input
+        v-model="value"
+        mode="palette"
+        :palette="palette"
+      />
+    </VbCard>
+
+    <VbCard title="palette mode with slot">
+      <va-color-picker-input
+        v-model="value"
+        mode="palette"
+        :palette="palette"
+      >
+        <color-dot :color="value"/>
+      </va-color-picker-input>
+      {{ value }}
+    </VbCard>
+  </VbDemo>
+</template>
+
+<script>
+import VaColorPickerInput from './VaColorPickerInput'
+import ColorDot from './ColorDot'
+import VaColorSquare from './VaColorSquare'
+import VaColorInput from './VaColorInput'
+import { colorArray } from './VuesticTheme'
+
+export default {
+  components: {
+    VaColorPickerInput,
+    ColorDot,
+    VaColorSquare,
+    VaColorInput,
+  },
+  data () {
+    return {
+      value: '#9b6caa',
+      palette: colorArray,
+    }
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.vue
new file mode 100644
index 0000000..3e3452a
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorPickerInput.vue
@@ -0,0 +1,111 @@
+<template>
+  <div class="va-color-picker-input">
+    <div v-if="validator(this.mode)">
+      <va-dropdown-popper fixed>
+        <div slot="anchor" class="va-color-picker-input__slot">
+          <slot>
+            <va-color-input
+              v-model="valueProxy"
+              mode="palette"
+              :disabled="isInputDisabled"
+              :selected="selected"
+            />
+          </slot>
+        </div>
+        <div class="va-color-picker-input__dropdown">
+          <va-advanced-color-picker
+            v-if="this.mode === 'advanced'"
+            v-model="valueProxy"
+          />
+          <va-simple-palette-picker
+            v-if="this.mode === 'palette'"
+            v-model="valueProxy"
+            :palette="palette"
+          />
+          <va-slider-color-picker
+            v-if="this.mode === 'slider'"
+            v-model="valueProxy"
+          />
+        </div>
+      </va-dropdown-popper>
+    </div>
+    <div v-else>
+      <slot>
+        <va-color-input
+          v-model="valueProxy"
+          mode="palette"
+          :disabled="isInputDisabled"
+        />
+      </slot>
+    </div>
+  </div>
+</template>
+
+<script>
+import VaAdvancedColorPicker from './VaAdvancedColorPicker'
+import VaSimplePalettePicker from './VaSimplePalettePicker'
+import VaSliderColorPicker from './VaSliderColorPicker'
+import VaColorSquare from './VaColorSquare'
+import VaColorInput from './VaColorInput'
+import VaDropdownPopper from '../va-dropdown/VaDropdown'
+
+export default {
+  name: 'va-color-picker-input',
+  components: {
+    VaDropdownPopper,
+    VaColorSquare,
+    VaSimplePalettePicker,
+    VaAdvancedColorPicker,
+    VaSliderColorPicker,
+    VaColorInput,
+  },
+  props: {
+    mode: {
+      type: String,
+    },
+    palette: {
+      type: Array,
+    },
+    value: {
+      default: '',
+    },
+    selected: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  computed: {
+    valueProxy: {
+      get () {
+        return this.value
+      },
+      set (value) {
+        this.$emit('input', value)
+      },
+    },
+    isInputDisabled () {
+      return !!(this.mode === 'palette' && this.palette)
+    },
+  },
+  methods: {
+    validator (value) {
+      return ['palette', 'slider', 'advanced'].includes(value)
+    },
+  },
+}
+</script>
+
+<style lang="scss">
+@import "../../vuestic-sass/resources/resources";
+
+.va-color-picker-input {
+  &__dropdown {
+    background: $white;
+    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
+  }
+
+  &__slot {
+    cursor: pointer
+  }
+}
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaColorSquare.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorSquare.vue
new file mode 100644
index 0000000..812a069
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaColorSquare.vue
@@ -0,0 +1,27 @@
+<template>
+  <div
+    class="va-color-square"
+    :style="{'background-color': value}"
+  />
+</template>
+
+<script>
+export default {
+  name: 'va-color-square',
+  props: {
+    value: {
+      required: true,
+    },
+  },
+}
+</script>
+
+<style lang='scss'>
+@import "../../vuestic-sass/resources/resources";
+
+.va-color-square {
+  height: 48px;
+  width: 48px;
+  border: 1px solid $gray-light;
+}
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.demo.vue
new file mode 100644
index 0000000..04edbbb
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.demo.vue
@@ -0,0 +1,32 @@
+<template>
+  <div class="demo-container">
+    <div class="demo-container__item">
+      <va-palette-custom :palette="palette" v-model="color"/>
+    </div>
+    <div class="demo-container__item">
+      <va-palette-custom :palette="palette" v-model="color"/>
+    </div>
+  </div>
+</template>
+
+<script>
+import { colorArray } from './VuesticTheme'
+
+import VaPaletteCustom from './VaPaletteCustom'
+
+export default {
+  components: {
+    VaPaletteCustom,
+  },
+  data () {
+    return {
+      palette: colorArray,
+      color: '#aaaaaa',
+    }
+  },
+}
+</script>
+
+<style lang="scss">
+
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.vue
new file mode 100644
index 0000000..17e57ff
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaPaletteCustom.vue
@@ -0,0 +1,67 @@
+<template>
+  <div class="va-palette-custom">
+    <va-simple-palette-picker
+      class="va-palette-custom__palette mr-2"
+      :palette="palette"
+      v-model="valueProxy"
+    />
+    <va-color-picker-input
+      class="va-palette-custom__input"
+      mode="advanced"
+      v-model="valueProxy"
+    >
+      <va-color-input
+        :selected="dotIsSelected"
+        v-model="valueProxy"
+      />
+    </va-color-picker-input>
+  </div>
+</template>
+
+<script>
+
+import VaColorPickerInput from './VaColorPickerInput'
+import VaSimplePalettePicker from './VaSimplePalettePicker'
+import VaColorInput from './VaColorInput'
+
+export default {
+  name: 'va-palette-custom',
+  components: {
+    VaColorInput,
+    VaColorPickerInput,
+    VaSimplePalettePicker,
+  },
+  props: {
+    value: {
+      type: String,
+      default: '',
+    },
+    palette: {
+      type: Array,
+    },
+  },
+  computed: {
+    valueProxy: {
+      get () {
+        return this.value
+      },
+      set (value) {
+        this.$emit('input', value)
+      },
+    },
+    dotIsSelected () {
+      return this.palette.includes(this.value)
+    },
+  },
+}
+</script>
+
+<style lang="scss">
+.va-palette-custom {
+  display: flex;
+
+  &__input {
+    float: right;
+  }
+}
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.demo.vue
new file mode 100644
index 0000000..64e65d0
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.demo.vue
@@ -0,0 +1,34 @@
+<template>
+  <div class="demo-container">
+    <div class="demo-container__item">
+      <va-simple-palette-picker
+        v-model="value"
+        :palette="palette"
+      />
+      <va-simple-palette-picker
+        v-model="value"
+        :palette="palette"
+      />
+      {{ value }}
+    </div>
+  </div>
+</template>
+
+<script>
+import VaSimplePalettePicker from './VaSimplePalettePicker'
+import { colorArray } from './VuesticTheme'
+import VaPaletteCustom from './VaPaletteCustom'
+
+export default {
+  components: {
+    VaSimplePalettePicker,
+    VaPaletteCustom,
+  },
+  data () {
+    return {
+      value: '#AAA',
+      palette: colorArray,
+    }
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.vue
new file mode 100644
index 0000000..09db5a5
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaSimplePalettePicker.vue
@@ -0,0 +1,61 @@
+<template>
+  <div class="va-simple-palette-picker">
+    <ul class="va-simple-palette-picker__colors">
+      <color-dot
+        v-for="(color, index) in palette" :key="index"
+        :color="color"
+        @click.native="handlerClick(color)"
+        :selected="isSelected(color)"
+      />
+    </ul>
+  </div>
+</template>
+
+<script>
+import ColorDot from './ColorDot'
+
+export default {
+  name: 'va-simple-palette-picker',
+  components: {
+    ColorDot,
+  },
+  props: {
+    palette: {
+      type: Array,
+    },
+    value: {
+      default: '',
+    },
+  },
+  computed: {
+    valueProxy: {
+      get () {
+        return this.value
+      },
+      set (value) {
+        this.$emit('input', value)
+      },
+    },
+  },
+  methods: {
+    isSelected (color) {
+      return this.value === color
+    },
+    handlerClick (color) {
+      this.valueProxy = color
+    },
+  },
+}
+</script>
+
+<style lang="scss">
+.va-simple-palette-picker {
+  padding-top: 3px;
+
+  &__colors {
+    padding: 3px;
+    display: flex;
+  }
+}
+
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.demo.vue
new file mode 100644
index 0000000..6a05368
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.demo.vue
@@ -0,0 +1,25 @@
+<template>
+  <div class="demo-container">
+    <div class="demo-container__item">
+      <va-slider-color-picker v-model="value"/>
+      <va-slider-color-picker v-model="value" style="padding-top: 20px"/>
+      {{value}}
+    </div>
+  </div>
+</template>
+
+<script>
+
+import VaSliderColorPicker from './VaSliderColorPicker'
+
+export default {
+  components: {
+    VaSliderColorPicker,
+  },
+  data () {
+    return {
+      value: '#34495e',
+    }
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.vue
new file mode 100644
index 0000000..366468f
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VaSliderColorPicker.vue
@@ -0,0 +1,35 @@
+<template>
+  <SliderPicker v-model="valueProxy" class="vuestic-slider-picker"/>
+</template>
+
+<script>
+import { Slider } from 'vue-color'
+
+export default {
+  name: 'va-slider-color-picker',
+  components: {
+    'SliderPicker': Slider,
+  },
+  props: {
+    value: {
+      default: '',
+    },
+  },
+  computed: {
+    valueProxy: {
+      get () {
+        return this.value
+      },
+      set (value) {
+        this.$emit('input', value.hex)
+      },
+    },
+  },
+}
+</script>
+
+<style>
+.vuestic-slider-picker {
+  padding: 8px;
+}
+</style>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticAdvancedColorPicker.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticAdvancedColorPicker.demo.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticAdvancedColorPicker.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticAdvancedColorPicker.demo.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticAdvancedColorPicker.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticAdvancedColorPicker.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticAdvancedColorPicker.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticAdvancedColorPicker.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorDropdown.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorDropdown.demo.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorDropdown.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorDropdown.demo.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorDropdown.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorDropdown.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorDropdown.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorDropdown.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorInput.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorInput.demo.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorInput.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorInput.demo.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorInput.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorInput.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorInput.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorInput.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorPickerInput.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorPickerInput.demo.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorPickerInput.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorPickerInput.demo.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorPickerInput.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorPickerInput.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorPickerInput.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorPickerInput.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorSquare.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorSquare.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticColorSquare.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticColorSquare.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticPalletCustom.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticPalletCustom.demo.vue
similarity index 84%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticPalletCustom.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticPalletCustom.demo.vue
index e001b61..447cac4 100644
--- a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticPalletCustom.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticPalletCustom.demo.vue
@@ -10,7 +10,7 @@
 </template>
 
 <script>
-import { colorArray } from '../../../vuestic-theme/vuestic-components/vuestic-color-picker/VuesticTheme'
+import { colorArray } from './/VuesticTheme'
 
 import VuesticPalletCustom from './VuesticPalletCustom'
 
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticPalletCustom.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticPalletCustom.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticPalletCustom.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticPalletCustom.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSimplePalettePicker.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticSimplePalettePicker.demo.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSimplePalettePicker.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticSimplePalettePicker.demo.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSimplePalettePicker.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticSimplePalettePicker.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSimplePalettePicker.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticSimplePalettePicker.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSliderColorPicker.demo.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticSliderColorPicker.demo.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSliderColorPicker.demo.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticSliderColorPicker.demo.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSliderColorPicker.vue b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticSliderColorPicker.vue
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticSliderColorPicker.vue
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticSliderColorPicker.vue
diff --git a/src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticTheme.js b/src/vuestic-theme/vuestic-components/va-color-picker/VuesticTheme.js
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-color-picker/VuesticTheme.js
rename to src/vuestic-theme/vuestic-components/va-color-picker/VuesticTheme.js
diff --git a/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js b/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js
index 8901f81..1198ad2 100644
--- a/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js
+++ b/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js
@@ -2,7 +2,7 @@ import utils from '../../../../services/utils'
 import {
   colorConfig,
   VuesticTheme
-} from './../../vuestic-color-picker/VuesticTheme'
+} from '../../va-color-picker/VuesticTheme'
 
 export const progressMixin = {
   props: {
diff --git a/src/vuestic-theme/vuestic-components/vuestic-radio-button/VuesticRadioButton.demo.vue b/src/vuestic-theme/vuestic-components/va-radio-button/VaRadioButton.demo.vue
similarity index 87%
rename from src/vuestic-theme/vuestic-components/vuestic-radio-button/VuesticRadioButton.demo.vue
rename to src/vuestic-theme/vuestic-components/va-radio-button/VaRadioButton.demo.vue
index 2d70f28..f6c39fe 100644
--- a/src/vuestic-theme/vuestic-components/vuestic-radio-button/VuesticRadioButton.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-radio-button/VaRadioButton.demo.vue
@@ -1,7 +1,7 @@
 <template>
   <div class="demo-container">
     <div class="demo-container__item">
-      <vuestic-radio-button
+      <va-radio-button
         v-for="(option, index) in options"
         :key="index"
         v-model="selectedOptionString"
@@ -9,7 +9,7 @@
       />
     </div>
     <div class="demo-container__item">
-      <vuestic-radio-button
+      <va-radio-button
         v-for="(option, index) in options"
         :key="index"
         v-model="selectedOptionString"
@@ -18,7 +18,7 @@
       />
     </div>
     <div class="demo-container__item">
-      <vuestic-radio-button
+      <va-radio-button
         v-for="(option, index) in options"
         :key="index"
         v-model="selectedOptionString"
@@ -31,7 +31,7 @@
     </div>
 
     <div class="demo-container__item">
-      <vuestic-radio-button
+      <va-radio-button
         v-for="option in objectOptions"
         :key="option.key"
         v-model="selectedOptionObject"
@@ -44,10 +44,10 @@
 </template>
 
 <script>
-import VuesticRadioButton from './VuesticRadioButton'
+import VaRadioButton from './VaRadioButton'
 
 export default {
-  components: { VuesticRadioButton },
+  components: { VaRadioButton },
   data () {
     const objectOptions = [
       { key: 1, name: 'one' },
diff --git a/src/vuestic-theme/vuestic-components/vuestic-radio-button/VuesticRadioButton.vue b/src/vuestic-theme/vuestic-components/va-radio-button/VaRadioButton.vue
similarity index 73%
rename from src/vuestic-theme/vuestic-components/vuestic-radio-button/VuesticRadioButton.vue
rename to src/vuestic-theme/vuestic-components/va-radio-button/VaRadioButton.vue
index 8180473..fe03ed1 100644
--- a/src/vuestic-theme/vuestic-components/vuestic-radio-button/VuesticRadioButton.vue
+++ b/src/vuestic-theme/vuestic-components/va-radio-button/VaRadioButton.vue
@@ -1,11 +1,11 @@
 <template>
   <div
-    class="vuestic-radio-button"
+    class="va-radio-button"
     :class="computedClass"
     @click="onClick"
   >
     <div
-      class="vuestic-radio-button__content"
+      class="va-radio-button__content"
       @mousedown="focused = false"
       @mouseup="focused = false"
       :class="{'active': isActive}"
@@ -14,14 +14,14 @@
         @focus="focused = true"
         @mouseout="focused = false"
         @blur="focused = false"
-        :checked="isActive" type="radio" class="vuestic-radio-button__input"
+        :checked="isActive" type="radio" class="va-radio-button__input"
         :disabled="disabled"
       />
-      <div class="vuestic-radio-button__icon">
-        <div class="vuestic-radio-button__icon-circle"/>
+      <div class="va-radio-button__icon">
+        <div class="va-radio-button__icon-circle"/>
       </div>
     </div>
-    <div class="vuestic-radio-button__slot-container">
+    <div class="va-radio-button__slot-container">
       <slot name="label">
         {{ computedLabel }}
       </slot>
@@ -32,7 +32,7 @@
 <script>
 
 export default {
-  name: 'vuestic-radio-button',
+  name: 'va-radio-button',
   props: {
     value: [Boolean, String, Number],
     option: [String, Boolean, Number],
@@ -44,15 +44,15 @@ export default {
   },
   data () {
     return {
-      isFocused: false
+      isFocused: false,
     }
   },
   computed: {
     computedClass () {
       return {
-        'vuestic-radio-button--active': this.isActive,
-        'vuestic-radio-button--disabled': this.disabled,
-        'vuestic-radio-button--on-focus': this.focused
+        'va-radio-button--active': this.isActive,
+        'va-radio-button--disabled': this.disabled,
+        'va-radio-button--on-focus': this.focused,
       }
     },
     focused: {
@@ -63,7 +63,7 @@ export default {
       },
       get () {
         return this.isFocused
-      }
+      },
     },
     computedLabel () {
       if (!this.label) {
@@ -73,7 +73,7 @@ export default {
     },
     isActive () {
       return this.value === this.option
-    }
+    },
   },
   methods: {
     onClick () {
@@ -88,29 +88,30 @@ export default {
 <style lang="scss">
 @import "../../vuestic-sass/resources/resources";
 
-.vuestic-radio-button {
+.va-radio-button {
   cursor: pointer;
   display: flex;
   flex-direction: row;
-  margin-bottom: $checkbox-between-items-margin;
+
   &__icon {
     width: 1.4rem;
     height: 1.4rem;
     border-radius: 1.8rem;
     border: $gray solid 0.15rem;
     @at-root {
-      .vuestic-radio-button.vuestic-radio-button--active & {
+      .va-radio-button.va-radio-button--active & {
         border: $vue-green solid 0.15rem;
       }
 
-      .vuestic-radio-button.vuestic-radio-button--disabled & {
+      .va-radio-button.va-radio-button--disabled & {
         opacity: 0.4;
       }
     }
   }
+
   &__icon-circle {
     @at-root {
-      .vuestic-radio-button.vuestic-radio-button--active & {
+      .va-radio-button.va-radio-button--active & {
         width: 0.625rem;
         height: 0.625rem;
         border-radius: 1rem;
@@ -120,6 +121,7 @@ export default {
       }
     }
   }
+
   &__input {
     width: 1.375rem;
     height: 1.375rem;
@@ -127,6 +129,7 @@ export default {
     cursor: pointer;
     opacity: 0;
   }
+
   #{&}__content {
     width: 32px;
     height: 32px;
@@ -134,16 +137,18 @@ export default {
     align-items: center;
     justify-content: center;
     @at-root {
-      .vuestic-radio-button--on-focus#{&} {
+      .va-radio-button--on-focus#{&} {
         background-color: $light-gray;
         transition: all, 0.6s, ease-in;
         border-radius: 3rem;
+
         &.active {
           background-color: $lighter-green;
         }
       }
     }
   }
+
   &__slot-container {
     padding-top: $checkbox-label-margin-top;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-tabs/VaTab.vue b/src/vuestic-theme/vuestic-components/va-tabs/VaTab.vue
index 5741b88..c7ab65c 100644
--- a/src/vuestic-theme/vuestic-components/va-tabs/VaTab.vue
+++ b/src/vuestic-theme/vuestic-components/va-tabs/VaTab.vue
@@ -1,14 +1,15 @@
 <template>
   <div
-    @click="selectTab"
     class="va-tab"
     :class="{
-    'va-tab--active': isActive(),
-    'va-tab--disabled': disabled
+      'va-tab--active': isActive,
+      'va-tab--disabled': disabled
     }"
-    :style="{width: widthComputed}"
+    @click="$emit('tabClick', !isActive)"
   >
-    <slot/>
+    <div class="va-tab__content" ref="content">
+      <slot/>
+    </div>
   </div>
 </template>
 
@@ -17,46 +18,71 @@ export default {
   name: 'va-tab',
   props: {
     disabled: {
-      type: Boolean
-    }
+      type: Boolean,
+    },
   },
-  computed: {
-    widthComputed () {
-      return this.$parent.grow ? 100 / this.$parent.$slots.default.length + '%' : ''
+  data () {
+    return {
+      isActive: false,
     }
   },
-  methods: {
-    selectTab () {
-      this.$parent.selectTab(this)
+  inject: {
+    tabGroup: {
+      default: null,
     },
-    isActive () {
-      return this.$parent.tabSelected(this)
-    }
+  },
+  created () {
+    this.tabGroup && this.tabGroup.register(this)
   },
   beforeDestroy () {
-    if (this.$parent.$children.indexOf(this) === this.$parent.value &&
-      this.$parent.value > 0) {
-      this.$parent.selectTab(this.$parent.$children[this.$parent.value - 1])
-    }
-  }
+    this.tabGroup && this.tabGroup.unregister(this)
+  },
 }
 </script>
 
 <style lang="scss">
+@import "../../vuestic-sass/resources/resources";
+
 .va-tab {
+  align-items: center;
+  display: inline-flex;
+  flex: 0 1 auto;
+  font-weight: $font-weight-bold;
+  line-height: normal;
+  height: inherit;
+  max-width: 264px;
+  text-align: center;
+  vertical-align: middle;
+
   padding: 0.4375rem 0.75rem;
-  margin-left: 0.5rem;
-  margin-right: 0.5rem;
-  opacity: 0.5;
   font-weight: $font-weight-bold;
   cursor: pointer;
-  display: flex;
-  justify-content: center;
-  &:hover, &--active {
-    opacity: 1;
+
+  &:not(.va-tab--active) {
+    opacity: .5;
+  }
+
+  &__content {
+    align-items: center;
+    color: inherit;
+    display: flex;
+    flex: 1 1 auto;
+    height: 100%;
+    justify-content: center;
+    max-width: inherit;
+    text-decoration: none;
+    transition: $transition-primary;
+    user-select: none;
+    white-space: normal;
   }
-  &--disabled {
-    cursor: default;
+
+  .va-tab--disabled {
+    .va-tab__container {
+      @include va-disabled();
+    }
+
+    pointer-events: none;
+    cursor: inherit;
   }
 }
 </style>
diff --git a/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.demo.vue b/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.demo.vue
index 47b2690..0a00fec 100644
--- a/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.demo.vue
@@ -1,58 +1,121 @@
 <template>
   <VbDemo>
-    <VbContainer style="width: 100%">
+    <VbCard refresh title="Tabs usage example">
+      <TabsExample/>
+    </VbCard>
+    <VbCard title="Default">
+      <va-tabs v-model="tabValue">
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+      </va-tabs>
+    </VbCard>
+    <VbCard title="Align right" width="400px">
+      <va-tabs right v-model="tabValue">
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+      </va-tabs>
+    </VbCard>
+    <VbCard title="Centered" width="400px">
+      <va-tabs center v-model="tabValue">
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+      </va-tabs>
+    </VbCard>
+    <VbCard title="Grow" width="400px">
+      <va-tabs grow v-model="tabValue">
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+      </va-tabs>
+    </VbCard>
+    <VbCard title="Overflow">
       <va-tabs
-        v-model="value"
-        :right="option === 'align-right'"
-        :grow="option == 'grow'"
-        :color="color"
-        :hide-slider="hideSlider"
+        style="width: 120px"
+        v-model="tabValue"
       >
-        <va-tab>{{'item1item1item1item1item1'}}</va-tab>
-        <va-tab
-          v-for="item in count"
-          :key="item"
-        >
-          {{'item'+ item}}
-        </va-tab>
-      </va-tabs>
-      {{value}}
-    </VbContainer>
-    <VbContainer>
-      <button @click="count++">add item</button>
-      <button @click="count--">remove item</button>
-    </VbContainer>
-    <VbContainer>
-      <vuestic-radio-button
-        v-for="value in options"
-        :key="value"
-        v-model="option"
-        :option="value"
-      />
-    </VbContainer>
-    <VbContainer>
-      <vuestic-checkbox v-model="hideSlider" label="hide-slider"/>
-    </VbContainer>
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+      </va-tabs>
+    </VbCard>
+    <VbCard title="Overflow text">
+      <va-tabs v-model="tabValue">
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+        <va-tab>
+          Somewhat long long long long long long long long text
+        </va-tab>
+      </va-tabs>
+    </VbCard>
+    <VbCard title="Overflow text + grow">
+      <va-tabs grow v-model="tabValue">
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+        <va-tab>
+          Somewhat long long long long long long long long long text
+        </va-tab>
+      </va-tabs>
+    </VbCard>
+    <VbCard title="Hide slider">
+      <va-tabs hideSlider v-model="tabValue">
+        <va-tab
+          v-for="title in tabTitles"
+          :key="title"
+        >
+          {{title}}
+        </va-tab>
+      </va-tabs>
+    </VbCard>
   </VbDemo>
 </template>
 
 <script>
-import VuesticCheckbox from '../va-checkbox/VaCheckbox'
-import VuesticRadioButton from '../vuestic-radio-button/VuesticRadioButton'
+import VaCheckbox from '../va-checkbox/VaCheckbox'
+import VaRadioButton from '../va-radio-button/VaRadioButton'
+import VaAdvancedColorPicker from '../va-color-picker/VaAdvancedColorPicker'
+import VaTabs from './VaTabs'
+import VaTab from './VaTab'
+import TabsExample from './__demo__/TabsExample'
 
 export default {
   components: {
-    VuesticRadioButton,
-    VuesticCheckbox,
+    TabsExample,
+    VaAdvancedColorPicker,
+    VaRadioButton,
+    VaCheckbox,
+    VaTabs,
+    VaTab,
   },
   data () {
     return {
-      count: 6,
-      value: 4,
-      options: ['default', 'align-right', 'grow'],
-      option: 'default',
-      color: 'White',
-      hideSlider: false,
+      tabTitles: ['One', 'Two', 'Three'],
+      tabValue: 1,
     }
   },
 }
diff --git a/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue b/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue
index c028f6a..75533c0 100644
--- a/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue
+++ b/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue
@@ -1,54 +1,65 @@
 <template>
   <div class="va-tabs">
-    <div
-      class="va-tabs__bar va-row"
-      :class="{
-       'va-tabs__bar--align-right': right,
-       'va-tabs__bar--grow': grow
-      }"
-    >
+    <div class="va-tabs__wrapper">
       <div
-        class="va-tabs__bar-content"
-        :class="{'va-tabs__bar-content--grow': grow}"
+        class="va-tabs__container"
+        :class="containerClass"
       >
-        <div
-          class="va-tabs__bar-content-items"
-          :class="{'grow': grow}"
-        >
-          <slot/>
-        </div>
-        <div
-          v-if="!hideSlider"
-          class="va-tabs__bar-content-slider"
-          :style="{
-             width: getBarWidth($slots.default),
-             marginLeft: getMarginLeft($slots.default)
-          }"
-        >
-          <div class="va-tabs__bar-content-slider-line"/>
+        <div class="va-tabs__slider-wrapper" :style="sliderStyles">
+          <div class="va-tabs__slider"/>
         </div>
+        <slot/>
       </div>
     </div>
   </div>
 </template>
 
 <script>
+
+import VaTab from './VaTab'
+
 export default {
   name: 'va-tabs',
-  props: {
-    value: {
-      required: true
-    },
-    right: {
-      type: Boolean
-    },
-    grow: {
-      type: Boolean
-    },
-    hideSlider: {
-      type: Boolean
+  components: {
+    VaTab,
+  },
+  provide () {
+    return {
+      tabGroup: {
+        register: this.register,
+        unregister: this.unregister,
+      },
+    }
+  },
+  data () {
+    return {
+      tabs: [],
+      tabsWidth: [],
+      sliderLeft: 0,
+      sliderWidth: 0,
     }
   },
+  subs: {
+    resizeEnd () {
+      this.updateSlider()
+    },
+  },
+  props: {
+    value: { type: Number },
+    right: Boolean,
+    center: Boolean,
+    grow: Boolean,
+    hideSlider: Boolean,
+  },
+  mounted () {
+    this.updateSlider()
+  },
+  watch: {
+    value: 'syncStateWithValue',
+    right: 'updateSlider',
+    grow: 'updateSlider',
+    hideSlider: 'updateSlider',
+  },
   computed: {
     valueProxy: {
       set (valueProxy) {
@@ -56,108 +67,133 @@ export default {
       },
       get () {
         return this.value
+      },
+    },
+    containerClass () {
+      return {
+        'va-tabs__container--grow': this.grow,
+        'va-tabs__container--right': this.right,
+        'va-tabs__container--center': this.center,
       }
-    }
+    },
+    sliderStyles () {
+      return this.selectedTab
+        ? {
+          left: `${this.sliderLeft}px`,
+          width: `${this.sliderWidth}px`,
+        }
+        : {}
+    },
+    selectedTab () {
+      return this.tabs[this.value] || null
+    },
   },
   methods: {
-    setTabsWidth (slots) {
-      let count = 0
-      slots.forEach(vnode => {
-        if (vnode.elm) {
-          this.tabsWidth[count] = vnode.elm.clientWidth
-          count++
-        }
+    syncStateWithValue () {
+      this.tabs.forEach((tab, index) => {
+        tab.isActive = index === this.value
       })
+      this.updateSlider()
     },
-    getBarWidth (slots) {
-      this.setTabsWidth(slots)
-      return this.grow ? 100 / slots.length + '%' : `calc(` + this.tabsWidth[this.value] + `px - 1.25rem)`
-    },
-    getMarginLeft (slots) {
-      this.setTabsWidth(slots)
-      if (!this.grow && this.value === 0) {
-        return 1.25 + 'rem'
+    register (tab) {
+      const index = this.tabs.push(tab) - 1
+      tab.$on('tabClick', () => this.onTabClick(tab))
+
+      if (index === this.value) {
+        tab.isActive = true
       }
-      if (!this.grow && this.value !== 0) {
-        let marginLeft = 0
-        for (let count = 0; count < this.value; count++) {
-          marginLeft += this.tabsWidth[count]
-        }
-        return `calc(` + marginLeft + `px + ` + this.value + `rem + 1.2rem)`
+    },
+    unregister (tab) {
+      if (this._isDestroyed) return
+
+      this.tabs.splice(this.tabs.indexOf(tab), 1)
+
+      this.syncStateWithValue()
+    },
+    onTabClick (tab) {
+      const index = this.tabs.indexOf(tab)
+      this.valueProxy = index
+    },
+    async updateSlider () {
+      await this.$nextTick()
+
+      if (this.hideSlider) {
+        return
       }
-      if (this.grow && this.value !== 0) {
-        return this.value * (100 / slots.length) + `%`
+      const selectedTab = this.selectedTab
+      if (!selectedTab || !selectedTab.$refs.content) {
+        return
       }
+      const content = selectedTab.$refs.content
+      this.sliderWidth = content.scrollWidth + 4
+      this.sliderLeft = content.offsetLeft - 2
     },
-    selectTab (tabToSelect) {
-      this.$slots.default.forEach((tabSlot, index) => {
-        if (tabSlot.componentInstance === tabToSelect) {
-          this.valueProxy = index
-        }
-      })
-    },
-    tabSelected (tabToCompare) {
-      return this.$slots.default.some((tabSlot, index) => {
-        if (tabSlot.componentInstance === tabToCompare) {
-          return index === this.value
-        }
-      })
-    }
-  },
-  data () {
-    return {
-      tabsWidth: []
-    }
   },
-  mounted () {
-    let count = 0
-    this.$slots.default.forEach(vnode => {
-      if (vnode.elm.clientWidth) {
-        this.tabsWidth[count] = vnode.elm.clientWidth
-        count++
-      }
-    })
-  }
 }
 </script>
 
 <style lang="scss">
+@import "../../vuestic-sass/resources/resources";
+
 .va-tabs {
-  &__bar {
-    padding-top: 1rem;
-    &--align-right {
-      justify-content: flex-end;
-    }
+  position: relative;
+
+  &--right {
+    display: flex;
+    justify-content: flex-end;
+  }
+
+  .va-tabs__wrapper {
+    overflow: auto;
+    contain: content;
+    display: flex;
+  }
+
+  .va-tabs__container {
+    flex: 1 0 auto;
+    display: flex;
+    height: 2.5rem;
+    list-style-type: none;
+    transition: transform 0.6s cubic-bezier(0.86, 0, 0.07, 1);
+    white-space: nowrap;
+    position: relative;
+
     &--grow {
-      justify-content: space-around;
+      .va-tab {
+        flex: 1 0 auto;
+        max-width: none;
+      }
     }
-    &-content {
-      margin-bottom: 2.5rem;
-      &--grow {
-        width: 100%;
+
+    &--center, &--right {
+      > .va-tab:first-child {
+        margin-left: auto
       }
-      &-items {
-        display: flex;
-        &.grow {
-          justify-content: space-around;
-        }
+
+      .va-tabs__slider-wrapper + .va-tab {
+        margin-left: auto;
       }
-      &-slider {
-        display: flex;
-        transition: margin-left 0.3s;
-        &.align-right {
-          justify-content: flex-end;
-        }
-        &.grow {
-          justify-content: space-around;
-        }
-        &-line {
-          width: 100%;
-          height: 2px;
-          background-color: $vue-green;
-        }
+    }
+
+    &--center {
+      > .va-tab:last-child {
+        margin-right: auto;
       }
     }
   }
+
+  .va-tabs__slider-wrapper {
+    bottom: 0;
+    margin: 0 !important;
+    position: absolute;
+    z-index: 4000;
+    transition: $transition-primary;
+
+    .va-tabs__slider {
+      width: 100%;
+      height: 0.125rem;
+      background-color: $brand-primary;
+    }
+  }
 }
 </style>
diff --git a/src/vuestic-theme/vuestic-components/va-tabs/__demo__/TabsExample.vue b/src/vuestic-theme/vuestic-components/va-tabs/__demo__/TabsExample.vue
new file mode 100644
index 0000000..adce66d
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-tabs/__demo__/TabsExample.vue
@@ -0,0 +1,70 @@
+<template>
+  <div>
+    <va-tabs v-model="selectedTabIndex">
+      <va-tab
+        v-for="tabObject in tabObjects"
+        :key="tabObject.key"
+      >
+        {{ tabObject.title }}
+        <va-icon
+          icon="fa fa-times"
+          class="ml-2"
+          @click.stop.native="removeTab(tabObject)"
+        />
+      </va-tab>
+    </va-tabs>
+
+    <div style="height: 20px"/>
+
+    <div>
+      Content: {{ selectedTab ? selectedTab.title : 'none' }}
+    </div>
+    <div>
+      <input type="number" v-model.number="selectedTabIndex">
+    </div>
+  </div>
+</template>
+
+<script>
+import VaTabs from '../VaTabs'
+import VaTab from '../VaTab'
+import VaIcon from '../../va-icon/VaIcon'
+
+export default {
+  name: 'TabsExample',
+  components: {
+    VaIcon,
+    VaTab,
+    VaTabs,
+  },
+  data () {
+    return {
+      selectedTabIndex: 0,
+      tabObjects: [
+        {
+          key: 'products',
+          title: 'Products',
+        },
+        {
+          key: 'orders',
+          title: 'Orders',
+        },
+        {
+          key: 'cart',
+          title: 'Cart',
+        },
+      ],
+    }
+  },
+  computed: {
+    selectedTab () {
+      return this.tabObjects[this.selectedTabIndex]
+    },
+  },
+  methods: {
+    removeTab (tabToRemove) {
+      this.tabObjects = this.tabObjects.filter(tab => tab !== tabToRemove)
+    },
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-tabs/tabs-docs.md b/src/vuestic-theme/vuestic-components/va-tabs/tabs-docs.md
new file mode 100644
index 0000000..c0e98e4
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-tabs/tabs-docs.md
@@ -0,0 +1,35 @@
+### Limitations
+
+For now we focus on bare-bones tabs and make them solid.
+
+* Only one style option.
+* There is no content, only tabs themselves.
+* Tabs are positioned only on top.
+* Scroll is native scroll. No fancy arrows or custom scroll.
+
+### Template
+
+```html
+<va-tabs v-model="tabValue">
+  <va-tab
+    v-for="title in tabTitles"
+    :key="title"
+  >
+    {{title}}
+  </va-tab>
+</va-tabs>
+```
+
+### Props
+
+#### Component: va-tabs
+
+* `value` - Number  - selected tab index.
+* `right` - Boolean - align right.
+* `center` - Boolean - align center.
+* `grow` - Boolean - tabs will take full width.
+* `hideSlider` - Boolean - hide slider.
+
+#### Component: va-tab
+
+Has no props.
diff --git a/src/vuestic-theme/vuestic-plugin.js b/src/vuestic-theme/vuestic-plugin.js
index f399299..9b77735 100644
--- a/src/vuestic-theme/vuestic-plugin.js
+++ b/src/vuestic-theme/vuestic-plugin.js
@@ -36,7 +36,7 @@ import VaButtonToggle
 import VaPagination
   from './vuestic-components/va-pagination/VaPagination.vue'
 import RadioButton
-  from './vuestic-components/vuestic-radio-button/VuesticRadioButton'
+  from './vuestic-components/va-radio-button/VaRadioButton'
 import Scrollbar
   from './vuestic-components/va-scrollbar/VaScrollbar.vue'
 import SimpleSelect
diff --git a/src/vuestic-theme/vuestic-sass/global/color-themes.demo.vue b/src/vuestic-theme/vuestic-sass/global/color-themes.demo.vue
index 3120848..c607895 100644
--- a/src/vuestic-theme/vuestic-sass/global/color-themes.demo.vue
+++ b/src/vuestic-theme/vuestic-sass/global/color-themes.demo.vue
@@ -61,7 +61,7 @@
 import VaButton from './../../vuestic-components/va-button/VaButton'
 import VaNotification from './../../vuestic-components/va-notification/VaNotification'
 import VaProgressBar from './../../vuestic-components/va-progress-bar/progress-types/VaProgressBar'
-import VaColorPickerInput from '../../vuestic-components/vuestic-color-picker/VuesticColorPickerInput'
+import VaColorPickerInput from '../../vuestic-components/va-color-picker/VuesticColorPickerInput'
 import VaSidebar from '../../vuestic-components/va-sidebar/VaSidebar'
 import SidebarLinkGroup from './../../../components/layout/app-sidebar/components/SidebarLinkGroup'
 import SidebarLink from './../../../components/layout/app-sidebar/components/SidebarLink'
diff --git a/src/vuestic-theme/vuestic-sass/resources/_variables.scss b/src/vuestic-theme/vuestic-sass/resources/_variables.scss
index dd19b86..c1bd55d 100644
--- a/src/vuestic-theme/vuestic-sass/resources/_variables.scss
+++ b/src/vuestic-theme/vuestic-sass/resources/_variables.scss
@@ -62,6 +62,9 @@ $theme-colors: (
   "pale": $theme-pale
 );
 
+$transition-primary: 0.3s cubic-bezier(.25,.8,.50,1); // swing
+$transition-secondary: 0.2s cubic-bezier(0.4, 0.0, 0.6, 1); // ease-in-out
+
 // Layout //
 $body-bg: $light-gray;
 $top-nav-bg: $dark-blue;
diff --git a/yarn.lock b/yarn.lock
index ca487cd..e16e3b7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6867,16 +6867,6 @@ lodash._reinterpolate@~3.0.0:
   resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
   integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
 
-lodash.assign@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
-  integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
-
-lodash.clonedeep@^4.3.2:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
-  integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
-
 lodash.defaultsdeep@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz#bec1024f85b1bd96cbea405b23c14ad6443a6f81"
@@ -6922,11 +6912,6 @@ lodash.merge@^4.6.0:
   resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
   integrity sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==
 
-lodash.mergewith@^4.6.0:
-  version "4.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
-  integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
-
 lodash.once@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
@@ -7453,7 +7438,12 @@ mute-stream@0.0.7:
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
   integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
 
-nan@^2.10.0, nan@^2.9.2:
+nan@^2.13.2:
+  version "2.14.0"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
+  integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
+
+nan@^2.9.2:
   version "2.13.1"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd"
   integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==
@@ -7629,10 +7619,10 @@ node-releases@^1.1.11:
   dependencies:
     semver "^5.3.0"
 
-node-sass@^4.9.0:
-  version "4.11.0"
-  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
-  integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
+node-sass@^4.12.0:
+  version "4.12.0"
+  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017"
+  integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
   dependencies:
     async-foreach "^0.1.3"
     chalk "^1.1.1"
@@ -7641,12 +7631,10 @@ node-sass@^4.9.0:
     get-stdin "^4.0.1"
     glob "^7.0.3"
     in-publish "^2.0.0"
-    lodash.assign "^4.2.0"
-    lodash.clonedeep "^4.3.2"
-    lodash.mergewith "^4.6.0"
+    lodash "^4.17.11"
     meow "^3.7.0"
     mkdirp "^0.5.1"
-    nan "^2.10.0"
+    nan "^2.13.2"
     node-gyp "^3.8.0"
     npmlog "^4.0.0"
     request "^2.88.0"
@@ -10800,10 +10788,10 @@ vm-browserify@0.0.4:
   dependencies:
     indexof "0.0.1"
 
-vue-book@0.1.0-alpha.14:
-  version "0.1.0-alpha.14"
-  resolved "https://registry.yarnpkg.com/vue-book/-/vue-book-0.1.0-alpha.14.tgz#8e4ccc6fd7f0a34f83e6647fd5f0f3829c186c5b"
-  integrity sha512-I3r0iK9oNMab44Ry0ZCKqkkfoQNcYh1nmKxfktBORq8VvrB/ODx2D49P8h98jMCdI5NCdw3H6U5y8Vc4bTsW7g==
+vue-book@0.1.0-alpha.17:
+  version "0.1.0-alpha.17"
+  resolved "https://registry.yarnpkg.com/vue-book/-/vue-book-0.1.0-alpha.17.tgz#4661bfc5a6813ccdfc5ce9ecb35cdcddbd163366"
+  integrity sha512-8BpuuImZfM3dZ1zsTdBA0fxpcbZ3VJ9JsPrNVVDqx0Gn9H5qgZXDGKmLnN4kSfXO5CZjDPns0Sj+ZeofdWt+Dg==
 
 vue-bulma-expanding@0.0.1:
   version "0.0.1"
-- 
GitLab


From 94b8692a6919e32b11903cfcbf0aadb2dbf751cb Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 2 Jul 2019 18:41:06 +0300
Subject: [PATCH 03/61] feat: upgrade version of va-selecrt, va-chart, va-icon,
 va-dropdown, fixes in va-navbar

---
 public/index.html                             |   1 +
 src/components/admin/UserActionsDropdown.vue  |   6 +-
 .../configSettings/forms/UploadForm.vue       |  23 +
 src/components/layout/AppLayout.vue           |   2 +-
 .../layout/app-navbar/AppNavbar.vue           |   2 +-
 .../components/dropdowns/LanguageDropdown.vue |  72 ++-
 .../components/dropdowns/MessageDropdown.vue  |  97 ++--
 .../dropdowns/NotificationDropdown.vue        | 111 ++--
 .../components/dropdowns/ProfileDropdown.vue  |  28 +-
 .../components/dropdowns/SettingsDropdown.vue |  75 +++
 .../layout/app-sidebar/AppSidebar.vue         |   2 +-
 .../app-sidebar/components/SidebarLink.vue    |   2 +-
 .../configSettings/ConfigSettingsPage.vue     |  31 +-
 src/data/Config.js                            |  22 +
 src/data/charts/BubbleChartData.js            | 236 ++++++++
 src/data/charts/DonutChartData.js             |   8 +
 src/data/charts/HorizontalBarChartData.js     |  17 +
 src/data/charts/LineChartData.js              |  43 ++
 src/data/charts/PieChartData.js               |   8 +
 src/data/charts/VerticalBarChartData.js       |  17 +
 .../{ConfigService => ConfigService.ts}       |   0
 .../vuestic-components/va-button/VaButton.vue |   4 +-
 .../va-chart/VaChart.demo.vue                 |  50 ++
 .../VuesticChart.vue => va-chart/VaChart.vue} |  17 +-
 .../VaChartConfigs.js}                        |   2 -
 .../chart-types/BubbleChart.js                |   0
 .../chart-types/DonutChart.js                 |   0
 .../chart-types/HorizontalBarChart.js         |   0
 .../chart-types/LineChart.js                  |   0
 .../chart-types/PieChart.js                   |   0
 .../chart-types/VerticalBarChart.js           |   0
 .../chart-types/chartMixin.js                 |   2 +-
 .../va-checkbox/VaCheckbox.vue                |   2 +-
 .../va-dropdown/VaDropdown.demo.vue           |   9 +
 .../va-dropdown/VaDropdown.vue                |  70 ++-
 .../va-dropdown/dropdown-popover-docs.md      |  21 +-
 .../va-icon/VaIcon.demo.vue                   | 118 ++--
 .../vuestic-components/va-icon/VaIcon.vue     |  31 +-
 .../va-icon/va-icon.docs.md                   |  20 +
 .../va-icon/va-iconset/VaIconCleanCode.vue    |   2 +
 .../va-icon/va-iconset/VaIconFree.vue         |   3 +
 .../va-icon/va-iconset/VaIconFresh.vue        |   2 +
 .../va-icon/va-iconset/VaIconResponsive.vue   |   2 +
 .../va-icon/va-iconset/VaIconRich.vue         |   2 +
 .../va-icon/va-iconset/VaIconVue.vue          |   2 +
 .../va-select/VaSelect.demo.vue               | 287 ++++++++++
 .../vuestic-components/va-select/VaSelect.vue | 519 ++++++++++++++++++
 .../va-select/va-select.docs.md               |  42 ++
 .../va-tabs/VuesticTabs.vue                   | 122 ----
 .../vuestic-chart/VuesticChart.demo.vue       |  45 --
 .../vuestic-chat/VuesticChat.vue              | 125 -----
 .../vuestic-grid/Spacing.demo.vue             |  15 -
 .../vuestic-grid/SpacingPlaygroud.vue         |  90 ---
 .../VuesticMultiSelect.demo.vue               |  36 --
 .../VuesticMultiSelect.vue                    | 168 ------
 .../VuesticSimpleSelect-413.demo.vue          |  38 --
 .../VuesticSimpleSelect.demo.vue              |  36 --
 .../VuesticSimpleSelect.vue                   | 228 --------
 src/vuestic-theme/vuestic-plugin.js           |  12 +-
 .../vuestic-sass/global/_typography.scss      |  59 +-
 .../vuestic-sass/resources/_mixins.scss       |   9 +
 .../vuestic-sass/resources/_variables.scss    |  23 +-
 62 files changed, 1823 insertions(+), 1193 deletions(-)
 create mode 100644 src/components/configSettings/forms/UploadForm.vue
 create mode 100644 src/components/layout/app-navbar/components/dropdowns/SettingsDropdown.vue
 create mode 100644 src/data/Config.js
 create mode 100644 src/data/charts/BubbleChartData.js
 create mode 100644 src/data/charts/DonutChartData.js
 create mode 100644 src/data/charts/HorizontalBarChartData.js
 create mode 100644 src/data/charts/LineChartData.js
 create mode 100644 src/data/charts/PieChartData.js
 create mode 100644 src/data/charts/VerticalBarChartData.js
 rename src/services/{ConfigService => ConfigService.ts} (100%)
 create mode 100644 src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue
 rename src/vuestic-theme/vuestic-components/{vuestic-chart/VuesticChart.vue => va-chart/VaChart.vue} (75%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart/VuesticChartConfigs.js => va-chart/VaChartConfigs.js} (91%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart => va-chart}/chart-types/BubbleChart.js (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart => va-chart}/chart-types/DonutChart.js (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart => va-chart}/chart-types/HorizontalBarChart.js (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart => va-chart}/chart-types/LineChart.js (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart => va-chart}/chart-types/PieChart.js (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart => va-chart}/chart-types/VerticalBarChart.js (100%)
 rename src/vuestic-theme/vuestic-components/{vuestic-chart => va-chart}/chart-types/chartMixin.js (88%)
 create mode 100644 src/vuestic-theme/vuestic-components/va-icon/va-icon.docs.md
 create mode 100644 src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-select/VaSelect.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-select/va-select.docs.md
 delete mode 100644 src/vuestic-theme/vuestic-components/va-tabs/VuesticTabs.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChart.demo.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-chat/VuesticChat.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-grid/Spacing.demo.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-grid/SpacingPlaygroud.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.demo.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect-413.demo.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.demo.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.vue

diff --git a/public/index.html b/public/index.html
index 537703a..80a78d4 100644
--- a/public/index.html
+++ b/public/index.html
@@ -7,6 +7,7 @@
   <meta http-equiv="Content-Security-Policy" content="base-uri 'self'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' https://fonts.googleapis.com/ 'unsafe-inline'; manifest-src 'self'; font-src 'self' data: https://fonts.googleapis.com/ https://fonts.gstatic.com/; script-src 'self' 'unsafe-eval';">
   <% } %>
   <link href="https://fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
   <link rel="icon" href="<%= BASE_URL %>favicon.ico">
 
   <title>EPIC ADMIN FE</title>
diff --git a/src/components/admin/UserActionsDropdown.vue b/src/components/admin/UserActionsDropdown.vue
index d20579b..c6a7fc9 100644
--- a/src/components/admin/UserActionsDropdown.vue
+++ b/src/components/admin/UserActionsDropdown.vue
@@ -18,10 +18,10 @@
       <va-icon
         color="danger"
         @click.native="confirmDeleteAccount = true"
-        icon="ion-ios-trash-outline"
+        name="ion-ios-trash-outline"
         class="user-actions-panel__icon pa-2"
       />
-      <va-dropdown-popper
+      <va-dropdown
         position="left"
         :max-width="250"
         :maxHeight="150"
@@ -45,7 +45,7 @@
             />
           </div>
         </va-card>
-      </va-dropdown-popper>
+      </va-dropdown>
     </div>
     <va-modal
       v-model="confirmDeleteAccount"
diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
new file mode 100644
index 0000000..c3fc2b7
--- /dev/null
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -0,0 +1,23 @@
+<template>
+  <div class="upload">
+    <va-select v-model="value" :options="options"></va-select>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: { },
+})
+export default class UploadForm extends Vue {
+  value = ''
+  options = ['list', 'of', 'options']
+}
+</script>
+
+<style lang="scss">
+.upload {
+
+}
+</style>
diff --git a/src/components/layout/AppLayout.vue b/src/components/layout/AppLayout.vue
index 7d9c929..b920a5c 100644
--- a/src/components/layout/AppLayout.vue
+++ b/src/components/layout/AppLayout.vue
@@ -5,7 +5,7 @@
     <main
       slot="content"
       id="content"
-      class="content va-layout gutter--lg fluid"
+      class="va-layout gutter--lg fluid"
       role="main"
     >
       <va-pre-loader
diff --git a/src/components/layout/app-navbar/AppNavbar.vue b/src/components/layout/app-navbar/AppNavbar.vue
index 9d7fb14..254cd6b 100644
--- a/src/components/layout/app-navbar/AppNavbar.vue
+++ b/src/components/layout/app-navbar/AppNavbar.vue
@@ -5,7 +5,7 @@
       <va-icon-vuestic/>
     </span>
     <div class="va-row flex-center justify--space-between">
-      <va-icon icon="i-nav-search" class="pointer"/>
+      <va-icon name="i-nav-search" class="pointer"/>
       <language-dropdown class="va-navbar__item"/>
       <profile-dropdown class="va-navbar__item">
         <span>{{username}}</span>
diff --git a/src/components/layout/app-navbar/components/dropdowns/LanguageDropdown.vue b/src/components/layout/app-navbar/components/dropdowns/LanguageDropdown.vue
index 4b10b86..357052b 100644
--- a/src/components/layout/app-navbar/components/dropdowns/LanguageDropdown.vue
+++ b/src/components/layout/app-navbar/components/dropdowns/LanguageDropdown.vue
@@ -1,25 +1,22 @@
 <template>
-    <va-dropdown
-      className="language-dropdown"
-      v-model="isShown"
-      position="B"
-      fixed
-    >
-      <va-icon slot="anchor" :icon="['flag-icon flag-icon-large', flagIconClass(currentLanguage())]"/>
-      <div class="py-3 px-2">
-        <div class="language-dropdown__item va-row align--center flex-nowrap"
-           v-for="(option, id) in options"
-           :key="id"
-           :class="{ active: option.code === currentLanguage() }"
-           @click="setLanguage(option.code)"
-        >
-          <va-icon :icon="['flag-icon flag-icon-small', flagIconClass(option.code)]"/>
-          <span class="dropdown-item__text">
-            {{ $t(`language.${option.name}`) }}
-          </span>
-        </div>
+  <va-dropdown
+    class="language-dropdown"
+  >
+    <va-icon slot="anchor" :name="['flag-icon flag-icon-large', flagIconClass(currentLanguage())]"/>
+    <div class="language-dropdown__content py-3 px-2">
+      <div class="language-dropdown__item va-row align--center"
+         v-for="(option, id) in options"
+         :key="id"
+         :class="{ active: option.code === currentLanguage() }"
+         @click="setLanguage(option.code)"
+      >
+        <va-icon :name="['flag-icon flag-icon-small', flagIconClass(option.code)]"/>
+        <span class="dropdown-item__text">
+        {{ $t(`language.${option.name}`) }}
+      </span>
       </div>
-    </va-dropdown>
+    </div>
+  </va-dropdown>
 </template>
 
 <script>
@@ -27,11 +24,6 @@ import Vue from 'vue'
 
 export default {
   name: 'language-dropdown',
-  data () {
-    return {
-      isShown: false,
-    }
-  },
   props: {
     options: {
       type: Array,
@@ -73,25 +65,25 @@ export default {
 
 <style lang="scss">
 @import '../../../../../vuestic-theme/vuestic-sass/resources/resources';
-@import "~flag-icon-css/css/flag-icon.css";
+@import "../../../../../../node_modules/flag-icon-css/css/flag-icon.css";
 
-.flag-icon-large {
-  display: block;
-  width: 31px;
-  height: 23px;
-  cursor: pointer;
-}
 .language-dropdown {
   cursor: pointer;
-  max-width: 9rem !important;
-  .flag-icon-small {
-    min-width: 1.5rem;
-    min-height: 1.5rem;
-    margin-right: .5rem;
+  &__content {
+    background-color: $dropdown-background;
+    box-shadow: $gray-box-shadow;
+    border-radius: .5rem;
+    max-width: 9rem !important;
+    .flag-icon-small {
+      min-width: 1.5rem;
+      min-height: 1.5rem;
+      margin-right: .5rem;
+    }
   }
   &__item {
     padding-bottom: 0.625rem;
     cursor: pointer;
+    flex-wrap: nowrap;
     &:last-of-type {
       padding-bottom: 0 !important;
     }
@@ -99,5 +91,11 @@ export default {
       color: $vue-green;
     }
   }
+  .flag-icon-large {
+    display: block;
+    width: 31px;
+    height: 23px;
+  }
+
 }
 </style>
diff --git a/src/components/layout/app-navbar/components/dropdowns/MessageDropdown.vue b/src/components/layout/app-navbar/components/dropdowns/MessageDropdown.vue
index 4c54d9f..1a6abfd 100644
--- a/src/components/layout/app-navbar/components/dropdowns/MessageDropdown.vue
+++ b/src/components/layout/app-navbar/components/dropdowns/MessageDropdown.vue
@@ -1,23 +1,28 @@
 <template>
-  <va-dropdown v-model="isOpen" position="B" class="message-dropdown py-3 px-2">
+  <va-dropdown
+    class="message-dropdown"
+  >
     <va-icon
-      icon="i-nav-messages"
-      :class="{'unread': !allRead}"
-      slot="actuator"
+      name="i-nav-messages"
+      slot="anchor"
+      class="message-dropdown__icon"
+      :class="{'message-dropdown__icon--unread': !allRead}"
     />
-    <div
-      v-for="option in computedOptions"
-      :key="option.id"
-      class="message-dropdown__item position-relative pr-3 flex-nowrap va-row"
-      :class="{'unread': option.unread}"
-      @click="markAsRead(option.id)"
-    >
-      <img :src="option.details.avatar" class="message-dropdown__item__avatar mr-1"/>
-      <span class="ellipsis">{{ $t(`messages.${option.name}`, { name: option.details.name})}}</span>
-    </div>
-    <div class="va-row justify--space-between">
-      <va-button>{{ $t('messages.all') }}</va-button>
-      <va-button outline @click="markAllAsRead" :disabled="allRead">{{ $t('messages.mark_as_read') }}</va-button>
+    <div class="message-dropdown__content py-3 px-2">
+      <div
+        v-for="option in computedOptions"
+        :key="option.id"
+        class="message-dropdown__item pr-3 va-row"
+        :class="{'message-dropdown__item--unread': option.unread}"
+        @click="option.unread = false"
+      >
+        <img :src="option.details.avatar" class="message-dropdown__item__avatar mr-1"/>
+        <span class="ellipsis">{{ $t(`messages.${option.name}`, { name: option.details.name})}}</span>
+      </div>
+      <div class="va-row justify--space-between">
+        <va-button class="m-0 mr-1" small>{{ $t('messages.all') }}</va-button>
+        <va-button class="m-0" small outline @click="markAllAsRead" :disabled="allRead">{{ $t('messages.mark_as_read') }}</va-button>
+      </div>
     </div>
   </va-dropdown>
 </template>
@@ -27,8 +32,7 @@ export default {
   name: 'message-dropdown',
   data () {
     return {
-      isOpen: false,
-      computedOptions: this.options.map(item => ({ ...item, unread: true })),
+      computedOptions: [...this.options],
     }
   },
   props: {
@@ -38,11 +42,13 @@ export default {
         {
           name: 'new',
           details: { name: 'Oleg M', avatar: 'https://picsum.photos/24?image=1083' },
+          unread: true,
           id: 1,
         },
         {
           name: 'new',
           details: { name: 'Andrei H', avatar: 'https://picsum.photos/24?image=1025' },
+          unread: true,
           id: 2,
         },
       ],
@@ -54,11 +60,6 @@ export default {
     },
   },
   methods: {
-    markAsRead (id) {
-      this.computedOptions = this.computedOptions.map(item => item.id === id
-        ? { ...item, unread: false }
-        : { ...item })
-    },
     markAllAsRead () {
       this.computedOptions = this.computedOptions.map(item => ({ ...item, unread: false }))
     },
@@ -68,30 +69,40 @@ export default {
 
 <style lang="scss">
 @import '../../../../../vuestic-theme/vuestic-sass/resources/resources';
-.i-nav-messages {
-  position: relative;
+
+.message-dropdown {
   cursor: pointer;
-  &.unread::before {
-    content: '';
-    position: absolute;
-    right: 0;
-    left: 0;
-    top: -.5rem;
-    background-color: $brand-danger;
-    height: .375rem;
-    width: .375rem;
-    margin: 0 auto;
-    border-radius: .187rem;
+
+  .message-dropdown__icon {
+    position: relative;
+    display: flex;
+    align-items: center;
+
+    &--unread::before {
+      content: '';
+      position: absolute;
+      right: 0;
+      left: 0;
+      top: -.5rem;
+      background-color: $brand-danger;
+      height: .375rem;
+      width: .375rem;
+      margin: 0 auto;
+      border-radius: .187rem;
+    }
+  }
+  &__content {
+    background-color: $dropdown-background;
+    box-shadow: $gray-box-shadow;
+    border-radius: .5rem;
   }
-}
-.message-dropdown {
-  max-width: 25rem;
-  margin-left: -2rem;
   &__item {
     cursor: pointer;
     margin-bottom: .75rem;
     color: $brand-secondary;
-    &.unread {
+    position: relative;
+    flex-wrap: nowrap;
+    &--unread {
       color: $vue-darkest-blue;
       &:after {
         content: '';
@@ -111,7 +122,7 @@ export default {
     }
     &__avatar {
       border-radius: 50%;
-      width: 1.5rem;
+      min-width: 1.5rem;
       height: 1.5rem;
     }
   }
diff --git a/src/components/layout/app-navbar/components/dropdowns/NotificationDropdown.vue b/src/components/layout/app-navbar/components/dropdowns/NotificationDropdown.vue
index cce413c..aab2071 100644
--- a/src/components/layout/app-navbar/components/dropdowns/NotificationDropdown.vue
+++ b/src/components/layout/app-navbar/components/dropdowns/NotificationDropdown.vue
@@ -1,31 +1,32 @@
 <template>
-  <va-dropdown
-    v-model="isShown"
-    position="B"
-    class="notification-dropdown py-3 px-2"
-  >
-    <va-icon
-      icon="i-nav-notification"
-      :class="{'unread': !allRead}"
-      slot="actuator"
-    />
-    <div
-      v-for="option in computedOptions"
-      :key="option.id"
-      class="notification-dropdown__item position-relative pr-3 flex-nowrap va-row"
-      :class="{'unread': option.unread}"
-      @click="markAsRead(option.id)"
-     >
-      <img v-if="option.details.avatar" class="mr-1 notification-dropdown__item__avatar" :src="option.details.avatar"/>
-      <span class="ellipsis">{{$t(`notifications.${option.name}`,
-        { name: option.details.name, type: option.details.type })}}
-      </span>
-    </div>
-    <div class="va-row justify--space-between">
-      <va-button>{{ $t('notifications.all') }}</va-button>
-      <va-button outline @click="markAllAsRead" :disabled="allRead">{{ $t('notifications.mark_as_read') }}</va-button>
-    </div>
-  </va-dropdown>
+    <va-dropdown
+      class="notification-dropdown"
+    >
+      <va-icon
+        slot="anchor"
+        name="i-nav-notification"
+        class="notification-dropdown__icon"
+        :class="{'notification-dropdown__icon--unread': !allRead}"
+      />
+      <div class="notification-dropdown__content py-3 px-2">
+        <div
+          v-for="option in computedOptions"
+          :key="option.id"
+          class="notification-dropdown__item pr-3 va-row"
+          :class="{'notification-dropdown__item--unread': option.unread}"
+          @click="option.unread = false"
+         >
+          <img v-if="option.details.avatar" class="mr-1 notification-dropdown__item__avatar" :src="option.details.avatar"/>
+          <span class="ellipsis">
+            {{$t(`notifications.${option.name}`, { name: option.details.name, type: option.details.type })}}
+          </span>
+        </div>
+        <div class="va-row justify--space-between">
+          <va-button class="m-0 mr-1" small>{{ $t('notifications.all') }}</va-button>
+          <va-button class="m-0" small outline @click="markAllAsRead" :disabled="allRead">{{ $t('notifications.mark_as_read') }}</va-button>
+        </div>
+      </div>
+    </va-dropdown>
 </template>
 
 <script>
@@ -33,8 +34,7 @@ export default {
   name: 'notification-dropdown',
   data () {
     return {
-      isShown: false,
-      computedOptions: this.options.map(item => ({ ...item, unread: true })),
+      computedOptions: [...this.options],
     }
   },
   props: {
@@ -44,16 +44,19 @@ export default {
         {
           name: 'sentMessage',
           details: { name: 'Vasily S', avatar: 'https://picsum.photos/100' },
+          unread: true,
           id: 1,
         },
         {
           name: 'uploadedZip',
           details: { name: 'Oleg M', avatar: 'https://picsum.photos/100', type: 'typography component' },
+          unread: true,
           id: 2,
         },
         {
           name: 'startedTopic',
           details: { name: 'Andrei H', avatar: 'https://picsum.photos/24' },
+          unread: true,
           id: 3,
         },
       ],
@@ -65,11 +68,6 @@ export default {
     },
   },
   methods: {
-    markAsRead (id) {
-      this.computedOptions = this.computedOptions.map(item => item.id === id
-        ? { ...item, unread: false }
-        : { ...item })
-    },
     markAllAsRead () {
       this.computedOptions = this.computedOptions.map(item => ({ ...item, unread: false }))
     },
@@ -80,32 +78,39 @@ export default {
 <style lang="scss">
 @import '../../../../../vuestic-theme/vuestic-sass/resources/resources';
 
-.i-nav-notification {
-  position: relative;
+.notification-dropdown {
   cursor: pointer;
+  .notification-dropdown__icon {
+    position: relative;
+    display: flex;
+    align-items: center;
 
-  &.unread::before {
-    content: '';
-    position: absolute;
-    right: 0;
-    left: 0;
-    top: -.5rem;
-    background-color: $brand-danger;
-    height: .375rem;
-    width: .375rem;
-    margin: 0 auto;
-    border-radius: .187rem;
+    &--unread::before {
+      content: '';
+      position: absolute;
+      right: 0;
+      left: 0;
+      top: -.5rem;
+      background-color: $brand-danger;
+      height: .375rem;
+      width: .375rem;
+      margin: 0 auto;
+      border-radius: .187rem;
+    }
+  }
+  &__content {
+    background-color: $dropdown-background;
+    box-shadow: $gray-box-shadow;
+    border-radius: .5rem;
+    max-width: 25rem;
   }
-}
-
-.notification-dropdown {
-  max-width: 25rem;
-  margin-left: -2rem;
   &__item {
     cursor: pointer;
     margin-bottom: .75rem;
     color: $brand-secondary;
-    &.unread {
+    flex-wrap: nowrap;
+    position: relative;
+    &--unread {
       color: $vue-darkest-blue;
       &:after {
         content: '';
diff --git a/src/components/layout/app-navbar/components/dropdowns/ProfileDropdown.vue b/src/components/layout/app-navbar/components/dropdowns/ProfileDropdown.vue
index 6268fe1..034bc83 100644
--- a/src/components/layout/app-navbar/components/dropdowns/ProfileDropdown.vue
+++ b/src/components/layout/app-navbar/components/dropdowns/ProfileDropdown.vue
@@ -1,14 +1,14 @@
 <template>
   <va-dropdown
-    v-model="isShown"
-    position="B"
-    className="profile-dropdown pa-3"
+    class="profile-dropdown"
+    @show="toggleVisibility(true)"
+    @hide="toggleVisibility(false)"
   >
     <span class="profile-dropdown__actuator" slot="anchor">
       <slot/>
-      <va-icon class="pa-1" :icon="`fa ${isShown ? 'fa-chevron-up' :'fa-chevron-down'}`"></va-icon>
+      <va-icon class="pa-1" :name="`fa ${isShown ? 'fa-chevron-up' :'fa-chevron-down'}`"/>
     </span>
-    <div class="pa-3">
+    <div class="profile-dropdown__content py-3 px-2">
       <router-link
         v-for="option in options"
         :key="option.name"
@@ -44,6 +44,11 @@ export default {
       ],
     },
   },
+  methods: {
+    toggleVisibility (val) {
+      this.isShown = val
+    },
+  },
 }
 </script>
 
@@ -52,12 +57,19 @@ export default {
   cursor: pointer;
   &__actuator {
     color: $vue-green;
-    cursor: pointer;
-    font-size: 1.3rem;
+  }
+  .va-dropdown-popper__anchor {
+    display: flex;
+    justify-content: flex-end;
+  }
+  &__content {
+    background-color: $dropdown-background;
+    box-shadow: $gray-box-shadow;
+    border-radius: .5rem;
   }
   &__item {
     display: block;
-    color: $text-gray;
+    color: $vue-darkest-blue;
 
     &:hover, &:active {
       color: $vue-green;
diff --git a/src/components/layout/app-navbar/components/dropdowns/SettingsDropdown.vue b/src/components/layout/app-navbar/components/dropdowns/SettingsDropdown.vue
new file mode 100644
index 0000000..bf5f24c
--- /dev/null
+++ b/src/components/layout/app-navbar/components/dropdowns/SettingsDropdown.vue
@@ -0,0 +1,75 @@
+<template>
+  <va-dropdown class="settings-dropdown">
+    <va-icon
+      name="vuestic-iconset vuestic-iconset-settings"
+      color="white"
+      style="font-size: 1.4rem; display: flex;"
+      class="settings-dropdown__icon"
+      slot="anchor"
+    />
+    <div class="settings-dropdown__content py-4 px-4">
+      <div class="title settings-dropdown__content-label">{{$t('dashboard.navigationLayout')}}</div>
+      <va-button-toggle
+        outline
+        v-model="navbarViewProxy"
+        :options="options"
+        class="settings-dropdown__control"
+      />
+    </div>
+  </va-dropdown>
+</template>
+
+<script>
+export default {
+  name: 'settings-dropdown',
+  components: {},
+  props: {
+    navbarView: Boolean,
+  },
+  data () {
+    return {
+      options: [
+        { label: this.$t('dashboard.topBarButton'), value: 'true' },
+        { label: this.$t('dashboard.sideBarButton'), value: 'false' },
+      ],
+    }
+  },
+  computed: {
+    navbarViewProxy: {
+      set (navbarView) {
+        this.$emit('update:navbarView', navbarView === 'true')
+      },
+      get () {
+        return this.navbarView + ''
+      },
+    },
+  },
+}
+</script>
+
+<style lang="scss">
+@import '../../../../../vuestic-theme/vuestic-sass/resources/resources';
+
+.settings-dropdown {
+  cursor: pointer;
+
+  &__icon {
+    position: relative;
+    display: flex;
+    align-items: center;
+  }
+  &__content {
+    background-color: $dropdown-background;
+    box-shadow: $gray-box-shadow;
+    border-radius: .5rem;
+    &-label {
+      margin-bottom: .5rem;
+    }
+  }
+  &__control {
+    .va-button-group {
+      margin: 0;
+    }
+  }
+}
+</style>
diff --git a/src/components/layout/app-sidebar/AppSidebar.vue b/src/components/layout/app-sidebar/AppSidebar.vue
index 15e49c9..2e8ec8d 100644
--- a/src/components/layout/app-sidebar/AppSidebar.vue
+++ b/src/components/layout/app-sidebar/AppSidebar.vue
@@ -3,7 +3,7 @@
     <template slot="menu">
       <li class="sidebar-link" @click="$router.push('/')" >
       <a class="sidebar-link__router-link" :class="{'sidebar-link__router-link--active': $route.path==='/'}">
-        <va-icon icon="sidebar-link__content__icon vuestic-iconset vuestic-iconset-user"/>
+        <va-icon name="sidebar-link__content__icon vuestic-iconset vuestic-iconset-user"/>
         <div class="sidebar-link__content__title">{{ $t('menu.users') }}
         </div>
       </a>
diff --git a/src/components/layout/app-sidebar/components/SidebarLink.vue b/src/components/layout/app-sidebar/components/SidebarLink.vue
index a9b0fde..c5e5659 100644
--- a/src/components/layout/app-sidebar/components/SidebarLink.vue
+++ b/src/components/layout/app-sidebar/components/SidebarLink.vue
@@ -13,7 +13,7 @@
         v-if="icon"
         class="sidebar-link__content__icon"
         :style="iconStyles"
-        :icon="icon"
+        :name="icon"
       />
       <div class="sidebar-link__content__title">
         <slot name="title"/>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 32dda03..ccb0c7d 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -1,19 +1,42 @@
 <template>
-  <va-card class="report-page">
-    ConfigSettings Page
-  </va-card>
+  <div>
+    <va-card class="config-settings-page" title="Settings">
+      <va-tabs v-model="value">
+        <va-tab
+          v-for="item in configKeys"
+          :key="item.key"
+        >
+          {{item.name}}
+        </va-tab>
+      </va-tabs>
+      <div class="config-settings-page__content pa-4">
+        <upload-form v-if="value === 0"/>
+      </div>
+    </va-card>
+  </div>
 </template>
 
 <script lang="ts">
 import { Component, Vue } from 'vue-property-decorator'
 import { FulfillingBouncingCircleSpinner } from 'epic-spinners'
+import { ConfigService } from '../../../services/ConfigService'
+import { configKeys } from '../../../data/Config'
+import UploadForm from '../../configSettings/forms/UploadForm.vue'
 
 @Component({
-  components: { FulfillingBouncingCircleSpinner },
+  components: { UploadForm, FulfillingBouncingCircleSpinner },
 })
 export default class ConfigSettingsPage extends Vue {
+  value:number = 0
+  config: any = null
+  configKeys: Array<object> = configKeys
+  async mounted () {
+    this.config = await ConfigService.listConfigSettings()
+  }
 }
 </script>
 
 <style lang="scss">
+  .config-settings-page {
+  }
 </style>
diff --git a/src/data/Config.js b/src/data/Config.js
new file mode 100644
index 0000000..4f8cea0
--- /dev/null
+++ b/src/data/Config.js
@@ -0,0 +1,22 @@
+export const configKeys = [
+  {
+    key: 'Pleroma.Upload',
+    name: 'Upload',
+  },
+  {
+    key: 'Pleroma.Emails',
+    name: 'Emails'
+  },
+  {
+    key: 'Pleroma.Web',
+    name: 'Web'
+  },
+  {
+    key: 'Pleroma.Captcha',
+    name: 'Captcha'
+  },
+  {
+    key: 'Pleroma.ScheduledActivity',
+    name: 'ScheduledActivity'
+  }
+]
diff --git a/src/data/charts/BubbleChartData.js b/src/data/charts/BubbleChartData.js
new file mode 100644
index 0000000..0653460
--- /dev/null
+++ b/src/data/charts/BubbleChartData.js
@@ -0,0 +1,236 @@
+import { hex2rgb } from '../../services/color-functions'
+
+export const getBubbleChartData = (themes) => ({
+  datasets: [
+    {
+      label: 'USA',
+      backgroundColor: hex2rgb(themes['danger'], 0.9).css,
+      borderColor: 'transparent',
+      data: [
+        {
+          x: 23,
+          y: 25,
+          r: 15,
+        },
+        {
+          x: 40,
+          y: 10,
+          r: 10,
+        },
+        {
+          x: 30,
+          y: 22,
+          r: 30,
+        },
+        {
+          x: 7,
+          y: 43,
+          r: 40,
+        },
+        {
+          x: 23,
+          y: 27,
+          r: 120,
+        },
+        {
+          x: 20,
+          y: 15,
+          r: 11,
+        },
+        {
+          x: 7,
+          y: 10,
+          r: 35,
+        },
+        {
+          x: 10,
+          y: 20,
+          r: 40,
+        },
+      ],
+    },
+    {
+      label: 'Russia',
+      backgroundColor: hex2rgb(themes['primary'], 0.9).css,
+      borderColor: 'transparent',
+      data: [
+        {
+          x: 0,
+          y: 30,
+          r: 15,
+        },
+        {
+          x: 20,
+          y: 20,
+          r: 20,
+        },
+        {
+          x: 15,
+          y: 15,
+          r: 50,
+        },
+        {
+          x: 31,
+          y: 46,
+          r: 30,
+        },
+        {
+          x: 20,
+          y: 14,
+          r: 25,
+        },
+        {
+          x: 34,
+          y: 17,
+          r: 30,
+        },
+        {
+          x: 44,
+          y: 44,
+          r: 10,
+        },
+        {
+          x: 39,
+          y: 25,
+          r: 35,
+        },
+      ],
+    },
+    {
+      label: 'Canada',
+      backgroundColor: hex2rgb(themes['warning'], 0.9).css,
+      borderColor: 'transparent',
+      data: [
+        {
+          x: 10,
+          y: 30,
+          r: 45,
+        },
+        {
+          x: 10,
+          y: 50,
+          r: 20,
+        },
+        {
+          x: 5,
+          y: 5,
+          r: 30,
+        },
+        {
+          x: 40,
+          y: 30,
+          r: 20,
+        },
+        {
+          x: 33,
+          y: 15,
+          r: 18,
+        },
+        {
+          x: 40,
+          y: 20,
+          r: 40,
+        },
+        {
+          x: 33,
+          y: 33,
+          r: 40,
+        },
+      ],
+    },
+    {
+      label: 'Belarus',
+      backgroundColor: hex2rgb(themes['info'], 0.9).css,
+      borderColor: 'transparent',
+      data: [
+        {
+          x: 35,
+          y: 30,
+          r: 45,
+        },
+        {
+          x: 25,
+          y: 40,
+          r: 35,
+        },
+        {
+          x: 5,
+          y: 5,
+          r: 30,
+        },
+        {
+          x: 5,
+          y: 20,
+          r: 40,
+        },
+        {
+          x: 10,
+          y: 40,
+          r: 15,
+        },
+        {
+          x: 3,
+          y: 10,
+          r: 10,
+        },
+        {
+          x: 15,
+          y: 40,
+          r: 40,
+        },
+        {
+          x: 7,
+          y: 15,
+          r: 10,
+        },
+      ],
+    },
+    {
+      label: 'Ukraine',
+      backgroundColor: hex2rgb(themes['success'], 0.9).css,
+      borderColor: 'transparent',
+      data: [
+        {
+          x: 25,
+          y: 10,
+          r: 40,
+        },
+        {
+          x: 17,
+          y: 40,
+          r: 40,
+        },
+        {
+          x: 35,
+          y: 10,
+          r: 20,
+        },
+        {
+          x: 3,
+          y: 40,
+          r: 10,
+        },
+        {
+          x: 40,
+          y: 40,
+          r: 40,
+        },
+        {
+          x: 20,
+          y: 10,
+          r: 10,
+        },
+        {
+          x: 10,
+          y: 27,
+          r: 35,
+        },
+        {
+          x: 7,
+          y: 26,
+          r: 40,
+        },
+      ],
+    },
+  ],
+})
diff --git a/src/data/charts/DonutChartData.js b/src/data/charts/DonutChartData.js
new file mode 100644
index 0000000..e8877d2
--- /dev/null
+++ b/src/data/charts/DonutChartData.js
@@ -0,0 +1,8 @@
+export const getDonutChartData = (themes) => ({
+  labels: ['North America', 'South America', 'Australia'],
+  datasets: [{
+    label: 'Population (millions)',
+    backgroundColor: [themes['danger'], themes['info'], themes['success']],
+    data: [2478, 5267, 734],
+  }],
+})
diff --git a/src/data/charts/HorizontalBarChartData.js b/src/data/charts/HorizontalBarChartData.js
new file mode 100644
index 0000000..19dbe89
--- /dev/null
+++ b/src/data/charts/HorizontalBarChartData.js
@@ -0,0 +1,17 @@
+export const getHorizontalBarChartData = (themes) => ({
+  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  datasets: [
+    {
+      label: 'Vuestic Satisfaction Score',
+      backgroundColor: themes['warning'],
+      borderColor: 'transparent',
+      data: [80, 90, 50, 70, 60, 90, 50, 90, 80, 40, 72, 93],
+    },
+    {
+      label: 'Bulma Satisfaction Score',
+      backgroundColor: themes['danger'],
+      borderColor: 'transparent',
+      data: [20, 30, 20, 40, 50, 40, 15, 60, 30, 20, 42, 53],
+    },
+  ],
+})
diff --git a/src/data/charts/LineChartData.js b/src/data/charts/LineChartData.js
new file mode 100644
index 0000000..7191ed5
--- /dev/null
+++ b/src/data/charts/LineChartData.js
@@ -0,0 +1,43 @@
+import { hex2rgb } from '../../services/color-functions'
+
+const generateValue = () => {
+  return Math.floor(Math.random() * 100)
+}
+
+const generateYLabels = () => {
+  const flip = !!Math.floor(Math.random() * 2)
+  return flip ? ['Debit', 'Credit'] : ['Credit', 'Debit']
+}
+
+const generateArray = (length) => {
+  return Array.from(Array(length), generateValue)
+}
+
+const getSize = () => {
+  const minSize = 4
+  return minSize + Math.floor(Math.random() * 3)
+}
+
+export const getLineChartData = (themes) => {
+  const size = getSize()
+  const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July']
+  const yLabels = generateYLabels()
+
+  return {
+    labels: months.splice(0, size),
+    datasets: [
+      {
+        label: yLabels[0],
+        backgroundColor: hex2rgb(themes['primary'], 0.6).css,
+        borderColor: 'transparent',
+        data: generateArray(size),
+      },
+      {
+        label: yLabels[1],
+        backgroundColor: hex2rgb(themes['info'], 0.6).css,
+        borderColor: 'transparent',
+        data: generateArray(size),
+      },
+    ],
+  }
+}
diff --git a/src/data/charts/PieChartData.js b/src/data/charts/PieChartData.js
new file mode 100644
index 0000000..d012189
--- /dev/null
+++ b/src/data/charts/PieChartData.js
@@ -0,0 +1,8 @@
+export const getPieChartData = (themes) => ({
+  labels: ['Africa', 'Asia', 'Europe'],
+  datasets: [{
+    label: 'Population (millions)',
+    backgroundColor: [themes['primary'], themes['warning'], themes['danger']],
+    data: [2478, 5267, 734],
+  }],
+})
diff --git a/src/data/charts/VerticalBarChartData.js b/src/data/charts/VerticalBarChartData.js
new file mode 100644
index 0000000..778721c
--- /dev/null
+++ b/src/data/charts/VerticalBarChartData.js
@@ -0,0 +1,17 @@
+export const getVerticalBarChartData = (themes) => ({
+  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  datasets: [
+    {
+      label: 'USA',
+      backgroundColor: themes['primary'],
+      borderColor: 'transparent',
+      data: [50, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11],
+    },
+    {
+      label: 'USSR',
+      backgroundColor: themes['info'],
+      borderColor: 'transparent',
+      data: [50, 10, 22, 39, 15, 20, 85, 32, 60, 50, 20, 30],
+    },
+  ],
+})
diff --git a/src/services/ConfigService b/src/services/ConfigService.ts
similarity index 100%
rename from src/services/ConfigService
rename to src/services/ConfigService.ts
diff --git a/src/vuestic-theme/vuestic-components/va-button/VaButton.vue b/src/vuestic-theme/vuestic-components/va-button/VaButton.vue
index 1fa884f..b73cf91 100644
--- a/src/vuestic-theme/vuestic-components/va-button/VaButton.vue
+++ b/src/vuestic-theme/vuestic-components/va-button/VaButton.vue
@@ -26,7 +26,7 @@
         v-if="icon"
         fixed-width
         class="va-button__content__icon va-button__content__icon-left"
-        :icon="icon"
+        :name="icon"
       />
       <div
         v-if="hasTitleData"
@@ -37,7 +37,7 @@
         v-if="iconRight"
         fixed-width
         class="va-button__content__icon va-button__content__icon-right"
-        :icon="iconRight"
+        :name="iconRight"
       />
     </div>
   </component>
diff --git a/src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue b/src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue
new file mode 100644
index 0000000..4e07de2
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue
@@ -0,0 +1,50 @@
+<template>
+  <VbDemo>
+    <VbCard>
+      <va-button @click="refreshData()">
+        refreshData
+      </va-button>
+    </VbCard>
+    <VbCard title="Pie">
+      <va-chart :data="chartData" type="pie"/>
+    </VbCard>
+    <VbCard title="Line">
+      <va-chart :data="chartData" type="line"/>
+    </VbCard>
+    <VbCard title="Bubble">
+      <va-chart :data="chartData" type="bubble"/>
+    </VbCard>
+    <VbCard title="Donut">
+      <va-chart :data="chartData" type="donut"/>
+    </VbCard>
+    <VbCard title="Horizontal-bar">
+      <va-chart :data="chartData" type="horizontal-bar"/>
+    </VbCard>
+    <VbCard title="Vertical-bar">
+      <va-chart :data="chartData" type="vertical-bar"/>
+    </VbCard>
+  </VbDemo>
+</template>
+
+<script>
+import VaButton from '../va-button/VaButton'
+import VaChart from './VaChart.vue'
+import { getLineChartData } from '../../../data/charts/LineChartData'
+
+export default {
+  data () {
+    return {
+      chartData: getLineChartData(this.$themes),
+    }
+  },
+  components: {
+    VaButton,
+    VaChart,
+  },
+  methods: {
+    refreshData () {
+      this.chartData = getLineChartData(this.$themes)
+    },
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChart.vue b/src/vuestic-theme/vuestic-components/va-chart/VaChart.vue
similarity index 75%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChart.vue
rename to src/vuestic-theme/vuestic-components/va-chart/VaChart.vue
index 7991cc2..a9c9398 100644
--- a/src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChart.vue
+++ b/src/vuestic-theme/vuestic-components/va-chart/VaChart.vue
@@ -1,7 +1,7 @@
 <template>
   <component
     ref="chart"
-    class='vuestic-chart'
+    class='va-chart'
     :is="chartComponent"
     :options="options"
     :chart-data="data"
@@ -15,23 +15,16 @@ import DonutChart from './chart-types/DonutChart'
 import HorizontalBarChart from './chart-types/HorizontalBarChart'
 import VerticalBarChart from './chart-types/VerticalBarChart'
 import LineChart from './chart-types/LineChart'
-import { chartTypesMap } from './VuesticChartConfigs'
+import { chartTypesMap } from './VaChartConfigs'
 
 export default {
-  name: 'vuestic-chart',
+  name: 'va-chart',
   props: {
     data: {},
     options: {},
     type: {
       validator (type) {
-        const valid = type in chartTypesMap
-
-        if (!valid) {
-          // eslint-disable-next-line no-console
-          console.warn(`There is no chart of ${type} type`)
-        }
-
-        return valid
+        return type in chartTypesMap
       },
     },
   },
@@ -52,7 +45,7 @@ export default {
 </script>
 
 <style lang='scss'>
-.vuestic-chart {
+.va-chart {
   width: 100%;
   height: 100%;
   display: flex;
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChartConfigs.js b/src/vuestic-theme/vuestic-components/va-chart/VaChartConfigs.js
similarity index 91%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChartConfigs.js
rename to src/vuestic-theme/vuestic-components/va-chart/VaChartConfigs.js
index 17fce37..fa04ed5 100644
--- a/src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChartConfigs.js
+++ b/src/vuestic-theme/vuestic-components/va-chart/VaChartConfigs.js
@@ -25,5 +25,3 @@ export const chartTypesMap = {
   'horizontal-bar': 'horizontal-bar-chart',
   'vertical-bar': 'vertical-bar-chart',
 }
-
-export const chartTypes = Object.keys(chartTypesMap)
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/BubbleChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/BubbleChart.js
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/BubbleChart.js
rename to src/vuestic-theme/vuestic-components/va-chart/chart-types/BubbleChart.js
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/DonutChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/DonutChart.js
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/DonutChart.js
rename to src/vuestic-theme/vuestic-components/va-chart/chart-types/DonutChart.js
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/HorizontalBarChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/HorizontalBarChart.js
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/HorizontalBarChart.js
rename to src/vuestic-theme/vuestic-components/va-chart/chart-types/HorizontalBarChart.js
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/LineChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/LineChart.js
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/LineChart.js
rename to src/vuestic-theme/vuestic-components/va-chart/chart-types/LineChart.js
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/PieChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/PieChart.js
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/PieChart.js
rename to src/vuestic-theme/vuestic-components/va-chart/chart-types/PieChart.js
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/VerticalBarChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/VerticalBarChart.js
similarity index 100%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/VerticalBarChart.js
rename to src/vuestic-theme/vuestic-components/va-chart/chart-types/VerticalBarChart.js
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/chartMixin.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/chartMixin.js
similarity index 88%
rename from src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/chartMixin.js
rename to src/vuestic-theme/vuestic-components/va-chart/chart-types/chartMixin.js
index 10f8b30..6dd4050 100644
--- a/src/vuestic-theme/vuestic-components/vuestic-chart/chart-types/chartMixin.js
+++ b/src/vuestic-theme/vuestic-components/va-chart/chart-types/chartMixin.js
@@ -1,5 +1,5 @@
 import { mixins } from 'vue-chartjs'
-import { defaultConfig } from '../VuesticChartConfigs'
+import { defaultConfig } from '../VaChartConfigs'
 
 export const chartMixin = {
   mixins: [mixins.reactiveProp],
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
index 030d041..a26a3cc 100644
--- a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
@@ -22,7 +22,7 @@
           @keypress.prevent="toggleSelection()"
           :disabled="disabled"
         />
-        <va-icon icon="ion ion-md-checkmark va-checkbox__icon-selected"/>
+        <va-icon name="ion ion-md-checkmark va-checkbox__icon-selected"/>
       </div>
       <div class="va-checkbox__label-text">
         <slot name="label">
diff --git a/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.demo.vue b/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.demo.vue
index 588335e..007109a 100644
--- a/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.demo.vue
@@ -110,6 +110,15 @@
       </template>
     </VbCard>
 
+    <VbCard title="Anchor width">
+      <va-dropdown keepAnchorWidth>
+        <button slot="anchor">
+          ------- Anchor ------
+        </button>
+        Same width as anchor
+      </va-dropdown>
+    </VbCard>
+
     <VbCard title="Disabled">
       <va-dropdown disabled>
         <button slot="anchor">
diff --git a/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.vue b/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.vue
index 0b504d9..7f3a8ad 100644
--- a/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.vue
+++ b/src/vuestic-theme/vuestic-components/va-dropdown/VaDropdown.vue
@@ -1,7 +1,7 @@
 <template>
-  <div class="va-dropdown-popper">
+  <div class="va-dropdown">
     <div
-      class="va-dropdown-popper__anchor"
+      class="va-dropdown__anchor"
       @mouseover="onMouseOver()"
       @mouseout="onMouseOut()"
       @click="onAnchorClick()"
@@ -10,14 +10,21 @@
       <slot name="anchor"/>
     </div>
     <div
-      class="va-dropdown-popper__content"
+      class="va-dropdown__content"
       v-if="showContent"
       @mouseover="isContentHoverable && onMouseOver()"
       @mouseout="onMouseOut()"
       ref="content"
-      :style="contentStyle"
     >
-      <slot/>
+      <div
+        v-if="keepAnchorWidth"
+        ref="anchorWidthContainer"
+        class="va-dropdown__anchor-width-container"
+        :style="anchorWidthContainerStyles"
+      >
+        <slot/>
+      </div>
+      <slot v-else/>
     </div>
   </div>
 </template>
@@ -32,6 +39,7 @@ export default {
     return {
       popperInstance: null,
       isClicked: false,
+      anchorWidth: undefined,
 
       isMouseHovered: false,
       hoverOverDebounceLoader: new DebounceLoader(
@@ -55,17 +63,14 @@ export default {
     this.unregisterClickOutsideListener()
     this.removePopper()
   },
+  mounted () {
+    this.handlePopperInstance()
+  },
   watch: {
     showContent: {
       immediate: true,
       handler (showContent) {
-        if (showContent && !this.popperInstance) {
-          this.$nextTick(() => {
-            this.initPopper()
-          })
-          return
-        }
-        this.removePopper()
+        this.handlePopperInstance()
       },
     },
   },
@@ -76,6 +81,7 @@ export default {
     offset: [String, Number],
     disabled: Boolean,
     fixed: Boolean,
+    keepAnchorWidth: Boolean, // Means dropdown width should be the same as anchor's width.
     closeOnClickOutside: {
       type: Boolean,
       default: true,
@@ -102,6 +108,22 @@ export default {
     },
   },
   methods: {
+    handlePopperInstance () {
+      if (this.popperInstance) {
+        this.removePopper()
+      }
+
+      if (!this.showContent) {
+        return
+      }
+
+      this.updateAnchorWidth()
+
+      // I'm not entirely sure why $nextTick is needed here.
+      this.$nextTick(() => {
+        this.initPopper()
+      })
+    },
     onAnchorClick () {
       this.$emit('anchorClick')
       if (this.disabled) {
@@ -160,6 +182,11 @@ export default {
       }
       this.hide()
     },
+    updateAnchorWidth () {
+      if (this.keepAnchorWidth) {
+        this.anchorWidth = this.$refs.anchor.offsetWidth
+      }
+    },
     // @public
     hide () {
       if (this.trigger === 'click') {
@@ -174,6 +201,9 @@ export default {
         arrow: {
           enabled: false,
         },
+        onUpdate: (data) => {
+          this.updateAnchorWidth()
+        },
       }
 
       if (this.offset) {
@@ -198,11 +228,19 @@ export default {
       this.popperInstance.destroy()
       this.popperInstance = null
     },
+    updatePopper () {
+      // used by select
+      if (!this.popperInstance) {
+        return
+      }
+      this.popperInstance.update()
+    },
   },
   computed: {
-    contentStyle () {
+    anchorWidthContainerStyles () {
       return {
-        padding: this.offset,
+        width: this.anchorWidth + 'px',
+        maxWidth: this.anchorWidth + 'px',
       }
     },
     showContent () {
@@ -214,7 +252,7 @@ export default {
       }
       if (this.trigger === 'none') {
         return this.value
-      } else return this.value
+      }
     },
   },
 }
@@ -223,7 +261,7 @@ export default {
 <style lang="scss">
 @import '../../vuestic-sass/resources/resources';
 
-.va-dropdown-popper {
+.va-dropdown {
   &__content {
     z-index: 100;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-dropdown/dropdown-popover-docs.md b/src/vuestic-theme/vuestic-components/va-dropdown/dropdown-popover-docs.md
index 255f1a5..4571d4e 100644
--- a/src/vuestic-theme/vuestic-components/va-dropdown/dropdown-popover-docs.md
+++ b/src/vuestic-theme/vuestic-components/va-dropdown/dropdown-popover-docs.md
@@ -27,19 +27,20 @@ For `hover` dropdown is shown only on hover, click does nothing.
 For `click` dropdown is shown on click and we handle click outside.
 For `none` no external handling is done. Instead we provide a prop `value` and a some events: `click`, `clickOutside`.
 
-* `isContentHoverable`: Boolean - default: `true`. Setting to false is useful for tooltips, where hanging dropdown become an obstacle.
-* `position`: String - default: 'bottom'. Appropriate values are 'top', 'bottom', 'left', 'right', 'auto', 'top-start', 'top-end' (and same for each direction). See [popper.js docs](https://popper.js.org/popper-documentation.html#Popper.placements) for details.
-* `fixed`: Boolean - default: `false`. Fixed dropdown works fine even if container is `position: relative; overflow: hidden`.
-* `disabled`: Boolean - default: `false`. Clicks and hovers won't do a thing.
-* `offset`: String - See [popper.js docs](https://popper.js.org/popper-documentation.html#modifiers..offset) for details.
-* value: Boolean - Used for passing down open/close state when trigger is 'none'.
+* `isContentHoverable` - Boolean - default: `true`. Setting to false is useful for tooltips, where hanging dropdown become an obstacle.
+* `position` - String - default: 'bottom'. Appropriate values are 'top', 'bottom', 'left', 'right', 'auto', 'top-start', 'top-end' (and same for each direction). See [popper.js docs](https://popper.js.org/popper-documentation.html#Popper.placements) for details.
+* `fixed` - Boolean - default: `false`. Fixed dropdown works fine even if container is `position: relative; overflow: hidden`.
+* `disabled` - Boolean - default: `false`. Clicks and hovers won't do a thing.
+* `offset` - String - See [popper.js docs](https://popper.js.org/popper-documentation.html#modifiers..offset) for details.
+* `value` - Boolean - default: `false`. Used for passing down open/close state when trigger is 'none'.
 
 #### Props (advanced)
 
-* `closeOnClickOutside`: Boolean - default: `true`. If set to `false`, click outside of dropdown won't close it. Useful for complex forms.
-* `closeOnAnchorClick`: Boolean - default: `true`. If set to `false`, click dropdown won't close on anchor click.
-* `hoverOverTimeout`: Number - default: 30. Hover dropdown will open after hovering for at least that value. Useful when you have a list with dropdowns and do not want to open every each of them on hover.
-* `hoverOutTimeout`: Number - default: 200. Hover dropdown will close after that timing. Allows to move cursor to content even with gaps in-between.
+* `closeOnClickOutside` - Boolean - default: `true`. If set to `false`, click outside of dropdown won't close it. Useful for complex forms.
+* `closeOnAnchorClick` - Boolean - default: `true`. If set to `false`, click dropdown won't close on anchor click.
+* `hoverOverTimeout` - Number - default: 30. Hover dropdown will open after hovering for at least that value. Useful when you have a list with dropdowns and do not want to open every each of them on hover.
+* `hoverOutTimeout` - Number - default: 200. Hover dropdown will close after that timing. Allows to move cursor to content even with gaps in-between.
+* `keepAnchorWidth` - Boolean - default: `false`. Dropdown should have the same width as anchor. Useful for selects.
 
 ### Additional things
 
diff --git a/src/vuestic-theme/vuestic-components/va-icon/VaIcon.demo.vue b/src/vuestic-theme/vuestic-components/va-icon/VaIcon.demo.vue
index 92710f0..d35eef5 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/VaIcon.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/VaIcon.demo.vue
@@ -1,79 +1,87 @@
 <template>
   <VbDemo>
-    <VbContainer title="Default">
-      <va-icon :icon="icon"/>
-    </VbContainer>
+    <VbCard title="Default">
+      <va-icon :name="icon"/>
+    </VbCard>
 
-    <VbContainer title="Size in px">
-      <va-icon :icon="icon" size="40px"/>
-    </VbContainer>
+    <VbCard title="Size in px">
+      <va-icon :name="icon" size="40px"/>
+    </VbCard>
 
-    <VbContainer title="Size as number">
-      <va-icon :icon="icon" :size="60"/>
-    </VbContainer>
+    <VbCard title="Size as number">
+      <va-icon :name="icon" :size="60"/>
+    </VbCard>
 
-    <VbContainer title="Size presets">
+    <VbCard title="Size presets">
       <div style="font-size: 24px">
-        <va-icon :icon="icon" small/>
-        <va-icon :icon="icon"/>
-        <va-icon :icon="icon" large/>
+        <va-icon :name="icon" small/>
+        <va-icon :name="icon"/>
+        <va-icon :name="icon" large/>
       </div>
-    </VbContainer>
+    </VbCard>
 
-    <VbContainer title="Themes">
-      <va-icon :icon="icon" color="info"/>
-      <va-icon :icon="icon" color="warning"/>
-      <va-icon :icon="icon" color="danger"/>
-      <va-icon :icon="icon" color="success"/>
-      <va-icon :icon="icon" color="gray"/>
-      <va-icon :icon="icon" color="dark"/>
-    </VbContainer>
+    <VbCard title="Themes">
+      <va-icon :name="icon" color="info"/>
+      <va-icon :name="icon" color="warning"/>
+      <va-icon :name="icon" color="danger"/>
+      <va-icon :name="icon" color="success"/>
+      <va-icon :name="icon" color="gray"/>
+      <va-icon :name="icon" color="dark"/>
+    </VbCard>
 
-    <VbContainer title="Rotation">
-      <va-icon :icon="icon" :rotation="45"/>&nbsp;
-      <va-icon :icon="icon" :rotation="180"/>&nbsp;
-      <va-icon :icon="icon" :rotation="270"/>&nbsp;
-    </VbContainer>
+    <VbCard title="Rotation">
+      <va-icon :name="icon" :rotation="45"/>&nbsp;
+      <va-icon :name="icon" :rotation="180"/>&nbsp;
+      <va-icon :name="icon" :rotation="270"/>&nbsp;
+    </VbCard>
 
-    <VbContainer title="Fixed width">
-      <va-button :icon="icon" :icon-right="icon">
+    <VbCard title="Fixed width">
+      <va-button :name="icon" :icon-right="icon">
         Some
       </va-button>
-    </VbContainer>
+    </VbCard>
 
-    <VbContainer title="Iconic">
-      <va-icon icon="iconicstroke iconicstroke-hash"/>
-      <va-icon icon="iconicstroke iconicstroke-at"/>
-    </VbContainer>
-    <VbContainer title="Glyphicon">
-      <va-icon icon="glyphicon glyphicon-star"/>
-      <va-icon icon="glyphicon glyphicon-glass"/>
-    </VbContainer>
-    <VbContainer title="Maki">
-      <va-icon icon="maki maki-belowground-rail"/>
-      <va-icon icon="maki maki-bicycle"/>
-    </VbContainer>
-    <VbContainer title="Entypo">
-      <va-icon icon="entypo entypo-note"/>
-      <va-icon icon="entypo entypo-star"/>
-    </VbContainer>
-    <VbContainer title="Brandico">
-      <va-icon icon="brandico brandico-facebook"/>
-      <va-icon icon="brandico brandico-twitter"/>
-    </VbContainer>
-    <VbContainer title="Font Awesome">
-      <va-icon icon="fa fa-anchor"/>
-      <va-icon icon="fa fa-area-chart"/>
-    </VbContainer>
+    <VbCard title="Iconic">
+      <va-icon name="iconicstroke iconicstroke-hash"/>
+      <va-icon name="iconicstroke iconicstroke-at"/>
+    </VbCard>
+    <VbCard title="Glyphicon">
+      <va-icon name="glyphicon glyphicon-star"/>
+      <va-icon name="glyphicon glyphicon-glass"/>
+    </VbCard>
+    <VbCard title="Maki">
+      <va-icon name="maki maki-belowground-rail"/>
+      <va-icon name="maki maki-bicycle"/>
+    </VbCard>
+    <VbCard title="Entypo">
+      <va-icon name="entypo entypo-note"/>
+      <va-icon name="entypo entypo-star"/>
+    </VbCard>
+    <VbCard title="Brandico">
+      <va-icon name="brandico brandico-facebook"/>
+      <va-icon name="brandico brandico-twitter"/>
+    </VbCard>
+    <VbCard title="Font Awesome">
+      <va-icon name="fa fa-anchor"/>
+      <va-icon name="fa fa-area-chart"/>
+    </VbCard>
+    <VbCard title="Material design icons">
+      <va-icon name="material-icons">face</va-icon>
+      <va-icon name="material-icons">&#xE87C;</va-icon>
+      <va-icon name="material-icons" color="danger">face</va-icon>
+    </VbCard>
   </VbDemo>
 </template>
 
 <script>
 
 import VaIcon from './VaIcon'
+import VaButton from '../va-button/VaButton'
+
 export default {
   components: {
-    VaIcon
+    VaButton,
+    VaIcon,
   },
   data () {
     return {
diff --git a/src/vuestic-theme/vuestic-components/va-icon/VaIcon.vue b/src/vuestic-theme/vuestic-components/va-icon/VaIcon.vue
index 1818b2c..e0b7987 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/VaIcon.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/VaIcon.vue
@@ -1,36 +1,34 @@
 <template>
   <i class="va-icon"
-    :class="[icon, iconClass]"
-    :style="iconStyle"
-  />
+     :class="[name, iconClass]"
+     :style="iconStyle"
+  ><slot/></i>
 </template>
 
 <script>
 export default {
   name: 'va-icon',
   props: {
-    icon: {
-      type: [String, Array]
+    name: {
+      type: [String, Array],
     },
     small: {
       type: Boolean,
-      default: false
     },
     large: {
       type: Boolean,
-      default: false
     },
     size: {
       type: [String, Number],
     },
     fixedWidth: {
-      type: Boolean
+      type: Boolean,
     },
     rotation: {
-      type: [String, Number]
+      type: [String, Number],
     },
     color: {
-      type: String
+      type: String,
     },
   },
   computed: {
@@ -39,21 +37,16 @@ export default {
         'va-icon--large': this.large,
         'va-icon--small': this.small,
         'va-icon--fixed': this.fixedWidth,
-        'va-icon--success': this.color === 'success',
-        'va-icon--info': this.color === 'info',
-        'va-icon--danger': this.color === 'danger',
-        'va-icon--warning': this.color === 'warning',
-        'va-icon--gray': this.color === 'gray',
-        'va-icon--dark': this.color === 'dark',
       }
     },
     iconStyle () {
       return {
         transform: 'rotate(' + this.rotation + 'deg)',
-        fontSize: typeof this.size === 'number' ? this.size + 'px' : this.size
+        fontSize: typeof this.size === 'number' ? this.size + 'px' : this.size,
+        color: this.$themes[this.color] || this.color,
       }
-    }
-  }
+    },
+  },
 }
 </script>
 
diff --git a/src/vuestic-theme/vuestic-components/va-icon/va-icon.docs.md b/src/vuestic-theme/vuestic-components/va-icon/va-icon.docs.md
new file mode 100644
index 0000000..fd31d64
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-icon/va-icon.docs.md
@@ -0,0 +1,20 @@
+```html
+  <va-icon :name="iconicstroke iconicstroke-info"/>
+
+  <va-icon
+    :name="fa fa-anchor"
+    color="info"
+    rotation="45"
+    size="60"
+  />
+```
+
+***Props***
+* name - `String, Array` - The name of icon set and icon (will be used as class). We support Vuestic, Iconic, Glyphicon, Maki, Entypo, Brandico, Font Awesome, Material design icons. 
+* small - `Boolean` - If `small` prop is `true`, icon size will be 1rem (16px by default)
+* large - `Boolean` - If `large` prop is `true`, icon size will be 2.25rem
+* size - `String, Number` - Sets the custom size of icon in pixels
+* fixedWidth - `Boolean` - If `fixedWidth` prop is `true`, width of the icon will be 1.25rem
+* rotation - `String, Number` - Sets the degree of icons rotation.
+* color - `String` - Sets the color of icon
+
diff --git a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconCleanCode.vue b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconCleanCode.vue
index 36eb4fd..3c05b38 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconCleanCode.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconCleanCode.vue
@@ -31,9 +31,11 @@ export default {
   display: inline-block;
   width: 56px;
   height: 50px;
+
   .cls-1 {
     fill: #4ae387;
   }
+
   .cls-2 {
     fill: #34495e;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFree.vue b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFree.vue
index 28479bc..dabe16e 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFree.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFree.vue
@@ -28,15 +28,18 @@ export default {
   display: inline-block;
   width: 55px;
   height: 47.8px;
+
   .cls-1 {
     fill: #4ae387;
   }
+
   .cls-2 {
     fill: none;
     stroke: #34495e;
     stroke-miterlimit: 10;
     stroke-width: 3px;
   }
+
   .cls-3 {
     fill: #34495e;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFresh.vue b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFresh.vue
index e11d9b1..3ba0e6e 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFresh.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconFresh.vue
@@ -25,9 +25,11 @@ export default {
   display: inline-block;
   width: 51px;
   height: 48px;
+
   .cls-1 {
     fill: #4ae387;
   }
+
   .cls-2 {
     fill: #34495e;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconResponsive.vue b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconResponsive.vue
index b6eb63a..7eb0792 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconResponsive.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconResponsive.vue
@@ -26,9 +26,11 @@ export default {
   display: inline-block;
   width: 47.5px;
   height: 49px;
+
   .cls-1 {
     fill: #4ae387;
   }
+
   .cls-2 {
     fill: #34495e;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconRich.vue b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconRich.vue
index b43193f..624a498 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconRich.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconRich.vue
@@ -30,9 +30,11 @@ export default {
   display: inline-block;
   width: 57px;
   height: 55px;
+
   .cls-1 {
     fill: #4ae387;
   }
+
   .cls-2 {
     fill: #34495e;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconVue.vue b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconVue.vue
index 6c7b655..6930fee 100644
--- a/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconVue.vue
+++ b/src/vuestic-theme/vuestic-components/va-icon/va-iconset/VaIconVue.vue
@@ -25,9 +25,11 @@ export default {
   display: inline-block;
   width: 55px;
   height: 47.8px;
+
   .cls-1 {
     fill: #4ae387;
   }
+
   .cls-2 {
     fill: #34495e;
   }
diff --git a/src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue b/src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue
new file mode 100644
index 0000000..08f673f
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue
@@ -0,0 +1,287 @@
+<template>
+  <VbDemo>
+    <VbCard title="String options (default)" style="width: 400px;">
+      <va-select
+        :options="defaultSelect.options"
+        v-model="defaultSelect.value"
+      />
+    </VbCard>
+    <VbCard title="error" style="width: 400px;">
+      <va-select
+        :options="defaultSelect.options"
+        v-model="defaultSelect.value"
+        error
+      />
+      <va-select
+        :options="defaultSelect.options"
+        v-model="defaultSelect.value"
+        success
+      />
+      <va-input-wrapper
+        error
+        :errorMessages="['error message']"
+      >
+        <va-select
+          :options="defaultSelect.options"
+          v-model="defaultSelect.value"
+          error
+        />
+      </va-input-wrapper>
+    </VbCard>
+    <VbCard title="Object options" style="width: 400px;">
+      <va-select
+        label="Object value"
+        v-model="objectSelect.value"
+        :options="objectSelect.options"
+      />
+      <p>key-by='value'</p>
+      <va-select
+        v-model="iconsSelect.value"
+        :options="iconsSelect.options"
+        key-by="value"
+      />
+      <p>textBy='icon'</p>
+      <va-select
+        text-by="icon"
+        v-model="iconsSelect.value"
+        :options="iconsSelect.options"
+      />
+      <p>key-by='value' (multiple)</p>
+      <va-select
+        key-by="value"
+        v-model="multipleValue"
+        :options="iconsSelect.options"
+        multiple
+      />
+    </VbCard>
+    <VbCard title="Options with icons" style="width: 400px;">
+      <va-select
+        v-model="iconsSelect.value"
+        :options="iconsSelect.options"
+      />
+    </VbCard>
+    <VbCard title="No options" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="[]"
+      />
+      <va-select
+        label="custom no options text"
+        v-model="defaultSelect.value"
+        :options="[]"
+        no-options-text="Sorry..."
+      />
+    </VbCard>
+    <VbCard title="Custom clear value" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        clear-value="1"
+        :options="defaultSelect.options"
+      />
+    </VbCard>
+    <VbCard title="Placeholder" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        placeholder="select country"
+      />
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,"
+      />
+    </VbCard>
+    <VbCard title="Label" style="width: 400px;">
+      <va-select
+        label="country label"
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+      />
+    </VbCard>
+    <VbCard title="Label and placeholder" style="width: 400px;">
+      <va-select
+        label="country label"
+        placeholder="select country"
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+      />
+    </VbCard>
+    <VbCard title="positions" style="width: 400px;">
+      <div v-for="position in positions" :key="position">
+        <p>{{position}}</p>
+        <va-select
+          :position="position"
+          v-model="defaultSelect.value"
+          :options="CountriesList"
+        />
+      </div>
+    </VbCard>
+    <VbCard title="disabled" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        disabled
+      />
+    </VbCard>
+    <VbCard title="multiple" style="width: 400px;">
+      <va-select
+        v-model="multipleValue"
+        multiple
+        :options="CountriesList"
+      />
+      <va-select
+        label="with custom max"
+        v-model="multipleValue"
+        multiple
+        :tagMax="8"
+        :options="CountriesList"
+      />
+    </VbCard>
+    <VbCard title="searchable" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        searchable
+      />
+    </VbCard>
+    <VbCard title="searchable + multiple" style="width: 400px;">
+      <va-select
+        v-model="multipleValue"
+        :options="CountriesList"
+        searchable
+        multiple
+      />
+    </VbCard>
+    <VbCard title="custom max-height (320px)" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        max-height="320px"
+      />
+    </VbCard>
+    <VbCard title="custom width (30%)" :style="{'width': '100%'}" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        width="30%"
+      />
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        width="120px"
+      />
+    </VbCard>
+    <VbCard title="loading" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        loading
+      />
+    </VbCard>
+    <VbCard title="with ajax" style="width: 400px;">
+      <va-select
+        searchable
+        v-model="defaultSelect.value"
+        :options="CountriesList"
+        :loading="isLoading"
+        @update-search="updateSearch"
+      />
+    </VbCard>
+    <VbCard title="long textes" style="width: 400px">
+      <va-select
+        searchable
+        placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,"
+        label="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,"
+        v-model="longSelect.value"
+        :options="longSelect.options"
+      />
+    </VbCard>
+    <VbCard :style="{ 'width': '100%' }">
+      <p>{{defaultSelect.value}}</p>
+      <p>{{objectSelect.value}}</p>
+      <p>{{iconsSelect.value}}</p>
+      <p>{{multipleValue}}</p>
+    </VbCard>
+  </VbDemo>
+</template>
+
+<script>
+
+import CountriesList from '../../../data/CountriesList'
+import VaSelect from './VaSelect'
+import VaInputWrapper from '../va-input/VaInputWrapper'
+
+const positions = ['top', 'bottom']
+
+export default {
+  components: { VaInputWrapper, VaSelect },
+  data () {
+    const objectSelectOptions = [{ id: 1, text: 'one' }, { id: 2, text: 'two' }, { id: 3, text: 'three' }]
+    const iconsSelectOptions = [
+      {
+        text: 'item1',
+        id: 0,
+        value: 0,
+        icon: 'fa fa-address-book',
+      },
+      {
+        text: 'item2',
+        id: 1,
+        value: 1,
+        icon: 'fa fa-android',
+      },
+      {
+        text: 'item2',
+        id: 2,
+        value: 2,
+        icon: 'fa fa-android',
+      },
+      {
+        text: 'item2',
+        id: 3,
+        value: 3,
+      },
+      {
+        text: 'item2',
+        id: 4,
+        value: 4,
+        icon: 'fa fa-android',
+      },
+    ]
+    return {
+      defaultSelect: {
+        options: ['one', 'two', 'three'],
+        value: 'one',
+      },
+      objectSelect: {
+        value: '',
+        options: objectSelectOptions,
+      },
+      iconsSelect: {
+        value: '',
+        options: iconsSelectOptions,
+      },
+      longSelect: {
+        value: '1st long long long long option sit amet, consectetur adipiscing elit,',
+        options: [
+          '1st long long long long option sit amet, consectetur adipiscing elit,',
+          '2nd long  sit amet, consectetur adipiscing elit, long long long long long option',
+        ],
+      },
+      multipleValue: [],
+      CountriesList,
+      positions,
+      isLoading: false,
+    }
+  },
+  methods: {
+    updateSearch (val) {
+      this.isLoading = true
+      setTimeout(() => {
+        this.isLoading = false
+        this.CountriesList = this.CountriesList.slice(0, Math.round(this.CountriesList.length / 2))
+      }, 2000)
+    },
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-select/VaSelect.vue b/src/vuestic-theme/vuestic-components/va-select/VaSelect.vue
new file mode 100644
index 0000000..6d6abf1
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-select/VaSelect.vue
@@ -0,0 +1,519 @@
+<template>
+  <va-dropdown
+    :position="position"
+    :disabled="disabled"
+    class="va-select__dropdown"
+    :max-height="maxHeight"
+    keepAnchorWidth
+    ref="dropdown"
+    :fixed="fixed"
+    :style="{width}"
+    :closeOnAnchorClick="false"
+  >
+    <ul
+      class="va-select__option-list"
+      :style="optionsListStyle"
+    >
+      <li
+        v-for="option in filteredOptions"
+        :key="getKey(option)"
+        :class="getOptionClass(option)"
+        :style="getOptionStyle(option)"
+        @click.stop="selectOption(option)"
+        @mouseleave="updateHoveredOption(null)"
+        @mouseover="updateHoveredOption(option)"
+      >
+        <va-icon v-show="option.icon" :name="option.icon" class="va-select__option__icon"/>
+        <span>{{getText(option)}}</span>
+        <va-icon
+          v-show="isSelected(option)"
+          class="va-select__option__selected-icon"
+          name="material-icons">
+          done
+        </va-icon>
+      </li>
+    </ul>
+    <div
+      class="va-select__option-list no-options"
+      :style="optionsListStyle"
+      v-if="!filteredOptions.length"
+    >
+      {{noOptionsText}}
+    </div>
+
+    <div
+      slot="anchor"
+      :class="selectClass"
+      :style="selectStyle"
+    >
+      <label
+        class="va-select__label"
+        aria-hidden="true"
+      >{{label}}</label>
+      <div
+        class="va-select__input-wrapper"
+        :style="inputWrapperStyles"
+      >
+        <span
+          class="va-select__tags"
+          v-if="multiple && valueProxy.length <= tagMax"
+        >
+          <va-chip
+            v-for="option in valueProxy"
+            :key="getKey(option)"
+            small
+          >
+            {{getText(option)}}
+          </va-chip>
+        </span>
+        <span v-else-if="displayedText" class="va-select__displayed-text">{{displayedText}}</span>
+        <span v-else class="va-select__placeholder">{{placeholder}}</span>
+        <input
+          v-if="searchable"
+          :placeholder="placeholder"
+          :value="search"
+          class="va-select__input"
+          @input="updateSearch($event.target.value)"
+          ref="search"
+          :style="inputStyles"
+        />
+      </div>
+      <va-icon
+        v-if="showClearIcon"
+        class="va-select__clear-icon mr-1"
+        name="fa fa-times-circle"
+        @click.native.stop="clear()"
+      />
+      <spring-spinner
+        :color="$themes.success"
+        v-if="loading"
+        :size="24"
+        class="va-select__loading"
+      />
+      <va-icon
+        class="va-select__open-icon"
+        :name="visible ? 'fa fa-chevron-up' : 'fa fa-chevron-down'"
+      />
+    </div>
+  </va-dropdown>
+</template>
+
+<script>
+import VaDropdown from '../va-dropdown/VaDropdown'
+import VaChip from '../va-chip/VaChip'
+import { SpringSpinner } from 'epic-spinners'
+import VaIcon from '../va-icon/VaIcon'
+import { getHoverColor } from '../../../services/color-functions'
+
+const positions = {
+  'top': 'T',
+  'bottom': 'B',
+}
+export default {
+  name: 'va-select',
+  components: { VaIcon, SpringSpinner, VaDropdown, VaChip },
+  data () {
+    return {
+      search: '',
+      mounted: false,
+      hoveredOption: null,
+    }
+  },
+  props: {
+    value: {},
+    label: String,
+    placeholder: String,
+    options: {
+      type: Array,
+      default: () => [],
+    },
+    position: {
+      type: String,
+      default: 'bottom',
+      validator: position => Object.keys(positions).includes(position),
+    },
+    tagMax: {
+      type: Number,
+      default: 5,
+    },
+    searchable: Boolean,
+    multiple: Boolean,
+    disabled: Boolean,
+    readonly: Boolean,
+    loading: Boolean,
+    width: {
+      type: String,
+      default: '100%',
+    },
+    maxHeight: {
+      type: String,
+      default: '128px',
+    },
+    keyBy: {
+      type: String,
+      default: 'id',
+    },
+    textBy: {
+      type: String,
+      default: 'text',
+    },
+    clearValue: {
+      default: '',
+    },
+    noOptionsText: {
+      type: String,
+      default: 'Items not found',
+    },
+    fixed: {
+      type: Boolean,
+      default: true,
+    },
+    error: Boolean,
+    success: Boolean,
+  },
+  watch: {
+    search (val) {
+      this.$emit('update-search', val)
+    },
+    visible (val) {
+      if (val && this.searchable) {
+        this.$refs.search.focus()
+      }
+    },
+  },
+  computed: {
+    visible () {
+      return this.mounted ? this.$refs.dropdown.isClicked : false
+    },
+    selectClass () {
+      return {
+        'va-select': true,
+        'va-select--multiple': this.multiple,
+        'va-select--visible': this.visible,
+        'va-select--searchable': this.searchable,
+        'va-select--disabled': this.disabled,
+        'va-select--loading': this.loading,
+      }
+    },
+    selectStyle () {
+      return {
+        backgroundColor:
+          this.error ? getHoverColor(this.$themes['danger'])
+            : this.success ? getHoverColor(this.$themes['success']) : '#f5f8f9',
+        borderColor:
+          this.error ? this.$themes.danger
+            : this.success ? this.$themes.success
+              : this.$themes.gray,
+      }
+    },
+    optionsListStyle () {
+      return { maxHeight: this.maxHeight }
+    },
+    displayedText () {
+      if (!this.valueProxy) {
+        return ''
+      }
+      if (this.multiple) {
+        return this.valueProxy.length ? `${this.valueProxy.length} items selected` : ''
+      }
+      // We try to find a match from options, if we don't find any - we take value.
+      // This way select can display value even when options are not loaded yet.
+      const selectedOption = this.valueProxy || this.selectedOption
+      const isString = typeof selectedOption === 'string'
+      return isString ? selectedOption : selectedOption[this.textBy]
+    },
+    selectedOption () {
+      return (!this.valueProxy || this.multiple) ? null : this.options.find(option => this.compareOptions(option, this.valueProxy)) || null
+    },
+    filteredOptions () {
+      if (!this.search) {
+        return this.options
+      }
+
+      return this.options.filter(option => {
+        const optionText = this.getText(option).toUpperCase()
+        const search = this.search.toUpperCase()
+        return optionText.includes(search)
+      })
+    },
+    showClearIcon () {
+      if (this.disabled) {
+        return false
+      }
+      return this.multiple ? this.valueProxy.length : this.valueProxy
+    },
+    inputWrapperStyles () {
+      let paddingRight = 1.5
+      if (this.showClearIcon) {
+        paddingRight += 2
+      }
+      return {
+        paddingRight: `${paddingRight}rem`,
+        paddingTop: this.label ? '.84rem' : 'inherit',
+        paddingBottom: this.label ? 0 : '.4375rem',
+      }
+    },
+    inputStyles () {
+      return this.visible && !this.disabled
+        ? { width: '100%' }
+        : { width: '0', position: 'absolute', padding: '0' }
+    },
+    valueProxy: {
+      get () {
+        return this.value
+      },
+      set (value) {
+        this.$emit('input', value)
+      },
+    },
+  },
+  methods: {
+    getOptionClass (option) {
+      return {
+        'va-select__option': true,
+        'va-select__option--selected': this.isSelected(option),
+      }
+    },
+    getOptionStyle (option) {
+      return {
+        color: this.isSelected(option) ? this.$themes['success'] : 'inherit',
+        backgroundColor: this.isHovered(option) ? getHoverColor(this.$themes['success']) : 'transparent',
+      }
+    },
+    getText (option) {
+      return typeof option === 'string' ? option : option[this.textBy]
+    },
+    getKey (option) {
+      return typeof option === 'string' ? option : option[this.keyBy]
+    },
+    updateSearch (val) {
+      this.search = val
+    },
+    compareOptions (one, two) {
+      // identity check works nice for strings and exact matches.
+      if (one === two) {
+        return true
+      }
+      if (typeof this.value === 'string') {
+        return false
+      }
+      return one[this.keyBy] === two[this.keyBy]
+    },
+    isSelected (option) {
+      if (typeof option === 'string') {
+        return this.multiple
+          ? this.valueProxy.includes(option)
+          : this.valueProxy === option
+      } else {
+        return this.multiple
+          ? this.valueProxy.filter(item => item[this.keyBy] === option[this.keyBy]).length
+          : this.valueProxy[this.keyBy] === option[this.keyBy]
+      }
+    },
+    isHovered (option) {
+      return this.hoveredOption
+        ? typeof option === 'string' ? option === this.hoveredOption : this.hoveredOption[this.keyBy] === option[this.keyBy]
+        : false
+    },
+    selectOption (option) {
+      this.search = ''
+      const isSelected = this.isSelected(option)
+      const value = this.value || []
+
+      if (this.multiple) {
+        this.valueProxy = isSelected
+          ? value.filter(optionSelected => option !== optionSelected)
+          : [...value, option]
+        this.$refs.dropdown.updatePopper()
+      } else {
+        this.valueProxy = typeof option === 'string' ? option : { ...option }
+        this.search = ''
+        this.$refs.dropdown.hide()
+      }
+      if (this.searchable) {
+        this.$refs.search.focus()
+      }
+    },
+    clear () {
+      this.valueProxy = this.multiple ? [] : this.clearValue
+      this.search = ''
+    },
+    updateHoveredOption (option) {
+      if (option) {
+        this.hoveredOption = typeof option === 'string' ? option : { ...option }
+      } else {
+        this.hoveredOption = null
+      }
+    },
+  },
+  mounted () {
+    this.mounted = true
+  },
+}
+</script>
+
+<style lang="scss">
+@import "../../vuestic-sass/resources/resources";
+
+.va-select {
+  cursor: pointer;
+  display: flex;
+  align-items: flex-end;
+  position: relative;
+  width: 100%;
+  min-height: 2.375rem;
+  border-style: solid;
+  border-width: 0 0 thin 0;
+  border-top-left-radius: 0.5rem;
+  border-top-right-radius: 0.5rem;
+  margin-bottom: 1rem;
+
+  &:focus {
+    outline: none;
+  }
+
+  &--disabled {
+    @include va-disabled()
+  }
+
+  &--loading {
+    .va-select__clear-icon,
+    .va-select__open-icon {
+      visibility: hidden;
+    }
+  }
+
+  &__label {
+    @include va-title();
+    position: absolute;
+    top: .125rem;
+    left: .5rem;
+    margin-bottom: .5rem;
+    max-width: calc(100% - .25rem);
+    @include va-ellipsis();
+    transform-origin: top left;
+  }
+
+  &__input-wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    height: 100%;
+    width: 100%;
+    justify-content: stretch;
+    padding-left: .5rem;
+  }
+
+  &__input {
+    border: none;
+    background: transparent;
+    padding: 0.25rem 0;
+    font-size: 1rem;
+    font-family: $font-family-sans-serif;
+    font-weight: normal;
+    font-style: normal;
+    font-stretch: normal;
+    line-height: 1.5;
+    letter-spacing: normal;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    &:focus {
+      outline: none;
+    }
+  }
+
+  &__displayed-text {
+    white-space: nowrap;
+    overflow-x: hidden;
+    text-overflow: ellipsis;
+    width: 100%;
+  }
+  &__placeholder {
+    opacity: .5;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    width: 100%;
+  }
+
+  &__clear-icon {
+    color: $va-link-color-secondary;
+    width: 1.5rem;
+    height: 1.5rem;
+    padding: .25rem;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    right: 1.5rem;
+    margin: auto;
+  }
+
+  &__open-icon {
+    @extend .va-select__clear-icon;
+    right: .5rem;
+  }
+
+  &__tags {
+    & > .va-chip{
+      margin-right: .25rem;
+      &:last-of-type {
+        margin-top: .125rem;
+        margin-bottom: .125rem;
+      }
+    }
+  }
+
+  &__loading {
+    position: absolute;
+    right: .5rem;
+    top: 0;
+    bottom: 0;
+    margin: auto;
+  }
+
+  &__dropdown {
+    outline: none;
+    margin: 0;
+    padding: 0;
+    background: $light-gray3;
+    border-radius: .5rem;
+
+    &.va-select__dropdown-position-top {
+      box-shadow: 0 -2px 3px 0 rgba(98, 106, 119, 0.25);
+    }
+    .va-dropdown__content {
+      background-color: $light-gray3;
+      margin: 0;
+      padding: 0;
+      overflow-y: auto;
+      box-shadow: $datepicker-box-shadow;
+      border-radius: .5rem;
+    }
+  }
+
+  &__option-list {
+    width: 100%;
+    list-style: none;
+    &.no-options {
+      padding: .5rem;
+    }
+  }
+
+  &__option {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    padding: .375rem .5rem .375rem .5rem;
+
+    &__selected-icon {
+      margin-left: auto;
+      font-size: 1.2rem;
+    }
+
+    &__icon {
+      margin-right: .5rem;
+    }
+  }
+}
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-select/va-select.docs.md b/src/vuestic-theme/vuestic-components/va-select/va-select.docs.md
new file mode 100644
index 0000000..0a4a194
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-select/va-select.docs.md
@@ -0,0 +1,42 @@
+#Select
+```html
+<va-select
+  :options="options"
+  v-model="value"
+  label="Country"
+  placeholder="Select country"
+  clearable="false"
+/>
+
+<va-select
+  :options="options"
+  v-model="value"
+  multiple
+  searchable
+/>
+```
+
+**Props**
+
+* `value` - String|Array
+* `label` - String - label of select
+* `placeholder` - String - placeholder of select
+* `options` - Array (default: []) - list of select options. You can use strings or objects as an option. See the object option structure below. 
+* `position`: String (default: 'bottom') - direction of select open (one of these values: 'bottom', 'top'),
+* `multiple` - Boolean (default: false) - changes select to multiple
+* `tagMax` - Number (default: 5) - the max number of chips, when the number of selected items is bigger then max, there is only text '6 items selected'
+* `searchable` - Boolean (default: false) - if set true, input can be edited and options are filter automatically by inputted text
+* `disabled` - Boolean (default: false) - disable the select
+* `readonly` - Boolean (default: false) - puts input in readonly state 
+* `width` - String (default: 100%) - the width of the select
+* `maxHeight` - String (default: 128px) - the maximum height of the select
+* `noOptionsText` - String (default: 'Items not found') - set the custom text, if there are no options in select     
+* `fixed` - Boolean (default: true) - Fixed dropdown works fine even if container is `position: relative; overflow: hidden`
+* `error` - Boolean (default: false) - Sets error state
+* `success` - Boolean (default: false) - Sets the success state
+
+***Option***
+
+* `text` - String - displayed text of option. Field name can be changed by `text-by` prop
+* `icon` - String
+* `id` - String | Number - option key. Field name can be changed by `key-by` prop
diff --git a/src/vuestic-theme/vuestic-components/va-tabs/VuesticTabs.vue b/src/vuestic-theme/vuestic-components/va-tabs/VuesticTabs.vue
deleted file mode 100644
index 5c932ae..0000000
--- a/src/vuestic-theme/vuestic-components/va-tabs/VuesticTabs.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-  <div class="vuestic-tabs">
-    <div>
-      <nav class="nav nav-pills va-row">
-        <div
-          class="nav-item col"
-          @click="setActive(name)"
-          :class="{active: name === currentActive}"
-          v-for="name in names"
-          :key="name"
-        >
-          <span class="nav-link"><h5>{{name}}</h5></span>
-        </div>
-      </nav>
-      <div class="track">
-        <div :class="underscoreClass"></div>
-      </div>
-    </div>
-    <vuestic-simple-select
-      class="simple-select"
-      v-show="false"
-      v-bind:options="names" v-model="currentActive"></vuestic-simple-select>
-    <div class="tab-content">
-      <div
-        class="tab-pane"
-        :class="{active: name === currentActive}"
-        v-for="name in names"
-        :key="name"
-      >
-        <slot :name="name"></slot>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-// d-none and d-lg-flex were deleted, bug will be fixed in the nearest update
-export default {
-  name: 'vuestic-tabs',
-  props: ['names'],
-  computed: {
-    underscoreClass () {
-      return 'underscore-' + this.names.length + '-' + this.names.indexOf(this.currentActive)
-    },
-  },
-  methods: {
-    setActive (name) {
-      this.currentActive = name
-    },
-  },
-  data () {
-    return {
-      currentActive: this.names[0],
-    }
-  },
-}
-</script>
-
-<style lang="scss">
-.vuestic-tabs {
-  background-color: white;
-  .simple-select {
-    padding-top: 1.5rem;
-  }
-  .nav {
-    margin: 0;
-    padding-top: 2.25rem;
-    .nav-item {
-      flex-grow: 1;
-      text-align: center;
-      padding: 0;
-      .nav-link {
-        padding: 0;
-        color: $gray;
-        transition: all .3s ease;
-      }
-      &:hover {
-        cursor: pointer;
-        .nav-link {
-          color: $vue-darkest-blue;
-        }
-      }
-      &.active {
-        .nav-link {
-          color: $vue-darkest-blue;
-        }
-      }
-    }
-  }
-  .track {
-    overflow: hidden;
-    width: 100%;
-    height: .125rem;
-    position: relative;
-    div[class^='underscore-'] {
-      background-color: $brand-primary;
-      height: .125rem;
-      position: absolute;
-    }
-    $koeff: 0.8;
-    @for $all from 1 through 10 {
-      $width: 1/$all;
-      div[class^='underscore-#{$all}'] {
-        width: $width * $koeff * 100%;
-        transition: left .5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-      }
-      @for $place from 0 through $all {
-        .underscore-#{$all}-#{$place} {
-          left: ($place + (1 - $koeff)/2) * $width * 100%;
-        }
-      }
-    }
-  }
-  .tab-content {
-    padding-bottom: $tab-content-pb;
-    padding-top: $tab-content-pt;
-    > .tab-pane {
-      width: 100%
-    }
-  }
-}
-</style>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChart.demo.vue b/src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChart.demo.vue
deleted file mode 100644
index 323ce55..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-chart/VuesticChart.demo.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-  <div class="demo-container">
-    <div class="demo-container__item">
-      <vuestic-simple-select
-        :options="chartTypes"
-        v-model="chartType"
-      />
-      <va-button @click="refreshData()">
-        refreshData
-      </va-button>
-
-      <vuestic-chart :data="chartData" :type="chartType"/>
-    </div>
-  </div>
-</template>
-
-<script>
-import VuesticChart from './VuesticChart.vue'
-import VuesticSimpleSelect from '../vuestic-simple-select/VuesticSimpleSelect'
-// TODO Demo is non operational
-// HACK Data is bound to vuex
-// import { chartTypes } from './VuesticChartConfigs'
-// import { getLineChartData } from '../../../data/charts/LineChartData'
-
-export default {
-  data () {
-    return {
-      // chartData: getLineChartData(),
-      chartType: 'pie',
-    }
-  },
-  components: {
-    VuesticSimpleSelect,
-    VuesticChart,
-  },
-  computed: {
-    // chartTypes: () => chartTypes,
-  },
-  methods: {
-    refreshData () {
-      // this.chartData = getLineChartData()
-    },
-  },
-}
-</script>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-chat/VuesticChat.vue b/src/vuestic-theme/vuestic-components/vuestic-chat/VuesticChat.vue
deleted file mode 100644
index e789941..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-chat/VuesticChat.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<template>
-  <div class="vuestic-chat">
-    <div class="chat-body" :style="{'height': height}"
-         v-sticky-scroll="{animate: true, duration: 500}">
-      <div
-        class="chat-message"
-        v-for="(message, index) in value"
-        :key="index"
-        :class="{'yours': message.yours, 'alien': !message.yours}"
-      >
-        {{message.text}}
-      </div>
-    </div>
-    <div class="chat-controls">
-      <fieldset>
-        <div class="form-group form-group-w-btn">
-          <div class="input-group">
-            <input @keypress="keyHandler($event)" v-model="inputMessage"
-                   required title=""/>
-            <label class="control-label">Your message</label><va-icon icon="bar"/>
-          </div>
-          <va-button @click="sendMessage()">
-            Send
-          </va-button>
-        </div>
-      </fieldset>
-    </div>
-  </div>
-</template>
-
-<script>
-import StickyScroll from 'vuestic-directives/StickyScroll'
-
-export default {
-  name: 'vuestic-chat',
-  directives: { StickyScroll },
-  props: {
-    value: {
-      type: Array,
-      default: () => [],
-    },
-    height: {
-      default: '20rem',
-    },
-  },
-
-  data () {
-    return {
-      inputMessage: '',
-    }
-  },
-
-  methods: {
-    keyHandler (event) {
-      if (event.keyCode === 13) {
-        this.sendMessage()
-      }
-    },
-
-    sendMessage () {
-      if (this.inputMessage) {
-        this.$emit('input', this.value.concat({
-          text: this.inputMessage,
-          yours: true,
-        }))
-        this.inputMessage = ''
-      }
-    },
-  },
-
-  mounted () {
-    this.$emit('input', this.value)
-  },
-}
-</script>
-
-<style lang='scss' scoped>
-$chat-body-min-height: 18.75rem;
-$chat-body-mb: 1.5rem;
-$chat-message-mb: 0.625rem;
-$chat-message-py: 0.657rem;
-$chat-message-px: 1.375rem;
-$chat-message-br: 0.875rem;
-
-.vuestic-chat {
-  width: 100%;
-}
-
-.chat-body {
-  min-height: $chat-body-min-height;
-  display: flex;
-  flex-direction: column;
-  margin-bottom: $chat-body-mb;
-  overflow: auto;
-}
-
-.chat-message {
-  padding: $chat-message-py $chat-message-px;
-  margin-bottom: $chat-message-mb;
-  border-radius: $chat-message-br;
-  max-width: 70%;
-  overflow-wrap: break-word;
-
-  &:last-child {
-    margin-bottom: 0;
-  }
-
-  &.alien {
-    align-self: flex-start;
-    border-top-left-radius: 0;
-    background-color: $light-gray2;
-  }
-
-  &.yours {
-    align-self: flex-end;
-    border-top-right-radius: 0;
-    background-color: $brand-primary;
-  }
-
-  .chat-message-input {
-    resize: vertical !important;
-  }
-}
-
-</style>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-grid/Spacing.demo.vue b/src/vuestic-theme/vuestic-components/vuestic-grid/Spacing.demo.vue
deleted file mode 100644
index 74add06..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-grid/Spacing.demo.vue
+++ /dev/null
@@ -1,15 +0,0 @@
-<template>
-  <div class="demo-container">
-    <div class="demo-container__item">
-      <SpacingPlaygroud/>
-    </div>
-  </div>
-</template>
-
-<script>
-import SpacingPlaygroud from './SpacingPlaygroud'
-
-export default {
-  components: { SpacingPlaygroud },
-}
-</script>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-grid/SpacingPlaygroud.vue b/src/vuestic-theme/vuestic-components/vuestic-grid/SpacingPlaygroud.vue
deleted file mode 100644
index 1da7ea6..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-grid/SpacingPlaygroud.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<template>
-  <div class="spacing-playground va-layout gutter--md">
-    <div class="va-row">
-      <h3>Spacing playground</h3>
-    </div>
-    <div class="va-row">
-      <div class="flex xs3">
-        <span>m</span><vuestic-simple-select :options="directionList" v-model="selectedMarginDirection"/>
-      </div>
-      <div class="flex xs2">
-        <span>-</span><vuestic-simple-select :options="sizesList" v-model="selectedMarginSize"/>
-      </div>
-      <div class="flex xs3 offset-2">
-        <span>p</span><vuestic-simple-select :options="directionList" v-model="selectedPaddingDirection"/>
-      </div>
-      <div class="flex xs2">
-        <span>-</span><vuestic-simple-select :options="sizesList" v-model="selectedPaddingSize"/>
-      </div>
-    </div>
-    <div class="va-row">
-      <div class="flex xs6">{{selectedMarginClass}}</div>
-      <div class="flex xs6">{{selectedPaddingClass}}</div>
-    </div>
-    <div class="va-row">
-      <div class="flex xs12">
-        <div class="playground-component">
-          <div class="playground-component__margin" :class="selectedMarginClass">
-            <div :class="selectedPaddingClass" class="playground-component__padding">
-              <div class="playground-component__inner"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="va-row">
-      <div class="flex xs12">
-        <vuestic-color-presentation color="#c9f7db" name="padding"/>
-        <vuestic-color-presentation color="#ffd093" name="margin"/>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import VuesticSimpleSelect from '../vuestic-simple-select/VuesticSimpleSelect'
-import VuesticColorPresentation from '../vuestic-color-presentation/VuesticColorPresentation'
-
-export default {
-  name: 'spacing-playgroud',
-  components: { VuesticColorPresentation, VuesticSimpleSelect },
-  data () {
-    return {
-      directionList: ['a', 'y', 'x', 't', 'r', 'b', 'l'],
-      selectedMarginDirection: '',
-      selectedPaddingDirection: '',
-      sizesList: ['1', '2', '3', '4', '5', 'auto'],
-      selectedMarginSize: '',
-      selectedPaddingSize: '',
-    }
-  },
-  computed: {
-    selectedMarginClass () {
-      return (this.selectedMarginDirection && this.selectedMarginSize)
-        ? `m${this.selectedMarginDirection}-${this.selectedMarginSize}`
-        : ''
-    },
-    selectedPaddingClass () {
-      return (this.selectedPaddingDirection && this.selectedPaddingSize)
-        ? `p${this.selectedPaddingDirection}-${this.selectedPaddingSize}`
-        : ''
-    }
-  }
-}
-</script>
-
-<style lang="scss">
-.spacing-playground {
-  .playground-component {
-    background-color: #ffd093;
-    &__padding {
-      background-color: #c9f7db;
-    }
-    &__inner {
-      background-color: white;
-      border: 1px solid rgba(0, 0, 0, 0.2);
-      height: 20px;
-    }
-  }
-}
-</style>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.demo.vue b/src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.demo.vue
deleted file mode 100644
index 346f555..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.demo.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-  <div class="demo-container">
-    <div class="demo-container__item">
-      <vuestic-multi-select
-        label="Select country"
-        v-model="selectedCountries"
-        :options="CountriesList"
-      />
-    </div>
-    <div class="demo-container__item">
-      <vuestic-multi-select
-        label="Select country duplicate"
-        v-model="selectedCountries"
-        :options="CountriesList"
-      />
-    </div>
-  </div>
-</template>
-
-<script>
-
-import CountriesList from '../../../data/CountriesList'
-import VuesticMultiSelect from './VuesticMultiSelect'
-
-export default {
-  components: {
-    VuesticMultiSelect,
-  },
-  data () {
-    return {
-      selectedCountries: [],
-      CountriesList,
-    }
-  },
-}
-</script>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.vue b/src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.vue
deleted file mode 100644
index 6e9a927..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-multi-select/VuesticMultiSelect.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<template>
-  <div
-    class="form-group with-icon-right dropdown select-form-group multiselect-form-group"
-    v-dropdown
-    :class="{'has-error': hasErrors()}">
-    <div class="input-group dropdown-toggle">
-      <input
-        readonly
-        :class="{'has-value': !!displayValue}"
-        v-bind:value="displayValue"
-        required/>
-      <label class="control-label">{{label}}</label><va-icon icon="bar"/>
-      <small v-show="hasErrors()" class="help text-danger">{{
-        showRequiredError() }}
-      </small>
-      <va-icon icon="ion ion-ios-arrow-down icon-right input-icon dropdown-ion"/>
-    </div>
-    <div v-if="isClearable">
-      <va-icon
-        icon="fa fa-close icon-cross icon-right input-icon multiselect-form-group__unselect"
-        @click.native="unselectOptions"/>
-    </div>
-    <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
-      <scrollbar ref="scrollbar">
-        <div class="dropdown-menu-content">
-          <div
-            class="dropdown-item"
-            :class="{'selected': isOptionSelected(option)}"
-            v-for="(option, index) in options"
-            :key="index"
-            @click="toggleSelection(option)"
-          >
-            <span
-              class="ellipsis">{{optionKey ? option[optionKey] : option}}</span>
-            <va-icon icon="fa fa-check selected-icon"/>
-          </div>
-        </div>
-      </scrollbar>
-    </div>
-  </div>
-</template>
-
-<script>
-import Dropdown from '../../vuestic-directives/Dropdown'
-import Scrollbar from '../va-scrollbar/VaScrollbar.vue'
-
-export default {
-  name: 'vuestic-multi-select',
-  components: {
-    Scrollbar,
-  },
-  directives: {
-    dropdown: Dropdown,
-  },
-  data () {
-    return {
-      displayValue: '',
-      validated: false,
-    }
-  },
-  props: {
-    label: String,
-    itemsChosenPlaceholder: {
-      type: String,
-      default: 'chosen',
-    },
-    clearable: {
-      type: Boolean,
-      default: true,
-    },
-    options: Array,
-    value: Array,
-    optionKey: String,
-    required: {
-      type: Boolean,
-      default: false,
-    },
-    name: {
-      type: String,
-      default: 'multiselect',
-    },
-  },
-  mounted () {
-    this.$emit('input', this.value)
-  },
-
-  updated: function () {
-    this.updateDisplayValue(this.value)
-  },
-
-  methods: {
-    unselectOptions () {
-      this.value.splice(0, this.value.length)
-      this.displayValue = ''
-      this.$emit('input', this.value)
-    },
-    toggleSelection (option) {
-      let newVal = this.isOptionSelected(option) ? this.deselectOption(option) : this.selectOption(option)
-      this.updateDisplayValue(newVal)
-      this.$emit('input', newVal)
-    },
-    isOptionSelected (option) {
-      return this.value.includes(option)
-    },
-    selectOption (option) {
-      return this.value.concat(option)
-    },
-    deselectOption (option) {
-      return this.value.filter(item => item !== option)
-    },
-    updateDisplayValue (newVal) {
-      if (newVal.length > 2) {
-        this.displayValue = `${newVal.length}/${this.options.length} ${this.itemsChosenPlaceholder}`
-      } else {
-        this.displayValue = (this.optionKey ? newVal.map(item => item[this.optionKey]) : newVal).join(', ')
-      }
-    },
-    validate () {
-      this.validated = true
-    },
-    isValid () {
-      let isValid = true
-      if (this.required) {
-        isValid = !!this.displayValue
-      }
-      return isValid
-    },
-    hasErrors () {
-      let hasErrors = false
-      if (this.required) {
-        hasErrors = this.validated && !this.displayValue
-      }
-      return hasErrors
-    },
-    showRequiredError () {
-      return `The ${this.name} field is required`
-    },
-  },
-  computed: {
-    isClearable () {
-      return (this.clearable && this.value.length !== 0 && this.displayValue !== '')
-    },
-  },
-}
-</script>
-
-<style lang="scss" scoped>
-@import "../../vuestic-sass/resources/resources";
-
-.multiselect-form-group {
-  &__unselect {
-    margin-right: 20px;
-    cursor: pointer;
-  }
-
-  .dropdown-ion {
-    top: 12px;
-    cursor: pointer;
-  }
-
-  .dropdown-menu {
-    padding: 0;
-    .vuestic-scrollbar {
-      max-height: $dropdown-item-height * 4;
-    }
-  }
-}
-</style>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect-413.demo.vue b/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect-413.demo.vue
deleted file mode 100644
index 716a20c..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect-413.demo.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-  <div class="demo-container">
-    <div class="demo-container__item" style="width: 500px;">
-      <vuestic-simple-select
-        label="Position"
-        v-model="value"
-        option-key="description"
-        :options="imagePositions"
-      />
-    </div>
-  </div>
-</template>
-
-<script>
-// Fixes https://github.com/epicmaxco/vuestic-admin/issues/413
-
-import VuesticSimpleSelect from './VuesticSimpleSelect'
-
-export default {
-  name: 'my-component',
-  components: { VuesticSimpleSelect },
-  data: () => {
-    return {
-      value: null,
-      imagePositions: [
-        {
-          id: '1',
-          description: '1',
-        },
-        {
-          id: '2',
-          description: '2',
-        },
-      ],
-    }
-  },
-}
-</script>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.demo.vue b/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.demo.vue
deleted file mode 100644
index e55c19c..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.demo.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-  <div class="demo-container">
-    <div class="demo-container__item">
-      <vuestic-simple-select
-        label="Select country"
-        v-model="selectedCountry"
-        :options="CountriesList"
-      />
-    </div>
-    <div class="demo-container__item">
-      <vuestic-simple-select
-        label="Select country duplicate"
-        v-model="selectedCountry"
-        :options="CountriesList"
-      />
-    </div>
-  </div>
-</template>
-
-<script>
-
-import CountriesList from '../../../data/CountriesList'
-import VuesticSimpleSelect from './VuesticSimpleSelect'
-
-export default {
-  components: {
-    VuesticSimpleSelect,
-  },
-  data () {
-    return {
-      selectedCountry: '',
-      CountriesList,
-    }
-  },
-}
-</script>
diff --git a/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.vue b/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.vue
deleted file mode 100644
index 1f56f93..0000000
--- a/src/vuestic-theme/vuestic-components/vuestic-simple-select/VuesticSimpleSelect.vue
+++ /dev/null
@@ -1,228 +0,0 @@
-<template>
-  <div class="vuestic-simple-select">
-    <div
-      class="form-group with-icon-right dropdown select-form-group"
-      v-dropdown="{ isBlocked: true, onDropdownClose: onDropdownClose }"
-      :class="{'has-error': hasErrors()}"
-    >
-      <div
-        class="input-group dropdown-toggle vuestic-simple-select__dropdown-toggle">
-        <div>
-          <input
-            @focus="showDropdown()"
-            :class="{'has-value': !!value}"
-            v-model="displayValue"
-            :name="name"
-            :options="options"
-          >
-          <label class="control-label">{{label}}</label><va-icon icon="bar"/>
-          <small v-show="hasErrors()" class="help text-danger">
-            {{ showRequiredError() }}
-          </small>
-        </div>
-        <va-icon
-          icon="ion ion-ios-arrow-down icon-right input-icon vuestic-simple-select__dropdown-arrow"
-          @click="showDropdown"
-        />
-      </div>
-      <div v-if="isClearable">
-        <va-icon
-          icon="fa fa-close icon-cross icon-right input-icon vuestic-simple-select__unselect"
-          @click="unselectOption"
-        />
-      </div>
-      <div
-        class="dropdown-menu vuestic-simple-select__dropdown-menu"
-        aria-labelledby="dropdownMenuButton">
-        <scrollbar ref="scrollbar">
-          <div
-            class="dropdown-menu-content vuestic-simple-select__dropdown-menu-content">
-            <div
-              class="dropdown-item vuestic-simple-select__dropdown-item"
-              v-for="(option, index) in filteredList"
-              :key="index"
-              :class="{'selected': isOptionSelected(option)}"
-              @click="toggleSelection(option)"
-            >
-            <span
-              class="ellipsis">{{optionKey ? option[optionKey] : option}}</span>
-            </div>
-          </div>
-        </scrollbar>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-import Dropdown from '../../vuestic-directives/Dropdown'
-import Scrollbar from '../va-scrollbar/VaScrollbar.vue'
-
-export default {
-  name: 'vuestic-simple-select',
-  components: {
-    Scrollbar,
-  },
-  directives: {
-    dropdown: Dropdown,
-  },
-  props: {
-    label: String,
-    options: Array,
-    value: {
-      default: '',
-      required: true,
-    },
-    optionKey: String,
-    required: {
-      type: Boolean,
-      default: false,
-    },
-    clearable: {
-      type: Boolean,
-      default: true,
-    },
-    name: {
-      type: String,
-      default: 'simple-select',
-    },
-  },
-  data () {
-    return {
-      validated: false,
-      displayValue: this.value || '',
-      selectedValue: this.value,
-    }
-  },
-  watch: {
-    value: {
-      handler (value) {
-        if (!value || !this.optionKey) {
-          this.displayValue = value || ''
-          this.selectedValue = value || ''
-          return
-        }
-        this.selectedValue = value[this.optionKey]
-        this.displayValue = value[this.optionKey]
-      },
-      immediate: true,
-    },
-  },
-  computed: {
-    filteredList () {
-      const optionKey = this.optionKey
-      const displayValue = this.displayValue
-      if (displayValue === '') {
-        return this.options
-      } else {
-        // HACK This is done poorly.
-        return this.options.filter(function (item) {
-          if (optionKey && item && item[optionKey]) {
-            // option is object
-            if (displayValue) {
-              return item[optionKey].toLowerCase()
-                .search(displayValue.toLowerCase()) === 0
-            }
-          } else {
-            // option is string
-            return (item + '').toLowerCase()
-              .search(displayValue.toLowerCase()) === 0
-          }
-        })
-      }
-    },
-    isClearable () {
-      return (this.clearable && this.selectedValue !== '' && this.displayValue !== '' && this.selectedValue !== undefined)
-    },
-    placeholder () {
-      if (this.optionKey && this.selectedValue) {
-        return this.selectedValue[this.optionKey]
-      } else {
-        return this.selectedValue
-      }
-    },
-  },
-  methods: {
-    onDropdownClose () {
-      if (!this.value) {
-        this.displayValue = ''
-      }
-      if (this.value && this.optionKey) {
-        this.displayValue = this.value[this.optionKey]
-      }
-    },
-    toggleSelection (option) {
-      this.isOptionSelected(option) ? this.unselectOption() : this.selectOption(option)
-    },
-    unselectOption () {
-      this.selectedValue = ''
-      this.$emit('input', this.selectedValue)
-    },
-    showDropdown () {
-      this.displayValue = ''
-    },
-    isOptionSelected (option) {
-      if (this.optionKey) {
-        return this.selectedValue === option[this.optionKey]
-      } else {
-        return this.selectedValue === option
-      }
-    },
-    selectOption (option) {
-      if (!option) {
-        this.displayValue = ''
-      }
-      if (option && this.optionKey) {
-        this.displayValue = option[this.optionKey]
-      }
-      this.selectedValue = option
-      this.$emit('input', option)
-    },
-    validate () {
-      this.validated = true
-    },
-    isValid () {
-      let isValid = true
-      if (this.required) {
-        isValid = !!this.value
-      }
-      return isValid
-    },
-    hasErrors () {
-      let hasErrors = false
-      if (this.required) {
-        hasErrors = this.validated && !this.value
-      }
-      return hasErrors
-    },
-    showRequiredError () {
-      return `The ${this.name} field is required`
-    },
-  },
-}
-</script>
-
-<style lang="scss">
-@import "../../vuestic-sass/resources/resources";
-
-.vuestic-simple-select {
-
-  &__unselect {
-    margin-right: 20px;
-    cursor: pointer;
-  }
-
-  .vuestic-simple-select__dropdown-arrow.vuestic-simple-select__dropdown-arrow {
-    top: 12px;
-    cursor: pointer;
-  }
-
-  &__dropdown-menu {
-    padding: 0;
-
-    .vuestic-scrollbar {
-      max-height: $dropdown-item-height * 4;
-    }
-  }
-}
-</style>
diff --git a/src/vuestic-theme/vuestic-plugin.js b/src/vuestic-theme/vuestic-plugin.js
index 9b77735..1b654f8 100644
--- a/src/vuestic-theme/vuestic-plugin.js
+++ b/src/vuestic-theme/vuestic-plugin.js
@@ -1,8 +1,6 @@
 import VaNotification from './vuestic-components/va-notification/VaNotification.vue'
 import Breadcrumbs
   from './vuestic-components/vuestic-breadcrumbs/VuesticBreadcrumbs.vue'
-import Chart from './vuestic-components/vuestic-chart/VuesticChart.vue'
-import Chat from './vuestic-components/vuestic-chat/VuesticChat.vue'
 import VaCheckbox from './vuestic-components/va-checkbox/VaCheckbox.vue'
 import VaProgressCircle
   from './vuestic-components/va-progress-bar/progress-types/VaProgressCircle.vue'
@@ -16,8 +14,6 @@ import VaSlider
 import MediumEditor
   from './vuestic-components/vuestic-medium-editor/VuesticMediumEditor.vue'
 import Modal from './vuestic-components/va-modal/VaModal.vue'
-import MultiSelect
-  from './vuestic-components/vuestic-multi-select/VuesticMultiSelect.vue'
 import Popover from './vuestic-components/vuestic-popover/VuesticPopover.vue'
 import PreLoader
   from './vuestic-components/va-preloader/VaPreLoader.vue'
@@ -39,8 +35,8 @@ import RadioButton
   from './vuestic-components/va-radio-button/VaRadioButton'
 import Scrollbar
   from './vuestic-components/va-scrollbar/VaScrollbar.vue'
-import SimpleSelect
-  from './vuestic-components/vuestic-simple-select/VuesticSimpleSelect.vue'
+import VaSelect
+  from './vuestic-components/va-select/VaSelect.vue'
 import SocialNews
   from './vuestic-components/vuestic-social-news/VuesticSocialNews.vue'
 import Switch from './vuestic-components/vuestic-switch/VuesticSwitch.vue'
@@ -97,14 +93,12 @@ const VuesticPlugin = {
       VaNotification,
       Breadcrumbs,
       Chart,
-      Chat,
       VaCheckbox,
       VaProgressBar,
       DataTable,
       Feed,
       VaProgressCircle,
       Modal,
-      MultiSelect,
       PreLoader,
       ProfileCard,
       VaProgressBar,
@@ -117,7 +111,7 @@ const VuesticPlugin = {
       VaPagination,
       RadioButton,
       Scrollbar,
-      SimpleSelect,
+      VaSelect,
       SocialNews,
       Switch,
       Tabs,
diff --git a/src/vuestic-theme/vuestic-sass/global/_typography.scss b/src/vuestic-theme/vuestic-sass/global/_typography.scss
index a85628f..7ad3114 100644
--- a/src/vuestic-theme/vuestic-sass/global/_typography.scss
+++ b/src/vuestic-theme/vuestic-sass/global/_typography.scss
@@ -1,6 +1,7 @@
 
 ::selection {
   background-color: $text-selected;
+  color: $white;
 }
 
 .link {
@@ -13,24 +14,22 @@
 
 .link, .link-secondary {
   cursor: pointer;
+
   &:hover {
     color: $va-link-color-hover;
   }
+
   &:active {
     color: $va-link-color-active;
   }
+
   &:visited {
     color: $va-link-color-visited;
   }
 }
 
 .title {
- color: $vue-green;
- font-size: 10px;
- letter-spacing: 0.6px;
- line-height: 1.2;
- font-weight: bold;
- text-transform: uppercase;
+  @include va-title();
 
   &--info {
     color: $theme-blue-dark;
@@ -43,47 +42,62 @@
   &--warning {
     color: $theme-warning;
   }
+
+  &--gray {
+    color: $brand-secondary;
+  }
 }
 
 .text {
   &--bold {
     font-weight: bold;
   }
+
   &--highlighted {
     background-color: $text-highlighted;
   }
+
   &--left {
     text-align: left !important;
   }
+
   &--right {
     text-align: right !important;
   }
+
   &--center {
     text-align: center !important;
   }
+
   &--uppercase {
     text-transform: uppercase !important;
   }
+
   &--lowercase {
     text-transform: lowercase !important;
   }
+
   &--capitalize {
     text-transform: capitalize !important;
   }
+
   &--no-wrap {
     white-space: nowrap !important;
   }
+
   &--truncate {
     white-space: nowrap !important;
     overflow: hidden !important;
     text-overflow: ellipsis !important;
   }
+
   &--code {
     color: $white;
     font-family: "Source Code Pro";
     background-color: $vue-darkest-blue;
     padding: 0.1rem 0.2rem;
   }
+
   &--secondary {
     opacity: 0.4;
   }
@@ -97,6 +111,7 @@
 
   p {
     margin-bottom: 0.5rem;
+
     &:last-child {
       margin-bottom: 0;
     }
@@ -168,6 +183,38 @@ ol.va-ordered {
   }
 }
 
+.va-table {
+  width: 100%;
+  border-collapse: collapse;
+  @extend .mb-3;
+
+  thead tr {
+    border-bottom: 2px solid $vue-darkest-blue;
+
+    td {
+      @extend .title;
+      color: $vue-darkest-blue;
+    }
+  }
+
+  td {
+    padding: .625rem;
+    font-size: .875rem;
+    line-height: 1.43;
+    color: $vue-darkest-blue;
+  }
+
+  &.striped {
+    tbody tr:nth-of-type(odd) {
+      background-color: $white;
+    }
+
+    tbody tr:nth-of-type(even) {
+      background-color: $light-gray3;
+    }
+  }
+}
+
 .vue-misc {
   margin-top: 5.625rem;
   margin-bottom: 2rem;
diff --git a/src/vuestic-theme/vuestic-sass/resources/_mixins.scss b/src/vuestic-theme/vuestic-sass/resources/_mixins.scss
index dc25538..5e4950a 100644
--- a/src/vuestic-theme/vuestic-sass/resources/_mixins.scss
+++ b/src/vuestic-theme/vuestic-sass/resources/_mixins.scss
@@ -1,5 +1,14 @@
 @import "../../vuestic-components/vuestic-grid/grid-mixins";
 
+@mixin va-title() {
+  color: $vue-green;
+  font-size: .625rem;
+  letter-spacing: 0.6px;
+  line-height: 1.2;
+  font-weight: bold;
+  text-transform: uppercase;
+}
+
 @mixin flex-center () {
   display: flex;
   justify-content: center;
diff --git a/src/vuestic-theme/vuestic-sass/resources/_variables.scss b/src/vuestic-theme/vuestic-sass/resources/_variables.scss
index c1bd55d..8fd59f9 100644
--- a/src/vuestic-theme/vuestic-sass/resources/_variables.scss
+++ b/src/vuestic-theme/vuestic-sass/resources/_variables.scss
@@ -70,12 +70,12 @@ $body-bg: $light-gray;
 $top-nav-bg: $dark-blue;
 $body-color: $vue-darkest-blue !default;
 $layout-padding: 24px;
-$layout-padding-right: 44px;
+$layout-padding-right: 2rem;
 $nav-button-bg: linear-gradient(to right, #484b4f, #161616);
 
 $top-nav-height: 72px;
 
-$nav-padding-left: $layout-padding;
+$nav-padding-left: 1rem;
 $nav-padding-right: $layout-padding-right;
 $navbar-brand-container-left: 75px;
 
@@ -85,7 +85,6 @@ $sidebar-width--hidden: 56px;
 $sidebar-top: $top-nav-height;
 $sidebar-left--hidden: calc(#{$layout-padding} + #{$sidebar-width--hidden});
 $sidebar-left: $layout-padding;
-$min-z-index: -1000;
 
 $content-wrap-ml: calc(#{$sidebar-left} + #{$sidebar-width});
 $content-wrap-pb: $layout-padding;
@@ -93,6 +92,15 @@ $made-by-footer-pb: 27px;
 
 $greeny-box-shadow: 0 4px 9.6px 0.4px rgba($vue-green, .5);
 
+$sidebar-left: $layout-padding;
+
+$content-wrap-ml: calc(#{$sidebar-left} + #{$sidebar-width});
+$content-wrap-pb: $layout-padding;
+$made-by-footer-pb: 27px;
+
+$greeny-box-shadow: 0 4px 9.6px 0.4px rgba($vue-green, .5);
+$gray-box-shadow: 0 2px 3px 0 rgba(98, 106, 119, 0.25);
+
 //Auth
 $auth-wallpaper-ivuestic-h: 2.625rem;
 $auth-wallpaper-oblique-line: $dark-gray;
@@ -262,8 +270,8 @@ $chip-with-icon-wrapper-padding-nrm: 0.5rem;
 $chip-with-icon-content-padding-nrm: 0.5rem;
 
 //Dropdowns
-$dropdown-box-shadow: $greeny-box-shadow;
-$dropdown-background: $darkest-gray;
+$dropdown-box-shadow: $gray-box-shadow;
+$dropdown-background: $light-gray3;
 $dropdown-item-height: 40px;
 $dropdown-menu-padding-y: 10px;
 $dropdown-menu-padding-x: 0;
@@ -364,3 +372,8 @@ $card-box-shadow: 0 2px 3px 0 rgba(52, 56, 85, 0.25);
 $card-title-font-size: 0.625rem;
 $card-title-letter-spacing: 0.0375rem;
 $card-title-color: #104fca;
+
+// Datepickers
+
+$datepicker-box-shadow: 0 2px 3px 0 rgba(98, 106, 119, 0.25);
+
-- 
GitLab


From d66d381cf60f7645223e80d205887a5ca1cd44c6 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 4 Jul 2019 09:32:36 +0300
Subject: [PATCH 04/61] feat: upgrade version of va-input, start of UI (upload
 form)

---
 .../configSettings/forms/UploadForm.vue       |  16 +-
 .../va-checkbox/KeyboardOnlyFocusMixin.js     |  16 +
 .../va-checkbox/VaCheckbox.demo.vue           | 109 +++++--
 .../va-checkbox/VaCheckbox.spec.js            |   8 +-
 .../va-checkbox/VaCheckbox.vue                | 284 +++++++++---------
 .../__snapshots__/VaCheckbox.spec.js.snap     |  28 +-
 .../va-checkbox/va-checkbox-docs.md           |  37 +++
 .../va-input/VaInput.demo.vue                 | 103 +++++--
 .../vuestic-components/va-input/VaInput.vue   | 279 ++++++++++-------
 .../va-input/VaInputWrapper.demo.vue          | 159 ++++------
 .../va-input/VaInputWrapper.vue               | 174 +++++------
 .../va-input/VaMessageList.demo.vue           |  32 ++
 .../va-input/VaMessageList.vue                |  60 ++++
 .../va-input/message-list-docs.md             |   7 +
 14 files changed, 790 insertions(+), 522 deletions(-)
 create mode 100644 src/vuestic-theme/vuestic-components/va-checkbox/KeyboardOnlyFocusMixin.js
 create mode 100644 src/vuestic-theme/vuestic-components/va-checkbox/va-checkbox-docs.md
 create mode 100644 src/vuestic-theme/vuestic-components/va-input/VaMessageList.demo.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-input/VaMessageList.vue
 create mode 100644 src/vuestic-theme/vuestic-components/va-input/message-list-docs.md

diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index c3fc2b7..1221604 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -1,6 +1,12 @@
 <template>
   <div class="upload">
-    <va-select v-model="value" :options="options"></va-select>
+    <va-select v-model="uploader" :options="uploaderOptions" label="uploader"/>
+    <template v-if="uploader === 'Local'">
+      <va-input v-model="uploads" label="uploads"/>
+    </template>
+    <va-select v-model="filters" :options="filterOptions" multiple label="filters"/>
+    <va-checkbox label="Link name" v-model="link_name"/>
+    <p>When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe</p>
   </div>
 </template>
 
@@ -11,8 +17,12 @@ import { Component, Vue } from 'vue-property-decorator'
   components: { },
 })
 export default class UploadForm extends Vue {
-  value = ''
-  options = ['list', 'of', 'options']
+  uploader = ''
+  uploaderOptions = ['Local', 'S3']
+  uploads = ''
+  filters = []
+  link_name = false
+  filterOptions = ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename']
 }
 </script>
 
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/KeyboardOnlyFocusMixin.js b/src/vuestic-theme/vuestic-components/va-checkbox/KeyboardOnlyFocusMixin.js
new file mode 100644
index 0000000..4d6e9bb
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/KeyboardOnlyFocusMixin.js
@@ -0,0 +1,16 @@
+export const KeyboardOnlyFocusMixin = {
+  data () {
+    return {
+      isKeyboardFocused: false,
+      hasMouseDown: false,
+    }
+  },
+  methods: {
+    onFocus (e) {
+      if (this.hasMouseDown) {
+        return
+      }
+      this.isKeyboardFocused = true
+    },
+  },
+}
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.demo.vue b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.demo.vue
index 48e8de5..8f866c5 100644
--- a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.demo.vue
@@ -1,33 +1,104 @@
 <template>
-  <div class="demo-container">
-    <div class="demo-container__item">
-      <vuestic-checkbox v-model="value">
-        <div slot="label">
-         Selected
-        </div>
-      </vuestic-checkbox>
-      <vuestic-checkbox v-model="value" label="Readonly" readonly/>
-      <vuestic-checkbox v-model="value" label="Disabled" disabled/>
-      <vuestic-checkbox v-model="value" error label="Error" isError/>
-      <vuestic-checkbox v-model="value" errorMessages="errorMessages" label="Error-message"/>
-      <vuestic-checkbox v-model="value" :errorMessages="errorMessages" :errorCount="2" label="Error-message"/>
-      <vuestic-checkbox v-model="value" error disabled label="Error + disabled"/>
-    </div>
-  </div>
+  <VbDemo>
+    <VbCard title="Default">
+      <va-checkbox v-model="value" label="Selected"/>
+    </VbCard>
+    <VbCard title="Long label">
+      <va-checkbox
+        style="width: 200px"
+        v-model="value"
+        label="Long long long long long long long long long long long long long long long label"
+      />
+    </VbCard>
+    <VbCard title="Readonly">
+      <va-checkbox v-model="value" label="Readonly" readonly/>
+    </VbCard>
+    <VbCard title="Disabled">
+      <va-checkbox v-model="value" label="Disabled" disabled/>
+    </VbCard>
+    <VbCard title="Disabled to normal comparison">
+      <va-checkbox :value="false" label="Disabled and false" disabled/>
+      <va-checkbox :value="false" label="false"/>
+      <va-checkbox :value="true" label="Disabled and true" disabled/>
+      <va-checkbox :value="true" label="true"/>
+    </VbCard>
+    <VbCard title="Indeterminate">
+      <va-checkbox v-model="value" label="Indeterminate" indeterminate/>
+    </VbCard>
+
+    <VbCard title="Error">
+      <va-checkbox v-model="value" label="Error" error/>
+    </VbCard>
+    <VbCard title="String error message">
+      <va-checkbox
+        v-model="value"
+        label="Error messages"
+        :errorMessages="stringErrorMessage"
+      />
+    </VbCard>
+    <VbCard title="Array error messages">
+      <va-checkbox
+        v-model="value"
+        :errorMessages="errorMessages"
+        label="Multiple error messages"
+      />
+    </VbCard>
+    <VbCard title="Array error messages with maxed limit">
+      <va-checkbox
+        style="width: 200px"
+        v-model="value"
+        :errorMessages="errorMessages"
+        :errorCount="3"
+        label="Label"
+      />
+    </VbCard>
+    <VbCard title="Errors + disabled">
+      <va-checkbox
+        v-model="value"
+        error
+        disabled
+        label="Error + disabled"
+      />
+    </VbCard>
+    <VbCard title="No label">
+      <va-checkbox v-model="value"/>
+    </VbCard>
+    <VbCard title="Accepts id">
+      <va-checkbox
+        :value="true"
+        id="checkbox-id"
+      />
+    </VbCard>
+    <VbCard title="Accepts name">
+      <va-checkbox
+        :value="true"
+        name="checkbox-name"
+      />
+    </VbCard>
+    <VbCard title="Array as model">
+      {{selection}}
+      <va-checkbox v-model="selection" array-value='one' label="one"/>
+      <va-checkbox v-model="selection" array-value='two' label="two"/>
+      <va-checkbox v-model="selection" array-value='three' label="three"/>
+      <va-checkbox v-model="selection" array-value='four' label="four"/>
+    </VbCard>
+  </VbDemo>
 </template>
 
 <script>
-import VuesticCheckbox from './VaCheckbox'
+import VaCheckbox from './VaCheckbox'
 
 export default {
   components: {
-    VuesticCheckbox
+    VaCheckbox,
   },
   data () {
     return {
       value: true,
-      errorMessages: ['error message 1', 'error message 2']
+      selection: [],
+      stringErrorMessage: 'String error message',
+      errorMessages: ['Error message', 'Another error message', 'Long long long long long long long long long long long long long long error message'],
     }
-  }
+  },
 }
 </script>
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.spec.js b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.spec.js
index 8205417..4a31263 100644
--- a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.spec.js
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.spec.js
@@ -1,16 +1,20 @@
+import Vue from 'vue'
 import { shallowMount } from '@vue/test-utils'
 import VaCheckbox from './VaCheckbox'
 
+import { ColorThemePlugin } from '../../../services/ColorThemePlugin'
+Vue.use(ColorThemePlugin)
+
 describe('VaCheckbox', () => {
   it('default', () => {
     const wrapper = shallowMount(VaCheckbox, {
-      propsData: { value: false }
+      propsData: { value: false },
     })
     expect(wrapper.html()).toMatchSnapshot()
   })
   it('true value', () => {
     const wrapper = shallowMount(VaCheckbox, {
-      propsData: { value: true }
+      propsData: { value: true },
     })
     expect(wrapper.html()).toMatchSnapshot()
   })
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
index a26a3cc..f247ef1 100644
--- a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
@@ -6,12 +6,12 @@
     <div
       class="va-checkbox__input-container"
       @click="toggleSelection()"
-      @mousedown="onMouseDown"
-      @mouseup="onMouseUp"
+      @mousedown="hasMouseDown = true"
+      @mouseup="hasMouseDown = false"
     >
       <div
         class="va-checkbox__square"
-        :class="{'active': value}"
+        :class="{'active': isChecked}"
       >
         <input
           :id="id"
@@ -21,8 +21,9 @@
           class="va-checkbox__input"
           @keypress.prevent="toggleSelection()"
           :disabled="disabled"
+          :indeterminate="indeterminate"
         />
-        <va-icon name="ion ion-md-checkmark va-checkbox__icon-selected"/>
+        <va-icon :name="computedIcon"/>
       </div>
       <div class="va-checkbox__label-text">
         <slot name="label">
@@ -30,41 +31,48 @@
         </slot>
       </div>
     </div>
-    <div class="va-checkbox__error-message-container" v-if="showError">
-      <div
-        class="va-checkbox__error-message"
-        v-for="(error, index) in computedErrorMessages"
-        :key="index"
-      >
-        {{ error }}
-      </div>
-    </div>
+    <va-message-list
+      class="va-checkbox__error-message-container"
+      :value="errorMessages"
+      color="danger"
+      :limit="errorCount"
+    />
   </div>
 </template>
 
 <script>
 import VaIcon from '../va-icon/VaIcon'
+import VaMessageList from '../va-input/VaMessageList'
+import { KeyboardOnlyFocusMixin } from './KeyboardOnlyFocusMixin'
 
 export default {
   name: 'va-checkbox',
-  components: { VaIcon },
+  components: { VaMessageList, VaIcon },
+  mixins: [KeyboardOnlyFocusMixin],
   props: {
+    id: String,
     label: String,
+    name: String,
     value: {
-      type: Boolean,
+      type: [Boolean, Array],
       required: true,
     },
-    id: {
-      type: String,
-    },
-    disabled: {
-      type: Boolean,
-      default: false,
+    arrayValue: String,
+    indeterminate: Boolean,
+
+    disabled: Boolean,
+    readonly: Boolean,
+
+    checkedIcon: {
+      type: [String, Array],
+      default: 'ion ion-md-checkmark',
     },
-    readonly: {
-      type: Boolean,
-      default: false,
+    indeterminateIcon: {
+      type: [String, Array],
+      default: 'ion ion-md-remove',
     },
+
+    error: Boolean,
     errorMessages: {
       type: [String, Array],
       default: () => [],
@@ -73,40 +81,29 @@ export default {
       type: Number,
       default: 1,
     },
-    name: String,
-    error: {
-      type: Boolean,
-      default: false,
-    },
-  },
-  data () {
-    return {
-      isKeyboardFocused: false,
-      hasMouseDown: false,
-    }
   },
   computed: {
     computedClass () {
       return {
-        'va-checkbox--selected': this.value,
+        'va-checkbox--selected': this.isChecked,
         'va-checkbox--readonly': this.readonly,
         'va-checkbox--disabled': this.disabled,
+        'va-checkbox--indeterminate': this.indeterminate,
         'va-checkbox--error': this.showError,
         'va-checkbox--on-keyboard-focus': this.isKeyboardFocused,
       }
     },
-    computedErrorMessages () {
-      const isArray = Array.isArray(this.errorMessages)
-      const errorMessages = isArray ? this.errorMessages : [this.errorMessages]
-      return errorMessages.slice(0, this.errorCount)
+    computedIcon () {
+      return [
+        'va-checkbox__icon-selected',
+        this.indeterminate ? this.indeterminateIcon : this.checkedIcon,
+      ]
     },
-    valueProxy: {
-      set (value) {
-        this.$emit('input', value)
-      },
-      get () {
-        return this.value
-      },
+    isChecked () {
+      return this.modelIsArray ? this.value.includes(this.arrayValue) : this.value
+    },
+    modelIsArray () {
+      return Array.isArray(this.value)
     },
     showError () {
       // We make error active, if the error-message is not empty and checkbox is not disabled
@@ -119,20 +116,6 @@ export default {
     },
   },
   methods: {
-    onFocus (e) {
-      if (this.hasMouseDown) {
-        return
-      }
-      this.isKeyboardFocused = true
-    },
-    onMouseDown (e) {
-      this.hasMouseDown = true
-      this.$emit('mousedown', e)
-    },
-    onMouseUp (e) {
-      this.hasMouseDown = false
-      this.$emit('mouseup', e)
-    },
     toggleSelection () {
       if (this.readonly) {
         return
@@ -140,120 +123,129 @@ export default {
       if (this.disabled) {
         return
       }
-      this.valueProxy = !this.valueProxy
+      if (this.modelIsArray) {
+        if (this.value.includes(this.arrayValue)) {
+          this.$emit('input', this.value.filter(option => option !== this.arrayValue))
+        } else {
+          this.$emit('input', this.value.concat(this.arrayValue))
+        }
+        return
+      }
+
+      this.$emit('input', !this.value)
     },
   },
 }
 </script>
 
 <style lang="scss">
-  @import "../../vuestic-sass/resources/resources";
+@import "../../vuestic-sass/resources/resources";
 
-  .va-checkbox {
-    margin-bottom: $checkbox-between-items-margin;
-    display: flex;
-    flex-direction: column;
+.va-checkbox {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
 
-    &__input-container {
-      align-items: center;
-      display: flex;
-      cursor: pointer;
+  &__input-container {
+    align-items: center;
+    display: flex;
+    cursor: pointer;
 
-      @at-root {
-        .va-checkbox--disabled  & {
-          @include va-disabled();
-        }
+    @at-root {
+      .va-checkbox--disabled & {
+        @include va-disabled();
+      }
 
-        .va-checkbox--readonly & {
-          cursor: initial;
-        }
+      .va-checkbox--readonly & {
+        cursor: initial;
+      }
 
-        .va-checkbox--disabled & {
-          cursor: default;
-        }
+      .va-checkbox--disabled & {
+        cursor: default;
       }
     }
+  }
 
-    #{&}__square {
-      @include flex-center();
-      width: 2rem;
-      height: 2rem;
-      position: relative;
-      flex: 0 0 2rem;
-      @at-root {
-        .va-checkbox--on-keyboard-focus#{&} {
-          background-color: $light-gray;
-          transition: all, 0.6s, ease-in;
-          border-radius: 5rem;
-        }
+  #{&}__square {
+    @include flex-center();
+    width: 2rem;
+    height: 2rem;
+    position: relative;
+    flex: 0 0 2rem;
+    @at-root {
+      .va-checkbox--on-keyboard-focus#{&} {
+        background-color: $light-gray;
+        transition: all, 0.6s, ease-in;
+        border-radius: 5rem;
       }
     }
+  }
 
-    #{&}__input {
-      height: 1.375rem;
-      width: 1.375rem;
-      cursor: inherit;
-      color: $white;
-      background-color: $white;
-      border: solid 0.125rem $gray-light;
-      border-radius: 0.25rem;
+  #{&}__input {
+    height: 1.375rem;
+    width: 1.375rem;
+    cursor: inherit;
+    color: $white;
+    background-color: $white;
+    border: solid 0.125rem $gray-light;
+    border-radius: 0.25rem;
+
+    &:focus {
+      outline: none;
+    }
 
-      &:focus {
-        outline: none;
+    @at-root {
+      .va-checkbox--selected#{&} {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        height: 1.4rem;
+        width: 1.4rem;
+        color: $white;
+        background-color: $vue-green;
+        border: 0;
       }
 
-      @at-root {
-        .va-checkbox--selected#{&} {
-          display: flex;
-          justify-content: center;
-          align-items: center;
-          height: 1.4rem;
-          width: 1.4rem;
-          color: $white;
-          background-color: $vue-green;
-          border: 0;
-        }
-
-        .va-checkbox--error#{&} {
-          border-color: $theme-red;
-        }
+      .va-checkbox--error#{&} {
+        border-color: $theme-red;
       }
     }
+  }
 
-    #{&}__label-text {
-      display: inline-block;
-      position: relative;
-      margin-left: 0.25rem;
-      @at-root {
-        .va-checkbox--error#{&} {
-          color: $theme-red;
-        }
+  #{&}__label-text {
+    display: inline-block;
+    position: relative;
+    margin-left: 0.25rem;
+    @at-root {
+      .va-checkbox--error#{&} {
+        color: $theme-red;
       }
     }
+  }
 
-    &__error-message {
-      vertical-align: middle;
-      color: $theme-red;
-      font-size: $font-size-mini;
-    }
+  &__error-message {
+    vertical-align: middle;
+    color: $theme-red;
+    font-size: $font-size-mini;
+  }
 
-    &__icon-selected {
-      pointer-events: none;
-      position: absolute;
-      color: $white;
-    }
+  &__icon-selected {
+    pointer-events: none;
+    position: absolute;
+    color: $white;
+  }
 
-    &__error-message-container {
-      flex: 0 0 100%;
-      margin-left: 0.3rem; // To fit with checkbox.
-    }
+  &__error-message-container {
+    flex: 0 0 100%;
+    margin-left: 0.3rem; // To fit with checkbox.
+  }
 
-    &__label-container {
-      margin-left: 2rem;
-    }
+  &__label-container {
+    margin-left: 2rem;
+  }
 
-    &__content {
-      flex-direction: row;
-    }
+  &__content {
+    flex-direction: row;
   }
+}
 </style>
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/__snapshots__/VaCheckbox.spec.js.snap b/src/vuestic-theme/vuestic-components/va-checkbox/__snapshots__/VaCheckbox.spec.js.snap
index 19aeecf..60b8777 100644
--- a/src/vuestic-theme/vuestic-components/va-checkbox/__snapshots__/VaCheckbox.spec.js.snap
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/__snapshots__/VaCheckbox.spec.js.snap
@@ -1,25 +1,29 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`VaCheckbox default 1`] = `
-<div class="vuestic-checkbox">
-  <div class="vuestic-checkbox__square"><input readonly="readonly" class="vuestic-checkbox__input">
-    <va-icon-stub icon="ion ion-md-checkmark vuestic-checkbox__icon-selected"></va-icon-stub>
-  </div>
-  <div class="vuestic-checkbox__label-text">
+<div class="va-checkbox">
+  <div class="va-checkbox__input-container">
+    <div class="va-checkbox__square"><input readonly="readonly" class="va-checkbox__input">
+      <va-icon-stub name="va-checkbox__icon-selected,ion ion-md-checkmark"></va-icon-stub>
+    </div>
+    <div class="va-checkbox__label-text">
 
+    </div>
   </div>
-  <!---->
+  <va-message-list-stub color="danger" value="" limit="1" class="va-checkbox__error-message-container"></va-message-list-stub>
 </div>
 `;
 
 exports[`VaCheckbox true value 1`] = `
-<div class="vuestic-checkbox vuestic-checkbox--selected">
-  <div class="vuestic-checkbox__square active"><input readonly="readonly" class="vuestic-checkbox__input">
-    <va-icon-stub icon="ion ion-md-checkmark vuestic-checkbox__icon-selected"></va-icon-stub>
-  </div>
-  <div class="vuestic-checkbox__label-text">
+<div class="va-checkbox va-checkbox--selected">
+  <div class="va-checkbox__input-container">
+    <div class="va-checkbox__square active"><input readonly="readonly" class="va-checkbox__input">
+      <va-icon-stub name="va-checkbox__icon-selected,ion ion-md-checkmark"></va-icon-stub>
+    </div>
+    <div class="va-checkbox__label-text">
 
+    </div>
   </div>
-  <!---->
+  <va-message-list-stub color="danger" value="" limit="1" class="va-checkbox__error-message-container"></va-message-list-stub>
 </div>
 `;
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/va-checkbox-docs.md b/src/vuestic-theme/vuestic-components/va-checkbox/va-checkbox-docs.md
new file mode 100644
index 0000000..d559fc1
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/va-checkbox-docs.md
@@ -0,0 +1,37 @@
+# Checkbox
+
+Сheckbox supports `disabled` and `checked` attributes
+
+```html
+<va-checkbox
+  v-model="value"
+  label="Selected"
+/>
+<va-checkbox
+  v-model="value"
+  :errorMessages="errorMessages"
+  :errorCount="3"
+  label="With errors"
+/>
+<va-checkbox
+  v-model="array"
+  array-value="one"
+/>
+```
+
+**Props**
+* `id` - String
+* `name` - String
+* `label` - String - label to the right of checkbox
+* `value` - Boolean | Array - main value
+* `arrayValue` - Any
+* `indeterminate` - Boolean - indeterminate state. Note that `value` should be `true` for indeterminate icon to be displayed.
+* `disabled` - Boolean
+* `readonly` - Boolean
+* `checkedIcon` - String | Array (default: 'ion ion-md-checkmark')
+* `indeterminateIcon` - String | Array (default: 'ion ion-md-remove')
+* `errorMessages` - Number - list of error messages for current input field
+* `errorCount` - Number (default: 1) - shows a number of errors to display, given an array of error messages is passed
+* `error` - Boolean - define whether the field is error or not
+
+[See demo](http://vuestic.epicmax.co/#/admin/forms/form-elements)
diff --git a/src/vuestic-theme/vuestic-components/va-input/VaInput.demo.vue b/src/vuestic-theme/vuestic-components/va-input/VaInput.demo.vue
index 0eece77..ef668ef 100644
--- a/src/vuestic-theme/vuestic-components/va-input/VaInput.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-input/VaInput.demo.vue
@@ -1,50 +1,55 @@
 <template>
   <VbDemo>
-    <VbContainer title="Design">
+    <VbCard title="Design">
       <div style="height: 200px; overflow-y: scroll">
         <img src="http://i68.tinypic.com/ne84fs.png" alt="">
       </div>
-    </VbContainer>
-    <VbContainer title="Input With Placeholder">
+    </VbCard>
+    <VbCard title="Placeholder">
       <va-input
         v-model="empty"
         placeholder="Name"
       />
-    </VbContainer>
-    <VbContainer title="Input With Label">
+    </VbCard>
+    <VbCard title="Label">
       <va-input
         v-model="text"
         label="Name"
       />
-    </VbContainer>
-    <VbContainer title="Input With Message">
+    </VbCard>
+    <VbCard title="Label long">
       <va-input
         v-model="text"
-        :messages="messages"/>
-    </VbContainer>
-    <VbContainer title="Disabled Input">
+        label="Name long long long long long long long long long long long long"
+      />
+    </VbCard>
+    <VbCard title="Message">
+      <va-input
+        v-model="text"
+        :messages="messages"
+      />
+    </VbCard>
+    <VbCard title="Disabled">
       <va-input
         v-model="text"
         disabled
       />
-    </VbContainer>
-    <VbContainer title="Readonly Input">
+    </VbCard>
+    <VbCard title="Readonly Input">
       <va-input
         v-model="text"
         readonly
       />
-    </VbContainer>
-    <VbContainer title="Input With Icon">
+    </VbCard>
+    <VbCard title="Icon">
       <va-input
         v-model="text"
         label="Name"
       >
-        <va-icon
-          icon="fa fa-anchor"
-        />
+        <va-icon name="fa fa-anchor"/>
       </va-input>
-    </VbContainer>
-    <VbContainer title="Input With Button">
+    </VbCard>
+    <VbCard title="Button">
       <va-input
         v-model="text"
         label="Name"
@@ -53,54 +58,84 @@
           Upload
         </va-button>
       </va-input>
-    </VbContainer>
-    <VbContainer title="Input With Prepend Slot">
+    </VbCard>
+    <VbCard title="Prepend Slot">
       <va-input
         v-model="text"
         label="Name"
       >
         <va-icon
           slot="prepend"
+          name="fa fa-anchor"
+        />
+      </va-input>
+    </VbCard>
+    <VbCard title="Append Slot">
+      <va-input
+        v-model="text"
+        label="Name"
+      >
+        <va-icon
+          slot="append"
           icon="fa fa-anchor"
         />
       </va-input>
-    </VbContainer>
-    <VbContainer title="Removable Icon">
+    </VbCard>
+    <VbCard title="Removable Icon">
       <va-input
         v-model="text"
         removable
       >
       </va-input>
-    </VbContainer>
-    <VbContainer title="Input With Error">
+    </VbCard>
+    <VbCard title="Error">
       <va-input
         v-model="text"
         label="Name"
         error
       />
-    </VbContainer>
-    <VbContainer title="Input With Success">
+    </VbCard>
+    <VbCard title="Success">
       <va-input
         v-model="text"
         label="Name"
         success
       />
-    </VbContainer>
-    <VbContainer title="Success and Removable">
+    </VbCard>
+    <VbCard title="Success and Removable">
       <va-input
         v-model="text"
         label="Name"
         removable
         success
+        :messages="successMessages"
+      />
+    </VbCard>
+    <VbCard title="Error Message">
+      <va-input
+        v-model="text"
+        label="Name"
+        error
+        :error-messages="errorMessages"
       />
-    </VbContainer>
-    <VbContainer title="Input With Error Message">
+    </VbCard>
+    <VbCard title="Error count 2">
       <va-input
         v-model="text"
         label="Name"
         error
-        :error-messages="errorMessages"/>
-    </VbContainer>
+        :errorCount="2"
+        :error-messages="['one', 'two']"
+      />
+    </VbCard>
+    <VbCard title="Textarea">
+      <va-input
+        v-model="text"
+        label="Name"
+        type="textarea"
+        rows="5"
+      />
+    </VbCard>
   </VbDemo>
 </template>
 
@@ -108,6 +143,7 @@
 import VaInput from './VaInput'
 import VaButton from './../va-button/VaButton'
 import VaIcon from './../va-icon/VaIcon'
+
 export default {
   components: {
     VaInput,
@@ -121,6 +157,7 @@ export default {
       phone: '33 310-86-24',
       messages: ['Required field'],
       errorMessages: ['Detailed error message'],
+      successMessages: ['Success message'],
     }
   },
 }
diff --git a/src/vuestic-theme/vuestic-components/va-input/VaInput.vue b/src/vuestic-theme/vuestic-components/va-input/VaInput.vue
index 2fb702b..67670fd 100644
--- a/src/vuestic-theme/vuestic-components/va-input/VaInput.vue
+++ b/src/vuestic-theme/vuestic-components/va-input/VaInput.vue
@@ -1,79 +1,99 @@
 <template>
   <va-input-wrapper
     class="va-input"
-    :class="{ 'va-input-wrapper--focused': isFocused }"
     :disabled="disabled"
-    :error="error"
     :success="success"
     :messages="messages"
+    :error="error"
     :error-messages="errorMessages"
+    :errorCount="errorCount"
   >
     <slot name="prepend" slot="prepend"/>
     <div
-      class="va-input__slot">
-      <label
-        :style="labelStyles"
-        aria-hidden="true"
-        class="va-input__slot__label"
+      class="va-input__container"
+      :class="{'va-input__container--textarea': isTextarea}"
+      :style="containerStyles"
+    >
+      <div
+        class="va-input__container__content-wrapper"
+        :style="{ paddingTop: label ? '' : '0'}"
       >
-        {{ label }}
-      </label>
-      <textarea
-        v-if="textarea"
-        :style="{ paddingBottom: label ? '0.125rem' : '0.875rem'}"
-        :aria-label="label"
-        :placeholder="placeholder"
-        :disabled="disabled"
-        :readonly="readonly"
-        :value="value"
-        v-on="inputListeners"
-      />
-      <input
-        v-else
-        :style="{ paddingBottom: label ? '0.125rem' : '0.875rem'}"
-        :aria-label="label"
-        :type="type"
-        :placeholder="placeholder"
-        :disabled="disabled"
-        :readonly="readonly"
-        :value="value"
-        v-on="inputListeners"
+        <label
+          :style="labelStyles"
+          aria-hidden="true"
+          class="va-input__container__label"
+        >
+          {{ label }}
+        </label>
+        <textarea
+          v-if="isTextarea"
+          class="va-input__container__input"
+          :style="textareaStyles"
+          :aria-label="label"
+          :placeholder="placeholder"
+          :disabled="disabled"
+          :readonly="readonly"
+          :value="value"
+          v-on="inputListeners"
+          v-bind="$attrs"
+        />
+        <input
+          v-else
+          class="va-input__container__input"
+          :style="{ paddingBottom: label ? '0.125rem' : '0.875rem' }"
+          :aria-label="label"
+          :type="type"
+          :placeholder="placeholder"
+          :disabled="disabled"
+          :readonly="readonly"
+          :value="value"
+          v-on="inputListeners"
+          v-bind="$attrs"
+        />
+      </div>
+      <div
+        v-if="success || error || $slots.append || (removable && hasContent)"
+        class="va-input__container__icon-wrapper"
       >
+        <va-icon
+          v-if="success"
+          class="va-input__container__icon"
+          name="fa fa-check"
+          color="success"
+        />
+        <va-icon
+          v-if="error"
+          class="va-input__container__icon"
+          name="fa fa-exclamation-triangle"
+          color="danger"
+        />
+        <slot name="append"/>
+        <va-icon
+          v-if="removable && hasContent"
+          @click.native="clearContent()"
+          class="va-input__container__close-icon"
+          :color="error ? 'danger': 'gray'"
+          name="ion ion-md-close ion"
+        />
+      </div>
     </div>
-    <va-icon
-      v-if="success"
-      slot="append"
-      icon="fa fa-check"
-      color="success"
-    />
-    <va-icon
-      v-if="error"
-      slot="append"
-      icon="fa fa-exclamation-triangle"
-      color="danger"
-    />
-    <slot slot="append"/>
-    <va-icon
-      v-if="removable && value.length"
-      @click.native="clearContent()"
-      slot="append"
-      class="va-input__close-icon"
-      :color="error ? 'danger': 'gray'"
-      icon="ion ion-md-close ion"
-    />
   </va-input-wrapper>
 </template>
 
 <script>
-import VaInputWrapper from '../va-input/VaInputWrapper'
+import VaInputWrapper from './VaInputWrapper'
 import VaIcon from '../va-icon/VaIcon'
+import {
+  getHoverColor,
+} from './../../../services/color-functions'
+
 export default {
   name: 'va-input',
   extends: VaInputWrapper,
   components: { VaInputWrapper, VaIcon },
   props: {
     value: {
-      type: String,
+      type: [String, Number],
     },
     label: {
       type: String,
@@ -94,9 +114,6 @@ export default {
     removable: {
       type: Boolean,
     },
-    textarea: {
-      type: Boolean
-    },
   },
   data () {
     return {
@@ -109,8 +126,30 @@ export default {
         color: this.error ? this.$themes.danger : '',
       }
     },
+    containerStyles () {
+      return {
+        backgroundColor:
+          this.error ? getHoverColor(this.$themes['danger'])
+            : this.success ? getHoverColor(this.$themes['success']) : '#f5f8f9',
+        borderColor:
+          this.error ? this.$themes.danger
+            : this.success ? this.$themes.success
+              : this.isFocused ? this.$themes.dark : this.$themes.gray,
+      }
+    },
+    textareaStyles () {
+      return {
+        paddingBottom: this.label ? '0.125rem' : '',
+        marginTop: this.label ? '0.875rem' : '',
+        paddingTop: this.label ? 0 : '',
+        minHeight: this.label ? '1.5rem' : '2.25rem',
+        marginBottom: 0,
+      }
+    },
     inputListeners () {
-      return Object.assign({},
+      // TODO Probably not the best idea to stick this in computed.
+      return Object.assign(
+        {},
         this.$listeners,
         {
           input: event => {
@@ -120,15 +159,13 @@ export default {
             this.$emit('click', event)
           },
           focus: event => {
-            /* eslint-disable vue/no-side-effects-in-computed-properties */
+            // eslint-disable-next-line vue/no-side-effects-in-computed-properties
             this.isFocused = true
-            /* eslint-enable vue/no-side-effects-in-computed-properties */
             this.$emit('focus', event)
           },
           blur: event => {
-            /* eslint-disable vue/no-side-effects-in-computed-properties */
+            // eslint-disable-next-line vue/no-side-effects-in-computed-properties
             this.isFocused = false
-            /* eslint-disable vue/no-side-effects-in-computed-properties */
             this.$emit('blur', event)
           },
           keyup: event => {
@@ -140,6 +177,12 @@ export default {
         }
       )
     },
+    hasContent () {
+      return ![null, undefined, ''].includes(this.value)
+    },
+    isTextarea () {
+      return this.type === 'textarea'
+    },
   },
   methods: {
     clearContent () {
@@ -150,55 +193,87 @@ export default {
 </script>
 
 <style lang='scss'>
-  @import '../../vuestic-sass/resources/resources';
-  .va-input {
-    &__slot {
+@import '../../vuestic-sass/resources/resources';
+
+.va-input {
+  &__container {
+    display: flex;
+    position: relative;
+    width: 100%;
+    min-height: 2.375rem;
+    border-style: solid;
+    border-width: 0 0 thin 0;
+    border-top-left-radius: 0.5rem;
+    border-top-right-radius: 0.5rem;
+
+    &__content-wrapper {
       display: flex;
-      position: relative;
+      align-items: flex-end;
       width: 100%;
-      &__label {
-        position: absolute;
-        bottom: 0.875rem;
-        left: 0.5rem;
-        width: 100%;
-        margin-bottom: 0.5rem;
-        color: $vue-green;
-        font-size: 0.625rem;
-        letter-spacing: 0.0375rem;
-        line-height: 1.2;
-        font-weight: bold;
-        text-transform: uppercase;
-      }
-      input, textarea {
-        width: 100%;
-        height: 1.5rem;
-        margin-bottom: 0.125rem;
-        padding: 0.25rem 0.5rem;
-        color: #34495e;
-        background-color: transparent;
-        border-style: none;
-        outline: none;
-        font-size: 1rem;
-        font-family: $font-family-sans-serif;
-        font-weight: normal;
-        font-style: normal;
-        font-stretch: normal;
-        line-height: 1.5;
-        letter-spacing: normal;
-        &::placeholder {
-          color: $brand-secondary;
-        }
-        &:placeholder-shown {
-          padding-bottom: 0.875rem;
-        }
-      }
+      /*min-width: 100%;*/
     }
-    textarea {
-      height: 3rem;
+
+    &__icon-wrapper {
+      display: flex;
+      align-items: center;
+      margin-right: 0.5rem;
     }
+
     &__close-icon {
       cursor: pointer;
       margin-left: 0.25rem;
     }
+
+    &__label {
+      position: absolute;
+      bottom: 0.875rem;
+      left: 0.5rem;
+      margin-bottom: 0.5rem;
+      max-width: calc(100% - 0.25rem);
+      color: $vue-green;
+      font-size: 0.625rem;
+      letter-spacing: 0.0375rem;
+      line-height: 1.2;
+      font-weight: bold;
+      text-transform: uppercase;
+      @include va-ellipsis();
+      transform-origin: top left;
+    }
+
+    &.va-input__container--textarea &__label {
+      bottom: auto;
+      top: 0.125rem;
+    }
+
+    &__input {
+      width: 100%;
+      height: 1.5rem;
+      margin-bottom: 0.125rem;
+      padding: 0.25rem 0.5rem;
+      color: #34495e;
+      background-color: transparent;
+      border-style: none;
+      outline: none;
+      font-size: 1rem;
+      font-family: $font-family-sans-serif;
+      font-weight: normal;
+      font-style: normal;
+      font-stretch: normal;
+      line-height: 1.5;
+      letter-spacing: normal;
+
+      &::placeholder {
+        color: $brand-secondary;
+      }
+
+      &:placeholder-shown {
+        padding-bottom: 0.875rem;
+      }
+    }
+
+    &.va-input__container--textarea &__input {
+      height: inherit;
+    }
   }
+}
 </style>
diff --git a/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.demo.vue b/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.demo.vue
index 0eece77..171dfe7 100644
--- a/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.demo.vue
@@ -1,126 +1,81 @@
 <template>
   <VbDemo>
-    <VbContainer title="Design">
-      <div style="height: 200px; overflow-y: scroll">
-        <img src="http://i68.tinypic.com/ne84fs.png" alt="">
-      </div>
-    </VbContainer>
-    <VbContainer title="Input With Placeholder">
-      <va-input
-        v-model="empty"
-        placeholder="Name"
-      />
-    </VbContainer>
-    <VbContainer title="Input With Label">
-      <va-input
-        v-model="text"
-        label="Name"
-      />
-    </VbContainer>
-    <VbContainer title="Input With Message">
-      <va-input
-        v-model="text"
-        :messages="messages"/>
-    </VbContainer>
-    <VbContainer title="Disabled Input">
-      <va-input
-        v-model="text"
-        disabled
-      />
-    </VbContainer>
-    <VbContainer title="Readonly Input">
-      <va-input
-        v-model="text"
-        readonly
-      />
-    </VbContainer>
-    <VbContainer title="Input With Icon">
-      <va-input
-        v-model="text"
-        label="Name"
-      >
-        <va-icon
-          icon="fa fa-anchor"
-        />
-      </va-input>
-    </VbContainer>
-    <VbContainer title="Input With Button">
-      <va-input
-        v-model="text"
-        label="Name"
+    <VbCard title="Default">
+      <va-input-wrapper :messages="messages">
+        Input
+      </va-input-wrapper>
+    </VbCard>
+
+    <VbCard title="Slots scheme">
+      <va-input-wrapper :messages="messages">
+        <div slot="prepend" style="width: 30px; height: 30px; border: 1px dotted black;" class="flex-center">
+          <va-icon name="fa fa-volume-off"/>
+        </div>
+        <div style="width: 200px; height: 30px; border: 1px dotted black;">Default Slot</div>
+        <div slot="append" style="width: 30px; height: 30px; border: 1px dotted black;" class="flex-center">
+          <va-icon name="fa fa-volume-up"/>
+        </div>
+      </va-input-wrapper>
+    </VbCard>
+
+    <VbCard title="Error">
+      <va-input-wrapper
+        :errorMessages="errorMessages"
       >
-        <va-button style="margin-right: 0;" small>
-          Upload
-        </va-button>
-      </va-input>
-    </VbContainer>
-    <VbContainer title="Input With Prepend Slot">
-      <va-input
-        v-model="text"
-        label="Name"
+        <div>Default Slot</div>
+      </va-input-wrapper>
+    </VbCard>
+
+    <VbCard title="Error">
+      <va-input-wrapper
+        :errorMessages="errorMessages"
       >
-        <va-icon
-          slot="prepend"
-          icon="fa fa-anchor"
+        <div>Default Slot</div>
+      </va-input-wrapper>
+    </VbCard>
+
+    <VbCard title="Input Wrapper For Checkbox and Radio Button">
+      <va-input-wrapper :messages="messages">
+        <va-checkbox name="agree-to-terms" v-model="agreedToTerms">
+          <template slot="label">
+            {{ $t('auth.agree') }}
+            <a class="link" href="javascript:void(0);">{{ $t('auth.termsOfUse') }}</a>
+          </template>
+        </va-checkbox>
+      </va-input-wrapper>
+
+      <va-input-wrapper :messages="messages">
+        <va-radio-button
+          option="option1"
+          v-model="radioSelectedOption"
+          label="Radio"
         />
-      </va-input>
-    </VbContainer>
-    <VbContainer title="Removable Icon">
-      <va-input
-        v-model="text"
-        removable
-      >
-      </va-input>
-    </VbContainer>
-    <VbContainer title="Input With Error">
-      <va-input
-        v-model="text"
-        label="Name"
-        error
-      />
-    </VbContainer>
-    <VbContainer title="Input With Success">
-      <va-input
-        v-model="text"
-        label="Name"
-        success
-      />
-    </VbContainer>
-    <VbContainer title="Success and Removable">
-      <va-input
-        v-model="text"
-        label="Name"
-        removable
-        success
-      />
-    </VbContainer>
-    <VbContainer title="Input With Error Message">
-      <va-input
-        v-model="text"
-        label="Name"
-        error
-        :error-messages="errorMessages"/>
-    </VbContainer>
+      </va-input-wrapper>
+    </VbCard>
   </VbDemo>
 </template>
 
 <script>
-import VaInput from './VaInput'
+import VaInputWrapper from './VaInputWrapper'
 import VaButton from './../va-button/VaButton'
 import VaIcon from './../va-icon/VaIcon'
+import VaCheckbox from '../va-checkbox/VaCheckbox'
+
 export default {
   components: {
-    VaInput,
+    VaCheckbox,
+    VaInputWrapper,
     VaButton,
     VaIcon,
   },
   data () {
     return {
       empty: '',
+      agreedToTerms: false,
+      radioSelectedOption: false,
       text: 'Vuestic',
-      phone: '33 310-86-24',
       messages: ['Required field'],
-      errorMessages: ['Detailed error message'],
+      errorMessages: ['Detailed error message', 'Detailed error message', 'Detailed error message'],
     }
   },
 }
diff --git a/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.vue b/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.vue
index 410347a..44a80d8 100644
--- a/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.vue
+++ b/src/vuestic-theme/vuestic-components/va-input/VaInputWrapper.vue
@@ -1,68 +1,44 @@
-
 <template>
-  <div
-    class="va-input-wrapper"
-    :class="{ 'va-input-wrapper--disabled' : disabled }"
-  >
+  <div class="va-input-wrapper">
     <div class="va-input-wrapper__control">
       <div
         tabindex="0"
-        :style="slotStyles"
         class="va-input-wrapper__slot">
         <div
-          v-if="hasPrependData"
+          v-if="$slots.prepend"
           class="va-input-wrapper__prepend-inner">
           <slot name="prepend"/>
         </div>
         <div class="va-input-wrapper__content">
           <slot/>
+          <div class="va-input-wrapper__details py-0 px-2">
+            <va-message-list
+              :color="messagesColor"
+              :value="messagesComputed"
+              :limit="error ? errorCount : 99"
+            />
+          </div>
         </div>
         <div
-          v-if="hasAppendData"
+          v-if="$slots.append"
           class="va-input-wrapper__append-inner">
           <slot name="append"/>
         </div>
       </div>
-      <div class="va-input-wrapper__details py-0 px-2">
-        <div class="va-input-wrapper__messages">
-          <div
-            v-if="error"
-            :style="messageStyles"
-            class="va-input-wrapper__messages__wrapper">
-            <template
-              v-for="errorMessage in errorMessages">
-              {{ errorMessage }}
-            </template>
-          </div>
-          <div
-            v-else
-            :style="messageStyles"
-            class="va-input-wrapper__messages__wrapper">
-            <template v-for="message in messages">
-              {{ message }}
-            </template>
-          </div>
-        </div>
-      </div>
     </div>
   </div>
 </template>
 
 <script>
-import { getHoverColor } from './../../../services/color-functions'
+import VaMessageList from './VaMessageList'
 
 export default {
   name: 'va-input-wrapper',
+  components: { VaMessageList },
   props: {
-    disabled: {
-      type: Boolean,
-    },
-    error: {
-      type: Boolean,
-    },
-    success: {
-      type: Boolean,
-    },
+    disabled: Boolean,
+    error: Boolean,
+    success: Boolean,
     messages: {
       type: Array,
       default: () => [],
@@ -71,79 +47,71 @@ export default {
       type: Array,
       default: () => [],
     },
+    errorCount: {
+      type: Number,
+      default: 1,
+    },
   },
   computed: {
-    slotStyles () {
-      return {
-        backgroundColor: this.error ? getHoverColor(this.$themes['danger']) : this.success ? getHoverColor(this.$themes['success']) : '#f5f8f9',
-        borderColor: this.error ? this.$themes.danger : this.success ? this.$themes.success : this.$themes.gray,
-      }
-    },
-    messageStyles () {
-      return {
-        color: this.error ? this.$themes.danger : '#babfc2',
-      }
-    },
-    hasPrependData () {
-      return this.$slots.prepend
+    messagesComputed () {
+      return this.error ? this.errorMessages : this.messages
     },
-    hasAppendData () {
-      return this.$slots.append
+    messagesColor () {
+      return (this.error && 'danger') || (this.success && 'success') || ''
     },
   },
 }
 </script>
 
 <style lang='scss'>
-  @import '../../vuestic-sass/resources/resources';
-  .va-input-wrapper {
+@import '../../vuestic-sass/resources/resources';
+
+.va-input-wrapper {
+  display: flex;
+  flex: 1 1 auto;
+  align-items: flex-end;
+  font-size: 1rem;
+  text-align: left;
+  margin-bottom: 1rem;
+
+  &__control, &__content {
+    width: 100%;
+  }
+
+  &__content {
     display: flex;
-    flex: 1 1 auto;
-    align-items: flex-end;
-    font-size: 1rem;
-    text-align: left;
-    &--focused {
-      .va-input-wrapper__slot {
-        border-color: $charcoal !important;
-      }
-    }
-    &--disabled {
-      .va-input-wrapper__slot {
-        border-color: $brand-secondary !important;
-      }
-    }
-    &__control, &__content {
-      width: 100%;
-    }
-    &__content {
-      display: flex;
-      align-items: flex-end;
-    }
-    &__prepend-inner, &__append-inner {
-      display: inline-flex;
-      align-items: center;
-    }
-    &__prepend-inner {
-      margin-left: 0.5rem;
-    }
-    &__append-inner {
-      margin-right: 0.5rem;
-    }
-    &__slot {
-      display: flex;
-      position: relative;
-      min-height: 2.375rem;
-      border-style: solid;
-      border-width: 0 0 thin 0;
-      border-top-left-radius: 0.5rem;
-      border-top-right-radius: 0.5rem;
-      outline: none;
-    }
-    &__details {
-      padding: 0 0.5rem;
-    }
-    &__messages__wrapper {
-      font-size: 0.875rem;
-    }
+    flex-direction: column;
   }
+
+  &__prepend-inner, &__append-inner {
+    display: inline-flex;
+    align-items: center;
+  }
+
+  &__prepend-inner {
+    margin-right: 0.5rem;
+  }
+
+  &__append-inner {
+    margin-left: 0.5rem;
+  }
+
+  &__slot {
+    display: flex;
+    position: relative;
+    outline: none;
+  }
+
+  &__details {
+    padding: 0 0.5rem;
+    width: 100%;
+  }
+
+  &__messages__wrapper {
+    font-size: 0.875rem;
+  }
+  .va-select {
+    margin-bottom: 0;
+  }
+}
 </style>
diff --git a/src/vuestic-theme/vuestic-components/va-input/VaMessageList.demo.vue b/src/vuestic-theme/vuestic-components/va-input/VaMessageList.demo.vue
new file mode 100644
index 0000000..9743f2a
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-input/VaMessageList.demo.vue
@@ -0,0 +1,32 @@
+<template>
+  <VbDemo>
+    <VbCard title="Default">
+      <VaMessageList :value="stringMessage"/>
+    </VbCard>
+    <VbCard title="Message array">
+      <VaMessageList :limit="3" :value="stringMessages"/>
+    </VbCard>
+    <VbCard title="Error">
+      <VaMessageList color="danger" :value="stringMessages"/>
+    </VbCard>
+    <VbCard title="Success">
+      <VaMessageList color="success" :value="stringMessages"/>
+    </VbCard>
+  </VbDemo>
+</template>
+
+<script>
+import VaMessageList from './VaMessageList.vue'
+
+export default {
+  components: {
+    VaMessageList,
+  },
+  data () {
+    return {
+      stringMessage: 'String message',
+      stringMessages: ['Message', 'Another message', 'Long long long long long long long long long long long long long long error message'],
+    }
+  },
+}
+</script>
diff --git a/src/vuestic-theme/vuestic-components/va-input/VaMessageList.vue b/src/vuestic-theme/vuestic-components/va-input/VaMessageList.vue
new file mode 100644
index 0000000..a9b29a2
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-input/VaMessageList.vue
@@ -0,0 +1,60 @@
+<template>
+  <div
+    v-if="messages.length"
+    class="va-message-list"
+    :style="computedStyle"
+  >
+    <div
+      class="va-message-list__message"
+      v-for="(message, index) in messages"
+      :key="index"
+    >
+      {{message}}
+    </div>
+  </div>
+</template>
+
+<script>
+import { ColorThemeMixin } from '../../../services/ColorThemePlugin'
+
+export default {
+  name: 'va-message-list',
+  mixins: [ColorThemeMixin],
+  data () {
+    return {
+      colorThemeDefault: 'gray', // mixin override
+    }
+  },
+  props: {
+    value: {},
+    limit: { type: Number, default: 1 },
+  },
+  computed: {
+    messages () {
+      if (!this.value) {
+        return []
+      }
+      if (!Array.isArray(this.value)) {
+        return [this.value]
+      }
+      return this.value.slice(0, this.limit)
+    },
+    computedStyle () {
+      return {
+        color: this.colorComputed,
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss">
+@import "../../vuestic-sass/resources/resources";
+
+.va-message-list {
+  &__message {
+    vertical-align: middle;
+    font-size: $font-size-mini;
+  }
+}
+</style>
diff --git a/src/vuestic-theme/vuestic-components/va-input/message-list-docs.md b/src/vuestic-theme/vuestic-components/va-input/message-list-docs.md
new file mode 100644
index 0000000..1b5e46f
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-input/message-list-docs.md
@@ -0,0 +1,7 @@
+## Gist 
+
+Message list is intended as internal component for displaying consistently list of messages.
+
+It is used in various input components to show descriptions and error messages.
+
+This component can be used as standalone in form component to show form-specific messages.
-- 
GitLab


From 0dfab0db6caec6e929e0ca58a71228d5f3f286b1 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 4 Jul 2019 16:18:38 +0300
Subject: [PATCH 05/61] feat: upload config form

---
 .../configSettings/forms/UploadForm.vue       | 49 +++++++++++++------
 src/entities/settings/UploadConfig.ts         | 14 ++++++
 2 files changed, 48 insertions(+), 15 deletions(-)
 create mode 100644 src/entities/settings/UploadConfig.ts

diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index 1221604..b62712b 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -1,28 +1,47 @@
 <template>
-  <div class="upload">
-    <va-select v-model="uploader" :options="uploaderOptions" label="uploader"/>
-    <template v-if="uploader === 'Local'">
-      <va-input v-model="uploads" label="uploads"/>
-    </template>
-    <va-select v-model="filters" :options="filterOptions" multiple label="filters"/>
-    <va-checkbox label="Link name" v-model="link_name"/>
-    <p>When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe</p>
+  <div>
+    <va-select v-model="formData.uploader" :options="selectOptions.uploader" label="uploader"/>
+    <div v-if="formData.uploader === 'Local'" class="mx-4">
+      <va-input v-model="formData.uploads" label="uploads"/>
+    </div>
+    <div v-if="formData.uploader === 'S3'" class="mx-4 my-3">
+      <va-input v-model="formData.bucket" label="S3 bucket name"/>
+      <va-input v-model="formData.public_endpoint" label="S3 endpoint that the user finally accesses"/>
+      <va-input v-model="formData.truncated_namespace" label="truncated namespace"/>
+      <p class="note">If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
+        For example, when using CDN to S3 virtual host format, set "".
+        At this time, write CNAME to CDN in public_endpoint.</p>
+    </div>
+    <va-select v-model="formData.filters" :options="selectOptions.filter" multiple label="filters"/>
+    <div v-if="formData.filters.includes('Pleroma.Upload.Filter.Mogrify')" class="mx-4 my-3">
+      <va-select v-model="formData.args" :options="selectOptions.args" multiple label="List of actions for the mogrify command"/>
+    </div>
+    <div v-if="formData.filters.includes('Pleroma.Upload.Filter.AnonymizeFilename')" class="mx-4 my-3">
+      <va-input v-model="formData.text" label="Anonymize filename"/>
+      <p class="note">Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.</p>
+      <p class="note">Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.</p>
+    </div>
+    <va-checkbox label="Link name" v-model="formData.link_name"/>
+    <p class="note">When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe</p>
+    <va-input v-model="formData.base_url" label="base URL"/>
+    <va-checkbox v-model="formData.proxy_remote" label="Proxy remote"/>
   </div>
 </template>
 
 <script lang="ts">
 import { Component, Vue } from 'vue-property-decorator'
+import UploadConfig from '../../../entities/settings/UploadConfig'
 
 @Component({
-  components: { },
+  components: {},
 })
 export default class UploadForm extends Vue {
-  uploader = ''
-  uploaderOptions = ['Local', 'S3']
-  uploads = ''
-  filters = []
-  link_name = false
-  filterOptions = ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename']
+  formData:UploadConfig = new UploadConfig()
+  selectOptions = {
+    uploader: ['Local', 'S3'],
+    filter: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
+    args: ['strip', 'auto-orient', `{'impode': '1'}`]
+  }
 }
 </script>
 
diff --git a/src/entities/settings/UploadConfig.ts b/src/entities/settings/UploadConfig.ts
new file mode 100644
index 0000000..814a74a
--- /dev/null
+++ b/src/entities/settings/UploadConfig.ts
@@ -0,0 +1,14 @@
+export default class SettingsConfig {
+  uploader: string = 'Pleroma.Uploaders.Local'
+  filters: Array<string> = []
+  upload: string = ''
+  bucket: string = ''
+  public_endpoint: string = ''
+  truncated_namespace
+  args: Array<string> = []
+  text: string = ''
+  link_name: boolean = false
+  base_url: string = ''
+  proxy_remote: boolean = false
+  proxy_opts?: any
+}
-- 
GitLab


From c0681b2fd17583fc338aa0cd7d3383a1b65e9792 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 5 Jul 2019 12:20:11 +0300
Subject: [PATCH 06/61] feat: email adapter components

---
 .../emailAdapters/AmazonSESAdapter.vue        | 41 +++++++++
 .../emailAdapters/DynAdapter.vue              | 31 +++++++
 .../emailAdapters/GmailAdapter.vue            | 31 +++++++
 .../emailAdapters/MailgunAdapter.vue          | 36 ++++++++
 .../emailAdapters/MailjetAdapter.vue          | 36 ++++++++
 .../emailAdapters/MandrillAdapter.vue         | 31 +++++++
 .../emailAdapters/PostmarkAdapter.vue         | 31 +++++++
 .../emailAdapters/SMTPAdapter.vue             | 83 ++++++++++++++++++
 .../emailAdapters/SendgridAdapter.vue         | 31 +++++++
 .../emailAdapters/SendmailAdapter.vue         | 41 +++++++++
 .../emailAdapters/SocketLabsAdapter.vue       | 36 ++++++++
 .../emailAdapters/SparkPostAdapter.vue        | 36 ++++++++
 .../configSettings/forms/EmailsForm.vue       | 85 +++++++++++++++++++
 .../configSettings/ConfigSettingsPage.vue     |  8 +-
 src/entities/settings/EmailsConfig.ts         |  3 +
 .../va-checkbox/VaCheckbox.vue                |  1 +
 16 files changed, 560 insertions(+), 1 deletion(-)
 create mode 100644 src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/DynAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/GmailAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/MailgunAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/MailjetAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/MandrillAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/PostmarkAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/SMTPAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/SendgridAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/SendmailAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
 create mode 100644 src/components/configSettings/emailAdapters/SparkPostAdapter.vue
 create mode 100644 src/components/configSettings/forms/EmailsForm.vue
 create mode 100644 src/entities/settings/EmailsConfig.ts

diff --git a/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue b/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
new file mode 100644
index 0000000..3688714
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
@@ -0,0 +1,41 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">AmazonSES adapter config</p>
+    <va-input
+      v-model="configProxy.region"
+      label="region"
+      @input="(val) => configProxy = {field:'region', val}"
+    />
+    <va-input
+      v-model="configProxy.access_key"
+      @input="(val) => configProxy = {field:'access_key', val}"
+      label="access_key"
+    />
+    <va-input
+      v-model="configProxy.secret"
+      @input="(val) => configProxy = {field:'secret', val}"
+      label="secret"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class AmazonSESAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/DynAdapter.vue b/src/components/configSettings/emailAdapters/DynAdapter.vue
new file mode 100644
index 0000000..c0171d4
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/DynAdapter.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Dyn adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class DynAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/GmailAdapter.vue b/src/components/configSettings/emailAdapters/GmailAdapter.vue
new file mode 100644
index 0000000..76e7dec
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/GmailAdapter.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Gmail adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class GmailAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/MailgunAdapter.vue b/src/components/configSettings/emailAdapters/MailgunAdapter.vue
new file mode 100644
index 0000000..6b82314
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/MailgunAdapter.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Mailgun adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+    <va-input
+      v-model="configProxy.domain"
+      label="domain"
+      @input="(val) => configProxy = {field:'domain', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class MailgunAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/MailjetAdapter.vue b/src/components/configSettings/emailAdapters/MailjetAdapter.vue
new file mode 100644
index 0000000..7423f64
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/MailjetAdapter.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Mailjet adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+    <va-input
+      v-model="configProxy.secret"
+      label="secret"
+      @input="(val) => configProxy = {field:'secret', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class MailjetAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/MandrillAdapter.vue b/src/components/configSettings/emailAdapters/MandrillAdapter.vue
new file mode 100644
index 0000000..5acbf76
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/MandrillAdapter.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Mandrill adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class MandrillAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/PostmarkAdapter.vue b/src/components/configSettings/emailAdapters/PostmarkAdapter.vue
new file mode 100644
index 0000000..a207e3c
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/PostmarkAdapter.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Postmark adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class PostmarkAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/SMTPAdapter.vue b/src/components/configSettings/emailAdapters/SMTPAdapter.vue
new file mode 100644
index 0000000..6eefee9
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/SMTPAdapter.vue
@@ -0,0 +1,83 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">SMTP adapter config</p>
+    <va-input
+      v-model="configProxy.relay"
+      label="relay"
+      @input="(val) => configProxy = {field:'relay', val}"
+    />
+    <va-input
+      v-model="configProxy.username"
+      @input="(val) => configProxy = {field:'username', val}"
+      label="username"
+    />
+    <va-input
+      v-model="configProxy.password"
+      @input="(val) => configProxy = {field:'password', val}"
+      label="password"
+      type="password"
+    />
+    <va-input-wrapper>
+      <va-checkbox
+        v-model="configProxy.ssl"
+        @input="(val) => configProxy = {field:'ssl', val}"
+        label="ssl"
+      />
+    </va-input-wrapper>
+    <va-input
+      v-model="configProxy.tls"
+      @input="(val) => configProxy = { field: 'tls', val }"
+      label="tls"
+    />
+    <va-input
+      v-model="configProxy.auth"
+      @input="(val) => configProxy = { field: 'auth', val }"
+      label="auth"
+    />
+    <va-input
+      v-model.number="configProxy.port"
+      type="number"
+      @input="(val) => configProxy = { field: 'port', val: +val }"
+      label="port"
+    />
+    <va-input
+      v-model="configProxy.dkim"
+      @input="(val) => configProxy = { field: 'dkim', val }"
+      label="dkim"
+    />
+    <va-input
+      v-model.number="configProxy.retries"
+      type="number"
+      @input="(val) => configProxy = { field: 'retries', val: +val }"
+      label="retries"
+    />
+    <va-input-wrapper>
+      <va-checkbox
+        v-model="configProxy.no_mx_lookups"
+        @input="(val) => configProxy = { field: 'no_mx_lookups', val }"
+        label="no_mx_lookups"
+      />
+    </va-input-wrapper>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class SMTPAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/SendgridAdapter.vue b/src/components/configSettings/emailAdapters/SendgridAdapter.vue
new file mode 100644
index 0000000..7b20f82
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/SendgridAdapter.vue
@@ -0,0 +1,31 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Sendgrid adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class SendgridAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/SendmailAdapter.vue b/src/components/configSettings/emailAdapters/SendmailAdapter.vue
new file mode 100644
index 0000000..946e0e0
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/SendmailAdapter.vue
@@ -0,0 +1,41 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">Sendmail adapter config</p>
+    <va-input
+      v-model="configProxy.cmd_path"
+      label="cmd_path"
+      @input="(val) => configProxy = {field:'cmd_path', val}"
+    />
+    <va-input
+      v-model="configProxy.cmd_args"
+      label="cmd_args"
+      @input="(val) => configProxy = {field:'cmd_args', val}"
+    />
+    <va-checkbox
+      v-model="configProxy.qmail"
+      label="qmail"
+      @input="(val) => configProxy = {field: 'qmail', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class SendmailAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue b/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
new file mode 100644
index 0000000..e482cae
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">SocketLabs adapter config</p>
+    <va-input
+      v-model="configProxy.server_id"
+      label="server_id"
+      @input="(val) => configProxy = {field:'server_id', val}"
+    />
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class SocketLabsAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/emailAdapters/SparkPostAdapter.vue b/src/components/configSettings/emailAdapters/SparkPostAdapter.vue
new file mode 100644
index 0000000..1df8cf3
--- /dev/null
+++ b/src/components/configSettings/emailAdapters/SparkPostAdapter.vue
@@ -0,0 +1,36 @@
+<template>
+  <div class="mx-4 my-3">
+    <p class="title">SparkPost adapter config</p>
+    <va-input
+      v-model="configProxy.api_key"
+      label="api_key"
+      @input="(val) => configProxy = {field:'api_key', val}"
+    />
+    <va-input
+      v-model="configProxy.endpoint"
+      label="endpoint"
+      @input="(val) => configProxy = {field:'endpoint', val}"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+
+@Component({
+  components: {},
+})
+export default class SparkPostAdapter extends Vue {
+  @Prop(Object) config!: object
+  get configProxy () {
+    return this.config
+  }
+  set configProxy (value) {
+    this.$emit('configChanged', value)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/forms/EmailsForm.vue b/src/components/configSettings/forms/EmailsForm.vue
new file mode 100644
index 0000000..d2ec987
--- /dev/null
+++ b/src/components/configSettings/forms/EmailsForm.vue
@@ -0,0 +1,85 @@
+<template>
+  <div>
+    <va-select v-model="formData.adapter" label="Adapter" :options="selectOptions.adapter" keyBy="value"/>
+    <component
+      :is="adapterComponent"
+      v-if="isAdapterComponentShouldBeRendered"
+      @configChanged="configChanged"
+      :config="adapterData"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Vue, Watch } from 'vue-property-decorator'
+import _ from 'lodash'
+import EmailsConfig from '../../../entities/settings/EmailsConfig'
+import SMTPAdapter from '../emailAdapters/SMTPAdapter'
+import SendgridAdapter from '../emailAdapters/SendgridAdapter'
+import SendmailAdapter from '../emailAdapters/SendmailAdapter'
+import MandrillAdapter from '../emailAdapters/MandrillAdapter'
+import MailgunAdapter from '../emailAdapters/MailgunAdapter'
+import MailjetAdapter from '../emailAdapters/MailjetAdapter'
+import PostmarkAdapter from '../emailAdapters/PostmarkAdapter'
+import SparkPostAdapter from '../emailAdapters/SparkPostAdapter'
+import DynAdapter from '../emailAdapters/DynAdapter'
+import SocketLabsAdapter from '../emailAdapters/SocketLabsAdapter'
+import GmailAdapter from '../emailAdapters/GmailAdapter'
+import AmazonSESAdapter from '../emailAdapters/AmazonSESAdapter'
+
+@Component({
+  components: {
+    SMTPAdapter,
+    SendgridAdapter,
+    SendmailAdapter,
+    MandrillAdapter,
+    MailgunAdapter,
+    MailjetAdapter,
+    PostmarkAdapter,
+    SparkPostAdapter,
+    DynAdapter,
+    SocketLabsAdapter,
+    GmailAdapter,
+    AmazonSESAdapter,
+  },
+})
+export default class EmailsForm extends Vue {
+  formData:EmailsConfig = new EmailsConfig()
+  adapterData = {}
+  selectOptions = {
+    adapter: [
+      { text: 'SMTP', value: 'Swoosh.Adapters.SMTP' },
+      { text: 'Sendgrid', value: 'Swoosh.Adapters.Sendgrid' },
+      { text: 'Sendmail', value: 'Swoosh.Adapters.Sendmail' },
+      { text: 'Mandrill', value: 'Swoosh.Adapters.Mandrill' },
+      { text: 'Mailgun', value: 'Swoosh.Adapters.Mailgun' },
+      { text: 'Mailjet', value: 'Swoosh.Adapters.Mailjet' },
+      { text: 'Postmark', value: 'Swoosh.Adapters.Postmark' },
+      { text: 'SparkPost', value: 'Swoosh.Adapters.SparkPost' },
+      { text: 'Amazon SES', value: 'Swoosh.Adapters.AmazonSES' },
+      { text: 'Dyn', value: 'Swoosh.Adapters.Dyn' },
+      { text: 'SocketLabs', value: 'Swoosh.Adapters.SocketLabs' },
+      { text: 'Gmail', value: 'Swoosh.Adapters.Gmail' },
+    ]
+  }
+  @Watch('formData.adapter')
+  onAdapterDataChanged () {
+    this.adapterData = {}
+  }
+  get isAdapterComponentShouldBeRendered () {
+    return !_.isEmpty(this.formData.adapter)
+  }
+  get adapterComponent () {
+    return this.formData.adapter.value === 'Swoosh.Adapters.AmazonSES'
+      ? 'AmazonSESAdapter'
+      : `${this.formData.adapter.text}Adapter`
+  }
+  configChanged ({ field, val }) {
+    this.adapterData[field] = val
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index ccb0c7d..6b5cb53 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -11,6 +11,7 @@
       </va-tabs>
       <div class="config-settings-page__content pa-4">
         <upload-form v-if="value === 0"/>
+        <emails-form v-if="value === 1"/>
       </div>
     </va-card>
   </div>
@@ -22,9 +23,10 @@ import { FulfillingBouncingCircleSpinner } from 'epic-spinners'
 import { ConfigService } from '../../../services/ConfigService'
 import { configKeys } from '../../../data/Config'
 import UploadForm from '../../configSettings/forms/UploadForm.vue'
+import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
 
 @Component({
-  components: { UploadForm, FulfillingBouncingCircleSpinner },
+  components: { EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
 })
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
@@ -38,5 +40,9 @@ export default class ConfigSettingsPage extends Vue {
 
 <style lang="scss">
   .config-settings-page {
+    .note {
+      font-size: .625rem;
+      opacity: .7;
+    }
   }
 </style>
diff --git a/src/entities/settings/EmailsConfig.ts b/src/entities/settings/EmailsConfig.ts
new file mode 100644
index 0000000..c77b433
--- /dev/null
+++ b/src/entities/settings/EmailsConfig.ts
@@ -0,0 +1,3 @@
+export default class EmailsConfig {
+  adapter: object = {}
+}
diff --git a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
index f247ef1..2694559 100644
--- a/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
+++ b/src/vuestic-theme/vuestic-components/va-checkbox/VaCheckbox.vue
@@ -56,6 +56,7 @@ export default {
     value: {
       type: [Boolean, Array],
       required: true,
+      default: false,
     },
     arrayValue: String,
     indeterminate: Boolean,
-- 
GitLab


From a3bbbb9b115aaefc620ecd21c5224ea508cad5f6 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 5 Jul 2019 12:49:43 +0300
Subject: [PATCH 07/61] fix: ts errors

---
 .../configSettings/forms/EmailsForm.vue       | 24 +++++++++----------
 src/entities/settings/EmailsConfig.ts         |  6 ++++-
 2 files changed, 17 insertions(+), 13 deletions(-)

diff --git a/src/components/configSettings/forms/EmailsForm.vue b/src/components/configSettings/forms/EmailsForm.vue
index d2ec987..81d3f1c 100644
--- a/src/components/configSettings/forms/EmailsForm.vue
+++ b/src/components/configSettings/forms/EmailsForm.vue
@@ -14,18 +14,18 @@
 import { Component, Vue, Watch } from 'vue-property-decorator'
 import _ from 'lodash'
 import EmailsConfig from '../../../entities/settings/EmailsConfig'
-import SMTPAdapter from '../emailAdapters/SMTPAdapter'
-import SendgridAdapter from '../emailAdapters/SendgridAdapter'
-import SendmailAdapter from '../emailAdapters/SendmailAdapter'
-import MandrillAdapter from '../emailAdapters/MandrillAdapter'
-import MailgunAdapter from '../emailAdapters/MailgunAdapter'
-import MailjetAdapter from '../emailAdapters/MailjetAdapter'
-import PostmarkAdapter from '../emailAdapters/PostmarkAdapter'
-import SparkPostAdapter from '../emailAdapters/SparkPostAdapter'
-import DynAdapter from '../emailAdapters/DynAdapter'
-import SocketLabsAdapter from '../emailAdapters/SocketLabsAdapter'
-import GmailAdapter from '../emailAdapters/GmailAdapter'
-import AmazonSESAdapter from '../emailAdapters/AmazonSESAdapter'
+import SMTPAdapter from '../emailAdapters/SMTPAdapter.vue'
+import SendgridAdapter from '../emailAdapters/SendgridAdapter.vue'
+import SendmailAdapter from '../emailAdapters/SendmailAdapter.vue'
+import MandrillAdapter from '../emailAdapters/MandrillAdapter.vue'
+import MailgunAdapter from '../emailAdapters/MailgunAdapter.vue'
+import MailjetAdapter from '../emailAdapters/MailjetAdapter.vue'
+import PostmarkAdapter from '../emailAdapters/PostmarkAdapter.vue'
+import SparkPostAdapter from '../emailAdapters/SparkPostAdapter.vue'
+import DynAdapter from '../emailAdapters/DynAdapter.vue'
+import SocketLabsAdapter from '../emailAdapters/SocketLabsAdapter.vue'
+import GmailAdapter from '../emailAdapters/GmailAdapter.vue'
+import AmazonSESAdapter from '../emailAdapters/AmazonSESAdapter.vue'
 
 @Component({
   components: {
diff --git a/src/entities/settings/EmailsConfig.ts b/src/entities/settings/EmailsConfig.ts
index c77b433..254beb3 100644
--- a/src/entities/settings/EmailsConfig.ts
+++ b/src/entities/settings/EmailsConfig.ts
@@ -1,3 +1,7 @@
+class AdapterOptionObject {
+  text?: string
+  value?: string
+}
 export default class EmailsConfig {
-  adapter: object = {}
+  adapter: AdapterOptionObject = new AdapterOptionObject()
 }
-- 
GitLab


From 212e6fbb0088fe56a8aeb242d9ec935ac7afa8fc Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 5 Jul 2019 18:36:18 +0300
Subject: [PATCH 08/61] feat: instance config

---
 .../configSettings/forms/InstanceForm.vue     | 114 ++++++++
 .../configSettings/forms/UploadForm.vue       |   5 +-
 .../configSettings/ConfigSettingsPage.vue     |  11 +-
 src/data/Config.js                            |   4 +
 src/entities/settings/InstanceConfig.ts       |  40 +++
 src/i18n/en.json                              | 265 ++++++------------
 6 files changed, 245 insertions(+), 194 deletions(-)
 create mode 100644 src/components/configSettings/forms/InstanceForm.vue
 create mode 100644 src/entities/settings/InstanceConfig.ts

diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
new file mode 100644
index 0000000..9715257
--- /dev/null
+++ b/src/components/configSettings/forms/InstanceForm.vue
@@ -0,0 +1,114 @@
+<template>
+  <div>
+    <va-input v-model="formData.name" :label="$t('config_settings.name')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.name_label')}}</p>
+    <va-input v-model="formData.email" :label="$t('config_settings.email')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.email_label')}}</p>
+    <va-input v-model="formData.notify_email" :label="$t('config_settings.notify_email')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.notify_email_label')}}</p>
+    <va-input v-model="formData.description" :label="$t('config_settings.description')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.description_label')}}</p>
+    <va-input v-model.number="formData.limit" type="number" :label="$t('config_settings.limit')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.limit_label')}}</p>
+    <va-input v-model.number="formData.remote_limit" type="number" :label="$t('config_settings.remote_limit')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.remote_limit_label')}}</p>
+    <va-input v-model="formData.upload_limit" :label="$t('config_settings.upload_limit')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.upload_limit_label')}}</p>
+    <va-input v-model="formData.avatar_upload_limit" :label="$t('config_settings.avatar_upload_limit')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.avatar_upload_limit_label')}}</p>
+    <va-input v-model="formData.background_upload_limit" :label="$t('config_settings.background_upload_limit')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.background_upload_limit_label')}}</p>
+    <va-input v-model="formData.banner_upload_limit" :label="$t('config_settings.banner_upload_limit')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.banner_upload_limit_label')}}</p>
+    <va-checkbox v-model="formData.registrations_open" :label="$t('config_settings.registrations_open')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.registrations_open_label')}}</p>
+    <va-checkbox v-model="formData.invites_enabled" :label="$t('config_settings.invites_enabled')"/>
+    <p class="note">{{$t('config_settings.invites_enabled_label')}}</p>
+    <va-checkbox v-model="formData.account_activation_required" :label="$t('config_settings.account_activation_required')"/>
+    <p class="note">{{$t('config_settings.account_activation_required_label')}}</p>
+    <va-checkbox v-model="formData.federating" :label="$t('config_settings.federating')"/>
+    <p class="note">{{$t('config_settings.federating_label')}}</p>
+    <va-input v-model.number="formData.federation_reachability_timeout_days" type="number" :label="$t('config_settings.federation_reachability_timeout_days')"/>
+    <p class="note">{{$t('config_settings.federation_reachability_timeout_days_label')}}</p>
+    <va-checkbox v-model="formData.allow_relay" :label="$t('config_settings.allow_relay_label')"/>
+    <p class="note">{{$t('config_settings.allow_relay_label')}}</p>
+    <va-select v-model="formData.rewrite_policy" :options="selectOptions.rewrite_policy" :label="$t('config_settings.invites_enabled')"/>
+    <p class="note">{{$t('config_settings.rewrite_policy_label')}}</p>
+    <va-checkbox v-model="formData.public" :label="$t('config_settings.public')"/>
+    <p class="note">{{$t('config_settings.public_label')}}</p>
+    <va-input v-model="formData.quarantined_instances" :label="$t('config_settings.quarantined_instances')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.quarantined_instances_label')}}</p>
+    <va-checkbox v-model="formData.managed_config" :label="$t('config_settings.managed_config')"/>
+    <p class="note">{{$t('config_settings.quarantined_instances_label')}}</p>
+    <va-select v-model="formData.allowed_post_formats" :options="selectOptions.allowed_post_formats" :label="$t('config_settings.allowed_post_formats')"/>
+    <p class="note">{{$t('config_settings.allowed_post_formats_label')}}</p>
+    <va-checkbox v-model="formData.mrf_transparency" :label="$t('config_settings.mrf_transparency')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.mrf_transparency_label')}}</p>
+    <va-checkbox v-model="formData.scope_copy" :label="$t('config_settings.scope_copy')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.scope_copy_label')}}</p>
+    <va-input v-model="formData.subject_line_behavior" :label="$t('config_settings.subject_line_behavior')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.subject_line_behavior_label')}}</p>
+    <va-checkbox v-model="formData.always_show_subject_input" :label="$t('config_settings.always_show_subject_input')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.always_show_subject_input_label')}}</p>
+    <va-checkbox v-model="formData.extended_nickname_format" :label="$t('config_settings.extended_nickname_format')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.extended_nickname_format_label')}}</p>
+    <va-input v-model.number="formData.max_pinned_statuses" type="number" :label="$t('config_settings.max_pinned_statuses')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.max_pinned_statuses_label')}}</p>
+    <va-input v-model="formData.autofollowed_nicknames" :label="$t('config_settings.autofollowed_nicknames')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.autofollowed_nicknames_label')}}</p>
+    <va-checkbox v-model="formData.no_attachment_links" :label="$t('config_settings.no_attachment_links')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.no_attachment_links_label')}}</p>
+    <va-input v-model="formData.welcome_message" :label="$t('config_settings.welcome_message')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.welcome_message_label')}}</p>
+    <va-input v-model="formData.welcome_user_nickname" :label="$t('config_settings.welcome_user_nickname')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.welcome_user_nickname_label')}}</p>
+    <va-input v-model.number="formData.max_report_comment_size" :label="$t('config_settings.max_report_comment_size')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.max_report_comment_size_label')}}</p>
+    <va-checkbox v-model="formData.safe_dm_mentions" :label="$t('config_settings.safe_dm_mentions')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.safe_dm_mentions_label')}}</p>
+    <va-checkbox v-model="formData.healthcheck" :label="$t('config_settings.healthcheck')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.healthcheck_label')}}</p>
+    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.remote_post_retention_days')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.remote_post_retention_days_label')}}</p>
+    <va-checkbox v-model="formData.skip_thread_containment" :label="$t('config_settings.skip_thread_containment')" class="mb-0"/>
+    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.remote_post_retention_days')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.remote_post_retention_days_label')}}</p>
+    <va-select v-model="formData.limit_to_local_content" :options="selectOptions.limit_to_local_content" :label="$t('config_settings.limit_to_local_content')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.limit_to_local_content_label')}}</p>
+    <va-checkbox v-model="formData.dynamic_configuration" :label="$t('config_settings.dynamic_configuration')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.dynamic_configuration_label')}}</p>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from 'vue-property-decorator'
+import InstanceConfig from '../../../entities/settings/InstanceConfig'
+
+@Component({
+  components: {}
+})
+
+export default class InstanceForm extends Vue {
+  formData: InstanceConfig = new InstanceConfig()
+  selectOptions = {
+    rewrite_policy: [
+      'Pleroma.Web.ActivityPub.MRF.NoOpPolicy',
+      'Pleroma.Web.ActivityPub.MRF.DropPolicy',
+      'Pleroma.Web.ActivityPub.MRF.SimplePolicy',
+      'Pleroma.Web.ActivityPub.MRF.TagPolicy',
+      'Pleroma.Web.ActivityPub.MRF.SubchainPolicy',
+      'Pleroma.Web.ActivityPub.MRF.RejectNonPublic',
+      'Pleroma.Web.ActivityPub.MRF.EnsureRePrepended',
+      'Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy',
+      'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy'
+    ],
+    allowed_post_formats: [],
+    subject_line_behavior: ['email', 'masto', 'noop'],
+    limit_to_local_content: [':unauthenticated', ':all', 'false']
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index b62712b..a9ede46 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -7,7 +7,7 @@
     <div v-if="formData.uploader === 'S3'" class="mx-4 my-3">
       <va-input v-model="formData.bucket" label="S3 bucket name"/>
       <va-input v-model="formData.public_endpoint" label="S3 endpoint that the user finally accesses"/>
-      <va-input v-model="formData.truncated_namespace" label="truncated namespace"/>
+      <va-input v-model="formData.truncated_namespace" label="truncated namespace" class="mb-0"/>
       <p class="note">If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
         For example, when using CDN to S3 virtual host format, set "".
         At this time, write CNAME to CDN in public_endpoint.</p>
@@ -17,8 +17,7 @@
       <va-select v-model="formData.args" :options="selectOptions.args" multiple label="List of actions for the mogrify command"/>
     </div>
     <div v-if="formData.filters.includes('Pleroma.Upload.Filter.AnonymizeFilename')" class="mx-4 my-3">
-      <va-input v-model="formData.text" label="Anonymize filename"/>
-      <p class="note">Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.</p>
+      <va-input v-model="formData.text" label="Anonymize filename" class="mb-0"/>
       <p class="note">Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.</p>
     </div>
     <va-checkbox label="Link name" v-model="formData.link_name"/>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 6b5cb53..9e4c5ae 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -9,9 +9,10 @@
           {{item.name}}
         </va-tab>
       </va-tabs>
-      <div class="config-settings-page__content pa-4">
-        <upload-form v-if="value === 0"/>
-        <emails-form v-if="value === 1"/>
+      <div class="config-settings-page__content py-4">
+        <upload-form v-if="configKeys[value].key === 'Pleroma.Upload'"/>
+        <emails-form v-if="configKeys[value].key === 'Pleroma.Emails'"/>
+        <instance-config v-if="configKeys[value].key === ':instance'"/>
       </div>
     </va-card>
   </div>
@@ -24,9 +25,10 @@ import { ConfigService } from '../../../services/ConfigService'
 import { configKeys } from '../../../data/Config'
 import UploadForm from '../../configSettings/forms/UploadForm.vue'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
+import InstanceConfig from '../../configSettings/forms/InstanceForm.vue'
 
 @Component({
-  components: { EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
+  components: { InstanceConfig, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
 })
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
@@ -43,6 +45,7 @@ export default class ConfigSettingsPage extends Vue {
     .note {
       font-size: .625rem;
       opacity: .7;
+      margin-bottom: 1rem;
     }
   }
 </style>
diff --git a/src/data/Config.js b/src/data/Config.js
index 4f8cea0..f12caca 100644
--- a/src/data/Config.js
+++ b/src/data/Config.js
@@ -7,6 +7,10 @@ export const configKeys = [
     key: 'Pleroma.Emails',
     name: 'Emails'
   },
+  {
+    key: ':instance',
+    name: 'Instance'
+  },
   {
     key: 'Pleroma.Web',
     name: 'Web'
diff --git a/src/entities/settings/InstanceConfig.ts b/src/entities/settings/InstanceConfig.ts
new file mode 100644
index 0000000..7c1dff6
--- /dev/null
+++ b/src/entities/settings/InstanceConfig.ts
@@ -0,0 +1,40 @@
+export default class InstanceConfig {
+  name: string = ''
+  email: string = ''
+  notify_email: string = ''
+  description: string = ''
+  limit?: number
+  remote_limit?: number
+  upload_limit: string = ''
+  avatar_upload_limit: string = ''
+  background_upload_limit: string = ''
+  banner_upload_limit: string = ''
+  registrations_open: boolean = false
+  invites_enabled: boolean = false
+  account_activation_required: boolean = false
+  federating: boolean = false
+  federation_reachability_timeout_days: number = 1
+  allow_relay: boolean = false
+  rewrite_policy: Array<string> = []
+  public: boolean = false
+  quarantined_instances: string = ''
+  managed_config: boolean = false
+  allowed_post_formats: Array<string> = []
+  mrf_transparency: boolean = false
+  scope_copy: boolean = false
+  subject_line_behavior: string = ''
+  always_show_subject_input: boolean = false
+  extended_nickname_format: boolean = false
+  max_pinned_statuses?: number
+  autofollowed_nicknames: string = ''
+  no_attachment_links: boolean = false
+  welcome_message: string = ''
+  welcome_user_nickname: string = ''
+  max_report_comment_size?: number
+  safe_dm_mentions: boolean = false
+  healthcheck: boolean = false
+  remote_post_retention_days?: number
+  skip_thread_containment: boolean = false
+  limit_to_local_content: string = ':unauthenticated'
+  dynamic_configuration: boolean = false
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6e9ab93..7c72ead 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -19,35 +19,6 @@
   "breadcrumbs": {
     "home": "Home"
   },
-  "buttons": {
-    "advanced": "Buttons With Icons",
-    "size": "Button Sizes",
-    "tags": "Button Tags",
-    "button": "BUTTON",
-    "buttonGroups": "Button Groups",
-    "buttonToggles": "Button Toggles",
-    "pagination": "Pagination",
-    "a-link": "A-LINK",
-    "router-link": "ROUTER-LINK",
-    "colors": "Button Colors",
-    "disabled": "DISABLED",
-    "dropdown": "DROPDOWN",
-    "hover": "HOVER",
-    "types": "Button Types",
-    "pressed": "PRESSED",
-    "default": "DEFAULT",
-    "outline": "OUTLINE",
-    "flat": "FLAT",
-    "large": "LARGE",
-    "small": "SMALL",
-    "normal": "NORMAL",
-    "success": "SUCCESS",
-    "info": "INFO",
-    "danger": "DANGER",
-    "warning": "WARNING",
-    "gray": "GRAY",
-    "dark": "DARK"
-  },
   "charts": {
     "horizontalBarChart": "Horizontal Bar Chart",
     "verticalBarChart": "Vertical Bar Chart",
@@ -56,26 +27,10 @@
     "donutChart": "Donut Chart",
     "bubbleChart": "Bubble Chart"
   },
-  "collapse": {
-    "basic": "Basic Collapse",
-    "collapseWithBackground": "Collapse with background",
-    "collapseWithCustomHeader": "Collapse with custom header"
-  },
   "sliders": {
     "slider": "Sliders",
     "range": "Ranges"
   },
-  "dashboard": {
-    "dataVisualization": "Data Visualization",
-    "success": "SUCCESS",
-    "successMessage": "You successfully read this important alert message.",
-    "elements": "Elements",
-    "features": "Features",
-    "setupProfile": "Setup Profile",
-    "teamMembers": "Team Members",
-    "usersAndMembers": "Users & Members",
-    "versions": "Versions"
-  },
   "notificationsPage": {
     "notifications": {
       "title": "Notifications",
@@ -110,78 +65,6 @@
       "launchToast": "Launch toast"
     }
   },
-  "extra": {
-    "tabs": {
-      "title": "Tabs",
-      "maps": "Maps",
-      "pages": "Pages",
-      "overview": "Overview",
-      "setupProfile": "Setup Profile"
-    },
-    "chat": "Chat",
-    "profileCard": "Profile Card"
-  },
-  "forms": {
-    "controls": {
-      "female": "Female",
-      "male": "Male",
-      "title": "Checkboxes, Radios, Switches",
-      "radioDisabled": "Disabled Radio",
-      "radio": "Radio",
-      "subscribe": "Subscribe to newsletter",
-      "unselected": "Unselected checkbox",
-      "selected": "Selected checkbox",
-      "readonly": "Readonly checkbox",
-      "disabled": "Disabled checkbox",
-      "error": "Checkbox with error",
-      "errorMessage": "Checkbox with error messages"
-    },
-    "dateTimePicker": {
-      "title": "Date time pickers",
-      "basic": "Basic",
-      "time": "Time",
-      "range": "Range",
-      "multiple": "Multiple",
-      "disabled": "Disabled",
-      "customFirstDay": "Custom first day",
-      "customDateFormat": "Custom date format"
-    },
-    "inputs": {
-      "emailValidatedSuccess": "Email (validated with success)",
-      "emailValidated": "Email (validated)",
-      "inputWithIcon": "Input With Icon",
-      "inputWithButton": "Input With Button",
-      "inputWithClearButton": "Input With Clear Button",
-      "inputWithRoundButton": "Input With Round Button",
-      "textInput": "Text Input",
-      "textInputWithDescription": "Text Input (with description)",
-      "textArea": "Text Area",
-      "title": "Inputs",
-      "upload": "UPLOAD"
-    },
-    "mediumEditor": {
-      "title": "Medium Editor"
-    },
-    "selects": {
-      "country": "Country Select",
-      "countryMulti": "Country Multi Select",
-      "multi": "Multi Select",
-      "simple": "Simple Select",
-      "title": "Selects"
-    },
-    "wizard": {
-      "name": "Name",
-      "completed": "Wizard completed!",
-      "confirmSelection": "Confirm selection",
-      "rich": "Rich Wizard",
-      "simple": "Simple Wizard",
-      "stepOne": "Step 1. Name",
-      "stepTwo": "Step 2. Country",
-      "stepThree": "Step 3. Confirm",
-      "verticalRich": "Vertical Rich Wizard",
-      "verticalSimple": "Vertical Simple Wizard"
-    }
-  },
   "grid": {
     "desktop": "Desktop Grid",
     "fixed": "Fixed Grid",
@@ -362,76 +245,84 @@
     "no_followings_yet": "No followings yet",
     "no_statuses": "No more statuses",
     "no_statuses_yet": "No statuses yet",
-    "no_reports": "No more reports",
-    "no_reports_yet": "No reports yet",
     "no_users_found": "No users found",
-    "load_more": "Load more",
-    "admin_menu": {
-      "moderation": "Moderation",
-      "grant_admin": "Grant Admin",
-      "revoke_admin": "Revoke Admin",
-      "grant_moderator": "Grant Moderator",
-      "revoke_moderator": "Revoke Moderator",
-      "activate_account": "Activate account",
-      "activate_accounts": "Activate accounts",
-      "deactivate_account": "Deactivate account",
-      "deactivate_accounts": "Deactivate accounts",
-      "deactivate_your_account": "You are trying to deactivate your account. Please type in the name of your account to confirm",
-      "deactivate_anyway": "Deactivate anyway",
-      "delete_account": "Delete account",
-      "delete_accounts": "Delete accounts",
-      "delete_user": "Delete user",
-      "delete_users": "Delete users",
-      "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone.",
-      "delete_your_account_confirmation": "You are trying to delete YOUR account. Please type in the name of your account to confirm.",
-      "change_tag_confirmation": "Do you want to {action}?"
-    },
-    "tags": {
-      "mrf_tag:media-force-nsfw": "Mark all posts as NSF",
-      "mrf_tag:media-force-nsfw-neg": "Remove NSF mark from posts",
-      "mrf_tag:media-strip": "Remove media from posts",
-      "mrf_tag:media-strip-neg": "Don't remove media from posts",
-      "mrf_tag:force-unlisted": "Force posts to be unlisted",
-      "mrf_tag:force-unlisted-neg": "Force posts to be unlisted",
-      "mrf_tag:disable-remote-subscription": "Disallow following user from remote instances",
-      "mrf_tag:disable-remote-subscription-neg": "Allow following user from remote instances",
-      "mrf_tag:disable-any-subscription":  "Disallow following user at all",
-      "mrf_tag:disable-any-subscription-neg":  "Allow following user at all",
-      "mrf_tag:sandbox": "Force posts to be followers-only",
-      "mrf_tag:sandbox-neg": "Force posts to be followers-only",
-      "mrf_tag:quarantine": "Disallow user posts from federating",
-      "mrf_tag:quarantine-neg": "Allow user posts from federating"
-    },
-    "reports": {
-      "reports": "reports",
-      "note": "Note",
-      "mark_as_resolved": "Mark as resolved",
-      "mark_as_unresolved": "Mark as unresolved",
-      "close": "Close",
-      "reported_account": "Reported account",
-      "reported_by": "Reported by",
-      "reported": "Reported",
-      "updated": "Updated",
-      "status": "Status",
-      "notes": "notes",
-      "reopen_report": "Reopen report",
-      "close_report": "Close report",
-      "action_taken_by": "Action taken by",
-      "assigned_moderator": "Assigned moderator",
-      "assign_to_me": "Assign to me",
-      "reopen_with_note": "Reopen with note",
-      "close_with_note": "Close with note",
-      "respond": "Respond to a report",
-      "statuses_header": "Statuses",
-      "offer_to_add_respond": "Would you like to respond a report?",
-      "statuses": {
-        "open": "Open",
-        "closed": "Closed",
-        "resolved": "Resolved"
-      },
-      "delete_status": "Delete status",
-      "delete_status_message": "Are you absolutely sure? This action cannot be undone.",
-      "add_note_placeholder": "Add note..."
-    }
+    "load_more": "Load more"
+  },
+  "config_settings": {
+    "name": "Name",
+    "name_label": "The instance’s name",
+    "email": "Email",
+    "email_label": "Email used to reach an Administrator/Moderator of the instance",
+    "notify_email": "Notify email",
+    "notify_email_label": "Email used for notifications",
+    "description": "Description",
+    "description_label": "The instance’s description, can be seen in nodeinfo and /api/v1/instance",
+    "limit": "Limit",
+    "limit_label": "Posts character limit (CW/Subject included in the counter)",
+    "remote_limit": "Remote limit",
+    "remote_limit_label": "Hard character limit beyond which remote posts will be dropped.",
+    "upload_limit": "Upload limit",
+    "upload_limit_label": "File size limit of uploads (except for avatar, background, banner)",
+    "avatar_upload_limit": "Avatar upload limit",
+    "avatar_upload_limit_label": "File size limit of user’s profile avatars",
+    "background_upload_limit": "Background upload limit",
+    "background_upload_limit_label": "File size limit of user’s profile backgrounds",
+    "banner_upload_limit": "Banner upload limit",
+    "banner_upload_limit_label": "File size limit of user’s profile banners",
+    "registrations_open": "Registrations open",
+    "registrations_open_label": "Enable registrations for anyone, invitations can be enabled when false.",
+    "invites_enabled": "Invites enabled",
+    "invites_enabled_label": "Enable user invitations for admins (depends on registrations_open: false).",
+    "account_activation_required": "Account activation required",
+    "account_activation_required_label": "Require users to confirm their emails before signing in.",
+    "federating": "Federating",
+    "federating_label": "Enable federation with other instances",
+    "federation_reachability_timeout_days": "Federation reachability timeout days",
+    "federation_reachability_timeout_days_label": "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.",
+    "allow_relay": "Allow relay",
+    "allow_relay_label": "Enable Pleroma’s Relay, which makes it possible to follow a whole instance",
+    "rewrite_policy": "Rewrite policy",
+    "rewrite_policy_label": "Message Rewrite Policy, either one or a list. Here are the ones available by default:",
+    "public": "Public",
+    "public_label": "Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.",
+    "quarantined_instances": "Quarantined instances",
+    "quarantined_instances_label": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send.",
+    "managed_config": "Managed config",
+    "managed_config_label": "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
+    "allowed_post_formats": "Allowed post formats",
+    "allowed_post_formats_label": "MIME-type list of formats allowed to be posted (transformed into HTML)",
+    "mrf_transparency": "mrf transparency",
+    "mrf_transparency_label": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
+    "scope_copy": "Scope copy",
+    "scope_copy_label": "Copy the scope (private/unlisted/public) in replies to posts by default.",
+    "subject_line_behavior": "Subject line behavior",
+    "subject_line_behavior_label": "Allows changing the default behaviour of subject lines in replies",
+    "always_show_subject_input": "Always show subject input",
+    "always_show_subject_input_label": "When set to false, auto-hide the subject field when it's empty.",
+    "extended_nickname_format": "Extended nickname format",
+    "extended_nickname_format_label": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
+    "max_pinned_statuses": "Max pinned status",
+    "max_pinned_statuses_label": "The maximum number of pinned statuses. 0 will disable the feature.",
+    "autofollowed_nicknames": "Autofollowed nicknames",
+    "autofollowed_nicknames_label": "Set to nicknames of (local) users that every new user should automatically follow.",
+    "no_attachment_links": "No attachment links",
+    "no_attachment_links_label": "Set to true to disable automatically adding attachment link text to statuses",
+    "welcome_message": "Welcome message",
+    "welcome_message_label": "A message that will be send to a newly registered users as a direct message.",
+    "welcome_user_nickname": "Welcome user nickname",
+    "welcome_user_nickname_label": "The nickname of the local user that sends the welcome message.",
+    "max_report_comment_size": "Max report comment size",
+    "max_report_comment_size_label": "The maximum size of the report comment (Default: 1000)",
+    "safe_dm_mentions": "Safe dm mentions",
+    "safe_dm_mentions_label": " If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. \"@friend hey i really don't like @enemy\")",
+    "healthcheck": "Healthcheck",
+    "healthcheck_label": "If set to true, system data will be shown on /api/pleroma/healthcheck.",
+    "remote_post_retention_days": "Remote post retention days",
+    "remote_post_retention_days_label": "The default amount of days to retain remote posts when pruning the database.",
+    "skip_thread_containment": "Skip thread containment",
+    "limit_to_local_content": "Limit to local content",
+    "limit_to_local_content_label": "Limit unauthenticated users to search for local statutes and users only.",
+    "dynamic_configuration": "Dynamic configuration",
+    "dynamic_configuration_label": "Allow transferring configuration to DB with the subsequent customization from Admin api."
   }
 }
-- 
GitLab


From 5ec2735f21c9c29a085af6ecb6d18893ba93ee33 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 8 Jul 2019 18:14:02 +0300
Subject: [PATCH 09/61] feat: login refactoring, i18 fixes

---
 npm-shrinkwrap.json                           |   6 +-
 src/components/admin/AdminMenu.vue            |  16 +-
 src/components/admin/UserActionsDropdown.vue  |  12 +-
 src/components/admin/UsersAdminPanel.vue      |  14 +-
 src/components/auth/login/AcceptOauthCode.vue |  51 -----
 src/components/auth/login/Login.vue           |  35 +++-
 src/components/followers/FollowersList.vue    |   2 +-
 .../configSettings/ConfigSettingsPage.vue     |   8 +-
 src/components/reports/Report.vue             |  30 +--
 src/components/reports/ReportGroup.vue        |   2 +-
 src/i18n/en.json                              | 187 ++++++++++++++++++
 src/router/router.js                          |   5 -
 src/services/ApiService.ts                    |  26 +--
 vue.config.js                                 |   2 +-
 14 files changed, 273 insertions(+), 123 deletions(-)
 delete mode 100644 src/components/auth/login/AcceptOauthCode.vue

diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 3c23051..7e388e0 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -16621,9 +16621,9 @@
       "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
     },
     "vue-book": {
-      "version": "0.1.0-alpha.14",
-      "resolved": "https://registry.npmjs.org/vue-book/-/vue-book-0.1.0-alpha.14.tgz",
-      "integrity": "sha512-I3r0iK9oNMab44Ry0ZCKqkkfoQNcYh1nmKxfktBORq8VvrB/ODx2D49P8h98jMCdI5NCdw3H6U5y8Vc4bTsW7g=="
+      "version": "0.1.0-alpha.17",
+      "resolved": "https://registry.npmjs.org/vue-book/-/vue-book-0.1.0-alpha.17.tgz",
+      "integrity": "sha512-8BpuuImZfM3dZ1zsTdBA0fxpcbZ3VJ9JsPrNVVDqx0Gn9H5qgZXDGKmLnN4kSfXO5CZjDPns0Sj+ZeofdWt+Dg=="
     },
     "vue-bulma-expanding": {
       "version": "0.0.1",
diff --git a/src/components/admin/AdminMenu.vue b/src/components/admin/AdminMenu.vue
index 53fafe1..4d4d1f7 100644
--- a/src/components/admin/AdminMenu.vue
+++ b/src/components/admin/AdminMenu.vue
@@ -1,10 +1,10 @@
 <template>
   <va-card class="admin-menu">
     <div class="va-row mb-3 align--center">
-      <va-button @click="togglePermissionGroup(availablePermissionGroups.admin)" class="ml-0">{{$t(isAdmin ? 'pleroma.admin_menu.revoke_admin' : 'pleroma.admin_menu.grant_admin')}}</va-button>
-      <va-button @click="togglePermissionGroup(availablePermissionGroups.moderator)">{{$t(isModerator ? 'pleroma.admin_menu.revoke_moderator' : 'pleroma.admin_menu.grant_moderator')}}</va-button>
-      <va-button @click="toggleUserActivation()">{{$t(deactivated ? 'pleroma.admin_menu.activate_account' : 'pleroma.admin_menu.deactivate_account')}}</va-button>
-      <va-button color="danger" @click="confirmDeleteAccount = true" class="admin-menu__delete-button ml-5">{{$t('pleroma.admin_menu.delete_account')}}</va-button>
+      <va-button @click="togglePermissionGroup(availablePermissionGroups.admin)" class="ml-0">{{$t(isAdmin ? 'admin_menu.revoke_admin' : 'admin_menu.grant_admin')}}</va-button>
+      <va-button @click="togglePermissionGroup(availablePermissionGroups.moderator)">{{$t(isModerator ? 'admin_menu.revoke_moderator' : 'admin_menu.grant_moderator')}}</va-button>
+      <va-button @click="toggleUserActivation()">{{$t(deactivated ? 'admin_menu.activate_account' : 'admin_menu.deactivate_account')}}</va-button>
+      <va-button color="danger" @click="confirmDeleteAccount = true" class="admin-menu__delete-button ml-5">{{$t('admin_menu.delete_account')}}</va-button>
     </div>
     <div class="content admin-menu__table va-row">
       <div
@@ -13,7 +13,7 @@
         style="width: 50%"
         @click="showActionConfirmationDialog(tag)"
         class="my-2 pointer link-secondary flex"
-      >{{$t(userOptions[tag] ? `pleroma.tags.${tag}-neg`: `pleroma.tags.${tag}`)}}</div>
+      >{{$t(userOptions[tag] ? `tags.${tag}-neg`: `tags.${tag}`)}}</div>
       <div class="flex-center admin-menu__loading mb-3" v-if="loading">
         <fulfilling-bouncing-circle-spinner
           :animation-duration="2500"
@@ -24,15 +24,15 @@
     </div>
     <va-modal
       v-model="confirmAction"
-      :message="$t('pleroma.admin_menu.change_tag_confirmation', {action: $t(userOptions[activeTag] ? `pleroma.tags.${activeTag}-neg` : `pleroma.tags.${activeTag}`)})"
+      :message="$t('pleroma.admin_menu.change_tag_confirmation', {action: $t(userOptions[activeTag] ? `tags.${activeTag}-neg` : `tags.${activeTag}`)})"
       @ok="toggleTag(activeTag)"
       @cancel="this.activeTag = null"
     />
     <va-modal
       v-model="confirmDeleteAccount"
-      :title="`${$t('pleroma.admin_menu.delete_user')} ${username}`"
+      :title="`${$t('admin_menu.delete_user')} ${username}`"
       position="center"
-      :message="$t('pleroma.admin_menu.delete_user_confirmation')"
+      :message="$t('admin_menu.delete_user_confirmation')"
       :okText="$t('modal.cancel')"
       :cancelText="$t('modal.delete')"
       @cancel="deleteUser"
diff --git a/src/components/admin/UserActionsDropdown.vue b/src/components/admin/UserActionsDropdown.vue
index c6a7fc9..a263937 100644
--- a/src/components/admin/UserActionsDropdown.vue
+++ b/src/components/admin/UserActionsDropdown.vue
@@ -3,13 +3,13 @@
     <div>
       <va-toggle
         v-model="isAdmin"
-        :label="$t(isAdmin ? 'Admin' : 'pleroma.admin_menu.grant_admin')"
+        :label="$t(isAdmin ? 'Admin' : 'admin_menu.grant_admin')"
         small
         @input="togglePermissionGroup(availablePermissionGroups.admin)"
       />
       <va-toggle
         v-model="isModerator"
-        :label="$t(isModerator ? 'Moderator' : 'pleroma.admin_menu.grant_moderator')"
+        :label="$t(isModerator ? 'Moderator' : 'admin_menu.grant_moderator')"
         small
         @input="togglePermissionGroup(availablePermissionGroups.moderator)"
       />
@@ -35,7 +35,7 @@
               :key="tag"
               @click="toggleTag(tag)"
               class="my-2 pointer link-secondary flex user-actions-panel__tag"
-            >{{$t(userOptions[tag] ? `pleroma.tags.${tag}-neg` : `pleroma.tags.${tag}`)}}</div>
+            >{{$t(userOptions[tag] ? `tags.${tag}-neg` : `tags.${tag}`)}}</div>
           </div>
           <div class="flex-center admin-menu__loading mb-3" v-if="loading">
             <fulfilling-bouncing-circle-spinner
@@ -49,9 +49,9 @@
     </div>
     <va-modal
       v-model="confirmDeleteAccount"
-      :title="`${$t('pleroma.admin_menu.delete_user')} ${simplifiedUser.nickname}`"
+      :title="`${$t('admin_menu.delete_user')} ${simplifiedUser.nickname}`"
       position="center"
-      :message="isYourAccount ?undefined :$t('pleroma.admin_menu.delete_user_confirmation')"
+      :message="isYourAccount ?undefined :$t('admin_menu.delete_user_confirmation')"
       :okText="$t('modal.cancel')"
       :cancelText="$t('modal.delete')"
       @cancel="deleteUser"
@@ -60,7 +60,7 @@
       size="small"
     >
       <div v-show="isYourAccount">
-        <p class="mb-2">{{$t('pleroma.admin_menu.delete_your_account_confirmation')}}</p>
+        <p class="mb-2">{{$t('admin_menu.delete_your_account_confirmation')}}</p>
         <va-input v-model="yourName" class="mb-2"/>
         <div class="flex justify--space-around va-row">
           <va-button :disabled="!checkDeleteConfirmation" @click="deleteUser">{{$t('modal.delete')}}</va-button>
diff --git a/src/components/admin/UsersAdminPanel.vue b/src/components/admin/UsersAdminPanel.vue
index fd4b742..1336b9d 100644
--- a/src/components/admin/UsersAdminPanel.vue
+++ b/src/components/admin/UsersAdminPanel.vue
@@ -10,7 +10,7 @@
       small
       flat
     >
-      {{$t(isAdmin ? 'pleroma.admin_menu.revoke_admin' : 'pleroma.admin_menu.grant_admin')}}
+      {{$t(isAdmin ? 'admin_menu.revoke_admin' : 'admin_menu.grant_admin')}}
     </va-button>
     <va-button
       color="info"
@@ -20,7 +20,7 @@
       small
       flat
     >
-      {{$t(isModerator ? 'pleroma.admin_menu.revoke_moderator' : 'pleroma.admin_menu.grant_moderator')}}
+      {{$t(isModerator ? 'admin_menu.revoke_moderator' : 'admin_menu.grant_moderator')}}
     </va-button>
     <va-button
       color="info"
@@ -30,7 +30,7 @@
       small
       flat
     >
-      {{$t(isDeativated ? 'pleroma.admin_menu.activate_accounts' : 'pleroma.admin_menu.deactivate_accounts')}}
+      {{$t(isDeativated ? 'admin_menu.activate_accounts' : 'admin_menu.deactivate_accounts')}}
     </va-button>
     <va-button
       color="danger"
@@ -40,7 +40,7 @@
       :disabled="isDisabled"
       flat
     >
-      {{$t('pleroma.admin_menu.delete_accounts')}}
+      {{$t('admin_menu.delete_accounts')}}
     </va-button>
     <va-modal
       v-model="confirmDeleteAccount"
@@ -56,7 +56,7 @@
       size="small"
     >
       <div v-show="confirmDeleteYourAccount">
-        <p class="mb-2">{{$t('pleroma.admin_menu.delete_your_account_confirmation')}}</p>
+        <p class="mb-2">{{$t('admin_menu.delete_your_account_confirmation')}}</p>
         <va-input v-model="yourName" class="mb-2"/>
         <div class="flex justify--space-around va-row">
           <va-button :disabled="!checkDeleteConfirmation" @click="deleteYourAccount">Delete anyway</va-button>
@@ -73,10 +73,10 @@
       size="small"
     >
       <div>
-        <p class="mb-2">{{$t('pleroma.admin_menu.deactivate_your_account')}}</p>
+        <p class="mb-2">{{$t('admin_menu.deactivate_your_account')}}</p>
         <va-input v-model="yourName" class="mb-2"/>
         <div class="flex justify--space-around va-row">
-          <va-button :disabled="!checkDeleteConfirmation" @click="toggleUserActivation">{{$t('pleroma.admin_menu.deactivate_anyway')}}</va-button>
+          <va-button :disabled="!checkDeleteConfirmation" @click="toggleUserActivation">{{$t('admin_menu.deactivate_anyway')}}</va-button>
           <va-button @click="resetActivationConfirmation" color="gray" flat>{{$t('modal.cancel')}}</va-button>
         </div>
       </div>
diff --git a/src/components/auth/login/AcceptOauthCode.vue b/src/components/auth/login/AcceptOauthCode.vue
deleted file mode 100644
index b919694..0000000
--- a/src/components/auth/login/AcceptOauthCode.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<template>
-  <div class="AcceptOauthCode">
-    <va-pre-loader/>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
-import { getByKey, setByKey } from './storage'
-import { ApiService } from '../../../services/ApiService'
-import { isEmpty } from 'lodash'
-
-@Component({
-  async created () {
-    const code = this.$route.query.code
-
-    if (isEmpty(code)) {
-      this.$router.push({ name: 'login' })
-    }
-
-    const apiService = new ApiService({
-      username: getByKey('username'),
-      instance: getByKey('instance'),
-    })
-
-    const token = await apiService.getOauthToken({
-      client_id: getByKey('client_id'),
-      client_secret: getByKey('client_secret'),
-      code,
-    })
-
-    setByKey('oauth_token', token)
-    setTimeout(() => {
-      this.$router.push('/')
-    }, 1000)
-  },
-})
-export default class AcceptOauthCode extends Vue {
-
-}
-</script>
-
-<style lang="scss">
-.AcceptOauthCode {
-  @include media-breakpoint-down(sm) {
-    .va-pre-loader {
-      margin-top: 4rem;
-    }
-  }
-}
-</style>
diff --git a/src/components/auth/login/Login.vue b/src/components/auth/login/Login.vue
index eea5d0b..d060f17 100644
--- a/src/components/auth/login/Login.vue
+++ b/src/components/auth/login/Login.vue
@@ -2,7 +2,7 @@
   <va-card class="login" title="To have an access to EPIC PLEROMA FE, please login">
     <form @submit.prevent="authorize">
       <va-icon-vuestic/>
-      <div class="va-row mb-4">
+      <div class="va-row">
         <va-input
           v-model="usernameAndInstance"
           type="email"
@@ -11,10 +11,18 @@
           :error-messages="[error]"
         />
       </div>
+      <div class="va-row mb-2">
+        <va-input
+          v-model="password"
+          type="password"
+          :label="$t('pleroma.password-placeholder')"
+        />
+      </div>
       <div class="va-row justify--center">
         <va-button type="submit" class="my-0">{{ $t('auth.login') }}</va-button>
       </div>
     </form>
+    <va-pre-loader class="login__preloader" v-if="loading"/>
   </va-card>
 </template>
 
@@ -29,11 +37,14 @@ export default {
   data () {
     return {
       usernameAndInstance: '',
+      password: '',
       error: '',
+      loading: false,
     }
   },
   methods: {
     async authorize () {
+      this.loading = true
       const parsedUsernameAndInstance = this.usernameAndInstance.split('@')
 
       if (parsedUsernameAndInstance.length !== 2) {
@@ -47,15 +58,15 @@ export default {
       setByKey('username', username)
 
       const service = new ApiService({ instance, username })
-
-      // const response = await service.checkAdmin()
-
       const app = await service.createApp()
 
       setByKey('client_id', app.client_id)
       setByKey('client_secret', app.client_secret)
+      const token = await service.getOauthToken(app, username, this.password)
 
-      window.location.href = service.getFullOauthUrl(app)
+      setByKey('oauth_token', token)
+      this.$router.push({ name: 'users' })
+      this.loading = false
     }
   }
 }
@@ -68,6 +79,20 @@ export default {
   .va-icon-vuestic {
     display: none;
   }
+  &__preloader {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    height: 100%;
+    margin: auto;
+    width: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    left: 0;
+    right: 0;
+    background: rgba(0, 0, 0, 0.3);
+  }
   @include media-breakpoint-down(sm) {
     width: 100%;
     .va-icon-vuestic {
diff --git a/src/components/followers/FollowersList.vue b/src/components/followers/FollowersList.vue
index ecdc4bd..a5c26b2 100644
--- a/src/components/followers/FollowersList.vue
+++ b/src/components/followers/FollowersList.vue
@@ -16,7 +16,7 @@
           :size="20"
           color="#fff"
         />
-        <span v-else>{{$t('pleroma.load_more')}}</span>
+        <span v-else>{{$t('load_more')}}</span>
       </va-button>
       <div v-else-if="followersList.length">{{$t('pleroma.no_followers')}}</div>
       <div v-else>{{$t('pleroma.no_followers')}}</div>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 9e4c5ae..4655c37 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -9,11 +9,14 @@
           {{item.name}}
         </va-tab>
       </va-tabs>
-      <div class="config-settings-page__content py-4">
+      <div class="config-settings-page__content pt-4">
         <upload-form v-if="configKeys[value].key === 'Pleroma.Upload'"/>
         <emails-form v-if="configKeys[value].key === 'Pleroma.Emails'"/>
         <instance-config v-if="configKeys[value].key === ':instance'"/>
       </div>
+      <div class="flex-center pb-4">
+        <va-button @click="onSaveButtunClick">Safe settings</va-button>
+      </div>
     </va-card>
   </div>
 </template>
@@ -37,6 +40,9 @@ export default class ConfigSettingsPage extends Vue {
   async mounted () {
     this.config = await ConfigService.listConfigSettings()
   }
+  async onSaveButtunClick () {
+    console.log('safe settings', this.config)
+  }
 }
 </script>
 
diff --git a/src/components/reports/Report.vue b/src/components/reports/Report.vue
index 3b7e5ac..898b39e 100644
--- a/src/components/reports/Report.vue
+++ b/src/components/reports/Report.vue
@@ -2,7 +2,7 @@
   <div class="report">
     <div class="va-row report__table mb-4">
       <div class="flex report__table__row" v-if="report.account">
-        <div>{{$t('pleroma.reports.reported_account')}}</div>
+        <div>{{$t('reports.reported_account')}}</div>
         <router-link
           :to="{ name: 'userDetails', params: {nickname: report.account.username} }"
         >
@@ -11,9 +11,9 @@
         </router-link>
         <!--<div class="report__additional justify&#45;&#45;end">-->
           <!--<span class="mr-2">-->
-            <!--<va-icon icon="ion-ios-flag-outline" class="mr-1"/>{{report.actor.reportsCount}} {{$t('pleroma.reports.reports')}}-->
+            <!--<va-icon icon="ion-ios-flag-outline" class="mr-1"/>{{report.actor.reportsCount}} {{$t('reports.reports')}}-->
           <!--</span>-->
-          <!--<span><va-icon icon="ion-ios-create-outline" class="mr-1"/>{{report.actor.reportsCount}} {{$t('pleroma.reports.notes')}}</span>-->
+          <!--<span><va-icon icon="ion-ios-create-outline" class="mr-1"/>{{report.actor.reportsCount}} {{$t('reports.notes')}}</span>-->
         <!--</div>-->
       </div>
       <div class="flex report__table__row" v-if="report.actor">
@@ -25,33 +25,33 @@
           {{report.actor.username}}
         </router-link>
         <!--<div class="report__additional justify&#45;&#45;end">-->
-          <!--<span class="mr-2"><va-icon icon="ion-ios-flag-outline" class="mr-1"/>{{report.actor.reportsCount}} {{$t('pleroma.reports.reports')}} </span>-->
-          <!--<span><va-icon icon="ion-ios-create-outline" class="mr-1"/>{{report.actor.notesCount}} {{$t('pleroma.reports.notes')}}</span>-->
+          <!--<span class="mr-2"><va-icon icon="ion-ios-flag-outline" class="mr-1"/>{{report.actor.reportsCount}} {{$t('reports.reports')}} </span>-->
+          <!--<span><va-icon icon="ion-ios-create-outline" class="mr-1"/>{{report.actor.notesCount}} {{$t('reports.notes')}}</span>-->
         <!--</div>-->
       </div>
       <div class="flex report__table__row">
-        <div>{{$t('pleroma.reports.reported')}}</div>
+        <div>{{$t('reports.reported')}}</div>
         <div>{{formattedReportedDate}}</div>
       </div>
       <div class="flex report__table__row" v-if="formattedUpdatedDate">
-        <div>{{$t('pleroma.reports.updated')}}</div>
+        <div>{{$t('reports.updated')}}</div>
         <div>{{formattedUpdatedDate}}</div>
       </div>
       <div class="flex report__table__row">
-        <div>{{$t('pleroma.reports.status')}}</div>
+        <div>{{$t('reports.status')}}</div>
         <div>
-          <span>{{$t(`pleroma.reports.statuses.${report.state}`)}}</span>
-          <va-button small flat v-if="report.state !== 'open'" @click="$emit('reopen')">{{$t('pleroma.reports.reopen_report')}}</va-button>
+          <span>{{$t(`reports.statuses.${report.state}`)}}</span>
+          <va-button small flat v-if="report.state !== 'open'" @click="$emit('reopen')">{{$t('reports.reopen_report')}}</va-button>
         </div>
       </div>
       <div class="flex report__table__row" v-if="report.content">
-        <div>{{$t('pleroma.reports.note')}}</div>
+        <div>{{$t('reports.note')}}</div>
         <div>
           <span v-html="report.content"/>
         </div>
       </div>
       <!--<div class="flex report__table__row" v-if="report.moderator">-->
-        <!--<div>{{$t('pleroma.reports.action_taken_by')}}</div>-->
+        <!--<div>{{$t('reports.action_taken_by')}}</div>-->
         <!--<router-link-->
           <!--:to="{ name: 'userDetails', params: {nickname: report.moderator.username} }"-->
         <!--&gt;-->
@@ -60,9 +60,9 @@
         <!--</router-link>-->
       <!--</div>-->
       <!--<div class="flex report__table__row" v-else>-->
-        <!--<div>{{$t('pleroma.reports.assigned_moderator')}}</div>-->
+        <!--<div>{{$t('reports.assigned_moderator')}}</div>-->
         <!--<div>-->
-          <!--<va-button small flat color="info" class="ml-0">{{$t('pleroma.reports.assign_to_me')}}</va-button>-->
+          <!--<va-button small flat color="info" class="ml-0">{{$t('reports.assign_to_me')}}</va-button>-->
         <!--</div>-->
       <!--</div>-->
     </div>
@@ -87,7 +87,7 @@
         <!--</va-timeline-item>-->
       <!--</va-timeline>-->
       <div class="flex report__status-list" v-if="report.statuses && report.statuses.length">
-        <div class="display-4">{{$t('pleroma.reports.statuses_header')}}</div>
+        <div class="display-4">{{$t('reports.statuses_header')}}</div>
         <status
           class="report__status mb-2"
           v-for="(status, index) in report.statuses"
diff --git a/src/components/reports/ReportGroup.vue b/src/components/reports/ReportGroup.vue
index 304ac06..bf6701b 100644
--- a/src/components/reports/ReportGroup.vue
+++ b/src/components/reports/ReportGroup.vue
@@ -17,7 +17,7 @@
             {{reportGroup.account.display_name}} (@{{reportGroup.account.username}})
           </router-link>
           <span class="report-group__reported-by">
-            <span class="text--lowercase mr-1">{{$t('pleroma.reports.reported_by')}}</span>
+            <span class="text--lowercase mr-1">{{$t('reports.reported_by')}}</span>
             <router-link class="report-group__names mr-1" :to="{ name: 'userDetails', params: {nickname: reportGroup.actor.username} }">
               {{reportGroup.actor.display_name}} (@{{reportGroup.actor.username}})
             </router-link>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7c72ead..3a1268b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -19,6 +19,123 @@
   "breadcrumbs": {
     "home": "Home"
   },
+  "buttons": {
+    "advanced": "Buttons With Icons",
+    "size": "Button Sizes",
+    "tags": "Button Tags",
+    "button": "BUTTON",
+    "buttonGroups": "Button Groups",
+    "buttonToggles": "Button Toggles",
+    "pagination": "Pagination",
+    "a-link": "A-LINK",
+    "router-link": "ROUTER-LINK",
+    "colors": "Button Colors",
+    "disabled": "DISABLED",
+    "dropdown": "DROPDOWN",
+    "hover": "HOVER",
+    "types": "Button Types",
+    "pressed": "PRESSED",
+    "default": "DEFAULT",
+    "outline": "OUTLINE",
+    "flat": "FLAT",
+    "large": "LARGE",
+    "small": "SMALL",
+    "normal": "NORMAL",
+    "success": "SUCCESS",
+    "info": "INFO",
+    "danger": "DANGER",
+    "warning": "WARNING",
+    "gray": "GRAY",
+    "dark": "DARK"
+  },
+  "collapse": {
+    "basic": "Basic Collapse",
+    "collapseWithBackground": "Collapse with background",
+    "collapseWithCustomHeader": "Collapse with custom header"
+  },
+  "dashboard": {
+    "dataVisualization": "Data Visualization",
+    "success": "SUCCESS",
+    "successMessage": "You successfully read this important alert message.",
+    "elements": "Elements",
+    "features": "Features",
+    "setupProfile": "Setup Profile",
+    "teamMembers": "Team Members",
+    "usersAndMembers": "Users & Members",
+    "versions": "Versions"
+  },
+  "extra": {
+    "tabs": {
+      "title": "Tabs",
+      "maps": "Maps",
+      "pages": "Pages",
+      "overview": "Overview",
+      "setupProfile": "Setup Profile"
+    },
+    "chat": "Chat",
+    "profileCard": "Profile Card"
+  },
+  "forms": {
+    "controls": {
+      "female": "Female",
+      "male": "Male",
+      "title": "Checkboxes, Radios, Switches",
+      "radioDisabled": "Disabled Radio",
+      "radio": "Radio",
+      "subscribe": "Subscribe to newsletter",
+      "unselected": "Unselected checkbox",
+      "selected": "Selected checkbox",
+      "readonly": "Readonly checkbox",
+      "disabled": "Disabled checkbox",
+      "error": "Checkbox with error",
+      "errorMessage": "Checkbox with error messages"
+    },
+    "dateTimePicker": {
+      "title": "Date time pickers",
+      "basic": "Basic",
+      "time": "Time",
+      "range": "Range",
+      "multiple": "Multiple",
+      "disabled": "Disabled",
+      "customFirstDay": "Custom first day",
+      "customDateFormat": "Custom date format"
+    },
+    "inputs": {
+      "emailValidatedSuccess": "Email (validated with success)",
+      "emailValidated": "Email (validated)",
+      "inputWithIcon": "Input With Icon",
+      "inputWithButton": "Input With Button",
+      "inputWithClearButton": "Input With Clear Button",
+      "inputWithRoundButton": "Input With Round Button",
+      "textInput": "Text Input",
+      "textInputWithDescription": "Text Input (with description)",
+      "textArea": "Text Area",
+      "title": "Inputs",
+      "upload": "UPLOAD"
+    },
+    "mediumEditor": {
+      "title": "Medium Editor"
+    },
+    "selects": {
+      "country": "Country Select",
+      "countryMulti": "Country Multi Select",
+      "multi": "Multi Select",
+      "simple": "Simple Select",
+      "title": "Selects"
+    },
+    "wizard": {
+      "name": "Name",
+      "completed": "Wizard completed!",
+      "confirmSelection": "Confirm selection",
+      "rich": "Rich Wizard",
+      "simple": "Simple Wizard",
+      "stepOne": "Step 1. Name",
+      "stepTwo": "Step 2. Country",
+      "stepThree": "Step 3. Confirm",
+      "verticalRich": "Vertical Rich Wizard",
+      "verticalSimple": "Vertical Simple Wizard"
+    }
+  },
   "charts": {
     "horizontalBarChart": "Horizontal Bar Chart",
     "verticalBarChart": "Vertical Bar Chart",
@@ -236,6 +353,7 @@
   },
   "pleroma": {
     "login-placeholder": "username@instance",
+    "password-placeholder": "password",
     "statuses": "Statuses",
     "following": "Following",
     "followers": "Followers",
@@ -246,8 +364,77 @@
     "no_statuses": "No more statuses",
     "no_statuses_yet": "No statuses yet",
     "no_users_found": "No users found",
+    "no_reports": "No more reports",
+    "no_reports_yet": "No reports yet",
     "load_more": "Load more"
   },
+  "admin_menu": {
+    "moderation": "Moderation",
+    "grant_admin": "Grant Admin",
+    "revoke_admin": "Revoke Admin",
+    "grant_moderator": "Grant Moderator",
+    "revoke_moderator": "Revoke Moderator",
+    "activate_account": "Activate account",
+    "activate_accounts": "Activate accounts",
+    "deactivate_account": "Deactivate account",
+    "deactivate_accounts": "Deactivate accounts",
+    "deactivate_your_account": "You are trying to deactivate your account. Please type in the name of your account to confirm",
+    "deactivate_anyway": "Deactivate anyway",
+    "delete_account": "Delete account",
+    "delete_accounts": "Delete accounts",
+    "delete_user": "Delete user",
+    "delete_users": "Delete users",
+    "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone.",
+    "delete_your_account_confirmation": "You are trying to delete YOUR account. Please type in the name of your account to confirm.",
+    "change_tag_confirmation": "Do you want to {action}?"
+  },
+  "tags": {
+    "mrf_tag:media-force-nsfw": "Mark all posts as NSF",
+    "mrf_tag:media-force-nsfw-neg": "Remove NSF mark from posts",
+    "mrf_tag:media-strip": "Remove media from posts",
+    "mrf_tag:media-strip-neg": "Don't remove media from posts",
+    "mrf_tag:force-unlisted": "Force posts to be unlisted",
+    "mrf_tag:force-unlisted-neg": "Force posts to be unlisted",
+    "mrf_tag:disable-remote-subscription": "Disallow following user from remote instances",
+    "mrf_tag:disable-remote-subscription-neg": "Allow following user from remote instances",
+    "mrf_tag:disable-any-subscription":  "Disallow following user at all",
+    "mrf_tag:disable-any-subscription-neg":  "Allow following user at all",
+    "mrf_tag:sandbox": "Force posts to be followers-only",
+    "mrf_tag:sandbox-neg": "Force posts to be followers-only",
+    "mrf_tag:quarantine": "Disallow user posts from federating",
+    "mrf_tag:quarantine-neg": "Allow user posts from federating"
+  },
+  "reports": {
+    "reports": "reports",
+    "note": "Note",
+    "mark_as_resolved": "Mark as resolved",
+    "mark_as_unresolved": "Mark as unresolved",
+    "close": "Close",
+    "reported_account": "Reported account",
+    "reported_by": "Reported by",
+    "reported": "Reported",
+    "updated": "Updated",
+    "status": "Status",
+    "notes": "notes",
+    "reopen_report": "Reopen report",
+    "close_report": "Close report",
+    "action_taken_by": "Action taken by",
+    "assigned_moderator": "Assigned moderator",
+    "assign_to_me": "Assign to me",
+    "reopen_with_note": "Reopen with note",
+    "close_with_note": "Close with note",
+    "respond": "Respond to a report",
+    "statuses_header": "Statuses",
+    "offer_to_add_respond": "Would you like to respond a report?",
+    "statuses": {
+      "open": "Open",
+      "closed": "Closed",
+      "resolved": "Resolved"
+    },
+    "delete_status": "Delete status",
+    "delete_status_message": "Are you absolutely sure? This action cannot be undone.",
+    "add_note_placeholder": "Add note..."
+  },
   "config_settings": {
     "name": "Name",
     "name_label": "The instance’s name",
diff --git a/src/router/router.js b/src/router/router.js
index 137fcd4..99592d0 100644
--- a/src/router/router.js
+++ b/src/router/router.js
@@ -59,11 +59,6 @@ export default new Router({
             next()
           }
         },
-        {
-          name: 'accept-oauth-code',
-          path: 'accept-oauth-code',
-          component: () => import('../components/auth/login/AcceptOauthCode.vue'),
-        },
       ],
     },
     {
diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts
index c526d88..2f7198b 100644
--- a/src/services/ApiService.ts
+++ b/src/services/ApiService.ts
@@ -1,11 +1,9 @@
 import axios from 'axios'
 import t from 'typy'
-import Qs from 'qs'
 import { axiosInstance } from './axiosInstance'
 
 const ACCOUNTS_RESOURCE = '/api/v1/accounts/'
 const APP_RESOURCE = '/api/v1/apps'
-const OAUTH_AUTHORIZE_URL = '/oauth/authorize'
 const OAUTH_TOKEN_URL = '/oauth/token'
 
 /* eslint-disable camelcase */
@@ -57,30 +55,20 @@ export class ApiService {
   async createApp () {
     const response = await axios.post(`${this.baseURL}${APP_RESOURCE}`, {
       client_name: `epic-pleroma-fe_${Math.random()}`,
-      redirect_uris: `${window.location.origin}#auth/accept-oauth-code`,
+      redirect_uris: `${window.location.origin}/auth/accept-oauth-code`,
       scopes: 'read write follow'
     })
     return response.data as OauthApp
   }
 
-  getFullOauthUrl (app: OauthApp): string {
-    const payload = {
-      response_type: 'code',
-      client_id: app.client_id,
-      redirect_uri: app.redirect_uri,
-      scope: 'read write follow'
-    }
-
-    return `${this.baseURL}${OAUTH_AUTHORIZE_URL}?${Qs.stringify(payload)}`
-  }
-
   /* eslint-disable camelcase */
-  async getOauthToken ({ client_id, client_secret, code }) {
+  async getOauthToken (app: OauthApp, username: string, password: string) {
     const payload = {
-      client_id,
-      client_secret,
-      grant_type: 'authorization_code',
-      code,
+      grant_type: 'password',
+      username: username,
+      password: password,
+      client_id: app.client_id,
+      client_secret: app.client_secret
     }
     return (await axiosInstance.post(`${this.baseURL}${OAUTH_TOKEN_URL}`, payload)).data as OauthToken
   }
diff --git a/vue.config.js b/vue.config.js
index 4390bd4..41a3a8c 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -13,7 +13,7 @@ module.exports = {
       filename: 'index.html',
       // when using title option,
       // template title tag needs to be <title><%= htmlWebpackPlugin.options.title %></title>
-      title: 'Vuestic Admin',
+      title: 'Epic Pleroma FE',
       // chunks to include on this page, by default includes
       // extracted common chunks and vendor chunks.
       chunks: ['chunk-vendors', 'chunk-common', 'index'],
-- 
GitLab


From 2b3894a0946b79e306e23a1081d00b0c7d49d4cc Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 9 Jul 2019 11:43:59 +0300
Subject: [PATCH 10/61] feat: add base converters to API for config settings

---
 .../configSettings/forms/EmailsForm.vue       | 10 +++++--
 .../configSettings/forms/InstanceForm.vue     | 10 +++++--
 .../configSettings/forms/UploadForm.vue       | 10 +++++--
 .../configSettings/ConfigSettingsPage.vue     | 26 ++++++++++++++-----
 src/components/reports/Report.vue             |  2 +-
 src/components/reports/ReportGroup.vue        |  2 +-
 src/entities/settings/UploadConfig.ts         |  4 ++-
 src/services/ConfigService.ts                 |  5 ++--
 src/utils/ConvertConfigToApiRequest.js        |  4 +++
 src/utils/ConvertConfigToState.js             | 11 ++++++++
 src/{services => utils}/utils.js              |  0
 .../progress-types/VaProgressBar.vue          |  2 +-
 .../progress-types/progressMixin.js           |  2 +-
 13 files changed, 69 insertions(+), 19 deletions(-)
 create mode 100644 src/utils/ConvertConfigToApiRequest.js
 create mode 100644 src/utils/ConvertConfigToState.js
 rename src/{services => utils}/utils.js (100%)

diff --git a/src/components/configSettings/forms/EmailsForm.vue b/src/components/configSettings/forms/EmailsForm.vue
index 81d3f1c..d99023f 100644
--- a/src/components/configSettings/forms/EmailsForm.vue
+++ b/src/components/configSettings/forms/EmailsForm.vue
@@ -11,7 +11,7 @@
 </template>
 
 <script lang="ts">
-import { Component, Vue, Watch } from 'vue-property-decorator'
+import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
 import _ from 'lodash'
 import EmailsConfig from '../../../entities/settings/EmailsConfig'
 import SMTPAdapter from '../emailAdapters/SMTPAdapter.vue'
@@ -44,7 +44,13 @@ import AmazonSESAdapter from '../emailAdapters/AmazonSESAdapter.vue'
   },
 })
 export default class EmailsForm extends Vue {
-  formData:EmailsConfig = new EmailsConfig()
+  @Prop(EmailsConfig) readonly value!
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
   adapterData = {}
   selectOptions = {
     adapter: [
diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
index 9715257..d09abd4 100644
--- a/src/components/configSettings/forms/InstanceForm.vue
+++ b/src/components/configSettings/forms/InstanceForm.vue
@@ -81,7 +81,7 @@
 </template>
 
 <script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
+import { Component, Prop, Vue } from 'vue-property-decorator'
 import InstanceConfig from '../../../entities/settings/InstanceConfig'
 
 @Component({
@@ -89,7 +89,13 @@ import InstanceConfig from '../../../entities/settings/InstanceConfig'
 })
 
 export default class InstanceForm extends Vue {
-  formData: InstanceConfig = new InstanceConfig()
+  @Prop(InstanceConfig) readonly value!
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
   selectOptions = {
     rewrite_policy: [
       'Pleroma.Web.ActivityPub.MRF.NoOpPolicy',
diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index a9ede46..ddee465 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -28,14 +28,20 @@
 </template>
 
 <script lang="ts">
-import { Component, Vue } from 'vue-property-decorator'
+import { Component, Prop, Vue } from 'vue-property-decorator'
 import UploadConfig from '../../../entities/settings/UploadConfig'
 
 @Component({
   components: {},
 })
 export default class UploadForm extends Vue {
-  formData:UploadConfig = new UploadConfig()
+  @Prop(UploadConfig) value!: object
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
   selectOptions = {
     uploader: ['Local', 'S3'],
     filter: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 4655c37..bedeb57 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -10,9 +10,9 @@
         </va-tab>
       </va-tabs>
       <div class="config-settings-page__content pt-4">
-        <upload-form v-if="configKeys[value].key === 'Pleroma.Upload'"/>
-        <emails-form v-if="configKeys[value].key === 'Pleroma.Emails'"/>
-        <instance-config v-if="configKeys[value].key === ':instance'"/>
+        <upload-form v-if="configKeys[value].key === 'Pleroma.Upload'" v-model="upload"/>
+        <emails-form v-if="configKeys[value].key === 'Pleroma.Emails'" v-model="emails"/>
+        <instance-config v-if="configKeys[value].key === ':instance'" v-model="instance"/>
       </div>
       <div class="flex-center pb-4">
         <va-button @click="onSaveButtunClick">Safe settings</va-button>
@@ -29,19 +29,33 @@ import { configKeys } from '../../../data/Config'
 import UploadForm from '../../configSettings/forms/UploadForm.vue'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
 import InstanceConfig from '../../configSettings/forms/InstanceForm.vue'
+import ConvertConfigToState from '../../../utils/ConvertConfigToState'
+import UploadConfig from '../../../entities/settings/UploadConfig'
+import EmailsConfig from '../../../entities/settings/EmailsConfig'
+import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest';
 
 @Component({
   components: { InstanceConfig, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
 })
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
-  config: any = null
+  upload?: UploadConfig = new UploadConfig()
+  emails?: EmailsConfig = new EmailsConfig()
+  instance?: InstanceConfig = new InstanceConfig()
   configKeys: Array<object> = configKeys
+  loading:boolean = false
   async mounted () {
-    this.config = await ConfigService.listConfigSettings()
+    this.loading = true
+    const { configs } = await ConfigService.listConfigSettings()
+    const { upload, emails, instance } = ConvertConfigToState(configs)
+    this.upload = upload
+    this.emails = emails
+    this.instance = instance
+    this.loading = false
   }
   async onSaveButtunClick () {
-    console.log('safe settings', this.config)
+    console.log('safe settings', this.upload)
+    const data = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest([this.upload, this.emails, this.instance]))
   }
 }
 </script>
diff --git a/src/components/reports/Report.vue b/src/components/reports/Report.vue
index 898b39e..ad7a4b2 100644
--- a/src/components/reports/Report.vue
+++ b/src/components/reports/Report.vue
@@ -103,7 +103,7 @@
 <script lang="ts">
 import { Component, Vue, Prop } from 'vue-property-decorator'
 import { Report as ReportClass } from '../../entities'
-import utils from '../../services/utils.js'
+import utils from '../../utils/utils.js'
 import Status from '../statuses/Status.vue'
 
 @Component({
diff --git a/src/components/reports/ReportGroup.vue b/src/components/reports/ReportGroup.vue
index bf6701b..432b236 100644
--- a/src/components/reports/ReportGroup.vue
+++ b/src/components/reports/ReportGroup.vue
@@ -33,7 +33,7 @@
 import { Component, Vue, Prop } from 'vue-property-decorator'
 import { Report } from '../../entities/'
 import { getGradientBackground } from '../../services/color-functions'
-import utils from '../../services/utils'
+import utils from '../../utils/utils'
 
 @Component({
   components: {},
diff --git a/src/entities/settings/UploadConfig.ts b/src/entities/settings/UploadConfig.ts
index 814a74a..46174b7 100644
--- a/src/entities/settings/UploadConfig.ts
+++ b/src/entities/settings/UploadConfig.ts
@@ -1,4 +1,6 @@
-export default class SettingsConfig {
+export default class UploadConfig {
+  constructor (existConfig?) {
+  }
   uploader: string = 'Pleroma.Uploaders.Local'
   filters: Array<string> = []
   upload: string = ''
diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts
index 65a1229..bf2fd7a 100644
--- a/src/services/ConfigService.ts
+++ b/src/services/ConfigService.ts
@@ -8,7 +8,8 @@ export class ConfigService {
     return executeApiRequest('get', urlBuilder(Url.configSettings, {}), {})
   }
 
-  static updateConfigSettings () {
-    return executeApiRequest('put', urlBuilder(Url.configSettings, {}), {})
+  static updateConfigSettings (configs) {
+    return Promise.resolve(configs)
+    // return executeApiRequest('put', urlBuilder(Url.configSettings, {}), {})
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
new file mode 100644
index 0000000..12bc34f
--- /dev/null
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -0,0 +1,4 @@
+export default (configs) => {
+  console.log('convert', configs)
+  return configs
+}
diff --git a/src/utils/ConvertConfigToState.js b/src/utils/ConvertConfigToState.js
new file mode 100644
index 0000000..b2f3eab
--- /dev/null
+++ b/src/utils/ConvertConfigToState.js
@@ -0,0 +1,11 @@
+import UploadConfig from '../entities/settings/UploadConfig';
+import EmailsConfig from '../entities/settings/EmailsConfig';
+import InstanceConfig from '../entities/settings/InstanceConfig';
+
+export default (configs) => {
+  return {
+    upload: new UploadConfig(configs.find(({ key }) => key === 'Pleroma.Upload')),
+    emails: new EmailsConfig(),
+    instance: new InstanceConfig()
+  }
+}
diff --git a/src/services/utils.js b/src/utils/utils.js
similarity index 100%
rename from src/services/utils.js
rename to src/utils/utils.js
diff --git a/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/VaProgressBar.vue b/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/VaProgressBar.vue
index 5c1a64e..75ab235 100644
--- a/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/VaProgressBar.vue
+++ b/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/VaProgressBar.vue
@@ -29,7 +29,7 @@
 
 <script>
 import { progressMixin } from './progressMixin'
-import utils from '../../../../services/utils'
+import utils from '../../../../utils/utils'
 
 export default {
   name: 'va-progress-bar',
diff --git a/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js b/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js
index 1198ad2..5ed7284 100644
--- a/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js
+++ b/src/vuestic-theme/vuestic-components/va-progress-bar/progress-types/progressMixin.js
@@ -1,4 +1,4 @@
-import utils from '../../../../services/utils'
+import utils from '../../../../utils/utils'
 import {
   colorConfig,
   VuesticTheme
-- 
GitLab


From f4d25f591102a2eb8aec3c7470a551bf6e596151 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 9 Jul 2019 13:07:31 +0300
Subject: [PATCH 11/61] feat: i18n

---
 .../emailAdapters/AmazonSESAdapter.vue        |   8 +-
 .../emailAdapters/DynAdapter.vue              |   4 +-
 .../emailAdapters/GmailAdapter.vue            |   4 +-
 .../emailAdapters/MailgunAdapter.vue          |   6 +-
 .../emailAdapters/MailjetAdapter.vue          |   6 +-
 .../emailAdapters/MandrillAdapter.vue         |   4 +-
 .../emailAdapters/PostmarkAdapter.vue         |   4 +-
 .../emailAdapters/SMTPAdapter.vue             |  22 +-
 .../emailAdapters/SendgridAdapter.vue         |   4 +-
 .../emailAdapters/SendmailAdapter.vue         |   8 +-
 .../emailAdapters/SocketLabsAdapter.vue       |   6 +-
 .../emailAdapters/SparkPostAdapter.vue        |   6 +-
 .../configSettings/forms/InstanceForm.vue     | 202 +++++++++++-------
 .../configSettings/forms/UploadForm.vue       |  68 ++++--
 src/i18n/en.json                              | 191 ++++++++++-------
 15 files changed, 334 insertions(+), 209 deletions(-)

diff --git a/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue b/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
index 3688714..443d1ef 100644
--- a/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
+++ b/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
@@ -1,20 +1,20 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">AmazonSES adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'AmazonSES'})}}</p>
     <va-input
       v-model="configProxy.region"
-      label="region"
+      :label="$t('config_settings.emails.region')"
       @input="(val) => configProxy = {field:'region', val}"
     />
     <va-input
       v-model="configProxy.access_key"
       @input="(val) => configProxy = {field:'access_key', val}"
-      label="access_key"
+      :label="$t('config_settings.emails.access_key')"
     />
     <va-input
       v-model="configProxy.secret"
       @input="(val) => configProxy = {field:'secret', val}"
-      label="secret"
+      :label="$t('config_settings.emails.secret')"
     />
   </div>
 </template>
diff --git a/src/components/configSettings/emailAdapters/DynAdapter.vue b/src/components/configSettings/emailAdapters/DynAdapter.vue
index c0171d4..18fc0ea 100644
--- a/src/components/configSettings/emailAdapters/DynAdapter.vue
+++ b/src/components/configSettings/emailAdapters/DynAdapter.vue
@@ -1,9 +1,9 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Dyn adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Dyn'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t('config_settings.emails.api_key')"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/GmailAdapter.vue b/src/components/configSettings/emailAdapters/GmailAdapter.vue
index 76e7dec..e3efe4c 100644
--- a/src/components/configSettings/emailAdapters/GmailAdapter.vue
+++ b/src/components/configSettings/emailAdapters/GmailAdapter.vue
@@ -1,9 +1,9 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Gmail adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'gmail'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t('config_settings.emails.api_key')"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/MailgunAdapter.vue b/src/components/configSettings/emailAdapters/MailgunAdapter.vue
index 6b82314..98f9f45 100644
--- a/src/components/configSettings/emailAdapters/MailgunAdapter.vue
+++ b/src/components/configSettings/emailAdapters/MailgunAdapter.vue
@@ -1,14 +1,14 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Mailgun adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Mailgun'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t('config_settings.emails.api_key')"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
     <va-input
       v-model="configProxy.domain"
-      label="domain"
+      :label="$t('config_settings.emails.domain')"
       @input="(val) => configProxy = {field:'domain', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/MailjetAdapter.vue b/src/components/configSettings/emailAdapters/MailjetAdapter.vue
index 7423f64..4ffdbd5 100644
--- a/src/components/configSettings/emailAdapters/MailjetAdapter.vue
+++ b/src/components/configSettings/emailAdapters/MailjetAdapter.vue
@@ -1,14 +1,14 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Mailjet adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Mailjet'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t('config_settings.emails.api_key')"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
     <va-input
       v-model="configProxy.secret"
-      label="secret"
+      :label="$t('config_settings.emails.secret')"
       @input="(val) => configProxy = {field:'secret', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/MandrillAdapter.vue b/src/components/configSettings/emailAdapters/MandrillAdapter.vue
index 5acbf76..e66f355 100644
--- a/src/components/configSettings/emailAdapters/MandrillAdapter.vue
+++ b/src/components/configSettings/emailAdapters/MandrillAdapter.vue
@@ -1,9 +1,9 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Mandrill adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Mandrill'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t('config_settings.emails.api_key')"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/PostmarkAdapter.vue b/src/components/configSettings/emailAdapters/PostmarkAdapter.vue
index a207e3c..d18b82c 100644
--- a/src/components/configSettings/emailAdapters/PostmarkAdapter.vue
+++ b/src/components/configSettings/emailAdapters/PostmarkAdapter.vue
@@ -1,9 +1,9 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Postmark adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Postmark'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t('config_settings.emails.api_key')"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/SMTPAdapter.vue b/src/components/configSettings/emailAdapters/SMTPAdapter.vue
index 6eefee9..148cc03 100644
--- a/src/components/configSettings/emailAdapters/SMTPAdapter.vue
+++ b/src/components/configSettings/emailAdapters/SMTPAdapter.vue
@@ -1,61 +1,61 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">SMTP adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'SMTP'})}}</p>
     <va-input
       v-model="configProxy.relay"
-      label="relay"
+      :label="$t(`config_settings.emails.relay`)"
       @input="(val) => configProxy = {field:'relay', val}"
     />
     <va-input
       v-model="configProxy.username"
       @input="(val) => configProxy = {field:'username', val}"
-      label="username"
+      :label="$t(`config_settings.emails.username`)"
     />
     <va-input
       v-model="configProxy.password"
       @input="(val) => configProxy = {field:'password', val}"
-      label="password"
       type="password"
+      :label="$t(`config_settings.emails.password`)"
     />
     <va-input-wrapper>
       <va-checkbox
         v-model="configProxy.ssl"
         @input="(val) => configProxy = {field:'ssl', val}"
-        label="ssl"
+        :label="$t(`config_settings.emails.ssl`)"
       />
     </va-input-wrapper>
     <va-input
       v-model="configProxy.tls"
       @input="(val) => configProxy = { field: 'tls', val }"
-      label="tls"
+      :label="$t(`config_settings.emails.tls`)"
     />
     <va-input
       v-model="configProxy.auth"
       @input="(val) => configProxy = { field: 'auth', val }"
-      label="auth"
+      :label="$t(`config_settings.emails.auth`)"
     />
     <va-input
       v-model.number="configProxy.port"
       type="number"
       @input="(val) => configProxy = { field: 'port', val: +val }"
-      label="port"
+      :label="$t(`config_settings.emails.port`)"
     />
     <va-input
       v-model="configProxy.dkim"
       @input="(val) => configProxy = { field: 'dkim', val }"
-      label="dkim"
+      :label="$t(`config_settings.emails.dkim`)"
     />
     <va-input
       v-model.number="configProxy.retries"
       type="number"
       @input="(val) => configProxy = { field: 'retries', val: +val }"
-      label="retries"
+      :label="$t(`config_settings.emails.retries`)"
     />
     <va-input-wrapper>
       <va-checkbox
         v-model="configProxy.no_mx_lookups"
         @input="(val) => configProxy = { field: 'no_mx_lookups', val }"
-        label="no_mx_lookups"
+        :label="$t(`config_settings.emails.no_mx_lookups`)"
       />
     </va-input-wrapper>
   </div>
diff --git a/src/components/configSettings/emailAdapters/SendgridAdapter.vue b/src/components/configSettings/emailAdapters/SendgridAdapter.vue
index 7b20f82..d591c9f 100644
--- a/src/components/configSettings/emailAdapters/SendgridAdapter.vue
+++ b/src/components/configSettings/emailAdapters/SendgridAdapter.vue
@@ -1,9 +1,9 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Sendgrid adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Sendgrid'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t('config_settings.emails.api_key')"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/SendmailAdapter.vue b/src/components/configSettings/emailAdapters/SendmailAdapter.vue
index 946e0e0..b5c15cf 100644
--- a/src/components/configSettings/emailAdapters/SendmailAdapter.vue
+++ b/src/components/configSettings/emailAdapters/SendmailAdapter.vue
@@ -1,19 +1,19 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">Sendmail adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Sendmail'})}}</p>
     <va-input
       v-model="configProxy.cmd_path"
-      label="cmd_path"
+      :label="$t(`config_settings.emails.cmd_path`)"
       @input="(val) => configProxy = {field:'cmd_path', val}"
     />
     <va-input
       v-model="configProxy.cmd_args"
-      label="cmd_args"
+      :label="$t(`config_settings.emails.cmd_args`)"
       @input="(val) => configProxy = {field:'cmd_args', val}"
     />
     <va-checkbox
       v-model="configProxy.qmail"
-      label="qmail"
+      :label="$t(`config_settings.emails.qmail`)"
       @input="(val) => configProxy = {field: 'qmail', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue b/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
index e482cae..1e36ac1 100644
--- a/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
+++ b/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
@@ -1,14 +1,14 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">SocketLabs adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'SocketLabs'})}}</p>
     <va-input
       v-model="configProxy.server_id"
-      label="server_id"
+      :label="$t(`config_settings.emails.server_id`)"
       @input="(val) => configProxy = {field:'server_id', val}"
     />
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t(`config_settings.emails.api_key`)"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
   </div>
diff --git a/src/components/configSettings/emailAdapters/SparkPostAdapter.vue b/src/components/configSettings/emailAdapters/SparkPostAdapter.vue
index 1df8cf3..f2f940b 100644
--- a/src/components/configSettings/emailAdapters/SparkPostAdapter.vue
+++ b/src/components/configSettings/emailAdapters/SparkPostAdapter.vue
@@ -1,14 +1,14 @@
 <template>
   <div class="mx-4 my-3">
-    <p class="title">SparkPost adapter config</p>
+    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'SparkPost'})}}</p>
     <va-input
       v-model="configProxy.api_key"
-      label="api_key"
+      :label="$t(`config_settings.emails.api_key`)"
       @input="(val) => configProxy = {field:'api_key', val}"
     />
     <va-input
       v-model="configProxy.endpoint"
-      label="endpoint"
+      :label="$t(`config_settings.emails.endpoint`)"
       @input="(val) => configProxy = {field:'endpoint', val}"
     />
   </div>
diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
index d09abd4..b9eaba2 100644
--- a/src/components/configSettings/forms/InstanceForm.vue
+++ b/src/components/configSettings/forms/InstanceForm.vue
@@ -1,82 +1,130 @@
 <template>
   <div>
-    <va-input v-model="formData.name" :label="$t('config_settings.name')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.name_label')}}</p>
-    <va-input v-model="formData.email" :label="$t('config_settings.email')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.email_label')}}</p>
-    <va-input v-model="formData.notify_email" :label="$t('config_settings.notify_email')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.notify_email_label')}}</p>
-    <va-input v-model="formData.description" :label="$t('config_settings.description')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.description_label')}}</p>
-    <va-input v-model.number="formData.limit" type="number" :label="$t('config_settings.limit')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.limit_label')}}</p>
-    <va-input v-model.number="formData.remote_limit" type="number" :label="$t('config_settings.remote_limit')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.remote_limit_label')}}</p>
-    <va-input v-model="formData.upload_limit" :label="$t('config_settings.upload_limit')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.upload_limit_label')}}</p>
-    <va-input v-model="formData.avatar_upload_limit" :label="$t('config_settings.avatar_upload_limit')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.avatar_upload_limit_label')}}</p>
-    <va-input v-model="formData.background_upload_limit" :label="$t('config_settings.background_upload_limit')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.background_upload_limit_label')}}</p>
-    <va-input v-model="formData.banner_upload_limit" :label="$t('config_settings.banner_upload_limit')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.banner_upload_limit_label')}}</p>
-    <va-checkbox v-model="formData.registrations_open" :label="$t('config_settings.registrations_open')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.registrations_open_label')}}</p>
-    <va-checkbox v-model="formData.invites_enabled" :label="$t('config_settings.invites_enabled')"/>
-    <p class="note">{{$t('config_settings.invites_enabled_label')}}</p>
-    <va-checkbox v-model="formData.account_activation_required" :label="$t('config_settings.account_activation_required')"/>
-    <p class="note">{{$t('config_settings.account_activation_required_label')}}</p>
-    <va-checkbox v-model="formData.federating" :label="$t('config_settings.federating')"/>
-    <p class="note">{{$t('config_settings.federating_label')}}</p>
-    <va-input v-model.number="formData.federation_reachability_timeout_days" type="number" :label="$t('config_settings.federation_reachability_timeout_days')"/>
-    <p class="note">{{$t('config_settings.federation_reachability_timeout_days_label')}}</p>
-    <va-checkbox v-model="formData.allow_relay" :label="$t('config_settings.allow_relay_label')"/>
-    <p class="note">{{$t('config_settings.allow_relay_label')}}</p>
-    <va-select v-model="formData.rewrite_policy" :options="selectOptions.rewrite_policy" :label="$t('config_settings.invites_enabled')"/>
-    <p class="note">{{$t('config_settings.rewrite_policy_label')}}</p>
-    <va-checkbox v-model="formData.public" :label="$t('config_settings.public')"/>
-    <p class="note">{{$t('config_settings.public_label')}}</p>
-    <va-input v-model="formData.quarantined_instances" :label="$t('config_settings.quarantined_instances')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.quarantined_instances_label')}}</p>
-    <va-checkbox v-model="formData.managed_config" :label="$t('config_settings.managed_config')"/>
-    <p class="note">{{$t('config_settings.quarantined_instances_label')}}</p>
-    <va-select v-model="formData.allowed_post_formats" :options="selectOptions.allowed_post_formats" :label="$t('config_settings.allowed_post_formats')"/>
-    <p class="note">{{$t('config_settings.allowed_post_formats_label')}}</p>
-    <va-checkbox v-model="formData.mrf_transparency" :label="$t('config_settings.mrf_transparency')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.mrf_transparency_label')}}</p>
-    <va-checkbox v-model="formData.scope_copy" :label="$t('config_settings.scope_copy')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.scope_copy_label')}}</p>
-    <va-input v-model="formData.subject_line_behavior" :label="$t('config_settings.subject_line_behavior')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.subject_line_behavior_label')}}</p>
-    <va-checkbox v-model="formData.always_show_subject_input" :label="$t('config_settings.always_show_subject_input')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.always_show_subject_input_label')}}</p>
-    <va-checkbox v-model="formData.extended_nickname_format" :label="$t('config_settings.extended_nickname_format')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.extended_nickname_format_label')}}</p>
-    <va-input v-model.number="formData.max_pinned_statuses" type="number" :label="$t('config_settings.max_pinned_statuses')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.max_pinned_statuses_label')}}</p>
-    <va-input v-model="formData.autofollowed_nicknames" :label="$t('config_settings.autofollowed_nicknames')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.autofollowed_nicknames_label')}}</p>
-    <va-checkbox v-model="formData.no_attachment_links" :label="$t('config_settings.no_attachment_links')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.no_attachment_links_label')}}</p>
-    <va-input v-model="formData.welcome_message" :label="$t('config_settings.welcome_message')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.welcome_message_label')}}</p>
-    <va-input v-model="formData.welcome_user_nickname" :label="$t('config_settings.welcome_user_nickname')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.welcome_user_nickname_label')}}</p>
-    <va-input v-model.number="formData.max_report_comment_size" :label="$t('config_settings.max_report_comment_size')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.max_report_comment_size_label')}}</p>
-    <va-checkbox v-model="formData.safe_dm_mentions" :label="$t('config_settings.safe_dm_mentions')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.safe_dm_mentions_label')}}</p>
-    <va-checkbox v-model="formData.healthcheck" :label="$t('config_settings.healthcheck')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.healthcheck_label')}}</p>
-    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.remote_post_retention_days')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.remote_post_retention_days_label')}}</p>
-    <va-checkbox v-model="formData.skip_thread_containment" :label="$t('config_settings.skip_thread_containment')" class="mb-0"/>
-    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.remote_post_retention_days')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.remote_post_retention_days_label')}}</p>
-    <va-select v-model="formData.limit_to_local_content" :options="selectOptions.limit_to_local_content" :label="$t('config_settings.limit_to_local_content')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.limit_to_local_content_label')}}</p>
-    <va-checkbox v-model="formData.dynamic_configuration" :label="$t('config_settings.dynamic_configuration')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.dynamic_configuration_label')}}</p>
+    <va-input
+      v-model="formData.name"
+      :label="$t('config_settings.instance_form.name')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.name_label')}}</p>
+    <va-input
+      v-model="formData.email"
+      :label="$t('config_settings.instance_form.email')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.email_label')}}</p>
+    <va-input
+      v-model="formData.notify_email"
+      :label="$t('config_settings.instance_form.notify_email')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.notify_email_label')}}</p>
+    <va-input
+      v-model="formData.description"
+      :label="$t('config_settings.instance_form.description')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.description_label')}}</p>
+    <va-input
+      v-model.number="formData.limit"
+      type="number"
+      :label="$t('config_settings.instance_form.limit')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.limit_label')}}</p>
+    <va-input
+      v-model.number="formData.remote_limit"
+      type="number"
+      :label="$t('config_settings.instance_form.remote_limit')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.remote_limit_label')}}</p>
+    <va-input
+      v-model="formData.upload_limit"
+      :label="$t('config_settings.instance_form.upload_limit')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.upload_limit_label')}}</p>
+    <va-input
+      v-model="formData.avatar_upload_limit"
+      :label="$t('config_settings.instance_form.avatar_upload_limit')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.avatar_upload_limit_label')}}</p>
+    <va-input
+      v-model="formData.background_upload_limit"
+      :label="$t('config_settings.instance_form.background_upload_limit')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.background_upload_limit_label')}}</p>
+    <va-input
+      v-model="formData.banner_upload_limit"
+      :label="$t('config_settings.instance_form.banner_upload_limit')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.banner_upload_limit_label')}}</p>
+    <va-checkbox
+      v-model="formData.registrations_open"
+      :label="$t('config_settings.instance_form.registrations_open')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.registrations_open_label')}}</p>
+    <va-checkbox
+      v-model="formData.invites_enabled"
+      :label="$t('config_settings.instance_form.invites_enabled')"/>
+    <p class="note">{{$t('config_settings.instance_form.invites_enabled_label')}}</p>
+    <va-checkbox v-model="formData.account_activation_required" :label="$t('config_settings.instance_form.account_activation_required')"/>
+    <p class="note">{{$t('config_settings.instance_form.account_activation_required_label')}}</p>
+    <va-checkbox v-model="formData.federating" :label="$t('config_settings.instance_form.federating')"/>
+    <p class="note">{{$t('config_settings.instance_form.federating_label')}}</p>
+    <va-input v-model.number="formData.federation_reachability_timeout_days" type="number" :label="$t('config_settings.instance_form.federation_reachability_timeout_days')"/>
+    <p class="note">{{$t('config_settings.instance_form.federation_reachability_timeout_days_label')}}</p>
+    <va-checkbox v-model="formData.allow_relay" :label="$t('config_settings.instance_form.allow_relay_label')"/>
+    <p class="note">{{$t('config_settings.instance_form.allow_relay_label')}}</p>
+    <va-select v-model="formData.rewrite_policy" :options="selectOptions.rewrite_policy" :label="$t('config_settings.instance_form.invites_enabled')"/>
+    <p class="note">{{$t('config_settings.instance_form.rewrite_policy_label')}}</p>
+    <va-checkbox v-model="formData.public" :label="$t('config_settings.instance_form.public')"/>
+    <p class="note">{{$t('config_settings.instance_form.public_label')}}</p>
+    <va-input v-model="formData.quarantined_instances" :label="$t('config_settings.instance_form.quarantined_instances')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.quarantined_instances_label')}}</p>
+    <va-checkbox v-model="formData.managed_config" :label="$t('config_settings.instance_form.managed_config')"/>
+    <p class="note">{{$t('config_settings.instance_form.quarantined_instances_label')}}</p>
+    <va-select v-model="formData.allowed_post_formats" :options="selectOptions.allowed_post_formats" :label="$t('config_settings.instance_form.allowed_post_formats')"/>
+    <p class="note">{{$t('config_settings.instance_form.allowed_post_formats_label')}}</p>
+    <va-checkbox v-model="formData.mrf_transparency" :label="$t('config_settings.instance_form.mrf_transparency')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.mrf_transparency_label')}}</p>
+    <va-checkbox v-model="formData.scope_copy" :label="$t('config_settings.instance_form.scope_copy')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.scope_copy_label')}}</p>
+    <va-input v-model="formData.subject_line_behavior" :label="$t('config_settings.instance_form.subject_line_behavior')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.subject_line_behavior_label')}}</p>
+    <va-checkbox v-model="formData.always_show_subject_input" :label="$t('config_settings.instance_form.always_show_subject_input')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.always_show_subject_input_label')}}</p>
+    <va-checkbox v-model="formData.extended_nickname_format" :label="$t('config_settings.instance_form.extended_nickname_format')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.extended_nickname_format_label')}}</p>
+    <va-input v-model.number="formData.max_pinned_statuses" type="number" :label="$t('config_settings.instance_form.max_pinned_statuses')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.max_pinned_statuses_label')}}</p>
+    <va-input v-model="formData.autofollowed_nicknames" :label="$t('config_settings.instance_form.autofollowed_nicknames')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.autofollowed_nicknames_label')}}</p>
+    <va-checkbox v-model="formData.no_attachment_links" :label="$t('config_settings.instance_form.no_attachment_links')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.no_attachment_links_label')}}</p>
+    <va-input v-model="formData.welcome_message" :label="$t('config_settings.instance_form.welcome_message')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.welcome_message_label')}}</p>
+    <va-input v-model="formData.welcome_user_nickname" :label="$t('config_settings.instance_form.welcome_user_nickname')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.welcome_user_nickname_label')}}</p>
+    <va-input v-model.number="formData.max_report_comment_size" :label="$t('config_settings.instance_form.max_report_comment_size')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.max_report_comment_size_label')}}</p>
+    <va-checkbox v-model="formData.safe_dm_mentions" :label="$t('config_settings.instance_form.safe_dm_mentions')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.safe_dm_mentions_label')}}</p>
+    <va-checkbox v-model="formData.healthcheck" :label="$t('config_settings.instance_form.healthcheck')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.healthcheck_label')}}</p>
+    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.instance_form.remote_post_retention_days')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.remote_post_retention_days_label')}}</p>
+    <va-checkbox v-model="formData.skip_thread_containment" :label="$t('config_settings.instance_form.skip_thread_containment')" class="mb-0"/>
+    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.instance_form.remote_post_retention_days')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.remote_post_retention_days_label')}}</p>
+    <va-select v-model="formData.limit_to_local_content" :options="selectOptions.limit_to_local_content" :label="$t('config_settings.instance_form.limit_to_local_content')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.limit_to_local_content_label')}}</p>
+    <va-checkbox v-model="formData.dynamic_configuration" :label="$t('config_settings.instance_form.dynamic_configuration')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.dynamic_configuration_label')}}</p>
   </div>
 </template>
 
diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index ddee465..e1deb6c 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -1,29 +1,65 @@
 <template>
   <div>
-    <va-select v-model="formData.uploader" :options="selectOptions.uploader" label="uploader"/>
+    <va-select
+      v-model="formData.uploader"
+      :options="selectOptions.uploader"
+      :label="$t('config_settings.upload_form.uploader')"
+    />
     <div v-if="formData.uploader === 'Local'" class="mx-4">
-      <va-input v-model="formData.uploads" label="uploads"/>
+      <va-input
+        v-model="formData.uploads"
+        :label="$t('config_settings.upload_form.uploads')"/>
     </div>
     <div v-if="formData.uploader === 'S3'" class="mx-4 my-3">
-      <va-input v-model="formData.bucket" label="S3 bucket name"/>
-      <va-input v-model="formData.public_endpoint" label="S3 endpoint that the user finally accesses"/>
-      <va-input v-model="formData.truncated_namespace" label="truncated namespace" class="mb-0"/>
-      <p class="note">If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
-        For example, when using CDN to S3 virtual host format, set "".
-        At this time, write CNAME to CDN in public_endpoint.</p>
+      <va-input
+        v-model="formData.bucket"
+        :label="$t('config_settings.upload_form.s3_bucket')"
+      />
+      <va-input
+        v-model="formData.public_endpoint"
+        :label="$t('config_settings.upload_form.s3_public_endpoint')"
+      />
+      <va-input
+        v-model="formData.truncated_namespace"
+        :label="$t('config_settings.upload_form.truncated_namespace')"
+        class="mb-0"
+      />
+      <p class="note">{{$t('config_settings.upload_form.truncated_namespace_note')}}</p>
     </div>
-    <va-select v-model="formData.filters" :options="selectOptions.filter" multiple label="filters"/>
+    <va-select
+      v-model="formData.filters"
+      :options="selectOptions.filter"
+      multiple
+      :label="$t('config_settings.upload_form.filters')"
+    />
     <div v-if="formData.filters.includes('Pleroma.Upload.Filter.Mogrify')" class="mx-4 my-3">
-      <va-select v-model="formData.args" :options="selectOptions.args" multiple label="List of actions for the mogrify command"/>
+      <va-select
+        v-model="formData.args"
+        :options="selectOptions.args"
+        multiple
+        :label="$t('config_settings.upload_form.args')"/>
     </div>
     <div v-if="formData.filters.includes('Pleroma.Upload.Filter.AnonymizeFilename')" class="mx-4 my-3">
-      <va-input v-model="formData.text" label="Anonymize filename" class="mb-0"/>
-      <p class="note">Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.</p>
+      <va-input
+        v-model="formData.text"
+        :label="$t('config_settings.upload_form.anonymize_filename')"
+        class="mb-0"
+      />
+      <p class="note">{{$t('config_settings.upload_form.anonymize_filename_note')}}</p>
     </div>
-    <va-checkbox label="Link name" v-model="formData.link_name"/>
-    <p class="note">When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe</p>
-    <va-input v-model="formData.base_url" label="base URL"/>
-    <va-checkbox v-model="formData.proxy_remote" label="Proxy remote"/>
+    <va-checkbox
+      :label="$t('config_settings.upload_form.link_name')"
+      v-model="formData.link_name"
+    />
+    <p class="note">{{$t('config_settings.upload_form.link_name_note')}}</p>
+    <va-input
+      v-model="formData.base_url"
+      :label="$t('config_settings.upload_form.base_url')"
+    />
+    <va-checkbox
+      v-model="formData.proxy_remote"
+      :label="$t('config_settings.upload_form.proxy_remote')"
+    />
   </div>
 </template>
 
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 3a1268b..ab12ff6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -436,80 +436,121 @@
     "add_note_placeholder": "Add note..."
   },
   "config_settings": {
-    "name": "Name",
-    "name_label": "The instance’s name",
-    "email": "Email",
-    "email_label": "Email used to reach an Administrator/Moderator of the instance",
-    "notify_email": "Notify email",
-    "notify_email_label": "Email used for notifications",
-    "description": "Description",
-    "description_label": "The instance’s description, can be seen in nodeinfo and /api/v1/instance",
-    "limit": "Limit",
-    "limit_label": "Posts character limit (CW/Subject included in the counter)",
-    "remote_limit": "Remote limit",
-    "remote_limit_label": "Hard character limit beyond which remote posts will be dropped.",
-    "upload_limit": "Upload limit",
-    "upload_limit_label": "File size limit of uploads (except for avatar, background, banner)",
-    "avatar_upload_limit": "Avatar upload limit",
-    "avatar_upload_limit_label": "File size limit of user’s profile avatars",
-    "background_upload_limit": "Background upload limit",
-    "background_upload_limit_label": "File size limit of user’s profile backgrounds",
-    "banner_upload_limit": "Banner upload limit",
-    "banner_upload_limit_label": "File size limit of user’s profile banners",
-    "registrations_open": "Registrations open",
-    "registrations_open_label": "Enable registrations for anyone, invitations can be enabled when false.",
-    "invites_enabled": "Invites enabled",
-    "invites_enabled_label": "Enable user invitations for admins (depends on registrations_open: false).",
-    "account_activation_required": "Account activation required",
-    "account_activation_required_label": "Require users to confirm their emails before signing in.",
-    "federating": "Federating",
-    "federating_label": "Enable federation with other instances",
-    "federation_reachability_timeout_days": "Federation reachability timeout days",
-    "federation_reachability_timeout_days_label": "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.",
-    "allow_relay": "Allow relay",
-    "allow_relay_label": "Enable Pleroma’s Relay, which makes it possible to follow a whole instance",
-    "rewrite_policy": "Rewrite policy",
-    "rewrite_policy_label": "Message Rewrite Policy, either one or a list. Here are the ones available by default:",
-    "public": "Public",
-    "public_label": "Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.",
-    "quarantined_instances": "Quarantined instances",
-    "quarantined_instances_label": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send.",
-    "managed_config": "Managed config",
-    "managed_config_label": "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
-    "allowed_post_formats": "Allowed post formats",
-    "allowed_post_formats_label": "MIME-type list of formats allowed to be posted (transformed into HTML)",
-    "mrf_transparency": "mrf transparency",
-    "mrf_transparency_label": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
-    "scope_copy": "Scope copy",
-    "scope_copy_label": "Copy the scope (private/unlisted/public) in replies to posts by default.",
-    "subject_line_behavior": "Subject line behavior",
-    "subject_line_behavior_label": "Allows changing the default behaviour of subject lines in replies",
-    "always_show_subject_input": "Always show subject input",
-    "always_show_subject_input_label": "When set to false, auto-hide the subject field when it's empty.",
-    "extended_nickname_format": "Extended nickname format",
-    "extended_nickname_format_label": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
-    "max_pinned_statuses": "Max pinned status",
-    "max_pinned_statuses_label": "The maximum number of pinned statuses. 0 will disable the feature.",
-    "autofollowed_nicknames": "Autofollowed nicknames",
-    "autofollowed_nicknames_label": "Set to nicknames of (local) users that every new user should automatically follow.",
-    "no_attachment_links": "No attachment links",
-    "no_attachment_links_label": "Set to true to disable automatically adding attachment link text to statuses",
-    "welcome_message": "Welcome message",
-    "welcome_message_label": "A message that will be send to a newly registered users as a direct message.",
-    "welcome_user_nickname": "Welcome user nickname",
-    "welcome_user_nickname_label": "The nickname of the local user that sends the welcome message.",
-    "max_report_comment_size": "Max report comment size",
-    "max_report_comment_size_label": "The maximum size of the report comment (Default: 1000)",
-    "safe_dm_mentions": "Safe dm mentions",
-    "safe_dm_mentions_label": " If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. \"@friend hey i really don't like @enemy\")",
-    "healthcheck": "Healthcheck",
-    "healthcheck_label": "If set to true, system data will be shown on /api/pleroma/healthcheck.",
-    "remote_post_retention_days": "Remote post retention days",
-    "remote_post_retention_days_label": "The default amount of days to retain remote posts when pruning the database.",
-    "skip_thread_containment": "Skip thread containment",
-    "limit_to_local_content": "Limit to local content",
-    "limit_to_local_content_label": "Limit unauthenticated users to search for local statutes and users only.",
-    "dynamic_configuration": "Dynamic configuration",
-    "dynamic_configuration_label": "Allow transferring configuration to DB with the subsequent customization from Admin api."
+    "upload_form": {
+      "uploader": "Uploader",
+      "uploads": "Uploads",
+      "s3_bucket": "S3 bucket name",
+      "s3_public_endpoint": "S3 endpoint that the user finally accesses",
+      "truncated_namespace": "Truncated_namespace",
+      "truncated_namespace_note": "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc.\n        For example, when using CDN to S3 virtual host format, set \"\".\n        At this time, write CNAME to CDN in public_endpoint.",
+      "filters": "Filters",
+      "args": "List of actions for the mogrify command",
+      "anonymize_filename": "Anonymize filename",
+      "anonymize_filename_note": "Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.",
+      "link_name": "Link name",
+      "link_name_note": "When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe",
+      "base_url": "Base URL",
+      "proxy_remote": "Proxy remote"
+    },
+    "emails": {
+      "adapter_title": "{name} adapter config",
+      "region": "Region",
+      "access_key": "Access key",
+      "secret": "Secret",
+      "api_key": "Api key",
+      "domain": "Domain",
+      "cmd_path": "cmd_path",
+      "cmd_args": "cmd_args",
+      "qmail": "qmail",
+      "relay": "relay",
+      "username": "username",
+      "password": "password",
+      "ssl": "ssl",
+      "tls": "tls",
+      "auth": "auth",
+      "port": "port",
+      "dkim": "dkim",
+      "retries": "retries",
+      "no_mx_lookups": "no_mx_lookups",
+      "server_id": "server id",
+      "endpoint": "endpoint"
+    },
+    "instance_form": {
+      "name": "Name",
+      "name_label": "The instance’s name",
+      "email": "Email",
+      "email_label": "Email used to reach an Administrator/Moderator of the instance",
+      "notify_email": "Notify email",
+      "notify_email_label": "Email used for notifications",
+      "description": "Description",
+      "description_label": "The instance’s description, can be seen in nodeinfo and /api/v1/instance",
+      "limit": "Limit",
+      "limit_label": "Posts character limit (CW/Subject included in the counter)",
+      "remote_limit": "Remote limit",
+      "remote_limit_label": "Hard character limit beyond which remote posts will be dropped.",
+      "upload_limit": "Upload limit",
+      "upload_limit_label": "File size limit of uploads (except for avatar, background, banner)",
+      "avatar_upload_limit": "Avatar upload limit",
+      "avatar_upload_limit_label": "File size limit of user’s profile avatars",
+      "background_upload_limit": "Background upload limit",
+      "background_upload_limit_label": "File size limit of user’s profile backgrounds",
+      "banner_upload_limit": "Banner upload limit",
+      "banner_upload_limit_label": "File size limit of user’s profile banners",
+      "registrations_open": "Registrations open",
+      "registrations_open_label": "Enable registrations for anyone, invitations can be enabled when false.",
+      "invites_enabled": "Invites enabled",
+      "invites_enabled_label": "Enable user invitations for admins (depends on registrations_open: false).",
+      "account_activation_required": "Account activation required",
+      "account_activation_required_label": "Require users to confirm their emails before signing in.",
+      "federating": "Federating",
+      "federating_label": "Enable federation with other instances",
+      "federation_reachability_timeout_days": "Federation reachability timeout days",
+      "federation_reachability_timeout_days_label": "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.",
+      "allow_relay": "Allow relay",
+      "allow_relay_label": "Enable Pleroma’s Relay, which makes it possible to follow a whole instance",
+      "rewrite_policy": "Rewrite policy",
+      "rewrite_policy_label": "Message Rewrite Policy, either one or a list. Here are the ones available by default:",
+      "public": "Public",
+      "public_label": "Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.",
+      "quarantined_instances": "Quarantined instances",
+      "quarantined_instances_label": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send.",
+      "managed_config": "Managed config",
+      "managed_config_label": "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
+      "allowed_post_formats": "Allowed post formats",
+      "allowed_post_formats_label": "MIME-type list of formats allowed to be posted (transformed into HTML)",
+      "mrf_transparency": "mrf transparency",
+      "mrf_transparency_label": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
+      "scope_copy": "Scope copy",
+      "scope_copy_label": "Copy the scope (private/unlisted/public) in replies to posts by default.",
+      "subject_line_behavior": "Subject line behavior",
+      "subject_line_behavior_label": "Allows changing the default behaviour of subject lines in replies",
+      "always_show_subject_input": "Always show subject input",
+      "always_show_subject_input_label": "When set to false, auto-hide the subject field when it's empty.",
+      "extended_nickname_format": "Extended nickname format",
+      "extended_nickname_format_label": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
+      "max_pinned_statuses": "Max pinned status",
+      "max_pinned_statuses_label": "The maximum number of pinned statuses. 0 will disable the feature.",
+      "autofollowed_nicknames": "Autofollowed nicknames",
+      "autofollowed_nicknames_label": "Set to nicknames of (local) users that every new user should automatically follow.",
+      "no_attachment_links": "No attachment links",
+      "no_attachment_links_label": "Set to true to disable automatically adding attachment link text to statuses",
+      "welcome_message": "Welcome message",
+      "welcome_message_label": "A message that will be send to a newly registered users as a direct message.",
+      "welcome_user_nickname": "Welcome user nickname",
+      "welcome_user_nickname_label": "The nickname of the local user that sends the welcome message.",
+      "max_report_comment_size": "Max report comment size",
+      "max_report_comment_size_label": "The maximum size of the report comment (Default: 1000)",
+      "safe_dm_mentions": "Safe dm mentions",
+      "safe_dm_mentions_label": " If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. \"@friend hey i really don't like @enemy\")",
+      "healthcheck": "Healthcheck",
+      "healthcheck_label": "If set to true, system data will be shown on /api/pleroma/healthcheck.",
+      "remote_post_retention_days": "Remote post retention days",
+      "remote_post_retention_days_label": "The default amount of days to retain remote posts when pruning the database.",
+      "skip_thread_containment": "Skip thread containment",
+      "limit_to_local_content": "Limit to local content",
+      "limit_to_local_content_label": "Limit unauthenticated users to search for local statutes and users only.",
+      "dynamic_configuration": "Dynamic configuration",
+      "dynamic_configuration_label": "Allow transferring configuration to DB with the subsequent customization from Admin api."
+    }
   }
 }
-- 
GitLab


From 35a5c45417c6985d959f41f43c8405049ec8e509 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 9 Jul 2019 16:34:44 +0300
Subject: [PATCH 12/61] feat: getConfigValue for api and normalize upload
 config object

---
 .../configSettings/forms/UploadForm.vue       |  6 +--
 .../configSettings/ConfigSettingsPage.vue     | 19 ++++++---
 src/entities/settings/UploadConfig.ts         |  2 +-
 src/services/ConfigService.ts                 |  3 +-
 src/utils/ConvertConfigToApiRequest.js        | 40 ++++++++++++++++++-
 5 files changed, 57 insertions(+), 13 deletions(-)

diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index e1deb6c..706ce27 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -5,12 +5,12 @@
       :options="selectOptions.uploader"
       :label="$t('config_settings.upload_form.uploader')"
     />
-    <div v-if="formData.uploader === 'Local'" class="mx-4">
+    <div v-if="formData.uploader === 'Pleroma.Uploaders.Local'" class="mx-4">
       <va-input
         v-model="formData.uploads"
         :label="$t('config_settings.upload_form.uploads')"/>
     </div>
-    <div v-if="formData.uploader === 'S3'" class="mx-4 my-3">
+    <div v-if="formData.uploader === 'Pleroma.Uploaders.S3'" class="mx-4 my-3">
       <va-input
         v-model="formData.bucket"
         :label="$t('config_settings.upload_form.s3_bucket')"
@@ -79,7 +79,7 @@ export default class UploadForm extends Vue {
     this.$emit('updateForm', val)
   }
   selectOptions = {
-    uploader: ['Local', 'S3'],
+    uploader: ['Pleroma.Uploaders.Local', 'Pleroma.Uploaders.S3'],
     filter: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
     args: ['strip', 'auto-orient', `{'impode': '1'}`]
   }
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index bedeb57..e0d457e 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -47,15 +47,24 @@ export default class ConfigSettingsPage extends Vue {
   async mounted () {
     this.loading = true
     const { configs } = await ConfigService.listConfigSettings()
+    this.loadConfigs(configs)
+    this.loading = false
+  }
+  async onSaveButtunClick () {
+    this.loading = true
+    const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest({
+      'upload': this.upload,
+      'emails': this.emails,
+      'instance': this.instance
+    }))
+    this.loadConfigs(configs)
+    this.loading = false
+  }
+  loadConfigs (configs) {
     const { upload, emails, instance } = ConvertConfigToState(configs)
     this.upload = upload
     this.emails = emails
     this.instance = instance
-    this.loading = false
-  }
-  async onSaveButtunClick () {
-    console.log('safe settings', this.upload)
-    const data = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest([this.upload, this.emails, this.instance]))
   }
 }
 </script>
diff --git a/src/entities/settings/UploadConfig.ts b/src/entities/settings/UploadConfig.ts
index 46174b7..dbd13e6 100644
--- a/src/entities/settings/UploadConfig.ts
+++ b/src/entities/settings/UploadConfig.ts
@@ -3,7 +3,7 @@ export default class UploadConfig {
   }
   uploader: string = 'Pleroma.Uploaders.Local'
   filters: Array<string> = []
-  upload: string = ''
+  uploads: string = ''
   bucket: string = ''
   public_endpoint: string = ''
   truncated_namespace
diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts
index bf2fd7a..3298bd0 100644
--- a/src/services/ConfigService.ts
+++ b/src/services/ConfigService.ts
@@ -9,7 +9,6 @@ export class ConfigService {
   }
 
   static updateConfigSettings (configs) {
-    return Promise.resolve(configs)
-    // return executeApiRequest('put', urlBuilder(Url.configSettings, {}), {})
+    return executeApiRequest('put', urlBuilder(Url.configSettings, {}), {data: configs})
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 12bc34f..d793fce 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,4 +1,40 @@
+import { forIn, isEmpty } from 'lodash'
+
 export default (configs) => {
-  console.log('convert', configs)
-  return configs
+  const settings = []
+  if (configs['upload']) {
+    const upload = {
+      group: 'pleroma',
+      key: 'Pleroma.Upload',
+      value: getConfigValue(normalizeUploadConfigValue(configs['upload']))
+    }
+    settings.push(upload)
+  }
+  return { configs: settings }
+}
+
+const normalizeUploadConfigValue = (config) => {
+  if (config.uploader === 'Pleroma.Uploaders.Local') {
+    delete config.s3_bucket
+    delete config.s3_public_endpoint
+    delete config.truncated_namespace
+  }
+  if (config.uploader === 'Pleroma.Uploaders.S3') {
+    delete config.uploads
+  }
+  return config
+}
+
+const getConfigValue = (config) => {
+  const newConfig = {}
+  forIn(config, (val, key) => {
+    if (!isEmpty(val)) {
+      if (typeof val === 'boolean') {
+        newConfig[key] = `:${val}`
+      } else {
+        newConfig[key] = val
+      }
+    }
+  })
+  return newConfig
 }
-- 
GitLab


From 8218a5fa1dd2f0bf38f6512e82348d25f6ab2e2e Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 9 Jul 2019 17:53:08 +0300
Subject: [PATCH 13/61] feat: captcha config

---
 .../configSettings/forms/CaptchaForm.vue      | 53 +++++++++++++++++++
 .../configSettings/forms/EmailsForm.vue       |  2 +-
 .../configSettings/forms/InstanceForm.vue     |  2 +-
 .../configSettings/forms/UploadForm.vue       |  4 +-
 .../configSettings/ConfigSettingsPage.vue     | 17 ++++--
 src/entities/settings/CaptchaConfig.ts        |  6 +++
 src/i18n/en.json                              |  8 +++
 src/services/ConfigService.ts                 |  2 +-
 src/utils/ConvertConfigToApiRequest.js        | 15 ++++++
 src/utils/ConvertConfigToState.js             | 12 +++--
 10 files changed, 106 insertions(+), 15 deletions(-)
 create mode 100644 src/components/configSettings/forms/CaptchaForm.vue
 create mode 100644 src/entities/settings/CaptchaConfig.ts

diff --git a/src/components/configSettings/forms/CaptchaForm.vue b/src/components/configSettings/forms/CaptchaForm.vue
new file mode 100644
index 0000000..3344448
--- /dev/null
+++ b/src/components/configSettings/forms/CaptchaForm.vue
@@ -0,0 +1,53 @@
+<template>
+  <div>
+    <va-checkbox
+      v-model="formData.enabled"
+      :label="$t('config_settings.captcha_form.enabled')"
+    />
+    <p class="note">{{$t('config_settings.captcha_form.enabled_note')}}</p>
+    <va-select
+      v-model="formData.method"
+      :options="selectOptions.method"
+      :label="$t('config_settings.captcha_form.method')"
+    />
+    <p class="note">{{$t('config_settings.captcha_form.method_note')}}</p>
+    <div v-if="formData.method === 'Pleroma.Captcha.Kocaptcha'" class="mx-4 my-3">
+      <va-input
+        v-model="formData.endpoint"
+        :label="$t('config_settings.captcha_form.endpoint')"
+      />
+    </div>
+    <va-input
+      v-model.number="formData.seconds_valid"
+      type="number"
+      :label="$t('config_settings.captcha_form.seconds_valid')"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import UploadConfig from '../../../entities/settings/UploadConfig'
+
+@Component({
+  components: {},
+})
+export default class UploadForm extends Vue {
+  @Prop(Object) readonly value!: UploadConfig
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  selectOptions = {
+    method: ['Pleroma.Captcha.Kocaptcha'],
+  }
+}
+</script>
+
+<style lang="scss">
+  .upload {
+
+  }
+</style>
diff --git a/src/components/configSettings/forms/EmailsForm.vue b/src/components/configSettings/forms/EmailsForm.vue
index d99023f..56b3683 100644
--- a/src/components/configSettings/forms/EmailsForm.vue
+++ b/src/components/configSettings/forms/EmailsForm.vue
@@ -44,7 +44,7 @@ import AmazonSESAdapter from '../emailAdapters/AmazonSESAdapter.vue'
   },
 })
 export default class EmailsForm extends Vue {
-  @Prop(EmailsConfig) readonly value!
+  @Prop(Object) readonly value!: EmailsConfig
   get formData () {
     return this.value
   }
diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
index b9eaba2..0bc77e8 100644
--- a/src/components/configSettings/forms/InstanceForm.vue
+++ b/src/components/configSettings/forms/InstanceForm.vue
@@ -137,7 +137,7 @@ import InstanceConfig from '../../../entities/settings/InstanceConfig'
 })
 
 export default class InstanceForm extends Vue {
-  @Prop(InstanceConfig) readonly value!
+  @Prop(Object) readonly value!: InstanceConfig
   get formData () {
     return this.value
   }
diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index 706ce27..a90824a 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -5,7 +5,7 @@
       :options="selectOptions.uploader"
       :label="$t('config_settings.upload_form.uploader')"
     />
-    <div v-if="formData.uploader === 'Pleroma.Uploaders.Local'" class="mx-4">
+    <div v-if="formData.uploader === 'Pleroma.Uploaders.Local'" class="mx-4 my-3">
       <va-input
         v-model="formData.uploads"
         :label="$t('config_settings.upload_form.uploads')"/>
@@ -71,7 +71,7 @@ import UploadConfig from '../../../entities/settings/UploadConfig'
   components: {},
 })
 export default class UploadForm extends Vue {
-  @Prop(UploadConfig) value!: object
+  @Prop(Object) value!: UploadConfig
   get formData () {
     return this.value
   }
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index e0d457e..4594f69 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -12,7 +12,8 @@
       <div class="config-settings-page__content pt-4">
         <upload-form v-if="configKeys[value].key === 'Pleroma.Upload'" v-model="upload"/>
         <emails-form v-if="configKeys[value].key === 'Pleroma.Emails'" v-model="emails"/>
-        <instance-config v-if="configKeys[value].key === ':instance'" v-model="instance"/>
+        <instance-form v-if="configKeys[value].key === ':instance'" v-model="instance"/>
+        <captcha-form v-if="configKeys[value].key === 'Pleroma.Captcha'" v-model="captcha"/>
       </div>
       <div class="flex-center pb-4">
         <va-button @click="onSaveButtunClick">Safe settings</va-button>
@@ -28,20 +29,24 @@ import { ConfigService } from '../../../services/ConfigService'
 import { configKeys } from '../../../data/Config'
 import UploadForm from '../../configSettings/forms/UploadForm.vue'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
-import InstanceConfig from '../../configSettings/forms/InstanceForm.vue'
+import InstanceForm from '../../configSettings/forms/InstanceForm.vue'
 import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import UploadConfig from '../../../entities/settings/UploadConfig'
 import EmailsConfig from '../../../entities/settings/EmailsConfig'
+import InstanceConfig from '../../../entities/settings/InstanceConfig'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest';
+import CaptchaForm from '../../configSettings/forms/CaptchaForm.vue'
+import CaptchaConfig from '../../../entities/settings/CaptchaConfig'
 
 @Component({
-  components: { InstanceConfig, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
+  components: { CaptchaForm, InstanceForm, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
 })
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
   upload?: UploadConfig = new UploadConfig()
   emails?: EmailsConfig = new EmailsConfig()
   instance?: InstanceConfig = new InstanceConfig()
+  captcha?: CaptchaConfig = new CaptchaConfig()
   configKeys: Array<object> = configKeys
   loading:boolean = false
   async mounted () {
@@ -55,16 +60,18 @@ export default class ConfigSettingsPage extends Vue {
     const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest({
       'upload': this.upload,
       'emails': this.emails,
-      'instance': this.instance
+      'instance': this.instance,
+      'captcha': this.captcha,
     }))
     this.loadConfigs(configs)
     this.loading = false
   }
   loadConfigs (configs) {
-    const { upload, emails, instance } = ConvertConfigToState(configs)
+    const { upload, emails, instance, captcha } = ConvertConfigToState(configs)
     this.upload = upload
     this.emails = emails
     this.instance = instance
+    this.captcha = captcha
   }
 }
 </script>
diff --git a/src/entities/settings/CaptchaConfig.ts b/src/entities/settings/CaptchaConfig.ts
new file mode 100644
index 0000000..c68ede7
--- /dev/null
+++ b/src/entities/settings/CaptchaConfig.ts
@@ -0,0 +1,6 @@
+export default class CaptchaConfig {
+  enabled: boolean = false
+  method: string = 'Pleroma.Captcha.Kocaptcha'
+  seconds_valid?: number
+  endpoint: string = 'https://captcha.kotobank.ch'
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index ab12ff6..af2c9ac 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -551,6 +551,14 @@
       "limit_to_local_content_label": "Limit unauthenticated users to search for local statutes and users only.",
       "dynamic_configuration": "Dynamic configuration",
       "dynamic_configuration_label": "Allow transferring configuration to DB with the subsequent customization from Admin api."
+    },
+    "captcha_form": {
+      "enabled": "Enabled",
+      "enabled_note": "Whether the captcha should be shown on registration",
+      "method": "Method",
+      "method_note": "The method/service to use for captcha",
+      "seconds_valid": "Seconds valid",
+      "endpoint": "The kocaptcha endpoint to use"
     }
   }
 }
diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts
index 3298bd0..5160e13 100644
--- a/src/services/ConfigService.ts
+++ b/src/services/ConfigService.ts
@@ -9,6 +9,6 @@ export class ConfigService {
   }
 
   static updateConfigSettings (configs) {
-    return executeApiRequest('put', urlBuilder(Url.configSettings, {}), {data: configs})
+    return executeApiRequest('post', urlBuilder(Url.configSettings, {}), {data: configs})
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index d793fce..ee75185 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -10,6 +10,13 @@ export default (configs) => {
     }
     settings.push(upload)
   }
+  if (configs['captcha']) {
+    settings.push({
+      group: 'pleroma',
+      key: 'Pleroma.Captcha',
+      value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
+    })
+  }
   return { configs: settings }
 }
 
@@ -25,6 +32,13 @@ const normalizeUploadConfigValue = (config) => {
   return config
 }
 
+const normalizeCaptchaConfigValue = (config) => {
+  if (config.method !== 'Pleroma.Captcha.Kocaptcha') {
+    delete config.endpoint
+  }
+  return config
+}
+
 const getConfigValue = (config) => {
   const newConfig = {}
   forIn(config, (val, key) => {
@@ -36,5 +50,6 @@ const getConfigValue = (config) => {
       }
     }
   })
+  console.log('newConfig', newConfig)
   return newConfig
 }
diff --git a/src/utils/ConvertConfigToState.js b/src/utils/ConvertConfigToState.js
index b2f3eab..22d18aa 100644
--- a/src/utils/ConvertConfigToState.js
+++ b/src/utils/ConvertConfigToState.js
@@ -1,11 +1,13 @@
-import UploadConfig from '../entities/settings/UploadConfig';
-import EmailsConfig from '../entities/settings/EmailsConfig';
-import InstanceConfig from '../entities/settings/InstanceConfig';
+import UploadConfig from '../entities/settings/UploadConfig'
+import EmailsConfig from '../entities/settings/EmailsConfig'
+import InstanceConfig from '../entities/settings/InstanceConfig'
+import CaptchaConfig from '../entities/settings/CaptchaConfig'
 
 export default (configs) => {
   return {
-    upload: new UploadConfig(configs.find(({ key }) => key === 'Pleroma.Upload')),
+    upload: new UploadConfig(),
     emails: new EmailsConfig(),
-    instance: new InstanceConfig()
+    instance: new InstanceConfig(),
+    captcha: new CaptchaConfig()
   }
 }
-- 
GitLab


From ef69176ec4aa694d4ff19ad0337b01384824ea05 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 11 Jul 2019 16:19:02 +0300
Subject: [PATCH 14/61] feat: add config keys enums, optimise API requests

---
 .../configSettings/ConfigSettingsPage.vue     | 15 +++++-----
 src/data/Config.js                            | 26 ----------------
 src/data/Config.ts                            | 30 +++++++++++++++++++
 src/entities/settings/CaptchaConfig.ts        |  5 ++++
 src/entities/settings/UploadConfig.ts         |  3 ++
 src/utils/ConvertConfigToApiRequest.js        | 11 ++-----
 src/utils/ConvertConfigToState.js             | 13 --------
 src/utils/ConvertConfigToState.ts             | 22 ++++++++++++++
 8 files changed, 70 insertions(+), 55 deletions(-)
 delete mode 100644 src/data/Config.js
 create mode 100644 src/data/Config.ts
 delete mode 100644 src/utils/ConvertConfigToState.js
 create mode 100644 src/utils/ConvertConfigToState.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 4594f69..a42cf3e 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -3,17 +3,17 @@
     <va-card class="config-settings-page" title="Settings">
       <va-tabs v-model="value">
         <va-tab
-          v-for="item in configKeys"
+          v-for="item in configKeysTabs"
           :key="item.key"
         >
           {{item.name}}
         </va-tab>
       </va-tabs>
       <div class="config-settings-page__content pt-4">
-        <upload-form v-if="configKeys[value].key === 'Pleroma.Upload'" v-model="upload"/>
-        <emails-form v-if="configKeys[value].key === 'Pleroma.Emails'" v-model="emails"/>
-        <instance-form v-if="configKeys[value].key === ':instance'" v-model="instance"/>
-        <captcha-form v-if="configKeys[value].key === 'Pleroma.Captcha'" v-model="captcha"/>
+        <upload-form v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD" v-model="upload"/>
+        <emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>
+        <instance-form v-if="configKeysTabs[value].key === configKeysEnum.INSTANCE" v-model="instance"/>
+        <captcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="captcha"/>
       </div>
       <div class="flex-center pb-4">
         <va-button @click="onSaveButtunClick">Safe settings</va-button>
@@ -26,7 +26,7 @@
 import { Component, Vue } from 'vue-property-decorator'
 import { FulfillingBouncingCircleSpinner } from 'epic-spinners'
 import { ConfigService } from '../../../services/ConfigService'
-import { configKeys } from '../../../data/Config'
+import { configKeysTabs, configKeys } from '../../../data/Config'
 import UploadForm from '../../configSettings/forms/UploadForm.vue'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
 import InstanceForm from '../../configSettings/forms/InstanceForm.vue'
@@ -47,7 +47,8 @@ export default class ConfigSettingsPage extends Vue {
   emails?: EmailsConfig = new EmailsConfig()
   instance?: InstanceConfig = new InstanceConfig()
   captcha?: CaptchaConfig = new CaptchaConfig()
-  configKeys: Array<object> = configKeys
+  configKeysTabs: Array<object> = configKeysTabs
+  configKeysEnum: Enumerator = configKeys
   loading:boolean = false
   async mounted () {
     this.loading = true
diff --git a/src/data/Config.js b/src/data/Config.js
deleted file mode 100644
index f12caca..0000000
--- a/src/data/Config.js
+++ /dev/null
@@ -1,26 +0,0 @@
-export const configKeys = [
-  {
-    key: 'Pleroma.Upload',
-    name: 'Upload',
-  },
-  {
-    key: 'Pleroma.Emails',
-    name: 'Emails'
-  },
-  {
-    key: ':instance',
-    name: 'Instance'
-  },
-  {
-    key: 'Pleroma.Web',
-    name: 'Web'
-  },
-  {
-    key: 'Pleroma.Captcha',
-    name: 'Captcha'
-  },
-  {
-    key: 'Pleroma.ScheduledActivity',
-    name: 'ScheduledActivity'
-  }
-]
diff --git a/src/data/Config.ts b/src/data/Config.ts
new file mode 100644
index 0000000..bc7e8bd
--- /dev/null
+++ b/src/data/Config.ts
@@ -0,0 +1,30 @@
+export enum configKeys {
+  UPLOAD = 'Pleroma.Upload',
+  EMAILS = 'Pleroma.Emails',
+  INSTANCE = ':instance',
+  WEB = 'Pleroma.Web',
+  CAPTCHA = 'Pleroma.Captcha',
+}
+
+export const configKeysTabs = [
+  {
+    key: configKeys.UPLOAD,
+    name: 'Upload',
+  },
+  {
+    key: configKeys.EMAILS,
+    name: 'Emails'
+  },
+  {
+    key: configKeys.INSTANCE,
+    name: 'Instance'
+  },
+  {
+    key: configKeys.WEB,
+    name: 'Web'
+  },
+  {
+    key: configKeys.CAPTCHA,
+    name: 'Captcha'
+  }
+]
diff --git a/src/entities/settings/CaptchaConfig.ts b/src/entities/settings/CaptchaConfig.ts
index c68ede7..20047a1 100644
--- a/src/entities/settings/CaptchaConfig.ts
+++ b/src/entities/settings/CaptchaConfig.ts
@@ -1,4 +1,9 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
 export default class CaptchaConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
   enabled: boolean = false
   method: string = 'Pleroma.Captcha.Kocaptcha'
   seconds_valid?: number
diff --git a/src/entities/settings/UploadConfig.ts b/src/entities/settings/UploadConfig.ts
index dbd13e6..539717e 100644
--- a/src/entities/settings/UploadConfig.ts
+++ b/src/entities/settings/UploadConfig.ts
@@ -1,5 +1,8 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
 export default class UploadConfig {
   constructor (existConfig?) {
+    normalizeApiConfig(existConfig, this)
   }
   uploader: string = 'Pleroma.Uploaders.Local'
   filters: Array<string> = []
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index ee75185..46452b1 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -40,16 +40,9 @@ const normalizeCaptchaConfigValue = (config) => {
 }
 
 const getConfigValue = (config) => {
-  const newConfig = {}
+  const newConfig = []
   forIn(config, (val, key) => {
-    if (!isEmpty(val)) {
-      if (typeof val === 'boolean') {
-        newConfig[key] = `:${val}`
-      } else {
-        newConfig[key] = val
-      }
-    }
+    newConfig.push({ tuple: [`:${key}`, val] })
   })
-  console.log('newConfig', newConfig)
   return newConfig
 }
diff --git a/src/utils/ConvertConfigToState.js b/src/utils/ConvertConfigToState.js
deleted file mode 100644
index 22d18aa..0000000
--- a/src/utils/ConvertConfigToState.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import UploadConfig from '../entities/settings/UploadConfig'
-import EmailsConfig from '../entities/settings/EmailsConfig'
-import InstanceConfig from '../entities/settings/InstanceConfig'
-import CaptchaConfig from '../entities/settings/CaptchaConfig'
-
-export default (configs) => {
-  return {
-    upload: new UploadConfig(),
-    emails: new EmailsConfig(),
-    instance: new InstanceConfig(),
-    captcha: new CaptchaConfig()
-  }
-}
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
new file mode 100644
index 0000000..2db5dc1
--- /dev/null
+++ b/src/utils/ConvertConfigToState.ts
@@ -0,0 +1,22 @@
+import UploadConfig from '../entities/settings/UploadConfig'
+import EmailsConfig from '../entities/settings/EmailsConfig'
+import InstanceConfig from '../entities/settings/InstanceConfig'
+import CaptchaConfig from '../entities/settings/CaptchaConfig'
+import { configKeys } from '../data/Config'
+
+export default (configs) => {
+  return {
+    upload: new UploadConfig(configs.find(({ key }) => key === configKeys.UPLOAD).value),
+    emails: new EmailsConfig(),
+    instance: new InstanceConfig(),
+    captcha: new CaptchaConfig(configs.find(({ key }) => key === configKeys.CAPTCHA).value)
+  }
+}
+
+export const normalizeApiConfig = function(existConfig, classObject) {
+  if (existConfig) {
+    existConfig.forEach(({tuple}) => {
+      classObject[tuple[0].substring(1)] = tuple[1]
+    })
+  }
+}
-- 
GitLab


From d71d6ba6f4131af64f149433e655275eb820e50f Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 12 Jul 2019 14:33:11 +0300
Subject: [PATCH 15/61] feat: finish upload config

---
 .../configSettings/forms/UploadForm.vue       | 69 +++++++++++++++++--
 .../configSettings/ConfigSettingsPage.vue     | 18 +++--
 src/entities/settings/UploadConfig.ts         | 17 ++++-
 src/i18n/en.json                              | 14 +++-
 src/utils/ConvertConfigToApiRequest.js        | 16 +++--
 src/utils/ConvertConfigToState.ts             | 22 +++++-
 6 files changed, 134 insertions(+), 22 deletions(-)

diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index a90824a..e92fce0 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="upload-form">
     <va-select
       v-model="formData.uploader"
       :options="selectOptions.uploader"
@@ -60,15 +60,71 @@
       v-model="formData.proxy_remote"
       :label="$t('config_settings.upload_form.proxy_remote')"
     />
+    <div class="my-4 pt-2 upload-form__proxy">
+      <p class="title">{{$t('config_settings.upload_form.proxy_options')}}</p>
+      <va-input-wrapper>
+        <va-checkbox
+          v-model="formData.proxy_opts.redirect_on_failure"
+          :label="$t('config_settings.upload_form.redirect_on_failure')"
+        />
+      </va-input-wrapper>
+      <p class="note">{{$t('config_settings.upload_form.redirect_on_failure_note')}}</p>
+      <va-input
+        class="mb-0"
+        v-model.number="formData.proxy_opts.max_body_length"
+        type="number"
+        :label="$t('config_settings.upload_form.max_body_length')"
+      />
+      <p class="note">{{$t('config_settings.upload_form.max_body_length_note')}}</p>
+      <va-input
+        class="mb-0"
+        v-model.number="formData.proxy_opts.max_read_duration"
+        type="number"
+        :label="$t('config_settings.upload_form.max_body_length')"
+      />
+      <p class="note">{{$t('config_settings.upload_form.max_body_length_note')}}</p>
+      <va-select
+        v-model="formData.proxy_opts.inline_content_types"
+        :options="selectOptions.inline_content_types"
+      />
+      <p class="note" v-if="formData.proxy_opts.inline_content_types !== 'a list of whitelisted content types'">
+        {{$t(`config_settings.upload_form.inline_content_types_${formData.proxy_opts.inline_content_types}`)}}
+      </p>
+      <va-input
+        v-model="formData.proxy_opts.req_headers"
+        :label="$t('config_settings.upload_form.req_headers')"
+      />
+      <va-input
+        class="mb-0"
+        v-model="formData.proxy_opts.resp_headers"
+        :label="$t('config_settings.upload_form.resp_headers')"
+      />
+      <p class="note">{{$t('config_settings.upload_form.req_headers_note')}}</p>
+    </div>
+    <div class="upload-form__http pt-3">
+      <p class="title">HTTP</p>
+      <va-input-wrapper>
+        <va-checkbox
+          v-model="formData.proxy_opts.http.follow_redirect"
+          :label="$t('config_settings.upload_form.http.follow_redirect')"
+        />
+      </va-input-wrapper>
+      <va-input
+        v-model="formData.proxy_opts.http.pool"
+        :label="$t('config_settings.upload_form.http.pool')"
+      />
+    </div>
   </div>
 </template>
 
 <script lang="ts">
 import { Component, Prop, Vue } from 'vue-property-decorator'
 import UploadConfig from '../../../entities/settings/UploadConfig'
+import VaSelect from "../../../vuestic-theme/vuestic-components/va-select/VaSelect.vue";
+import VaInputWrapper from "../../../vuestic-theme/vuestic-components/va-input/VaInputWrapper.vue";
 
 @Component({
-  components: {},
+  components: {VaInputWrapper, VaSelect},
 })
 export default class UploadForm extends Vue {
   @Prop(Object) value!: UploadConfig
@@ -81,13 +137,16 @@ export default class UploadForm extends Vue {
   selectOptions = {
     uploader: ['Pleroma.Uploaders.Local', 'Pleroma.Uploaders.S3'],
     filter: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
-    args: ['strip', 'auto-orient', `{'impode': '1'}`]
+    args: ['strip', 'auto-orient', `{'impode': '1'}`],
+    inline_content_types: ['true', 'false', 'a list of whitelisted content types']
   }
 }
 </script>
 
 <style lang="scss">
-.upload {
-
+.upload-form {
+  &__proxy, &__http {
+    border-top: 1px solid $border-color;
+  }
 }
 </style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index a42cf3e..dde9ab1 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -16,7 +16,7 @@
         <captcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="captcha"/>
       </div>
       <div class="flex-center pb-4">
-        <va-button @click="onSaveButtunClick">Safe settings</va-button>
+        <va-button @click="onSaveButtunClick">Save settings</va-button>
       </div>
     </va-card>
   </div>
@@ -34,7 +34,7 @@ import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import UploadConfig from '../../../entities/settings/UploadConfig'
 import EmailsConfig from '../../../entities/settings/EmailsConfig'
 import InstanceConfig from '../../../entities/settings/InstanceConfig'
-import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest';
+import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
 import CaptchaForm from '../../configSettings/forms/CaptchaForm.vue'
 import CaptchaConfig from '../../../entities/settings/CaptchaConfig'
 
@@ -48,7 +48,7 @@ export default class ConfigSettingsPage extends Vue {
   instance?: InstanceConfig = new InstanceConfig()
   captcha?: CaptchaConfig = new CaptchaConfig()
   configKeysTabs: Array<object> = configKeysTabs
-  configKeysEnum: Enumerator = configKeys
+  configKeysEnum: Enumerator<string> = configKeys
   loading:boolean = false
   async mounted () {
     this.loading = true
@@ -58,11 +58,12 @@ export default class ConfigSettingsPage extends Vue {
   }
   async onSaveButtunClick () {
     this.loading = true
+    const { upload, emails, instance, captcha } = this
     const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest({
-      'upload': this.upload,
-      'emails': this.emails,
-      'instance': this.instance,
-      'captcha': this.captcha,
+      upload,
+      emails,
+      instance,
+      captcha,
     }))
     this.loadConfigs(configs)
     this.loading = false
@@ -84,5 +85,8 @@ export default class ConfigSettingsPage extends Vue {
       opacity: .7;
       margin-bottom: 1rem;
     }
+    .va-select {
+      margin-bottom: 0;
+    }
   }
 </style>
diff --git a/src/entities/settings/UploadConfig.ts b/src/entities/settings/UploadConfig.ts
index 539717e..9779d4c 100644
--- a/src/entities/settings/UploadConfig.ts
+++ b/src/entities/settings/UploadConfig.ts
@@ -1,4 +1,17 @@
-import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+class ReverseProxy {
+  redirect_on_failure: boolean = false
+  max_body_length?: number
+  max_read_duration: number = 3000
+  inline_content_types: string = 'true'
+  req_headers: string = ''
+  resp_headers: string = ''
+  http: object = {
+    follow_redirect: true,
+    pool: ':upload'
+  }
+}
 
 export default class UploadConfig {
   constructor (existConfig?) {
@@ -15,5 +28,5 @@ export default class UploadConfig {
   link_name: boolean = false
   base_url: string = ''
   proxy_remote: boolean = false
-  proxy_opts?: any
+  proxy_opts?: ReverseProxy = new ReverseProxy()
 }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index af2c9ac..4777c65 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -450,7 +450,19 @@
       "link_name": "Link name",
       "link_name_note": "When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe",
       "base_url": "Base URL",
-      "proxy_remote": "Proxy remote"
+      "proxy_remote": "Proxy remote",
+      "proxy_options": "Proxy options",
+      "redirect_on_failure": "Redirect on failure",
+      "redirect_on_failure_note": "Redirects the client to the real remote URL if there's any HTTP errors. Any error during body processing will not be redirected as the response is chunked.",
+      "max_body_length": "Max body length",
+      "max_body_length_note": "limits the content length to be approximately the specified length. It is validated with the content-length header and also verified when proxying.",
+      "max_read_duration": "Max read duration",
+      "max_read_duration_note": "the total time the connection is allowed to read from the remote upstream.",
+      "inline_content_types_true": "will not alter `content-disposition` (up to the upstream),",
+      "inline_content_types_false": "will add content-disposition: attachment to any request",
+      "req_headers": "Req_headers",
+      "resp_headers": "Resp_headers",
+      "req_headers_note": "Additional headers"
     },
     "emails": {
       "adapter_title": "{name} adapter config",
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 46452b1..190bd7c 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,4 +1,4 @@
-import { forIn, isEmpty } from 'lodash'
+import { forIn } from 'lodash'
 
 export default (configs) => {
   const settings = []
@@ -41,8 +41,16 @@ const normalizeCaptchaConfigValue = (config) => {
 
 const getConfigValue = (config) => {
   const newConfig = []
-  forIn(config, (val, key) => {
-    newConfig.push({ tuple: [`:${key}`, val] })
-  })
+  createTupledObject(newConfig, config)
+  function createTupledObject (resultConfig, nestedObj) {
+    forIn(nestedObj, (val, key) => {
+      if (typeof val === 'object' && !Array.isArray(val)) {
+        resultConfig.push({ tuple: [`:${key}`, createTupledObject([], val)] })
+      } else {
+        resultConfig.push({ tuple: [`:${key}`, val] })
+      }
+    })
+    return resultConfig
+  }
   return newConfig
 }
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 2db5dc1..c51d360 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -5,18 +5,34 @@ import CaptchaConfig from '../entities/settings/CaptchaConfig'
 import { configKeys } from '../data/Config'
 
 export default (configs) => {
-  return {
+  const newConfig = {
     upload: new UploadConfig(configs.find(({ key }) => key === configKeys.UPLOAD).value),
     emails: new EmailsConfig(),
     instance: new InstanceConfig(),
     captcha: new CaptchaConfig(configs.find(({ key }) => key === configKeys.CAPTCHA).value)
   }
+  return newConfig
 }
 
 export const normalizeApiConfig = function(existConfig, classObject) {
   if (existConfig) {
-    existConfig.forEach(({tuple}) => {
-      classObject[tuple[0].substring(1)] = tuple[1]
+    parseObj(existConfig, classObject)
+  }
+  function parseObj(config, resultObject) {
+    config.forEach(({tuple}) => {
+      const key = tuple[0].substring(1)
+      const val = tuple[1]
+      if (Array.isArray(val)) {
+        const a = val.find(item => typeof item === 'object' && item.tuple)
+        if (a) {
+          resultObject[key] = parseObj(val, resultObject[key])
+        } else {
+          resultObject[key] = val
+        }
+      } else {
+        resultObject[key] = val
+      }
     })
+    return resultObject
   }
 }
-- 
GitLab


From 085db310532c6f5b9e20b336a059df2c4ab6d6bf Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 15 Jul 2019 11:31:27 +0300
Subject: [PATCH 16/61] feat: finish instance config, add ability to save maps

---
 .../configSettings/forms/InstanceForm.vue     | 33 +++++++++++++++++--
 src/entities/settings/InstanceConfig.ts       | 12 +++++++
 src/i18n/en.json                              | 10 +++++-
 src/utils/ConvertConfigToApiRequest.js        | 19 +++++++++--
 src/utils/ConvertConfigToState.ts             |  5 ++-
 5 files changed, 72 insertions(+), 7 deletions(-)

diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
index 0bc77e8..7fce65f 100644
--- a/src/components/configSettings/forms/InstanceForm.vue
+++ b/src/components/configSettings/forms/InstanceForm.vue
@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div class="instance_form">
     <va-input
       v-model="formData.name"
       :label="$t('config_settings.instance_form.name')"
@@ -62,6 +62,28 @@
       class="mb-0"
     />
     <p class="note">{{$t('config_settings.instance_form.banner_upload_limit_label')}}</p>
+    <div class="py-3 my-3 instance_form__poll-limits">
+      <p class="title">poll limits for <span :style="{color: $themes['warning']}">local</span> polls</p>
+      <va-input
+        v-model="formData.poll_limits.max_options"
+        type="number"
+        :label="$t('config_settings.instance_form.max_options_label')"
+      />
+      <va-input
+        v-model="formData.poll_limits.max_option_chars"
+        type="number"
+        :label="$t('config_settings.instance_form.max_option_chars')"
+      />
+      <va-input
+        v-model="formData.poll_limits.min_expiration"
+        type="number"
+        :label="$t('config_settings.instance_form.min_expiration')"
+      /> <va-input
+        v-model="formData.poll_limits.max_expiration"
+        type="number"
+        :label="$t('config_settings.instance_form.max_expiration')"
+      />
+    </div>
     <va-checkbox
       v-model="formData.registrations_open"
       :label="$t('config_settings.instance_form.registrations_open')"
@@ -125,6 +147,8 @@
     <p class="note">{{$t('config_settings.instance_form.limit_to_local_content_label')}}</p>
     <va-checkbox v-model="formData.dynamic_configuration" :label="$t('config_settings.instance_form.dynamic_configuration')" class="mb-0"/>
     <p class="note">{{$t('config_settings.instance_form.dynamic_configuration_label')}}</p>
+    <va-checkbox v-model="formData.external_user_synchronization" :label="$t('config_settings.instance_form.external_user_synchronization')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.instance_form.external_user_synchronization_note')}}</p>
   </div>
 </template>
 
@@ -164,5 +188,10 @@ export default class InstanceForm extends Vue {
 </script>
 
 <style scoped lang="scss">
-
+.instance_form {
+  &__poll-limits {
+    border-top: 1px solid $border-color;
+    border-bottom: 1px solid $border-color;
+  }
+}
 </style>
diff --git a/src/entities/settings/InstanceConfig.ts b/src/entities/settings/InstanceConfig.ts
index 7c1dff6..f719f7d 100644
--- a/src/entities/settings/InstanceConfig.ts
+++ b/src/entities/settings/InstanceConfig.ts
@@ -1,4 +1,9 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
 export default class InstanceConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
   name: string = ''
   email: string = ''
   notify_email: string = ''
@@ -9,6 +14,13 @@ export default class InstanceConfig {
   avatar_upload_limit: string = ''
   background_upload_limit: string = ''
   banner_upload_limit: string = ''
+  poll_limits: object = {
+    max_options:  20,
+    max_option_chars: 200,
+    min_expiration: 0,
+    max_expiration: 31536000,
+    sendAsMap: true
+  }
   registrations_open: boolean = false
   invites_enabled: boolean = false
   account_activation_required: boolean = false
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4777c65..8215cbb 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -508,6 +508,12 @@
       "background_upload_limit_label": "File size limit of user’s profile backgrounds",
       "banner_upload_limit": "Banner upload limit",
       "banner_upload_limit_label": "File size limit of user’s profile banners",
+      "poll_limits": "Poll limits",
+      "poll_limits_label": "A map with poll limits for local polls",
+      "max_options": "Maximum number of options",
+      "max_option_chars": "Maximum number of characters per option",
+      "min_expiration": "Minimum expiration time (in seconds)",
+      "max_expiration": "Maximum expiration time (in seconds)",
       "registrations_open": "Registrations open",
       "registrations_open_label": "Enable registrations for anyone, invitations can be enabled when false.",
       "invites_enabled": "Invites enabled",
@@ -562,7 +568,9 @@
       "limit_to_local_content": "Limit to local content",
       "limit_to_local_content_label": "Limit unauthenticated users to search for local statutes and users only.",
       "dynamic_configuration": "Dynamic configuration",
-      "dynamic_configuration_label": "Allow transferring configuration to DB with the subsequent customization from Admin api."
+      "dynamic_configuration_label": "Allow transferring configuration to DB with the subsequent customization from Admin api.",
+      "external_user_synchronization": "External user synchronization",
+      "external_user_synchronization_note": "Enabling following/followers counters synchronization for external users."
     },
     "captcha_form": {
       "enabled": "Enabled",
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 190bd7c..e8bc81b 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,11 +1,12 @@
 import { forIn } from 'lodash'
+import { configKeys } from '../data/Config';
 
 export default (configs) => {
   const settings = []
   if (configs['upload']) {
     const upload = {
       group: 'pleroma',
-      key: 'Pleroma.Upload',
+      key: configKeys.UPLOAD,
       value: getConfigValue(normalizeUploadConfigValue(configs['upload']))
     }
     settings.push(upload)
@@ -13,10 +14,17 @@ export default (configs) => {
   if (configs['captcha']) {
     settings.push({
       group: 'pleroma',
-      key: 'Pleroma.Captcha',
+      key: configKeys.CAPTCHA,
       value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
     })
   }
+  if (configs['instance']) {
+    settings.push({
+      group: 'pleroma',
+      key: configKeys.INSTANCE,
+      value: getConfigValue(normalizeCaptchaConfigValue(configs['instance']))
+    })
+  }
   return { configs: settings }
 }
 
@@ -45,7 +53,12 @@ const getConfigValue = (config) => {
   function createTupledObject (resultConfig, nestedObj) {
     forIn(nestedObj, (val, key) => {
       if (typeof val === 'object' && !Array.isArray(val)) {
-        resultConfig.push({ tuple: [`:${key}`, createTupledObject([], val)] })
+        if (val.sendAsMap) {
+          delete val.sendAsMap
+          resultConfig.push({ tuple: [`:${key}`, val] })
+        } else {
+          resultConfig.push({ tuple: [`:${key}`, createTupledObject([], val)] })
+        }
       } else {
         resultConfig.push({ tuple: [`:${key}`, val] })
       }
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index c51d360..416e428 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -8,7 +8,7 @@ export default (configs) => {
   const newConfig = {
     upload: new UploadConfig(configs.find(({ key }) => key === configKeys.UPLOAD).value),
     emails: new EmailsConfig(),
-    instance: new InstanceConfig(),
+    instance: new InstanceConfig(configs.find(({ key }) => key === configKeys.INSTANCE).value),
     captcha: new CaptchaConfig(configs.find(({ key }) => key === configKeys.CAPTCHA).value)
   }
   return newConfig
@@ -27,6 +27,9 @@ export const normalizeApiConfig = function(existConfig, classObject) {
         if (a) {
           resultObject[key] = parseObj(val, resultObject[key])
         } else {
+          if (resultObject[key].sendAsMap) {
+            val.sendAsMap = true
+          }
           resultObject[key] = val
         }
       } else {
-- 
GitLab


From 1f9ab1ab58cc066379cc96fba421454d31f3d5d9 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 16 Jul 2019 19:28:31 +0300
Subject: [PATCH 17/61] feat: frontend configurations

---
 .../forms/FrontendConfigurationsForm.vue      | 106 ++++++++++++++++++
 .../configSettings/forms/InstanceForm.vue     |   9 +-
 .../configSettings/forms/LoggerForm.vue       |  21 ++++
 .../configSettings/ConfigSettingsPage.vue     |  52 +++++----
 src/data/Config.ts                            |  10 ++
 .../settings/FrontentConfigurationsConfig.ts  |  30 +++++
 src/entities/settings/InstanceConfig.ts       |   3 -
 src/entities/settings/LoggerConfig.ts         |   2 +
 src/i18n/en.json                              |  36 +++++-
 src/services/StaticRecourcesService.ts        |   8 ++
 src/services/urlBuilder.ts                    |  34 +++---
 src/utils/ConvertConfigToApiRequest.js        |  46 ++++----
 src/utils/ConvertConfigToState.ts             |  40 +++----
 13 files changed, 300 insertions(+), 97 deletions(-)
 create mode 100644 src/components/configSettings/forms/FrontendConfigurationsForm.vue
 create mode 100644 src/components/configSettings/forms/LoggerForm.vue
 create mode 100644 src/entities/settings/FrontentConfigurationsConfig.ts
 create mode 100644 src/entities/settings/LoggerConfig.ts
 create mode 100644 src/services/StaticRecourcesService.ts

diff --git a/src/components/configSettings/forms/FrontendConfigurationsForm.vue b/src/components/configSettings/forms/FrontendConfigurationsForm.vue
new file mode 100644
index 0000000..20f4fe4
--- /dev/null
+++ b/src/components/configSettings/forms/FrontendConfigurationsForm.vue
@@ -0,0 +1,106 @@
+<template>
+  <div class="frontend-configurations-form">
+    <va-select
+      v-model="formData.pleroma_fe.theme"
+      :options="selectOptions.theme"
+      :label="$t('config_settings.frontend_configurations_form.theme')"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.theme_note')}}</p>
+    <va-input v-model="formData.pleroma_fe.logo" class="mb-0"/>
+    <p class="note">{{$t('config_settings.frontend_configurations_form.logo_note')}}</p>
+    <va-input
+      v-model="formData.pleroma_fe.background"
+      :label="$t('config_settings.frontend_configurations_form.background')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.background_note')}}</p>
+    <va-input
+      v-model="formData.pleroma_fe.redirectRootNoLogin"
+      class="mb-0"
+      :label="$t('config_settings.frontend_configurations_form.redirect_url')"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.redirect_root_no_login')}}</p>
+    <va-input
+      v-model="formData.pleroma_fe.redirectRootLogin"
+      class="mb-0"
+      :label="$t('config_settings.frontend_configurations_form.redirect_url')"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.redirect_root_login')}}</p>
+    <va-select
+      v-model="formData.pleroma_fe.subjectLineBehavior"
+      :label="$t('config_settings.frontend_configurations_form.subject_line_behavior')"
+      class="mb-0"
+      :options="selectOptions.subject_line_behavior"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.subject_line_behavior_label')}}</p>
+    <va-checkbox
+      v-model="formData.pleroma_fe.scopeOptionsEnabled"
+      :label="$t('config_settings.frontend_configurations_form.show_instance_specific_panel')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.show_instance_specific_panel_note')}}</p>
+    <va-checkbox v-model="formData.pleroma_fe.scope_copy" :label="$t('config_settings.frontend_configurations_form.scope_copy')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.frontend_configurations_form.scope_copy_label')}}</p>
+    <va-checkbox
+      v-model="formData.pleroma_fe.formattingOptionsEnabled"
+      :label="$t('config_settings.frontend_configurations_form.formatting_options_enabled')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.formatting_options_enabled_note')}}</p>
+    <va-checkbox v-model="formData.pleroma_fe.always_show_subject_input" :label="$t('config_settings.frontend_configurations_form.always_show_subject_input')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.frontend_configurations_form.always_show_subject_input_label')}}</p>
+    <va-checkbox
+      v-model="formData.pleroma_fe.collapseMessageWithSubject"
+      :label="$t('config_settings.frontend_configurations_form.collapse_message_with_subjects')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.frontend_configurations_form.collapse_message_with_subjects_note')}}</p>
+    <va-input-wrapper>
+      <va-checkbox
+        v-model="formData.pleroma_fe.hidePostStats"
+        :label="$t('config_settings.frontend_configurations_form.hide_post_stats')"
+        class="mb-0"
+      />
+    </va-input-wrapper>
+    <va-input-wrapper>
+      <va-checkbox
+        v-model="formData.pleroma_fe.hideUserStats"
+        :label="$t('config_settings.frontend_configurations_form.hide_user_stats')"
+        class="mb-0"
+      />
+    </va-input-wrapper>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import FrontendConfigurationsConfig from '../../../entities/settings/FrontentConfigurationsConfig'
+import { StaticRecourcesService } from '../../../services/StaticRecourcesService'
+import { keys } from 'lodash'
+
+@Component({
+  components: {}
+})
+// TODO: insert file uploaders
+export default class FrontendConfigurationsForm extends Vue {
+  @Prop(Object) readonly value!: FrontendConfigurationsConfig
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  selectOptions = {
+    theme: [],
+    subject_line_behavior: ['email', 'masto', 'noop'],
+  }
+  async mounted () {
+    const themeConfig = await StaticRecourcesService.getThemesList()
+    this.selectOptions.theme = keys(themeConfig)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
index 7fce65f..e0a4063 100644
--- a/src/components/configSettings/forms/InstanceForm.vue
+++ b/src/components/configSettings/forms/InstanceForm.vue
@@ -114,13 +114,7 @@
     <p class="note">{{$t('config_settings.instance_form.allowed_post_formats_label')}}</p>
     <va-checkbox v-model="formData.mrf_transparency" :label="$t('config_settings.instance_form.mrf_transparency')" class="mb-0"/>
     <p class="note">{{$t('config_settings.instance_form.mrf_transparency_label')}}</p>
-    <va-checkbox v-model="formData.scope_copy" :label="$t('config_settings.instance_form.scope_copy')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.instance_form.scope_copy_label')}}</p>
-    <va-input v-model="formData.subject_line_behavior" :label="$t('config_settings.instance_form.subject_line_behavior')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.instance_form.subject_line_behavior_label')}}</p>
-    <va-checkbox v-model="formData.always_show_subject_input" :label="$t('config_settings.instance_form.always_show_subject_input')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.instance_form.always_show_subject_input_label')}}</p>
-    <va-checkbox v-model="formData.extended_nickname_format" :label="$t('config_settings.instance_form.extended_nickname_format')" class="mb-0"/>
+   <va-checkbox v-model="formData.extended_nickname_format" :label="$t('config_settings.instance_form.extended_nickname_format')" class="mb-0"/>
     <p class="note">{{$t('config_settings.instance_form.extended_nickname_format_label')}}</p>
     <va-input v-model.number="formData.max_pinned_statuses" type="number" :label="$t('config_settings.instance_form.max_pinned_statuses')" class="mb-0"/>
     <p class="note">{{$t('config_settings.instance_form.max_pinned_statuses_label')}}</p>
@@ -181,7 +175,6 @@ export default class InstanceForm extends Vue {
       'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy'
     ],
     allowed_post_formats: [],
-    subject_line_behavior: ['email', 'masto', 'noop'],
     limit_to_local_content: [':unauthenticated', ':all', 'false']
   }
 }
diff --git a/src/components/configSettings/forms/LoggerForm.vue b/src/components/configSettings/forms/LoggerForm.vue
new file mode 100644
index 0000000..5d02f92
--- /dev/null
+++ b/src/components/configSettings/forms/LoggerForm.vue
@@ -0,0 +1,21 @@
+<template>
+<div class="logger-form">
+</div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import LoggerConfig from '../../../entities/settings/LoggerConfig'
+
+@Component({
+  components: {}
+})
+
+export default class InstanceForm extends Vue {
+  @Prop(Object) readonly value!: LoggerConfig
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index dde9ab1..9cec8e5 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -1,25 +1,24 @@
 <template>
-  <div>
-    <va-card class="config-settings-page" title="Settings">
-      <va-tabs v-model="value">
-        <va-tab
-          v-for="item in configKeysTabs"
-          :key="item.key"
-        >
-          {{item.name}}
-        </va-tab>
-      </va-tabs>
-      <div class="config-settings-page__content pt-4">
-        <upload-form v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD" v-model="upload"/>
-        <emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>
-        <instance-form v-if="configKeysTabs[value].key === configKeysEnum.INSTANCE" v-model="instance"/>
-        <captcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="captcha"/>
-      </div>
-      <div class="flex-center pb-4">
-        <va-button @click="onSaveButtunClick">Save settings</va-button>
-      </div>
-    </va-card>
-  </div>
+  <va-card class="config-settings-page" title="Settings">
+    <va-tabs v-model="value">
+      <va-tab
+        v-for="item in configKeysTabs"
+        :key="item.key"
+      >
+        {{item.name}}
+      </va-tab>
+    </va-tabs>
+    <div class="config-settings-page__content pt-4">
+      <!--<upload-form v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD" v-model="upload"/>-->
+      <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
+      <!--<instance-form v-if="configKeysTabs[value].key === configKeysEnum.INSTANCE" v-model="instance"/>-->
+      <frontend-configurations-form v-if="configKeysTabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS" v-model="frontendConfiguration"/>
+      <!--<captcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="captcha"/>-->
+    </div>
+    <div class="flex-center pb-4">
+      <va-button @click="onSaveButtunClick">Save settings</va-button>
+    </div>
+  </va-card>
 </template>
 
 <script lang="ts">
@@ -37,9 +36,11 @@ import InstanceConfig from '../../../entities/settings/InstanceConfig'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
 import CaptchaForm from '../../configSettings/forms/CaptchaForm.vue'
 import CaptchaConfig from '../../../entities/settings/CaptchaConfig'
+import FrontendConfigurationsForm from '../../configSettings/forms/FrontendConfigurationsForm.vue'
+import FrontentConfigurationsConfig from '../../../entities/settings/FrontentConfigurationsConfig'
 
 @Component({
-  components: { CaptchaForm, InstanceForm, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
+  components: { FrontendConfigurationsForm, CaptchaForm, InstanceForm, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
 })
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
@@ -47,6 +48,7 @@ export default class ConfigSettingsPage extends Vue {
   emails?: EmailsConfig = new EmailsConfig()
   instance?: InstanceConfig = new InstanceConfig()
   captcha?: CaptchaConfig = new CaptchaConfig()
+  frontendConfiguration?: FrontentConfigurationsConfig = new FrontentConfigurationsConfig()
   configKeysTabs: Array<object> = configKeysTabs
   configKeysEnum: Enumerator<string> = configKeys
   loading:boolean = false
@@ -58,22 +60,24 @@ export default class ConfigSettingsPage extends Vue {
   }
   async onSaveButtunClick () {
     this.loading = true
-    const { upload, emails, instance, captcha } = this
+    const { upload, emails, instance, captcha, frontendConfiguration } = this
     const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest({
       upload,
       emails,
       instance,
       captcha,
+      frontendConfiguration,
     }))
     this.loadConfigs(configs)
     this.loading = false
   }
   loadConfigs (configs) {
-    const { upload, emails, instance, captcha } = ConvertConfigToState(configs)
+    const { upload, emails, instance, captcha, frontendConfiguration } = ConvertConfigToState(configs)
     this.upload = upload
     this.emails = emails
     this.instance = instance
     this.captcha = captcha
+    this.frontendConfiguration = frontendConfiguration
   }
 }
 </script>
diff --git a/src/data/Config.ts b/src/data/Config.ts
index bc7e8bd..19a81a6 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -2,6 +2,8 @@ export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
   EMAILS = 'Pleroma.Emails',
   INSTANCE = ':instance',
+  LOGGER = ':logger',
+  FRONTEND_CONFIGURATIONS = ':frontend_configurations',
   WEB = 'Pleroma.Web',
   CAPTCHA = 'Pleroma.Captcha',
 }
@@ -19,6 +21,14 @@ export const configKeysTabs = [
     key: configKeys.INSTANCE,
     name: 'Instance'
   },
+  {
+    key: configKeys.LOGGER,
+    name: 'Logger'
+  },
+  {
+    key: configKeys.FRONTEND_CONFIGURATIONS,
+    name: 'Frontend configurations'
+  },
   {
     key: configKeys.WEB,
     name: 'Web'
diff --git a/src/entities/settings/FrontentConfigurationsConfig.ts b/src/entities/settings/FrontentConfigurationsConfig.ts
new file mode 100644
index 0000000..c429ca9
--- /dev/null
+++ b/src/entities/settings/FrontentConfigurationsConfig.ts
@@ -0,0 +1,30 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class FrontentConfigurationsConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  pleroma_fe: object = {
+    theme: 'pleroma-dark',
+    logo: '/static/logo.png',
+    background: '/images/city.jpg',
+    redirectRootNoLogin: '/main/all',
+    redirectRootLogin: '/main/friends',
+    hidePostStats: false,
+    hideUserStats: false,
+    collapseMessageWithSubject: false,
+    showInstanceSpecificPanel: true,
+    scopeOptionsEnabled: false,
+    formattingOptionsEnabled: false,
+    scopeCopy: true,
+    alwaysShowSubjectInput: true,
+    subjectLineBehavior: "email",
+    sendAsMap: true,
+  }
+  masto_fe: object = {
+    showInstanceSpecificPanel: true,
+    sendAsMap: true,
+  }
+  // logo_mask:boolean = false
+  // logo_margin?: number
+}
diff --git a/src/entities/settings/InstanceConfig.ts b/src/entities/settings/InstanceConfig.ts
index f719f7d..9c5e7dc 100644
--- a/src/entities/settings/InstanceConfig.ts
+++ b/src/entities/settings/InstanceConfig.ts
@@ -33,9 +33,6 @@ export default class InstanceConfig {
   managed_config: boolean = false
   allowed_post_formats: Array<string> = []
   mrf_transparency: boolean = false
-  scope_copy: boolean = false
-  subject_line_behavior: string = ''
-  always_show_subject_input: boolean = false
   extended_nickname_format: boolean = false
   max_pinned_statuses?: number
   autofollowed_nicknames: string = ''
diff --git a/src/entities/settings/LoggerConfig.ts b/src/entities/settings/LoggerConfig.ts
new file mode 100644
index 0000000..e22ddc2
--- /dev/null
+++ b/src/entities/settings/LoggerConfig.ts
@@ -0,0 +1,2 @@
+export default class LoggerConfig {
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 8215cbb..63b2d0a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -538,12 +538,6 @@
       "allowed_post_formats_label": "MIME-type list of formats allowed to be posted (transformed into HTML)",
       "mrf_transparency": "mrf transparency",
       "mrf_transparency_label": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
-      "scope_copy": "Scope copy",
-      "scope_copy_label": "Copy the scope (private/unlisted/public) in replies to posts by default.",
-      "subject_line_behavior": "Subject line behavior",
-      "subject_line_behavior_label": "Allows changing the default behaviour of subject lines in replies",
-      "always_show_subject_input": "Always show subject input",
-      "always_show_subject_input_label": "When set to false, auto-hide the subject field when it's empty.",
       "extended_nickname_format": "Extended nickname format",
       "extended_nickname_format_label": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
       "max_pinned_statuses": "Max pinned status",
@@ -572,6 +566,36 @@
       "external_user_synchronization": "External user synchronization",
       "external_user_synchronization_note": "Enabling following/followers counters synchronization for external users."
     },
+    "frontend_configurations_form" : {
+      "theme": "theme",
+      "theme_note": "Which theme to use",
+      "logo_note": "URL of the logo, defaults to Pleroma’s logo",
+      "logo_mask": "Logo mask",
+      "logo_mask_note": "Whether to use only the logo's shape as a mask (true) or as a regular image (false)",
+      "logo_margin": "Logo margin",
+      "logo_margin_note": "What margin to use around the logo",
+      "background": "Background",
+      "background_note": "URL of the background, unless viewing a user profile with a background that is set",
+      "redirect_url": "Redirect URL",
+      "redirect_root_no_login": "Relative URL which indicates where to redirect when a user isn’t logged in.",
+      "redirect_root_login": "Relative URL which indicates where to redirect when a user is logged in.",
+      "show_instance_specific_panel": "Show instance specific panel",
+      "show_instance_specific_panel_note": "Whenether to show the instance’s specific panel.",
+      "subject_line_behavior": "Subject line behavior",
+      "subject_line_behavior_label": "Allows changing the default behaviour of subject lines in replies",
+      "scope_options_enabled": "Enable scope options",
+      "scope_options_enabled_note": "Enable setting an notice visibility and subject/CW when posting",
+      "scope_copy": "Scope copy",
+      "scope_copy_label": "Copy the scope (private/unlisted/public) in replies to posts by default.",
+      "formatting_options_enabled": "Enable formatting options",
+      "formatting_options_enabled_note": "Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting",
+      "collapse_message_with_subjects": "Collapse message with subject",
+      "collapse_message_with_subjects_note": "When a message has a subject(aka Content Warning), collapse it by default",
+      "always_show_subject_input": "Always show subject input",
+      "always_show_subject_input_label": "When set to false, auto-hide the subject field when it's empty.",
+      "hide_post_stats": "Hide notices statistics",
+      "hide_user_stats": "Hide profile statistics"
+    },
     "captcha_form": {
       "enabled": "Enabled",
       "enabled_note": "Whether the captcha should be shown on registration",
diff --git a/src/services/StaticRecourcesService.ts b/src/services/StaticRecourcesService.ts
new file mode 100644
index 0000000..1837eb3
--- /dev/null
+++ b/src/services/StaticRecourcesService.ts
@@ -0,0 +1,8 @@
+import executeApiRequest from './executeApiRequest'
+import urlBuilder, {Url} from './urlBuilder'
+
+export class StaticRecourcesService {
+  static getThemesList () {
+    return executeApiRequest('get', urlBuilder(Url.getThemesList, {}), {})
+  }
+}
diff --git a/src/services/urlBuilder.ts b/src/services/urlBuilder.ts
index 232cefa..5c984bb 100644
--- a/src/services/urlBuilder.ts
+++ b/src/services/urlBuilder.ts
@@ -30,6 +30,7 @@ export enum Url {
   respondReport = 'respondReport',
   changeReportedStatus = 'changeReportedStatus',
   configSettings = 'configSettings',
+  getThemesList = 'getThemesList',
 }
 
 export enum UserData {
@@ -50,25 +51,24 @@ interface UrlBuilderOptions {
 }
 
 const urls = {
-  [Url.getUsers]: () => 'users',
-  [Url.getSimplifiedUser]: (options: UrlBuilderOptions) => `users/${options.id}`,
-  [Url.getUser]: (options: UrlBuilderOptions) => `${options.id}`,
-  [Url.getUserData]: (options: UrlBuilderOptions) => `${options.id}/${options.dataType}`,
-  [Url.toggleTag]: () => 'users/tag/',
-  [Url.toggleUserActivation]: (options: UrlBuilderOptions) => `users/${options.id}/activation_status`,
-  [Url.togglePermissionGroup]: (options: UrlBuilderOptions) => `users/${options.id}/permission_group/${options.permissionGroup}`,
-  [Url.deleteUser]: (options: UrlBuilderOptions) => `user`,
-  [Url.getReports]: (options: UrlBuilderOptions) => `reports`,
-  [Url.getReport]: (options: UrlBuilderOptions) => `reports/${options.id}`,
-  [Url.changeReportState]: (options: UrlBuilderOptions) => `reports/${options.id}`,
-  [Url.respondReport]: (options: UrlBuilderOptions) => `reports/${options.id}/respond`,
-  [Url.changeReportedStatus]: (options: UrlBuilderOptions) => `statuses/${options.id}`,
-  [Url.configSettings]: (options: UrlBuilderOptions) => `config`
+  [Url.getUsers]: () => 'api/pleroma/admin/users',
+  [Url.getSimplifiedUser]: (options: UrlBuilderOptions) => `api/pleroma/admin/users/${options.id}`,
+  [Url.getUser]: (options: UrlBuilderOptions) => `api/v1/accounts/${options.id}`,
+  [Url.getUserData]: (options: UrlBuilderOptions) => `api/v1/accounts/${options.id}/${options.dataType}`,
+  [Url.toggleTag]: () => 'api/pleroma/admin/users/tag/',
+  [Url.toggleUserActivation]: (options: UrlBuilderOptions) => `api/pleroma/admin/users/${options.id}/activation_status`,
+  [Url.togglePermissionGroup]: (options: UrlBuilderOptions) => `api/pleroma/admin/users/${options.id}/permission_group/${options.permissionGroup}`,
+  [Url.deleteUser]: (options: UrlBuilderOptions) => `api/pleroma/admin/user`,
+  [Url.getReports]: (options: UrlBuilderOptions) => `api/pleroma/admin/reports`,
+  [Url.getReport]: (options: UrlBuilderOptions) => `api/pleroma/admin/reports/${options.id}`,
+  [Url.changeReportState]: (options: UrlBuilderOptions) => `api/pleroma/admin/reports/${options.id}`,
+  [Url.respondReport]: (options: UrlBuilderOptions) => `api/pleroma/admin/reports/${options.id}/respond`,
+  [Url.changeReportedStatus]: (options: UrlBuilderOptions) => `api/pleroma/admin/statuses/${options.id}`,
+  [Url.configSettings]: (options: UrlBuilderOptions) => `api/pleroma/admin/config`,
+  [Url.getThemesList]: (options: UrlBuilderOptions) => 'static/styles.json',
 }
 
 export default (action: Url, options: UrlBuilderOptions):string => {
   const code = getAll()
-  return action === 'getUserData' || action === 'getUser'
-    ? `https://${code.instance}/api/v1/accounts/${urls[action](options)}`
-    : `https://${code.instance}/api/pleroma/admin/${urls[action](options)}`
+  return `https://${code.instance}/${urls[action](options)}`
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index e8bc81b..fb8ed18 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,28 +1,36 @@
 import { forIn } from 'lodash'
-import { configKeys } from '../data/Config';
+import { configKeys } from '../data/Config'
 
 export default (configs) => {
   const settings = []
-  if (configs['upload']) {
-    const upload = {
-      group: 'pleroma',
-      key: configKeys.UPLOAD,
-      value: getConfigValue(normalizeUploadConfigValue(configs['upload']))
-    }
-    settings.push(upload)
-  }
-  if (configs['captcha']) {
-    settings.push({
-      group: 'pleroma',
-      key: configKeys.CAPTCHA,
-      value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
-    })
-  }
-  if (configs['instance']) {
+  // TODO: Now we need to control API requests. Refactore it.
+  // if (configs['upload']) {
+  //   const upload = {
+  //     group: 'pleroma',
+  //     key: configKeys.UPLOAD,
+  //     value: getConfigValue(normalizeUploadConfigValue(configs['upload']))
+  //   }
+  //   settings.push(upload)
+  // }
+  // if (configs['captcha']) {
+  //   settings.push({
+  //     group: 'pleroma',
+  //     key: configKeys.CAPTCHA,
+  //     value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
+  //   })
+  // }
+  // if (configs['instance']) {
+  //   settings.push({
+  //     group: 'pleroma',
+  //     key: configKeys.INSTANCE,
+  //     value: getConfigValue(normalizeCaptchaConfigValue(configs['instance']))
+  //   })
+  // }
+  if (configs['frontendConfiguration']) {
     settings.push({
       group: 'pleroma',
-      key: configKeys.INSTANCE,
-      value: getConfigValue(normalizeCaptchaConfigValue(configs['instance']))
+      key: configKeys.FRONTEND_CONFIGURATIONS,
+      value: getConfigValue(configs['frontendConfiguration'])
     })
   }
   return { configs: settings }
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 416e428..c0b66e1 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -3,16 +3,17 @@ import EmailsConfig from '../entities/settings/EmailsConfig'
 import InstanceConfig from '../entities/settings/InstanceConfig'
 import CaptchaConfig from '../entities/settings/CaptchaConfig'
 import { configKeys } from '../data/Config'
+import FrontentConfigurationsConfig from '../entities/settings/FrontentConfigurationsConfig'
+import t from 'typy'
+import { forIn } from 'lodash'
 
-export default (configs) => {
-  const newConfig = {
-    upload: new UploadConfig(configs.find(({ key }) => key === configKeys.UPLOAD).value),
-    emails: new EmailsConfig(),
-    instance: new InstanceConfig(configs.find(({ key }) => key === configKeys.INSTANCE).value),
-    captcha: new CaptchaConfig(configs.find(({ key }) => key === configKeys.CAPTCHA).value)
-  }
-  return newConfig
-}
+export default (configs) => ({
+  upload: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
+  emails: new EmailsConfig(),
+  instance: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
+  captcha: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
+  frontendConfiguration: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject)
+})
 
 export const normalizeApiConfig = function(existConfig, classObject) {
   if (existConfig) {
@@ -20,20 +21,19 @@ export const normalizeApiConfig = function(existConfig, classObject) {
   }
   function parseObj(config, resultObject) {
     config.forEach(({tuple}) => {
-      const key = tuple[0].substring(1)
+      const key = tuple[0]
       const val = tuple[1]
-      if (Array.isArray(val)) {
-        const a = val.find(item => typeof item === 'object' && item.tuple)
-        if (a) {
-          resultObject[key] = parseObj(val, resultObject[key])
-        } else {
-          if (resultObject[key].sendAsMap) {
-            val.sendAsMap = true
-          }
-          resultObject[key] = val
+      if (typeof val === 'object' && !Array.isArray(val)) {
+        forIn(val, (value, innerKey) => {
+          delete val[innerKey]
+          val[innerKey.substring(1)] = value
+        })
+        if (resultObject[key.substring(1)].sendAsMap) {
+          val.sendAsMap = true
         }
+        resultObject[key.substring(1)] = val
       } else {
-        resultObject[key] = val
+        resultObject[key.substring(1)] = val
       }
     })
     return resultObject
-- 
GitLab


From 9d18295617ce7d84b7b544b18578ea6201e66469 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 17 Jul 2019 15:03:19 +0300
Subject: [PATCH 18/61] feat: instance config

---
 .../configSettings/forms/InstanceForm.vue     | 188 +++++++++++++++---
 .../configSettings/ConfigSettingsPage.vue     |   4 +-
 src/data/Config.ts                            |  40 ++--
 src/entities/settings/InstanceConfig.ts       |  28 ++-
 src/i18n/en.json                              |  13 +-
 src/utils/ConvertConfigToApiRequest.js        |  29 ++-
 6 files changed, 235 insertions(+), 67 deletions(-)

diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
index e0a4063..bdfe4c7 100644
--- a/src/components/configSettings/forms/InstanceForm.vue
+++ b/src/components/configSettings/forms/InstanceForm.vue
@@ -67,7 +67,7 @@
       <va-input
         v-model="formData.poll_limits.max_options"
         type="number"
-        :label="$t('config_settings.instance_form.max_options_label')"
+        :label="$t('config_settings.instance_form.max_options')"
       />
       <va-input
         v-model="formData.poll_limits.max_option_chars"
@@ -90,58 +90,189 @@
       class="mb-0"
     />
     <p class="note">{{$t('config_settings.instance_form.registrations_open_label')}}</p>
+    <va-checkbox
+      v-model="formData.dedupe_media"
+      :label="$t('config_settings.instance_form.dedupe_media')"
+      class="mb-3"
+    />
     <va-checkbox
       v-model="formData.invites_enabled"
-      :label="$t('config_settings.instance_form.invites_enabled')"/>
+      :label="$t('config_settings.instance_form.invites_enabled')"
+    />
     <p class="note">{{$t('config_settings.instance_form.invites_enabled_label')}}</p>
-    <va-checkbox v-model="formData.account_activation_required" :label="$t('config_settings.instance_form.account_activation_required')"/>
+    <va-checkbox
+      v-model="formData.account_activation_required"
+      :label="$t('config_settings.instance_form.account_activation_required')"
+    />
     <p class="note">{{$t('config_settings.instance_form.account_activation_required_label')}}</p>
-    <va-checkbox v-model="formData.federating" :label="$t('config_settings.instance_form.federating')"/>
+    <va-checkbox
+      v-model="formData.federating"
+      :label="$t('config_settings.instance_form.federating')"
+    />
     <p class="note">{{$t('config_settings.instance_form.federating_label')}}</p>
-    <va-input v-model.number="formData.federation_reachability_timeout_days" type="number" :label="$t('config_settings.instance_form.federation_reachability_timeout_days')"/>
+    <va-input
+      v-model.number="formData.federation_reachability_timeout_days"
+      type="number"
+      :label="$t('config_settings.instance_form.federation_reachability_timeout_days')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.federation_reachability_timeout_days_label')}}</p>
-    <va-checkbox v-model="formData.allow_relay" :label="$t('config_settings.instance_form.allow_relay_label')"/>
+    <va-input
+      v-model.number="formData.federation_incoming_replies_max_depth"
+      type="number"
+      :label="$t('config_settings.instance_form.federation_incoming_replies_max_depth')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.federation_incoming_replies_max_depth_label')}}</p>
+    <va-select
+      v-model="formData.federation_publisher_modules"
+      :options="selectOptions.federation_publisher_modules"
+      multiple
+      :label="$t('config_settings.instance_form.federation_publisher_modules')"
+    />
+    <p class="note">{{$t('config_settings.instance_form.federation_publisher_modules_label')}}</p>
+    <va-checkbox
+      v-model="formData.allow_relay"
+      :label="$t('config_settings.instance_form.allow_relay_label')"
+    />
     <p class="note">{{$t('config_settings.instance_form.allow_relay_label')}}</p>
-    <va-select v-model="formData.rewrite_policy" :options="selectOptions.rewrite_policy" :label="$t('config_settings.instance_form.invites_enabled')"/>
+    <va-select
+      v-model="formData.rewrite_policy"
+      :options="selectOptions.rewrite_policy"
+      :label="$t('config_settings.instance_form.invites_enabled')"
+    />
     <p class="note">{{$t('config_settings.instance_form.rewrite_policy_label')}}</p>
-    <va-checkbox v-model="formData.public" :label="$t('config_settings.instance_form.public')"/>
+    <va-checkbox
+      v-model="formData.public"
+      :label="$t('config_settings.instance_form.public')"
+    />
     <p class="note">{{$t('config_settings.instance_form.public_label')}}</p>
-    <va-input v-model="formData.quarantined_instances" :label="$t('config_settings.instance_form.quarantined_instances')" class="mb-0"/>
+    <va-input
+      v-model="formData.quarantined_instances"
+      :label="$t('config_settings.instance_form.quarantined_instances')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.quarantined_instances_label')}}</p>
-    <va-checkbox v-model="formData.managed_config" :label="$t('config_settings.instance_form.managed_config')"/>
+    <va-checkbox
+      v-model="formData.managed_config"
+      :label="$t('config_settings.instance_form.managed_config')"
+    />
     <p class="note">{{$t('config_settings.instance_form.quarantined_instances_label')}}</p>
-    <va-select v-model="formData.allowed_post_formats" :options="selectOptions.allowed_post_formats" :label="$t('config_settings.instance_form.allowed_post_formats')"/>
+    <va-input
+      v-model="formData.static_dir"
+      :label="$t('config_settings.instance_form.static_dir')"
+    />
+    <p class="note">{{$t('config_settings.instance_form.static_dir_label')}}</p>
+    <va-select
+      v-model="formData.allowed_post_formats"
+      :options="selectOptions.allowed_post_formats"
+      :label="$t('config_settings.instance_form.allowed_post_formats')"
+    />
     <p class="note">{{$t('config_settings.instance_form.allowed_post_formats_label')}}</p>
-    <va-checkbox v-model="formData.mrf_transparency" :label="$t('config_settings.instance_form.mrf_transparency')" class="mb-0"/>
+    <va-checkbox
+      v-model="formData.mrf_transparency"
+      :label="$t('config_settings.instance_form.mrf_transparency')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.mrf_transparency_label')}}</p>
-   <va-checkbox v-model="formData.extended_nickname_format" :label="$t('config_settings.instance_form.extended_nickname_format')" class="mb-0"/>
+    <va-input
+      v-model="formData.mrf_transparency_exclusions"
+      :label="$t('config_settings.instance_form.mrf_transparency_exclusions')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.instance_form.mrf_transparency_exclusions_label')}}</p>
+    <va-checkbox
+      v-model="formData.extended_nickname_format"
+      :label="$t('config_settings.instance_form.extended_nickname_format')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.extended_nickname_format_label')}}</p>
-    <va-input v-model.number="formData.max_pinned_statuses" type="number" :label="$t('config_settings.instance_form.max_pinned_statuses')" class="mb-0"/>
+    <va-input
+      v-model.number="formData.max_pinned_statuses"
+      type="number"
+      :label="$t('config_settings.instance_form.max_pinned_statuses')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.max_pinned_statuses_label')}}</p>
-    <va-input v-model="formData.autofollowed_nicknames" :label="$t('config_settings.instance_form.autofollowed_nicknames')" class="mb-0"/>
+    <va-input
+      v-model="formData.autofollowed_nicknames"
+      :label="$t('config_settings.instance_form.autofollowed_nicknames')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.autofollowed_nicknames_label')}}</p>
-    <va-checkbox v-model="formData.no_attachment_links" :label="$t('config_settings.instance_form.no_attachment_links')" class="mb-0"/>
+    <va-checkbox
+      v-model="formData.no_attachment_links"
+      :label="$t('config_settings.instance_form.no_attachment_links')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.no_attachment_links_label')}}</p>
-    <va-input v-model="formData.welcome_message" :label="$t('config_settings.instance_form.welcome_message')" class="mb-0"/>
+    <va-input
+      v-model="formData.welcome_message"
+      :label="$t('config_settings.instance_form.welcome_message')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.welcome_message_label')}}</p>
-    <va-input v-model="formData.welcome_user_nickname" :label="$t('config_settings.instance_form.welcome_user_nickname')" class="mb-0"/>
+    <va-input
+      v-model="formData.welcome_user_nickname"
+      :label="$t('config_settings.instance_form.welcome_user_nickname')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.welcome_user_nickname_label')}}</p>
-    <va-input v-model.number="formData.max_report_comment_size" :label="$t('config_settings.instance_form.max_report_comment_size')" class="mb-0"/>
+    <va-input
+      v-model.number="formData.max_report_comment_size"
+      :label="$t('config_settings.instance_form.max_report_comment_size')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.max_report_comment_size_label')}}</p>
-    <va-checkbox v-model="formData.safe_dm_mentions" :label="$t('config_settings.instance_form.safe_dm_mentions')" class="mb-0"/>
+    <va-checkbox
+      v-model="formData.safe_dm_mentions"
+      :label="$t('config_settings.instance_form.safe_dm_mentions')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.safe_dm_mentions_label')}}</p>
-    <va-checkbox v-model="formData.healthcheck" :label="$t('config_settings.instance_form.healthcheck')" class="mb-0"/>
+    <va-checkbox
+      v-model="formData.healthcheck"
+      :label="$t('config_settings.instance_form.healthcheck')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.healthcheck_label')}}</p>
-    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.instance_form.remote_post_retention_days')" class="mb-0"/>
+    <va-input
+      v-model.number="formData.remote_post_retention_days"
+      type="number"
+      :label="$t('config_settings.instance_form.remote_post_retention_days')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.remote_post_retention_days_label')}}</p>
-    <va-checkbox v-model="formData.skip_thread_containment" :label="$t('config_settings.instance_form.skip_thread_containment')" class="mb-0"/>
-    <va-input v-model.number="formData.remote_post_retention_days" type="number" :label="$t('config_settings.instance_form.remote_post_retention_days')" class="mb-0"/>
+    <va-checkbox
+      v-model="formData.skip_thread_containment"
+      :label="$t('config_settings.instance_form.skip_thread_containment')"
+      class="mb-0"
+    />
+    <va-input
+      v-model.number="formData.remote_post_retention_days"
+      type="number"
+      :label="$t('config_settings.instance_form.remote_post_retention_days')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.remote_post_retention_days_label')}}</p>
-    <va-select v-model="formData.limit_to_local_content" :options="selectOptions.limit_to_local_content" :label="$t('config_settings.instance_form.limit_to_local_content')" class="mb-0"/>
+    <va-select
+      v-model="formData.limit_to_local_content"
+      :options="selectOptions.limit_to_local_content"
+      :label="$t('config_settings.instance_form.limit_to_local_content')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.limit_to_local_content_label')}}</p>
-    <va-checkbox v-model="formData.dynamic_configuration" :label="$t('config_settings.instance_form.dynamic_configuration')" class="mb-0"/>
+    <va-checkbox
+      v-model="formData.dynamic_configuration"
+      :label="$t('config_settings.instance_form.dynamic_configuration')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.dynamic_configuration_label')}}</p>
-    <va-checkbox v-model="formData.external_user_synchronization" :label="$t('config_settings.instance_form.external_user_synchronization')" class="mb-0"/>
+    <va-checkbox
+      v-model="formData.external_user_synchronization"
+      :label="$t('config_settings.instance_form.external_user_synchronization')"
+      class="mb-0"
+    />
     <p class="note">{{$t('config_settings.instance_form.external_user_synchronization_note')}}</p>
   </div>
 </template>
@@ -175,7 +306,8 @@ export default class InstanceForm extends Vue {
       'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy'
     ],
     allowed_post_formats: [],
-    limit_to_local_content: [':unauthenticated', ':all', 'false']
+    limit_to_local_content: [':unauthenticated', ':all', 'false'],
+    federation_publisher_modules: ['Pleroma.Web.ActivityPub.Publisher', 'Pleroma.Web.Websub', 'Pleroma.Web.Salmon'],
   }
 }
 </script>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 9cec8e5..3338a97 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -11,7 +11,7 @@
     <div class="config-settings-page__content pt-4">
       <!--<upload-form v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD" v-model="upload"/>-->
       <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
-      <!--<instance-form v-if="configKeysTabs[value].key === configKeysEnum.INSTANCE" v-model="instance"/>-->
+      <instance-form v-if="configKeysTabs[value].key === configKeysEnum.INSTANCE" v-model="instance"/>
       <frontend-configurations-form v-if="configKeysTabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS" v-model="frontendConfiguration"/>
       <!--<captcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="captcha"/>-->
     </div>
@@ -50,7 +50,7 @@ export default class ConfigSettingsPage extends Vue {
   captcha?: CaptchaConfig = new CaptchaConfig()
   frontendConfiguration?: FrontentConfigurationsConfig = new FrontentConfigurationsConfig()
   configKeysTabs: Array<object> = configKeysTabs
-  configKeysEnum: Enumerator<string> = configKeys
+  configKeysEnum = configKeys
   loading:boolean = false
   async mounted () {
     this.loading = true
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 19a81a6..a562303 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -9,32 +9,32 @@ export enum configKeys {
 }
 
 export const configKeysTabs = [
-  {
-    key: configKeys.UPLOAD,
-    name: 'Upload',
-  },
-  {
-    key: configKeys.EMAILS,
-    name: 'Emails'
-  },
+  // {
+  //   key: configKeys.UPLOAD,
+  //   name: 'Upload',
+  // },
+  // {
+  //   key: configKeys.EMAILS,
+  //   name: 'Emails'
+  // },
   {
     key: configKeys.INSTANCE,
     name: 'Instance'
   },
-  {
-    key: configKeys.LOGGER,
-    name: 'Logger'
-  },
+  // {
+  //   key: configKeys.LOGGER,
+  //   name: 'Logger'
+  // },
   {
     key: configKeys.FRONTEND_CONFIGURATIONS,
     name: 'Frontend configurations'
   },
-  {
-    key: configKeys.WEB,
-    name: 'Web'
-  },
-  {
-    key: configKeys.CAPTCHA,
-    name: 'Captcha'
-  }
+  // {
+  //   key: configKeys.WEB,
+  //   name: 'Web'
+  // },
+  // {
+  //   key: configKeys.CAPTCHA,
+  //   name: 'Captcha'
+  // }
 ]
diff --git a/src/entities/settings/InstanceConfig.ts b/src/entities/settings/InstanceConfig.ts
index 9c5e7dc..b714846 100644
--- a/src/entities/settings/InstanceConfig.ts
+++ b/src/entities/settings/InstanceConfig.ts
@@ -3,17 +3,26 @@ import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
 export default class InstanceConfig {
   constructor(existConfig?) {
     normalizeApiConfig(existConfig, this)
+    this.quarantined_instances = (this.quarantined_instances && Array.isArray(this.quarantined_instances))
+      ? this.quarantined_instances.join(';')
+      : this.quarantined_instances
+    this.mrf_transparency_exclusions = (this.mrf_transparency_exclusions && Array.isArray(this.mrf_transparency_exclusions))
+      ? this.mrf_transparency_exclusions.join(';')
+      : this.mrf_transparency_exclusions
+    this.autofollowed_nicknames = (this.autofollowed_nicknames && Array.isArray(this.autofollowed_nicknames))
+      ? this.autofollowed_nicknames.join(';')
+      : this.autofollowed_nicknames
   }
   name: string = ''
   email: string = ''
   notify_email: string = ''
   description: string = ''
-  limit?: number
+  limit: number = 5000
   remote_limit?: number
   upload_limit: string = ''
   avatar_upload_limit: string = ''
-  background_upload_limit: string = ''
-  banner_upload_limit: string = ''
+ background_upload_limit: string = ''
+ banner_upload_limit: string = ''
   poll_limits: object = {
     max_options:  20,
     max_option_chars: 200,
@@ -22,20 +31,25 @@ export default class InstanceConfig {
     sendAsMap: true
   }
   registrations_open: boolean = false
+  dedupe_media: boolean = false
   invites_enabled: boolean = false
   account_activation_required: boolean = false
   federating: boolean = false
   federation_reachability_timeout_days: number = 1
+  federation_incoming_replies_max_depth: number = 100
+  federation_publisher_modules: Array<string> = [ 'Pleroma.Web.ActivityPub.Publisher', 'Pleroma.Web.Websub', 'Pleroma.Web.Salmon']
   allow_relay: boolean = false
   rewrite_policy: Array<string> = []
   public: boolean = false
-  quarantined_instances: string = ''
+  quarantined_instances: any = ''
   managed_config: boolean = false
+  static_dir: string = 'instance/static'
   allowed_post_formats: Array<string> = []
-  mrf_transparency: boolean = false
+ mrf_transparency: boolean = false
+  mrf_transparency_exclusions: any = []
   extended_nickname_format: boolean = false
   max_pinned_statuses?: number
-  autofollowed_nicknames: string = ''
+  autofollowed_nicknames: any = ''
   no_attachment_links: boolean = false
   welcome_message: string = ''
   welcome_user_nickname: string = ''
@@ -45,5 +59,7 @@ export default class InstanceConfig {
   remote_post_retention_days?: number
   skip_thread_containment: boolean = false
   limit_to_local_content: string = ':unauthenticated'
+  external_user_synchronization: boolean = true
   dynamic_configuration: boolean = false
+  sendAsMap: boolean = true
 }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 63b2d0a..09d7763 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -516,6 +516,7 @@
       "max_expiration": "Maximum expiration time (in seconds)",
       "registrations_open": "Registrations open",
       "registrations_open_label": "Enable registrations for anyone, invitations can be enabled when false.",
+      "dedupe_media": "Dedupe media",
       "invites_enabled": "Invites enabled",
       "invites_enabled_label": "Enable user invitations for admins (depends on registrations_open: false).",
       "account_activation_required": "Account activation required",
@@ -524,6 +525,10 @@
       "federating_label": "Enable federation with other instances",
       "federation_reachability_timeout_days": "Federation reachability timeout days",
       "federation_reachability_timeout_days_label": "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.",
+      "federation_incoming_replies_max_depth": "Max. depth of reply-to activities fetching on incoming federation",
+      "federation_incoming_replies_max_depth_label": "Used to prevent out-of-memory situations while fetching very long threads. If set to nil, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
+      "federation_publisher_modules": "",
+      "federation_publisher_modules_label": "",
       "allow_relay": "Allow relay",
       "allow_relay_label": "Enable Pleroma’s Relay, which makes it possible to follow a whole instance",
       "rewrite_policy": "Rewrite policy",
@@ -531,19 +536,23 @@
       "public": "Public",
       "public_label": "Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.",
       "quarantined_instances": "Quarantined instances",
-      "quarantined_instances_label": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send.",
+      "quarantined_instances_label": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send. Separate items with ;",
       "managed_config": "Managed config",
       "managed_config_label": "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
+      "static_dir": "",
+      "static_dir_label": "",
       "allowed_post_formats": "Allowed post formats",
       "allowed_post_formats_label": "MIME-type list of formats allowed to be posted (transformed into HTML)",
       "mrf_transparency": "mrf transparency",
       "mrf_transparency_label": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
+      "mrf_transparency_exclusions": "Exclude specific instance names from MRF transparency.",
+      "mrf_transparency_exclusions_label": "The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. Separate items with ;",
       "extended_nickname_format": "Extended nickname format",
       "extended_nickname_format_label": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
       "max_pinned_statuses": "Max pinned status",
       "max_pinned_statuses_label": "The maximum number of pinned statuses. 0 will disable the feature.",
       "autofollowed_nicknames": "Autofollowed nicknames",
-      "autofollowed_nicknames_label": "Set to nicknames of (local) users that every new user should automatically follow.",
+      "autofollowed_nicknames_label": "Set to nicknames of (local) users that every new user should automatically follow. Separate items with ;",
       "no_attachment_links": "No attachment links",
       "no_attachment_links_label": "Set to true to disable automatically adding attachment link text to statuses",
       "welcome_message": "Welcome message",
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index fb8ed18..fc3e56b 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -19,13 +19,13 @@ export default (configs) => {
   //     value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
   //   })
   // }
-  // if (configs['instance']) {
-  //   settings.push({
-  //     group: 'pleroma',
-  //     key: configKeys.INSTANCE,
-  //     value: getConfigValue(normalizeCaptchaConfigValue(configs['instance']))
-  //   })
-  // }
+  if (configs['instance']) {
+    settings.push({
+      group: 'pleroma',
+      key: configKeys.INSTANCE,
+      value: getConfigValue(normalizeInstanceConfigValue(configs['instance']))
+    })
+  }
   if (configs['frontendConfiguration']) {
     settings.push({
       group: 'pleroma',
@@ -55,15 +55,26 @@ const normalizeCaptchaConfigValue = (config) => {
   return config
 }
 
+const normalizeInstanceConfigValue = (config) => {
+  config.quarantined_instances = config.quarantined_instances.split(';')
+  config.mrf_transparency_exclusions = config.mrf_transparency_exclusions.split(';')
+  config.autofollowed_nicknames = config.autofollowed_nicknames.split(';')
+  return config
+}
+
 const getConfigValue = (config) => {
   const newConfig = []
   createTupledObject(newConfig, config)
   function createTupledObject (resultConfig, nestedObj) {
     forIn(nestedObj, (val, key) => {
-      if (typeof val === 'object' && !Array.isArray(val)) {
+      if (val && typeof val === 'object' && !Array.isArray(val)) {
         if (val.sendAsMap) {
           delete val.sendAsMap
-          resultConfig.push({ tuple: [`:${key}`, val] })
+          const resultVal = {}
+          forIn(val, (innerVal, innerKey) => {
+            resultVal[`:${innerKey}`] = innerVal
+          })
+          resultConfig.push({ tuple: [`:${key}`, resultVal] })
         } else {
           resultConfig.push({ tuple: [`:${key}`, createTupledObject([], val)] })
         }
-- 
GitLab


From 3f449d8e0dec4017f89cb8881c4f76612e99e4e8 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 17 Jul 2019 16:29:45 +0300
Subject: [PATCH 19/61] feat: captcha & kocaptcha config

---
 .../configSettings/forms/CaptchaForm.vue      | 55 +++++++++-------
 .../configSettings/forms/KocaptchaForm.vue    | 64 +++++++++++++++++++
 .../configSettings/ConfigSettingsPage.vue     | 22 +++++--
 src/data/Config.ts                            |  9 +--
 src/entities/settings/CaptchaConfig.ts        |  1 -
 src/entities/settings/KocaptchaConfig.ts      |  8 +++
 src/i18n/en.json                              |  5 +-
 src/utils/ConvertConfigToApiRequest.js        | 21 ++++--
 src/utils/ConvertConfigToState.ts             |  2 +
 src/utils/GetFieldList.ts                     | 22 +++++++
 10 files changed, 168 insertions(+), 41 deletions(-)
 create mode 100644 src/components/configSettings/forms/KocaptchaForm.vue
 create mode 100644 src/entities/settings/KocaptchaConfig.ts
 create mode 100644 src/utils/GetFieldList.ts

diff --git a/src/components/configSettings/forms/CaptchaForm.vue b/src/components/configSettings/forms/CaptchaForm.vue
index 3344448..dab33d8 100644
--- a/src/components/configSettings/forms/CaptchaForm.vue
+++ b/src/components/configSettings/forms/CaptchaForm.vue
@@ -1,38 +1,45 @@
 <template>
   <div>
-    <va-checkbox
-      v-model="formData.enabled"
-      :label="$t('config_settings.captcha_form.enabled')"
-    />
-    <p class="note">{{$t('config_settings.captcha_form.enabled_note')}}</p>
-    <va-select
-      v-model="formData.method"
-      :options="selectOptions.method"
-      :label="$t('config_settings.captcha_form.method')"
-    />
-    <p class="note">{{$t('config_settings.captcha_form.method_note')}}</p>
-    <div v-if="formData.method === 'Pleroma.Captcha.Kocaptcha'" class="mx-4 my-3">
-      <va-input
-        v-model="formData.endpoint"
-        :label="$t('config_settings.captcha_form.endpoint')"
+    <template
+      v-for="(field, index) in fields"
+    >
+      <component
+        v-if="typeof formData[field.model] === 'number'"
+        :is="field.component"
+        :key="field.model"
+        v-model.number="formData[field.model]"
+        :label="$t(`config_settings.captcha_form.${field.model}`)"
+        class="mb-0"
+        type="number"
       />
-    </div>
-    <va-input
-      v-model.number="formData.seconds_valid"
-      type="number"
-      :label="$t('config_settings.captcha_form.seconds_valid')"
-    />
+      <component
+        v-else
+        :is="field.component"
+        :key="field.model"
+        v-model="formData[field.model]"
+        :label="$t(`config_settings.captcha_form.${field.model}`)"
+        class="mb-0"
+      />
+      <p
+        class="note"
+        :key="index"
+      >
+        {{$t(`config_settings.captcha_form.${field.model}_note`)}}
+      </p>
+    </template>
   </div>
 </template>
 
 <script lang="ts">
 import { Component, Prop, Vue } from 'vue-property-decorator'
 import UploadConfig from '../../../entities/settings/UploadConfig'
+import { forIn } from 'lodash'
+import getFieldList from '../../../utils/GetFieldList'
 
 @Component({
   components: {},
 })
-export default class UploadForm extends Vue {
+export default class CaptchaForm extends Vue {
   @Prop(Object) readonly value!: UploadConfig
   get formData () {
     return this.value
@@ -40,8 +47,8 @@ export default class UploadForm extends Vue {
   set formData (val) {
     this.$emit('updateForm', val)
   }
-  selectOptions = {
-    method: ['Pleroma.Captcha.Kocaptcha'],
+  get fields () {
+    return getFieldList(this.formData)
   }
 }
 </script>
diff --git a/src/components/configSettings/forms/KocaptchaForm.vue b/src/components/configSettings/forms/KocaptchaForm.vue
new file mode 100644
index 0000000..ea18c49
--- /dev/null
+++ b/src/components/configSettings/forms/KocaptchaForm.vue
@@ -0,0 +1,64 @@
+<template>
+  <div>
+    <p class="title">{{$t(`config_settings.captcha_form.title`)}}</p>
+    <template
+      v-for="(field, index) in fields"
+    >
+      <component
+        v-if="typeof formData[field.model] === 'number'"
+        :is="field.component"
+        :key="field.model"
+        v-model.number="formData[field.model]"
+        :label="$t(`config_settings.captcha_form.${field.model}`)"
+        class="mb-0"
+        type="number"
+      />
+      <component
+        v-else
+        :is="field.component"
+        :key="field.model"
+        v-model="formData[field.model]"
+        :label="$t(`config_settings.captcha_form.${field.model}`)"
+        class="mb-0"
+      />
+      <p
+        class="note"
+        :key="index"
+      >
+        {{$t(`config_settings.captcha_form.${field.model}_note`)}}
+      </p>
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import UploadConfig from '../../../entities/settings/UploadConfig'
+import { forIn } from 'lodash'
+import getFieldList from '../../../utils/GetFieldList'
+
+@Component({
+  components: {},
+})
+export default class KocaptchaForm extends Vue {
+  @Prop(Object) readonly value!: UploadConfig
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  get fields () {
+    return getFieldList(this.formData)
+  }
+  selectOptions = {
+    method: ['Pleroma.Captcha.Kocaptcha'],
+  }
+}
+</script>
+
+<style lang="scss">
+.upload {
+
+}
+</style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 3338a97..c79bf75 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -13,7 +13,12 @@
       <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
       <instance-form v-if="configKeysTabs[value].key === configKeysEnum.INSTANCE" v-model="instance"/>
       <frontend-configurations-form v-if="configKeysTabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS" v-model="frontendConfiguration"/>
-      <!--<captcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="captcha"/>-->
+      <captcha-form
+        v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA"
+        v-model="captcha"
+        class="mb-3 captcha-form"
+      />
+      <kocaptcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="kocaptcha"/>
     </div>
     <div class="flex-center pb-4">
       <va-button @click="onSaveButtunClick">Save settings</va-button>
@@ -28,6 +33,7 @@ import { ConfigService } from '../../../services/ConfigService'
 import { configKeysTabs, configKeys } from '../../../data/Config'
 import UploadForm from '../../configSettings/forms/UploadForm.vue'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
+import KocaptchaForm from '../../configSettings/forms/KocaptchaForm.vue'
 import InstanceForm from '../../configSettings/forms/InstanceForm.vue'
 import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import UploadConfig from '../../../entities/settings/UploadConfig'
@@ -38,9 +44,10 @@ import CaptchaForm from '../../configSettings/forms/CaptchaForm.vue'
 import CaptchaConfig from '../../../entities/settings/CaptchaConfig'
 import FrontendConfigurationsForm from '../../configSettings/forms/FrontendConfigurationsForm.vue'
 import FrontentConfigurationsConfig from '../../../entities/settings/FrontentConfigurationsConfig'
+import KocaptchaConfig from '../../../entities/settings/KocaptchaConfig'
 
 @Component({
-  components: { FrontendConfigurationsForm, CaptchaForm, InstanceForm, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner },
+  components: { FrontendConfigurationsForm, CaptchaForm, InstanceForm, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner, KocaptchaForm },
 })
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
@@ -48,6 +55,7 @@ export default class ConfigSettingsPage extends Vue {
   emails?: EmailsConfig = new EmailsConfig()
   instance?: InstanceConfig = new InstanceConfig()
   captcha?: CaptchaConfig = new CaptchaConfig()
+  kocaptcha?: KocaptchaConfig = new KocaptchaConfig()
   frontendConfiguration?: FrontentConfigurationsConfig = new FrontentConfigurationsConfig()
   configKeysTabs: Array<object> = configKeysTabs
   configKeysEnum = configKeys
@@ -60,23 +68,25 @@ export default class ConfigSettingsPage extends Vue {
   }
   async onSaveButtunClick () {
     this.loading = true
-    const { upload, emails, instance, captcha, frontendConfiguration } = this
+    const { upload, emails, instance, captcha, frontendConfiguration, kocaptcha } = this
     const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest({
       upload,
       emails,
       instance,
       captcha,
       frontendConfiguration,
+      kocaptcha
     }))
     this.loadConfigs(configs)
     this.loading = false
   }
   loadConfigs (configs) {
-    const { upload, emails, instance, captcha, frontendConfiguration } = ConvertConfigToState(configs)
+    const { upload, emails, instance, captcha, frontendConfiguration, kocaptcha } = ConvertConfigToState(configs)
     this.upload = upload
     this.emails = emails
     this.instance = instance
     this.captcha = captcha
+    this.kocaptcha = kocaptcha
     this.frontendConfiguration = frontendConfiguration
   }
 }
@@ -92,5 +102,9 @@ export default class ConfigSettingsPage extends Vue {
     .va-select {
       margin-bottom: 0;
     }
+    .captcha-form {
+      padding-bottom: 1rem;
+      border-bottom: 1px solid $border-color;
+    }
   }
 </style>
diff --git a/src/data/Config.ts b/src/data/Config.ts
index a562303..af9c1f5 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -6,6 +6,7 @@ export enum configKeys {
   FRONTEND_CONFIGURATIONS = ':frontend_configurations',
   WEB = 'Pleroma.Web',
   CAPTCHA = 'Pleroma.Captcha',
+  KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
 }
 
 export const configKeysTabs = [
@@ -33,8 +34,8 @@ export const configKeysTabs = [
   //   key: configKeys.WEB,
   //   name: 'Web'
   // },
-  // {
-  //   key: configKeys.CAPTCHA,
-  //   name: 'Captcha'
-  // }
+  {
+    key: configKeys.CAPTCHA,
+    name: 'Captcha'
+  },
 ]
diff --git a/src/entities/settings/CaptchaConfig.ts b/src/entities/settings/CaptchaConfig.ts
index 20047a1..2666eb0 100644
--- a/src/entities/settings/CaptchaConfig.ts
+++ b/src/entities/settings/CaptchaConfig.ts
@@ -7,5 +7,4 @@ export default class CaptchaConfig {
   enabled: boolean = false
   method: string = 'Pleroma.Captcha.Kocaptcha'
   seconds_valid?: number
-  endpoint: string = 'https://captcha.kotobank.ch'
 }
diff --git a/src/entities/settings/KocaptchaConfig.ts b/src/entities/settings/KocaptchaConfig.ts
new file mode 100644
index 0000000..95227f1
--- /dev/null
+++ b/src/entities/settings/KocaptchaConfig.ts
@@ -0,0 +1,8 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
+export default class KocaptchaConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  endpoint: string = 'https://captcha.kotobank.ch'
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 09d7763..b577532 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -606,12 +606,15 @@
       "hide_user_stats": "Hide profile statistics"
     },
     "captcha_form": {
+      "title": "Kocaptcha service",
       "enabled": "Enabled",
       "enabled_note": "Whether the captcha should be shown on registration",
       "method": "Method",
       "method_note": "The method/service to use for captcha",
       "seconds_valid": "Seconds valid",
-      "endpoint": "The kocaptcha endpoint to use"
+      "seconds_valid_note": "",
+      "endpoint": "The kocaptcha endpoint to use",
+      "endpoint_note": ""
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index fc3e56b..ee6c0e2 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -12,13 +12,20 @@ export default (configs) => {
   //   }
   //   settings.push(upload)
   // }
-  // if (configs['captcha']) {
-  //   settings.push({
-  //     group: 'pleroma',
-  //     key: configKeys.CAPTCHA,
-  //     value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
-  //   })
-  // }
+  if (configs['captcha']) {
+    settings.push({
+      group: 'pleroma',
+      key: configKeys.CAPTCHA,
+      value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
+    })
+  }
+  if (configs['kocaptcha']) {
+    settings.push({
+      group: 'pleroma',
+      key: configKeys.KOCAPTCHA,
+      value: getConfigValue(configs['kocaptcha'])
+    })
+  }
   if (configs['instance']) {
     settings.push({
       group: 'pleroma',
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index c0b66e1..26ba541 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -6,12 +6,14 @@ import { configKeys } from '../data/Config'
 import FrontentConfigurationsConfig from '../entities/settings/FrontentConfigurationsConfig'
 import t from 'typy'
 import { forIn } from 'lodash'
+import KocaptchaConfig from "../entities/settings/KocaptchaConfig";
 
 export default (configs) => ({
   upload: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
   emails: new EmailsConfig(),
   instance: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
   captcha: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
+  kocaptcha: new KocaptchaConfig(t(configs.find(({ key }) => key === configKeys.KOCAPTCHA), 'value').safeObject),
   frontendConfiguration: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject)
 })
 
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
new file mode 100644
index 0000000..38c9a3d
--- /dev/null
+++ b/src/utils/GetFieldList.ts
@@ -0,0 +1,22 @@
+import { forIn } from 'lodash'
+
+export default (formData) => {
+  const list: Array<object> = []
+  forIn(formData, (val, key) => {
+    let component = ''
+    if (typeof val === 'string' || typeof val === 'number') {
+      component = 'va-input'
+    }
+    if (typeof val === 'boolean') {
+      component = 'va-checkbox'
+    }
+    if (typeof val === 'object' && Array.isArray(val)) {
+      component = 'va-select'
+    }
+    list.push({
+      model: key,
+      component,
+    })
+  })
+  return list
+}
-- 
GitLab


From c8b2580a22d64f179c561bdd85b4fe42dff27745 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 18 Jul 2019 14:53:08 +0300
Subject: [PATCH 20/61] feat: make one general form for all configs(!!!)

---
 .../configSettings/forms/CaptchaForm.vue      |  60 ----
 .../configSettings/forms/ConfigForm.vue       |  83 +++++
 .../forms/FrontendConfigurationsForm.vue      | 106 ------
 .../configSettings/forms/InstanceForm.vue     | 322 ------------------
 .../configSettings/forms/KocaptchaForm.vue    |  64 ----
 .../configSettings/forms/UploadForm.vue       |   4 +-
 .../configSettings/ConfigSettingsPage.vue     |  68 ++--
 .../settings/FrontentConfigurationsConfig.ts  |   1 +
 src/i18n/en.json                              | 169 ++++-----
 src/utils/ConvertConfigToApiRequest.js        |  49 +--
 src/utils/ConvertConfigToState.ts             |  13 +-
 src/utils/GetFieldList.ts                     |  53 ++-
 12 files changed, 261 insertions(+), 731 deletions(-)
 delete mode 100644 src/components/configSettings/forms/CaptchaForm.vue
 create mode 100644 src/components/configSettings/forms/ConfigForm.vue
 delete mode 100644 src/components/configSettings/forms/FrontendConfigurationsForm.vue
 delete mode 100644 src/components/configSettings/forms/InstanceForm.vue
 delete mode 100644 src/components/configSettings/forms/KocaptchaForm.vue

diff --git a/src/components/configSettings/forms/CaptchaForm.vue b/src/components/configSettings/forms/CaptchaForm.vue
deleted file mode 100644
index dab33d8..0000000
--- a/src/components/configSettings/forms/CaptchaForm.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
-  <div>
-    <template
-      v-for="(field, index) in fields"
-    >
-      <component
-        v-if="typeof formData[field.model] === 'number'"
-        :is="field.component"
-        :key="field.model"
-        v-model.number="formData[field.model]"
-        :label="$t(`config_settings.captcha_form.${field.model}`)"
-        class="mb-0"
-        type="number"
-      />
-      <component
-        v-else
-        :is="field.component"
-        :key="field.model"
-        v-model="formData[field.model]"
-        :label="$t(`config_settings.captcha_form.${field.model}`)"
-        class="mb-0"
-      />
-      <p
-        class="note"
-        :key="index"
-      >
-        {{$t(`config_settings.captcha_form.${field.model}_note`)}}
-      </p>
-    </template>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import UploadConfig from '../../../entities/settings/UploadConfig'
-import { forIn } from 'lodash'
-import getFieldList from '../../../utils/GetFieldList'
-
-@Component({
-  components: {},
-})
-export default class CaptchaForm extends Vue {
-  @Prop(Object) readonly value!: UploadConfig
-  get formData () {
-    return this.value
-  }
-  set formData (val) {
-    this.$emit('updateForm', val)
-  }
-  get fields () {
-    return getFieldList(this.formData)
-  }
-}
-</script>
-
-<style lang="scss">
-  .upload {
-
-  }
-</style>
diff --git a/src/components/configSettings/forms/ConfigForm.vue b/src/components/configSettings/forms/ConfigForm.vue
new file mode 100644
index 0000000..2a7c2a5
--- /dev/null
+++ b/src/components/configSettings/forms/ConfigForm.vue
@@ -0,0 +1,83 @@
+<template>
+  <div class='config-form' :style="{margin: margin}">
+    <p class="title" v-if="showTitle">{{title}}</p>
+    <template v-for="(field, index) in fields">
+      <config-form
+        v-if="field.component === 'parent'"
+        v-model="formData[field.model]"
+        :title="field.model"
+        showTitle
+        margin="1rem"
+        :key="field.model"
+      />
+      <div v-else :key="field.model">
+        <component
+          v-if="typeof formData[field.model] === 'number'"
+          :is="field.component"
+          v-model.number="formData[field.model]"
+          :label="$t(`config_settings.${title}_form.${field.model}`)"
+          class="mb-0"
+          type="number"
+        />
+        <component
+          v-else
+          :is="field.component"
+          v-model="formData[field.model]"
+          :label="$t(`config_settings.${title}_form.${field.model}`)"
+          class="mb-0"
+          :options="selectOptions[field.model]"
+          :multiple="field.multiple"
+        />
+        <p
+          class="note"
+          :key="index"
+        >
+          {{$t(`config_settings.${title}_form.${field.model}_note`)}}
+        </p>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import UploadConfig from '../../../entities/settings/UploadConfig'
+import getFieldList, { selectOptions } from '../../../utils/GetFieldList'
+import { StaticRecourcesService } from '../../../services/StaticRecourcesService'
+import { keys } from 'lodash'
+import { configKeys } from '../../../data/Config'
+
+@Component({
+  components: {},
+})
+export default class ConfigForm extends Vue {
+  @Prop(Object) readonly value!: UploadConfig
+  @Prop(String) readonly title!: string
+  @Prop(Boolean) readonly showTitle!: boolean
+  @Prop(String) readonly margin!: string
+  selectOptions = selectOptions
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  get fields () {
+    return getFieldList(this.formData)
+  }
+  async mounted () {
+    if (this.title === configKeys.FRONTEND_CONFIGURATIONS) {
+      const themeConfig = await StaticRecourcesService.getThemesList()
+      this.selectOptions.theme = keys(themeConfig)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .config-form {
+  }
+  .note {
+    margin-bottom: 1rem !important;
+  }
+</style>
diff --git a/src/components/configSettings/forms/FrontendConfigurationsForm.vue b/src/components/configSettings/forms/FrontendConfigurationsForm.vue
deleted file mode 100644
index 20f4fe4..0000000
--- a/src/components/configSettings/forms/FrontendConfigurationsForm.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<template>
-  <div class="frontend-configurations-form">
-    <va-select
-      v-model="formData.pleroma_fe.theme"
-      :options="selectOptions.theme"
-      :label="$t('config_settings.frontend_configurations_form.theme')"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.theme_note')}}</p>
-    <va-input v-model="formData.pleroma_fe.logo" class="mb-0"/>
-    <p class="note">{{$t('config_settings.frontend_configurations_form.logo_note')}}</p>
-    <va-input
-      v-model="formData.pleroma_fe.background"
-      :label="$t('config_settings.frontend_configurations_form.background')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.background_note')}}</p>
-    <va-input
-      v-model="formData.pleroma_fe.redirectRootNoLogin"
-      class="mb-0"
-      :label="$t('config_settings.frontend_configurations_form.redirect_url')"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.redirect_root_no_login')}}</p>
-    <va-input
-      v-model="formData.pleroma_fe.redirectRootLogin"
-      class="mb-0"
-      :label="$t('config_settings.frontend_configurations_form.redirect_url')"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.redirect_root_login')}}</p>
-    <va-select
-      v-model="formData.pleroma_fe.subjectLineBehavior"
-      :label="$t('config_settings.frontend_configurations_form.subject_line_behavior')"
-      class="mb-0"
-      :options="selectOptions.subject_line_behavior"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.subject_line_behavior_label')}}</p>
-    <va-checkbox
-      v-model="formData.pleroma_fe.scopeOptionsEnabled"
-      :label="$t('config_settings.frontend_configurations_form.show_instance_specific_panel')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.show_instance_specific_panel_note')}}</p>
-    <va-checkbox v-model="formData.pleroma_fe.scope_copy" :label="$t('config_settings.frontend_configurations_form.scope_copy')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.frontend_configurations_form.scope_copy_label')}}</p>
-    <va-checkbox
-      v-model="formData.pleroma_fe.formattingOptionsEnabled"
-      :label="$t('config_settings.frontend_configurations_form.formatting_options_enabled')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.formatting_options_enabled_note')}}</p>
-    <va-checkbox v-model="formData.pleroma_fe.always_show_subject_input" :label="$t('config_settings.frontend_configurations_form.always_show_subject_input')" class="mb-0"/>
-    <p class="note">{{$t('config_settings.frontend_configurations_form.always_show_subject_input_label')}}</p>
-    <va-checkbox
-      v-model="formData.pleroma_fe.collapseMessageWithSubject"
-      :label="$t('config_settings.frontend_configurations_form.collapse_message_with_subjects')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.frontend_configurations_form.collapse_message_with_subjects_note')}}</p>
-    <va-input-wrapper>
-      <va-checkbox
-        v-model="formData.pleroma_fe.hidePostStats"
-        :label="$t('config_settings.frontend_configurations_form.hide_post_stats')"
-        class="mb-0"
-      />
-    </va-input-wrapper>
-    <va-input-wrapper>
-      <va-checkbox
-        v-model="formData.pleroma_fe.hideUserStats"
-        :label="$t('config_settings.frontend_configurations_form.hide_user_stats')"
-        class="mb-0"
-      />
-    </va-input-wrapper>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import FrontendConfigurationsConfig from '../../../entities/settings/FrontentConfigurationsConfig'
-import { StaticRecourcesService } from '../../../services/StaticRecourcesService'
-import { keys } from 'lodash'
-
-@Component({
-  components: {}
-})
-// TODO: insert file uploaders
-export default class FrontendConfigurationsForm extends Vue {
-  @Prop(Object) readonly value!: FrontendConfigurationsConfig
-  get formData () {
-    return this.value
-  }
-  set formData (val) {
-    this.$emit('updateForm', val)
-  }
-  selectOptions = {
-    theme: [],
-    subject_line_behavior: ['email', 'masto', 'noop'],
-  }
-  async mounted () {
-    const themeConfig = await StaticRecourcesService.getThemesList()
-    this.selectOptions.theme = keys(themeConfig)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/forms/InstanceForm.vue b/src/components/configSettings/forms/InstanceForm.vue
deleted file mode 100644
index bdfe4c7..0000000
--- a/src/components/configSettings/forms/InstanceForm.vue
+++ /dev/null
@@ -1,322 +0,0 @@
-<template>
-  <div class="instance_form">
-    <va-input
-      v-model="formData.name"
-      :label="$t('config_settings.instance_form.name')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.name_label')}}</p>
-    <va-input
-      v-model="formData.email"
-      :label="$t('config_settings.instance_form.email')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.email_label')}}</p>
-    <va-input
-      v-model="formData.notify_email"
-      :label="$t('config_settings.instance_form.notify_email')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.notify_email_label')}}</p>
-    <va-input
-      v-model="formData.description"
-      :label="$t('config_settings.instance_form.description')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.description_label')}}</p>
-    <va-input
-      v-model.number="formData.limit"
-      type="number"
-      :label="$t('config_settings.instance_form.limit')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.limit_label')}}</p>
-    <va-input
-      v-model.number="formData.remote_limit"
-      type="number"
-      :label="$t('config_settings.instance_form.remote_limit')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.remote_limit_label')}}</p>
-    <va-input
-      v-model="formData.upload_limit"
-      :label="$t('config_settings.instance_form.upload_limit')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.upload_limit_label')}}</p>
-    <va-input
-      v-model="formData.avatar_upload_limit"
-      :label="$t('config_settings.instance_form.avatar_upload_limit')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.avatar_upload_limit_label')}}</p>
-    <va-input
-      v-model="formData.background_upload_limit"
-      :label="$t('config_settings.instance_form.background_upload_limit')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.background_upload_limit_label')}}</p>
-    <va-input
-      v-model="formData.banner_upload_limit"
-      :label="$t('config_settings.instance_form.banner_upload_limit')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.banner_upload_limit_label')}}</p>
-    <div class="py-3 my-3 instance_form__poll-limits">
-      <p class="title">poll limits for <span :style="{color: $themes['warning']}">local</span> polls</p>
-      <va-input
-        v-model="formData.poll_limits.max_options"
-        type="number"
-        :label="$t('config_settings.instance_form.max_options')"
-      />
-      <va-input
-        v-model="formData.poll_limits.max_option_chars"
-        type="number"
-        :label="$t('config_settings.instance_form.max_option_chars')"
-      />
-      <va-input
-        v-model="formData.poll_limits.min_expiration"
-        type="number"
-        :label="$t('config_settings.instance_form.min_expiration')"
-      /> <va-input
-        v-model="formData.poll_limits.max_expiration"
-        type="number"
-        :label="$t('config_settings.instance_form.max_expiration')"
-      />
-    </div>
-    <va-checkbox
-      v-model="formData.registrations_open"
-      :label="$t('config_settings.instance_form.registrations_open')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.registrations_open_label')}}</p>
-    <va-checkbox
-      v-model="formData.dedupe_media"
-      :label="$t('config_settings.instance_form.dedupe_media')"
-      class="mb-3"
-    />
-    <va-checkbox
-      v-model="formData.invites_enabled"
-      :label="$t('config_settings.instance_form.invites_enabled')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.invites_enabled_label')}}</p>
-    <va-checkbox
-      v-model="formData.account_activation_required"
-      :label="$t('config_settings.instance_form.account_activation_required')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.account_activation_required_label')}}</p>
-    <va-checkbox
-      v-model="formData.federating"
-      :label="$t('config_settings.instance_form.federating')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.federating_label')}}</p>
-    <va-input
-      v-model.number="formData.federation_reachability_timeout_days"
-      type="number"
-      :label="$t('config_settings.instance_form.federation_reachability_timeout_days')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.federation_reachability_timeout_days_label')}}</p>
-    <va-input
-      v-model.number="formData.federation_incoming_replies_max_depth"
-      type="number"
-      :label="$t('config_settings.instance_form.federation_incoming_replies_max_depth')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.federation_incoming_replies_max_depth_label')}}</p>
-    <va-select
-      v-model="formData.federation_publisher_modules"
-      :options="selectOptions.federation_publisher_modules"
-      multiple
-      :label="$t('config_settings.instance_form.federation_publisher_modules')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.federation_publisher_modules_label')}}</p>
-    <va-checkbox
-      v-model="formData.allow_relay"
-      :label="$t('config_settings.instance_form.allow_relay_label')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.allow_relay_label')}}</p>
-    <va-select
-      v-model="formData.rewrite_policy"
-      :options="selectOptions.rewrite_policy"
-      :label="$t('config_settings.instance_form.invites_enabled')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.rewrite_policy_label')}}</p>
-    <va-checkbox
-      v-model="formData.public"
-      :label="$t('config_settings.instance_form.public')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.public_label')}}</p>
-    <va-input
-      v-model="formData.quarantined_instances"
-      :label="$t('config_settings.instance_form.quarantined_instances')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.quarantined_instances_label')}}</p>
-    <va-checkbox
-      v-model="formData.managed_config"
-      :label="$t('config_settings.instance_form.managed_config')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.quarantined_instances_label')}}</p>
-    <va-input
-      v-model="formData.static_dir"
-      :label="$t('config_settings.instance_form.static_dir')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.static_dir_label')}}</p>
-    <va-select
-      v-model="formData.allowed_post_formats"
-      :options="selectOptions.allowed_post_formats"
-      :label="$t('config_settings.instance_form.allowed_post_formats')"
-    />
-    <p class="note">{{$t('config_settings.instance_form.allowed_post_formats_label')}}</p>
-    <va-checkbox
-      v-model="formData.mrf_transparency"
-      :label="$t('config_settings.instance_form.mrf_transparency')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.mrf_transparency_label')}}</p>
-    <va-input
-      v-model="formData.mrf_transparency_exclusions"
-      :label="$t('config_settings.instance_form.mrf_transparency_exclusions')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.mrf_transparency_exclusions_label')}}</p>
-    <va-checkbox
-      v-model="formData.extended_nickname_format"
-      :label="$t('config_settings.instance_form.extended_nickname_format')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.extended_nickname_format_label')}}</p>
-    <va-input
-      v-model.number="formData.max_pinned_statuses"
-      type="number"
-      :label="$t('config_settings.instance_form.max_pinned_statuses')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.max_pinned_statuses_label')}}</p>
-    <va-input
-      v-model="formData.autofollowed_nicknames"
-      :label="$t('config_settings.instance_form.autofollowed_nicknames')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.autofollowed_nicknames_label')}}</p>
-    <va-checkbox
-      v-model="formData.no_attachment_links"
-      :label="$t('config_settings.instance_form.no_attachment_links')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.no_attachment_links_label')}}</p>
-    <va-input
-      v-model="formData.welcome_message"
-      :label="$t('config_settings.instance_form.welcome_message')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.welcome_message_label')}}</p>
-    <va-input
-      v-model="formData.welcome_user_nickname"
-      :label="$t('config_settings.instance_form.welcome_user_nickname')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.welcome_user_nickname_label')}}</p>
-    <va-input
-      v-model.number="formData.max_report_comment_size"
-      :label="$t('config_settings.instance_form.max_report_comment_size')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.max_report_comment_size_label')}}</p>
-    <va-checkbox
-      v-model="formData.safe_dm_mentions"
-      :label="$t('config_settings.instance_form.safe_dm_mentions')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.safe_dm_mentions_label')}}</p>
-    <va-checkbox
-      v-model="formData.healthcheck"
-      :label="$t('config_settings.instance_form.healthcheck')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.healthcheck_label')}}</p>
-    <va-input
-      v-model.number="formData.remote_post_retention_days"
-      type="number"
-      :label="$t('config_settings.instance_form.remote_post_retention_days')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.remote_post_retention_days_label')}}</p>
-    <va-checkbox
-      v-model="formData.skip_thread_containment"
-      :label="$t('config_settings.instance_form.skip_thread_containment')"
-      class="mb-0"
-    />
-    <va-input
-      v-model.number="formData.remote_post_retention_days"
-      type="number"
-      :label="$t('config_settings.instance_form.remote_post_retention_days')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.remote_post_retention_days_label')}}</p>
-    <va-select
-      v-model="formData.limit_to_local_content"
-      :options="selectOptions.limit_to_local_content"
-      :label="$t('config_settings.instance_form.limit_to_local_content')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.limit_to_local_content_label')}}</p>
-    <va-checkbox
-      v-model="formData.dynamic_configuration"
-      :label="$t('config_settings.instance_form.dynamic_configuration')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.dynamic_configuration_label')}}</p>
-    <va-checkbox
-      v-model="formData.external_user_synchronization"
-      :label="$t('config_settings.instance_form.external_user_synchronization')"
-      class="mb-0"
-    />
-    <p class="note">{{$t('config_settings.instance_form.external_user_synchronization_note')}}</p>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import InstanceConfig from '../../../entities/settings/InstanceConfig'
-
-@Component({
-  components: {}
-})
-
-export default class InstanceForm extends Vue {
-  @Prop(Object) readonly value!: InstanceConfig
-  get formData () {
-    return this.value
-  }
-  set formData (val) {
-    this.$emit('updateForm', val)
-  }
-  selectOptions = {
-    rewrite_policy: [
-      'Pleroma.Web.ActivityPub.MRF.NoOpPolicy',
-      'Pleroma.Web.ActivityPub.MRF.DropPolicy',
-      'Pleroma.Web.ActivityPub.MRF.SimplePolicy',
-      'Pleroma.Web.ActivityPub.MRF.TagPolicy',
-      'Pleroma.Web.ActivityPub.MRF.SubchainPolicy',
-      'Pleroma.Web.ActivityPub.MRF.RejectNonPublic',
-      'Pleroma.Web.ActivityPub.MRF.EnsureRePrepended',
-      'Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy',
-      'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy'
-    ],
-    allowed_post_formats: [],
-    limit_to_local_content: [':unauthenticated', ':all', 'false'],
-    federation_publisher_modules: ['Pleroma.Web.ActivityPub.Publisher', 'Pleroma.Web.Websub', 'Pleroma.Web.Salmon'],
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.instance_form {
-  &__poll-limits {
-    border-top: 1px solid $border-color;
-    border-bottom: 1px solid $border-color;
-  }
-}
-</style>
diff --git a/src/components/configSettings/forms/KocaptchaForm.vue b/src/components/configSettings/forms/KocaptchaForm.vue
deleted file mode 100644
index ea18c49..0000000
--- a/src/components/configSettings/forms/KocaptchaForm.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<template>
-  <div>
-    <p class="title">{{$t(`config_settings.captcha_form.title`)}}</p>
-    <template
-      v-for="(field, index) in fields"
-    >
-      <component
-        v-if="typeof formData[field.model] === 'number'"
-        :is="field.component"
-        :key="field.model"
-        v-model.number="formData[field.model]"
-        :label="$t(`config_settings.captcha_form.${field.model}`)"
-        class="mb-0"
-        type="number"
-      />
-      <component
-        v-else
-        :is="field.component"
-        :key="field.model"
-        v-model="formData[field.model]"
-        :label="$t(`config_settings.captcha_form.${field.model}`)"
-        class="mb-0"
-      />
-      <p
-        class="note"
-        :key="index"
-      >
-        {{$t(`config_settings.captcha_form.${field.model}_note`)}}
-      </p>
-    </template>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import UploadConfig from '../../../entities/settings/UploadConfig'
-import { forIn } from 'lodash'
-import getFieldList from '../../../utils/GetFieldList'
-
-@Component({
-  components: {},
-})
-export default class KocaptchaForm extends Vue {
-  @Prop(Object) readonly value!: UploadConfig
-  get formData () {
-    return this.value
-  }
-  set formData (val) {
-    this.$emit('updateForm', val)
-  }
-  get fields () {
-    return getFieldList(this.formData)
-  }
-  selectOptions = {
-    method: ['Pleroma.Captcha.Kocaptcha'],
-  }
-}
-</script>
-
-<style lang="scss">
-.upload {
-
-}
-</style>
diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
index e92fce0..bd524bc 100644
--- a/src/components/configSettings/forms/UploadForm.vue
+++ b/src/components/configSettings/forms/UploadForm.vue
@@ -120,11 +120,9 @@
 <script lang="ts">
 import { Component, Prop, Vue } from 'vue-property-decorator'
 import UploadConfig from '../../../entities/settings/UploadConfig'
-import VaSelect from "../../../vuestic-theme/vuestic-components/va-select/VaSelect.vue";
-import VaInputWrapper from "../../../vuestic-theme/vuestic-components/va-input/VaInputWrapper.vue";
 
 @Component({
-  components: {VaInputWrapper, VaSelect},
+  components: {},
 })
 export default class UploadForm extends Vue {
   @Prop(Object) value!: UploadConfig
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index c79bf75..96233ee 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -11,14 +11,22 @@
     <div class="config-settings-page__content pt-4">
       <!--<upload-form v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD" v-model="upload"/>-->
       <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
-      <instance-form v-if="configKeysTabs[value].key === configKeysEnum.INSTANCE" v-model="instance"/>
-      <frontend-configurations-form v-if="configKeysTabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS" v-model="frontendConfiguration"/>
-      <captcha-form
-        v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA"
-        v-model="captcha"
-        class="mb-3 captcha-form"
-      />
-      <kocaptcha-form v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA" v-model="kocaptcha"/>
+      <div v-if="config">
+        <template v-for="tab in configKeysTabs">
+          <universe-form
+            :key="tab.key"
+            v-model="config[tab.key]"
+            :title="tab.key"
+            v-if="configKeysTabs[value].key === tab.key"
+          />
+        </template>
+        <universe-form
+          :title="configKeysEnum.KOCAPTCHA"
+          v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA"
+          v-model="config[configKeysEnum.KOCAPTCHA]"
+          showTitle
+        />
+      </div>
     </div>
     <div class="flex-center pb-4">
       <va-button @click="onSaveButtunClick">Save settings</va-button>
@@ -33,30 +41,21 @@ import { ConfigService } from '../../../services/ConfigService'
 import { configKeysTabs, configKeys } from '../../../data/Config'
 import UploadForm from '../../configSettings/forms/UploadForm.vue'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
-import KocaptchaForm from '../../configSettings/forms/KocaptchaForm.vue'
-import InstanceForm from '../../configSettings/forms/InstanceForm.vue'
 import ConvertConfigToState from '../../../utils/ConvertConfigToState'
-import UploadConfig from '../../../entities/settings/UploadConfig'
-import EmailsConfig from '../../../entities/settings/EmailsConfig'
-import InstanceConfig from '../../../entities/settings/InstanceConfig'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
-import CaptchaForm from '../../configSettings/forms/CaptchaForm.vue'
-import CaptchaConfig from '../../../entities/settings/CaptchaConfig'
-import FrontendConfigurationsForm from '../../configSettings/forms/FrontendConfigurationsForm.vue'
-import FrontentConfigurationsConfig from '../../../entities/settings/FrontentConfigurationsConfig'
-import KocaptchaConfig from '../../../entities/settings/KocaptchaConfig'
+import UniverseForm from '../../configSettings/forms/ConfigForm.vue'
 
 @Component({
-  components: { FrontendConfigurationsForm, CaptchaForm, InstanceForm, EmailsForm, UploadForm, FulfillingBouncingCircleSpinner, KocaptchaForm },
+  components: {
+    UniverseForm,
+    EmailsForm,
+    UploadForm,
+    FulfillingBouncingCircleSpinner
+  },
 })
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
-  upload?: UploadConfig = new UploadConfig()
-  emails?: EmailsConfig = new EmailsConfig()
-  instance?: InstanceConfig = new InstanceConfig()
-  captcha?: CaptchaConfig = new CaptchaConfig()
-  kocaptcha?: KocaptchaConfig = new KocaptchaConfig()
-  frontendConfiguration?: FrontentConfigurationsConfig = new FrontentConfigurationsConfig()
+  config:any = null
   configKeysTabs: Array<object> = configKeysTabs
   configKeysEnum = configKeys
   loading:boolean = false
@@ -68,26 +67,13 @@ export default class ConfigSettingsPage extends Vue {
   }
   async onSaveButtunClick () {
     this.loading = true
-    const { upload, emails, instance, captcha, frontendConfiguration, kocaptcha } = this
-    const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest({
-      upload,
-      emails,
-      instance,
-      captcha,
-      frontendConfiguration,
-      kocaptcha
-    }))
+    const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest(this.config))
     this.loadConfigs(configs)
     this.loading = false
   }
   loadConfigs (configs) {
-    const { upload, emails, instance, captcha, frontendConfiguration, kocaptcha } = ConvertConfigToState(configs)
-    this.upload = upload
-    this.emails = emails
-    this.instance = instance
-    this.captcha = captcha
-    this.kocaptcha = kocaptcha
-    this.frontendConfiguration = frontendConfiguration
+    const config = ConvertConfigToState(configs)
+    this.config = config
   }
 }
 </script>
diff --git a/src/entities/settings/FrontentConfigurationsConfig.ts b/src/entities/settings/FrontentConfigurationsConfig.ts
index c429ca9..28e160d 100644
--- a/src/entities/settings/FrontentConfigurationsConfig.ts
+++ b/src/entities/settings/FrontentConfigurationsConfig.ts
@@ -2,6 +2,7 @@ import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
 
 export default class FrontentConfigurationsConfig {
   constructor(existConfig?) {
+    // debugger;
     normalizeApiConfig(existConfig, this)
   }
   pleroma_fe: object = {
diff --git a/src/i18n/en.json b/src/i18n/en.json
index b577532..e9b158b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -487,132 +487,149 @@
       "server_id": "server id",
       "endpoint": "endpoint"
     },
-    "instance_form": {
+    ":instance_form": {
       "name": "Name",
-      "name_label": "The instance’s name",
+      "name_note": "The instance’s name",
       "email": "Email",
-      "email_label": "Email used to reach an Administrator/Moderator of the instance",
+      "email_note": "Email used to reach an Administrator/Moderator of the instance",
       "notify_email": "Notify email",
-      "notify_email_label": "Email used for notifications",
+      "notify_email_note": "Email used for notifications",
       "description": "Description",
-      "description_label": "The instance’s description, can be seen in nodeinfo and /api/v1/instance",
+      "description_note": "The instance’s description, can be seen in nodeinfo and /api/v1/instance",
       "limit": "Limit",
-      "limit_label": "Posts character limit (CW/Subject included in the counter)",
+      "limit_note": "Posts character limit (CW/Subject included in the counter)",
       "remote_limit": "Remote limit",
-      "remote_limit_label": "Hard character limit beyond which remote posts will be dropped.",
+      "remote_limit_note": "Hard character limit beyond which remote posts will be dropped.",
       "upload_limit": "Upload limit",
-      "upload_limit_label": "File size limit of uploads (except for avatar, background, banner)",
+      "upload_limit_note": "File size limit of uploads (except for avatar, background, banner)",
       "avatar_upload_limit": "Avatar upload limit",
-      "avatar_upload_limit_label": "File size limit of user’s profile avatars",
+      "avatar_upload_limit_note": "File size limit of user’s profile avatars",
       "background_upload_limit": "Background upload limit",
-      "background_upload_limit_label": "File size limit of user’s profile backgrounds",
+      "background_upload_limit_note": "File size limit of user’s profile backgrounds",
       "banner_upload_limit": "Banner upload limit",
-      "banner_upload_limit_label": "File size limit of user’s profile banners",
-      "poll_limits": "Poll limits",
-      "poll_limits_label": "A map with poll limits for local polls",
-      "max_options": "Maximum number of options",
-      "max_option_chars": "Maximum number of characters per option",
-      "min_expiration": "Minimum expiration time (in seconds)",
-      "max_expiration": "Maximum expiration time (in seconds)",
-      "registrations_open": "Registrations open",
-      "registrations_open_label": "Enable registrations for anyone, invitations can be enabled when false.",
-      "dedupe_media": "Dedupe media",
-      "invites_enabled": "Invites enabled",
-      "invites_enabled_label": "Enable user invitations for admins (depends on registrations_open: false).",
+      "banner_upload_limit_note": "File size limit of user’s profile banners",
+      "poll_limits": "A map with poll limits for local polls",
       "account_activation_required": "Account activation required",
-      "account_activation_required_label": "Require users to confirm their emails before signing in.",
+      "account_activation_required_note": "Require users to confirm their emails before signing in.",
       "federating": "Federating",
-      "federating_label": "Enable federation with other instances",
+      "federating_note": "Enable federation with other instances",
       "federation_reachability_timeout_days": "Federation reachability timeout days",
-      "federation_reachability_timeout_days_label": "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.",
+      "federation_reachability_timeout_days_note": "Timeout (in days) of each external federation target being unreachable prior to pausing federating to it.",
       "federation_incoming_replies_max_depth": "Max. depth of reply-to activities fetching on incoming federation",
-      "federation_incoming_replies_max_depth_label": "Used to prevent out-of-memory situations while fetching very long threads. If set to nil, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
+      "federation_incoming_replies_max_depth_note": "Used to prevent out-of-memory situations while fetching very long threads. If set to nil, threads of any depth will be fetched. Lower this value if you experience out-of-memory crashes.",
       "federation_publisher_modules": "",
-      "federation_publisher_modules_label": "",
+      "federation_publisher_modules_note": "",
+      "dedupe_media": "Dedupe media",
+      "dedupe_media_note": "",
       "allow_relay": "Allow relay",
-      "allow_relay_label": "Enable Pleroma’s Relay, which makes it possible to follow a whole instance",
+      "allow_relay_note": "Enable Pleroma’s Relay, which makes it possible to follow a whole instance",
       "rewrite_policy": "Rewrite policy",
-      "rewrite_policy_label": "Message Rewrite Policy, either one or a list. Here are the ones available by default:",
+      "rewrite_policy_note": "Message Rewrite Policy, either one or a list. Here are the ones available by default:",
       "public": "Public",
-      "public_label": "Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.",
+      "public_note": "Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.",
       "quarantined_instances": "Quarantined instances",
-      "quarantined_instances_label": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send. Separate items with ;",
+      "quarantined_instances_note": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send. Separate items with ;",
       "managed_config": "Managed config",
-      "managed_config_label": "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
+      "managed_config_note": "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
       "static_dir": "",
-      "static_dir_label": "",
+      "static_dir_note": "",
       "allowed_post_formats": "Allowed post formats",
-      "allowed_post_formats_label": "MIME-type list of formats allowed to be posted (transformed into HTML)",
+      "allowed_post_formats_note": "MIME-type list of formats allowed to be posted (transformed into HTML)",
       "mrf_transparency": "mrf transparency",
-      "mrf_transparency_label": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
+      "mrf_transparency_note": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
       "mrf_transparency_exclusions": "Exclude specific instance names from MRF transparency.",
-      "mrf_transparency_exclusions_label": "The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. Separate items with ;",
+      "mrf_transparency_exclusions_note": "The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. Separate items with ;",
       "extended_nickname_format": "Extended nickname format",
-      "extended_nickname_format_label": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
+      "extended_nickname_format_note": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
       "max_pinned_statuses": "Max pinned status",
-      "max_pinned_statuses_label": "The maximum number of pinned statuses. 0 will disable the feature.",
+      "max_pinned_statuses_note": "The maximum number of pinned statuses. 0 will disable the feature.",
       "autofollowed_nicknames": "Autofollowed nicknames",
-      "autofollowed_nicknames_label": "Set to nicknames of (local) users that every new user should automatically follow. Separate items with ;",
+      "autofollowed_nicknames_note": "Set to nicknames of (local) users that every new user should automatically follow. Separate items with ;",
       "no_attachment_links": "No attachment links",
-      "no_attachment_links_label": "Set to true to disable automatically adding attachment link text to statuses",
+      "no_attachment_links_note": "Set to true to disable automatically adding attachment link text to statuses",
       "welcome_message": "Welcome message",
-      "welcome_message_label": "A message that will be send to a newly registered users as a direct message.",
+      "welcome_message_note": "A message that will be send to a newly registered users as a direct message.",
       "welcome_user_nickname": "Welcome user nickname",
-      "welcome_user_nickname_label": "The nickname of the local user that sends the welcome message.",
+      "welcome_user_nickname_note": "The nickname of the local user that sends the welcome message.",
       "max_report_comment_size": "Max report comment size",
-      "max_report_comment_size_label": "The maximum size of the report comment (Default: 1000)",
+      "max_report_comment_size_note": "The maximum size of the report comment (Default: 1000)",
       "safe_dm_mentions": "Safe dm mentions",
-      "safe_dm_mentions_label": " If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. \"@friend hey i really don't like @enemy\")",
+      "safe_dm_mentions_note": " If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. \"@friend hey i really don't like @enemy\")",
       "healthcheck": "Healthcheck",
-      "healthcheck_label": "If set to true, system data will be shown on /api/pleroma/healthcheck.",
+      "healthcheck_note": "If set to true, system data will be shown on /api/pleroma/healthcheck.",
       "remote_post_retention_days": "Remote post retention days",
-      "remote_post_retention_days_label": "The default amount of days to retain remote posts when pruning the database.",
+      "remote_post_retention_days_note": "The default amount of days to retain remote posts when pruning the database.",
       "skip_thread_containment": "Skip thread containment",
       "limit_to_local_content": "Limit to local content",
-      "limit_to_local_content_label": "Limit unauthenticated users to search for local statutes and users only.",
+      "limit_to_local_content_note": "Limit unauthenticated users to search for local statutes and users only.",
       "dynamic_configuration": "Dynamic configuration",
-      "dynamic_configuration_label": "Allow transferring configuration to DB with the subsequent customization from Admin api.",
+      "dynamic_configuration_note": "Allow transferring configuration to DB with the subsequent customization from Admin api.",
       "external_user_synchronization": "External user synchronization",
-      "external_user_synchronization_note": "Enabling following/followers counters synchronization for external users."
+      "external_user_synchronization_note": "Enabling following/followers counters synchronization for external users.",
+      "registrations_open": "Registrations open",
+      "registrations_open_note": "Enable registrations for anyone, invitations can be enabled when false.",
+      "invites_enabled": "Invites enabled",
+      "invites_enabled_note": "Enable user invitations for admins (depends on registrations_open: false)."
     },
-    "frontend_configurations_form" : {
+    "poll_limits_form": {
+      "max_option_chars": "Maximum number of characters per option",
+      "max_option_chars_note": "",
+      "min_expiration": "Minimum expiration time (in seconds)",
+      "min_expiration_note": "",
+      "max_expiration": "Maximum expiration time (in seconds)",
+      "max_expiration_note": "",
+      "max_options": "Maximum number of options",
+      "max_options_note": ""
+    },
+    "pleroma_fe_form" : {
       "theme": "theme",
       "theme_note": "Which theme to use",
       "logo_note": "URL of the logo, defaults to Pleroma’s logo",
-      "logo_mask": "Logo mask",
-      "logo_mask_note": "Whether to use only the logo's shape as a mask (true) or as a regular image (false)",
-      "logo_margin": "Logo margin",
-      "logo_margin_note": "What margin to use around the logo",
+      "logoMask": "Logo mask",
+      "logoMask_note": "Whether to use only the logo's shape as a mask (true) or as a regular image (false)",
+      "logoMargin": "Logo margin",
+      "logoMargin_note": "What margin to use around the logo",
       "background": "Background",
       "background_note": "URL of the background, unless viewing a user profile with a background that is set",
-      "redirect_url": "Redirect URL",
-      "redirect_root_no_login": "Relative URL which indicates where to redirect when a user isn’t logged in.",
-      "redirect_root_login": "Relative URL which indicates where to redirect when a user is logged in.",
-      "show_instance_specific_panel": "Show instance specific panel",
-      "show_instance_specific_panel_note": "Whenether to show the instance’s specific panel.",
-      "subject_line_behavior": "Subject line behavior",
-      "subject_line_behavior_label": "Allows changing the default behaviour of subject lines in replies",
-      "scope_options_enabled": "Enable scope options",
-      "scope_options_enabled_note": "Enable setting an notice visibility and subject/CW when posting",
-      "scope_copy": "Scope copy",
-      "scope_copy_label": "Copy the scope (private/unlisted/public) in replies to posts by default.",
-      "formatting_options_enabled": "Enable formatting options",
-      "formatting_options_enabled_note": "Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting",
-      "collapse_message_with_subjects": "Collapse message with subject",
-      "collapse_message_with_subjects_note": "When a message has a subject(aka Content Warning), collapse it by default",
-      "always_show_subject_input": "Always show subject input",
-      "always_show_subject_input_label": "When set to false, auto-hide the subject field when it's empty.",
-      "hide_post_stats": "Hide notices statistics",
-      "hide_user_stats": "Hide profile statistics"
+      "redirectUrl": "Redirect URL",
+      "redirectUrl_note": "",
+      "redirectRootNoLogin": "Relative URL which indicates where to redirect when a user isn’t logged in.",
+      "redirectRootNoLogin_note": "",
+      "redirectRootLogin": "Relative URL which indicates where to redirect when a user is logged in.",
+      "redirectRootLogin_note": "",
+      "showInstanceSpecificPanel": "Show instance specific panel",
+      "showInstanceSpecificPanel_note": "Whenether to show the instance’s specific panel.",
+      "subjectLineBehavior": "Subject line behavior",
+      "subjectLineBehavior_note": "Allows changing the default behaviour of subject lines in replies",
+      "scopeOptionsEnabled": "Enable scope options",
+      "scopeOptionsEnabled_note": "Enable setting an notice visibility and subject/CW when posting",
+      "scopeCopy": "Scope copy",
+      "scopeCopy_note": "Copy the scope (private/unlisted/public) in replies to posts by default.",
+      "formattingOptionsEnabled": "Enable formatting options",
+      "formattingOptionsEnabled_note": "Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting",
+      "collapseMessageWithSubject": "Collapse message with subject",
+      "collapseMessageWithSubject_note": "When a message has a subject(aka Content Warning), collapse it by default",
+      "alwaysShowSubjectInput": "Always show subject input",
+      "alwaysShowSubjectInput_note": "When set to false, auto-hide the subject field when it's empty.",
+      "hidePostStats": "Hide notices statistics",
+      "hidePostStats_note": "",
+      "hideUserStats": "Hide profile statistics",
+      "hideUserStats_note": ""
     },
-    "captcha_form": {
-      "title": "Kocaptcha service",
+    "masto_fe_form": {
+      "showInstanceSpecificPanel": "Show instance specific panel",
+      "showInstanceSpecificPanel_note": "Whenether to show the instance’s specific panel."
+    },
+    "Pleroma.Captcha_form": {
       "enabled": "Enabled",
       "enabled_note": "Whether the captcha should be shown on registration",
       "method": "Method",
       "method_note": "The method/service to use for captcha",
       "seconds_valid": "Seconds valid",
-      "seconds_valid_note": "",
+      "seconds_valid_note": ""
+    },
+    "Pleroma.Captcha.Kocaptcha_form": {
+      "title": "Kocaptcha service",
       "endpoint": "The kocaptcha endpoint to use",
       "endpoint_note": ""
     }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index ee6c0e2..5372911 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -3,43 +3,17 @@ import { configKeys } from '../data/Config'
 
 export default (configs) => {
   const settings = []
-  // TODO: Now we need to control API requests. Refactore it.
-  // if (configs['upload']) {
-  //   const upload = {
-  //     group: 'pleroma',
-  //     key: configKeys.UPLOAD,
-  //     value: getConfigValue(normalizeUploadConfigValue(configs['upload']))
-  //   }
-  //   settings.push(upload)
-  // }
-  if (configs['captcha']) {
+  forIn(configs, (val, key) => {
+    let newVal = { ...val }
+    if (key === configKeys.INSTANCE) {
+      newVal = normalizeInstanceConfigValue(val)
+    }
     settings.push({
       group: 'pleroma',
-      key: configKeys.CAPTCHA,
-      value: getConfigValue(normalizeCaptchaConfigValue(configs['captcha']))
+      key,
+      value: getConfigValue(newVal)
     })
-  }
-  if (configs['kocaptcha']) {
-    settings.push({
-      group: 'pleroma',
-      key: configKeys.KOCAPTCHA,
-      value: getConfigValue(configs['kocaptcha'])
-    })
-  }
-  if (configs['instance']) {
-    settings.push({
-      group: 'pleroma',
-      key: configKeys.INSTANCE,
-      value: getConfigValue(normalizeInstanceConfigValue(configs['instance']))
-    })
-  }
-  if (configs['frontendConfiguration']) {
-    settings.push({
-      group: 'pleroma',
-      key: configKeys.FRONTEND_CONFIGURATIONS,
-      value: getConfigValue(configs['frontendConfiguration'])
-    })
-  }
+  })
   return { configs: settings }
 }
 
@@ -55,13 +29,6 @@ const normalizeUploadConfigValue = (config) => {
   return config
 }
 
-const normalizeCaptchaConfigValue = (config) => {
-  if (config.method !== 'Pleroma.Captcha.Kocaptcha') {
-    delete config.endpoint
-  }
-  return config
-}
-
 const normalizeInstanceConfigValue = (config) => {
   config.quarantined_instances = config.quarantined_instances.split(';')
   config.mrf_transparency_exclusions = config.mrf_transparency_exclusions.split(';')
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 26ba541..456bd22 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -9,16 +9,17 @@ import { forIn } from 'lodash'
 import KocaptchaConfig from "../entities/settings/KocaptchaConfig";
 
 export default (configs) => ({
-  upload: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
-  emails: new EmailsConfig(),
-  instance: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
-  captcha: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
-  kocaptcha: new KocaptchaConfig(t(configs.find(({ key }) => key === configKeys.KOCAPTCHA), 'value').safeObject),
-  frontendConfiguration: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject)
+  // upload: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
+  // emails: new EmailsConfig(),
+  [configKeys.INSTANCE]: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
+  [configKeys.CAPTCHA]: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
+  [configKeys.KOCAPTCHA]: new KocaptchaConfig(t(configs.find(({ key }) => key === configKeys.KOCAPTCHA), 'value').safeObject),
+  [configKeys.FRONTEND_CONFIGURATIONS]: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject)
 })
 
 export const normalizeApiConfig = function(existConfig, classObject) {
   if (existConfig) {
+    // debugger;
     parseObj(existConfig, classObject)
   }
   function parseObj(config, resultObject) {
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 38c9a3d..3588980 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -1,22 +1,51 @@
-import { forIn } from 'lodash'
+import { forIn, get } from 'lodash'
+
+export const selectOptions = {
+  rewrite_policy: [
+    'Pleroma.Web.ActivityPub.MRF.NoOpPolicy',
+    'Pleroma.Web.ActivityPub.MRF.DropPolicy',
+    'Pleroma.Web.ActivityPub.MRF.SimplePolicy',
+    'Pleroma.Web.ActivityPub.MRF.TagPolicy',
+    'Pleroma.Web.ActivityPub.MRF.SubchainPolicy',
+    'Pleroma.Web.ActivityPub.MRF.RejectNonPublic',
+    'Pleroma.Web.ActivityPub.MRF.EnsureRePrepended',
+    'Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy',
+    'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy'
+  ],
+  allowed_post_formats: [],
+  limit_to_local_content: [':unauthenticated', ':all', 'false'],
+  federation_publisher_modules: ['Pleroma.Web.ActivityPub.Publisher', 'Pleroma.Web.Websub', 'Pleroma.Web.Salmon'],
+  method: ['Pleroma.Captcha.Kocaptcha'],
+  theme: [],
+  subject_line_behavior: ['email', 'masto', 'noop'],
+}
 
 export default (formData) => {
   const list: Array<object> = []
   forIn(formData, (val, key) => {
-    let component = ''
-    if (typeof val === 'string' || typeof val === 'number') {
-      component = 'va-input'
+    //TODO: type
+    if (key === 'sendAsMap') {
+      return
     }
-    if (typeof val === 'boolean') {
-      component = 'va-checkbox'
+    const field: any= {
+      component: '',
+      model: key,
     }
-    if (typeof val === 'object' && Array.isArray(val)) {
-      component = 'va-select'
+    if (selectOptions[key]) {
+      field.component = 'va-select'
+      if (Array.isArray(val)){
+        field.multiple = true
+      }
+    } else if (typeof val === 'string' || typeof val === 'number') {
+      field.component = 'va-input'
+    } else if (typeof val === 'boolean') {
+      field.component = 'va-checkbox'
+    } else if (typeof val === 'object' && val){
+      field.component = 'parent'
+    }
+    if (field.component) {
+      list.push(field)
     }
-    list.push({
-      model: key,
-      component,
-    })
   })
   return list
 }
-- 
GitLab


From efd8ff20145c91a60341394289eade11daaa8f1f Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 19 Jul 2019 15:56:21 +0300
Subject: [PATCH 21/61] feat: :media_proxy, :database, add loader

---
 .../configSettings/forms/ConfigForm.vue       |  6 ++++
 .../configSettings/ConfigSettingsPage.vue     |  7 ++++
 src/data/Config.ts                            | 10 ++++++
 src/entities/settings/DatabaseConfig.ts       |  8 +++++
 src/entities/settings/InstanceConfig.ts       |  6 ++--
 src/entities/settings/MediaProxyConfig.ts     | 15 ++++++++
 src/entities/settings/UploadConfig.ts         |  2 +-
 src/i18n/en.json                              | 32 +++++++++++++++++
 src/utils/ConvertConfigToApiRequest.js        | 10 +++++-
 src/utils/ConvertConfigToState.ts             | 35 +++++++++++++------
 src/utils/GetFieldList.ts                     |  4 +++
 11 files changed, 119 insertions(+), 16 deletions(-)
 create mode 100644 src/entities/settings/DatabaseConfig.ts
 create mode 100644 src/entities/settings/MediaProxyConfig.ts

diff --git a/src/components/configSettings/forms/ConfigForm.vue b/src/components/configSettings/forms/ConfigForm.vue
index 2a7c2a5..6bbf95c 100644
--- a/src/components/configSettings/forms/ConfigForm.vue
+++ b/src/components/configSettings/forms/ConfigForm.vue
@@ -76,6 +76,12 @@ export default class ConfigForm extends Vue {
 
 <style lang="scss" scoped>
   .config-form {
+    padding-top: 0.5rem;
+    border-top: 1px solid $border-color;
+    &:first-of-type {
+      padding-top: 0;
+      border-top: none;
+    }
   }
   .note {
     margin-bottom: 1rem !important;
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 96233ee..21f8bd3 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -27,6 +27,13 @@
           showTitle
         />
       </div>
+      <div class="loading flex-center" v-if="loading">
+        <fulfilling-bouncing-circle-spinner
+          :animation-duration="2500"
+          :size="70"
+          color="#4ae387"
+        />
+      </div>
     </div>
     <div class="flex-center pb-4">
       <va-button @click="onSaveButtunClick">Save settings</va-button>
diff --git a/src/data/Config.ts b/src/data/Config.ts
index af9c1f5..0afa091 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -7,6 +7,8 @@ export enum configKeys {
   WEB = 'Pleroma.Web',
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
+  DATABASE = ':database',
+  MEDIA_PROXY = ':media_proxy'
 }
 
 export const configKeysTabs = [
@@ -38,4 +40,12 @@ export const configKeysTabs = [
     key: configKeys.CAPTCHA,
     name: 'Captcha'
   },
+  {
+    key: configKeys.DATABASE,
+    name: 'Database options'
+  },
+  {
+    key: configKeys.MEDIA_PROXY,
+    name: 'Media proxy'
+  }
 ]
diff --git a/src/entities/settings/DatabaseConfig.ts b/src/entities/settings/DatabaseConfig.ts
new file mode 100644
index 0000000..5d94cb3
--- /dev/null
+++ b/src/entities/settings/DatabaseConfig.ts
@@ -0,0 +1,8 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class DatabaseConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  rum_enabled: boolean = false
+}
diff --git a/src/entities/settings/InstanceConfig.ts b/src/entities/settings/InstanceConfig.ts
index b714846..83808f9 100644
--- a/src/entities/settings/InstanceConfig.ts
+++ b/src/entities/settings/InstanceConfig.ts
@@ -21,8 +21,8 @@ export default class InstanceConfig {
   remote_limit?: number
   upload_limit: string = ''
   avatar_upload_limit: string = ''
- background_upload_limit: string = ''
- banner_upload_limit: string = ''
+  background_upload_limit: string = ''
+  banner_upload_limit: string = ''
   poll_limits: object = {
     max_options:  20,
     max_option_chars: 200,
@@ -45,7 +45,7 @@ export default class InstanceConfig {
   managed_config: boolean = false
   static_dir: string = 'instance/static'
   allowed_post_formats: Array<string> = []
- mrf_transparency: boolean = false
+  mrf_transparency: boolean = false
   mrf_transparency_exclusions: any = []
   extended_nickname_format: boolean = false
   max_pinned_statuses?: number
diff --git a/src/entities/settings/MediaProxyConfig.ts b/src/entities/settings/MediaProxyConfig.ts
new file mode 100644
index 0000000..d3f291c
--- /dev/null
+++ b/src/entities/settings/MediaProxyConfig.ts
@@ -0,0 +1,15 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import {ReverseProxy} from "./UploadConfig";
+
+export default class MediaProxyConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+    this.whitelist = (this.whitelist && Array.isArray(this.whitelist))
+      ? this.whitelist.join(';')
+      : this.whitelist
+  }
+  proxy_opts?: ReverseProxy = new ReverseProxy()
+  base_url: string = ''
+  whitelist: any = ''
+  enabled: boolean = false
+}
diff --git a/src/entities/settings/UploadConfig.ts b/src/entities/settings/UploadConfig.ts
index 9779d4c..8db6365 100644
--- a/src/entities/settings/UploadConfig.ts
+++ b/src/entities/settings/UploadConfig.ts
@@ -1,6 +1,6 @@
 import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
 
-class ReverseProxy {
+export class ReverseProxy {
   redirect_on_failure: boolean = false
   max_body_length?: number
   max_read_duration: number = 3000
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e9b158b..1dc5e13 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -632,6 +632,38 @@
       "title": "Kocaptcha service",
       "endpoint": "The kocaptcha endpoint to use",
       "endpoint_note": ""
+    },
+    ":database_form" : {
+      "rum_enabled": "RUM indexing for full text search",
+      "rum_enabled_note": "If RUM indexes should be used"
+    },
+    ":media_proxy_form": {
+      "whitelist": "Whitelist",
+      "whitelist_note": "List of domains to bypass the mediaproxy. Separate items with ;",
+      "enabled": "Enabled",
+      "enabled_note": "Enables proxying of remote media to the instance’s proxy",
+      "base_url": "Base URL",
+      "base_url_note": "The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts."
+    },
+    "proxy_opts_form": {
+      "redirect_on_failure": "Redirect on failure",
+      "redirect_on_failure_note": "Redirects the client to the real remote URL if there's any HTTP errors. Any error during body processing will not be redirected as the response is chunked.",
+      "max_body_length": "Max body length",
+      "max_body_length_note": "limits the content length to be approximately the specified length. It is validated with the content-length header and also verified when proxying.",
+      "max_read_duration": "Max read duration",
+      "max_read_duration_note": "The total time the connection is allowed to read from the remote upstream.",
+      "inline_content_types": "Inline content types",
+      "inline_content_types_note": "true - Will not alter `content-disposition` (up to the upstream); false - Will add content-disposition: attachment to any request",
+      "req_headers": "Request headers",
+      "resp_headers": "Response headers",
+      "resp_headers_note": "",
+      "req_headers_note": "Additional headers"
+    },
+    "http_form": {
+      "follow_redirect": "Follow redirect",
+      "follow_redirect_note": "",
+      "pool": "Pool",
+      "pool_note": ""
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 5372911..a56e4a0 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -6,7 +6,10 @@ export default (configs) => {
   forIn(configs, (val, key) => {
     let newVal = { ...val }
     if (key === configKeys.INSTANCE) {
-      newVal = normalizeInstanceConfigValue(val)
+      newVal = normalizeInstanceConfigValue(newVal)
+    }
+    if (key === configKeys.MEDIA_PROXY) {
+      newVal = normalizeMediaProxyConfigValue(newVal)
     }
     settings.push({
       group: 'pleroma',
@@ -36,6 +39,11 @@ const normalizeInstanceConfigValue = (config) => {
   return config
 }
 
+const normalizeMediaProxyConfigValue = (config) => {
+  config.whitelist = config.whitelist.split(';')
+  return config
+}
+
 const getConfigValue = (config) => {
   const newConfig = []
   createTupledObject(newConfig, config)
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 456bd22..35ed5c3 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -6,7 +6,9 @@ import { configKeys } from '../data/Config'
 import FrontentConfigurationsConfig from '../entities/settings/FrontentConfigurationsConfig'
 import t from 'typy'
 import { forIn } from 'lodash'
-import KocaptchaConfig from "../entities/settings/KocaptchaConfig";
+import KocaptchaConfig from '../entities/settings/KocaptchaConfig'
+import DatabaseConfig from '../entities/settings/DatabaseConfig'
+import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
 
 export default (configs) => ({
   // upload: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
@@ -14,27 +16,38 @@ export default (configs) => ({
   [configKeys.INSTANCE]: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
   [configKeys.CAPTCHA]: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
   [configKeys.KOCAPTCHA]: new KocaptchaConfig(t(configs.find(({ key }) => key === configKeys.KOCAPTCHA), 'value').safeObject),
-  [configKeys.FRONTEND_CONFIGURATIONS]: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject)
+  [configKeys.FRONTEND_CONFIGURATIONS]: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject),
+  [configKeys.DATABASE]: new DatabaseConfig(t(configs.find(({ key }) => key === configKeys.DATABASE), 'value').safeObject),
+  [configKeys.MEDIA_PROXY]: new MediaProxyConfig(t(configs.find(({ key }) => key === configKeys.MEDIA_PROXY), 'value').safeObject),
 })
 
 export const normalizeApiConfig = function(existConfig, classObject) {
   if (existConfig) {
-    // debugger;
     parseObj(existConfig, classObject)
   }
   function parseObj(config, resultObject) {
     config.forEach(({tuple}) => {
       const key = tuple[0]
       const val = tuple[1]
-      if (typeof val === 'object' && !Array.isArray(val)) {
-        forIn(val, (value, innerKey) => {
-          delete val[innerKey]
-          val[innerKey.substring(1)] = value
-        })
-        if (resultObject[key.substring(1)].sendAsMap) {
-          val.sendAsMap = true
+      if (typeof val === 'object') {
+        if (Array.isArray(val)) {
+          const a = val.find(item => typeof item === 'object' && item.tuple)
+          if (a) {
+            delete resultObject[key]
+            resultObject[key.substring(1)] = parseObj(val, resultObject[key.substring(1)])
+          } else {
+            resultObject[key.substring(1)] = val
+          }
+        } else {
+          forIn(val, (value, innerKey) => {
+            delete val[innerKey]
+            val[innerKey.substring(1)] = value
+          })
+          if (resultObject[key.substring(1)].sendAsMap) {
+            val.sendAsMap = true
+          }
+          resultObject[key.substring(1)] = val
         }
-        resultObject[key.substring(1)] = val
       } else {
         resultObject[key.substring(1)] = val
       }
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 3588980..af1e9d4 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -18,6 +18,10 @@ export const selectOptions = {
   method: ['Pleroma.Captcha.Kocaptcha'],
   theme: [],
   subject_line_behavior: ['email', 'masto', 'noop'],
+  uploader: ['Pleroma.Uploaders.Local', 'Pleroma.Uploaders.S3'],
+  filter: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
+  args: ['strip', 'auto-orient', `{'impode': '1'}`],
+  inline_content_types: ['true', 'false', 'a list of whitelisted content types']
 }
 
 export default (formData) => {
-- 
GitLab


From d853f7e7ff795e4207a3d14546e93b954b2f2910 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 22 Jul 2019 11:40:56 +0300
Subject: [PATCH 22/61] feat: Pleroma.Upload

---
 .../configSettings/forms/UploadForm.vue       | 150 ------------------
 .../configSettings/ConfigSettingsPage.vue     |   3 -
 src/data/Config.ts                            |   8 +-
 src/i18n/en.json                              |  28 ++--
 src/utils/ConvertConfigToState.ts             |   2 +-
 src/utils/GetFieldList.ts                     |   2 +-
 6 files changed, 19 insertions(+), 174 deletions(-)
 delete mode 100644 src/components/configSettings/forms/UploadForm.vue

diff --git a/src/components/configSettings/forms/UploadForm.vue b/src/components/configSettings/forms/UploadForm.vue
deleted file mode 100644
index bd524bc..0000000
--- a/src/components/configSettings/forms/UploadForm.vue
+++ /dev/null
@@ -1,150 +0,0 @@
-<template>
-  <div class="upload-form">
-    <va-select
-      v-model="formData.uploader"
-      :options="selectOptions.uploader"
-      :label="$t('config_settings.upload_form.uploader')"
-    />
-    <div v-if="formData.uploader === 'Pleroma.Uploaders.Local'" class="mx-4 my-3">
-      <va-input
-        v-model="formData.uploads"
-        :label="$t('config_settings.upload_form.uploads')"/>
-    </div>
-    <div v-if="formData.uploader === 'Pleroma.Uploaders.S3'" class="mx-4 my-3">
-      <va-input
-        v-model="formData.bucket"
-        :label="$t('config_settings.upload_form.s3_bucket')"
-      />
-      <va-input
-        v-model="formData.public_endpoint"
-        :label="$t('config_settings.upload_form.s3_public_endpoint')"
-      />
-      <va-input
-        v-model="formData.truncated_namespace"
-        :label="$t('config_settings.upload_form.truncated_namespace')"
-        class="mb-0"
-      />
-      <p class="note">{{$t('config_settings.upload_form.truncated_namespace_note')}}</p>
-    </div>
-    <va-select
-      v-model="formData.filters"
-      :options="selectOptions.filter"
-      multiple
-      :label="$t('config_settings.upload_form.filters')"
-    />
-    <div v-if="formData.filters.includes('Pleroma.Upload.Filter.Mogrify')" class="mx-4 my-3">
-      <va-select
-        v-model="formData.args"
-        :options="selectOptions.args"
-        multiple
-        :label="$t('config_settings.upload_form.args')"/>
-    </div>
-    <div v-if="formData.filters.includes('Pleroma.Upload.Filter.AnonymizeFilename')" class="mx-4 my-3">
-      <va-input
-        v-model="formData.text"
-        :label="$t('config_settings.upload_form.anonymize_filename')"
-        class="mb-0"
-      />
-      <p class="note">{{$t('config_settings.upload_form.anonymize_filename_note')}}</p>
-    </div>
-    <va-checkbox
-      :label="$t('config_settings.upload_form.link_name')"
-      v-model="formData.link_name"
-    />
-    <p class="note">{{$t('config_settings.upload_form.link_name_note')}}</p>
-    <va-input
-      v-model="formData.base_url"
-      :label="$t('config_settings.upload_form.base_url')"
-    />
-    <va-checkbox
-      v-model="formData.proxy_remote"
-      :label="$t('config_settings.upload_form.proxy_remote')"
-    />
-    <div class="my-4 pt-2 upload-form__proxy">
-      <p class="title">{{$t('config_settings.upload_form.proxy_options')}}</p>
-      <va-input-wrapper>
-        <va-checkbox
-          v-model="formData.proxy_opts.redirect_on_failure"
-          :label="$t('config_settings.upload_form.redirect_on_failure')"
-        />
-      </va-input-wrapper>
-      <p class="note">{{$t('config_settings.upload_form.redirect_on_failure_note')}}</p>
-      <va-input
-        class="mb-0"
-        v-model.number="formData.proxy_opts.max_body_length"
-        type="number"
-        :label="$t('config_settings.upload_form.max_body_length')"
-      />
-      <p class="note">{{$t('config_settings.upload_form.max_body_length_note')}}</p>
-      <va-input
-        class="mb-0"
-        v-model.number="formData.proxy_opts.max_read_duration"
-        type="number"
-        :label="$t('config_settings.upload_form.max_body_length')"
-      />
-      <p class="note">{{$t('config_settings.upload_form.max_body_length_note')}}</p>
-      <va-select
-        v-model="formData.proxy_opts.inline_content_types"
-        :options="selectOptions.inline_content_types"
-      />
-      <p class="note" v-if="formData.proxy_opts.inline_content_types !== 'a list of whitelisted content types'">
-        {{$t(`config_settings.upload_form.inline_content_types_${formData.proxy_opts.inline_content_types}`)}}
-      </p>
-      <va-input
-        v-model="formData.proxy_opts.req_headers"
-        :label="$t('config_settings.upload_form.req_headers')"
-      />
-      <va-input
-        class="mb-0"
-        v-model="formData.proxy_opts.resp_headers"
-        :label="$t('config_settings.upload_form.resp_headers')"
-      />
-      <p class="note">{{$t('config_settings.upload_form.req_headers_note')}}</p>
-    </div>
-    <div class="upload-form__http pt-3">
-      <p class="title">HTTP</p>
-      <va-input-wrapper>
-        <va-checkbox
-          v-model="formData.proxy_opts.http.follow_redirect"
-          :label="$t('config_settings.upload_form.http.follow_redirect')"
-        />
-      </va-input-wrapper>
-      <va-input
-        v-model="formData.proxy_opts.http.pool"
-        :label="$t('config_settings.upload_form.http.pool')"
-      />
-    </div>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import UploadConfig from '../../../entities/settings/UploadConfig'
-
-@Component({
-  components: {},
-})
-export default class UploadForm extends Vue {
-  @Prop(Object) value!: UploadConfig
-  get formData () {
-    return this.value
-  }
-  set formData (val) {
-    this.$emit('updateForm', val)
-  }
-  selectOptions = {
-    uploader: ['Pleroma.Uploaders.Local', 'Pleroma.Uploaders.S3'],
-    filter: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
-    args: ['strip', 'auto-orient', `{'impode': '1'}`],
-    inline_content_types: ['true', 'false', 'a list of whitelisted content types']
-  }
-}
-</script>
-
-<style lang="scss">
-.upload-form {
-  &__proxy, &__http {
-    border-top: 1px solid $border-color;
-  }
-}
-</style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 21f8bd3..70c236c 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -9,7 +9,6 @@
       </va-tab>
     </va-tabs>
     <div class="config-settings-page__content pt-4">
-      <!--<upload-form v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD" v-model="upload"/>-->
       <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
       <div v-if="config">
         <template v-for="tab in configKeysTabs">
@@ -46,7 +45,6 @@ import { Component, Vue } from 'vue-property-decorator'
 import { FulfillingBouncingCircleSpinner } from 'epic-spinners'
 import { ConfigService } from '../../../services/ConfigService'
 import { configKeysTabs, configKeys } from '../../../data/Config'
-import UploadForm from '../../configSettings/forms/UploadForm.vue'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
 import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
@@ -56,7 +54,6 @@ import UniverseForm from '../../configSettings/forms/ConfigForm.vue'
   components: {
     UniverseForm,
     EmailsForm,
-    UploadForm,
     FulfillingBouncingCircleSpinner
   },
 })
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 0afa091..289d051 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -12,10 +12,10 @@ export enum configKeys {
 }
 
 export const configKeysTabs = [
-  // {
-  //   key: configKeys.UPLOAD,
-  //   name: 'Upload',
-  // },
+  {
+    key: configKeys.UPLOAD,
+    name: 'Upload',
+  },
   // {
   //   key: configKeys.EMAILS,
   //   name: 'Emails'
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 1dc5e13..e02e68c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -436,33 +436,31 @@
     "add_note_placeholder": "Add note..."
   },
   "config_settings": {
-    "upload_form": {
+    "Pleroma.Upload_form": {
       "uploader": "Uploader",
+      "uploader_note": "",
       "uploads": "Uploads",
-      "s3_bucket": "S3 bucket name",
-      "s3_public_endpoint": "S3 endpoint that the user finally accesses",
+      "uploads_note": "",
+      "bucket": "S3 bucket name",
+      "bucket_note": "",
+      "public_endpoint": "S3 endpoint that the user finally accesses",
+      "public_endpoint_note": "",
       "truncated_namespace": "Truncated_namespace",
       "truncated_namespace_note": "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc.\n        For example, when using CDN to S3 virtual host format, set \"\".\n        At this time, write CNAME to CDN in public_endpoint.",
       "filters": "Filters",
+      "filters_note": "",
       "args": "List of actions for the mogrify command",
-      "anonymize_filename": "Anonymize filename",
-      "anonymize_filename_note": "Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.",
+      "args_note": "",
       "link_name": "Link name",
       "link_name_note": "When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe",
       "base_url": "Base URL",
+      "base_url_note": "",
       "proxy_remote": "Proxy remote",
-      "proxy_options": "Proxy options",
+      "proxy_remote_note": "If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.",
       "redirect_on_failure": "Redirect on failure",
       "redirect_on_failure_note": "Redirects the client to the real remote URL if there's any HTTP errors. Any error during body processing will not be redirected as the response is chunked.",
-      "max_body_length": "Max body length",
-      "max_body_length_note": "limits the content length to be approximately the specified length. It is validated with the content-length header and also verified when proxying.",
-      "max_read_duration": "Max read duration",
-      "max_read_duration_note": "the total time the connection is allowed to read from the remote upstream.",
-      "inline_content_types_true": "will not alter `content-disposition` (up to the upstream),",
-      "inline_content_types_false": "will add content-disposition: attachment to any request",
-      "req_headers": "Req_headers",
-      "resp_headers": "Resp_headers",
-      "req_headers_note": "Additional headers"
+      "text": "Anonymize filename",
+      "text_note": "Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.\n"
     },
     "emails": {
       "adapter_title": "{name} adapter config",
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 35ed5c3..b1be7cc 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -11,7 +11,7 @@ import DatabaseConfig from '../entities/settings/DatabaseConfig'
 import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
 
 export default (configs) => ({
-  // upload: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
+  [configKeys.UPLOAD]: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
   // emails: new EmailsConfig(),
   [configKeys.INSTANCE]: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
   [configKeys.CAPTCHA]: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index af1e9d4..2395c62 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -19,7 +19,7 @@ export const selectOptions = {
   theme: [],
   subject_line_behavior: ['email', 'masto', 'noop'],
   uploader: ['Pleroma.Uploaders.Local', 'Pleroma.Uploaders.S3'],
-  filter: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
+  filters: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
   args: ['strip', 'auto-orient', `{'impode': '1'}`],
   inline_content_types: ['true', 'false', 'a list of whitelisted content types']
 }
-- 
GitLab


From 67b6cf8b796471b7293abb0ec0afb04afb2385cc Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 22 Jul 2019 14:20:18 +0300
Subject: [PATCH 23/61] feat: :ldap

---
 .../configSettings/forms/ConfigForm.vue       |  2 +-
 .../configSettings/ConfigSettingsPage.vue     |  9 ++++---
 src/data/Config.ts                            |  7 ++++-
 src/entities/settings/LdapConfig.ts           | 22 +++++++++++++++
 src/i18n/en.json                              | 26 ++++++++++++++++++
 src/utils/ConvertConfigToApiRequest.js        | 27 +++++++++----------
 src/utils/ConvertConfigToState.ts             |  2 ++
 7 files changed, 74 insertions(+), 21 deletions(-)
 create mode 100644 src/entities/settings/LdapConfig.ts

diff --git a/src/components/configSettings/forms/ConfigForm.vue b/src/components/configSettings/forms/ConfigForm.vue
index 6bbf95c..80f8208 100644
--- a/src/components/configSettings/forms/ConfigForm.vue
+++ b/src/components/configSettings/forms/ConfigForm.vue
@@ -1,6 +1,6 @@
 <template>
   <div class='config-form' :style="{margin: margin}">
-    <p class="title" v-if="showTitle">{{title}}</p>
+    <p class="title" v-if="showTitle">{{$t(`config_settings.${title}_form.title`)}}</p>
     <template v-for="(field, index) in fields">
       <config-form
         v-if="field.component === 'parent'"
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 70c236c..89dd37a 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -12,14 +12,15 @@
       <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
       <div v-if="config">
         <template v-for="tab in configKeysTabs">
-          <universe-form
+          <config-form
             :key="tab.key"
             v-model="config[tab.key]"
             :title="tab.key"
+            :showTitle="configKeysTabs[value].key === configKeysEnum.LDAP"
             v-if="configKeysTabs[value].key === tab.key"
           />
         </template>
-        <universe-form
+        <config-form
           :title="configKeysEnum.KOCAPTCHA"
           v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA"
           v-model="config[configKeysEnum.KOCAPTCHA]"
@@ -48,11 +49,11 @@ import { configKeysTabs, configKeys } from '../../../data/Config'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
 import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
-import UniverseForm from '../../configSettings/forms/ConfigForm.vue'
+import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 
 @Component({
   components: {
-    UniverseForm,
+    ConfigForm,
     EmailsForm,
     FulfillingBouncingCircleSpinner
   },
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 289d051..d3e2a14 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -8,7 +8,8 @@ export enum configKeys {
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
   DATABASE = ':database',
-  MEDIA_PROXY = ':media_proxy'
+  MEDIA_PROXY = ':media_proxy',
+  LDAP = ':ldap'
 }
 
 export const configKeysTabs = [
@@ -47,5 +48,9 @@ export const configKeysTabs = [
   {
     key: configKeys.MEDIA_PROXY,
     name: 'Media proxy'
+  },
+  {
+    key: configKeys.LDAP,
+    name: 'LDAP'
   }
 ]
diff --git a/src/entities/settings/LdapConfig.ts b/src/entities/settings/LdapConfig.ts
new file mode 100644
index 0000000..1b12771
--- /dev/null
+++ b/src/entities/settings/LdapConfig.ts
@@ -0,0 +1,22 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class LdapConfig {
+  constructor (existConfig?) {
+    normalizeApiConfig(existConfig, this)
+    this.sslopts = (this.sslopts && Array.isArray(this.sslopts))
+      ? this.sslopts.join(';')
+      : this.sslopts
+    this.tlsopts = (this.tlsopts && Array.isArray(this.tlsopts))
+      ? this.tlsopts.join(';')
+      : this.tlsopts
+  }
+  enabled: boolean = false
+  host: string = 'localhost'
+  port: number = 389
+  ssl: boolean = false
+  sslopts: any = ''
+  tls: number = 389
+  tlsopts: any = ''
+  base: string = 'dc=example,dc=com'
+  uid: string = 'cn'
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e02e68c..88f76ff 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -570,6 +570,7 @@
       "invites_enabled_note": "Enable user invitations for admins (depends on registrations_open: false)."
     },
     "poll_limits_form": {
+      "title": "Poll limits",
       "max_option_chars": "Maximum number of characters per option",
       "max_option_chars_note": "",
       "min_expiration": "Minimum expiration time (in seconds)",
@@ -580,6 +581,7 @@
       "max_options_note": ""
     },
     "pleroma_fe_form" : {
+      "title": "Pleroma FE",
       "theme": "theme",
       "theme_note": "Which theme to use",
       "logo_note": "URL of the logo, defaults to Pleroma’s logo",
@@ -615,6 +617,7 @@
       "hideUserStats_note": ""
     },
     "masto_fe_form": {
+      "title": "Masto FE",
       "showInstanceSpecificPanel": "Show instance specific panel",
       "showInstanceSpecificPanel_note": "Whenether to show the instance’s specific panel."
     },
@@ -644,6 +647,7 @@
       "base_url_note": "The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts."
     },
     "proxy_opts_form": {
+      "title": "Proxy options",
       "redirect_on_failure": "Redirect on failure",
       "redirect_on_failure_note": "Redirects the client to the real remote URL if there's any HTTP errors. Any error during body processing will not be redirected as the response is chunked.",
       "max_body_length": "Max body length",
@@ -658,10 +662,32 @@
       "req_headers_note": "Additional headers"
     },
     "http_form": {
+      "title": "HTTP",
       "follow_redirect": "Follow redirect",
       "follow_redirect_note": "",
       "pool": "Pool",
       "pool_note": ""
+    },
+    ":ldap_form": {
+      "title": "Use LDAP for user authentication.  When a user logs in to the Pleroma\ninstance, the name and password will be verified by trying to authenticate\n(bind) to an LDAP server.  If a user exists in the LDAP directory but there\nis no account with the same name yet on the Pleroma instance then a new\nPleroma account will be created with the same name as the LDAP user name.",
+      "enabled": "Enabled",
+      "enabled_note": "Enables LDAP authentication",
+      "host": "LDAP server hostname",
+      "host_note": "",
+      "port": "LDAP port",
+      "port_note": "e.g. 389 or 636",
+      "ssl": "SSL",
+      "ssl_note": "true to use SSL, usually implies the port 636",
+      "sslopts": "Additional SSL options",
+      "sslopts_note": "Saparate items with ;",
+      "tls": "TLS",
+      "tls_note": "true to start TLS, usually implies the port 389",
+      "tlsopts": "Additional TLS options",
+      "tlsopts_note": "Separate items with ;",
+      "base": "LDAP base",
+      "base_note": "e.g. 'dc=example,dc=com'",
+      "uid": "UID",
+      "uid_note": "LDAP attribute name to authenticate the user, e.g. when 'cn', the filter will be 'cn=username,base'"
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index a56e4a0..3a8d033 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -11,6 +11,9 @@ export default (configs) => {
     if (key === configKeys.MEDIA_PROXY) {
       newVal = normalizeMediaProxyConfigValue(newVal)
     }
+    if (key === configKeys.LDAP) {
+      newVal = normalizeLdapConfigValue(newVal)
+    }
     settings.push({
       group: 'pleroma',
       key,
@@ -20,27 +23,21 @@ export default (configs) => {
   return { configs: settings }
 }
 
-const normalizeUploadConfigValue = (config) => {
-  if (config.uploader === 'Pleroma.Uploaders.Local') {
-    delete config.s3_bucket
-    delete config.s3_public_endpoint
-    delete config.truncated_namespace
-  }
-  if (config.uploader === 'Pleroma.Uploaders.S3') {
-    delete config.uploads
-  }
+const normalizeInstanceConfigValue = (config) => {
+  config.quarantined_instances = config.quarantined_instances ? config.quarantined_instances.split(';') : []
+  config.mrf_transparency_exclusions = config.mrf_transparency_exclusions ? config.mrf_transparency_exclusions.split(';') : []
+  config.autofollowed_nicknames = config.autofollowed_nicknames ? config.autofollowed_nicknames.split(';') : []
   return config
 }
 
-const normalizeInstanceConfigValue = (config) => {
-  config.quarantined_instances = config.quarantined_instances.split(';')
-  config.mrf_transparency_exclusions = config.mrf_transparency_exclusions.split(';')
-  config.autofollowed_nicknames = config.autofollowed_nicknames.split(';')
+const normalizeMediaProxyConfigValue = (config) => {
+  config.whitelist = config.whitelist ? config.whitelist.split(';') : []
   return config
 }
 
-const normalizeMediaProxyConfigValue = (config) => {
-  config.whitelist = config.whitelist.split(';')
+const normalizeLdapConfigValue = (config) => {
+  config.sslopts = config.sslopts ? config.sslopts.split(';') : []
+  config.tlsopts = config.tlsopts ? config.tlsopts.split(';') : []
   return config
 }
 
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index b1be7cc..f21df3c 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -9,6 +9,7 @@ import { forIn } from 'lodash'
 import KocaptchaConfig from '../entities/settings/KocaptchaConfig'
 import DatabaseConfig from '../entities/settings/DatabaseConfig'
 import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
+import LdapConfig from "../entities/settings/LdapConfig";
 
 export default (configs) => ({
   [configKeys.UPLOAD]: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
@@ -19,6 +20,7 @@ export default (configs) => ({
   [configKeys.FRONTEND_CONFIGURATIONS]: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject),
   [configKeys.DATABASE]: new DatabaseConfig(t(configs.find(({ key }) => key === configKeys.DATABASE), 'value').safeObject),
   [configKeys.MEDIA_PROXY]: new MediaProxyConfig(t(configs.find(({ key }) => key === configKeys.MEDIA_PROXY), 'value').safeObject),
+  [configKeys.LDAP]: new LdapConfig(t(configs.find(({ key }) => key === configKeys.LDAP), 'value').safeObject),
 })
 
 export const normalizeApiConfig = function(existConfig, classObject) {
-- 
GitLab


From 2bb531d260aeea27b8564e50408aabe37e0f0855 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 22 Jul 2019 19:35:32 +0300
Subject: [PATCH 24/61] feat: upload

---
 .../configSettings/ConfigSettingsPage.vue     | 12 +++++++++
 src/data/Config.ts                            |  2 ++
 src/entities/settings/UploadConfig.ts         |  5 ----
 src/entities/settings/UploadersLocalConfig.ts |  8 ++++++
 src/entities/settings/UploadersS3Config.ts    | 10 ++++++++
 src/i18n/en.json                              | 25 +++++++++++++------
 src/utils/ConvertConfigToState.ts             |  8 ++++--
 7 files changed, 55 insertions(+), 15 deletions(-)
 create mode 100644 src/entities/settings/UploadersLocalConfig.ts
 create mode 100644 src/entities/settings/UploadersS3Config.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 89dd37a..48468de 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -20,6 +20,18 @@
             v-if="configKeysTabs[value].key === tab.key"
           />
         </template>
+        <config-form
+          :title="configKeysEnum.UPLOADERSS3"
+          v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD"
+          v-model="config[configKeysEnum.UPLOADERSS3]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.UPLOADERSLOCAL"
+          v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD"
+          v-model="config[configKeysEnum.UPLOADERSLOCAL]"
+          showTitle
+        />
         <config-form
           :title="configKeysEnum.KOCAPTCHA"
           v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA"
diff --git a/src/data/Config.ts b/src/data/Config.ts
index d3e2a14..0997fbd 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -1,5 +1,7 @@
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
+  UPLOADERSS3 = 'Pleroma.Uploaders.S3',
+  UPLOADERSLOCAL = 'Pleroma.Uploaders.Local',
   EMAILS = 'Pleroma.Emails',
   INSTANCE = ':instance',
   LOGGER = ':logger',
diff --git a/src/entities/settings/UploadConfig.ts b/src/entities/settings/UploadConfig.ts
index 8db6365..4a1e43f 100644
--- a/src/entities/settings/UploadConfig.ts
+++ b/src/entities/settings/UploadConfig.ts
@@ -19,11 +19,6 @@ export default class UploadConfig {
   }
   uploader: string = 'Pleroma.Uploaders.Local'
   filters: Array<string> = []
-  uploads: string = ''
-  bucket: string = ''
-  public_endpoint: string = ''
-  truncated_namespace
-  args: Array<string> = []
   text: string = ''
   link_name: boolean = false
   base_url: string = ''
diff --git a/src/entities/settings/UploadersLocalConfig.ts b/src/entities/settings/UploadersLocalConfig.ts
new file mode 100644
index 0000000..890e6fe
--- /dev/null
+++ b/src/entities/settings/UploadersLocalConfig.ts
@@ -0,0 +1,8 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
+export default class UploadersLocalConfig {
+  constructor (existConfig) {
+    normalizeApiConfig(existConfig, this)
+  }
+  uploads: string = ''
+}
diff --git a/src/entities/settings/UploadersS3Config.ts b/src/entities/settings/UploadersS3Config.ts
new file mode 100644
index 0000000..b246ef6
--- /dev/null
+++ b/src/entities/settings/UploadersS3Config.ts
@@ -0,0 +1,10 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
+export default class UploadersS3Config {
+  constructor (existConfig) {
+    normalizeApiConfig(existConfig, this)
+  }
+  bucket = null
+  public_endpoint: string = ''
+  truncated_namespace: string = ''
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 88f76ff..71b97fe 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -439,14 +439,6 @@
     "Pleroma.Upload_form": {
       "uploader": "Uploader",
       "uploader_note": "",
-      "uploads": "Uploads",
-      "uploads_note": "",
-      "bucket": "S3 bucket name",
-      "bucket_note": "",
-      "public_endpoint": "S3 endpoint that the user finally accesses",
-      "public_endpoint_note": "",
-      "truncated_namespace": "Truncated_namespace",
-      "truncated_namespace_note": "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc.\n        For example, when using CDN to S3 virtual host format, set \"\".\n        At this time, write CNAME to CDN in public_endpoint.",
       "filters": "Filters",
       "filters_note": "",
       "args": "List of actions for the mogrify command",
@@ -461,6 +453,23 @@
       "redirect_on_failure_note": "Redirects the client to the real remote URL if there's any HTTP errors. Any error during body processing will not be redirected as the response is chunked.",
       "text": "Anonymize filename",
       "text_note": "Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.\n"
+    },
+    "Pleroma.Uploaders.Local_form": {
+      "title": "Pleroma.Uploaders.Local",
+      "uploads": "Uploads",
+      "uploads_note": "Which directory to store the user-uploads in, relative to pleroma’s working directory"
+    },
+    "Pleroma.Uploaders.S3_form": {
+      "title": "Pleroma.Uploaders.S3",
+      "bucket": "S3 bucket name",
+      "bucket_note": "",
+      "public_endpoint": "S3 endpoint that the user finally accesses",
+      "public_endpoint_note": "",
+      "truncated_namespace": "Truncated_namespace",
+      "truncated_namespace_note": "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc.\n        For example, when using CDN to S3 virtual host format, set \"\".\n        At this time, write CNAME to CDN in public_endpoint."
+    },
+    "Pleroma.Uploaders.MDII_form": {
+
     },
     "emails": {
       "adapter_title": "{name} adapter config",
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index f21df3c..d2c52a5 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -9,10 +9,14 @@ import { forIn } from 'lodash'
 import KocaptchaConfig from '../entities/settings/KocaptchaConfig'
 import DatabaseConfig from '../entities/settings/DatabaseConfig'
 import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
-import LdapConfig from "../entities/settings/LdapConfig";
+import LdapConfig from '../entities/settings/LdapConfig'
+import UploadersS3Config from '../entities/settings/UploadersS3Config'
+import UploadersLocalConfig from '../entities/settings/UploadersLocalConfig'
 
 export default (configs) => ({
   [configKeys.UPLOAD]: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
+  [configKeys.UPLOADERSS3]: new UploadersS3Config(t(configs.find(({ key }) => key === configKeys.UPLOADERSS3), 'value').safeObject),
+  [configKeys.UPLOADERSLOCAL]: new UploadersLocalConfig(t(configs.find(({ key }) => key === configKeys.UPLOADERSLOCAL), 'value').safeObject),
   // emails: new EmailsConfig(),
   [configKeys.INSTANCE]: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
   [configKeys.CAPTCHA]: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
@@ -45,7 +49,7 @@ export const normalizeApiConfig = function(existConfig, classObject) {
             delete val[innerKey]
             val[innerKey.substring(1)] = value
           })
-          if (resultObject[key.substring(1)].sendAsMap) {
+          if (resultObject[key.substring(1)] && resultObject[key.substring(1)].sendAsMap) {
             val.sendAsMap = true
           }
           resultObject[key.substring(1)] = val
-- 
GitLab


From edfd2338ab1e3fff974f639875cecb4eef7e3ed6 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 22 Jul 2019 19:46:39 +0300
Subject: [PATCH 25/61] feat: :uri_schemes

---
 src/data/Config.ts                        |  5 +++++
 src/entities/settings/UriSchemesConfig.ts | 10 ++++++++++
 src/i18n/en.json                          |  4 ++++
 src/utils/ConvertConfigToState.ts         |  2 ++
 src/utils/GetFieldList.ts                 |  3 ++-
 5 files changed, 23 insertions(+), 1 deletion(-)
 create mode 100644 src/entities/settings/UriSchemesConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 0997fbd..ab1344d 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -3,6 +3,7 @@ export enum configKeys {
   UPLOADERSS3 = 'Pleroma.Uploaders.S3',
   UPLOADERSLOCAL = 'Pleroma.Uploaders.Local',
   EMAILS = 'Pleroma.Emails',
+  URI_SCHEMES = ':uri_schemes',
   INSTANCE = ':instance',
   LOGGER = ':logger',
   FRONTEND_CONFIGURATIONS = ':frontend_configurations',
@@ -23,6 +24,10 @@ export const configKeysTabs = [
   //   key: configKeys.EMAILS,
   //   name: 'Emails'
   // },
+  {
+    key: configKeys.URI_SCHEMES,
+    name: 'URI schemes'
+  },
   {
     key: configKeys.INSTANCE,
     name: 'Instance'
diff --git a/src/entities/settings/UriSchemesConfig.ts b/src/entities/settings/UriSchemesConfig.ts
new file mode 100644
index 0000000..c0af763
--- /dev/null
+++ b/src/entities/settings/UriSchemesConfig.ts
@@ -0,0 +1,10 @@
+import {normalizeApiConfig} from '../../utils/ConvertConfigToState'
+
+export default class UriSchemesConfig {
+  constructor (existConfig?) {
+    console.log(existConfig)
+    normalizeApiConfig(existConfig, this)
+  }
+  valid_schemes: Array<String> = []
+  sendAsMap = true
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 71b97fe..3be99bd 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -494,6 +494,10 @@
       "server_id": "server id",
       "endpoint": "endpoint"
     },
+    ":uri_schemes_form": {
+      "valid_schemes": "Valid schemes",
+      "valid_schemes_note": "List of the scheme part that is considered valid to be an URL"
+    },
     ":instance_form": {
       "name": "Name",
       "name_note": "The instance’s name",
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index d2c52a5..3f6aa41 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -12,12 +12,14 @@ import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
 import LdapConfig from '../entities/settings/LdapConfig'
 import UploadersS3Config from '../entities/settings/UploadersS3Config'
 import UploadersLocalConfig from '../entities/settings/UploadersLocalConfig'
+import UriSchemesConfig from "../entities/settings/UriSchemesConfig";
 
 export default (configs) => ({
   [configKeys.UPLOAD]: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
   [configKeys.UPLOADERSS3]: new UploadersS3Config(t(configs.find(({ key }) => key === configKeys.UPLOADERSS3), 'value').safeObject),
   [configKeys.UPLOADERSLOCAL]: new UploadersLocalConfig(t(configs.find(({ key }) => key === configKeys.UPLOADERSLOCAL), 'value').safeObject),
   // emails: new EmailsConfig(),
+  [configKeys.URI_SCHEMES]: new UriSchemesConfig(t(configs.find(({ key }) => key === configKeys.URI_SCHEMES), 'value').safeObject),
   [configKeys.INSTANCE]: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
   [configKeys.CAPTCHA]: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
   [configKeys.KOCAPTCHA]: new KocaptchaConfig(t(configs.find(({ key }) => key === configKeys.KOCAPTCHA), 'value').safeObject),
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 2395c62..0cc8d0c 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -21,7 +21,8 @@ export const selectOptions = {
   uploader: ['Pleroma.Uploaders.Local', 'Pleroma.Uploaders.S3'],
   filters: ['Pleroma.Upload.Filter.Mogrify', 'Pleroma.Upload.Filter.Dedupe', 'Pleroma.Upload.Filter.AnonymizeFilename'],
   args: ['strip', 'auto-orient', `{'impode': '1'}`],
-  inline_content_types: ['true', 'false', 'a list of whitelisted content types']
+  inline_content_types: ['true', 'false', 'a list of whitelisted content types'],
+  valid_schemes: ['https', 'http', 'dat', 'dweb', 'gopher', 'ipfs', 'ipns', 'irc', 'ircs', 'magnet', 'mailto', 'mumble', 'ssb', 'xmpp'],
 }
 
 export default (formData) => {
-- 
GitLab


From 1f600b12173eca5ab4c73d4564a3a7e8c5ef5aaf Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 23 Jul 2019 14:51:23 +0300
Subject: [PATCH 26/61] feat: :activitypub

---
 src/data/Config.ts                         | 15 +++--
 src/entities/settings/ActivityPubConfig.ts | 12 ++++
 src/i18n/en.json                           | 16 +++++
 src/utils/ConvertConfigToState.ts          | 78 +++++++++++++++++-----
 4 files changed, 101 insertions(+), 20 deletions(-)
 create mode 100644 src/entities/settings/ActivityPubConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index ab1344d..b7a7526 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -12,7 +12,8 @@ export enum configKeys {
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
   DATABASE = ':database',
   MEDIA_PROXY = ':media_proxy',
-  LDAP = ':ldap'
+  LDAP = ':ldap',
+  ACTIVITY_PUB = ':activitypub'
 }
 
 export const configKeysTabs = [
@@ -32,10 +33,10 @@ export const configKeysTabs = [
     key: configKeys.INSTANCE,
     name: 'Instance'
   },
-  // {
-  //   key: configKeys.LOGGER,
-  //   name: 'Logger'
-  // },
+  {
+    key: configKeys.LOGGER,
+    name: 'Logger'
+  },
   {
     key: configKeys.FRONTEND_CONFIGURATIONS,
     name: 'Frontend configurations'
@@ -59,5 +60,9 @@ export const configKeysTabs = [
   {
     key: configKeys.LDAP,
     name: 'LDAP'
+  },
+  {
+    key: configKeys.ACTIVITY_PUB,
+    name: 'Activity pub'
   }
 ]
diff --git a/src/entities/settings/ActivityPubConfig.ts b/src/entities/settings/ActivityPubConfig.ts
new file mode 100644
index 0000000..c75f8aa
--- /dev/null
+++ b/src/entities/settings/ActivityPubConfig.ts
@@ -0,0 +1,12 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
+export default class ActivityPubConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  accept_blocks: boolean = true
+  unfollow_blocked: boolean = true
+  outgoing_blocks: boolean = true
+  follow_handshake_timeout: number = 500
+  sign_object_fetches: boolean = true
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 3be99bd..c640750 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -701,6 +701,22 @@
       "base_note": "e.g. 'dc=example,dc=com'",
       "uid": "UID",
       "uid_note": "LDAP attribute name to authenticate the user, e.g. when 'cn', the filter will be 'cn=username,base'"
+    },
+    ":activitypub_form": {
+      "accept_blocks": "Accept blocks",
+      "accept_blocks_note":"Whether to accept incoming block activities from other instances",
+      "unfollow_blocked": "Unfollow blocked",
+      "unfollow_blocked_note": "Whether blocks result in people getting unfollowed",
+      "outgoing_blocks": "Outgoing blocks",
+      "outgoing_blocks_note": "Whether to federate blocks to other instances",
+      "sign_object_fetches": "Sign object fetches with HTTP signatures",
+      "sign_object_fetches_note": "",
+      "follow_handshake_timeout": "Follow handshake timeout",
+      "follow_handshake_timeout_note": ""
+    },
+    ":user_form": {
+      "deny_follow_blocked": "Deny follow blocked",
+      "deny_follow_blocked_note": "Whether to disallow following an account that has blocked the user in question"
     }
   }
 }
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 3f6aa41..615393b 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -12,22 +12,70 @@ import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
 import LdapConfig from '../entities/settings/LdapConfig'
 import UploadersS3Config from '../entities/settings/UploadersS3Config'
 import UploadersLocalConfig from '../entities/settings/UploadersLocalConfig'
-import UriSchemesConfig from "../entities/settings/UriSchemesConfig";
+import UriSchemesConfig from '../entities/settings/UriSchemesConfig'
+import LoggerConfig from '../entities/settings/LoggerConfig'
+import ActivityPubConfig from "../entities/settings/ActivityPubConfig";
 
-export default (configs) => ({
-  [configKeys.UPLOAD]: new UploadConfig(t(configs.find(({ key }) => key === configKeys.UPLOAD), 'value').safeObject),
-  [configKeys.UPLOADERSS3]: new UploadersS3Config(t(configs.find(({ key }) => key === configKeys.UPLOADERSS3), 'value').safeObject),
-  [configKeys.UPLOADERSLOCAL]: new UploadersLocalConfig(t(configs.find(({ key }) => key === configKeys.UPLOADERSLOCAL), 'value').safeObject),
-  // emails: new EmailsConfig(),
-  [configKeys.URI_SCHEMES]: new UriSchemesConfig(t(configs.find(({ key }) => key === configKeys.URI_SCHEMES), 'value').safeObject),
-  [configKeys.INSTANCE]: new InstanceConfig(t(configs.find(({ key }) => key === configKeys.INSTANCE), 'value').safeObject),
-  [configKeys.CAPTCHA]: new CaptchaConfig(t(configs.find(({ key }) => key === configKeys.CAPTCHA), 'value').safeObject),
-  [configKeys.KOCAPTCHA]: new KocaptchaConfig(t(configs.find(({ key }) => key === configKeys.KOCAPTCHA), 'value').safeObject),
-  [configKeys.FRONTEND_CONFIGURATIONS]: new FrontentConfigurationsConfig(t(configs.find(({ key }) => key === configKeys.FRONTEND_CONFIGURATIONS), 'value').safeObject),
-  [configKeys.DATABASE]: new DatabaseConfig(t(configs.find(({ key }) => key === configKeys.DATABASE), 'value').safeObject),
-  [configKeys.MEDIA_PROXY]: new MediaProxyConfig(t(configs.find(({ key }) => key === configKeys.MEDIA_PROXY), 'value').safeObject),
-  [configKeys.LDAP]: new LdapConfig(t(configs.find(({ key }) => key === configKeys.LDAP), 'value').safeObject),
-})
+export default (configs) => {
+  const configObj = {}
+  configs.forEach( item => {
+    switch (item.key) {
+      case configKeys.UPLOAD: {
+        configObj[item.key] = new UploadConfig(t(item, 'value').safeObject)
+        break;
+      }
+      case configKeys.UPLOADERSS3: {
+        configObj[item.key] = new UploadersS3Config(t(item, 'value').safeObject)
+        break;
+      }
+      case configKeys.UPLOADERSLOCAL:{
+        configObj[item.key] = new UploadersLocalConfig(t(item, 'value').safeObject)
+        break;
+      }
+        // emails: new EmailsConfig(),
+      case configKeys.URI_SCHEMES:{
+        configObj[item.key] = new UriSchemesConfig(t(item, 'value').safeObject)
+        break;
+      }
+      case configKeys.INSTANCE:{
+        configObj[item.key] = new InstanceConfig(t(item, 'value').safeObject)
+        break;
+      }
+      case configKeys.LOGGER:{
+        configObj[item.key] = new LoggerConfig(t(item, 'value').safeObject)
+        break
+      }
+      case configKeys.CAPTCHA: {
+        configObj[item.key] = new CaptchaConfig(t(item, 'value').safeObject)
+        break
+      }
+      case configKeys.KOCAPTCHA: {
+        configObj[item.key] = new KocaptchaConfig(t(item, 'value').safeObject)
+        break
+      }
+      case configKeys.FRONTEND_CONFIGURATIONS: {
+        configObj[item.key] = new FrontentConfigurationsConfig(t(item, 'value').safeObject)
+        break
+      }
+      case configKeys.DATABASE:{
+        configObj[item.key] = new DatabaseConfig(t(item, 'value').safeObject)
+        break
+      }
+      case configKeys.MEDIA_PROXY: {
+        configObj[item.key] = new MediaProxyConfig(t(item, 'value').safeObject)
+        break
+      }
+      case configKeys.LDAP: {
+        configObj[item.key] = new LdapConfig(t(item, 'value').safeObject)
+        break
+      }
+      case configKeys.ACTIVITY_PUB: {
+        configObj[item.key] = new ActivityPubConfig(t(item, 'value').safeObject)
+      }
+    }
+  })
+  return configObj
+}
 
 export const normalizeApiConfig = function(existConfig, classObject) {
   if (existConfig) {
-- 
GitLab


From 6f8bc628d8d8ee1e85793072032b08c4a039f2db Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 23 Jul 2019 16:06:12 +0300
Subject: [PATCH 27/61] feat: :user, :http_security

---
 .../configSettings/ConfigSettingsPage.vue     | 18 ++--
 src/data/Config.ts                            | 94 ++++++++++++++++---
 src/entities/settings/HttpSecurityConfig.ts   | 12 +++
 src/entities/settings/LoggerConfig.ts         |  6 ++
 src/entities/settings/UserConfig.ts           |  8 ++
 src/i18n/en.json                              | 12 +++
 src/utils/ConvertConfigToState.ts             | 74 +--------------
 src/utils/GetFieldList.ts                     |  1 +
 8 files changed, 133 insertions(+), 92 deletions(-)
 create mode 100644 src/entities/settings/HttpSecurityConfig.ts
 create mode 100644 src/entities/settings/UserConfig.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 48468de..844bf9a 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -2,7 +2,7 @@
   <va-card class="config-settings-page" title="Settings">
     <va-tabs v-model="value">
       <va-tab
-        v-for="item in configKeysTabs"
+        v-for="item in tabs"
         :key="item.key"
       >
         {{item.name}}
@@ -11,30 +11,30 @@
     <div class="config-settings-page__content pt-4">
       <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
       <div v-if="config">
-        <template v-for="tab in configKeysTabs">
+        <template v-for="tab in tabs">
           <config-form
             :key="tab.key"
             v-model="config[tab.key]"
             :title="tab.key"
-            :showTitle="configKeysTabs[value].key === configKeysEnum.LDAP"
-            v-if="configKeysTabs[value].key === tab.key"
+            :showTitle="tabs[value].key === configKeysEnum.LDAP"
+            v-if="tabs[value].key === tab.key"
           />
         </template>
         <config-form
           :title="configKeysEnum.UPLOADERSS3"
-          v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD"
+          v-if="tabs[value].key === configKeysEnum.UPLOAD"
           v-model="config[configKeysEnum.UPLOADERSS3]"
           showTitle
         />
         <config-form
           :title="configKeysEnum.UPLOADERSLOCAL"
-          v-if="configKeysTabs[value].key === configKeysEnum.UPLOAD"
+          v-if="tabs[value].key === configKeysEnum.UPLOAD"
           v-model="config[configKeysEnum.UPLOADERSLOCAL]"
           showTitle
         />
         <config-form
           :title="configKeysEnum.KOCAPTCHA"
-          v-if="configKeysTabs[value].key === configKeysEnum.CAPTCHA"
+          v-if="tabs[value].key === configKeysEnum.CAPTCHA"
           v-model="config[configKeysEnum.KOCAPTCHA]"
           showTitle
         />
@@ -73,7 +73,6 @@ import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 export default class ConfigSettingsPage extends Vue {
   value:number = 0
   config:any = null
-  configKeysTabs: Array<object> = configKeysTabs
   configKeysEnum = configKeys
   loading:boolean = false
   async mounted () {
@@ -92,6 +91,9 @@ export default class ConfigSettingsPage extends Vue {
     const config = ConvertConfigToState(configs)
     this.config = config
   }
+  get tabs () {
+    return configKeysTabs.filter(({ tab }) => tab)
+  }
 }
 </script>
 
diff --git a/src/data/Config.ts b/src/data/Config.ts
index b7a7526..306a064 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -1,3 +1,19 @@
+import UploadConfig from "../entities/settings/UploadConfig";
+import UploadersS3Config from "../entities/settings/UploadersS3Config";
+import UriSchemesConfig from "../entities/settings/UriSchemesConfig";
+import UploadersLocalConfig from "../entities/settings/UploadersLocalConfig";
+import InstanceConfig from "../entities/settings/InstanceConfig";
+import LoggerConfig from "../entities/settings/LoggerConfig";
+import FrontentConfigurationsConfig from "../entities/settings/FrontentConfigurationsConfig";
+import CaptchaConfig from "../entities/settings/CaptchaConfig";
+import KocaptchaConfig from "../entities/settings/KocaptchaConfig";
+import DatabaseConfig from "../entities/settings/DatabaseConfig";
+import MediaProxyConfig from "../entities/settings/MediaProxyConfig";
+import LdapConfig from "../entities/settings/LdapConfig";
+import ActivityPubConfig from "../entities/settings/ActivityPubConfig";
+import UserConfig from "../entities/settings/UserConfig";
+import HttpSecurityConfig from "../entities/settings/HttpSecurityConfig";
+
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
   UPLOADERSS3 = 'Pleroma.Uploaders.S3',
@@ -13,56 +29,104 @@ export enum configKeys {
   DATABASE = ':database',
   MEDIA_PROXY = ':media_proxy',
   LDAP = ':ldap',
-  ACTIVITY_PUB = ':activitypub'
+  ACTIVITY_PUB = ':activitypub',
+  USER = ':user',
+  HTTP_SECURITY = ':http_security'
 }
 
 export const configKeysTabs = [
   {
     key: configKeys.UPLOAD,
     name: 'Upload',
+    constructor: UploadConfig,
+    tab: true,
+  },
+  {
+    key: configKeys.UPLOADERSS3,
+    tab: false,
+    name: 'Uploader S3',
+    constructor: UploadersS3Config,
+  },
+  {
+    key: configKeys.UPLOADERSLOCAL,
+    tab: false,
+    name: 'Uploader Local',
+    constructor: UploadersLocalConfig,
   },
-  // {
-  //   key: configKeys.EMAILS,
-  //   name: 'Emails'
-  // },
   {
     key: configKeys.URI_SCHEMES,
-    name: 'URI schemes'
+    name: 'URI schemes',
+    constructor: UriSchemesConfig,
+    tab: true,
   },
   {
     key: configKeys.INSTANCE,
-    name: 'Instance'
+    name: 'Instance',
+    constructor: InstanceConfig
   },
   {
     key: configKeys.LOGGER,
-    name: 'Logger'
+    name: 'Logger',
+    tab: false,
+    constructor: LoggerConfig
   },
   {
     key: configKeys.FRONTEND_CONFIGURATIONS,
-    name: 'Frontend configurations'
+    name: 'Frontend configurations',
+    constructor: FrontentConfigurationsConfig,
+    tab: true,
   },
   // {
   //   key: configKeys.WEB,
-  //   name: 'Web'
+  //   name: 'Web',
+  //   tab: false,
   // },
   {
     key: configKeys.CAPTCHA,
-    name: 'Captcha'
+    name: 'Captcha',
+    constructor: CaptchaConfig,
+    tab: true,
+  },
+  {
+    key: configKeys.KOCAPTCHA,
+    name: 'Kocaptcha',
+    tab: false,
+    constructor: KocaptchaConfig
   },
   {
     key: configKeys.DATABASE,
-    name: 'Database options'
+    name: 'Database options',
+    constructor: DatabaseConfig,
+    tab: true,
   },
   {
     key: configKeys.MEDIA_PROXY,
-    name: 'Media proxy'
+    name: 'Media proxy',
+    constructor: MediaProxyConfig,
+    tab: true,
   },
   {
     key: configKeys.LDAP,
-    name: 'LDAP'
+    name: 'LDAP',
+    constructor: LdapConfig,
+    tab: true,
   },
   {
     key: configKeys.ACTIVITY_PUB,
-    name: 'Activity pub'
+    name: 'Activity pub',
+    constructor: ActivityPubConfig,
+    tab: true,
+  },
+  {
+    key: configKeys.USER,
+    name: 'User',
+    constructor: UserConfig,
+    tab: true,
+  },
+  {
+    key: configKeys.HTTP_SECURITY,
+    name: 'HTTP security',
+    constructor: HttpSecurityConfig,
+    tab: true,
   }
 ]
diff --git a/src/entities/settings/HttpSecurityConfig.ts b/src/entities/settings/HttpSecurityConfig.ts
new file mode 100644
index 0000000..ff04500
--- /dev/null
+++ b/src/entities/settings/HttpSecurityConfig.ts
@@ -0,0 +1,12 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class HttpSecurityConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  enabled: boolean = true
+  sts: boolean = false
+  sts_max_age: number = 31536000
+  ct_max_age: number = 2592000
+  referrer_policy: string = 'same-origin'
+}
diff --git a/src/entities/settings/LoggerConfig.ts b/src/entities/settings/LoggerConfig.ts
index e22ddc2..418e4f9 100644
--- a/src/entities/settings/LoggerConfig.ts
+++ b/src/entities/settings/LoggerConfig.ts
@@ -1,2 +1,8 @@
+import {normalizeApiConfig} from '../../utils/ConvertConfigToState'
+
 export default class LoggerConfig {
+  constructor(existConfig?: any) {
+    normalizeApiConfig(existConfig, this)
+  }
+
 }
diff --git a/src/entities/settings/UserConfig.ts b/src/entities/settings/UserConfig.ts
new file mode 100644
index 0000000..ff6b487
--- /dev/null
+++ b/src/entities/settings/UserConfig.ts
@@ -0,0 +1,8 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class UserConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  deny_follow_blocked: boolean = true
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c640750..e135216 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -717,6 +717,18 @@
     ":user_form": {
       "deny_follow_blocked": "Deny follow blocked",
       "deny_follow_blocked_note": "Whether to disallow following an account that has blocked the user in question"
+    },
+    ":http_security_form": {
+      "enabled": "Enabled",
+      "enabled_note": "Whether the managed content security policy is enabled",
+      "sts": "Send Strict-Transport-Security header",
+      "sts_note": "",
+      "sts_max_age": "The maximum age for the Strict-Transport-Security header",
+      "sts_max_age_note": "",
+      "ct_max_age": "The maximum age for the Expect-CT header",
+      "ct_max_age_note": "",
+      "referrer_policy": "Referrer policy",
+      "referrer_policy_note": ""
     }
   }
 }
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 615393b..656e069 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -1,77 +1,13 @@
-import UploadConfig from '../entities/settings/UploadConfig'
-import EmailsConfig from '../entities/settings/EmailsConfig'
-import InstanceConfig from '../entities/settings/InstanceConfig'
-import CaptchaConfig from '../entities/settings/CaptchaConfig'
-import { configKeys } from '../data/Config'
-import FrontentConfigurationsConfig from '../entities/settings/FrontentConfigurationsConfig'
 import t from 'typy'
 import { forIn } from 'lodash'
-import KocaptchaConfig from '../entities/settings/KocaptchaConfig'
-import DatabaseConfig from '../entities/settings/DatabaseConfig'
-import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
-import LdapConfig from '../entities/settings/LdapConfig'
-import UploadersS3Config from '../entities/settings/UploadersS3Config'
-import UploadersLocalConfig from '../entities/settings/UploadersLocalConfig'
-import UriSchemesConfig from '../entities/settings/UriSchemesConfig'
-import LoggerConfig from '../entities/settings/LoggerConfig'
-import ActivityPubConfig from "../entities/settings/ActivityPubConfig";
+import { configKeysTabs } from '../data/Config'
 
 export default (configs) => {
   const configObj = {}
-  configs.forEach( item => {
-    switch (item.key) {
-      case configKeys.UPLOAD: {
-        configObj[item.key] = new UploadConfig(t(item, 'value').safeObject)
-        break;
-      }
-      case configKeys.UPLOADERSS3: {
-        configObj[item.key] = new UploadersS3Config(t(item, 'value').safeObject)
-        break;
-      }
-      case configKeys.UPLOADERSLOCAL:{
-        configObj[item.key] = new UploadersLocalConfig(t(item, 'value').safeObject)
-        break;
-      }
-        // emails: new EmailsConfig(),
-      case configKeys.URI_SCHEMES:{
-        configObj[item.key] = new UriSchemesConfig(t(item, 'value').safeObject)
-        break;
-      }
-      case configKeys.INSTANCE:{
-        configObj[item.key] = new InstanceConfig(t(item, 'value').safeObject)
-        break;
-      }
-      case configKeys.LOGGER:{
-        configObj[item.key] = new LoggerConfig(t(item, 'value').safeObject)
-        break
-      }
-      case configKeys.CAPTCHA: {
-        configObj[item.key] = new CaptchaConfig(t(item, 'value').safeObject)
-        break
-      }
-      case configKeys.KOCAPTCHA: {
-        configObj[item.key] = new KocaptchaConfig(t(item, 'value').safeObject)
-        break
-      }
-      case configKeys.FRONTEND_CONFIGURATIONS: {
-        configObj[item.key] = new FrontentConfigurationsConfig(t(item, 'value').safeObject)
-        break
-      }
-      case configKeys.DATABASE:{
-        configObj[item.key] = new DatabaseConfig(t(item, 'value').safeObject)
-        break
-      }
-      case configKeys.MEDIA_PROXY: {
-        configObj[item.key] = new MediaProxyConfig(t(item, 'value').safeObject)
-        break
-      }
-      case configKeys.LDAP: {
-        configObj[item.key] = new LdapConfig(t(item, 'value').safeObject)
-        break
-      }
-      case configKeys.ACTIVITY_PUB: {
-        configObj[item.key] = new ActivityPubConfig(t(item, 'value').safeObject)
-      }
+  configs.forEach(item => {
+    const constructorObj = configKeysTabs.find(tab => tab.key === item.key)
+    if (constructorObj) {
+      configObj[item.key] = new constructorObj.constructor(t(item, 'value').safeObject)
     }
   })
   return configObj
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 0cc8d0c..7e9224d 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -23,6 +23,7 @@ export const selectOptions = {
   args: ['strip', 'auto-orient', `{'impode': '1'}`],
   inline_content_types: ['true', 'false', 'a list of whitelisted content types'],
   valid_schemes: ['https', 'http', 'dat', 'dweb', 'gopher', 'ipfs', 'ipns', 'irc', 'ircs', 'magnet', 'mailto', 'mumble', 'ssb', 'xmpp'],
+  referrer_policy: ['same-origin', 'no-referrer']
 }
 
 export default (formData) => {
-- 
GitLab


From 57cfa3adbf777e3d43a4e549faf0d486214ee532 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 24 Jul 2019 16:05:44 +0300
Subject: [PATCH 28/61] feat: :rich_media, add functions to process all array
 params to string

---
 src/data/Config.ts                            | 48 ++++++++++++-------
 src/entities/settings/InstanceConfig.ts       | 18 +++----
 src/entities/settings/LdapConfig.ts           | 13 +++--
 src/entities/settings/MediaProxyConfig.ts     |  9 ++--
 src/entities/settings/RichMediaConfig.ts      | 12 +++++
 src/entities/settings/UploadersLocalConfig.ts |  2 +-
 src/entities/settings/UriSchemesConfig.ts     |  1 -
 src/i18n/en.json                              | 10 ++++
 src/utils/ConvertConfigToApiRequest.js        | 31 +++---------
 src/utils/ConvertConfigToState.ts             | 16 ++++++-
 src/utils/GetFieldList.ts                     |  3 +-
 11 files changed, 95 insertions(+), 68 deletions(-)
 create mode 100644 src/entities/settings/RichMediaConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 306a064..0b0a41d 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -1,18 +1,19 @@
-import UploadConfig from "../entities/settings/UploadConfig";
-import UploadersS3Config from "../entities/settings/UploadersS3Config";
-import UriSchemesConfig from "../entities/settings/UriSchemesConfig";
-import UploadersLocalConfig from "../entities/settings/UploadersLocalConfig";
-import InstanceConfig from "../entities/settings/InstanceConfig";
-import LoggerConfig from "../entities/settings/LoggerConfig";
-import FrontentConfigurationsConfig from "../entities/settings/FrontentConfigurationsConfig";
-import CaptchaConfig from "../entities/settings/CaptchaConfig";
-import KocaptchaConfig from "../entities/settings/KocaptchaConfig";
-import DatabaseConfig from "../entities/settings/DatabaseConfig";
-import MediaProxyConfig from "../entities/settings/MediaProxyConfig";
-import LdapConfig from "../entities/settings/LdapConfig";
-import ActivityPubConfig from "../entities/settings/ActivityPubConfig";
-import UserConfig from "../entities/settings/UserConfig";
-import HttpSecurityConfig from "../entities/settings/HttpSecurityConfig";
+import UploadConfig from '../entities/settings/UploadConfig'
+import UploadersS3Config from '../entities/settings/UploadersS3Config'
+import UriSchemesConfig from '../entities/settings/UriSchemesConfig'
+import UploadersLocalConfig from '../entities/settings/UploadersLocalConfig'
+import InstanceConfig from '../entities/settings/InstanceConfig'
+import LoggerConfig from '../entities/settings/LoggerConfig'
+import FrontentConfigurationsConfig from '../entities/settings/FrontentConfigurationsConfig'
+import CaptchaConfig from '../entities/settings/CaptchaConfig'
+import KocaptchaConfig from '../entities/settings/KocaptchaConfig'
+import DatabaseConfig from '../entities/settings/DatabaseConfig'
+import MediaProxyConfig from '../entities/settings/MediaProxyConfig'
+import LdapConfig from '../entities/settings/LdapConfig'
+import ActivityPubConfig from '../entities/settings/ActivityPubConfig'
+import UserConfig from '../entities/settings/UserConfig'
+import HttpSecurityConfig from '../entities/settings/HttpSecurityConfig'
+import RichMediaConfig from '../entities/settings/RichMediaConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -26,6 +27,7 @@ export enum configKeys {
   WEB = 'Pleroma.Web',
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
+  RICH_MEDIA = ':rich_media',
   DATABASE = ':database',
   MEDIA_PROXY = ':media_proxy',
   LDAP = ':ldap',
@@ -34,6 +36,13 @@ export enum configKeys {
   HTTP_SECURITY = ':http_security'
 }
 
+export const arrayParams = {
+  [configKeys.INSTANCE]: ['quarantined_instances', 'mrf_transparency_exclusions', 'autofollowed_nicknames'],
+  [configKeys.LDAP]: ['sslopts', 'tlsopts'],
+  [configKeys.MEDIA_PROXY]: ['whitelist'],
+  [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld']
+}
+
 export const configKeysTabs = [
   {
     key: configKeys.UPLOAD,
@@ -62,7 +71,8 @@ export const configKeysTabs = [
   {
     key: configKeys.INSTANCE,
     name: 'Instance',
-    constructor: InstanceConfig
+    constructor: InstanceConfig,
+    tab: true,
   },
   {
     key: configKeys.LOGGER,
@@ -128,5 +138,11 @@ export const configKeysTabs = [
     name: 'HTTP security',
     constructor: HttpSecurityConfig,
     tab: true,
+  },
+  {
+    key: configKeys.RICH_MEDIA,
+    name: 'Rich media',
+    constructor: RichMediaConfig,
+    tab: true
   }
 ]
diff --git a/src/entities/settings/InstanceConfig.ts b/src/entities/settings/InstanceConfig.ts
index 83808f9..81606f7 100644
--- a/src/entities/settings/InstanceConfig.ts
+++ b/src/entities/settings/InstanceConfig.ts
@@ -1,17 +1,13 @@
-import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import { configKeys, arrayParams } from '../../data/Config'
 
 export default class InstanceConfig {
   constructor(existConfig?) {
-    normalizeApiConfig(existConfig, this)
-    this.quarantined_instances = (this.quarantined_instances && Array.isArray(this.quarantined_instances))
-      ? this.quarantined_instances.join(';')
-      : this.quarantined_instances
-    this.mrf_transparency_exclusions = (this.mrf_transparency_exclusions && Array.isArray(this.mrf_transparency_exclusions))
-      ? this.mrf_transparency_exclusions.join(';')
-      : this.mrf_transparency_exclusions
-    this.autofollowed_nicknames = (this.autofollowed_nicknames && Array.isArray(this.autofollowed_nicknames))
-      ? this.autofollowed_nicknames.join(';')
-      : this.autofollowed_nicknames
+    normalizeApiConfig(
+      existConfig,
+      this,
+      arrayParams[configKeys.INSTANCE]
+    )
   }
   name: string = ''
   email: string = ''
diff --git a/src/entities/settings/LdapConfig.ts b/src/entities/settings/LdapConfig.ts
index 1b12771..4680c20 100644
--- a/src/entities/settings/LdapConfig.ts
+++ b/src/entities/settings/LdapConfig.ts
@@ -1,14 +1,13 @@
 import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import { configKeys, arrayParams } from '../../data/Config'
 
 export default class LdapConfig {
   constructor (existConfig?) {
-    normalizeApiConfig(existConfig, this)
-    this.sslopts = (this.sslopts && Array.isArray(this.sslopts))
-      ? this.sslopts.join(';')
-      : this.sslopts
-    this.tlsopts = (this.tlsopts && Array.isArray(this.tlsopts))
-      ? this.tlsopts.join(';')
-      : this.tlsopts
+    normalizeApiConfig(
+      existConfig,
+      this,
+      arrayParams[configKeys.LDAP]
+    )
   }
   enabled: boolean = false
   host: string = 'localhost'
diff --git a/src/entities/settings/MediaProxyConfig.ts b/src/entities/settings/MediaProxyConfig.ts
index d3f291c..f528c2f 100644
--- a/src/entities/settings/MediaProxyConfig.ts
+++ b/src/entities/settings/MediaProxyConfig.ts
@@ -1,12 +1,11 @@
 import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
-import {ReverseProxy} from "./UploadConfig";
+import { ReverseProxy } from './UploadConfig'
+import { configKeys, arrayParams } from '../../data/Config'
+
 
 export default class MediaProxyConfig {
   constructor(existConfig?) {
-    normalizeApiConfig(existConfig, this)
-    this.whitelist = (this.whitelist && Array.isArray(this.whitelist))
-      ? this.whitelist.join(';')
-      : this.whitelist
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.MEDIA_PROXY])
   }
   proxy_opts?: ReverseProxy = new ReverseProxy()
   base_url: string = ''
diff --git a/src/entities/settings/RichMediaConfig.ts b/src/entities/settings/RichMediaConfig.ts
new file mode 100644
index 0000000..21ec0cc
--- /dev/null
+++ b/src/entities/settings/RichMediaConfig.ts
@@ -0,0 +1,12 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import { configKeys, arrayParams } from '../../data/Config'
+
+export default class RichMediaConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.RICH_MEDIA])
+  }
+  enabled: boolean = true
+  ignore_hosts: any = ''
+  ignore_tld: any = ''
+  parsers: Array<string> = ['Pleroma.Web.RichMedia.Parsers.TwitterCard', 'Pleroma.Web.RichMedia.Parsers.OGP', 'Pleroma.Web.RichMedia.Parsers.OEmbed']
+}
diff --git a/src/entities/settings/UploadersLocalConfig.ts b/src/entities/settings/UploadersLocalConfig.ts
index 890e6fe..9da95bd 100644
--- a/src/entities/settings/UploadersLocalConfig.ts
+++ b/src/entities/settings/UploadersLocalConfig.ts
@@ -1,4 +1,4 @@
-import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
 
 export default class UploadersLocalConfig {
   constructor (existConfig) {
diff --git a/src/entities/settings/UriSchemesConfig.ts b/src/entities/settings/UriSchemesConfig.ts
index c0af763..abe5afe 100644
--- a/src/entities/settings/UriSchemesConfig.ts
+++ b/src/entities/settings/UriSchemesConfig.ts
@@ -2,7 +2,6 @@ import {normalizeApiConfig} from '../../utils/ConvertConfigToState'
 
 export default class UriSchemesConfig {
   constructor (existConfig?) {
-    console.log(existConfig)
     normalizeApiConfig(existConfig, this)
   }
   valid_schemes: Array<String> = []
diff --git a/src/i18n/en.json b/src/i18n/en.json
index e135216..b712599 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -729,6 +729,16 @@
       "ct_max_age_note": "",
       "referrer_policy": "Referrer policy",
       "referrer_policy_note": ""
+    },
+    ":rich_media_form": {
+      "enabled": "Enabled",
+      "enabled_note": "If enabled the instance will parse metadata from attached links to generate link previews",
+      "ignore_hosts": "Ignored hosts",
+      "ignore_hosts_note": "List of hosts which will be ignored by the metadata parser. Separate items by ;",
+      "ignore_tld": "Ignored top-level domains",
+      "ignore_tld_note": "List TLDs (top-level domains) which will ignore for parse metadata. Separate items by ;",
+      "parsers": "Parsers",
+      "parsers_note": ""
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 3a8d033..88b489a 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,18 +1,12 @@
 import { forIn } from 'lodash'
-import { configKeys } from '../data/Config'
+import { arrayParams } from '../data/Config'
 
 export default (configs) => {
   const settings = []
   forIn(configs, (val, key) => {
     let newVal = { ...val }
-    if (key === configKeys.INSTANCE) {
-      newVal = normalizeInstanceConfigValue(newVal)
-    }
-    if (key === configKeys.MEDIA_PROXY) {
-      newVal = normalizeMediaProxyConfigValue(newVal)
-    }
-    if (key === configKeys.LDAP) {
-      newVal = normalizeLdapConfigValue(newVal)
+    if (arrayParams[key]) {
+      newVal = normalizeConfigValue(newVal, key)
     }
     settings.push({
       group: 'pleroma',
@@ -23,21 +17,10 @@ export default (configs) => {
   return { configs: settings }
 }
 
-const normalizeInstanceConfigValue = (config) => {
-  config.quarantined_instances = config.quarantined_instances ? config.quarantined_instances.split(';') : []
-  config.mrf_transparency_exclusions = config.mrf_transparency_exclusions ? config.mrf_transparency_exclusions.split(';') : []
-  config.autofollowed_nicknames = config.autofollowed_nicknames ? config.autofollowed_nicknames.split(';') : []
-  return config
-}
-
-const normalizeMediaProxyConfigValue = (config) => {
-  config.whitelist = config.whitelist ? config.whitelist.split(';') : []
-  return config
-}
-
-const normalizeLdapConfigValue = (config) => {
-  config.sslopts = config.sslopts ? config.sslopts.split(';') : []
-  config.tlsopts = config.tlsopts ? config.tlsopts.split(';') : []
+const normalizeConfigValue = (config, key) => {
+  arrayParams[key].forEach(param => {
+    config[param] = config[param] ? config[param].split(';') : []
+  })
   return config
 }
 
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index 656e069..c8e0b4e 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -13,9 +13,13 @@ export default (configs) => {
   return configObj
 }
 
-export const normalizeApiConfig = function(existConfig, classObject) {
+export const normalizeApiConfig = function(existConfig, classObject, arrayParams?) {
   if (existConfig) {
-    parseObj(existConfig, classObject)
+    const newConfig = parseObj(existConfig, classObject)
+    if (arrayParams && arrayParams.length) {
+      convertArrayParamsToState(newConfig, arrayParams)
+    }
+    return newConfig
   }
   function parseObj(config, resultObject) {
     config.forEach(({tuple}) => {
@@ -46,4 +50,12 @@ export const normalizeApiConfig = function(existConfig, classObject) {
     })
     return resultObject
   }
+  function convertArrayParamsToState (config, arrayParams) {
+    arrayParams.forEach(param => {
+      config[param] = (config[param] && Array.isArray(config[param]))
+        ? config[param].join(';')
+        : config[param]
+    })
+    return config
+  }
 }
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 7e9224d..73f96f4 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -23,7 +23,8 @@ export const selectOptions = {
   args: ['strip', 'auto-orient', `{'impode': '1'}`],
   inline_content_types: ['true', 'false', 'a list of whitelisted content types'],
   valid_schemes: ['https', 'http', 'dat', 'dweb', 'gopher', 'ipfs', 'ipns', 'irc', 'ircs', 'magnet', 'mailto', 'mumble', 'ssb', 'xmpp'],
-  referrer_policy: ['same-origin', 'no-referrer']
+  referrer_policy: ['same-origin', 'no-referrer'],
+  parsers: ['Pleroma.Web.RichMedia.Parsers.TwitterCard', 'Pleroma.Web.RichMedia.Parsers.OGP', 'Pleroma.Web.RichMedia.Parsers.OEmbed']
 }
 
 export default (formData) => {
-- 
GitLab


From c79ca9a79bb4926c78add52f6cc8a6119e0f28ac Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 24 Jul 2019 16:14:43 +0300
Subject: [PATCH 29/61] feat: :fetch_initial_posts

---
 src/data/Config.ts                               | 8 ++++++++
 src/entities/settings/FetchInitialPostsConfig.ts | 9 +++++++++
 src/i18n/en.json                                 | 6 ++++++
 3 files changed, 23 insertions(+)
 create mode 100644 src/entities/settings/FetchInitialPostsConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 0b0a41d..17b9908 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -14,6 +14,7 @@ import ActivityPubConfig from '../entities/settings/ActivityPubConfig'
 import UserConfig from '../entities/settings/UserConfig'
 import HttpSecurityConfig from '../entities/settings/HttpSecurityConfig'
 import RichMediaConfig from '../entities/settings/RichMediaConfig'
+import FetchInitialPostsConfig from '../entities/settings/FetchInitialPostsConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -28,6 +29,7 @@ export enum configKeys {
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
   RICH_MEDIA = ':rich_media',
+  FETCH_INITIAL_POSTS = ':fetch_initial_posts',
   DATABASE = ':database',
   MEDIA_PROXY = ':media_proxy',
   LDAP = ':ldap',
@@ -144,5 +146,11 @@ export const configKeysTabs = [
     name: 'Rich media',
     constructor: RichMediaConfig,
     tab: true
+  },
+  {
+    key: configKeys.FETCH_INITIAL_POSTS,
+    name: 'Fetch initial posts',
+    tab: true,
+    constructor: FetchInitialPostsConfig
   }
 ]
diff --git a/src/entities/settings/FetchInitialPostsConfig.ts b/src/entities/settings/FetchInitialPostsConfig.ts
new file mode 100644
index 0000000..88939ee
--- /dev/null
+++ b/src/entities/settings/FetchInitialPostsConfig.ts
@@ -0,0 +1,9 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class FetchInitialPostsConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  enabled: boolean = false
+  pages: number = 5
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index b712599..af95731 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -739,6 +739,12 @@
       "ignore_tld_note": "List TLDs (top-level domains) which will ignore for parse metadata. Separate items by ;",
       "parsers": "Parsers",
       "parsers_note": ""
+    },
+    ":fetch_initial_posts_form": {
+      "enabled": "Enabled",
+      "enabled_note": "If enabled, when a new user is federated with, fetch some of their latest posts",
+      "pages": "Pages",
+      "pages_note": "The amount of pages to fetch"
     }
   }
 }
-- 
GitLab


From c05168ccc9cb671972213c9626e6e472b5584c30 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 26 Jul 2019 17:33:06 +0300
Subject: [PATCH 30/61] feat: :hackney_pools, :auto_linker

---
 .../configSettings/forms/AutoLinkerForm.vue   | 54 +++++++++++++++++++
 .../configSettings/ConfigSettingsPage.vue     |  9 +++-
 src/data/Config.ts                            | 18 ++++++-
 src/entities/settings/AutoLinkerConfig.ts     | 49 +++++++++++++++++
 src/entities/settings/HackneyPoolsConfig.ts   | 22 ++++++++
 src/i18n/en.json                              | 38 +++++++++++++
 src/utils/ConvertConfigToApiRequest.js        |  6 ++-
 7 files changed, 193 insertions(+), 3 deletions(-)
 create mode 100644 src/components/configSettings/forms/AutoLinkerForm.vue
 create mode 100644 src/entities/settings/AutoLinkerConfig.ts
 create mode 100644 src/entities/settings/HackneyPoolsConfig.ts

diff --git a/src/components/configSettings/forms/AutoLinkerForm.vue b/src/components/configSettings/forms/AutoLinkerForm.vue
new file mode 100644
index 0000000..6072577
--- /dev/null
+++ b/src/components/configSettings/forms/AutoLinkerForm.vue
@@ -0,0 +1,54 @@
+<template>
+  <div class='autolinker-form config-form' :style="{margin: margin}">
+    <p class="title" v-if="showTitle">{{$t(`config_settings.${title}_form.title`)}}</p>
+    <va-checkbox v-model="formData.opts.scheme" :label="$t('config_settings.opts_form.scheme')"/>
+    <p class="note">{{$t('config_settings.opts_form.scheme_note')}}</p>
+    <va-checkbox v-model="formData.opts.extra" :label="$t('config_settings.opts_form.extra')"/>
+    <p class="note">{{$t('config_settings.opts_form.extra_note')}}</p>
+    <va-checkbox v-model="formData.opts.validate_tld" :label="$t('config_settings.opts_form.validate_tld')"/>
+    <p class="note">{{$t('config_settings.opts_form.validate_tld_note')}}</p>
+    <va-input-wrapper :class="{'mb-0': !formData.opts.class}">
+      <va-checkbox v-model="formData.opts.class" :label="$t('config_settings.opts_form.class')"/>
+    </va-input-wrapper>
+    <va-input v-model="formData.class_text" v-if="formData.opts.class" :label="$t('config_settings.opts_form.class')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.opts_form.class_note')}}</p>
+    <va-input-wrapper :class="{'mb-0': !formData.opts.strip_prefix}">
+      <va-checkbox v-model="formData.opts.strip_prefix" :label="$t('config_settings.opts_form.strip_prefix')"/>
+    </va-input-wrapper>
+    <va-input v-model="formData.strip_prefix_text" v-if="formData.opts.strip_prefix" :label="$t('config_settings.opts_form.strip_prefix')" class="mb-0"/>
+    <p class="note">{{$t('config_settings.opts_form.strip_prefix_note')}}</p>
+
+    <va-checkbox v-model="formData.opts.new_window" :label="$t('config_settings.opts_form.new_window')"/>
+    <p class="note">{{$t('config_settings.opts_form.new_window_note')}}</p>
+    <va-checkbox v-model="formData.opts.rel" :label="$t('config_settings.opts_form.rel')"/>
+    <p class="note">{{$t('config_settings.opts_form.rel_note')}}</p>
+
+  </div>
+</template>
+<!--rel: false-->
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import UploadConfig from '../../../entities/settings/UploadConfig'
+import { keys } from 'lodash'
+
+@Component({
+  components: {},
+})
+export default class ConfigForm extends Vue {
+  @Prop(Object) readonly value!: UploadConfig
+  @Prop(String) readonly title!: string
+  @Prop(Boolean) readonly showTitle!: boolean
+  @Prop(String) readonly margin!: string
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+</style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 844bf9a..42415e0 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -17,7 +17,7 @@
             v-model="config[tab.key]"
             :title="tab.key"
             :showTitle="tabs[value].key === configKeysEnum.LDAP"
-            v-if="tabs[value].key === tab.key"
+            v-if="tabs[value].key === tab.key && tab.key !== configKeysEnum.AUTO_LINKER"
           />
         </template>
         <config-form
@@ -38,6 +38,11 @@
           v-model="config[configKeysEnum.KOCAPTCHA]"
           showTitle
         />
+        <auto-linker-form
+          :title="configKeysEnum.AUTO_LINKER"
+          v-if="tabs[value].key === configKeysEnum.AUTO_LINKER"
+          v-model="config[configKeysEnum.AUTO_LINKER]"
+        />
       </div>
       <div class="loading flex-center" v-if="loading">
         <fulfilling-bouncing-circle-spinner
@@ -62,9 +67,11 @@ import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
 import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
 import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
+import AutoLinkerForm from '../../configSettings/forms/AutoLinkerForm.vue'
 
 @Component({
   components: {
+    AutoLinkerForm,
     ConfigForm,
     EmailsForm,
     FulfillingBouncingCircleSpinner
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 17b9908..06a2f1a 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -15,6 +15,8 @@ import UserConfig from '../entities/settings/UserConfig'
 import HttpSecurityConfig from '../entities/settings/HttpSecurityConfig'
 import RichMediaConfig from '../entities/settings/RichMediaConfig'
 import FetchInitialPostsConfig from '../entities/settings/FetchInitialPostsConfig'
+import HackneyPoolsConfig from '../entities/settings/HackneyPoolsConfig'
+import AutoLinkerConfig from "../entities/settings/AutoLinkerConfig";
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -30,12 +32,14 @@ export enum configKeys {
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
   RICH_MEDIA = ':rich_media',
   FETCH_INITIAL_POSTS = ':fetch_initial_posts',
+  HACKNEY_POOLS = ':hackney_pools',
   DATABASE = ':database',
   MEDIA_PROXY = ':media_proxy',
   LDAP = ':ldap',
   ACTIVITY_PUB = ':activitypub',
   USER = ':user',
-  HTTP_SECURITY = ':http_security'
+  HTTP_SECURITY = ':http_security',
+  AUTO_LINKER = ':auto_linker',
 }
 
 export const arrayParams = {
@@ -152,5 +156,17 @@ export const configKeysTabs = [
     name: 'Fetch initial posts',
     tab: true,
     constructor: FetchInitialPostsConfig
+  },
+  {
+    key: configKeys.HACKNEY_POOLS,
+    name: 'Hackney pools',
+    tab: true,
+    constructor: HackneyPoolsConfig
+  },
+  {
+    key: configKeys.AUTO_LINKER,
+    name: 'Auto linker',
+    tab: true,
+    constructor: AutoLinkerConfig
   }
 ]
diff --git a/src/entities/settings/AutoLinkerConfig.ts b/src/entities/settings/AutoLinkerConfig.ts
new file mode 100644
index 0000000..614a1f6
--- /dev/null
+++ b/src/entities/settings/AutoLinkerConfig.ts
@@ -0,0 +1,49 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import t from 'typy'
+
+export default class AutoLinkerConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+    if (t(this.opts, 'class').safeObject) {
+      this.class_text = t(this.opts, 'class').safeObject
+      this.opts['class'] = true
+    }
+    if (t(this.opts, 'strip_prefix').safeObject) {
+      this.strip_prefix_text = t(this.opts, 'strip_prefix').safeObject
+      this.opts['strip_prefix'] = true
+    }
+  }
+
+  opts: object = {
+    scheme: true,
+    extra: true,
+    validate_tld: true,
+    class: false,
+    strip_prefix: false,
+    new_window: false,
+    rel: false,
+    sendAsMap: true
+  }
+  class_text: string = ''
+  strip_prefix_text: string = ''
+}
+
+export const normalizeAutoLinkerConfig = (config) => {
+  const apiConf = {...config}
+
+  if (apiConf.opts.class && apiConf.class_text.length) {
+    apiConf.opts.class = apiConf.class_text
+  } else {
+    apiConf.opts.class = false
+  }
+  delete apiConf.class_text
+
+  if (apiConf.opts.strip_prefix && apiConf.strip_prefix_text.length) {
+    apiConf.opts.strip_prefix = apiConf.strip_prefix_text
+  } else {
+    apiConf.opts.strip_prefix = false
+  }
+  delete apiConf.strip_prefix_text
+
+  return apiConf
+}
diff --git a/src/entities/settings/HackneyPoolsConfig.ts b/src/entities/settings/HackneyPoolsConfig.ts
new file mode 100644
index 0000000..661a27c
--- /dev/null
+++ b/src/entities/settings/HackneyPoolsConfig.ts
@@ -0,0 +1,22 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class HackneyPoolsConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  federation:object = {
+    max_connections: 50,
+    timeout: 150000,
+    sendAsMap: true,
+  }
+  media:object = {
+    max_connections: 50,
+    timeout: 150000,
+    sendAsMap: true,
+  }
+  upload:object = {
+    max_connections: 25,
+    timeout: 300000,
+    sendAsMap: true
+  }
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index af95731..febbc74 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -745,6 +745,44 @@
       "enabled_note": "If enabled, when a new user is federated with, fetch some of their latest posts",
       "pages": "Pages",
       "pages_note": "The amount of pages to fetch"
+    },
+    "federation_form": {
+      "title": "For the federation jobs.\nYou may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.",
+      "max_connections": "Maximum of connections",
+      "max_connections_note": "How much connections a pool can hold",
+      "timeout": "Timeout",
+      "timeout_note": "Retention duration for connections"
+    },
+    "media_form": {
+      "title": "For rich media, media proxy",
+      "max_connections": "Maximum of connections",
+      "max_connections_note": "How much connections a pool can hold",
+      "timeout": "Timeout",
+      "timeout_note": "Retention duration for connections"
+    },
+    "upload_form": {
+      "title": "For uploaded media (if using a remote uploader and proxy_remote: true)",
+      "max_connections": "Maximum of connections",
+      "max_connections_note": "How much connections a pool can hold",
+      "timeout": "Timeout",
+      "timeout_note": "Retention duration for connections"
+    },
+    "opts_form": {
+      "scheme": "Scheme",
+      "scheme_note": "Set to true to link urls with schema http://google.com",
+      "class": "Class",
+      "class_note": "Will be added to the generated link. Set false to clear",
+      "rel": "Rel",
+      "rel_note": "Override the rel attribute.Set false to clear,",
+      "new_window": "New window",
+      "new_window_note": "Set to false to remove target='_blank' attribute",
+      "strip_prefix": "Strip the scheme prefix",
+      "strip_prefix_note": "",
+      "extra": "Extra",
+      "extra_note": "false - link urls with rarely used schemes (magnet, ipfs, irc, etc.)",
+      "validate_tld": "Validate TLD",
+      "validate_tld_note": ""
+
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 88b489a..b874852 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,5 +1,6 @@
 import { forIn } from 'lodash'
-import { arrayParams } from '../data/Config'
+import { arrayParams, configKeys } from '../data/Config'
+import { normalizeAutoLinkerConfig } from '../entities/settings/AutoLinkerConfig'
 
 export default (configs) => {
   const settings = []
@@ -8,6 +9,9 @@ export default (configs) => {
     if (arrayParams[key]) {
       newVal = normalizeConfigValue(newVal, key)
     }
+    if (key === configKeys.AUTO_LINKER) {
+      newVal = normalizeAutoLinkerConfig(newVal)
+    }
     settings.push({
       group: 'pleroma',
       key,
-- 
GitLab


From fde0e84c7ee2a159958eeee12d59cec508c21e1a Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 26 Jul 2019 17:35:24 +0300
Subject: [PATCH 31/61] feat: add setConfigToDB request, when config in DB is
 empty

---
 .../pages/configSettings/ConfigSettingsPage.vue           | 8 +++++++-
 src/services/ConfigService.ts                             | 4 ++++
 src/services/urlBuilder.ts                                | 2 ++
 src/utils/ConvertConfigToState.ts                         | 8 +++-----
 4 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 42415e0..56c15bf 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -85,7 +85,13 @@ export default class ConfigSettingsPage extends Vue {
   async mounted () {
     this.loading = true
     const { configs } = await ConfigService.listConfigSettings()
-    this.loadConfigs(configs)
+    if (!configs.length) {
+      await ConfigService.setConfigToDB()
+      const { configs } = await ConfigService.listConfigSettings()
+      this.loadConfigs(configs)
+    } else {
+      this.loadConfigs(configs)
+    }
     this.loading = false
   }
   async onSaveButtunClick () {
diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts
index 5160e13..9a42ecd 100644
--- a/src/services/ConfigService.ts
+++ b/src/services/ConfigService.ts
@@ -11,4 +11,8 @@ export class ConfigService {
   static updateConfigSettings (configs) {
     return executeApiRequest('post', urlBuilder(Url.configSettings, {}), {data: configs})
   }
+
+  static setConfigToDB () {
+    return executeApiRequest('get', urlBuilder(Url.setConfigToDB, {}), {})
+  }
 }
diff --git a/src/services/urlBuilder.ts b/src/services/urlBuilder.ts
index 5c984bb..cef2dd0 100644
--- a/src/services/urlBuilder.ts
+++ b/src/services/urlBuilder.ts
@@ -30,6 +30,7 @@ export enum Url {
   respondReport = 'respondReport',
   changeReportedStatus = 'changeReportedStatus',
   configSettings = 'configSettings',
+  setConfigToDB = 'setConfigToDB',
   getThemesList = 'getThemesList',
 }
 
@@ -65,6 +66,7 @@ const urls = {
   [Url.respondReport]: (options: UrlBuilderOptions) => `api/pleroma/admin/reports/${options.id}/respond`,
   [Url.changeReportedStatus]: (options: UrlBuilderOptions) => `api/pleroma/admin/statuses/${options.id}`,
   [Url.configSettings]: (options: UrlBuilderOptions) => `api/pleroma/admin/config`,
+  [Url.setConfigToDB]: (options: UrlBuilderOptions) => `api/pleroma/admin/config_to_db`,
   [Url.getThemesList]: (options: UrlBuilderOptions) => 'static/styles.json',
 }
 
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index c8e0b4e..fad481d 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -4,11 +4,9 @@ import { configKeysTabs } from '../data/Config'
 
 export default (configs) => {
   const configObj = {}
-  configs.forEach(item => {
-    const constructorObj = configKeysTabs.find(tab => tab.key === item.key)
-    if (constructorObj) {
-      configObj[item.key] = new constructorObj.constructor(t(item, 'value').safeObject)
-    }
+  configKeysTabs.forEach(({ key, constructor }) => {
+    const apiConfig = configs.find(item => key === item.key)
+    configObj[key] = new constructor(t(apiConfig, 'value').safeObject)
   })
   return configObj
 }
-- 
GitLab


From 048ebaf48906a07373f3b58797b749bb046c87f0 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 26 Jul 2019 18:30:27 +0300
Subject: [PATCH 32/61] feat: Pleroma.ScheduledActivity, ':oauth2'

---
 src/data/Config.ts                            | 18 +++++++++++++++++-
 src/entities/settings/Oauth2Config.ts         | 11 +++++++++++
 .../settings/ScheduledActivityConfig.ts       | 10 ++++++++++
 src/i18n/en.json                              | 19 ++++++++++++++++++-
 4 files changed, 56 insertions(+), 2 deletions(-)
 create mode 100644 src/entities/settings/Oauth2Config.ts
 create mode 100644 src/entities/settings/ScheduledActivityConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 06a2f1a..64e650d 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -16,7 +16,9 @@ import HttpSecurityConfig from '../entities/settings/HttpSecurityConfig'
 import RichMediaConfig from '../entities/settings/RichMediaConfig'
 import FetchInitialPostsConfig from '../entities/settings/FetchInitialPostsConfig'
 import HackneyPoolsConfig from '../entities/settings/HackneyPoolsConfig'
-import AutoLinkerConfig from "../entities/settings/AutoLinkerConfig";
+import AutoLinkerConfig from '../entities/settings/AutoLinkerConfig'
+import ScheduledActivityConfig from '../entities/settings/ScheduledActivityConfig'
+import Oauth2Config from '../entities/settings/Oauth2Config'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -40,6 +42,8 @@ export enum configKeys {
   USER = ':user',
   HTTP_SECURITY = ':http_security',
   AUTO_LINKER = ':auto_linker',
+  SCHEDULED_ACTIVITY = 'Pleroma.ScheduledActivity',
+  OAUTH2 = ':oauth2'
 }
 
 export const arrayParams = {
@@ -168,5 +172,17 @@ export const configKeysTabs = [
     name: 'Auto linker',
     tab: true,
     constructor: AutoLinkerConfig
+  },
+  {
+    key: configKeys.SCHEDULED_ACTIVITY,
+    name: 'Scheduled activity',
+    tab: true,
+    constructor: ScheduledActivityConfig
+  },
+  {
+    key: configKeys.OAUTH2,
+    name: 'OAUTH2',
+    tab: true,
+    constructor: Oauth2Config
   }
 ]
diff --git a/src/entities/settings/Oauth2Config.ts b/src/entities/settings/Oauth2Config.ts
new file mode 100644
index 0000000..8388156
--- /dev/null
+++ b/src/entities/settings/Oauth2Config.ts
@@ -0,0 +1,11 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class Oauth2Config {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  token_expires_in:number = 600
+  issue_new_refresh_token:boolean = true
+  clean_expired_tokens:boolean = false
+  clean_expired_tokens_interval:number = 86400000
+}
diff --git a/src/entities/settings/ScheduledActivityConfig.ts b/src/entities/settings/ScheduledActivityConfig.ts
new file mode 100644
index 0000000..1106dc7
--- /dev/null
+++ b/src/entities/settings/ScheduledActivityConfig.ts
@@ -0,0 +1,10 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class ScheduledActivityConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  daily_user_limit:number = 25
+  total_user_limit:number = 300
+  enabled:boolean = true
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index febbc74..4b81f8f 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -782,7 +782,24 @@
       "extra_note": "false - link urls with rarely used schemes (magnet, ipfs, irc, etc.)",
       "validate_tld": "Validate TLD",
       "validate_tld_note": ""
-
+    },
+    "Pleroma.ScheduledActivity_form": {
+      "daily_user_limit": "Daily user limit",
+      "daily_user_limit_note": "The number of scheduled activities a user is allowed to create in a single day (Default: 25)",
+      "total_user_limit": "Total user limit",
+      "total_user_limit_note": "The number of scheduled activities a user is allowed to create in total (Default: 300)",
+      "enabled": "Enabled",
+      "enabled_note": "Whether scheduled activities are sent to the job queue to be executed"
+    },
+    ":oauth2_form": {
+      "token_expires_in": "Token expires in",
+      "token_expires_in_note": "The lifetime in seconds of the access token",
+      "issue_new_refresh_token": "Issue new refresh token",
+      "issue_new_refresh_token_note": "Keeps old refresh token or generate new refresh token when to obtain an access token",
+      "clean_expired_tokens": "Clean expired tokens",
+      "clean_expired_tokens_note": "Enable a background job to clean expired oauth tokens. Defaults to false.",
+      "clean_expired_tokens_interval": "Clean expired tokens interval",
+      "clean_expired_tokens_interval_note": "Interval to run the job to clean expired tokens. Defaults to 86400000 (24 hours)"
     }
   }
 }
-- 
GitLab


From 0ab732f66ab4c0b8321a2394c4dce4467e376da9 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 29 Jul 2019 16:08:28 +0300
Subject: [PATCH 33/61] feat: :rate_limit

---
 src/data/Config.ts                       | 10 +++-
 src/entities/settings/RateLimitConfig.ts | 55 ++++++++++++++++++++++
 src/i18n/en.json                         | 58 ++++++++++++++++++++++++
 src/utils/ConvertConfigToApiRequest.js   |  3 +-
 4 files changed, 124 insertions(+), 2 deletions(-)
 create mode 100644 src/entities/settings/RateLimitConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 64e650d..1b7d4a0 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -19,6 +19,7 @@ import HackneyPoolsConfig from '../entities/settings/HackneyPoolsConfig'
 import AutoLinkerConfig from '../entities/settings/AutoLinkerConfig'
 import ScheduledActivityConfig from '../entities/settings/ScheduledActivityConfig'
 import Oauth2Config from '../entities/settings/Oauth2Config'
+import RateLimitConfig from '../entities/settings/RateLimitConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -43,7 +44,8 @@ export enum configKeys {
   HTTP_SECURITY = ':http_security',
   AUTO_LINKER = ':auto_linker',
   SCHEDULED_ACTIVITY = 'Pleroma.ScheduledActivity',
-  OAUTH2 = ':oauth2'
+  OAUTH2 = ':oauth2',
+  RATE_LIMIT = ':rate_limit'
 }
 
 export const arrayParams = {
@@ -184,5 +186,11 @@ export const configKeysTabs = [
     name: 'OAUTH2',
     tab: true,
     constructor: Oauth2Config
+  },
+  {
+    key: configKeys.RATE_LIMIT,
+    name: 'Rate limit',
+    tab: true,
+    constructor: RateLimitConfig
   }
 ]
diff --git a/src/entities/settings/RateLimitConfig.ts b/src/entities/settings/RateLimitConfig.ts
new file mode 100644
index 0000000..6b7f29b
--- /dev/null
+++ b/src/entities/settings/RateLimitConfig.ts
@@ -0,0 +1,55 @@
+import t from 'typy'
+import { forIn } from 'lodash'
+
+export default class RateLimitConfig {
+  constructor(existConfig?) {
+    existConfig.forEach(({ tuple }) => {
+      const name = tuple[0].substring(1)
+      if (name !== 'search') {
+        const fieldConfig = t(tuple[1], 'tuple').safeObject
+        if (fieldConfig[0]) {
+          this[name].scale = fieldConfig[0]
+        }
+        if (fieldConfig[1]) {
+          this[name].limit = fieldConfig[1]
+        }
+      } else {
+        tuple[1].forEach((search, ind) => {
+          const name = ind === 0 ? 'account' : 'status'
+          const val = search.tuple
+          if (val[0]) {
+            this.search[name].scale = val[0]
+          }
+          if (val[1]) {
+            this.search[name].limit = val[1]
+          }
+        })
+      }
+    })
+  }
+  search:any = {
+    account: { scale: 1000, limit: 10 },
+    status: { scale: 1000, limit: 30 }
+  }
+  app_account_creation:any = {scale: 1800000, limit: 25}
+  relations_actions:any = {scale: 10000, limit: 10}
+  relation_id_action:any = {scale: 60000, limit: 2}
+  statuses_actions:any = {scale: 10000, limit: 15}
+  status_id_action:any = {scale: 60000, limit: 3}
+  password_reset:any = {scale: 1800000, limit: 5}
+}
+
+
+export const normalizeRateLimitConfig = (config) => {
+  let apiConf = []
+  forIn(config, (field, key) => {
+    if (key !== 'search') {
+      // @ts-ignore
+      apiConf.push({tuple: [`:${key}`, {tuple: [field.scale, field.limit]}]})
+    } else {
+      // @ts-ignore
+      apiConf.push({tuple: [`:search`, [{tuple: [field.account.scale, field.account.limit]}, {tuple: [field.status.scale, field.status.limit]}]]})
+    }
+  })
+  return apiConf
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4b81f8f..2ec5f1a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -800,6 +800,64 @@
       "clean_expired_tokens_note": "Enable a background job to clean expired oauth tokens. Defaults to false.",
       "clean_expired_tokens_interval": "Clean expired tokens interval",
       "clean_expired_tokens_interval_note": "Interval to run the job to clean expired tokens. Defaults to 86400000 (24 hours)"
+    },
+    "search_form": {
+      "title": "Search"
+    },
+    "account_form": {
+      "title": "Account",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit_note": "How many requests to limit in the time scale provided."
+    },
+    "status_form": {
+      "title": "Status",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit": "Limit",
+      "limit_note": "How many requests to limit in the time scale provided."
+    },
+    "app_account_creation_form": {
+      "title": "App account creation form",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit": "Limit",
+      "limit_note": "How many requests to limit in the time scale provided."
+    },
+    "relations_actions_form": {
+      "title": "Relations actions",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit": "Limit",
+      "limit_note": "How many requests to limit in the time scale provided."
+    },
+    "relation_id_action_form": {
+      "title": "Relation is action",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit": "Limit",
+      "limit_note": "How many requests to limit in the time scale provided."
+    },
+    "statuses_actions_form": {
+      "title": "Statuses actions",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit": "Limit",
+      "limit_note": "How many requests to limit in the time scale provided."
+    },
+    "status_id_action_form": {
+      "title": "Status id action",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit": "Limit",
+      "limit_note": "How many requests to limit in the time scale provided."
+    },
+    "password_reset_form": {
+      "title": "Password reset",
+      "scale": "Scale",
+      "scale_note": "The time scale in milliseconds",
+      "limit": "Limit",
+      "limit_note": "How many requests to limit in the time scale provided."
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index b874852..4f131ee 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,6 +1,7 @@
 import { forIn } from 'lodash'
 import { arrayParams, configKeys } from '../data/Config'
 import { normalizeAutoLinkerConfig } from '../entities/settings/AutoLinkerConfig'
+import { normalizeRateLimitConfig } from '../entities/settings/RateLimitConfig'
 
 export default (configs) => {
   const settings = []
@@ -15,7 +16,7 @@ export default (configs) => {
     settings.push({
       group: 'pleroma',
       key,
-      value: getConfigValue(newVal)
+      value: key !== configKeys.RATE_LIMIT ? getConfigValue(newVal) : normalizeRateLimitConfig(newVal)
     })
   })
   return { configs: settings }
-- 
GitLab


From 12f64b69e8927d085fa589b8795f14a224b8191e Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 29 Jul 2019 16:36:03 +0300
Subject: [PATCH 34/61] feat: :chat

---
 .../pages/configSettings/ConfigSettingsPage.vue           | 6 ++++++
 src/data/Config.ts                                        | 8 ++++++++
 src/entities/settings/ChatConfig.ts                       | 8 ++++++++
 src/i18n/en.json                                          | 5 +++++
 4 files changed, 27 insertions(+)
 create mode 100644 src/entities/settings/ChatConfig.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 56c15bf..c8e21d6 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -32,6 +32,12 @@
           v-model="config[configKeysEnum.UPLOADERSLOCAL]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.CHAT"
+          v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
+          v-model="config[configKeysEnum.CHAT]"
+          showTitle
+        />
         <config-form
           :title="configKeysEnum.KOCAPTCHA"
           v-if="tabs[value].key === configKeysEnum.CAPTCHA"
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 1b7d4a0..76fa4ca 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -20,6 +20,7 @@ import AutoLinkerConfig from '../entities/settings/AutoLinkerConfig'
 import ScheduledActivityConfig from '../entities/settings/ScheduledActivityConfig'
 import Oauth2Config from '../entities/settings/Oauth2Config'
 import RateLimitConfig from '../entities/settings/RateLimitConfig'
+import ChatConfig from '../entities/settings/ChatConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -30,6 +31,7 @@ export enum configKeys {
   INSTANCE = ':instance',
   LOGGER = ':logger',
   FRONTEND_CONFIGURATIONS = ':frontend_configurations',
+  CHAT = ':chat',
   WEB = 'Pleroma.Web',
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
@@ -103,6 +105,12 @@ export const configKeysTabs = [
   //   name: 'Web',
   //   tab: false,
   // },
+  {
+    key: configKeys.CHAT,
+    name: 'Chat',
+    tab: false,
+    constructor: ChatConfig
+  },
   {
     key: configKeys.CAPTCHA,
     name: 'Captcha',
diff --git a/src/entities/settings/ChatConfig.ts b/src/entities/settings/ChatConfig.ts
new file mode 100644
index 0000000..5da0e7e
--- /dev/null
+++ b/src/entities/settings/ChatConfig.ts
@@ -0,0 +1,8 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class ChatConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  enabled: boolean = true
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 2ec5f1a..0fb5ebb 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -858,6 +858,11 @@
       "scale_note": "The time scale in milliseconds",
       "limit": "Limit",
       "limit_note": "How many requests to limit in the time scale provided."
+    },
+    ":chat_form": {
+      "title": "Chat",
+      "enabled": "Enabled",
+      "enabled_note": ""
     }
   }
 }
-- 
GitLab


From f108856373a26c49de646b6f945e056cefdd181c Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 30 Jul 2019 23:11:16 +0300
Subject: [PATCH 35/61] feat: some special server's error processing

---
 .../layout/app-navbar/AppNavbar.vue           |  4 +++-
 .../configSettings/ConfigSettingsPage.vue     | 19 ++++++++++++-------
 src/components/pages/users/UsersPage.vue      |  5 ++++-
 src/services/executeApiRequest.js             |  3 ++-
 4 files changed, 21 insertions(+), 10 deletions(-)

diff --git a/src/components/layout/app-navbar/AppNavbar.vue b/src/components/layout/app-navbar/AppNavbar.vue
index 254cd6b..a3adff2 100644
--- a/src/components/layout/app-navbar/AppNavbar.vue
+++ b/src/components/layout/app-navbar/AppNavbar.vue
@@ -61,7 +61,9 @@ export default {
     if (t(key, 'username').safeObject) {
       await axiosInstance.get(urlBuilder('getUser', { id: key.username }))
         .then(({ data }) => { this.loadUserProfile(data) })
-        .catch(({ response: { data } }) => console.log(data.error))
+        .catch(e => {
+          this.$toasted.show(`Account loading: ${e}`)
+        })
     }
   }
 }
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index c8e21d6..bd8f9ce 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -90,15 +90,20 @@ export default class ConfigSettingsPage extends Vue {
   loading:boolean = false
   async mounted () {
     this.loading = true
-    const { configs } = await ConfigService.listConfigSettings()
-    if (!configs.length) {
-      await ConfigService.setConfigToDB()
+    try {
       const { configs } = await ConfigService.listConfigSettings()
-      this.loadConfigs(configs)
-    } else {
-      this.loadConfigs(configs)
+      if (!configs.length) {
+        await ConfigService.setConfigToDB()
+        const { configs } = await ConfigService.listConfigSettings()
+        this.loadConfigs(configs)
+      } else {
+        this.loadConfigs(configs)
+      }
+    } catch (e) {
+      (this as any).$toasted.show(e)
+    } finally {
+      this.loading = false
     }
-    this.loading = false
   }
   async onSaveButtunClick () {
     this.loading = true
diff --git a/src/components/pages/users/UsersPage.vue b/src/components/pages/users/UsersPage.vue
index a106601..956c057 100644
--- a/src/components/pages/users/UsersPage.vue
+++ b/src/components/pages/users/UsersPage.vue
@@ -55,6 +55,7 @@ import UsersAdminPanel from '../../admin/UsersAdminPanel.vue'
 import { UsersService } from '../../../services/UsersService'
 import { RawLocation } from 'vue-router'
 import approx from 'approximate-number'
+import t from 'typy'
 
 @Component({
   components: { UsersAdminPanel, UsersTable, FulfillingBouncingCircleSpinner },
@@ -137,7 +138,9 @@ export default class Users extends Vue {
         this.loading = false
       }
     } catch (e) {
-      (this as any).$toasted.show(e.data.errors.detail)
+      (this as any).$toasted.show(t(e, 'data.errors.detail').safeObject || 'Error: Network error')
+    } finally {
+      this.loading = false
     }
   }
   selectAll (val) {
diff --git a/src/services/executeApiRequest.js b/src/services/executeApiRequest.js
index d6be52a..77c1002 100644
--- a/src/services/executeApiRequest.js
+++ b/src/services/executeApiRequest.js
@@ -1,5 +1,6 @@
 import { axiosInstance } from './axiosInstance'
 import Router from '../router/router'
+import t from 'typy'
 
 export default function (method, url, data, options) {
   return axiosInstance({
@@ -9,7 +10,7 @@ export default function (method, url, data, options) {
   })
     .then((res) => options && options.returnFullRes ? res : res.data)
     .catch(err => {
-      if (err.response.status === 403) {
+      if (t(err, 'response.status').safeObject === 403) {
         Router.push({ name: 'login' })
       } else {
         throw err
-- 
GitLab


From 713cd8e6d509e7c34f8f9e1069bc19ab78448fea Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 30 Jul 2019 23:11:47 +0300
Subject: [PATCH 36/61] feat: :gopher

---
 src/data/Config.ts                     | 11 ++++++++++-
 src/entities/settings/GopherConfig.ts  | 22 ++++++++++++++++++++++
 src/i18n/en.json                       | 10 ++++++++++
 src/utils/ConvertConfigToApiRequest.js |  4 ++++
 4 files changed, 46 insertions(+), 1 deletion(-)
 create mode 100644 src/entities/settings/GopherConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 76fa4ca..b1da3cb 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -21,6 +21,7 @@ import ScheduledActivityConfig from '../entities/settings/ScheduledActivityConfi
 import Oauth2Config from '../entities/settings/Oauth2Config'
 import RateLimitConfig from '../entities/settings/RateLimitConfig'
 import ChatConfig from '../entities/settings/ChatConfig'
+import GopherConfig from '../entities/settings/GopherConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -40,6 +41,7 @@ export enum configKeys {
   HACKNEY_POOLS = ':hackney_pools',
   DATABASE = ':database',
   MEDIA_PROXY = ':media_proxy',
+  GOPHER = ':gopher',
   LDAP = ':ldap',
   ACTIVITY_PUB = ':activitypub',
   USER = ':user',
@@ -54,7 +56,8 @@ export const arrayParams = {
   [configKeys.INSTANCE]: ['quarantined_instances', 'mrf_transparency_exclusions', 'autofollowed_nicknames'],
   [configKeys.LDAP]: ['sslopts', 'tlsopts'],
   [configKeys.MEDIA_PROXY]: ['whitelist'],
-  [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld']
+  [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld'],
+  [configKeys.GOPHER]: ['ip'],
 }
 
 export const configKeysTabs = [
@@ -135,6 +138,12 @@ export const configKeysTabs = [
     constructor: MediaProxyConfig,
     tab: true,
   },
+  {
+    key: configKeys.GOPHER,
+    name: 'Gopher',
+    tab: true,
+    constructor: GopherConfig
+  },
   {
     key: configKeys.LDAP,
     name: 'LDAP',
diff --git a/src/entities/settings/GopherConfig.ts b/src/entities/settings/GopherConfig.ts
new file mode 100644
index 0000000..f0ea14c
--- /dev/null
+++ b/src/entities/settings/GopherConfig.ts
@@ -0,0 +1,22 @@
+import {normalizeApiConfig} from '../../utils/ConvertConfigToState'
+import {arrayParams, configKeys} from '../../data/Config'
+
+export default class GopherConfig {
+  constructor(existConfig?) {
+    const ip = existConfig.find(({ tuple }) => tuple[0] === ':ip')
+    if (ip) {
+      ip.tuple[1] = ip.tuple[1].tuple
+    }
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.GOPHER])
+  }
+  enabled: boolean = false
+  ip: any = '0000'
+  port: number = 9999
+}
+
+
+export const normalizeGopherConfig = (config) => {
+  const apiConf = {...config}
+  apiConf.ip = { tuple: apiConf.ip, sendAsMap: true}
+  return apiConf
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0fb5ebb..feb0231 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -863,6 +863,16 @@
       "title": "Chat",
       "enabled": "Enabled",
       "enabled_note": ""
+    },
+    ":gopher_form": {
+      "enabled": "Enabled",
+      "enabled_note": "Enables the gopher interface",
+      "ip": "IP address",
+      "ip_note": "",
+      "port": "Port",
+      "port_note": "",
+      "dstport": "Dst port",
+      "dstport_note": "Port advertised in urls (optional, defaults to port)"
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 4f131ee..4205f89 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -2,6 +2,7 @@ import { forIn } from 'lodash'
 import { arrayParams, configKeys } from '../data/Config'
 import { normalizeAutoLinkerConfig } from '../entities/settings/AutoLinkerConfig'
 import { normalizeRateLimitConfig } from '../entities/settings/RateLimitConfig'
+import { normalizeGopherConfig } from '../entities/settings/GopherConfig'
 
 export default (configs) => {
   const settings = []
@@ -13,6 +14,9 @@ export default (configs) => {
     if (key === configKeys.AUTO_LINKER) {
       newVal = normalizeAutoLinkerConfig(newVal)
     }
+    if (key === configKeys.GOPHER) {
+      newVal = normalizeGopherConfig(newVal)
+    }
     settings.push({
       group: 'pleroma',
       key,
-- 
GitLab


From bf54a85a3e28169ce220c41136fc8962a9d4c459 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 31 Jul 2019 16:48:01 +0300
Subject: [PATCH 37/61] feat: :emoji

---
 .../configSettings/forms/EmojiForm.vue        | 105 ++++++++++++++++++
 .../configSettings/ConfigSettingsPage.vue     |  19 +++-
 src/data/Config.ts                            |   9 ++
 src/entities/settings/EmojiConfig.ts          |  34 ++++++
 src/i18n/en.json                              |  12 ++
 src/utils/ConvertConfigToApiRequest.js        |   7 +-
 .../vuestic-components/va-tabs/VaTabs.vue     |   2 +-
 7 files changed, 183 insertions(+), 5 deletions(-)
 create mode 100644 src/components/configSettings/forms/EmojiForm.vue
 create mode 100644 src/entities/settings/EmojiConfig.ts

diff --git a/src/components/configSettings/forms/EmojiForm.vue b/src/components/configSettings/forms/EmojiForm.vue
new file mode 100644
index 0000000..5d348c5
--- /dev/null
+++ b/src/components/configSettings/forms/EmojiForm.vue
@@ -0,0 +1,105 @@
+<template>
+  <div class="emoji-form">
+    <p class="title">{{$t('config_settings.:emoji_form.title')}}</p>
+    <div class="mb-3">
+      <span class="title">{{$t('config_settings.:emoji_form.shortcode_globs')}}</span>
+      <p class="note mb-0">{{$t('config_settings.:emoji_form.shortcode_globs_note')}}</p>
+      <div v-for="(item, index) in formData.shortcode_globs" :key="index" class="va-row">
+        <va-input v-model="formData.shortcode_globs[index]"/>
+        <va-icon
+          color="danger"
+          @click.native="removeShortcodeGlob(index)"
+          name="ion-ios-trash-outline"
+          class="px-2 emoji-form__delete"
+        />
+      </div>
+      <div class="va-row mb-2">
+        <va-button small @click="addShortcodeGlob" outline class="ma-0">
+          {{$t('config_settings.:emoji_form.add_shortcode_glob')}}
+        </va-button>
+      </div>
+    </div>
+    <va-input
+      class="mb-0"
+      v-model='formData.pack_extensions'
+      :label="$t('config_settings.:emoji_form.pack_extensions')"
+    />
+    <p class="note">{{$t('config_settings.:emoji_form.pack_extensions_note')}}</p>
+    <div class="mb-3">
+      <p class="title mb-0">{{$t('config_settings.:emoji_form.groups')}}</p>
+      <p class="note mb-0">{{$t('config_settings.:emoji_form.groups_note')}}</p>
+      <div v-for="(group, index) in formData.groups" class="va-row" :key="index">
+        <va-input v-model="formData.groups[index][0]" class="emoji-form__group-name"/>
+        <va-input v-model="formData.groups[index][1]"/>
+        <va-icon
+          color="danger"
+          @click.native="removeGroup(index)"
+          name="ion-ios-trash-outline"
+          class="px-2 emoji-form__delete"
+        />
+      </div>
+      <div class="va-row mb-2">
+        <va-button small outline @click="addGroup" class="ma-0">Add group</va-button>
+      </div>
+    </div>
+    <va-input v-model="formData.default_manifest"/>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import { selectOptions } from '../../../utils/GetFieldList'
+import { keys } from 'lodash'
+import EmojiConfig from '../../../entities/settings/EmojiConfig'
+
+@Component({
+  components: {},
+})
+export default class EmojiForm extends Vue {
+  @Prop(Object) readonly value!: EmojiConfig
+  @Prop(String) readonly title!: string
+  @Prop(Boolean) readonly showTitle!: boolean
+  @Prop(String) readonly margin!: string
+  selectOptions = selectOptions
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  addShortcodeGlob () {
+    this.formData.shortcode_globs.push('')
+  }
+  removeShortcodeGlob (index) {
+    this.formData.shortcode_globs.splice(index, 1)
+  }
+  addGroup () {
+    this.formData.groups.push(['', ''])
+  }
+  removeGroup (index) {
+    this.formData.groups.splice(index, 1)
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.emoji-form {
+  border-top: 1px solid $border-color;
+  &__delete {
+    font-size: 1.5rem;
+    cursor: pointer;
+  }
+  .va-row {
+    margin-left: 0 !important;
+    margin-right: 0 !important;
+  }
+  &__group-name {
+    margin-right: 0.5rem;
+  }
+  @include media-breakpoint-down(sm) {
+    &__group-name {
+      margin-right: 0;
+    }
+  }
+}
+</style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index bd8f9ce..831c972 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -38,6 +38,12 @@
           v-model="config[configKeysEnum.CHAT]"
           showTitle
         />
+        <emoji-form
+          :title="configKeysEnum.EMOJI"
+          v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
+          v-model="config[configKeysEnum.EMOJI]"
+          showTitle
+        />
         <config-form
           :title="configKeysEnum.KOCAPTCHA"
           v-if="tabs[value].key === configKeysEnum.CAPTCHA"
@@ -74,9 +80,11 @@ import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
 import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 import AutoLinkerForm from '../../configSettings/forms/AutoLinkerForm.vue'
+import EmojiForm from "../../configSettings/forms/EmojiForm.vue";
 
 @Component({
   components: {
+    EmojiForm,
     AutoLinkerForm,
     ConfigForm,
     EmailsForm,
@@ -107,9 +115,14 @@ export default class ConfigSettingsPage extends Vue {
   }
   async onSaveButtunClick () {
     this.loading = true
-    const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest(this.config))
-    this.loadConfigs(configs)
-    this.loading = false
+    try {
+      const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest(this.config))
+      this.loadConfigs(configs)
+    } catch (e) {
+      (this as any).$toasted.show(e)
+    } finally {
+      this.loading = false
+    }
   }
   loadConfigs (configs) {
     const config = ConvertConfigToState(configs)
diff --git a/src/data/Config.ts b/src/data/Config.ts
index b1da3cb..d3fb4ad 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -22,6 +22,7 @@ import Oauth2Config from '../entities/settings/Oauth2Config'
 import RateLimitConfig from '../entities/settings/RateLimitConfig'
 import ChatConfig from '../entities/settings/ChatConfig'
 import GopherConfig from '../entities/settings/GopherConfig'
+import EmojiConfig from '../entities/settings/EmojiConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -33,6 +34,7 @@ export enum configKeys {
   LOGGER = ':logger',
   FRONTEND_CONFIGURATIONS = ':frontend_configurations',
   CHAT = ':chat',
+  EMOJI = ':emoji',
   WEB = 'Pleroma.Web',
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
@@ -58,6 +60,7 @@ export const arrayParams = {
   [configKeys.MEDIA_PROXY]: ['whitelist'],
   [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld'],
   [configKeys.GOPHER]: ['ip'],
+  [configKeys.EMOJI]: ['pack_extensions'],
 }
 
 export const configKeysTabs = [
@@ -114,6 +117,12 @@ export const configKeysTabs = [
     tab: false,
     constructor: ChatConfig
   },
+  {
+    key: configKeys.EMOJI,
+    name: 'Emoji',
+    tab: false,
+    constructor: EmojiConfig,
+  },
   {
     key: configKeys.CAPTCHA,
     name: 'Captcha',
diff --git a/src/entities/settings/EmojiConfig.ts b/src/entities/settings/EmojiConfig.ts
new file mode 100644
index 0000000..e78a0c7
--- /dev/null
+++ b/src/entities/settings/EmojiConfig.ts
@@ -0,0 +1,34 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import { forIn } from 'lodash'
+import {arrayParams, configKeys} from '../../data/Config'
+
+export default class EmojiConfig {
+  constructor(existConfig?) {
+    console.log(existConfig)
+    const groups = existConfig.find(({ tuple }) => tuple[0] === ':groups')
+    const newGroupsList:any = []
+    groups.tuple[1].forEach(({ tuple }) => {
+      tuple[1] = tuple[1].join(';')
+      newGroupsList.push(tuple)
+    })
+    groups.tuple[1] = newGroupsList
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.EMOJI])
+  }
+  shortcode_globs: any = []
+  pack_extensions: any = []
+  default_manifest: string = 'https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json'
+  groups: any = []
+}
+
+export const normalizeEmojiConfig = (config) => {
+  const apiConfig = [...config]
+  const groups = apiConfig.find(({ tuple }) => tuple[0] === ':groups')
+  groups.tuple[1] = groups.tuple[1].map(group => {
+    if (group[0].length && group[1].length) {
+      return { tuple: [ group[0], group[1].split(';')] }
+    }
+  }).filter(group => group)
+  const shortcode_globs = apiConfig.find(({ tuple }) => tuple[0] === ':shortcode_globs')
+  shortcode_globs.tuple[1] = shortcode_globs.tuple[1].filter(globe => globe.length)
+  return apiConfig
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index feb0231..786e2ca 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -873,6 +873,18 @@
       "port_note": "",
       "dstport": "Dst port",
       "dstport_note": "Port advertised in urls (optional, defaults to port)"
+    },
+    ":emoji_form": {
+      "title": "Emoji",
+      "shortcode_globs": "Locations of custom emoji files",
+      "shortcode_globs_note": "* can be used as a wildcard",
+      "add_shortcode_glob": "Add one more location",
+      "pack_extensions": "A list of file extensions for emojis",
+      "pack_extensions_note": "Used, when no emoji.txt for a pack is present. Separate items with ;",
+      "default_manifest": "Location of the JSON-manifest",
+      "default_manifest_note": "This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays)",
+      "groups": "Emoji groups",
+      "groups_note": " Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. * can be used as a wildcard"
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 4205f89..c8cfd34 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -3,6 +3,7 @@ import { arrayParams, configKeys } from '../data/Config'
 import { normalizeAutoLinkerConfig } from '../entities/settings/AutoLinkerConfig'
 import { normalizeRateLimitConfig } from '../entities/settings/RateLimitConfig'
 import { normalizeGopherConfig } from '../entities/settings/GopherConfig'
+import {normalizeEmojiConfig} from '../entities/settings/EmojiConfig'
 
 export default (configs) => {
   const settings = []
@@ -17,10 +18,14 @@ export default (configs) => {
     if (key === configKeys.GOPHER) {
       newVal = normalizeGopherConfig(newVal)
     }
+    newVal = key !== configKeys.RATE_LIMIT ? getConfigValue(newVal) : normalizeRateLimitConfig(newVal)
+    if (key === configKeys.EMOJI) {
+      newVal = normalizeEmojiConfig(newVal)
+    }
     settings.push({
       group: 'pleroma',
       key,
-      value: key !== configKeys.RATE_LIMIT ? getConfigValue(newVal) : normalizeRateLimitConfig(newVal)
+      value: newVal
     })
   })
   return { configs: settings }
diff --git a/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue b/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue
index 75533c0..6341fc0 100644
--- a/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue
+++ b/src/vuestic-theme/vuestic-components/va-tabs/VaTabs.vue
@@ -152,7 +152,7 @@ export default {
   .va-tabs__container {
     flex: 1 0 auto;
     display: flex;
-    height: 2.5rem;
+    height: calc(2.5rem + 18px);
     list-style-type: none;
     transition: transform 0.6s cubic-bezier(0.86, 0, 0.07, 1);
     white-space: nowrap;
-- 
GitLab


From e21ef5de438010e1664eaa903ebffee1c310362e Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 31 Jul 2019 19:45:07 +0300
Subject: [PATCH 38/61] feat: fix error after restart server (temporary remove
 hackney_pools config) && the last version of select

---
 src/data/Config.ts                            | 38 +++++------
 src/entities/settings/EmojiConfig.ts          |  1 -
 src/entities/settings/InstanceConfig.ts       |  1 -
 src/i18n/en.json                              | 30 +++++----
 src/utils/GetFieldList.ts                     |  2 +-
 .../va-select/VaSelect.demo.vue               | 50 +++++----------
 .../vuestic-components/va-select/VaSelect.vue | 63 ++++++++++---------
 .../va-select/getDemoData.js                  | 33 ++++++++++
 8 files changed, 119 insertions(+), 99 deletions(-)
 create mode 100644 src/vuestic-theme/vuestic-components/va-select/getDemoData.js

diff --git a/src/data/Config.ts b/src/data/Config.ts
index d3fb4ad..91f78ad 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -58,18 +58,12 @@ export const arrayParams = {
   [configKeys.INSTANCE]: ['quarantined_instances', 'mrf_transparency_exclusions', 'autofollowed_nicknames'],
   [configKeys.LDAP]: ['sslopts', 'tlsopts'],
   [configKeys.MEDIA_PROXY]: ['whitelist'],
-  [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld'],
+  [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld', 'ttl_setters'],
   [configKeys.GOPHER]: ['ip'],
   [configKeys.EMOJI]: ['pack_extensions'],
 }
 
 export const configKeysTabs = [
-  {
-    key: configKeys.UPLOAD,
-    name: 'Upload',
-    constructor: UploadConfig,
-    tab: true,
-  },
   {
     key: configKeys.UPLOADERSS3,
     tab: false,
@@ -82,12 +76,6 @@ export const configKeysTabs = [
     name: 'Uploader Local',
     constructor: UploadersLocalConfig,
   },
-  {
-    key: configKeys.URI_SCHEMES,
-    name: 'URI schemes',
-    constructor: UriSchemesConfig,
-    tab: true,
-  },
   {
     key: configKeys.INSTANCE,
     name: 'Instance',
@@ -135,6 +123,18 @@ export const configKeysTabs = [
     tab: false,
     constructor: KocaptchaConfig
   },
+  {
+    key: configKeys.UPLOAD,
+    name: 'Upload',
+    constructor: UploadConfig,
+    tab: true,
+  },
+  {
+    key: configKeys.URI_SCHEMES,
+    name: 'URI schemes',
+    constructor: UriSchemesConfig,
+    tab: true,
+  },
   {
     key: configKeys.DATABASE,
     name: 'Database options',
@@ -189,12 +189,12 @@ export const configKeysTabs = [
     tab: true,
     constructor: FetchInitialPostsConfig
   },
-  {
-    key: configKeys.HACKNEY_POOLS,
-    name: 'Hackney pools',
-    tab: true,
-    constructor: HackneyPoolsConfig
-  },
+  // {
+  //   key: configKeys.HACKNEY_POOLS,
+  //   name: 'Hackney pools',
+  //   tab: true,
+  //   constructor: HackneyPoolsConfig
+  // },
   {
     key: configKeys.AUTO_LINKER,
     name: 'Auto linker',
diff --git a/src/entities/settings/EmojiConfig.ts b/src/entities/settings/EmojiConfig.ts
index e78a0c7..de2a981 100644
--- a/src/entities/settings/EmojiConfig.ts
+++ b/src/entities/settings/EmojiConfig.ts
@@ -4,7 +4,6 @@ import {arrayParams, configKeys} from '../../data/Config'
 
 export default class EmojiConfig {
   constructor(existConfig?) {
-    console.log(existConfig)
     const groups = existConfig.find(({ tuple }) => tuple[0] === ':groups')
     const newGroupsList:any = []
     groups.tuple[1].forEach(({ tuple }) => {
diff --git a/src/entities/settings/InstanceConfig.ts b/src/entities/settings/InstanceConfig.ts
index 81606f7..92e3dd2 100644
--- a/src/entities/settings/InstanceConfig.ts
+++ b/src/entities/settings/InstanceConfig.ts
@@ -27,7 +27,6 @@ export default class InstanceConfig {
     sendAsMap: true
   }
   registrations_open: boolean = false
-  dedupe_media: boolean = false
   invites_enabled: boolean = false
   account_activation_required: boolean = false
   federating: boolean = false
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 786e2ca..d33053b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -521,7 +521,7 @@
       "banner_upload_limit_note": "File size limit of user’s profile banners",
       "poll_limits": "A map with poll limits for local polls",
       "account_activation_required": "Account activation required",
-      "account_activation_required_note": "Require users to confirm their emails before signing in.",
+      "account_activation_required_note": "Require users to confirm their emails before signing in",
       "federating": "Federating",
       "federating_note": "Enable federation with other instances",
       "federation_reachability_timeout_days": "Federation reachability timeout days",
@@ -535,27 +535,27 @@
       "allow_relay": "Allow relay",
       "allow_relay_note": "Enable Pleroma’s Relay, which makes it possible to follow a whole instance",
       "rewrite_policy": "Rewrite policy",
-      "rewrite_policy_note": "Message Rewrite Policy, either one or a list. Here are the ones available by default:",
+      "rewrite_policy_note": "Message Rewrite Policy, either one or a list",
       "public": "Public",
       "public_note": "Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.",
       "quarantined_instances": "Quarantined instances",
-      "quarantined_instances_note": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send. Separate items with ;",
+      "quarantined_instances_note": "List of ActivityPub instances where private(DMs, followers-only) activities will not be send. Separate items with ';'",
       "managed_config": "Managed config",
       "managed_config_note": "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
-      "static_dir": "",
+      "static_dir": "Static directory",
       "static_dir_note": "",
       "allowed_post_formats": "Allowed post formats",
       "allowed_post_formats_note": "MIME-type list of formats allowed to be posted (transformed into HTML)",
       "mrf_transparency": "mrf transparency",
-      "mrf_transparency_note": "Make the content of your Message Rewrite Facility settings public (via nodeinfo).",
+      "mrf_transparency_note": "Make the content of your Message Rewrite Facility settings public (via nodeinfo)",
       "mrf_transparency_exclusions": "Exclude specific instance names from MRF transparency.",
-      "mrf_transparency_exclusions_note": "The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. Separate items with ;",
+      "mrf_transparency_exclusions_note": "The use of the exclusions feature will be disclosed in nodeinfo as a boolean value. Separate items with ';'",
       "extended_nickname_format": "Extended nickname format",
       "extended_nickname_format_note": "Set to true to use extended local nicknames format (allows underscores/dashes). This will break federation with folder software for theses nicknames.",
       "max_pinned_statuses": "Max pinned status",
       "max_pinned_statuses_note": "The maximum number of pinned statuses. 0 will disable the feature.",
       "autofollowed_nicknames": "Autofollowed nicknames",
-      "autofollowed_nicknames_note": "Set to nicknames of (local) users that every new user should automatically follow. Separate items with ;",
+      "autofollowed_nicknames_note": "Set to nicknames of (local) users that every new user should automatically follow. Separate items with ';'",
       "no_attachment_links": "No attachment links",
       "no_attachment_links_note": "Set to true to disable automatically adding attachment link text to statuses",
       "welcome_message": "Welcome message",
@@ -653,9 +653,11 @@
     },
     ":media_proxy_form": {
       "whitelist": "Whitelist",
-      "whitelist_note": "List of domains to bypass the mediaproxy. Separate items with ;",
+      "whitelist_note": "List of domains to bypass the mediaproxy. Separate items with ';'",
       "enabled": "Enabled",
       "enabled_note": "Enables proxying of remote media to the instance’s proxy",
+      "redirect_on_failure": "Redirect on failure",
+      "redirect_on_failure_note": "",
       "base_url": "Base URL",
       "base_url_note": "The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts."
     },
@@ -696,7 +698,7 @@
       "tls": "TLS",
       "tls_note": "true to start TLS, usually implies the port 389",
       "tlsopts": "Additional TLS options",
-      "tlsopts_note": "Separate items with ;",
+      "tlsopts_note": "Separate items with ';'",
       "base": "LDAP base",
       "base_note": "e.g. 'dc=example,dc=com'",
       "uid": "UID",
@@ -734,11 +736,13 @@
       "enabled": "Enabled",
       "enabled_note": "If enabled the instance will parse metadata from attached links to generate link previews",
       "ignore_hosts": "Ignored hosts",
-      "ignore_hosts_note": "List of hosts which will be ignored by the metadata parser. Separate items by ;",
+      "ignore_hosts_note": "List of hosts which will be ignored by the metadata parser. Separate items with ';'",
       "ignore_tld": "Ignored top-level domains",
-      "ignore_tld_note": "List TLDs (top-level domains) which will ignore for parse metadata. Separate items by ;",
+      "ignore_tld_note": "List TLDs (top-level domains) which will ignore for parse metadata. Separate items with ';'",
       "parsers": "Parsers",
-      "parsers_note": ""
+      "parsers_note": "",
+      "ttl_setters": "TTL setters",
+      "ttl_setters_note": "Separate items with ';'"
     },
     ":fetch_initial_posts_form": {
       "enabled": "Enabled",
@@ -880,7 +884,7 @@
       "shortcode_globs_note": "* can be used as a wildcard",
       "add_shortcode_glob": "Add one more location",
       "pack_extensions": "A list of file extensions for emojis",
-      "pack_extensions_note": "Used, when no emoji.txt for a pack is present. Separate items with ;",
+      "pack_extensions_note": "Used, when no emoji.txt for a pack is present. Separate items with ';'",
       "default_manifest": "Location of the JSON-manifest",
       "default_manifest_note": "This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays)",
       "groups": "Emoji groups",
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 73f96f4..416e39e 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -12,7 +12,7 @@ export const selectOptions = {
     'Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy',
     'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy'
   ],
-  allowed_post_formats: [],
+  allowed_post_formats: ['text/plain', 'text/html', 'text/markdown', 'text/bbcode'],
   limit_to_local_content: [':unauthenticated', ':all', 'false'],
   federation_publisher_modules: ['Pleroma.Web.ActivityPub.Publisher', 'Pleroma.Web.Websub', 'Pleroma.Web.Salmon'],
   method: ['Pleroma.Captcha.Kocaptcha'],
diff --git a/src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue b/src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue
index 08f673f..ab3af1b 100644
--- a/src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue
+++ b/src/vuestic-theme/vuestic-components/va-select/VaSelect.demo.vue
@@ -79,6 +79,13 @@
         :options="defaultSelect.options"
       />
     </VbCard>
+    <VbCard title="No clear" style="width: 400px;">
+      <va-select
+        v-model="defaultSelect.value"
+        :options="defaultSelect.options"
+        no-clear
+      />
+    </VbCard>
     <VbCard title="Placeholder" style="width: 400px;">
       <va-select
         v-model="defaultSelect.value"
@@ -130,7 +137,7 @@
         :options="CountriesList"
       />
       <va-select
-        label="with custom max"
+        label="with custom tag-max"
         v-model="multipleValue"
         multiple
         :tagMax="8"
@@ -201,6 +208,7 @@
       <p>{{objectSelect.value}}</p>
       <p>{{iconsSelect.value}}</p>
       <p>{{multipleValue}}</p>
+      <p>{{longSelect.value}}</p>
     </VbCard>
   </VbDemo>
 </template>
@@ -210,44 +218,13 @@
 import CountriesList from '../../../data/CountriesList'
 import VaSelect from './VaSelect'
 import VaInputWrapper from '../va-input/VaInputWrapper'
+import { objectOptionsList, iconOptionsList } from './getDemoData'
 
 const positions = ['top', 'bottom']
 
 export default {
   components: { VaInputWrapper, VaSelect },
   data () {
-    const objectSelectOptions = [{ id: 1, text: 'one' }, { id: 2, text: 'two' }, { id: 3, text: 'three' }]
-    const iconsSelectOptions = [
-      {
-        text: 'item1',
-        id: 0,
-        value: 0,
-        icon: 'fa fa-address-book',
-      },
-      {
-        text: 'item2',
-        id: 1,
-        value: 1,
-        icon: 'fa fa-android',
-      },
-      {
-        text: 'item2',
-        id: 2,
-        value: 2,
-        icon: 'fa fa-android',
-      },
-      {
-        text: 'item2',
-        id: 3,
-        value: 3,
-      },
-      {
-        text: 'item2',
-        id: 4,
-        value: 4,
-        icon: 'fa fa-android',
-      },
-    ]
     return {
       defaultSelect: {
         options: ['one', 'two', 'three'],
@@ -255,11 +232,11 @@ export default {
       },
       objectSelect: {
         value: '',
-        options: objectSelectOptions,
+        options: objectOptionsList,
       },
       iconsSelect: {
         value: '',
-        options: iconsSelectOptions,
+        options: iconOptionsList,
       },
       longSelect: {
         value: '1st long long long long option sit amet, consectetur adipiscing elit,',
@@ -279,7 +256,8 @@ export default {
       this.isLoading = true
       setTimeout(() => {
         this.isLoading = false
-        this.CountriesList = this.CountriesList.slice(0, Math.round(this.CountriesList.length / 2))
+        // eslint-disable-next-line no-console
+        console.log(val)
       }, 2000)
     },
   },
diff --git a/src/vuestic-theme/vuestic-components/va-select/VaSelect.vue b/src/vuestic-theme/vuestic-components/va-select/VaSelect.vue
index 6d6abf1..a6b6832 100644
--- a/src/vuestic-theme/vuestic-components/va-select/VaSelect.vue
+++ b/src/vuestic-theme/vuestic-components/va-select/VaSelect.vue
@@ -10,6 +10,14 @@
     :style="{width}"
     :closeOnAnchorClick="false"
   >
+    <va-input
+      v-if="searchable"
+      :placeholder="placeholder"
+      v-model="search"
+      class="va-select__input"
+      ref="search"
+      removable
+    />
     <ul
       class="va-select__option-list"
       :style="optionsListStyle"
@@ -48,6 +56,7 @@
     >
       <label
         class="va-select__label"
+        :style="{ color: $themes.success }"
         aria-hidden="true"
       >{{label}}</label>
       <div
@@ -68,19 +77,10 @@
         </span>
         <span v-else-if="displayedText" class="va-select__displayed-text">{{displayedText}}</span>
         <span v-else class="va-select__placeholder">{{placeholder}}</span>
-        <input
-          v-if="searchable"
-          :placeholder="placeholder"
-          :value="search"
-          class="va-select__input"
-          @input="updateSearch($event.target.value)"
-          ref="search"
-          :style="inputStyles"
-        />
       </div>
       <va-icon
         v-if="showClearIcon"
-        class="va-select__clear-icon mr-1"
+        class="va-select__clear-icon"
         name="fa fa-times-circle"
         @click.native.stop="clear()"
       />
@@ -103,6 +103,7 @@ import VaDropdown from '../va-dropdown/VaDropdown'
 import VaChip from '../va-chip/VaChip'
 import { SpringSpinner } from 'epic-spinners'
 import VaIcon from '../va-icon/VaIcon'
+import VaInput from '../va-input/VaInput'
 import { getHoverColor } from '../../../services/color-functions'
 
 const positions = {
@@ -111,7 +112,7 @@ const positions = {
 }
 export default {
   name: 'va-select',
-  components: { VaIcon, SpringSpinner, VaDropdown, VaChip },
+  components: { VaIcon, SpringSpinner, VaDropdown, VaChip, VaInput },
   data () {
     return {
       search: '',
@@ -168,6 +169,7 @@ export default {
       type: Boolean,
       default: true,
     },
+    noClear: Boolean,
     error: Boolean,
     success: Boolean,
   },
@@ -177,7 +179,9 @@ export default {
     },
     visible (val) {
       if (val && this.searchable) {
-        this.$refs.search.focus()
+        this.$nextTick(() => {
+          this.$refs.search.$refs.input.focus()
+        })
       }
     },
   },
@@ -237,27 +241,25 @@ export default {
       })
     },
     showClearIcon () {
+      if (this.noClear) {
+        return false
+      }
       if (this.disabled) {
         return false
       }
-      return this.multiple ? this.valueProxy.length : this.valueProxy
+      return this.multiple ? this.valueProxy.length : this.valueProxy !== this.clearValue
     },
     inputWrapperStyles () {
-      let paddingRight = 1.5
+      let paddingRight = 2
       if (this.showClearIcon) {
         paddingRight += 2
       }
       return {
         paddingRight: `${paddingRight}rem`,
-        paddingTop: this.label ? '.84rem' : 'inherit',
-        paddingBottom: this.label ? 0 : '.4375rem',
+        paddingTop: this.label ? this.multiple ? '.59rem' : '.84rem' : 'inherit',
+        paddingBottom: this.label ? 0 : this.multiple ? '.3125rem' : '.4375rem',
       }
     },
-    inputStyles () {
-      return this.visible && !this.disabled
-        ? { width: '100%' }
-        : { width: '0', position: 'absolute', padding: '0' }
-    },
     valueProxy: {
       get () {
         return this.value
@@ -331,11 +333,13 @@ export default {
         this.$refs.dropdown.hide()
       }
       if (this.searchable) {
-        this.$refs.search.focus()
+        this.$refs.search.$refs.input.focus()
       }
     },
     clear () {
-      this.valueProxy = this.multiple ? [] : this.clearValue
+      this.valueProxy = this.multiple
+        ? (Array.isArray(this.clearValue) ? this.clearValue : [])
+        : this.clearValue
       this.search = ''
     },
     updateHoveredOption (option) {
@@ -368,10 +372,6 @@ export default {
   border-top-right-radius: 0.5rem;
   margin-bottom: 1rem;
 
-  &:focus {
-    outline: none;
-  }
-
   &--disabled {
     @include va-disabled()
   }
@@ -418,6 +418,7 @@ export default {
     white-space: nowrap;
     text-overflow: ellipsis;
     overflow: hidden;
+    margin: 0 .5rem;
     &:focus {
       outline: none;
     }
@@ -445,7 +446,7 @@ export default {
     position: absolute;
     top: 0;
     bottom: 0;
-    right: 1.5rem;
+    right: 2rem;
     margin: auto;
   }
 
@@ -482,6 +483,11 @@ export default {
     &.va-select__dropdown-position-top {
       box-shadow: 0 -2px 3px 0 rgba(98, 106, 119, 0.25);
     }
+
+    .va-dropdown__anchor {
+      display: block;
+    }
+
     .va-dropdown__content {
       background-color: $light-gray3;
       margin: 0;
@@ -505,6 +511,7 @@ export default {
     display: flex;
     align-items: center;
     padding: .375rem .5rem .375rem .5rem;
+    min-height: 2.25rem;
 
     &__selected-icon {
       margin-left: auto;
diff --git a/src/vuestic-theme/vuestic-components/va-select/getDemoData.js b/src/vuestic-theme/vuestic-components/va-select/getDemoData.js
new file mode 100644
index 0000000..c17b3a4
--- /dev/null
+++ b/src/vuestic-theme/vuestic-components/va-select/getDemoData.js
@@ -0,0 +1,33 @@
+export const objectOptionsList = [{ id: 1, text: 'one' }, { id: 2, text: 'two' }, { id: 3, text: 'three' }]
+
+export const iconOptionsList = [
+  {
+    text: 'item1',
+    id: 0,
+    value: 0,
+    icon: 'fa fa-address-book',
+  },
+  {
+    text: 'item2',
+    id: 1,
+    value: 1,
+    icon: 'fa fa-android',
+  },
+  {
+    text: 'item2',
+    id: 2,
+    value: 2,
+    icon: 'fa fa-android',
+  },
+  {
+    text: 'item2',
+    id: 3,
+    value: 3,
+  },
+  {
+    text: 'item2',
+    id: 4,
+    value: 4,
+    icon: 'fa fa-android',
+  },
+]
-- 
GitLab


From 67aafc1be8bb817be3f90461b5bff48d5c26bb78 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 31 Jul 2019 20:17:49 +0300
Subject: [PATCH 39/61] fix: minor

---
 src/vuestic-theme/vuestic-plugin.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/vuestic-theme/vuestic-plugin.js b/src/vuestic-theme/vuestic-plugin.js
index 1b654f8..fcc8f29 100644
--- a/src/vuestic-theme/vuestic-plugin.js
+++ b/src/vuestic-theme/vuestic-plugin.js
@@ -92,7 +92,6 @@ const VuesticPlugin = {
     [
       VaNotification,
       Breadcrumbs,
-      Chart,
       VaCheckbox,
       VaProgressBar,
       DataTable,
-- 
GitLab


From 3f4805b9556f3edeef1078d2724a0ce7330fc383 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 31 Jul 2019 22:56:22 +0300
Subject: [PATCH 40/61] fix: minor

---
 src/components/auth/login/Login.vue           |  2 +-
 src/i18n/en.json                              | 24 +++++++++----------
 .../vuestic-sass/icons/fonts.scss             | 12 ----------
 3 files changed, 13 insertions(+), 25 deletions(-)

diff --git a/src/components/auth/login/Login.vue b/src/components/auth/login/Login.vue
index 1e3c6c3..5a05909 100644
--- a/src/components/auth/login/Login.vue
+++ b/src/components/auth/login/Login.vue
@@ -6,7 +6,7 @@
         <va-input
           v-model="usernameAndInstance"
           type="email"
-          :label="$t('login-placeholder')"
+          :label="$t('pleroma.login-placeholder')"
           :error="!!error.length"
           :error-messages="[error]"
         />
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d33053b..607b0ad 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -356,18 +356,18 @@
     "password-placeholder": "password",
     "statuses": "Statuses",
     "following": "Following",
-    "followers": "Followers",
-    "no_followers": "No more followers",
-    "no_followings": "No more followings",
-    "no_followers_yet": "No followers yet",
-    "no_followings_yet": "No followings yet",
-    "no_statuses": "No more statuses",
-    "no_statuses_yet": "No statuses yet",
-    "no_users_found": "No users found",
-    "no_reports": "No more reports",
-    "no_reports_yet": "No reports yet",
-    "load_more": "Load more"
-  },
+    "followers": "Followers"
+  },
+  "no_followers": "No more followers",
+  "no_followings": "No more followings",
+  "no_followers_yet": "No followers yet",
+  "no_followings_yet": "No followings yet",
+  "no_statuses": "No more statuses",
+  "no_statuses_yet": "No statuses yet",
+  "no_users_found": "No users found",
+  "no_reports": "No more reports",
+  "no_reports_yet": "No reports yet",
+  "load_more": "Load more",
   "admin_menu": {
     "moderation": "Moderation",
     "grant_admin": "Grant Admin",
diff --git a/src/vuestic-theme/vuestic-sass/icons/fonts.scss b/src/vuestic-theme/vuestic-sass/icons/fonts.scss
index 5a907a9..485d2cd 100644
--- a/src/vuestic-theme/vuestic-sass/icons/fonts.scss
+++ b/src/vuestic-theme/vuestic-sass/icons/fonts.scss
@@ -17,15 +17,3 @@ $icon-font-name: 'glyphicons-halflings-regular';
 $icon-font-svg-id: 'glyphicons_halflingsregular';
 $icon-font-path: './../fonts/';
 
-// Fonts //
-//@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro);
-//
-//@font-face {
-//  font-family: 'Glyphicons Halflings';
-//  src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot'), '#{$icon-font-path}#{$icon-font-name}.eot'));
-//  src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot?#iefix'), '#{$icon-font-path}#{$icon-font-name}.eot?#iefix')) format('eot'),
-//  url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff2'), '#{$icon-font-path}#{$icon-font-name}.woff2')) format('woff2'),
-//  url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff'), '#{$icon-font-path}#{$icon-font-name}.woff')) format('woff'),
-//  url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.ttf'), '#{$icon-font-path}#{$icon-font-name}.ttf')) format('truetype'),
-//  url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}'), '#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}')) format('svg');
-//}
-- 
GitLab


From 47ffb8f34ce417bb8cc3486add3c7b448dc9dc35 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 2 Aug 2019 13:56:11 +0300
Subject: [PATCH 41/61] feat: Pleroma.Emails.Mailer

---
 .../emailAdapters/AmazonSESAdapter.vue        |  41 -----
 .../emailAdapters/DynAdapter.vue              |  31 ----
 .../emailAdapters/GmailAdapter.vue            |  31 ----
 .../emailAdapters/MailgunAdapter.vue          |  36 ----
 .../emailAdapters/MailjetAdapter.vue          |  36 ----
 .../emailAdapters/MandrillAdapter.vue         |  31 ----
 .../emailAdapters/PostmarkAdapter.vue         |  31 ----
 .../emailAdapters/SMTPAdapter.vue             |  83 ---------
 .../emailAdapters/SendgridAdapter.vue         |  31 ----
 .../emailAdapters/SendmailAdapter.vue         |  41 -----
 .../emailAdapters/SocketLabsAdapter.vue       |  36 ----
 .../emailAdapters/SparkPostAdapter.vue        |  36 ----
 .../configSettings/forms/EmailsForm.vue       | 173 +++++++++++-------
 .../configSettings/forms/EmojiForm.vue        |   4 +-
 .../configSettings/ConfigSettingsPage.vue     |  15 +-
 src/data/Config.ts                            |   9 +-
 src/entities/settings/EmailsConfig.ts         |  52 +++++-
 src/entities/settings/EmojiConfig.ts          |  16 +-
 src/entities/settings/GopherConfig.ts         |   8 +-
 src/entities/settings/RateLimitConfig.ts      |  44 ++---
 src/i18n/en.json                              |  30 +--
 src/utils/ConvertConfigToApiRequest.js        |   6 +-
 src/utils/GetFieldList.ts                     |  55 ++++--
 23 files changed, 265 insertions(+), 611 deletions(-)
 delete mode 100644 src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/DynAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/GmailAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/MailgunAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/MailjetAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/MandrillAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/PostmarkAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/SMTPAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/SendgridAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/SendmailAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
 delete mode 100644 src/components/configSettings/emailAdapters/SparkPostAdapter.vue

diff --git a/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue b/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
deleted file mode 100644
index 443d1ef..0000000
--- a/src/components/configSettings/emailAdapters/AmazonSESAdapter.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'AmazonSES'})}}</p>
-    <va-input
-      v-model="configProxy.region"
-      :label="$t('config_settings.emails.region')"
-      @input="(val) => configProxy = {field:'region', val}"
-    />
-    <va-input
-      v-model="configProxy.access_key"
-      @input="(val) => configProxy = {field:'access_key', val}"
-      :label="$t('config_settings.emails.access_key')"
-    />
-    <va-input
-      v-model="configProxy.secret"
-      @input="(val) => configProxy = {field:'secret', val}"
-      :label="$t('config_settings.emails.secret')"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class AmazonSESAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/DynAdapter.vue b/src/components/configSettings/emailAdapters/DynAdapter.vue
deleted file mode 100644
index 18fc0ea..0000000
--- a/src/components/configSettings/emailAdapters/DynAdapter.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Dyn'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t('config_settings.emails.api_key')"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class DynAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/GmailAdapter.vue b/src/components/configSettings/emailAdapters/GmailAdapter.vue
deleted file mode 100644
index e3efe4c..0000000
--- a/src/components/configSettings/emailAdapters/GmailAdapter.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'gmail'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t('config_settings.emails.api_key')"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class GmailAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/MailgunAdapter.vue b/src/components/configSettings/emailAdapters/MailgunAdapter.vue
deleted file mode 100644
index 98f9f45..0000000
--- a/src/components/configSettings/emailAdapters/MailgunAdapter.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Mailgun'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t('config_settings.emails.api_key')"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-    <va-input
-      v-model="configProxy.domain"
-      :label="$t('config_settings.emails.domain')"
-      @input="(val) => configProxy = {field:'domain', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class MailgunAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/MailjetAdapter.vue b/src/components/configSettings/emailAdapters/MailjetAdapter.vue
deleted file mode 100644
index 4ffdbd5..0000000
--- a/src/components/configSettings/emailAdapters/MailjetAdapter.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Mailjet'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t('config_settings.emails.api_key')"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-    <va-input
-      v-model="configProxy.secret"
-      :label="$t('config_settings.emails.secret')"
-      @input="(val) => configProxy = {field:'secret', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class MailjetAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/MandrillAdapter.vue b/src/components/configSettings/emailAdapters/MandrillAdapter.vue
deleted file mode 100644
index e66f355..0000000
--- a/src/components/configSettings/emailAdapters/MandrillAdapter.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Mandrill'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t('config_settings.emails.api_key')"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class MandrillAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/PostmarkAdapter.vue b/src/components/configSettings/emailAdapters/PostmarkAdapter.vue
deleted file mode 100644
index d18b82c..0000000
--- a/src/components/configSettings/emailAdapters/PostmarkAdapter.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Postmark'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t('config_settings.emails.api_key')"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class PostmarkAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/SMTPAdapter.vue b/src/components/configSettings/emailAdapters/SMTPAdapter.vue
deleted file mode 100644
index 148cc03..0000000
--- a/src/components/configSettings/emailAdapters/SMTPAdapter.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'SMTP'})}}</p>
-    <va-input
-      v-model="configProxy.relay"
-      :label="$t(`config_settings.emails.relay`)"
-      @input="(val) => configProxy = {field:'relay', val}"
-    />
-    <va-input
-      v-model="configProxy.username"
-      @input="(val) => configProxy = {field:'username', val}"
-      :label="$t(`config_settings.emails.username`)"
-    />
-    <va-input
-      v-model="configProxy.password"
-      @input="(val) => configProxy = {field:'password', val}"
-      type="password"
-      :label="$t(`config_settings.emails.password`)"
-    />
-    <va-input-wrapper>
-      <va-checkbox
-        v-model="configProxy.ssl"
-        @input="(val) => configProxy = {field:'ssl', val}"
-        :label="$t(`config_settings.emails.ssl`)"
-      />
-    </va-input-wrapper>
-    <va-input
-      v-model="configProxy.tls"
-      @input="(val) => configProxy = { field: 'tls', val }"
-      :label="$t(`config_settings.emails.tls`)"
-    />
-    <va-input
-      v-model="configProxy.auth"
-      @input="(val) => configProxy = { field: 'auth', val }"
-      :label="$t(`config_settings.emails.auth`)"
-    />
-    <va-input
-      v-model.number="configProxy.port"
-      type="number"
-      @input="(val) => configProxy = { field: 'port', val: +val }"
-      :label="$t(`config_settings.emails.port`)"
-    />
-    <va-input
-      v-model="configProxy.dkim"
-      @input="(val) => configProxy = { field: 'dkim', val }"
-      :label="$t(`config_settings.emails.dkim`)"
-    />
-    <va-input
-      v-model.number="configProxy.retries"
-      type="number"
-      @input="(val) => configProxy = { field: 'retries', val: +val }"
-      :label="$t(`config_settings.emails.retries`)"
-    />
-    <va-input-wrapper>
-      <va-checkbox
-        v-model="configProxy.no_mx_lookups"
-        @input="(val) => configProxy = { field: 'no_mx_lookups', val }"
-        :label="$t(`config_settings.emails.no_mx_lookups`)"
-      />
-    </va-input-wrapper>
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class SMTPAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/SendgridAdapter.vue b/src/components/configSettings/emailAdapters/SendgridAdapter.vue
deleted file mode 100644
index d591c9f..0000000
--- a/src/components/configSettings/emailAdapters/SendgridAdapter.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Sendgrid'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t('config_settings.emails.api_key')"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class SendgridAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/SendmailAdapter.vue b/src/components/configSettings/emailAdapters/SendmailAdapter.vue
deleted file mode 100644
index b5c15cf..0000000
--- a/src/components/configSettings/emailAdapters/SendmailAdapter.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'Sendmail'})}}</p>
-    <va-input
-      v-model="configProxy.cmd_path"
-      :label="$t(`config_settings.emails.cmd_path`)"
-      @input="(val) => configProxy = {field:'cmd_path', val}"
-    />
-    <va-input
-      v-model="configProxy.cmd_args"
-      :label="$t(`config_settings.emails.cmd_args`)"
-      @input="(val) => configProxy = {field:'cmd_args', val}"
-    />
-    <va-checkbox
-      v-model="configProxy.qmail"
-      :label="$t(`config_settings.emails.qmail`)"
-      @input="(val) => configProxy = {field: 'qmail', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class SendmailAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue b/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
deleted file mode 100644
index 1e36ac1..0000000
--- a/src/components/configSettings/emailAdapters/SocketLabsAdapter.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'SocketLabs'})}}</p>
-    <va-input
-      v-model="configProxy.server_id"
-      :label="$t(`config_settings.emails.server_id`)"
-      @input="(val) => configProxy = {field:'server_id', val}"
-    />
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t(`config_settings.emails.api_key`)"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class SocketLabsAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/emailAdapters/SparkPostAdapter.vue b/src/components/configSettings/emailAdapters/SparkPostAdapter.vue
deleted file mode 100644
index f2f940b..0000000
--- a/src/components/configSettings/emailAdapters/SparkPostAdapter.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-  <div class="mx-4 my-3">
-    <p class="title">{{$t(`config_settings.emails.adapter_title`, { name: 'SparkPost'})}}</p>
-    <va-input
-      v-model="configProxy.api_key"
-      :label="$t(`config_settings.emails.api_key`)"
-      @input="(val) => configProxy = {field:'api_key', val}"
-    />
-    <va-input
-      v-model="configProxy.endpoint"
-      :label="$t(`config_settings.emails.endpoint`)"
-      @input="(val) => configProxy = {field:'endpoint', val}"
-    />
-  </div>
-</template>
-
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-
-@Component({
-  components: {},
-})
-export default class SparkPostAdapter extends Vue {
-  @Prop(Object) config!: object
-  get configProxy () {
-    return this.config
-  }
-  set configProxy (value) {
-    this.$emit('configChanged', value)
-  }
-}
-</script>
-
-<style scoped lang="scss">
-
-</style>
diff --git a/src/components/configSettings/forms/EmailsForm.vue b/src/components/configSettings/forms/EmailsForm.vue
index 56b3683..17de4d4 100644
--- a/src/components/configSettings/forms/EmailsForm.vue
+++ b/src/components/configSettings/forms/EmailsForm.vue
@@ -1,91 +1,134 @@
 <template>
-  <div>
-    <va-select v-model="formData.adapter" label="Adapter" :options="selectOptions.adapter" keyBy="value"/>
-    <component
-      :is="adapterComponent"
-      v-if="isAdapterComponentShouldBeRendered"
-      @configChanged="configChanged"
-      :config="adapterData"
-    />
+  <div class="emails-form">
+    <va-input-wrapper>
+      <va-checkbox
+        v-model="formData.enabled"
+        :label="$t('config_settings.Pleroma.Emails.Mailer_form.enabled')"
+      />
+    </va-input-wrapper>
+    <va-input-wrapper>
+      <va-select
+        v-model="formData.adapter"
+        :label="$t('config_settings.Pleroma.Emails.Mailer_form.adapter')"
+        :options="selectOptions"
+      />
+    </va-input-wrapper>
+    <div>
+      <p class="title">{{$t('config_settings.Pleroma.Emails.Mailer_form.adapters_options')}}</p>
+      <div
+        class="va-row"
+        v-for="(option, index) in formData.adapterOptions"
+        :key="index"
+      >
+        <va-input
+          v-model="formData.adapterOptions[index].name"
+          :label="$t('config_settings.Pleroma.Emails.Mailer_form.option_name')"
+          class="emails-form__group-name"
+        />
+        <component
+          v-if="formData.adapterOptions[index].type.isNumber"
+          :is="formData.adapterOptions[index].type.component"
+          v-model.number="formData.adapterOptions[index].value"
+          type="number"
+          :label="$t('config_settings.Pleroma.Emails.Mailer_form.option_value')"
+          class="emails-form__group-value mb-3"
+        />
+        <component
+          v-else
+          :is="formData.adapterOptions[index].type.component"
+          v-model="formData.adapterOptions[index].value"
+          :label="$t('config_settings.Pleroma.Emails.Mailer_form.option_value')"
+          class="emails-form__group-value mb-3"
+        />
+        <va-icon
+          color="danger"
+          @click.native="removeOption(index)"
+          name="ion-ios-trash-outline"
+          class="px-2 emails-form__delete"
+        />
+      </div>
+      <va-button @click="addOption" outline small class="ma-0">
+        {{$t('config_settings.Pleroma.Emails.Mailer_form.add_adapted_option')}}
+      </va-button>
+      <va-modal
+        v-model="isModalShow"
+        @ok="onAddOptionConfirm"
+        >
+        <va-select
+          v-model="newComponentType"
+          :options="newComponentOptions"
+          :label="$t('config_settings.Pleroma.Emails.Mailer_form.choose_type_of_field')"
+        />
+      </va-modal>
+    </div>
   </div>
 </template>
 
 <script lang="ts">
 import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
-import _ from 'lodash'
-import EmailsConfig from '../../../entities/settings/EmailsConfig'
-import SMTPAdapter from '../emailAdapters/SMTPAdapter.vue'
-import SendgridAdapter from '../emailAdapters/SendgridAdapter.vue'
-import SendmailAdapter from '../emailAdapters/SendmailAdapter.vue'
-import MandrillAdapter from '../emailAdapters/MandrillAdapter.vue'
-import MailgunAdapter from '../emailAdapters/MailgunAdapter.vue'
-import MailjetAdapter from '../emailAdapters/MailjetAdapter.vue'
-import PostmarkAdapter from '../emailAdapters/PostmarkAdapter.vue'
-import SparkPostAdapter from '../emailAdapters/SparkPostAdapter.vue'
-import DynAdapter from '../emailAdapters/DynAdapter.vue'
-import SocketLabsAdapter from '../emailAdapters/SocketLabsAdapter.vue'
-import GmailAdapter from '../emailAdapters/GmailAdapter.vue'
-import AmazonSESAdapter from '../emailAdapters/AmazonSESAdapter.vue'
+import { getFieldComponent, selectOptions } from '../../../utils/GetFieldList'
+import EmailsConfig, { AdapterOption } from '../../../entities/settings/EmailsConfig'
 
 @Component({
-  components: {
-    SMTPAdapter,
-    SendgridAdapter,
-    SendmailAdapter,
-    MandrillAdapter,
-    MailgunAdapter,
-    MailjetAdapter,
-    PostmarkAdapter,
-    SparkPostAdapter,
-    DynAdapter,
-    SocketLabsAdapter,
-    GmailAdapter,
-    AmazonSESAdapter,
-  },
+  components: {},
 })
 export default class EmailsForm extends Vue {
   @Prop(Object) readonly value!: EmailsConfig
+  selectOptions = selectOptions.adapter
+  newComponentOptions = [
+    { text: 'string', id: 'text' },
+    { text: 'number', id: 1 },
+    { text: 'boolean', id: true }
+  ]
+  newComponentType:any = { text: 'string', id: 'text' }
+  isModalShow:boolean = false
   get formData () {
     return this.value
   }
   set formData (val) {
     this.$emit('updateForm', val)
   }
-  adapterData = {}
-  selectOptions = {
-    adapter: [
-      { text: 'SMTP', value: 'Swoosh.Adapters.SMTP' },
-      { text: 'Sendgrid', value: 'Swoosh.Adapters.Sendgrid' },
-      { text: 'Sendmail', value: 'Swoosh.Adapters.Sendmail' },
-      { text: 'Mandrill', value: 'Swoosh.Adapters.Mandrill' },
-      { text: 'Mailgun', value: 'Swoosh.Adapters.Mailgun' },
-      { text: 'Mailjet', value: 'Swoosh.Adapters.Mailjet' },
-      { text: 'Postmark', value: 'Swoosh.Adapters.Postmark' },
-      { text: 'SparkPost', value: 'Swoosh.Adapters.SparkPost' },
-      { text: 'Amazon SES', value: 'Swoosh.Adapters.AmazonSES' },
-      { text: 'Dyn', value: 'Swoosh.Adapters.Dyn' },
-      { text: 'SocketLabs', value: 'Swoosh.Adapters.SocketLabs' },
-      { text: 'Gmail', value: 'Swoosh.Adapters.Gmail' },
-    ]
+  @Watch('formData.adapter') setDefaultAdapterOption () {
+    this.formData.adapterOptions = [new AdapterOption({ type: getFieldComponent('text') })]
   }
-  @Watch('formData.adapter')
-  onAdapterDataChanged () {
-    this.adapterData = {}
+  removeOption (index) {
+    this.formData.adapterOptions.splice(index, 1)
   }
-  get isAdapterComponentShouldBeRendered () {
-    return !_.isEmpty(this.formData.adapter)
+  addOption () {
+    this.isModalShow = true
   }
-  get adapterComponent () {
-    return this.formData.adapter.value === 'Swoosh.Adapters.AmazonSES'
-      ? 'AmazonSESAdapter'
-      : `${this.formData.adapter.text}Adapter`
-  }
-  configChanged ({ field, val }) {
-    this.adapterData[field] = val
+  onAddOptionConfirm () {
+    this.formData.adapterOptions.push(new AdapterOption({ type: getFieldComponent(this.newComponentType.id) }))
   }
 }
 </script>
 
 <style scoped lang="scss">
-
+.emails-form {
+  .va-row {
+    margin-left: 0 !important;
+    margin-right: 0 !important;
+    position: relative;
+  }
+  &__delete {
+    font-size: 1.5rem;
+    cursor: pointer;
+    @include media-breakpoint-down(sm) {
+      top: 0;
+      bottom: 0;
+      height: 1.5rem;
+      margin: auto;
+      right: 0;
+      position: absolute;
+    }
+  }
+  &__group-name, &__group-value {
+    padding-right: .5rem;
+    width: calc(50% - 15px);
+    @include media-breakpoint-down(sm) {
+      padding-right: 30px;
+      width: calc(100% - 30px);
+    }
+  }
+}
 </style>
diff --git a/src/components/configSettings/forms/EmojiForm.vue b/src/components/configSettings/forms/EmojiForm.vue
index 5d348c5..fe2ea13 100644
--- a/src/components/configSettings/forms/EmojiForm.vue
+++ b/src/components/configSettings/forms/EmojiForm.vue
@@ -10,7 +10,7 @@
           color="danger"
           @click.native="removeShortcodeGlob(index)"
           name="ion-ios-trash-outline"
-          class="px-2 emoji-form__delete"
+          class="pl-2 emoji-form__delete"
         />
       </div>
       <div class="va-row mb-2">
@@ -35,7 +35,7 @@
           color="danger"
           @click.native="removeGroup(index)"
           name="ion-ios-trash-outline"
-          class="px-2 emoji-form__delete"
+          class="pl-2 emoji-form__delete"
         />
       </div>
       <div class="va-row mb-2">
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 831c972..9a30679 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -9,7 +9,6 @@
       </va-tab>
     </va-tabs>
     <div class="config-settings-page__content pt-4">
-      <!--<emails-form v-if="configKeysTabs[value].key === configKeysEnum.EMAILS" v-model="emails"/>-->
       <div v-if="config">
         <template v-for="tab in tabs">
           <config-form
@@ -17,9 +16,10 @@
             v-model="config[tab.key]"
             :title="tab.key"
             :showTitle="tabs[value].key === configKeysEnum.LDAP"
-            v-if="tabs[value].key === tab.key && tab.key !== configKeysEnum.AUTO_LINKER"
+            v-if="showTab(tab)"
           />
         </template>
+        <emails-form v-if="tabs[value].key === configKeysEnum.EMAILS" v-model="config[configKeysEnum.EMAILS]"/>
         <config-form
           :title="configKeysEnum.UPLOADERSS3"
           v-if="tabs[value].key === configKeysEnum.UPLOAD"
@@ -65,7 +65,7 @@
       </div>
     </div>
     <div class="flex-center pb-4">
-      <va-button @click="onSaveButtunClick">Save settings</va-button>
+      <va-button @click="onSaveButtonClick">Save settings</va-button>
     </div>
   </va-card>
 </template>
@@ -80,7 +80,7 @@ import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
 import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 import AutoLinkerForm from '../../configSettings/forms/AutoLinkerForm.vue'
-import EmojiForm from "../../configSettings/forms/EmojiForm.vue";
+import EmojiForm from '../../configSettings/forms/EmojiForm.vue'
 
 @Component({
   components: {
@@ -113,7 +113,7 @@ export default class ConfigSettingsPage extends Vue {
       this.loading = false
     }
   }
-  async onSaveButtunClick () {
+  async onSaveButtonClick () {
     this.loading = true
     try {
       const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest(this.config))
@@ -128,6 +128,11 @@ export default class ConfigSettingsPage extends Vue {
     const config = ConvertConfigToState(configs)
     this.config = config
   }
+  showTab ({ key }) {
+    return this.tabs[this.value].key === key &&
+      key !== this.configKeysEnum.AUTO_LINKER &&
+      key !== this.configKeysEnum.EMAILS
+  }
   get tabs () {
     return configKeysTabs.filter(({ tab }) => tab)
   }
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 91f78ad..70be21d 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -23,12 +23,13 @@ import RateLimitConfig from '../entities/settings/RateLimitConfig'
 import ChatConfig from '../entities/settings/ChatConfig'
 import GopherConfig from '../entities/settings/GopherConfig'
 import EmojiConfig from '../entities/settings/EmojiConfig'
+import EmailsConfig from "../entities/settings/EmailsConfig";
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
   UPLOADERSS3 = 'Pleroma.Uploaders.S3',
   UPLOADERSLOCAL = 'Pleroma.Uploaders.Local',
-  EMAILS = 'Pleroma.Emails',
+  EMAILS = 'Pleroma.Emails.Mailer',
   URI_SCHEMES = ':uri_schemes',
   INSTANCE = ':instance',
   LOGGER = ':logger',
@@ -64,6 +65,12 @@ export const arrayParams = {
 }
 
 export const configKeysTabs = [
+  {
+    key: configKeys.EMAILS,
+    tab: true,
+    name: 'Emails',
+    constructor: EmailsConfig,
+  },
   {
     key: configKeys.UPLOADERSS3,
     tab: false,
diff --git a/src/entities/settings/EmailsConfig.ts b/src/entities/settings/EmailsConfig.ts
index 254beb3..6775821 100644
--- a/src/entities/settings/EmailsConfig.ts
+++ b/src/entities/settings/EmailsConfig.ts
@@ -1,7 +1,51 @@
-class AdapterOptionObject {
-  text?: string
-  value?: string
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import t from 'typy'
+import { forEach } from 'lodash'
+import {getFieldComponent} from "../../utils/GetFieldList";
+
+export class AdapterOption {
+  constructor(options?) {
+    if (options) {
+      this.type = options.type || { component: 'va-input' }
+      switch (t(options, 'type.component').safeObject) {
+        case 'va-checkbox': {
+          this.value = options.value ? options.value : true
+          break
+        }
+        case 'va-input': {
+          this.value = options.value ? options.value : t(options, 'type.isNumber').safeObject ? 0 : ''
+        }
+      }
+      this.name = options.name ? options.name : ''
+    }
+  }
+  name: any = ''
+  value: any = ''
+  type: any = { component: 'va-input' }
 }
 export default class EmailsConfig {
-  adapter: AdapterOptionObject = new AdapterOptionObject()
+  constructor (existConfig? ){
+    normalizeApiConfig(existConfig, this)
+    forEach(existConfig, ({ tuple }) => {
+      const key = tuple[0].substring(1)
+      const value = tuple[1]
+      if (key !== 'adapter' && key !== 'enabled') {
+        this.adapterOptions.push(new AdapterOption({ type: getFieldComponent(value), value, name: key }))
+      }
+    })
+  }
+  adapter: string = ''
+  enabled: boolean = false
+  adapterOptions: Array<AdapterOption>= []
+}
+
+export const normalizeEmailsConfig = (config) => {
+  const apiConf = { ...config }
+  apiConf.adapterOptions
+    .filter(({ name, value }) => name.length)
+    .forEach(({ name, value }) => {
+      apiConf[name] = value
+    })
+  delete apiConf.adapterOptions
+  return apiConf
 }
diff --git a/src/entities/settings/EmojiConfig.ts b/src/entities/settings/EmojiConfig.ts
index de2a981..7f042d4 100644
--- a/src/entities/settings/EmojiConfig.ts
+++ b/src/entities/settings/EmojiConfig.ts
@@ -4,13 +4,15 @@ import {arrayParams, configKeys} from '../../data/Config'
 
 export default class EmojiConfig {
   constructor(existConfig?) {
-    const groups = existConfig.find(({ tuple }) => tuple[0] === ':groups')
-    const newGroupsList:any = []
-    groups.tuple[1].forEach(({ tuple }) => {
-      tuple[1] = tuple[1].join(';')
-      newGroupsList.push(tuple)
-    })
-    groups.tuple[1] = newGroupsList
+    if (existConfig) {
+      const groups = existConfig.find(({ tuple }) => tuple[0] === ':groups')
+      const newGroupsList:any = []
+      groups.tuple[1].forEach(({ tuple }) => {
+        tuple[1] = tuple[1].join(';')
+        newGroupsList.push(tuple)
+      })
+      groups.tuple[1] = newGroupsList
+    }
     normalizeApiConfig(existConfig, this, arrayParams[configKeys.EMOJI])
   }
   shortcode_globs: any = []
diff --git a/src/entities/settings/GopherConfig.ts b/src/entities/settings/GopherConfig.ts
index f0ea14c..44b2f41 100644
--- a/src/entities/settings/GopherConfig.ts
+++ b/src/entities/settings/GopherConfig.ts
@@ -3,9 +3,11 @@ import {arrayParams, configKeys} from '../../data/Config'
 
 export default class GopherConfig {
   constructor(existConfig?) {
-    const ip = existConfig.find(({ tuple }) => tuple[0] === ':ip')
-    if (ip) {
-      ip.tuple[1] = ip.tuple[1].tuple
+    if (existConfig) {
+      const ip = existConfig.find(({ tuple }) => tuple[0] === ':ip')
+      if (ip) {
+        ip.tuple[1] = ip.tuple[1].tuple
+      }
     }
     normalizeApiConfig(existConfig, this, arrayParams[configKeys.GOPHER])
   }
diff --git a/src/entities/settings/RateLimitConfig.ts b/src/entities/settings/RateLimitConfig.ts
index 6b7f29b..f05f655 100644
--- a/src/entities/settings/RateLimitConfig.ts
+++ b/src/entities/settings/RateLimitConfig.ts
@@ -3,29 +3,31 @@ import { forIn } from 'lodash'
 
 export default class RateLimitConfig {
   constructor(existConfig?) {
-    existConfig.forEach(({ tuple }) => {
-      const name = tuple[0].substring(1)
-      if (name !== 'search') {
-        const fieldConfig = t(tuple[1], 'tuple').safeObject
-        if (fieldConfig[0]) {
-          this[name].scale = fieldConfig[0]
-        }
-        if (fieldConfig[1]) {
-          this[name].limit = fieldConfig[1]
-        }
-      } else {
-        tuple[1].forEach((search, ind) => {
-          const name = ind === 0 ? 'account' : 'status'
-          const val = search.tuple
-          if (val[0]) {
-            this.search[name].scale = val[0]
+    if (existConfig) {
+      existConfig.forEach(({ tuple }) => {
+        const name = tuple[0].substring(1)
+        if (name !== 'search') {
+          const fieldConfig = t(tuple[1], 'tuple').safeObject
+          if (fieldConfig[0]) {
+            this[name].scale = fieldConfig[0]
           }
-          if (val[1]) {
-            this.search[name].limit = val[1]
+          if (fieldConfig[1]) {
+            this[name].limit = fieldConfig[1]
           }
-        })
-      }
-    })
+        } else {
+          tuple[1].forEach((search, ind) => {
+            const name = ind === 0 ? 'account' : 'status'
+            const val = search.tuple
+            if (val[0]) {
+              this.search[name].scale = val[0]
+            }
+            if (val[1]) {
+              this.search[name].limit = val[1]
+            }
+          })
+        }
+      })
+    }
   }
   search:any = {
     account: { scale: 1000, limit: 10 },
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 607b0ad..3ff5243 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -471,28 +471,14 @@
     "Pleroma.Uploaders.MDII_form": {
 
     },
-    "emails": {
-      "adapter_title": "{name} adapter config",
-      "region": "Region",
-      "access_key": "Access key",
-      "secret": "Secret",
-      "api_key": "Api key",
-      "domain": "Domain",
-      "cmd_path": "cmd_path",
-      "cmd_args": "cmd_args",
-      "qmail": "qmail",
-      "relay": "relay",
-      "username": "username",
-      "password": "password",
-      "ssl": "ssl",
-      "tls": "tls",
-      "auth": "auth",
-      "port": "port",
-      "dkim": "dkim",
-      "retries": "retries",
-      "no_mx_lookups": "no_mx_lookups",
-      "server_id": "server id",
-      "endpoint": "endpoint"
+    "Pleroma.Emails.Mailer_form": {
+      "adapters_options": "Adapter's option",
+      "add_adapted_option": "Add adapter option",
+      "option_name": "Name",
+      "option_value": "Value",
+      "enabled": "Enabled",
+      "adapter": "Adapter",
+      "choose_type_of_field": "Choose type of field"
     },
     ":uri_schemes_form": {
       "valid_schemes": "Valid schemes",
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index c8cfd34..bd549d7 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -3,7 +3,8 @@ import { arrayParams, configKeys } from '../data/Config'
 import { normalizeAutoLinkerConfig } from '../entities/settings/AutoLinkerConfig'
 import { normalizeRateLimitConfig } from '../entities/settings/RateLimitConfig'
 import { normalizeGopherConfig } from '../entities/settings/GopherConfig'
-import {normalizeEmojiConfig} from '../entities/settings/EmojiConfig'
+import { normalizeEmojiConfig } from '../entities/settings/EmojiConfig'
+import { normalizeEmailsConfig } from '../entities/settings/EmailsConfig'
 
 export default (configs) => {
   const settings = []
@@ -18,6 +19,9 @@ export default (configs) => {
     if (key === configKeys.GOPHER) {
       newVal = normalizeGopherConfig(newVal)
     }
+    if (key === configKeys.EMAILS) {
+      newVal = normalizeEmailsConfig(newVal)
+    }
     newVal = key !== configKeys.RATE_LIMIT ? getConfigValue(newVal) : normalizeRateLimitConfig(newVal)
     if (key === configKeys.EMOJI) {
       newVal = normalizeEmojiConfig(newVal)
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 416e39e..03663a4 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -1,6 +1,21 @@
 import { forIn, get } from 'lodash'
 
 export const selectOptions = {
+  adapter: [
+    'Swoosh.Adapters.Local',
+    'Swoosh.Adapters.SMTP',
+    'Swoosh.Adapters.Sendgrid',
+    'Swoosh.Adapters.Sendmail',
+    'Swoosh.Adapters.Mandrill',
+    'Swoosh.Adapters.Mailgun',
+    'Swoosh.Adapters.Mailjet',
+    'Swoosh.Adapters.Postmark',
+    'Swoosh.Adapters.SparkPost',
+    'Swoosh.Adapters.AmazonSES',
+    'Swoosh.Adapters.Dyn',
+    'Swoosh.Adapters.SocketLabs',
+    'Swoosh.Adapters.Gmail',
+  ],
   rewrite_policy: [
     'Pleroma.Web.ActivityPub.MRF.NoOpPolicy',
     'Pleroma.Web.ActivityPub.MRF.DropPolicy',
@@ -34,25 +49,33 @@ export default (formData) => {
     if (key === 'sendAsMap') {
       return
     }
-    const field: any= {
-      component: '',
-      model: key,
-    }
-    if (selectOptions[key]) {
-      field.component = 'va-select'
-      if (Array.isArray(val)){
-        field.multiple = true
-      }
-    } else if (typeof val === 'string' || typeof val === 'number') {
-      field.component = 'va-input'
-    } else if (typeof val === 'boolean') {
-      field.component = 'va-checkbox'
-    } else if (typeof val === 'object' && val){
-      field.component = 'parent'
-    }
+    const field = getFieldComponent(val, key)
     if (field.component) {
       list.push(field)
     }
   })
   return list
 }
+
+export const getFieldComponent = (val, key?) => {
+  const field: any= {
+    component: '',
+    model: key,
+  }
+  if (key && selectOptions[key]) {
+    field.component = 'va-select'
+    if (Array.isArray(val)){
+      field.multiple = true
+    }
+  } else if (typeof val === 'string'){
+    field.component = 'va-input'
+  } else if (typeof val === 'number') {
+    field.component = 'va-input'
+    field.isNumber = true
+  } else if (typeof val === 'boolean') {
+    field.component = 'va-checkbox'
+  } else if (typeof val === 'object' && val){
+    field.component = 'parent'
+  }
+  return field
+}
-- 
GitLab


From 4c97e848e4da1b0a0437f81187dde3f058c835f3 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 5 Aug 2019 17:41:35 +0300
Subject: [PATCH 42/61] feat: :assets

---
 .../configSettings/forms/AssetsForm.vue       | 119 ++++++++++++++++++
 .../configSettings/forms/AutoLinkerForm.vue   |   1 -
 .../configSettings/ConfigSettingsPage.vue     |  31 +++--
 src/data/Config.ts                            |  10 +-
 src/entities/settings/AssetsConfig.ts         |  30 +++++
 src/i18n/en.json                              |  10 ++
 src/services/ConfigService.ts                 |   4 +
 src/utils/ConvertConfigToApiRequest.js        |   4 +
 8 files changed, 200 insertions(+), 9 deletions(-)
 create mode 100644 src/components/configSettings/forms/AssetsForm.vue
 create mode 100644 src/entities/settings/AssetsConfig.ts

diff --git a/src/components/configSettings/forms/AssetsForm.vue b/src/components/configSettings/forms/AssetsForm.vue
new file mode 100644
index 0000000..8387c63
--- /dev/null
+++ b/src/components/configSettings/forms/AssetsForm.vue
@@ -0,0 +1,119 @@
+<template>
+  <div class="assets-form">
+    <p class="title">{{$t('config_settings.:assets_form.mascots')}}</p>
+    <div class="va-row" v-for="(mascot, index) in formData.mascots" :key="index">
+      <va-input
+        v-model="formData.mascots[index].name"
+        :label="$t('config_settings.:assets_form.name')"
+        :error="!!error.mascots[index]"
+        class="assets-form__col"
+        :class="{ 'mb-0': !!error.mascots[index] }"
+      />
+      <va-input
+        v-model="formData.mascots[index].mime_type"
+        :label="$t('config_settings.:assets_form.mime_type')"
+        class="assets-form__col"
+        :error="!!error.mascots[index]"
+        :class="{ 'mb-0':!!error.mascots[index] }"
+      />
+      <va-input
+        v-model="formData.mascots[index].url"
+        :label="$t('config_settings.:assets_form.url')"
+        class="assets-form__col"
+        :error="!!error.mascots[index]"
+        :class="{ 'mb-0':!!error.mascots[index] }"
+      />
+      <va-icon
+        color="danger"
+        @click.native="removeMascots(index)"
+        name="ion-ios-trash-outline"
+        class="px-2 assets-form__delete"
+      />
+      <span class="assets-form__error pl-2" v-if="!!error.mascots[index]">
+        {{$t(`config_settings.:assets_form.${error.mascots[index]}`)}}
+      </span>
+    </div>
+    <va-button
+      small outline
+      @click="addMascot"
+      class="mx-0 mb-3"
+    >{{$t('config_settings.:assets_form.add_mascots')}}</va-button>
+    <va-select
+      :options="selectOptions"
+      :label="$t('config_settings.:assets_form.default_mascot')"
+      v-model="formData.default_mascot"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import { keys, isEmpty } from 'lodash'
+import AssetsConfig from '../../../entities/settings/AssetsConfig'
+
+@Component({
+  components: {},
+})
+export default class AssetsForm extends Vue {
+  @Prop(Object) readonly value!: AssetsConfig
+  @Prop(String) readonly title!: string
+  @Prop(Boolean) readonly showTitle!: boolean
+  @Prop(String) readonly margin!: string
+  get selectOptions () {
+    return this.formData.mascots.map(({ name }) => name)
+  }
+  error: object = { mascots: [] }
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  addMascot () {
+    this.formData.mascots.push({ name: '', mime_type: '', url: '' })
+  }
+  removeMascots (index) {
+    this.formData.mascots.splice(index, 1)
+  }
+  // public method
+  validate () {
+    this.error.mascots = this.formData.mascots.map((item) => (!item.name.length || !item.url.length || !item.mime_type.length ? 'mascots_error' : ''))
+    return this.error.mascots.findIndex(error => error.length) === -1
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.assets-form {
+  .va-row {
+    position: relative;
+  }
+  &__col {
+    width: 30%;
+    padding-right: .5rem;
+    padding-left: .5rem;
+    @include media-breakpoint-down(xs) {
+      width: calc(100% - 30px);
+      padding-right: 30px;
+      padding-left: 0;
+    }
+  }
+  &__delete {
+    font-size: 1.5rem;
+    cursor: pointer;
+    height: 1.5rem;
+    @include media-breakpoint-down(xs) {
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      margin: auto;
+      right: 0;
+    }
+  }
+  &__error {
+    color: $danger !important;
+    @include va-title;
+    margin-bottom: .5rem;
+  }
+}
+</style>
diff --git a/src/components/configSettings/forms/AutoLinkerForm.vue b/src/components/configSettings/forms/AutoLinkerForm.vue
index 6072577..8db50c4 100644
--- a/src/components/configSettings/forms/AutoLinkerForm.vue
+++ b/src/components/configSettings/forms/AutoLinkerForm.vue
@@ -25,7 +25,6 @@
 
   </div>
 </template>
-<!--rel: false-->
 
 <script lang="ts">
 import { Component, Prop, Vue } from 'vue-property-decorator'
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 9a30679..55abb61 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -44,6 +44,12 @@
           v-model="config[configKeysEnum.EMOJI]"
           showTitle
         />
+        <assets-form
+          :title="configKeysEnum.ASSETS"
+          v-show="tabs[value].key === configKeysEnum.ASSETS"
+          v-model="config[configKeysEnum.ASSETS]"
+          ref="ASSETS"
+        />
         <config-form
           :title="configKeysEnum.KOCAPTCHA"
           v-if="tabs[value].key === configKeysEnum.CAPTCHA"
@@ -81,6 +87,7 @@ import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
 import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 import AutoLinkerForm from '../../configSettings/forms/AutoLinkerForm.vue'
 import EmojiForm from '../../configSettings/forms/EmojiForm.vue'
+import AssetsForm from '../../configSettings/forms/AssetsForm.vue'
 
 @Component({
   components: {
@@ -88,6 +95,7 @@ import EmojiForm from '../../configSettings/forms/EmojiForm.vue'
     AutoLinkerForm,
     ConfigForm,
     EmailsForm,
+    AssetsForm,
     FulfillingBouncingCircleSpinner
   },
 })
@@ -115,12 +123,17 @@ export default class ConfigSettingsPage extends Vue {
   }
   async onSaveButtonClick () {
     this.loading = true
-    try {
-      const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest(this.config))
-      this.loadConfigs(configs)
-    } catch (e) {
-      (this as any).$toasted.show(e)
-    } finally {
+    if (this.validateForms()) {
+      try {
+        const { configs } = await ConfigService.updateConfigSettings(ConvertConfigToApiRequest(this.config))
+        this.loadConfigs(configs)
+      } catch (e) {
+        (this as any).$toasted.show(e)
+      } finally {
+        this.loading = false
+      }
+    } else {
+      (this as any).$toasted.show('Something went wrong')
       this.loading = false
     }
   }
@@ -131,11 +144,15 @@ export default class ConfigSettingsPage extends Vue {
   showTab ({ key }) {
     return this.tabs[this.value].key === key &&
       key !== this.configKeysEnum.AUTO_LINKER &&
-      key !== this.configKeysEnum.EMAILS
+      key !== this.configKeysEnum.EMAILS &&
+      key !== this.configKeysEnum.ASSETS
   }
   get tabs () {
     return configKeysTabs.filter(({ tab }) => tab)
   }
+  validateForms () {
+    return this.$refs.ASSETS.validate()
+  }
 }
 </script>
 
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 70be21d..a0387f8 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -23,7 +23,8 @@ import RateLimitConfig from '../entities/settings/RateLimitConfig'
 import ChatConfig from '../entities/settings/ChatConfig'
 import GopherConfig from '../entities/settings/GopherConfig'
 import EmojiConfig from '../entities/settings/EmojiConfig'
-import EmailsConfig from "../entities/settings/EmailsConfig";
+import EmailsConfig from '../entities/settings/EmailsConfig'
+import AssetsConfig from '../entities/settings/AssetsConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -36,6 +37,7 @@ export enum configKeys {
   FRONTEND_CONFIGURATIONS = ':frontend_configurations',
   CHAT = ':chat',
   EMOJI = ':emoji',
+  ASSETS = ':assets',
   WEB = 'Pleroma.Web',
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
@@ -118,6 +120,12 @@ export const configKeysTabs = [
     tab: false,
     constructor: EmojiConfig,
   },
+  {
+    key: configKeys.ASSETS,
+    name: 'Assets',
+    tab: true,
+    constructor: AssetsConfig,
+  },
   {
     key: configKeys.CAPTCHA,
     name: 'Captcha',
diff --git a/src/entities/settings/AssetsConfig.ts b/src/entities/settings/AssetsConfig.ts
new file mode 100644
index 0000000..c93e823
--- /dev/null
+++ b/src/entities/settings/AssetsConfig.ts
@@ -0,0 +1,30 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import { forIn } from 'lodash'
+
+export default class AssetsConfig {
+  constructor(existConfig? ){
+    normalizeApiConfig(existConfig, this)
+    if (existConfig) {
+      const mascots = existConfig.find(({ tuple }) => tuple[0] === ':mascots').tuple[1]
+      this.mascots = mascots.map(({ tuple }) => ({
+          name: tuple[0],
+          mime_type: tuple[1].mime_type,
+          url: tuple[1].url
+        })
+      )
+    }
+  }
+  mascots: Array<any> = []
+  default_mascot: string = ''
+}
+
+export const normalizeAssetsConfig = (config) => {
+  const apiConfig = { ...config }
+  const newMascots = []
+  forIn(apiConfig.mascots, ({ name, mime_type, url }) => {
+    // @ts-ignore
+    newMascots.push({tuple: [name, {':mime_type': mime_type, ':url': url}]})
+  })
+  apiConfig.mascots = newMascots
+  return apiConfig
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 3ff5243..5759da3 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -875,6 +875,16 @@
       "default_manifest_note": "This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays)",
       "groups": "Emoji groups",
       "groups_note": " Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. * can be used as a wildcard"
+    },
+    ":assets_form": {
+      "title": "This section configures assets to be used with various frontends. Currently the only option relates to mascots on the mastodon frontend",
+      "mascots": "Keyword list of mascots",
+      "name": "Name",
+      "mime_type": "Mime type",
+      "url": "URL",
+      "mascots_error": "MUST contain both a url and a mime_type key",
+      "default_mascot": "Default mascot on MastoFE",
+      "add_mascots": "Add mascot"
     }
   }
 }
diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts
index 9a42ecd..d6447c9 100644
--- a/src/services/ConfigService.ts
+++ b/src/services/ConfigService.ts
@@ -9,6 +9,10 @@ export class ConfigService {
   }
 
   static updateConfigSettings (configs) {
+    // console.log(configs)
+    // return new Promise(res => {
+    //   setTimeout(() => res({}), 1000)
+    // })
     return executeApiRequest('post', urlBuilder(Url.configSettings, {}), {data: configs})
   }
 
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index bd549d7..8dc20ce 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -5,6 +5,7 @@ import { normalizeRateLimitConfig } from '../entities/settings/RateLimitConfig'
 import { normalizeGopherConfig } from '../entities/settings/GopherConfig'
 import { normalizeEmojiConfig } from '../entities/settings/EmojiConfig'
 import { normalizeEmailsConfig } from '../entities/settings/EmailsConfig'
+import { normalizeAssetsConfig } from '../entities/settings/AssetsConfig'
 
 export default (configs) => {
   const settings = []
@@ -22,6 +23,9 @@ export default (configs) => {
     if (key === configKeys.EMAILS) {
       newVal = normalizeEmailsConfig(newVal)
     }
+    if (key === configKeys.ASSETS) {
+      newVal = normalizeAssetsConfig(newVal)
+    }
     newVal = key !== configKeys.RATE_LIMIT ? getConfigValue(newVal) : normalizeRateLimitConfig(newVal)
     if (key === configKeys.EMOJI) {
       newVal = normalizeEmojiConfig(newVal)
-- 
GitLab


From c23819017efa5966c0b0f334d492ef0bfa837ce1 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 5 Aug 2019 18:12:08 +0300
Subject: [PATCH 43/61] feat: :mrf_simple

---
 src/data/Config.ts                 | 20 +++++++++++++++++++-
 src/entities/settings/MRFSimple.ts | 16 ++++++++++++++++
 src/i18n/en.json                   | 18 ++++++++++++++++++
 3 files changed, 53 insertions(+), 1 deletion(-)
 create mode 100644 src/entities/settings/MRFSimple.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index a0387f8..8faaf78 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -25,6 +25,7 @@ import GopherConfig from '../entities/settings/GopherConfig'
 import EmojiConfig from '../entities/settings/EmojiConfig'
 import EmailsConfig from '../entities/settings/EmailsConfig'
 import AssetsConfig from '../entities/settings/AssetsConfig'
+import MRFSimple from '../entities/settings/MRFSimple'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -54,7 +55,8 @@ export enum configKeys {
   AUTO_LINKER = ':auto_linker',
   SCHEDULED_ACTIVITY = 'Pleroma.ScheduledActivity',
   OAUTH2 = ':oauth2',
-  RATE_LIMIT = ':rate_limit'
+  RATE_LIMIT = ':rate_limit',
+  MRF_SIMPLE = ':mrf_simple',
 }
 
 export const arrayParams = {
@@ -64,6 +66,16 @@ export const arrayParams = {
   [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld', 'ttl_setters'],
   [configKeys.GOPHER]: ['ip'],
   [configKeys.EMOJI]: ['pack_extensions'],
+  [configKeys.MRF_SIMPLE]: [
+    'media_removal',
+    'media_nsfw',
+    'federated_timeline_removal',
+    'reject',
+    'accept',
+    'report_removal',
+    'avatar_removal',
+    'banner_removal'
+  ]
 }
 
 export const configKeysTabs = [
@@ -233,5 +245,11 @@ export const configKeysTabs = [
     name: 'Rate limit',
     tab: true,
     constructor: RateLimitConfig
+  },
+  {
+    key: configKeys.MRF_SIMPLE,
+    name: 'MRF_SIMPLE',
+    tab: true,
+    constructor: MRFSimple
   }
 ]
diff --git a/src/entities/settings/MRFSimple.ts b/src/entities/settings/MRFSimple.ts
new file mode 100644
index 0000000..618b728
--- /dev/null
+++ b/src/entities/settings/MRFSimple.ts
@@ -0,0 +1,16 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import {arrayParams, configKeys} from '../../data/Config'
+
+export default class MRFSimple {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.MRF_SIMPLE])
+  }
+  media_removal: any = ''
+  media_nsfw: any = ''
+  federated_timeline_removal: any = ''
+  reject: any = ''
+  accept: any = ''
+  report_removal: any = ''
+  avatar_removal: any = ''
+  banner_removal: any = ''
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 5759da3..4aa473d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -885,6 +885,24 @@
       "mascots_error": "MUST contain both a url and a mime_type key",
       "default_mascot": "Default mascot on MastoFE",
       "add_mascots": "Add mascot"
+    },
+    ":mrf_simple_form": {
+      "media_removal": "List of instances to remove medias from",
+      "media_removal_note": "Separate items with ;",
+      "media_nsfw": "List of instances to put medias as NSFW(sensitive) from",
+      "media_nsfw_note": "Separate items with ;",
+      "federated_timeline_removal": "List of instances to remove from Federated (aka The Whole Known Network) Timeline",
+      "federated_timeline_removal_note": "Separate items with ;",
+      "reject": "List of instances to reject any activities from",
+      "reject_note": "Separate items with ;",
+      "accept": "List of instances to accept any activities from",
+      "accept_note": "Separate items with ;",
+      "report_removal": "List of instances to reject reports from",
+      "report_removal_note": "Separate items with ;",
+      "avatar_removal": "List of instances to strip avatars from",
+      "avatar_removal_note": "Separate items with ;",
+      "banner_removal": "List of instances to strip banners from",
+      "banner_removal_note": "Separate items with ;"
     }
   }
 }
-- 
GitLab


From 0052cc68f295e0024eef37871da2ca1642d7aaa5 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 6 Aug 2019 11:12:08 +0300
Subject: [PATCH 44/61] feat: :mrf_rejectnonpublic

---
 src/data/Config.ts                          | 16 ++++++++++++++++
 src/entities/settings/MRFRejectnonpublic.ts |  9 +++++++++
 src/entities/settings/MRFSubchain.ts        |  7 +++++++
 src/i18n/en.json                            |  6 ++++++
 4 files changed, 38 insertions(+)
 create mode 100644 src/entities/settings/MRFRejectnonpublic.ts
 create mode 100644 src/entities/settings/MRFSubchain.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 8faaf78..6a71cc3 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -26,6 +26,8 @@ import EmojiConfig from '../entities/settings/EmojiConfig'
 import EmailsConfig from '../entities/settings/EmailsConfig'
 import AssetsConfig from '../entities/settings/AssetsConfig'
 import MRFSimple from '../entities/settings/MRFSimple'
+import MRFSubchain from '../entities/settings/MRFSubchain'
+import MRFRejectnonpublic from "../entities/settings/MRFRejectnonpublic";
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -57,6 +59,8 @@ export enum configKeys {
   OAUTH2 = ':oauth2',
   RATE_LIMIT = ':rate_limit',
   MRF_SIMPLE = ':mrf_simple',
+  MRF_SUBCHAIN = ':mrf_subchain',
+  MRF_REJECTNONPUBLIC = ':mrf_rejectnonpublic'
 }
 
 export const arrayParams = {
@@ -251,5 +255,17 @@ export const configKeysTabs = [
     name: 'MRF_SIMPLE',
     tab: true,
     constructor: MRFSimple
+  },
+  {
+    key: configKeys.MRF_SUBCHAIN,
+    name: 'mrf_subchain',
+    tab: true,
+    constructor: MRFSubchain
+  },
+  {
+    key: configKeys.MRF_REJECTNONPUBLIC,
+    name: 'mrf_rejectnonpublic',
+    tab: true,
+    constructor: MRFRejectnonpublic
   }
 ]
diff --git a/src/entities/settings/MRFRejectnonpublic.ts b/src/entities/settings/MRFRejectnonpublic.ts
new file mode 100644
index 0000000..007125e
--- /dev/null
+++ b/src/entities/settings/MRFRejectnonpublic.ts
@@ -0,0 +1,9 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class MRFRejectnonpublic {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  allow_followersonly: boolean = false
+  allow_direct: boolean = false
+}
diff --git a/src/entities/settings/MRFSubchain.ts b/src/entities/settings/MRFSubchain.ts
new file mode 100644
index 0000000..6cfd705
--- /dev/null
+++ b/src/entities/settings/MRFSubchain.ts
@@ -0,0 +1,7 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class MRFSubchain {
+  constructor(existCofig?) {
+    normalizeApiConfig(existCofig, this)
+  }
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4aa473d..ac5b0cc 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -903,6 +903,12 @@
       "avatar_removal_note": "Separate items with ;",
       "banner_removal": "List of instances to strip banners from",
       "banner_removal_note": "Separate items with ;"
+    },
+    ":mrf_rejectnonpublic_form": {
+      "allow_followersonly": "Allow followers only",
+      "allow_followersonly_note": "whether to allow followers-only posts",
+      "allow_direct": "Allow direct",
+      "allow_direct_note": "whether to allow direct messages"
     }
   }
 }
-- 
GitLab


From 0b8327c398bd5e70c2a57be726c4b23ef957940b Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 6 Aug 2019 14:21:16 +0300
Subject: [PATCH 45/61] feat: :hackney_pools fix, :mrf_hellthread, :suggestions

---
 src/data/Config.ts                          | 56 ++++++++++++++++++---
 src/entities/settings/AuthConstructor.ts    |  8 +++
 src/entities/settings/HackneyPoolsConfig.ts | 17 +++----
 src/entities/settings/MRFHellthread.ts      |  9 ++++
 src/entities/settings/MRFKeyword.ts         | 11 ++++
 src/entities/settings/MRFMention.ts         |  8 +++
 src/entities/settings/MRFSubchain.ts        |  1 +
 src/entities/settings/SuggestionsConfig.ts  | 12 +++++
 src/i18n/en.json                            | 30 +++++++++++
 src/utils/ConvertConfigToState.ts           |  1 +
 10 files changed, 135 insertions(+), 18 deletions(-)
 create mode 100644 src/entities/settings/AuthConstructor.ts
 create mode 100644 src/entities/settings/MRFHellthread.ts
 create mode 100644 src/entities/settings/MRFKeyword.ts
 create mode 100644 src/entities/settings/MRFMention.ts
 create mode 100644 src/entities/settings/SuggestionsConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 6a71cc3..a80c133 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -27,7 +27,12 @@ import EmailsConfig from '../entities/settings/EmailsConfig'
 import AssetsConfig from '../entities/settings/AssetsConfig'
 import MRFSimple from '../entities/settings/MRFSimple'
 import MRFSubchain from '../entities/settings/MRFSubchain'
-import MRFRejectnonpublic from "../entities/settings/MRFRejectnonpublic";
+import MRFRejectnonpublic from '../entities/settings/MRFRejectnonpublic'
+import MRFHellthread from '../entities/settings/MRFHellthread'
+import MRFKeyword from '../entities/settings/MRFKeyword'
+import MRFMention from '../entities/settings/MRFMention'
+import SuggestionsConfig from '../entities/settings/SuggestionsConfig'
+import AuthConstructor from '../entities/settings/AuthConstructor'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -56,11 +61,16 @@ export enum configKeys {
   HTTP_SECURITY = ':http_security',
   AUTO_LINKER = ':auto_linker',
   SCHEDULED_ACTIVITY = 'Pleroma.ScheduledActivity',
+  AUTH = ':auth',
   OAUTH2 = ':oauth2',
   RATE_LIMIT = ':rate_limit',
   MRF_SIMPLE = ':mrf_simple',
   MRF_SUBCHAIN = ':mrf_subchain',
-  MRF_REJECTNONPUBLIC = ':mrf_rejectnonpublic'
+  MRF_REJECTNONPUBLIC = ':mrf_rejectnonpublic',
+  MRF_HELLTHREAD = ':mrf_hellthread',
+  MRF_KEYWORD = ':mrf_keyword',
+  MRF_MENTION = ':mrf_mention',
+  SUGGESTIONS = ':suggestions',
 }
 
 export const arrayParams = {
@@ -220,12 +230,12 @@ export const configKeysTabs = [
     tab: true,
     constructor: FetchInitialPostsConfig
   },
-  // {
-  //   key: configKeys.HACKNEY_POOLS,
-  //   name: 'Hackney pools',
-  //   tab: true,
-  //   constructor: HackneyPoolsConfig
-  // },
+  {
+    key: configKeys.HACKNEY_POOLS,
+    name: 'Hackney pools',
+    tab: true,
+    constructor: HackneyPoolsConfig
+  },
   {
     key: configKeys.AUTO_LINKER,
     name: 'Auto linker',
@@ -238,6 +248,12 @@ export const configKeysTabs = [
     tab: true,
     constructor: ScheduledActivityConfig
   },
+  // {
+  //   key: configKeys.AUTH,
+  //   name: 'AUTH',
+  //   tab: true,
+  //   constructor: AuthConstructor
+  // },
   {
     key: configKeys.OAUTH2,
     name: 'OAUTH2',
@@ -267,5 +283,29 @@ export const configKeysTabs = [
     name: 'mrf_rejectnonpublic',
     tab: true,
     constructor: MRFRejectnonpublic
+  },
+  {
+    key: configKeys.MRF_HELLTHREAD,
+    name: 'mrf_hellthread',
+    tab: true,
+    constructor: MRFHellthread
+  },
+  // {
+  //   key: configKeys.MRF_KEYWORD,
+  //   name: 'mrf_keyword',
+  //   tab: true,
+  //   constructor: MRFKeyword
+  // },
+  // {
+  //   key: configKeys.MRF_MENTION,
+  //   name: 'mrf_mention',
+  //   tab: true,
+  //   constructor: MRFMention
+  // },
+  {
+    key: configKeys.SUGGESTIONS,
+    name: 'suggestions',
+    tab: true,
+    constructor: SuggestionsConfig
   }
 ]
diff --git a/src/entities/settings/AuthConstructor.ts b/src/entities/settings/AuthConstructor.ts
new file mode 100644
index 0000000..a5320b6
--- /dev/null
+++ b/src/entities/settings/AuthConstructor.ts
@@ -0,0 +1,8 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class AuthConstructor {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  oauth_consumer_strategies: any = []
+}
diff --git a/src/entities/settings/HackneyPoolsConfig.ts b/src/entities/settings/HackneyPoolsConfig.ts
index 661a27c..c472cb0 100644
--- a/src/entities/settings/HackneyPoolsConfig.ts
+++ b/src/entities/settings/HackneyPoolsConfig.ts
@@ -4,19 +4,16 @@ export default class HackneyPoolsConfig {
   constructor(existConfig?) {
     normalizeApiConfig(existConfig, this)
   }
-  federation:object = {
+  federation: any = {
     max_connections: 50,
-    timeout: 150000,
-    sendAsMap: true,
+    timeout: 1500000
   }
-  media:object = {
+  media: any = {
     max_connections: 50,
-    timeout: 150000,
-    sendAsMap: true,
+    timeout: 1500000
   }
-  upload:object = {
-    max_connections: 25,
-    timeout: 300000,
-    sendAsMap: true
+  upload: any = {
+    max_connections: 26,
+    timeout: 300000
   }
 }
diff --git a/src/entities/settings/MRFHellthread.ts b/src/entities/settings/MRFHellthread.ts
new file mode 100644
index 0000000..638c241
--- /dev/null
+++ b/src/entities/settings/MRFHellthread.ts
@@ -0,0 +1,9 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class MRFHellthread {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  delist_threshold: number = 0
+  reject_threshold: number = 0
+}
diff --git a/src/entities/settings/MRFKeyword.ts b/src/entities/settings/MRFKeyword.ts
new file mode 100644
index 0000000..aff40ac
--- /dev/null
+++ b/src/entities/settings/MRFKeyword.ts
@@ -0,0 +1,11 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import { arrayParams, configKeys } from '../../data/Config'
+
+export default class MRFKeyword {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.MRF_KEYWORD])
+  }
+  reject: any = ''
+  federated_timeline_removal: any = ''
+  replace: any = ''
+}
diff --git a/src/entities/settings/MRFMention.ts b/src/entities/settings/MRFMention.ts
new file mode 100644
index 0000000..a80d85c
--- /dev/null
+++ b/src/entities/settings/MRFMention.ts
@@ -0,0 +1,8 @@
+import {normalizeApiConfig} from "../../utils/ConvertConfigToState";
+
+export default class MRFMention {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  actors: any = ''
+}
diff --git a/src/entities/settings/MRFSubchain.ts b/src/entities/settings/MRFSubchain.ts
index 6cfd705..d44a488 100644
--- a/src/entities/settings/MRFSubchain.ts
+++ b/src/entities/settings/MRFSubchain.ts
@@ -4,4 +4,5 @@ export default class MRFSubchain {
   constructor(existCofig?) {
     normalizeApiConfig(existCofig, this)
   }
+  match_actor: any = {}
 }
diff --git a/src/entities/settings/SuggestionsConfig.ts b/src/entities/settings/SuggestionsConfig.ts
new file mode 100644
index 0000000..ee9fa6b
--- /dev/null
+++ b/src/entities/settings/SuggestionsConfig.ts
@@ -0,0 +1,12 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class SuggestionsConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  enabled: boolean = false
+  third_party_engine: string = 'http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}'
+  timeout: number = 300000
+  limit: number = 41
+  web: string = 'https://vinayaka.distsn.org'
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index ac5b0cc..c7ca518 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -904,11 +904,41 @@
       "banner_removal": "List of instances to strip banners from",
       "banner_removal_note": "Separate items with ;"
     },
+    ":mrf_mention_form": {
+      "actors": "Actors",
+      "actors_note": "A list of actors, for which to drop any posts mentioning"
+    },
     ":mrf_rejectnonpublic_form": {
       "allow_followersonly": "Allow followers only",
       "allow_followersonly_note": "whether to allow followers-only posts",
       "allow_direct": "Allow direct",
       "allow_direct_note": "whether to allow direct messages"
+    },
+    ":mrf_hellthread_form": {
+      "delist_threshold": "",
+      "delist_threshold_note": "Number of mentioned users after which the message gets delisted (the message can still be seen, but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.",
+      "reject_threshold": "",
+      "reject_threshold_note": "Number of mentioned users after which the messaged gets rejected. Set to 0 to disable."
+    },
+    ":mrf_keyword_form": {
+      "reject": "Reject",
+      "reject_note": "A list of patterns which result in message being rejected, each pattern can be a string or a regular expression",
+      "federated_timeline_removal": "Federated timeline removal",
+      "federated_timeline_removal_note": "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression",
+      "replace": "Replace",
+      "replace_note": "A list of tuples containing {pattern, replacement}, pattern can be a string or a regular expression"
+    },
+    ":suggestions_form": {
+      "enabled": "Enabled",
+      "enabled_note": "",
+      "third_party_engine": "Third-party engine",
+      "third_party_engine_note": "",
+      "timeout": "Timeout",
+      "timeout_note": "",
+      "limit": "Limit",
+      "limit_note": "",
+      "web": "Web",
+      "web_note": ""
     }
   }
 }
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index fad481d..f1d8381 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -6,6 +6,7 @@ export default (configs) => {
   const configObj = {}
   configKeysTabs.forEach(({ key, constructor }) => {
     const apiConfig = configs.find(item => key === item.key)
+    // @ts-ignore
     configObj[key] = new constructor(t(apiConfig, 'value').safeObject)
   })
   return configObj
-- 
GitLab


From 06099449edf322c6284f818f1e0832fa93e38e6b Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 6 Aug 2019 16:25:42 +0300
Subject: [PATCH 46/61] feat: :web_push_encryption, :ecto_repos

---
 .../configSettings/ConfigSettingsPage.vue     |  5 ++++
 src/data/Config.ts                            | 28 +++++++++++++++----
 src/entities/settings/EctoReposConfig.ts      |  9 ++++++
 src/entities/settings/PushEncryptionConfig.ts | 10 +++++++
 src/i18n/en.json                              | 12 ++++++++
 src/utils/ConvertConfigToState.ts             | 17 +++++------
 src/utils/GetFieldList.ts                     |  2 +-
 7 files changed, 68 insertions(+), 15 deletions(-)
 create mode 100644 src/entities/settings/EctoReposConfig.ts
 create mode 100644 src/entities/settings/PushEncryptionConfig.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 55abb61..a5c6a73 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -44,6 +44,11 @@
           v-model="config[configKeysEnum.EMOJI]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.ECTO_REPOS"
+          v-if="tabs[value].key === configKeysEnum.DATABASE"
+          v-model="config[configKeysEnum.ECTO_REPOS]"
+        />
         <assets-form
           :title="configKeysEnum.ASSETS"
           v-show="tabs[value].key === configKeysEnum.ASSETS"
diff --git a/src/data/Config.ts b/src/data/Config.ts
index a80c133..d307a1e 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -33,6 +33,8 @@ import MRFKeyword from '../entities/settings/MRFKeyword'
 import MRFMention from '../entities/settings/MRFMention'
 import SuggestionsConfig from '../entities/settings/SuggestionsConfig'
 import AuthConstructor from '../entities/settings/AuthConstructor'
+import EctoReposConfig from '../entities/settings/EctoReposConfig'
+import PushEncryptionConfig from '../entities/settings/PushEncryptionConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -52,6 +54,7 @@ export enum configKeys {
   RICH_MEDIA = ':rich_media',
   FETCH_INITIAL_POSTS = ':fetch_initial_posts',
   HACKNEY_POOLS = ':hackney_pools',
+  ECTO_REPOS = ':ecto_repos',
   DATABASE = ':database',
   MEDIA_PROXY = ':media_proxy',
   GOPHER = ':gopher',
@@ -71,6 +74,7 @@ export enum configKeys {
   MRF_KEYWORD = ':mrf_keyword',
   MRF_MENTION = ':mrf_mention',
   SUGGESTIONS = ':suggestions',
+  PUSH_ENCRYPTION = ':web_push_encryption'
 }
 
 export const arrayParams = {
@@ -182,6 +186,12 @@ export const configKeysTabs = [
     constructor: DatabaseConfig,
     tab: true,
   },
+  {
+    key: configKeys.ECTO_REPOS,
+    name: 'Ecto repos',
+    tab: false,
+    constructor: EctoReposConfig
+  },
   {
     key: configKeys.MEDIA_PROXY,
     name: 'Media proxy',
@@ -248,12 +258,12 @@ export const configKeysTabs = [
     tab: true,
     constructor: ScheduledActivityConfig
   },
-  // {
-  //   key: configKeys.AUTH,
-  //   name: 'AUTH',
-  //   tab: true,
-  //   constructor: AuthConstructor
-  // },
+  {
+    key: configKeys.AUTH,
+    name: 'AUTH',
+    tab: true,
+    constructor: AuthConstructor
+  },
   {
     key: configKeys.OAUTH2,
     name: 'OAUTH2',
@@ -307,5 +317,11 @@ export const configKeysTabs = [
     name: 'suggestions',
     tab: true,
     constructor: SuggestionsConfig
+  },
+  {
+    key: configKeys.PUSH_ENCRYPTION,
+    name: 'Push encryption',
+    tab: true,
+    constructor: PushEncryptionConfig
   }
 ]
diff --git a/src/entities/settings/EctoReposConfig.ts b/src/entities/settings/EctoReposConfig.ts
new file mode 100644
index 0000000..e7bd105
--- /dev/null
+++ b/src/entities/settings/EctoReposConfig.ts
@@ -0,0 +1,9 @@
+import {convertArrayParamsToState} from "../../utils/ConvertConfigToState";
+
+export default class EctoReposConfig {
+  constructor(existConfig?) {
+    this.ecto_repos = [...existConfig]
+    convertArrayParamsToState(this, ['ecto_repos'])
+  }
+  ecto_repos: any = ''
+}
diff --git a/src/entities/settings/PushEncryptionConfig.ts b/src/entities/settings/PushEncryptionConfig.ts
new file mode 100644
index 0000000..e170e09
--- /dev/null
+++ b/src/entities/settings/PushEncryptionConfig.ts
@@ -0,0 +1,10 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class PushEncryptionConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  subject: string = ''
+  public_key: string = ''
+  private_key: string = ''
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c7ca518..40fcef3 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -637,6 +637,10 @@
       "rum_enabled": "RUM indexing for full text search",
       "rum_enabled_note": "If RUM indexes should be used"
     },
+    ":ecto_repos_form": {
+      "ecto_repos": "Ecto repos",
+      "ecto_repos_note": "Separate items with ;"
+    },
     ":media_proxy_form": {
       "whitelist": "Whitelist",
       "whitelist_note": "List of domains to bypass the mediaproxy. Separate items with ';'",
@@ -939,6 +943,14 @@
       "limit_note": "",
       "web": "Web",
       "web_note": ""
+    },
+    ":web_push_encryption_form": {
+      "subject": "Subject",
+      "subject_note": "A mailto link for the administrative contact. It’s best if this email is not a personal email address, but rather a group email so that if a person leaves an organization, is unavailable for an extended period, or otherwise can’t respond, someone else on the list can",
+      "public_key": "VAPID public key",
+      "public_key_note": "",
+      "private_key": "VAPID private key",
+      "private_key_note": ""
     }
   }
 }
diff --git a/src/utils/ConvertConfigToState.ts b/src/utils/ConvertConfigToState.ts
index f1d8381..1b41e63 100644
--- a/src/utils/ConvertConfigToState.ts
+++ b/src/utils/ConvertConfigToState.ts
@@ -49,12 +49,13 @@ export const normalizeApiConfig = function(existConfig, classObject, arrayParams
     })
     return resultObject
   }
-  function convertArrayParamsToState (config, arrayParams) {
-    arrayParams.forEach(param => {
-      config[param] = (config[param] && Array.isArray(config[param]))
-        ? config[param].join(';')
-        : config[param]
-    })
-    return config
-  }
+
+}
+export const convertArrayParamsToState = (config, arrayParams) => {
+  arrayParams.forEach(param => {
+    config[param] = (config[param] && Array.isArray(config[param]))
+      ? config[param].join(';')
+      : config[param]
+  })
+  return config
 }
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 03663a4..5a9e8c8 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -39,7 +39,7 @@ export const selectOptions = {
   inline_content_types: ['true', 'false', 'a list of whitelisted content types'],
   valid_schemes: ['https', 'http', 'dat', 'dweb', 'gopher', 'ipfs', 'ipns', 'irc', 'ircs', 'magnet', 'mailto', 'mumble', 'ssb', 'xmpp'],
   referrer_policy: ['same-origin', 'no-referrer'],
-  parsers: ['Pleroma.Web.RichMedia.Parsers.TwitterCard', 'Pleroma.Web.RichMedia.Parsers.OGP', 'Pleroma.Web.RichMedia.Parsers.OEmbed']
+  parsers: ['Pleroma.Web.RichMedia.Parsers.TwitterCard', 'Pleroma.Web.RichMedia.Parsers.OGP', 'Pleroma.Web.RichMedia.Parsers.OEmbed'],
 }
 
 export default (formData) => {
-- 
GitLab


From 6f34eb2c5ea2a25bc1ec33b240afbccb5fd1b7b9 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 8 Aug 2019 14:14:16 +0300
Subject: [PATCH 47/61] feat: mrf group configs, move tabs titles to i18n, make
 normalizerObject

---
 .../configSettings/forms/MRFKeywordForm.vue   |  93 ++++++++++
 .../configSettings/forms/MRFSubchainForm.vue  |  87 +++++++++
 .../forms/MRFUserAllowlistForm.vue            |  83 +++++++++
 .../configSettings/ConfigSettingsPage.vue     |  70 ++++++-
 src/data/Config.ts                            | 171 +++++++++---------
 src/entities/settings/MRFKeyword.ts           |  16 +-
 .../settings/MRFNormalizeMarkupConfig.ts      |   8 +
 src/entities/settings/MRFSubchain.ts          |   8 -
 src/entities/settings/MRFSubchainConfig.ts    |  22 +++
 .../settings/MRFUserAllowlistConfig.ts        |  21 +++
 src/i18n/en.json                              |  56 +++++-
 src/services/ConfigService.ts                 |  10 +-
 src/utils/ConvertConfigToApiRequest.js        |  36 ++--
 13 files changed, 547 insertions(+), 134 deletions(-)
 create mode 100644 src/components/configSettings/forms/MRFKeywordForm.vue
 create mode 100644 src/components/configSettings/forms/MRFSubchainForm.vue
 create mode 100644 src/components/configSettings/forms/MRFUserAllowlistForm.vue
 create mode 100644 src/entities/settings/MRFNormalizeMarkupConfig.ts
 delete mode 100644 src/entities/settings/MRFSubchain.ts
 create mode 100644 src/entities/settings/MRFSubchainConfig.ts
 create mode 100644 src/entities/settings/MRFUserAllowlistConfig.ts

diff --git a/src/components/configSettings/forms/MRFKeywordForm.vue b/src/components/configSettings/forms/MRFKeywordForm.vue
new file mode 100644
index 0000000..bce414d
--- /dev/null
+++ b/src/components/configSettings/forms/MRFKeywordForm.vue
@@ -0,0 +1,93 @@
+<template>
+  <div class="mrf_keyword_form">
+    <p class="title">{{$t('config_settings.:mrf_keyword_form.title')}}</p>
+    <va-input
+      v-model="formData.reject"
+      :label="$t('config_settings.:mrf_keyword_form.reject')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.:mrf_keyword_form.reject_note')}}</p>
+    <va-input
+      v-model="formData.federated_timeline_removal"
+      :label="$t('config_settings.:mrf_keyword_form.federated_timeline_removal')"
+      class="mb-0"
+    />
+    <p class="note">{{$t('config_settings.:mrf_keyword_form.federated_timeline_removal_note')}}</p>
+    <div class="va-row" v-for="(replaceItem, index) in formData.replace" :key="index">
+      <va-input
+        v-model="formData.replace[index].pattern"
+        class="mrf_keyword_form__group-name"
+        :label="$t('config_settings.:mrf_keyword_form.pattern')"
+      />
+      <va-input
+        v-model="formData.replace[index].replacement"
+        class="mrf_keyword_form__group-value mb-3"
+        :label="$t('config_settings.:mrf_keyword_form.replacement')"
+        :tagMax="0"
+      />
+      <va-icon
+        color="danger"
+        @click.native="removeOption(index)"
+        name="ion-ios-trash-outline"
+        class="px-2 mrf_keyword_form__delete"
+      />
+    </div>
+    <va-button small outline @click="addOption">{{$t('config_settings.:mrf_keyword_form.add_replace_item')}}</va-button>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import MRFKeyword, { ReplaceItemClass } from '../../../entities/settings/MRFKeyword'
+
+@Component({
+  components: {},
+})
+export default class MRFKeywordForm extends Vue {
+  @Prop(Object) readonly value!: MRFKeyword
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  removeOption (index) {
+    this.formData.replace.splice(index, 1)
+  }
+  addOption () {
+    this.formData.replace.push(new ReplaceItemClass())
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .mrf_keyword_form {
+    padding-top: .5rem;
+    border-top: 1px solid $border-color;
+    .va-row {
+      margin-left: 0 !important;
+      margin-right: 0 !important;
+      position: relative;
+    }
+    &__delete {
+      font-size: 1.5rem;
+      cursor: pointer;
+      @include media-breakpoint-down(sm) {
+        top: 0;
+        bottom: 0;
+        height: 1.5rem;
+        margin: auto;
+        right: 0;
+        position: absolute;
+      }
+    }
+    &__group-name, &__group-value {
+      padding-right: .5rem;
+      width: calc(50% - 15px);
+      @include media-breakpoint-down(sm) {
+        padding-right: 30px;
+        width: calc(100% - 30px);
+      }
+    }
+  }
+</style>
diff --git a/src/components/configSettings/forms/MRFSubchainForm.vue b/src/components/configSettings/forms/MRFSubchainForm.vue
new file mode 100644
index 0000000..ee025a8
--- /dev/null
+++ b/src/components/configSettings/forms/MRFSubchainForm.vue
@@ -0,0 +1,87 @@
+<template>
+  <div class="mrf_keyword_form">
+    <p class="title">{{$t('config_settings.:mrf_subchain_form.title')}}</p>
+    <p class="note">{{$t('config_settings.:mrf_subchain_form.note')}}</p>
+    <div class="va-row" v-for="(match_actor, index) in formData.match_actor" :key="index">
+      <va-input
+        v-model="formData.match_actor[index].regular_expression"
+        class="mrf-subchain-form__group-name"
+        :label="$t('config_settings.:mrf_subchain_form.regular_expression')"
+      />
+      <va-select
+        :options="selectOptions"
+        multiple
+        v-model="formData.match_actor[index].policy_modules"
+        class="mrf-subchain-form__group-value mb-3"
+        :label="$t('config_settings.:mrf_subchain_form.policy_modules')"
+        width="calc(50% -30px)"
+        :tagMax="0"
+      />
+      <va-icon
+        color="danger"
+        @click.native="removeOption(index)"
+        name="ion-ios-trash-outline"
+        class="px-2 mrf-subchain-form__delete"
+      />
+    </div>
+    <va-button small outline @click="addOption">{{$t('config_settings.:mrf_subchain_form.add_match_actor')}}</va-button>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import MRFSubchainConfig, { MatchActor } from '../../../entities/settings/MRFSubchainConfig'
+import { selectOptions } from '../../../utils/GetFieldList'
+
+@Component({
+  components: {},
+})
+export default class MRFSubchainForm extends Vue {
+  @Prop(Object) readonly value!: MRFSubchainConfig
+  selectOptions = selectOptions.rewrite_policy
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  removeOption (index) {
+    this.formData.match_actor.splice(index, 1)
+  }
+  addOption () {
+    this.formData.match_actor.push(new MatchActor())
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .mrf_keyword_form {
+    padding-top: .5rem;
+    border-top: 1px solid $border-color;
+    .va-row {
+      margin-left: 0 !important;
+      margin-right: 0 !important;
+      position: relative;
+    }
+    &__delete {
+      font-size: 1.5rem;
+      cursor: pointer;
+      @include media-breakpoint-down(sm) {
+        top: 0;
+        bottom: 0;
+        height: 1.5rem;
+        margin: auto;
+        right: 0;
+        position: absolute;
+      }
+    }
+    &__group-name, &__group-value {
+      padding-right: .5rem;
+      width: calc(50% - 15px);
+      @include media-breakpoint-down(sm) {
+        padding-right: 30px;
+        width: calc(100% - 30px);
+      }
+    }
+  }
+</style>
diff --git a/src/components/configSettings/forms/MRFUserAllowlistForm.vue b/src/components/configSettings/forms/MRFUserAllowlistForm.vue
new file mode 100644
index 0000000..febc618
--- /dev/null
+++ b/src/components/configSettings/forms/MRFUserAllowlistForm.vue
@@ -0,0 +1,83 @@
+<template>
+  <div class="mrf_user_allowlist_form">
+    <p class="title">{{$t(`config_settings.${title}_form.title`)}}</p>
+    <p class="note">{{$t(`config_settings.${title}_form.note`)}}</p>
+    <div class="va-row" v-for="(domain, index) in formData.allow_list" :key="index">
+      <va-input
+        v-model="formData.allow_list[index].domain"
+        class="mrf_user_allowlist_form__group-name"
+        :label="$t(`config_settings.${title}_form.domain`)"
+      />
+      <va-input
+        v-model="formData.allow_list[index].list_of_users"
+        class="mrf_user_allowlist_form__group-value mb-3"
+        :label="$t(`config_settings.${title}_form.list_of_users`)"
+        :tagMax="0"
+      />
+      <va-icon
+        color="danger"
+        @click.native="removeOption(index)"
+        name="ion-ios-trash-outline"
+        class="px-2 mrf_user_allowlist_form__delete"
+      />
+    </div>
+    <va-button small outline @click="addOption">{{$t(`config_settings.${title}_form.add_domain`)}}</va-button>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import MRFUserAllowlistConfig, { UserAllowListItem } from '../../../entities/settings/MRFUserAllowlistConfig'
+
+@Component({
+  components: {},
+})
+export default class MrfUserAllowlistForm extends Vue {
+  @Prop(Object) readonly value!: MRFUserAllowlistConfig
+  @Prop(String) readonly title!: string
+  get formData () {
+    return this.value
+  }
+  set formData (val) {
+    this.$emit('updateForm', val)
+  }
+  removeOption (index) {
+    this.formData.allow_list.splice(index, 1)
+  }
+  addOption () {
+    this.formData.allow_list.push(new UserAllowListItem())
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.mrf_user_allowlist_form {
+  padding-top: .5rem;
+  border-top: 1px solid $border-color;
+  .va-row {
+    margin-left: 0 !important;
+    margin-right: 0 !important;
+    position: relative;
+  }
+  &__delete {
+    font-size: 1.5rem;
+    cursor: pointer;
+    @include media-breakpoint-down(sm) {
+      top: 0;
+      bottom: 0;
+      height: 1.5rem;
+      margin: auto;
+      right: 0;
+      position: absolute;
+    }
+  }
+  &__group-name, &__group-value {
+    padding-right: .5rem;
+    width: calc(50% - 15px);
+    @include media-breakpoint-down(sm) {
+      padding-right: 30px;
+      width: calc(100% - 30px);
+    }
+  }
+}
+</style>
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index a5c6a73..2dd7eef 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -5,7 +5,7 @@
         v-for="item in tabs"
         :key="item.key"
       >
-        {{item.name}}
+        {{$t(`config_settings.tabs.${item.key}`)}}
       </va-tab>
     </va-tabs>
     <div class="config-settings-page__content pt-4">
@@ -15,7 +15,7 @@
             :key="tab.key"
             v-model="config[tab.key]"
             :title="tab.key"
-            :showTitle="tabs[value].key === configKeysEnum.LDAP"
+            :showTitle="showTabTitle(tab)"
             v-if="showTab(tab)"
           />
         </template>
@@ -51,8 +51,9 @@
         />
         <assets-form
           :title="configKeysEnum.ASSETS"
-          v-show="tabs[value].key === configKeysEnum.ASSETS"
+          v-show="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
           v-model="config[configKeysEnum.ASSETS]"
+          showTitle
           ref="ASSETS"
         />
         <config-form
@@ -61,11 +62,59 @@
           v-model="config[configKeysEnum.KOCAPTCHA]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.OAUTH2"
+          v-if="tabs[value].key === configKeysEnum.OAUTH2"
+          v-model="config[configKeysEnum.OAUTH2]"
+          showTitle
+        />
         <auto-linker-form
           :title="configKeysEnum.AUTO_LINKER"
           v-if="tabs[value].key === configKeysEnum.AUTO_LINKER"
           v-model="config[configKeysEnum.AUTO_LINKER]"
         />
+        <mrf-subchain-form
+          :title="configKeysEnum.MRF_SUBCHAIN"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_SUBCHAIN]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.MRF_REJECTNONPUBLIC"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_REJECTNONPUBLIC]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.MRF_HELLTHREAD"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_HELLTHREAD]"
+          showTitle
+        />
+        <mrf-keyword-form
+          :title="configKeysEnum.MRF_KEYWORD"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_KEYWORD]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.MRF_MENTION"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_MENTION]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.MRF_NORMALIZE_MARKUP"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_NORMALIZE_MARKUP]"
+          showTitle
+        />
+        <m-r-f-user-allowlist-form
+          :title="configKeysEnum.MRF_USER_ALLOWLIST"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_USER_ALLOWLIST]"
+          showTitle
+        />
       </div>
       <div class="loading flex-center" v-if="loading">
         <fulfilling-bouncing-circle-spinner
@@ -93,14 +142,20 @@ import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 import AutoLinkerForm from '../../configSettings/forms/AutoLinkerForm.vue'
 import EmojiForm from '../../configSettings/forms/EmojiForm.vue'
 import AssetsForm from '../../configSettings/forms/AssetsForm.vue'
+import MrfSubchainForm from '../../configSettings/forms/MRFSubchainForm.vue'
+import MrfKeywordForm from '../../configSettings/forms/MRFKeywordForm.vue'
+import MRFUserAllowlistForm from "../../configSettings/forms/MRFUserAllowlistForm.vue";
 
 @Component({
   components: {
+    MRFUserAllowlistForm,
     EmojiForm,
     AutoLinkerForm,
     ConfigForm,
     EmailsForm,
     AssetsForm,
+    MrfSubchainForm,
+    MrfKeywordForm,
     FulfillingBouncingCircleSpinner
   },
 })
@@ -147,11 +202,18 @@ export default class ConfigSettingsPage extends Vue {
     this.config = config
   }
   showTab ({ key }) {
-    return this.tabs[this.value].key === key &&
+    return this.currentTabKey === key &&
       key !== this.configKeysEnum.AUTO_LINKER &&
       key !== this.configKeysEnum.EMAILS &&
       key !== this.configKeysEnum.ASSETS
   }
+  showTabTitle ({ key }) {
+    return this.currentTabKey === this.configKeysEnum.LDAP ||
+      this.currentTabKey === this.configKeysEnum.MRF_SIMPLE
+  }
+  get currentTabKey () {
+    return this.tabs[this.value].key
+  }
   get tabs () {
     return configKeysTabs.filter(({ tab }) => tab)
   }
diff --git a/src/data/Config.ts b/src/data/Config.ts
index d307a1e..753ff14 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -16,25 +16,26 @@ import HttpSecurityConfig from '../entities/settings/HttpSecurityConfig'
 import RichMediaConfig from '../entities/settings/RichMediaConfig'
 import FetchInitialPostsConfig from '../entities/settings/FetchInitialPostsConfig'
 import HackneyPoolsConfig from '../entities/settings/HackneyPoolsConfig'
-import AutoLinkerConfig from '../entities/settings/AutoLinkerConfig'
+import AutoLinkerConfig, {normalizeAutoLinkerConfig} from '../entities/settings/AutoLinkerConfig'
 import ScheduledActivityConfig from '../entities/settings/ScheduledActivityConfig'
 import Oauth2Config from '../entities/settings/Oauth2Config'
-import RateLimitConfig from '../entities/settings/RateLimitConfig'
+import RateLimitConfig, {normalizeRateLimitConfig} from '../entities/settings/RateLimitConfig'
 import ChatConfig from '../entities/settings/ChatConfig'
-import GopherConfig from '../entities/settings/GopherConfig'
-import EmojiConfig from '../entities/settings/EmojiConfig'
-import EmailsConfig from '../entities/settings/EmailsConfig'
-import AssetsConfig from '../entities/settings/AssetsConfig'
+import GopherConfig, {normalizeGopherConfig} from '../entities/settings/GopherConfig'
+import EmojiConfig, {normalizeEmojiConfig} from '../entities/settings/EmojiConfig'
+import EmailsConfig, {normalizeEmailsConfig} from '../entities/settings/EmailsConfig'
+import AssetsConfig, {normalizeAssetsConfig} from '../entities/settings/AssetsConfig'
 import MRFSimple from '../entities/settings/MRFSimple'
-import MRFSubchain from '../entities/settings/MRFSubchain'
+import MRFSubchainConfig, {normalizeMRFSubchainConfig} from '../entities/settings/MRFSubchainConfig'
 import MRFRejectnonpublic from '../entities/settings/MRFRejectnonpublic'
 import MRFHellthread from '../entities/settings/MRFHellthread'
-import MRFKeyword from '../entities/settings/MRFKeyword'
+import MRFKeyword, {normalizeMrfKeywordConfig} from '../entities/settings/MRFKeyword'
 import MRFMention from '../entities/settings/MRFMention'
 import SuggestionsConfig from '../entities/settings/SuggestionsConfig'
-import AuthConstructor from '../entities/settings/AuthConstructor'
-import EctoReposConfig from '../entities/settings/EctoReposConfig'
+import EctoReposConfig, {normalizeEctoReposConfig} from '../entities/settings/EctoReposConfig'
 import PushEncryptionConfig from '../entities/settings/PushEncryptionConfig'
+import MRFUserAllowlist, {normalizeMRFUserAllowlistConfig} from "../entities/settings/MRFUserAllowlistConfig";
+import MRFNormalizeMarkupConfig from "../entities/settings/MRFNormalizeMarkupConfig";
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -73,6 +74,8 @@ export enum configKeys {
   MRF_HELLTHREAD = ':mrf_hellthread',
   MRF_KEYWORD = ':mrf_keyword',
   MRF_MENTION = ':mrf_mention',
+  MRF_USER_ALLOWLIST = ':mrf_user_allowlist',
+  MRF_NORMALIZE_MARKUP = ':mrf_normalize_markup',
   SUGGESTIONS = ':suggestions',
   PUSH_ENCRYPTION = ':web_push_encryption'
 }
@@ -84,6 +87,7 @@ export const arrayParams = {
   [configKeys.RICH_MEDIA]: ['ignore_hosts', 'ignore_tld', 'ttl_setters'],
   [configKeys.GOPHER]: ['ip'],
   [configKeys.EMOJI]: ['pack_extensions'],
+  [configKeys.ECTO_REPOS]: ['ecto_repos'],
   [configKeys.MRF_SIMPLE]: [
     'media_removal',
     'media_nsfw',
@@ -93,43 +97,56 @@ export const arrayParams = {
     'report_removal',
     'avatar_removal',
     'banner_removal'
-  ]
+  ],
+  [configKeys.MRF_KEYWORD]: ['reject', 'federated_timeline_removal']
+}
+
+export const normalizerMapper = {
+  [configKeys.AUTO_LINKER]: { normalizer: normalizeAutoLinkerConfig, normalizeBeforeGeneralFunc: true },
+  [configKeys.GOPHER]: { normalizer: normalizeGopherConfig, normalizeBeforeGeneralFunc: true },
+  [configKeys.EMAILS]: { normalizer: normalizeEmailsConfig, normalizeBeforeGeneralFunc: true },
+  [configKeys.ASSETS]: { normalizer: normalizeAssetsConfig, normalizeBeforeGeneralFunc: true },
+  [configKeys.RATE_LIMIT]: { normalizer: normalizeRateLimitConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.EMOJI]: { normalizer: normalizeEmojiConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.ECTO_REPOS]: { normalizer: normalizeEctoReposConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.MRF_SUBCHAIN]: { normalizer: normalizeMRFSubchainConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.MRF_KEYWORD]: { normalizer: normalizeMrfKeywordConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.MRF_USER_ALLOWLIST]: { normalizer: normalizeMRFUserAllowlistConfig, normalizeBeforeGeneralFunc: false },
 }
 
 export const configKeysTabs = [
   {
     key: configKeys.EMAILS,
     tab: true,
-    name: 'Emails',
     constructor: EmailsConfig,
   },
+  {
+    key: configKeys.UPLOAD,
+    constructor: UploadConfig,
+    tab: true,
+  },
   {
     key: configKeys.UPLOADERSS3,
     tab: false,
-    name: 'Uploader S3',
     constructor: UploadersS3Config,
   },
   {
     key: configKeys.UPLOADERSLOCAL,
     tab: false,
-    name: 'Uploader Local',
     constructor: UploadersLocalConfig,
   },
   {
     key: configKeys.INSTANCE,
-    name: 'Instance',
     constructor: InstanceConfig,
     tab: true,
   },
   {
     key: configKeys.LOGGER,
-    name: 'Logger',
     tab: false,
-    constructor: LoggerConfig
+    constructor: LoggerConfig,
   },
   {
     key: configKeys.FRONTEND_CONFIGURATIONS,
-    name: 'Frontend configurations',
     constructor: FrontentConfigurationsConfig,
     tab: true,
   },
@@ -140,188 +157,162 @@ export const configKeysTabs = [
   // },
   {
     key: configKeys.CHAT,
-    name: 'Chat',
     tab: false,
-    constructor: ChatConfig
+    constructor: ChatConfig,
   },
   {
     key: configKeys.EMOJI,
-    name: 'Emoji',
     tab: false,
     constructor: EmojiConfig,
   },
   {
     key: configKeys.ASSETS,
-    name: 'Assets',
-    tab: true,
+    tab: false,
     constructor: AssetsConfig,
   },
   {
     key: configKeys.CAPTCHA,
-    name: 'Captcha',
     constructor: CaptchaConfig,
     tab: true,
   },
   {
     key: configKeys.KOCAPTCHA,
-    name: 'Kocaptcha',
     tab: false,
     constructor: KocaptchaConfig
   },
-  {
-    key: configKeys.UPLOAD,
-    name: 'Upload',
-    constructor: UploadConfig,
-    tab: true,
-  },
   {
     key: configKeys.URI_SCHEMES,
-    name: 'URI schemes',
     constructor: UriSchemesConfig,
     tab: true,
   },
   {
     key: configKeys.DATABASE,
-    name: 'Database options',
     constructor: DatabaseConfig,
     tab: true,
   },
   {
     key: configKeys.ECTO_REPOS,
-    name: 'Ecto repos',
     tab: false,
-    constructor: EctoReposConfig
+    constructor: EctoReposConfig,
   },
   {
     key: configKeys.MEDIA_PROXY,
-    name: 'Media proxy',
     constructor: MediaProxyConfig,
     tab: true,
   },
   {
     key: configKeys.GOPHER,
-    name: 'Gopher',
     tab: true,
-    constructor: GopherConfig
+    constructor: GopherConfig,
   },
   {
     key: configKeys.LDAP,
-    name: 'LDAP',
     constructor: LdapConfig,
     tab: true,
   },
   {
     key: configKeys.ACTIVITY_PUB,
-    name: 'Activity pub',
     constructor: ActivityPubConfig,
     tab: true,
   },
   {
     key: configKeys.USER,
-    name: 'User',
     constructor: UserConfig,
     tab: true,
   },
   {
     key: configKeys.HTTP_SECURITY,
-    name: 'HTTP security',
     constructor: HttpSecurityConfig,
     tab: true,
   },
   {
     key: configKeys.RICH_MEDIA,
-    name: 'Rich media',
     constructor: RichMediaConfig,
-    tab: true
+    tab: true,
   },
   {
     key: configKeys.FETCH_INITIAL_POSTS,
-    name: 'Fetch initial posts',
     tab: true,
-    constructor: FetchInitialPostsConfig
+    constructor: FetchInitialPostsConfig,
   },
   {
     key: configKeys.HACKNEY_POOLS,
-    name: 'Hackney pools',
     tab: true,
-    constructor: HackneyPoolsConfig
+    constructor: HackneyPoolsConfig,
   },
   {
     key: configKeys.AUTO_LINKER,
-    name: 'Auto linker',
     tab: true,
-    constructor: AutoLinkerConfig
+    constructor: AutoLinkerConfig,
   },
   {
     key: configKeys.SCHEDULED_ACTIVITY,
-    name: 'Scheduled activity',
-    tab: true,
-    constructor: ScheduledActivityConfig
-  },
-  {
-    key: configKeys.AUTH,
-    name: 'AUTH',
     tab: true,
-    constructor: AuthConstructor
+    constructor: ScheduledActivityConfig,
   },
+  // {
+  //   key: configKeys.AUTH,
+  //   tab: true,
+  //   constructor: AuthConstructor
+  // },
   {
     key: configKeys.OAUTH2,
-    name: 'OAUTH2',
-    tab: true,
-    constructor: Oauth2Config
+    tab: false,
+    constructor: Oauth2Config,
   },
   {
     key: configKeys.RATE_LIMIT,
-    name: 'Rate limit',
     tab: true,
-    constructor: RateLimitConfig
+    constructor: RateLimitConfig,
   },
   {
     key: configKeys.MRF_SIMPLE,
-    name: 'MRF_SIMPLE',
     tab: true,
-    constructor: MRFSimple
+    constructor: MRFSimple,
   },
   {
     key: configKeys.MRF_SUBCHAIN,
-    name: 'mrf_subchain',
-    tab: true,
-    constructor: MRFSubchain
+    tab: false,
+    constructor: MRFSubchainConfig,
   },
   {
     key: configKeys.MRF_REJECTNONPUBLIC,
-    name: 'mrf_rejectnonpublic',
-    tab: true,
-    constructor: MRFRejectnonpublic
+    tab: false,
+    constructor: MRFRejectnonpublic,
   },
   {
     key: configKeys.MRF_HELLTHREAD,
-    name: 'mrf_hellthread',
-    tab: true,
-    constructor: MRFHellthread
+    tab: false,
+    constructor: MRFHellthread,
+  },
+  {
+    key: configKeys.MRF_KEYWORD,
+    tab: false,
+    constructor: MRFKeyword,
+  },
+  {
+    key: configKeys.MRF_MENTION,
+    tab: false,
+    constructor: MRFMention,
+  },
+  {
+    key: configKeys.MRF_USER_ALLOWLIST,
+    tab: false,
+    constructor: MRFUserAllowlist,
+  },
+  {
+    key: configKeys.MRF_NORMALIZE_MARKUP,
+    tab: false,
+    constructor: MRFNormalizeMarkupConfig,
   },
-  // {
-  //   key: configKeys.MRF_KEYWORD,
-  //   name: 'mrf_keyword',
-  //   tab: true,
-  //   constructor: MRFKeyword
-  // },
-  // {
-  //   key: configKeys.MRF_MENTION,
-  //   name: 'mrf_mention',
-  //   tab: true,
-  //   constructor: MRFMention
-  // },
   {
     key: configKeys.SUGGESTIONS,
-    name: 'suggestions',
     tab: true,
-    constructor: SuggestionsConfig
+    constructor: SuggestionsConfig,
   },
   {
     key: configKeys.PUSH_ENCRYPTION,
-    name: 'Push encryption',
     tab: true,
-    constructor: PushEncryptionConfig
+    constructor: PushEncryptionConfig,
   }
 ]
diff --git a/src/entities/settings/MRFKeyword.ts b/src/entities/settings/MRFKeyword.ts
index aff40ac..91b084e 100644
--- a/src/entities/settings/MRFKeyword.ts
+++ b/src/entities/settings/MRFKeyword.ts
@@ -1,11 +1,25 @@
 import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
 import { arrayParams, configKeys } from '../../data/Config'
 
+export class ReplaceItemClass {
+  pattern: string = ''
+  replacement: string = ''
+}
+
 export default class MRFKeyword {
   constructor(existConfig?) {
     normalizeApiConfig(existConfig, this, arrayParams[configKeys.MRF_KEYWORD])
   }
   reject: any = ''
   federated_timeline_removal: any = ''
-  replace: any = ''
+  replace: Array<ReplaceItemClass> = [new ReplaceItemClass()]
+}
+
+export const normalizeMrfKeywordConfig = (config) => {
+  const apiConf = [ ...config ]
+  const replaceIndex = apiConf.findIndex(({ tuple }) => tuple[0] === ':replace')
+  apiConf[replaceIndex].tuple[1] = apiConf[replaceIndex].tuple[1]
+    .filter(({ pattern }) => !!pattern.length)
+    .map(({ pattern , replacement }) => ({ tuple: [pattern, replacement]}))
+  return apiConf
 }
diff --git a/src/entities/settings/MRFNormalizeMarkupConfig.ts b/src/entities/settings/MRFNormalizeMarkupConfig.ts
new file mode 100644
index 0000000..0bac177
--- /dev/null
+++ b/src/entities/settings/MRFNormalizeMarkupConfig.ts
@@ -0,0 +1,8 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class MRFNormalizeMarkupConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  scrub_policy:string = ''
+}
diff --git a/src/entities/settings/MRFSubchain.ts b/src/entities/settings/MRFSubchain.ts
deleted file mode 100644
index d44a488..0000000
--- a/src/entities/settings/MRFSubchain.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
-
-export default class MRFSubchain {
-  constructor(existCofig?) {
-    normalizeApiConfig(existCofig, this)
-  }
-  match_actor: any = {}
-}
diff --git a/src/entities/settings/MRFSubchainConfig.ts b/src/entities/settings/MRFSubchainConfig.ts
new file mode 100644
index 0000000..c0538bd
--- /dev/null
+++ b/src/entities/settings/MRFSubchainConfig.ts
@@ -0,0 +1,22 @@
+export class MatchActor {
+  constructor(data?) {
+    this.regular_expression = data ? data.regular_expression : ''
+    this.policy_modules = data ? data.policy_modules : ''
+  }
+  regular_expression: string = ''
+  policy_modules: string = ''
+}
+
+export default class MRFSubchainConfig {
+  match_actor: Array<MatchActor> = [new MatchActor()]
+}
+
+export const normalizeMRFSubchainConfig = (config) => {
+  const apiConf = [...config ]
+  apiConf[0].tuple[1] = apiConf[0].tuple[1].filter(item => {
+    return !!item.regular_expression.length
+  }).map(({ regular_expression, policy_modules}) => {
+    return {[regular_expression]: [...policy_modules]}
+  })
+  return apiConf
+}
diff --git a/src/entities/settings/MRFUserAllowlistConfig.ts b/src/entities/settings/MRFUserAllowlistConfig.ts
new file mode 100644
index 0000000..031e25e
--- /dev/null
+++ b/src/entities/settings/MRFUserAllowlistConfig.ts
@@ -0,0 +1,21 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export class UserAllowListItem {
+  domain: string = ''
+  list_of_users: string = ''
+}
+
+export default class MRFUserAllowlistConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  allow_list: Array<UserAllowListItem> = [new UserAllowListItem()]
+}
+
+export const normalizeMRFUserAllowlistConfig = (config) => {
+  let apiConf = [...config]
+  apiConf = apiConf[0].tuple[1]
+    .filter(({ domain }) => !!domain.length)
+    .map(item => ({ tuple: {[item.domain]: item.list_of_users.split(';')}}))
+  return apiConf
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 40fcef3..4a80f14 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -436,6 +436,31 @@
     "add_note_placeholder": "Add note..."
   },
   "config_settings": {
+    "tabs": {
+      "Pleroma.Emails.Mailer": "Emails",
+      "Pleroma.Upload": "Upload",
+      ":frontend_configurations": "Frontend configurations",
+      "Pleroma.Captcha": "Captcha",
+      ":instance": "Instance",
+      ":database": "Database options",
+      ":gopher": "Gopher",
+      ":auth": "Authentication",
+      ":mrf_simple": "MRF",
+      ":web_push_encryption": "Web push encryption",
+      ":activitypub": "Activity pub",
+      ":suggestions": "Suggestions",
+      ":uri_schemes": "URI schemes",
+      ":media_proxy": "Media proxy",
+      ":ldap": "LDAP",
+      ":http_security": "HTTP security",
+      ":rate_limit": "Rate limit",
+      ":user": "User",
+      ":rich_media": "Rich media",
+      ":fetch_initial_posts": "Fetch initial posts",
+      ":hackney_pools": "Hackney pools",
+      ":auto_linker": "Auto linker",
+      "Pleroma.ScheduledActivity": "Scheduled activity"
+    },
     "Pleroma.Upload_form": {
       "uploader": "Uploader",
       "uploader_note": "",
@@ -891,6 +916,7 @@
       "add_mascots": "Add mascot"
     },
     ":mrf_simple_form": {
+      "title": "MRF Simple",
       "media_removal": "List of instances to remove medias from",
       "media_removal_note": "Separate items with ;",
       "media_nsfw": "List of instances to put medias as NSFW(sensitive) from",
@@ -909,29 +935,55 @@
       "banner_removal_note": "Separate items with ;"
     },
     ":mrf_mention_form": {
+      "title": "MRF mention",
       "actors": "Actors",
       "actors_note": "A list of actors, for which to drop any posts mentioning"
     },
+    ":mrf_subchain_form": {
+      "title": "MRF subchain",
+      "note": "This policy processes messages through an alternate pipeline when a given message matches certain criteria. All criteria are configured as a map of regular expressions to lists of policy modules",
+      "regular_expression": "Regular expression",
+      "policy_modules": "Policy modules",
+      "add_match_actor": "Add match actor"
+    },
     ":mrf_rejectnonpublic_form": {
+      "title": "MRF reject non public",
       "allow_followersonly": "Allow followers only",
       "allow_followersonly_note": "whether to allow followers-only posts",
       "allow_direct": "Allow direct",
       "allow_direct_note": "whether to allow direct messages"
     },
     ":mrf_hellthread_form": {
+      "title": "MRF Heallthread",
       "delist_threshold": "",
       "delist_threshold_note": "Number of mentioned users after which the message gets delisted (the message can still be seen, but it will not show up in public timelines and mentioned users won't get notifications about it). Set to 0 to disable.",
       "reject_threshold": "",
       "reject_threshold_note": "Number of mentioned users after which the messaged gets rejected. Set to 0 to disable."
     },
     ":mrf_keyword_form": {
+      "title": "MRF keyword",
       "reject": "Reject",
-      "reject_note": "A list of patterns which result in message being rejected, each pattern can be a string or a regular expression",
+      "reject_note": "A list of patterns which result in message being rejected, each pattern can be a string or a regular expression. Separate items with ;",
       "federated_timeline_removal": "Federated timeline removal",
-      "federated_timeline_removal_note": "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression",
+      "federated_timeline_removal_note": "A list of patterns which result in message being removed from federated timelines (a.k.a unlisted), each pattern can be a string or a regular expression. Separate items with ;",
       "replace": "Replace",
+      "add_replace_item": "Add replace tuple",
+      "pattern": "Pattern",
+      "replacement": "Replacement",
       "replace_note": "A list of tuples containing {pattern, replacement}, pattern can be a string or a regular expression"
     },
+    ":mrf_normalize_markup_form": {
+      "title": "MRF mormalize markup",
+      "scrub_policy": "Scrub policy",
+      "scrub_policy_note": ""
+    },
+    ":mrf_user_allowlist_form": {
+      "title": "MRF user allowlist",
+      "note": "The keys in this section are the domain names that the policy should apply to. Each key should be assigned a list of users that should be allowed through by their ActivityPub ID",
+      "domain": "Domain",
+      "list_of_users": "List of users",
+      "add_domain": "Add domain"
+    },
     ":suggestions_form": {
       "enabled": "Enabled",
       "enabled_note": "",
diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts
index d6447c9..74b9433 100644
--- a/src/services/ConfigService.ts
+++ b/src/services/ConfigService.ts
@@ -9,11 +9,11 @@ export class ConfigService {
   }
 
   static updateConfigSettings (configs) {
-    // console.log(configs)
-    // return new Promise(res => {
-    //   setTimeout(() => res({}), 1000)
-    // })
-    return executeApiRequest('post', urlBuilder(Url.configSettings, {}), {data: configs})
+    console.log(configs)
+    return new Promise(res => {
+      setTimeout(() => res({}), 1000)
+    })
+    // return executeApiRequest('post', urlBuilder(Url.configSettings, {}), {data: configs})
   }
 
   static setConfigToDB () {
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 8dc20ce..14b7616 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,11 +1,6 @@
 import { forIn } from 'lodash'
-import { arrayParams, configKeys } from '../data/Config'
-import { normalizeAutoLinkerConfig } from '../entities/settings/AutoLinkerConfig'
+import { arrayParams, configKeys, normalizerMapper } from '../data/Config'
 import { normalizeRateLimitConfig } from '../entities/settings/RateLimitConfig'
-import { normalizeGopherConfig } from '../entities/settings/GopherConfig'
-import { normalizeEmojiConfig } from '../entities/settings/EmojiConfig'
-import { normalizeEmailsConfig } from '../entities/settings/EmailsConfig'
-import { normalizeAssetsConfig } from '../entities/settings/AssetsConfig'
 
 export default (configs) => {
   const settings = []
@@ -14,27 +9,20 @@ export default (configs) => {
     if (arrayParams[key]) {
       newVal = normalizeConfigValue(newVal, key)
     }
-    if (key === configKeys.AUTO_LINKER) {
-      newVal = normalizeAutoLinkerConfig(newVal)
-    }
-    if (key === configKeys.GOPHER) {
-      newVal = normalizeGopherConfig(newVal)
-    }
-    if (key === configKeys.EMAILS) {
-      newVal = normalizeEmailsConfig(newVal)
-    }
-    if (key === configKeys.ASSETS) {
-      newVal = normalizeAssetsConfig(newVal)
+    if (normalizerMapper[key] && normalizerMapper[key].normalizeBeforeGeneralFunc) {
+      newVal = normalizerMapper[key].normalizer(newVal)
     }
     newVal = key !== configKeys.RATE_LIMIT ? getConfigValue(newVal) : normalizeRateLimitConfig(newVal)
-    if (key === configKeys.EMOJI) {
-      newVal = normalizeEmojiConfig(newVal)
+    if (normalizerMapper[key] && !normalizerMapper[key].normalizeBeforeGeneralFunc) {
+      newVal = normalizerMapper[key].normalizer(newVal)
+    }
+    if (key !== configKeys.MRF_USER_ALLOWLIST || (key === configKeys.MRF_USER_ALLOWLIST && !!newVal.length)) {
+      settings.push({
+        group: 'pleroma',
+        key,
+        value: newVal
+      })
     }
-    settings.push({
-      group: 'pleroma',
-      key,
-      value: newVal
-    })
   })
   return { configs: settings }
 }
-- 
GitLab


From 5e91a5328f367b94932a8f71f5f432852ad2ab63 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 8 Aug 2019 14:14:37 +0300
Subject: [PATCH 48/61] feat: :ecto_repos, :assets

---
 src/components/configSettings/forms/AssetsForm.vue |  4 +++-
 src/entities/settings/EctoReposConfig.ts           |  7 +++++--
 src/services/ConfigService.ts                      | 10 +++++-----
 3 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/src/components/configSettings/forms/AssetsForm.vue b/src/components/configSettings/forms/AssetsForm.vue
index 8387c63..1e4118d 100644
--- a/src/components/configSettings/forms/AssetsForm.vue
+++ b/src/components/configSettings/forms/AssetsForm.vue
@@ -1,5 +1,6 @@
 <template>
-  <div class="assets-form">
+  <div class="assets-form" :style="{margin: margin}">
+    <p class="note">{{$t('config_settings.:assets_form.title')}}</p>
     <p class="title">{{$t('config_settings.:assets_form.mascots')}}</p>
     <div class="va-row" v-for="(mascot, index) in formData.mascots" :key="index">
       <va-input
@@ -85,6 +86,7 @@ export default class AssetsForm extends Vue {
 
 <style scoped lang="scss">
 .assets-form {
+  border-top: 1px solid $border-color;
   .va-row {
     position: relative;
   }
diff --git a/src/entities/settings/EctoReposConfig.ts b/src/entities/settings/EctoReposConfig.ts
index e7bd105..0d3d0d0 100644
--- a/src/entities/settings/EctoReposConfig.ts
+++ b/src/entities/settings/EctoReposConfig.ts
@@ -1,9 +1,12 @@
-import {convertArrayParamsToState} from "../../utils/ConvertConfigToState";
+import { convertArrayParamsToState } from '../../utils/ConvertConfigToState'
+import { arrayParams, configKeys } from '../../data/Config'
 
 export default class EctoReposConfig {
   constructor(existConfig?) {
     this.ecto_repos = [...existConfig]
-    convertArrayParamsToState(this, ['ecto_repos'])
+    convertArrayParamsToState(this, arrayParams[configKeys.ECTO_REPOS])
   }
   ecto_repos: any = ''
 }
+
+export const normalizeEctoReposConfig = (config) => ([...config[0].tuple[1]])
diff --git a/src/services/ConfigService.ts b/src/services/ConfigService.ts
index 74b9433..d6447c9 100644
--- a/src/services/ConfigService.ts
+++ b/src/services/ConfigService.ts
@@ -9,11 +9,11 @@ export class ConfigService {
   }
 
   static updateConfigSettings (configs) {
-    console.log(configs)
-    return new Promise(res => {
-      setTimeout(() => res({}), 1000)
-    })
-    // return executeApiRequest('post', urlBuilder(Url.configSettings, {}), {data: configs})
+    // console.log(configs)
+    // return new Promise(res => {
+    //   setTimeout(() => res({}), 1000)
+    // })
+    return executeApiRequest('post', urlBuilder(Url.configSettings, {}), {data: configs})
   }
 
   static setConfigToDB () {
-- 
GitLab


From 4cfd4f54e1cc07d293c0daddd1bde860e015947b Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 8 Aug 2019 21:22:59 +0300
Subject: [PATCH 49/61] feat: Pleroma.Web.Endpoint

---
 .../configSettings/forms/AssetsForm.vue       |  2 +-
 .../configSettings/forms/ConfigForm.vue       |  1 +
 .../configSettings/ConfigSettingsPage.vue     |  3 +-
 src/data/Config.ts                            | 13 +++-
 src/entities/settings/WebEndpointConfig.ts    | 68 +++++++++++++++++++
 src/i18n/en.json                              | 61 ++++++++++++++++-
 src/utils/GetFieldList.ts                     |  3 +
 7 files changed, 146 insertions(+), 5 deletions(-)
 create mode 100644 src/entities/settings/WebEndpointConfig.ts

diff --git a/src/components/configSettings/forms/AssetsForm.vue b/src/components/configSettings/forms/AssetsForm.vue
index 1e4118d..bd84247 100644
--- a/src/components/configSettings/forms/AssetsForm.vue
+++ b/src/components/configSettings/forms/AssetsForm.vue
@@ -63,7 +63,7 @@ export default class AssetsForm extends Vue {
   get selectOptions () {
     return this.formData.mascots.map(({ name }) => name)
   }
-  error: object = { mascots: [] }
+  error: {mascots?: any} = { mascots: [] }
   get formData () {
     return this.value
   }
diff --git a/src/components/configSettings/forms/ConfigForm.vue b/src/components/configSettings/forms/ConfigForm.vue
index 80f8208..ee03bf4 100644
--- a/src/components/configSettings/forms/ConfigForm.vue
+++ b/src/components/configSettings/forms/ConfigForm.vue
@@ -27,6 +27,7 @@
           class="mb-0"
           :options="selectOptions[field.model]"
           :multiple="field.multiple"
+          :type="field.isTextarea ? 'textarea' : 'text'"
         />
         <p
           class="note"
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 2dd7eef..83ae5fe 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -144,7 +144,7 @@ import EmojiForm from '../../configSettings/forms/EmojiForm.vue'
 import AssetsForm from '../../configSettings/forms/AssetsForm.vue'
 import MrfSubchainForm from '../../configSettings/forms/MRFSubchainForm.vue'
 import MrfKeywordForm from '../../configSettings/forms/MRFKeywordForm.vue'
-import MRFUserAllowlistForm from "../../configSettings/forms/MRFUserAllowlistForm.vue";
+import MRFUserAllowlistForm from '../../configSettings/forms/MRFUserAllowlistForm.vue'
 
 @Component({
   components: {
@@ -218,6 +218,7 @@ export default class ConfigSettingsPage extends Vue {
     return configKeysTabs.filter(({ tab }) => tab)
   }
   validateForms () {
+    // @ts-ignore
     return this.$refs.ASSETS.validate()
   }
 }
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 753ff14..18dc548 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -36,6 +36,8 @@ import EctoReposConfig, {normalizeEctoReposConfig} from '../entities/settings/Ec
 import PushEncryptionConfig from '../entities/settings/PushEncryptionConfig'
 import MRFUserAllowlist, {normalizeMRFUserAllowlistConfig} from "../entities/settings/MRFUserAllowlistConfig";
 import MRFNormalizeMarkupConfig from "../entities/settings/MRFNormalizeMarkupConfig";
+import WebEndpointConfig, {normalizeWebEndpointConfig} from "../entities/settings/WebEndpointConfig";
+import {config} from "@vue/test-utils";
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -49,7 +51,7 @@ export enum configKeys {
   CHAT = ':chat',
   EMOJI = ':emoji',
   ASSETS = ':assets',
-  WEB = 'Pleroma.Web',
+  WEB_ENDPOINT = 'Pleroma.Web.Endpoint',
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
   RICH_MEDIA = ':rich_media',
@@ -98,7 +100,8 @@ export const arrayParams = {
     'avatar_removal',
     'banner_removal'
   ],
-  [configKeys.MRF_KEYWORD]: ['reject', 'federated_timeline_removal']
+  [configKeys.MRF_KEYWORD]: ['reject', 'federated_timeline_removal'],
+  [configKeys.WEB_ENDPOINT]: ['instrumenters', 'extra_cookie_attrs', 'watchers']
 }
 
 export const normalizerMapper = {
@@ -112,6 +115,7 @@ export const normalizerMapper = {
   [configKeys.MRF_SUBCHAIN]: { normalizer: normalizeMRFSubchainConfig, normalizeBeforeGeneralFunc: false },
   [configKeys.MRF_KEYWORD]: { normalizer: normalizeMrfKeywordConfig, normalizeBeforeGeneralFunc: false },
   [configKeys.MRF_USER_ALLOWLIST]: { normalizer: normalizeMRFUserAllowlistConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.WEB_ENDPOINT]: { normalizer: normalizeWebEndpointConfig, normalizeBeforeGeneralFunc: true }
 }
 
 export const configKeysTabs = [
@@ -170,6 +174,11 @@ export const configKeysTabs = [
     tab: false,
     constructor: AssetsConfig,
   },
+  // {
+  //   key: configKeys.WEB_ENDPOINT,
+  //   tab: true,
+  //   constructor: WebEndpointConfig
+  // },
   {
     key: configKeys.CAPTCHA,
     constructor: CaptchaConfig,
diff --git a/src/entities/settings/WebEndpointConfig.ts b/src/entities/settings/WebEndpointConfig.ts
new file mode 100644
index 0000000..2d23a2b
--- /dev/null
+++ b/src/entities/settings/WebEndpointConfig.ts
@@ -0,0 +1,68 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import {arrayParams, configKeys} from '../../data/Config'
+
+class Pubsub {
+  name: string = ''
+  adapter: string = ''
+}
+
+class RenderErrors {
+  accepts: any = ''
+  view: string = ''
+}
+
+class Http {
+  dispatch: string = ''
+  port:  number = 400
+  ip: any = [127, 0, 0, 1]
+  protocol_options: object = {
+    max_request_line_length: 8192,
+    max_header_value_length: 8192
+  }
+}
+
+export default class WebEndpointConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.WEB_ENDPOINT])
+    this.render_errors.accepts = this.render_errors.accepts.join(';')
+    this.http.ip = this.http.ip.uple.join(';')
+    this.http.dispatch = this.http.dispatch[0]
+  }
+  instrumenters: any = []
+  render_errors: RenderErrors = {
+    view: 'Pleroma.Web.ErrorView',
+    accepts: 'json'
+  }
+  pubsub: Pubsub = {
+    name: 'Pleroma.PubSub',
+    adapter: 'Phoenix.PubSub.PG2'
+  }
+  extra_cookie_attrs: any = ["SameSite=Lax"]
+  debug_errors: boolean = true
+  protocol: string = 'http'
+  signing_salt: string = ''
+  code_reloader: boolean = false
+  check_origin: boolean = false
+  watchers: any = []
+  secure_cookie_flag: boolean = false
+  secret_key_base: string = ''
+  url: object = {
+    host: "localhost",
+    scheme: "https",
+    port: 443
+  }
+  http: Http = {
+    dispatch: '',
+    port: 400,
+    ip: [127, 0, 0, 1],
+    protocol_options: {
+      max_request_line_length: 8192,
+      max_header_value_length: 8192
+    }
+  }
+}
+
+export const normalizeWebEndpointConfig = (config) => {
+  const apiConf = {...config}
+  return apiConf
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4a80f14..0cdac41 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -441,6 +441,7 @@
       "Pleroma.Upload": "Upload",
       ":frontend_configurations": "Frontend configurations",
       "Pleroma.Captcha": "Captcha",
+      "Pleroma.Web.Endpoint": "Endpoint",
       ":instance": "Instance",
       ":database": "Database options",
       ":gopher": "Gopher",
@@ -696,7 +697,13 @@
       "follow_redirect": "Follow redirect",
       "follow_redirect_note": "",
       "pool": "Pool",
-      "pool_note": ""
+      "pool_note": "",
+      "port": "Port",
+      "port_note": "",
+      "ip": "IP",
+      "ip_note": "",
+      "dispatch": "Dispatch",
+      "dispatch_note": "Elixir code snipped"
     },
     ":ldap_form": {
       "title": "Use LDAP for user authentication.  When a user logs in to the Pleroma\ninstance, the name and password will be verified by trying to authenticate\n(bind) to an LDAP server.  If a user exists in the LDAP directory but there\nis no account with the same name yet on the Pleroma instance then a new\nPleroma account will be created with the same name as the LDAP user name.",
@@ -1003,6 +1010,58 @@
       "public_key_note": "",
       "private_key": "VAPID private key",
       "private_key_note": ""
+    },
+    "Pleroma.Web.Endpoint_form": {
+      "instrumenters": "Instrumenters",
+      "instrumenters_note": "",
+      "secure_cookie_flag": "Secure cookie flag",
+      "secure_cookie_flag_note": "",
+      "check_origin": "Check origin",
+      "check_origin_note": "",
+      "protocol": "Protocol",
+      "protocol_note": "",
+      "signing_salt": "Signing salt",
+      "signing_salt_note": "",
+      "secret_key_base": "Secret key base",
+      "secret_key_base_note": "",
+      "debug_errors": "Debug errors",
+      "debug_errors_note": "",
+      "code_reloader": "Code reloader",
+      "code_reloader_note": "",
+      "extra_cookie_attrs": "Extra cookie attributes",
+      "extra_cookie_attrs_note": "",
+      "watchers": "Watchers",
+      "watchers_note": ""
+    },
+    "protocol_options_form": {
+      "title": "Protocol options",
+      "max_request_line_length": "Max request line length",
+      "max_request_line_length_note": "",
+      "max_header_value_length": "Max header value length",
+      "max_header_value_length_note": ""
+    },
+    "render_errors_form": {
+      "title": "Render errors",
+      "view": "View",
+      "view_note": "",
+      "accepts": "Accepts",
+      "accepts_note": "Separate items with ;"
+    },
+    "pubsub_form": {
+      "title": "Pubsub",
+      "name": "Name",
+      "name_note": "",
+      "adapter": "Adapter",
+      "adapter_note": ""
+    },
+    "url_form": {
+      "title": "URL",
+      "host": "Host",
+      "host_note": "",
+      "scheme": "Scheme",
+      "scheme_note": "",
+      "port": "Port",
+      "port_note": ""
     }
   }
 }
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 5a9e8c8..28c7e42 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -69,6 +69,9 @@ export const getFieldComponent = (val, key?) => {
     }
   } else if (typeof val === 'string'){
     field.component = 'va-input'
+    if (val.length > 140) {
+      field.isTextarea = true
+    }
   } else if (typeof val === 'number') {
     field.component = 'va-input'
     field.isNumber = true
-- 
GitLab


From 6fdc0bc31f66698155730bd73381be8014424bd3 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 9 Aug 2019 12:12:27 +0300
Subject: [PATCH 50/61] fix: recursive config form rendering in the production
 mode

---
 .../configSettings/forms/ConfigForm.vue       | 59 ++++++++++++-------
 .../configSettings/ConfigSettingsPage.vue     |  4 +-
 2 files changed, 40 insertions(+), 23 deletions(-)

diff --git a/src/components/configSettings/forms/ConfigForm.vue b/src/components/configSettings/forms/ConfigForm.vue
index ee03bf4..717cfee 100644
--- a/src/components/configSettings/forms/ConfigForm.vue
+++ b/src/components/configSettings/forms/ConfigForm.vue
@@ -40,32 +40,49 @@
   </div>
 </template>
 
-<script lang="ts">
-import { Component, Prop, Vue } from 'vue-property-decorator'
-import UploadConfig from '../../../entities/settings/UploadConfig'
+<script>
 import getFieldList, { selectOptions } from '../../../utils/GetFieldList'
 import { StaticRecourcesService } from '../../../services/StaticRecourcesService'
 import { keys } from 'lodash'
 import { configKeys } from '../../../data/Config'
 
-@Component({
-  components: {},
-})
-export default class ConfigForm extends Vue {
-  @Prop(Object) readonly value!: UploadConfig
-  @Prop(String) readonly title!: string
-  @Prop(Boolean) readonly showTitle!: boolean
-  @Prop(String) readonly margin!: string
-  selectOptions = selectOptions
-  get formData () {
-    return this.value
-  }
-  set formData (val) {
-    this.$emit('updateForm', val)
-  }
-  get fields () {
-    return getFieldList(this.formData)
-  }
+export default {
+  name: 'config-form',
+  props: {
+    value: {
+      required: true,
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    showTitle: {
+      type: Boolean,
+      default: false
+    },
+    margin: {
+      type: String,
+      default: '0'
+    }
+  },
+  data () {
+    return {
+      selectOptions
+    }
+  },
+  computed: {
+    formData: {
+      get () {
+        return this.value
+      },
+      set () {
+        this.$emit('updateForm', val)
+      }
+    },
+    fields () {
+      return getFieldList(this.formData)
+    }
+  },
   async mounted () {
     if (this.title === configKeys.FRONTEND_CONFIGURATIONS) {
       const themeConfig = await StaticRecourcesService.getThemesList()
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 83ae5fe..1baac9f 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -138,20 +138,20 @@ import { configKeysTabs, configKeys } from '../../../data/Config'
 import EmailsForm from '../../configSettings/forms/EmailsForm.vue'
 import ConvertConfigToState from '../../../utils/ConvertConfigToState'
 import ConvertConfigToApiRequest from '../../../utils/ConvertConfigToApiRequest'
-import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 import AutoLinkerForm from '../../configSettings/forms/AutoLinkerForm.vue'
 import EmojiForm from '../../configSettings/forms/EmojiForm.vue'
 import AssetsForm from '../../configSettings/forms/AssetsForm.vue'
 import MrfSubchainForm from '../../configSettings/forms/MRFSubchainForm.vue'
 import MrfKeywordForm from '../../configSettings/forms/MRFKeywordForm.vue'
 import MRFUserAllowlistForm from '../../configSettings/forms/MRFUserAllowlistForm.vue'
+import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
 
 @Component({
   components: {
+    ConfigForm,
     MRFUserAllowlistForm,
     EmojiForm,
     AutoLinkerForm,
-    ConfigForm,
     EmailsForm,
     AssetsForm,
     MrfSubchainForm,
-- 
GitLab


From facb901ec9b388e74d903b53b38018c4832c0f0d Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 9 Aug 2019 13:34:23 +0300
Subject: [PATCH 51/61] feat: Pleroma.Web.Endpoint normalizer

---
 src/data/Config.ts                         | 15 +++++----------
 src/entities/settings/WebEndpointConfig.ts |  7 +++++--
 2 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 18dc548..59bbec7 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -154,11 +154,6 @@ export const configKeysTabs = [
     constructor: FrontentConfigurationsConfig,
     tab: true,
   },
-  // {
-  //   key: configKeys.WEB,
-  //   name: 'Web',
-  //   tab: false,
-  // },
   {
     key: configKeys.CHAT,
     tab: false,
@@ -174,11 +169,11 @@ export const configKeysTabs = [
     tab: false,
     constructor: AssetsConfig,
   },
-  // {
-  //   key: configKeys.WEB_ENDPOINT,
-  //   tab: true,
-  //   constructor: WebEndpointConfig
-  // },
+  {
+    key: configKeys.WEB_ENDPOINT,
+    tab: true,
+    constructor: WebEndpointConfig
+  },
   {
     key: configKeys.CAPTCHA,
     constructor: CaptchaConfig,
diff --git a/src/entities/settings/WebEndpointConfig.ts b/src/entities/settings/WebEndpointConfig.ts
index 2d23a2b..4dd248a 100644
--- a/src/entities/settings/WebEndpointConfig.ts
+++ b/src/entities/settings/WebEndpointConfig.ts
@@ -12,7 +12,7 @@ class RenderErrors {
 }
 
 class Http {
-  dispatch: string = ''
+  dispatch: any = ''
   port:  number = 400
   ip: any = [127, 0, 0, 1]
   protocol_options: object = {
@@ -62,7 +62,10 @@ export default class WebEndpointConfig {
   }
 }
 
-export const normalizeWebEndpointConfig = (config) => {
+export const normalizeWebEndpointConfig = (config: WebEndpointConfig) => {
   const apiConf = {...config}
+  apiConf.render_errors.accepts = apiConf.render_errors.accepts.split(';')
+  apiConf.http.dispatch = [apiConf.http.dispatch]
+  apiConf.http.ip = apiConf.http.ip.split(';')
   return apiConf
 }
-- 
GitLab


From 9293a850a27bf92a7f9989579b2385eeb9faf346 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 9 Aug 2019 14:32:30 +0300
Subject: [PATCH 52/61] feat: Pleroma.Web.Metadata

---
 src/data/Config.ts                         | 14 ++++++++++----
 src/entities/settings/WebMetadataConfig.ts |  7 +++++++
 src/i18n/en.json                           |  7 +++++++
 src/utils/GetFieldList.ts                  |  1 +
 4 files changed, 25 insertions(+), 4 deletions(-)
 create mode 100644 src/entities/settings/WebMetadataConfig.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 59bbec7..386f17c 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -34,10 +34,10 @@ import MRFMention from '../entities/settings/MRFMention'
 import SuggestionsConfig from '../entities/settings/SuggestionsConfig'
 import EctoReposConfig, {normalizeEctoReposConfig} from '../entities/settings/EctoReposConfig'
 import PushEncryptionConfig from '../entities/settings/PushEncryptionConfig'
-import MRFUserAllowlist, {normalizeMRFUserAllowlistConfig} from "../entities/settings/MRFUserAllowlistConfig";
-import MRFNormalizeMarkupConfig from "../entities/settings/MRFNormalizeMarkupConfig";
-import WebEndpointConfig, {normalizeWebEndpointConfig} from "../entities/settings/WebEndpointConfig";
-import {config} from "@vue/test-utils";
+import MRFUserAllowlist, {normalizeMRFUserAllowlistConfig} from '../entities/settings/MRFUserAllowlistConfig'
+import MRFNormalizeMarkupConfig from '../entities/settings/MRFNormalizeMarkupConfig'
+import WebEndpointConfig, {normalizeWebEndpointConfig} from '../entities/settings/WebEndpointConfig'
+import WebMetadataConfig from '../entities/settings/WebMetadataConfig'
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -52,6 +52,7 @@ export enum configKeys {
   EMOJI = ':emoji',
   ASSETS = ':assets',
   WEB_ENDPOINT = 'Pleroma.Web.Endpoint',
+  WEB_METADATA = 'Pleroma.Web.Metadata',
   CAPTCHA = 'Pleroma.Captcha',
   KOCAPTCHA = 'Pleroma.Captcha.Kocaptcha',
   RICH_MEDIA = ':rich_media',
@@ -174,6 +175,11 @@ export const configKeysTabs = [
     tab: true,
     constructor: WebEndpointConfig
   },
+  {
+    key: configKeys.WEB_METADATA,
+    tab: true,
+    constructor: WebMetadataConfig,
+  },
   {
     key: configKeys.CAPTCHA,
     constructor: CaptchaConfig,
diff --git a/src/entities/settings/WebMetadataConfig.ts b/src/entities/settings/WebMetadataConfig.ts
new file mode 100644
index 0000000..1c1d912
--- /dev/null
+++ b/src/entities/settings/WebMetadataConfig.ts
@@ -0,0 +1,7 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class WebMetadataConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0cdac41..003bbb7 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -442,6 +442,7 @@
       ":frontend_configurations": "Frontend configurations",
       "Pleroma.Captcha": "Captcha",
       "Pleroma.Web.Endpoint": "Endpoint",
+      "Pleroma.Web.Metadata": "Metadata",
       ":instance": "Instance",
       ":database": "Database options",
       ":gopher": "Gopher",
@@ -1062,6 +1063,12 @@
       "scheme_note": "",
       "port": "Port",
       "port_note": ""
+    },
+    "Pleroma.Web.Metadata_form": {
+      "unfurl_nsfw": "Show nsfw attachments in previews",
+      "unfurl_nsfw_note": "",
+      "providers": "Providers",
+      "providers_note": "Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the <header> as <link rel=me>"
     }
   }
 }
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 28c7e42..38a9b66 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -40,6 +40,7 @@ export const selectOptions = {
   valid_schemes: ['https', 'http', 'dat', 'dweb', 'gopher', 'ipfs', 'ipns', 'irc', 'ircs', 'magnet', 'mailto', 'mumble', 'ssb', 'xmpp'],
   referrer_policy: ['same-origin', 'no-referrer'],
   parsers: ['Pleroma.Web.RichMedia.Parsers.TwitterCard', 'Pleroma.Web.RichMedia.Parsers.OGP', 'Pleroma.Web.RichMedia.Parsers.OEmbed'],
+  providers: ['Pleroma.Web.Metadata.Providers.OpenGraph', 'Pleroma.Web.Metadata.Providers.TwitterCard', 'Pleroma.Web.Metadata.Providers.RelMe']
 }
 
 export default (formData) => {
-- 
GitLab


From f2f16149cb70674a4bb9fa18cecfdff43d8ba6af Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Fri, 9 Aug 2019 14:56:36 +0300
Subject: [PATCH 53/61] feat: Pleroma.Uploaders.MDII

---
 .../pages/configSettings/ConfigSettingsPage.vue          | 6 ++++++
 src/data/Config.ts                                       | 7 +++++++
 src/entities/settings/UploadersMDIIConfig.ts             | 9 +++++++++
 src/i18n/en.json                                         | 6 +++++-
 4 files changed, 27 insertions(+), 1 deletion(-)
 create mode 100644 src/entities/settings/UploadersMDIIConfig.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 1baac9f..55cc35c 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -32,6 +32,12 @@
           v-model="config[configKeysEnum.UPLOADERSLOCAL]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.UPLOADERSMDII"
+          v-if="tabs[value].key === configKeysEnum.UPLOAD"
+          v-model="config[configKeysEnum.UPLOADERSMDII]"
+          show-title
+        />
         <config-form
           :title="configKeysEnum.CHAT"
           v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 386f17c..1955c3a 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -38,11 +38,13 @@ import MRFUserAllowlist, {normalizeMRFUserAllowlistConfig} from '../entities/set
 import MRFNormalizeMarkupConfig from '../entities/settings/MRFNormalizeMarkupConfig'
 import WebEndpointConfig, {normalizeWebEndpointConfig} from '../entities/settings/WebEndpointConfig'
 import WebMetadataConfig from '../entities/settings/WebMetadataConfig'
+import UploadersMDIIConfig from "../entities/settings/UploadersMDIIConfig";
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
   UPLOADERSS3 = 'Pleroma.Uploaders.S3',
   UPLOADERSLOCAL = 'Pleroma.Uploaders.Local',
+  UPLOADERSMDII = 'Pleroma.Uploaders.MDII',
   EMAILS = 'Pleroma.Emails.Mailer',
   URI_SCHEMES = ':uri_schemes',
   INSTANCE = ':instance',
@@ -140,6 +142,11 @@ export const configKeysTabs = [
     tab: false,
     constructor: UploadersLocalConfig,
   },
+  {
+    key: configKeys.UPLOADERSMDII,
+    tab: false,
+    constructor: UploadersMDIIConfig,
+  },
   {
     key: configKeys.INSTANCE,
     constructor: InstanceConfig,
diff --git a/src/entities/settings/UploadersMDIIConfig.ts b/src/entities/settings/UploadersMDIIConfig.ts
new file mode 100644
index 0000000..9965204
--- /dev/null
+++ b/src/entities/settings/UploadersMDIIConfig.ts
@@ -0,0 +1,9 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class UploadersMDIIConfig {
+  constructor (existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  cgi: string = ''
+  files: string = ''
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 003bbb7..d0927d4 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -496,7 +496,11 @@
       "truncated_namespace_note": "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc.\n        For example, when using CDN to S3 virtual host format, set \"\".\n        At this time, write CNAME to CDN in public_endpoint."
     },
     "Pleroma.Uploaders.MDII_form": {
-
+      "title": "Pleroma.Uploaders.MDII",
+      "cgi": "CGI",
+      "cgi_note": "",
+      "files": "Files",
+      "files_note": ""
     },
     "Pleroma.Emails.Mailer_form": {
       "adapters_options": "Adapter's option",
-- 
GitLab


From ffe89c6fb10819e66858a863d05cce4650c8a682 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Mon, 12 Aug 2019 14:47:18 +0300
Subject: [PATCH 54/61] feat: :queues, Pleroma.Web.Federator.RetryQueue, + add
 configGroup enum

---
 .../configSettings/ConfigSettingsPage.vue     |  6 ++
 src/data/Config.ts                            | 66 ++++++++++++++++++-
 src/entities/settings/JobQueueConfig.ts       | 13 ++++
 .../settings/WebFederationQueueConfig.ts      | 11 ++++
 src/i18n/en.json                              | 28 +++++++-
 src/utils/ConvertConfigToApiRequest.js        | 16 +++--
 6 files changed, 133 insertions(+), 7 deletions(-)
 create mode 100644 src/entities/settings/JobQueueConfig.ts
 create mode 100644 src/entities/settings/WebFederationQueueConfig.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 55cc35c..b58f1d6 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -121,6 +121,12 @@
           v-model="config[configKeysEnum.MRF_USER_ALLOWLIST]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.WEB_FEDERATOR_RETRY_QUEUE"
+          v-if="tabs[value].key === configKeysEnum.JOB_QUEUE"
+          v-model="config[configKeysEnum.WEB_FEDERATOR_RETRY_QUEUE]"
+          showTitle
+        />
       </div>
       <div class="loading flex-center" v-if="loading">
         <fulfilling-bouncing-circle-spinner
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 1955c3a..9145cec 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -39,6 +39,13 @@ import MRFNormalizeMarkupConfig from '../entities/settings/MRFNormalizeMarkupCon
 import WebEndpointConfig, {normalizeWebEndpointConfig} from '../entities/settings/WebEndpointConfig'
 import WebMetadataConfig from '../entities/settings/WebMetadataConfig'
 import UploadersMDIIConfig from "../entities/settings/UploadersMDIIConfig";
+import JobQueueConfig from "../entities/settings/JobQueueConfig";
+import WebFederationQueueConfig from "../entities/settings/WebFederationQueueConfig";
+
+export enum configGroups {
+  PLEROMA = 'pleroma',
+  PLEROMA_JOB_QUEUE = 'pleroma_job_queue'
+}
 
 export enum configKeys {
   UPLOAD = 'Pleroma.Upload',
@@ -82,7 +89,9 @@ export enum configKeys {
   MRF_USER_ALLOWLIST = ':mrf_user_allowlist',
   MRF_NORMALIZE_MARKUP = ':mrf_normalize_markup',
   SUGGESTIONS = ':suggestions',
-  PUSH_ENCRYPTION = ':web_push_encryption'
+  PUSH_ENCRYPTION = ':web_push_encryption',
+  JOB_QUEUE = ':queues',
+  WEB_FEDERATOR_RETRY_QUEUE = 'Pleroma.Web.Federator.RetryQueue'
 }
 
 export const arrayParams = {
@@ -121,34 +130,46 @@ export const normalizerMapper = {
   [configKeys.WEB_ENDPOINT]: { normalizer: normalizeWebEndpointConfig, normalizeBeforeGeneralFunc: true }
 }
 
+export const getConfigGroup = (configKey) => {
+  const configObj = configKeysTabs.find(({ key }) => key === configKey)
+  // @ts-ignore
+  return configObj.group || configGroups.PLEROMA
+}
+
 export const configKeysTabs = [
   {
     key: configKeys.EMAILS,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: EmailsConfig,
   },
   {
     key: configKeys.UPLOAD,
+    group: configGroups.PLEROMA,
     constructor: UploadConfig,
     tab: true,
   },
   {
     key: configKeys.UPLOADERSS3,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: UploadersS3Config,
   },
   {
     key: configKeys.UPLOADERSLOCAL,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: UploadersLocalConfig,
   },
   {
     key: configKeys.UPLOADERSMDII,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: UploadersMDIIConfig,
   },
   {
     key: configKeys.INSTANCE,
+    group: configGroups.PLEROMA,
     constructor: InstanceConfig,
     tab: true,
   },
@@ -159,46 +180,55 @@ export const configKeysTabs = [
   },
   {
     key: configKeys.FRONTEND_CONFIGURATIONS,
+    group: configGroups.PLEROMA,
     constructor: FrontentConfigurationsConfig,
     tab: true,
   },
   {
     key: configKeys.CHAT,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: ChatConfig,
   },
   {
     key: configKeys.EMOJI,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: EmojiConfig,
   },
   {
     key: configKeys.ASSETS,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: AssetsConfig,
   },
   {
     key: configKeys.WEB_ENDPOINT,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: WebEndpointConfig
   },
   {
     key: configKeys.WEB_METADATA,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: WebMetadataConfig,
   },
   {
     key: configKeys.CAPTCHA,
+    group: configGroups.PLEROMA,
     constructor: CaptchaConfig,
     tab: true,
   },
   {
     key: configKeys.KOCAPTCHA,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: KocaptchaConfig
   },
   {
     key: configKeys.URI_SCHEMES,
+    group: configGroups.PLEROMA,
     constructor: UriSchemesConfig,
     tab: true,
   },
@@ -209,51 +239,61 @@ export const configKeysTabs = [
   },
   {
     key: configKeys.ECTO_REPOS,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: EctoReposConfig,
   },
   {
     key: configKeys.MEDIA_PROXY,
+    group: configGroups.PLEROMA,
     constructor: MediaProxyConfig,
     tab: true,
   },
   {
     key: configKeys.GOPHER,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: GopherConfig,
   },
   {
     key: configKeys.LDAP,
+    group: configGroups.PLEROMA,
     constructor: LdapConfig,
     tab: true,
   },
   {
     key: configKeys.ACTIVITY_PUB,
+    group: configGroups.PLEROMA,
     constructor: ActivityPubConfig,
     tab: true,
   },
   {
     key: configKeys.USER,
+    group: configGroups.PLEROMA,
     constructor: UserConfig,
     tab: true,
   },
   {
     key: configKeys.HTTP_SECURITY,
+    group: configGroups.PLEROMA,
     constructor: HttpSecurityConfig,
     tab: true,
   },
   {
     key: configKeys.RICH_MEDIA,
+    group: configGroups.PLEROMA,
     constructor: RichMediaConfig,
     tab: true,
   },
   {
     key: configKeys.FETCH_INITIAL_POSTS,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: FetchInitialPostsConfig,
   },
   {
     key: configKeys.HACKNEY_POOLS,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: HackneyPoolsConfig,
   },
@@ -264,6 +304,7 @@ export const configKeysTabs = [
   },
   {
     key: configKeys.SCHEDULED_ACTIVITY,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: ScheduledActivityConfig,
   },
@@ -274,36 +315,43 @@ export const configKeysTabs = [
   // },
   {
     key: configKeys.OAUTH2,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: Oauth2Config,
   },
   {
     key: configKeys.RATE_LIMIT,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: RateLimitConfig,
   },
   {
     key: configKeys.MRF_SIMPLE,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: MRFSimple,
   },
   {
     key: configKeys.MRF_SUBCHAIN,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: MRFSubchainConfig,
   },
   {
     key: configKeys.MRF_REJECTNONPUBLIC,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: MRFRejectnonpublic,
   },
   {
     key: configKeys.MRF_HELLTHREAD,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: MRFHellthread,
   },
   {
     key: configKeys.MRF_KEYWORD,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: MRFKeyword,
   },
@@ -319,11 +367,13 @@ export const configKeysTabs = [
   },
   {
     key: configKeys.MRF_NORMALIZE_MARKUP,
+    group: configGroups.PLEROMA,
     tab: false,
     constructor: MRFNormalizeMarkupConfig,
   },
   {
     key: configKeys.SUGGESTIONS,
+    group: configGroups.PLEROMA,
     tab: true,
     constructor: SuggestionsConfig,
   },
@@ -331,5 +381,17 @@ export const configKeysTabs = [
     key: configKeys.PUSH_ENCRYPTION,
     tab: true,
     constructor: PushEncryptionConfig,
-  }
+  },
+  {
+    key: configKeys.JOB_QUEUE,
+    group: configGroups.PLEROMA_JOB_QUEUE,
+    tab: true,
+    constructor: JobQueueConfig,
+  },
+  {
+    key: configKeys.WEB_FEDERATOR_RETRY_QUEUE,
+    group: configGroups.PLEROMA,
+    tab: false,
+    constructor: WebFederationQueueConfig,
+  },
 ]
diff --git a/src/entities/settings/JobQueueConfig.ts b/src/entities/settings/JobQueueConfig.ts
new file mode 100644
index 0000000..4ebdf05
--- /dev/null
+++ b/src/entities/settings/JobQueueConfig.ts
@@ -0,0 +1,13 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class JobQueueConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  federator_outgoing: number = 0
+  federator_incoming: number = 0
+  mailer: string = ''
+  transmogrifier: number = 0
+  web_push: boolean = false
+  scheduled_activities: string = ''
+}
diff --git a/src/entities/settings/WebFederationQueueConfig.ts b/src/entities/settings/WebFederationQueueConfig.ts
new file mode 100644
index 0000000..1b15136
--- /dev/null
+++ b/src/entities/settings/WebFederationQueueConfig.ts
@@ -0,0 +1,11 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+export default class WebFederationQueueConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this)
+  }
+  enabled: boolean = false
+  max_jobs: number = 0
+  initial_timeout: number = 0
+  max_retries: number = 0
+}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d0927d4..00d40b7 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -461,7 +461,8 @@
       ":fetch_initial_posts": "Fetch initial posts",
       ":hackney_pools": "Hackney pools",
       ":auto_linker": "Auto linker",
-      "Pleroma.ScheduledActivity": "Scheduled activity"
+      "Pleroma.ScheduledActivity": "Scheduled activity",
+      ":queues": "Job queue"
     },
     "Pleroma.Upload_form": {
       "uploader": "Uploader",
@@ -1073,6 +1074,31 @@
       "unfurl_nsfw_note": "",
       "providers": "Providers",
       "providers_note": "Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the <header> as <link rel=me>"
+    },
+    "Pleroma.Web.Federator.RetryQueue_form": {
+      "title": "Web.Federator.RetryQueue",
+      "enabled": "Enabled",
+      "enabled_note": "",
+      "max_jobs": "Max jobs",
+      "max_jobs_note": "The maximum amount of parallel federation jobs running at the same time",
+      "initial_timeout": "Initial timeout (seconds)",
+      "initial_timeout_note": "",
+      "max_retries": "The maximum number of times a federation job is retried",
+      "max_retries_note": ""
+    },
+    ":queues_form": {
+      "federator_outgoing": "Outgoing federation",
+      "federator_outgoing_note": "",
+      "federator_incoming": "Incoming federation",
+      "federator_incoming_note": "",
+      "mailer": "Email sender",
+      "mailer_note": "",
+      "transmogrifier": "Transmogrifier",
+      "transmogrifier_note": "",
+      "web_push": "Web push notifications",
+      "web_push_note": "",
+      "scheduled_activities": "Scheduled activities",
+      "scheduled_activities_note": ""
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 14b7616..7e0b92d 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -1,6 +1,7 @@
-import { forIn } from 'lodash'
-import { arrayParams, configKeys, normalizerMapper } from '../data/Config'
+import { forIn, isEqual } from 'lodash'
+import { arrayParams, configKeys, normalizerMapper, getConfigGroup } from '../data/Config'
 import { normalizeRateLimitConfig } from '../entities/settings/RateLimitConfig'
+import JobQueueConfig from '../entities/settings/JobQueueConfig'
 
 export default (configs) => {
   const settings = []
@@ -16,9 +17,9 @@ export default (configs) => {
     if (normalizerMapper[key] && !normalizerMapper[key].normalizeBeforeGeneralFunc) {
       newVal = normalizerMapper[key].normalizer(newVal)
     }
-    if (key !== configKeys.MRF_USER_ALLOWLIST || (key === configKeys.MRF_USER_ALLOWLIST && !!newVal.length)) {
+    if (checkConfigIsNeedsToBePushed(key, newVal)) {
       settings.push({
-        group: 'pleroma',
+        group: getConfigGroup(key),
         key,
         value: newVal
       })
@@ -27,6 +28,13 @@ export default (configs) => {
   return { configs: settings }
 }
 
+const checkConfigIsNeedsToBePushed = (key, configVal) => {
+  return (key !== configKeys.MRF_USER_ALLOWLIST ||
+    (key === configKeys.MRF_USER_ALLOWLIST && !!configVal.length)) &&
+    (key !== configKeys.JOB_QUEUE ||
+    (key === configKeys.JOB_QUEUE && !isEqual(getConfigValue(new JobQueueConfig()), configVal)))
+}
+
 const normalizeConfigValue = (config, key) => {
   arrayParams[key].forEach(param => {
     config[param] = config[param] ? config[param].split(';') : []
-- 
GitLab


From fc0c18fded3c63f685b19c14867f53d9f748f466 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 13 Aug 2019 15:17:51 +0300
Subject: [PATCH 55/61] feat: :admin_token, try to fix error in
 Pleroma.Web.Endpoint(still has an error after the server restarting)

---
 src/data/Config.ts                         | 18 +++++++++---
 src/entities/settings/AdminTokenConfig.ts  | 10 +++++++
 src/entities/settings/AuthConfig.ts        | 11 ++++++++
 src/entities/settings/AuthConstructor.ts   |  8 ------
 src/entities/settings/WebEndpointConfig.ts | 33 +++++++++++++++++-----
 src/i18n/en.json                           | 15 +++++++++-
 src/utils/ConvertConfigToApiRequest.js     |  4 ++-
 7 files changed, 78 insertions(+), 21 deletions(-)
 create mode 100644 src/entities/settings/AdminTokenConfig.ts
 create mode 100644 src/entities/settings/AuthConfig.ts
 delete mode 100644 src/entities/settings/AuthConstructor.ts

diff --git a/src/data/Config.ts b/src/data/Config.ts
index 9145cec..73a9af2 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -41,6 +41,8 @@ import WebMetadataConfig from '../entities/settings/WebMetadataConfig'
 import UploadersMDIIConfig from "../entities/settings/UploadersMDIIConfig";
 import JobQueueConfig from "../entities/settings/JobQueueConfig";
 import WebFederationQueueConfig from "../entities/settings/WebFederationQueueConfig";
+import AuthConfig from "../entities/settings/AuthConfig";
+import AdminTokenConfig, {normalizeAdminTokenConfig} from "../entities/settings/AdminTokenConfig";
 
 export enum configGroups {
   PLEROMA = 'pleroma',
@@ -91,7 +93,8 @@ export enum configKeys {
   SUGGESTIONS = ':suggestions',
   PUSH_ENCRYPTION = ':web_push_encryption',
   JOB_QUEUE = ':queues',
-  WEB_FEDERATOR_RETRY_QUEUE = 'Pleroma.Web.Federator.RetryQueue'
+  WEB_FEDERATOR_RETRY_QUEUE = 'Pleroma.Web.Federator.RetryQueue',
+  ADMIN_TOKEN = ':admin_token'
 }
 
 export const arrayParams = {
@@ -113,7 +116,8 @@ export const arrayParams = {
     'banner_removal'
   ],
   [configKeys.MRF_KEYWORD]: ['reject', 'federated_timeline_removal'],
-  [configKeys.WEB_ENDPOINT]: ['instrumenters', 'extra_cookie_attrs', 'watchers']
+  [configKeys.WEB_ENDPOINT]: ['instrumenters', 'extra_cookie_attrs', 'watchers'],
+  [configKeys.AUTH]: ['oauth_consumer_strategies']
 }
 
 export const normalizerMapper = {
@@ -127,7 +131,8 @@ export const normalizerMapper = {
   [configKeys.MRF_SUBCHAIN]: { normalizer: normalizeMRFSubchainConfig, normalizeBeforeGeneralFunc: false },
   [configKeys.MRF_KEYWORD]: { normalizer: normalizeMrfKeywordConfig, normalizeBeforeGeneralFunc: false },
   [configKeys.MRF_USER_ALLOWLIST]: { normalizer: normalizeMRFUserAllowlistConfig, normalizeBeforeGeneralFunc: false },
-  [configKeys.WEB_ENDPOINT]: { normalizer: normalizeWebEndpointConfig, normalizeBeforeGeneralFunc: true }
+  [configKeys.WEB_ENDPOINT]: { normalizer: normalizeWebEndpointConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.ADMIN_TOKEN]: { normalizer: normalizeAdminTokenConfig, normalizeBeforeGeneralFunc: false }
 }
 
 export const getConfigGroup = (configKey) => {
@@ -311,8 +316,13 @@ export const configKeysTabs = [
   // {
   //   key: configKeys.AUTH,
   //   tab: true,
-  //   constructor: AuthConstructor
+  //   constructor: AuthConfig
   // },
+  {
+    key: configKeys.ADMIN_TOKEN,
+    tab: true,
+    constructor: AdminTokenConfig
+  },
   {
     key: configKeys.OAUTH2,
     group: configGroups.PLEROMA,
diff --git a/src/entities/settings/AdminTokenConfig.ts b/src/entities/settings/AdminTokenConfig.ts
new file mode 100644
index 0000000..ec88c96
--- /dev/null
+++ b/src/entities/settings/AdminTokenConfig.ts
@@ -0,0 +1,10 @@
+export default class AdminTokenConfig {
+  constructor(existConfig?) {
+    this.token = existConfig || ''
+  }
+  token: string = ''
+}
+
+export const normalizeAdminTokenConfig = (config) => {
+  return config[0].tuple[1]
+}
diff --git a/src/entities/settings/AuthConfig.ts b/src/entities/settings/AuthConfig.ts
new file mode 100644
index 0000000..59f165d
--- /dev/null
+++ b/src/entities/settings/AuthConfig.ts
@@ -0,0 +1,11 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+import {arrayParams, configKeys} from '../../data/Config'
+
+export default class AuthConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.AUTH])
+  }
+  oauth_consumer_strategies: any = []
+  auth_template: string = ''
+  oauth_consumer_template: string = ''
+}
diff --git a/src/entities/settings/AuthConstructor.ts b/src/entities/settings/AuthConstructor.ts
deleted file mode 100644
index a5320b6..0000000
--- a/src/entities/settings/AuthConstructor.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
-
-export default class AuthConstructor {
-  constructor(existConfig?) {
-    normalizeApiConfig(existConfig, this)
-  }
-  oauth_consumer_strategies: any = []
-}
diff --git a/src/entities/settings/WebEndpointConfig.ts b/src/entities/settings/WebEndpointConfig.ts
index 4dd248a..99b65be 100644
--- a/src/entities/settings/WebEndpointConfig.ts
+++ b/src/entities/settings/WebEndpointConfig.ts
@@ -23,10 +23,20 @@ class Http {
 
 export default class WebEndpointConfig {
   constructor(existConfig?) {
+    const httpIndex = existConfig.findIndex(({ tuple }) => tuple[0] === ':http')
+    if (httpIndex !== -1) {
+      const ip = existConfig[httpIndex].tuple[1].find(({ tuple }) => tuple[0] === ':ip')
+      if(ip.tuple && ip) {
+        this.http.ip = ip.tuple[1] && ip.tuple[1].tuple ? ip.tuple[1].tuple.join(';') : ''
+      }
+      const dispatch = existConfig[httpIndex].tuple[1].find(({ tuple }) => tuple[0] === ':dispatch')
+      if (dispatch.tuple && dispatch) {
+        this.http.dispatch = dispatch.tuple[1] ? dispatch.tuple[1][0] : ''
+      }
+      existConfig.splice(httpIndex, 1)
+    }
     normalizeApiConfig(existConfig, this, arrayParams[configKeys.WEB_ENDPOINT])
     this.render_errors.accepts = this.render_errors.accepts.join(';')
-    this.http.ip = this.http.ip.uple.join(';')
-    this.http.dispatch = this.http.dispatch[0]
   }
   instrumenters: any = []
   render_errors: RenderErrors = {
@@ -62,10 +72,19 @@ export default class WebEndpointConfig {
   }
 }
 
-export const normalizeWebEndpointConfig = (config: WebEndpointConfig) => {
-  const apiConf = {...config}
-  apiConf.render_errors.accepts = apiConf.render_errors.accepts.split(';')
-  apiConf.http.dispatch = [apiConf.http.dispatch]
-  apiConf.http.ip = apiConf.http.ip.split(';')
+export const normalizeWebEndpointConfig = (config) => {
+  const apiConf = [...config]
+  const render_errors = apiConf.find(({ tuple }) => tuple[0] === ':render_errors')
+  if (render_errors.tuple && render_errors && render_errors.tuple[1]) {
+    const accepts = render_errors.tuple[1].find(({ tuple }) => tuple[0] === ':accepts')
+    accepts.tuple[1] = accepts.tuple[1].split(';')
+  }
+  const http = apiConf.find(({ tuple }) => tuple[0] === ':http')
+  if (http.tuple && http && http.tuple[1]) {
+    const dispatch = http.tuple[1].find(({ tuple }) => tuple[0] === ':dispatch')
+    dispatch.tuple[1] = [dispatch.tuple[1]]
+    const ip = http.tuple[1].find(({ tuple }) => tuple[0] === ':ip')
+    ip.tuple[1] = { tuple: ip.tuple[1].split(';')}
+  }
   return apiConf
 }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 00d40b7..13edc25 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -462,7 +462,8 @@
       ":hackney_pools": "Hackney pools",
       ":auto_linker": "Auto linker",
       "Pleroma.ScheduledActivity": "Scheduled activity",
-      ":queues": "Job queue"
+      ":queues": "Job queue",
+      ":admin_token":  "Admin token"
     },
     "Pleroma.Upload_form": {
       "uploader": "Uploader",
@@ -1099,6 +1100,18 @@
       "web_push_note": "",
       "scheduled_activities": "Scheduled activities",
       "scheduled_activities_note": ""
+    },
+    ":auth_form": {
+      "auth_template": "authentication form template",
+      "auth_template_note": "By default it's show.html which corresponds to lib/pleroma/web/templates/o_auth/o_auth/show.html.eex",
+      "oauth_consumer_template": "OAuth consumer mode authentication form template",
+      "oauth_consumer_template_note": "By default it's consumer.html which corresponds to lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex",
+      "oauth_consumer_strategies": "OAuth consumer strategies",
+      "oauth_consumer_strategies_note": "By default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. Each entry in this space-delimited string should be of format <strategy> or <strategy>:<dependency> (e.g. twitter or keycloak:ueberauth_keycloak_strategy in case dependency is named differently than ueberauth_<strategy>)"
+    },
+    ":admin_token_form": {
+      "token": "Admin token",
+      "token_note": "Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter"
     }
   }
 }
diff --git a/src/utils/ConvertConfigToApiRequest.js b/src/utils/ConvertConfigToApiRequest.js
index 7e0b92d..604393a 100644
--- a/src/utils/ConvertConfigToApiRequest.js
+++ b/src/utils/ConvertConfigToApiRequest.js
@@ -32,7 +32,9 @@ const checkConfigIsNeedsToBePushed = (key, configVal) => {
   return (key !== configKeys.MRF_USER_ALLOWLIST ||
     (key === configKeys.MRF_USER_ALLOWLIST && !!configVal.length)) &&
     (key !== configKeys.JOB_QUEUE ||
-    (key === configKeys.JOB_QUEUE && !isEqual(getConfigValue(new JobQueueConfig()), configVal)))
+    (key === configKeys.JOB_QUEUE && !isEqual(getConfigValue(new JobQueueConfig()), configVal))) &&
+    (key !== configKeys.ADMIN_TOKEN ||
+    (key === configKeys.ADMIN_TOKEN && !!configVal.length))
 }
 
 const normalizeConfigValue = (config, key) => {
-- 
GitLab


From 1656cafd230e71a561917739fb3e7c86ff6982a2 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 13 Aug 2019 17:50:40 +0300
Subject: [PATCH 56/61] feat: auth strategies

---
 .../configSettings/ConfigSettingsPage.vue     | 26 +++++++-
 src/data/Config.ts                            | 60 +++++++++++++++----
 src/entities/settings/UeberauthConfigs.ts     | 27 +++++++++
 src/entities/settings/WebEndpointConfig.ts    | 31 +++++-----
 src/i18n/en.json                              | 35 +++++++++++
 5 files changed, 151 insertions(+), 28 deletions(-)
 create mode 100644 src/entities/settings/UeberauthConfigs.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index b58f1d6..419eb4d 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -68,9 +68,33 @@
           v-model="config[configKeysEnum.KOCAPTCHA]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.TWITTER_OAUTH"
+          v-if="tabs[value].key === configKeysEnum.AUTH"
+          v-model="config[configKeysEnum.TWITTER_OAUTH]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.FACEBOOK_OAUTH"
+          v-if="tabs[value].key === configKeysEnum.AUTH"
+          v-model="config[configKeysEnum.FACEBOOK_OAUTH]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.GOOGLE_OAUTH"
+          v-if="tabs[value].key === configKeysEnum.AUTH"
+          v-model="config[configKeysEnum.GOOGLE_OAUTH]"
+          showTitle
+        />
+        <config-form
+          :title="configKeysEnum.MICROSOFT_OAUTH"
+          v-if="tabs[value].key === configKeysEnum.AUTH"
+          v-model="config[configKeysEnum.MICROSOFT_OAUTH]"
+          showTitle
+        />
         <config-form
           :title="configKeysEnum.OAUTH2"
-          v-if="tabs[value].key === configKeysEnum.OAUTH2"
+          v-if="tabs[value].key === configKeysEnum.AUTH"
           v-model="config[configKeysEnum.OAUTH2]"
           showTitle
         />
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 73a9af2..152dc30 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -38,15 +38,21 @@ import MRFUserAllowlist, {normalizeMRFUserAllowlistConfig} from '../entities/set
 import MRFNormalizeMarkupConfig from '../entities/settings/MRFNormalizeMarkupConfig'
 import WebEndpointConfig, {normalizeWebEndpointConfig} from '../entities/settings/WebEndpointConfig'
 import WebMetadataConfig from '../entities/settings/WebMetadataConfig'
-import UploadersMDIIConfig from "../entities/settings/UploadersMDIIConfig";
-import JobQueueConfig from "../entities/settings/JobQueueConfig";
-import WebFederationQueueConfig from "../entities/settings/WebFederationQueueConfig";
-import AuthConfig from "../entities/settings/AuthConfig";
-import AdminTokenConfig, {normalizeAdminTokenConfig} from "../entities/settings/AdminTokenConfig";
+import UploadersMDIIConfig from '../entities/settings/UploadersMDIIConfig'
+import JobQueueConfig from '../entities/settings/JobQueueConfig'
+import WebFederationQueueConfig from '../entities/settings/WebFederationQueueConfig'
+import AuthConfig from '../entities/settings/AuthConfig'
+import AdminTokenConfig, {normalizeAdminTokenConfig} from '../entities/settings/AdminTokenConfig'
+import {
+  UeberauthFacebookOAuth,
+  UeberauthGoogleOAuth, UeberauthMicrosoftOAuth,
+  UeberauthTwitterOAuth
+} from "../entities/settings/UeberauthConfigs";
 
 export enum configGroups {
   PLEROMA = 'pleroma',
-  PLEROMA_JOB_QUEUE = 'pleroma_job_queue'
+  PLEROMA_JOB_QUEUE = 'pleroma_job_queue',
+  UEBERAUTH = 'ueberauth'
 }
 
 export enum configKeys {
@@ -94,7 +100,11 @@ export enum configKeys {
   PUSH_ENCRYPTION = ':web_push_encryption',
   JOB_QUEUE = ':queues',
   WEB_FEDERATOR_RETRY_QUEUE = 'Pleroma.Web.Federator.RetryQueue',
-  ADMIN_TOKEN = ':admin_token'
+  ADMIN_TOKEN = ':admin_token',
+  TWITTER_OAUTH = 'Ueberauth.Strategy.Twitter.OAuth',
+  FACEBOOK_OAUTH = 'Ueberauth.Strategy.Facebook.OAuth',
+  GOOGLE_OAUTH = 'Ueberauth.Strategy.Google.OAuth',
+  MICROSOFT_OAUTH = 'Ueberauth.Strategy.Microsoft.OAuth'
 }
 
 export const arrayParams = {
@@ -313,14 +323,38 @@ export const configKeysTabs = [
     tab: true,
     constructor: ScheduledActivityConfig,
   },
-  // {
-  //   key: configKeys.AUTH,
-  //   tab: true,
-  //   constructor: AuthConfig
-  // },
   {
-    key: configKeys.ADMIN_TOKEN,
+    key: configKeys.AUTH,
     tab: true,
+    constructor: AuthConfig
+  },
+  {
+    key: configKeys.TWITTER_OAUTH,
+    group: configGroups.UEBERAUTH,
+    tab: false,
+    constructor: UeberauthTwitterOAuth
+  },
+  {
+    key: configKeys.FACEBOOK_OAUTH,
+    group: configGroups.UEBERAUTH,
+    tab: false,
+    constructor: UeberauthFacebookOAuth
+  },
+  {
+    key: configKeys.GOOGLE_OAUTH,
+    group: configGroups.UEBERAUTH,
+    tab: false,
+    constructor: UeberauthGoogleOAuth
+  },
+  {
+    key: configKeys.MICROSOFT_OAUTH,
+    group: configGroups.UEBERAUTH,
+    tab: false,
+    constructor: UeberauthMicrosoftOAuth
+  },
+  {
+    key: configKeys.ADMIN_TOKEN,
+    tab: false,
     constructor: AdminTokenConfig
   },
   {
diff --git a/src/entities/settings/UeberauthConfigs.ts b/src/entities/settings/UeberauthConfigs.ts
new file mode 100644
index 0000000..90f95d5
--- /dev/null
+++ b/src/entities/settings/UeberauthConfigs.ts
@@ -0,0 +1,27 @@
+import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
+
+class UeberauthBaseOAuth {
+  constructor( existConfig? ) {
+    normalizeApiConfig(existConfig, this)
+  }
+  client_id:string = ''
+  client_secret: string = ''
+}
+
+export class UeberauthTwitterOAuth {
+  constructor( existConfig? ) {
+    normalizeApiConfig(existConfig, this)
+  }
+  consumer_key: string = ''
+  consumer_secret: string = ''
+}
+
+export class UeberauthFacebookOAuth extends UeberauthBaseOAuth{
+  redirect_uri: string = ''
+}
+
+export class UeberauthGoogleOAuth extends UeberauthBaseOAuth{
+  redirect_uri: string = ''
+}
+
+export class UeberauthMicrosoftOAuth extends UeberauthBaseOAuth{}
diff --git a/src/entities/settings/WebEndpointConfig.ts b/src/entities/settings/WebEndpointConfig.ts
index 99b65be..bdc40a7 100644
--- a/src/entities/settings/WebEndpointConfig.ts
+++ b/src/entities/settings/WebEndpointConfig.ts
@@ -1,5 +1,6 @@
 import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
 import {arrayParams, configKeys} from '../../data/Config'
+import data from '../../services/configs'
 
 class Pubsub {
   name: string = ''
@@ -73,18 +74,20 @@ export default class WebEndpointConfig {
 }
 
 export const normalizeWebEndpointConfig = (config) => {
-  const apiConf = [...config]
-  const render_errors = apiConf.find(({ tuple }) => tuple[0] === ':render_errors')
-  if (render_errors.tuple && render_errors && render_errors.tuple[1]) {
-    const accepts = render_errors.tuple[1].find(({ tuple }) => tuple[0] === ':accepts')
-    accepts.tuple[1] = accepts.tuple[1].split(';')
-  }
-  const http = apiConf.find(({ tuple }) => tuple[0] === ':http')
-  if (http.tuple && http && http.tuple[1]) {
-    const dispatch = http.tuple[1].find(({ tuple }) => tuple[0] === ':dispatch')
-    dispatch.tuple[1] = [dispatch.tuple[1]]
-    const ip = http.tuple[1].find(({ tuple }) => tuple[0] === ':ip')
-    ip.tuple[1] = { tuple: ip.tuple[1].split(';')}
-  }
-  return apiConf
+  // const apiConf = [...config]
+  // const render_errors = apiConf.find(({ tuple }) => tuple[0] === ':render_errors')
+  // if (render_errors.tuple && render_errors && render_errors.tuple[1]) {
+  //   const accepts = render_errors.tuple[1].find(({ tuple }) => tuple[0] === ':accepts')
+  //   accepts.tuple[1] = accepts.tuple[1].split(';')
+  // }
+  // const http = apiConf.find(({ tuple }) => tuple[0] === ':http')
+  // if (http.tuple && http && http.tuple[1]) {
+  //   const dispatch = http.tuple[1].find(({ tuple }) => tuple[0] === ':dispatch')
+  //   dispatch.tuple[1] = [dispatch.tuple[1]]
+  //   const ip = http.tuple[1].find(({ tuple }) => tuple[0] === ':ip')
+  //   ip.tuple[1] = { tuple: ip.tuple[1].split(';')}
+  // }
+  // return apiConf
+  console.log(data)
+  return data.configs.find(({ key }) => key === configKeys.WEB_ENDPOINT).value
 }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 13edc25..fabb1ea 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -825,6 +825,7 @@
       "enabled_note": "Whether scheduled activities are sent to the job queue to be executed"
     },
     ":oauth2_form": {
+      "title": "OAuth2",
       "token_expires_in": "Token expires in",
       "token_expires_in_note": "The lifetime in seconds of the access token",
       "issue_new_refresh_token": "Issue new refresh token",
@@ -1112,6 +1113,40 @@
     ":admin_token_form": {
       "token": "Admin token",
       "token_note": "Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter"
+    },
+    "Ueberauth.Strategy": {
+      "Twitter.OAuth_form": {
+        "title": "Ueberauth.Strategy.Twitter",
+        "consumer_key": "Consumer key",
+        "consumer_key_note": "",
+        "consumer_secret": "Consumer secret",
+        "consumer_secret_note": ""
+      },
+      "Facebook.OAuth_form": {
+        "title": "Ueberauth.Strategy.Facebook",
+        "client_id": "Client id",
+        "client_id_note": "",
+        "client_secret": "Client secret",
+        "client_secret_note": "",
+        "redirect_uri": "Redirect URI",
+        "redirect_uri_note": ""
+      },
+      "Google.OAuth_form": {
+        "title": "Ueberauth.Strategy.Google",
+        "client_id": "Client id",
+        "client_id_note": "",
+        "client_secret": "Client secret",
+        "client_secret_note": "",
+        "redirect_uri": "Redirect URI",
+        "redirect_uri_note": ""
+      },
+      "Microsoft.OAuth_form": {
+        "title": "Ueberauth.Strategy.Microsoft",
+        "client_id": "Client id",
+        "client_id_note": "",
+        "client_secret": "Client secret",
+        "client_secret_note": ""
+      }
     }
   }
 }
-- 
GitLab


From 45712853729d3a7a40427c6936fe1b6357bc3fcb Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Tue, 13 Aug 2019 23:11:43 +0300
Subject: [PATCH 57/61] feat: Pleroma.Web.Auth.Authenticator, :markup

---
 .../configSettings/ConfigSettingsPage.vue     | 11 +++
 src/data/Config.ts                            | 23 ++++-
 src/entities/settings/MarkupConfig.ts         | 13 +++
 .../settings/WebAuthenticatorConfig.ts        | 10 ++
 src/i18n/en.json                              | 98 +++++++++++--------
 src/utils/GetFieldList.ts                     |  3 +-
 6 files changed, 115 insertions(+), 43 deletions(-)
 create mode 100644 src/entities/settings/MarkupConfig.ts
 create mode 100644 src/entities/settings/WebAuthenticatorConfig.ts

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index 419eb4d..dd47ed5 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -38,6 +38,12 @@
           v-model="config[configKeysEnum.UPLOADERSMDII]"
           show-title
         />
+        <config-form
+          :title="configKeysEnum.MARKUP"
+          v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
+          v-model="config[configKeysEnum.MARKUP]"
+          showTitle
+        />
         <config-form
           :title="configKeysEnum.CHAT"
           v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
@@ -68,6 +74,11 @@
           v-model="config[configKeysEnum.KOCAPTCHA]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.WEB_AUTHENTICATOR"
+          v-if="tabs[value].key === configKeysEnum.AUTH"
+          v-model="config[configKeysEnum.WEB_AUTHENTICATOR]"
+        />
         <config-form
           :title="configKeysEnum.TWITTER_OAUTH"
           v-if="tabs[value].key === configKeysEnum.AUTH"
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 152dc30..32e29a1 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -48,6 +48,8 @@ import {
   UeberauthGoogleOAuth, UeberauthMicrosoftOAuth,
   UeberauthTwitterOAuth
 } from "../entities/settings/UeberauthConfigs";
+import WebAuthenticatorConfig, {normalizeWebAuthenticatorConfig} from "../entities/settings/WebAuthenticatorConfig";
+import MarkupConfig from "../entities/settings/MarkupConfig";
 
 export enum configGroups {
   PLEROMA = 'pleroma',
@@ -85,6 +87,7 @@ export enum configKeys {
   HTTP_SECURITY = ':http_security',
   AUTO_LINKER = ':auto_linker',
   SCHEDULED_ACTIVITY = 'Pleroma.ScheduledActivity',
+  WEB_AUTHENTICATOR = 'Pleroma.Web.Auth.Authenticator',
   AUTH = ':auth',
   OAUTH2 = ':oauth2',
   RATE_LIMIT = ':rate_limit',
@@ -104,7 +107,8 @@ export enum configKeys {
   TWITTER_OAUTH = 'Ueberauth.Strategy.Twitter.OAuth',
   FACEBOOK_OAUTH = 'Ueberauth.Strategy.Facebook.OAuth',
   GOOGLE_OAUTH = 'Ueberauth.Strategy.Google.OAuth',
-  MICROSOFT_OAUTH = 'Ueberauth.Strategy.Microsoft.OAuth'
+  MICROSOFT_OAUTH = 'Ueberauth.Strategy.Microsoft.OAuth',
+  MARKUP = ':markup'
 }
 
 export const arrayParams = {
@@ -127,7 +131,8 @@ export const arrayParams = {
   ],
   [configKeys.MRF_KEYWORD]: ['reject', 'federated_timeline_removal'],
   [configKeys.WEB_ENDPOINT]: ['instrumenters', 'extra_cookie_attrs', 'watchers'],
-  [configKeys.AUTH]: ['oauth_consumer_strategies']
+  [configKeys.AUTH]: ['oauth_consumer_strategies'],
+  [configKeys.MARKUP]: ['scrub_policy']
 }
 
 export const normalizerMapper = {
@@ -142,7 +147,8 @@ export const normalizerMapper = {
   [configKeys.MRF_KEYWORD]: { normalizer: normalizeMrfKeywordConfig, normalizeBeforeGeneralFunc: false },
   [configKeys.MRF_USER_ALLOWLIST]: { normalizer: normalizeMRFUserAllowlistConfig, normalizeBeforeGeneralFunc: false },
   [configKeys.WEB_ENDPOINT]: { normalizer: normalizeWebEndpointConfig, normalizeBeforeGeneralFunc: false },
-  [configKeys.ADMIN_TOKEN]: { normalizer: normalizeAdminTokenConfig, normalizeBeforeGeneralFunc: false }
+  [configKeys.ADMIN_TOKEN]: { normalizer: normalizeAdminTokenConfig, normalizeBeforeGeneralFunc: false },
+  [configKeys.WEB_AUTHENTICATOR]: { normalizer: normalizeWebAuthenticatorConfig, normalizeBeforeGeneralFunc: false }
 }
 
 export const getConfigGroup = (configKey) => {
@@ -328,6 +334,11 @@ export const configKeysTabs = [
     tab: true,
     constructor: AuthConfig
   },
+  {
+    key: configKeys.WEB_AUTHENTICATOR,
+    tab: false,
+    constructor: WebAuthenticatorConfig
+  },
   {
     key: configKeys.TWITTER_OAUTH,
     group: configGroups.UEBERAUTH,
@@ -438,4 +449,10 @@ export const configKeysTabs = [
     tab: false,
     constructor: WebFederationQueueConfig,
   },
+  {
+    key: configKeys.MARKUP,
+    group: configGroups.PLEROMA,
+    tab: false,
+    constructor: MarkupConfig
+  }
 ]
diff --git a/src/entities/settings/MarkupConfig.ts b/src/entities/settings/MarkupConfig.ts
new file mode 100644
index 0000000..8db86d2
--- /dev/null
+++ b/src/entities/settings/MarkupConfig.ts
@@ -0,0 +1,13 @@
+import {normalizeApiConfig} from '../../utils/ConvertConfigToState'
+import {arrayParams, configKeys} from '../../data/Config'
+
+export default class MarkupConfig {
+  constructor(existConfig?) {
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.MARKUP])
+  }
+  allow_inline_images: boolean = true
+  allow_headings: boolean = false
+  allow_tables: boolean = false
+  allow_fonts: boolean = false
+  scrub_policy: any = 'Pleroma.HTML.Transform.MediaProxy;Pleroma.HTML.Scrubber.Default'
+}
diff --git a/src/entities/settings/WebAuthenticatorConfig.ts b/src/entities/settings/WebAuthenticatorConfig.ts
new file mode 100644
index 0000000..70aab34
--- /dev/null
+++ b/src/entities/settings/WebAuthenticatorConfig.ts
@@ -0,0 +1,10 @@
+export default class WebAuthenticatorConfig {
+  constructor(existConfig?) {
+    if (existConfig) {
+      this.authenticator = existConfig
+    }
+  }
+  authenticator:string = 'none'
+}
+
+export const normalizeWebAuthenticatorConfig = (config) => config[0].tuple[1] === 'none' ? '' : config[0].tuple[1]
diff --git a/src/i18n/en.json b/src/i18n/en.json
index fabb1ea..6fd2e39 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1019,28 +1019,6 @@
       "private_key": "VAPID private key",
       "private_key_note": ""
     },
-    "Pleroma.Web.Endpoint_form": {
-      "instrumenters": "Instrumenters",
-      "instrumenters_note": "",
-      "secure_cookie_flag": "Secure cookie flag",
-      "secure_cookie_flag_note": "",
-      "check_origin": "Check origin",
-      "check_origin_note": "",
-      "protocol": "Protocol",
-      "protocol_note": "",
-      "signing_salt": "Signing salt",
-      "signing_salt_note": "",
-      "secret_key_base": "Secret key base",
-      "secret_key_base_note": "",
-      "debug_errors": "Debug errors",
-      "debug_errors_note": "",
-      "code_reloader": "Code reloader",
-      "code_reloader_note": "",
-      "extra_cookie_attrs": "Extra cookie attributes",
-      "extra_cookie_attrs_note": "",
-      "watchers": "Watchers",
-      "watchers_note": ""
-    },
     "protocol_options_form": {
       "title": "Protocol options",
       "max_request_line_length": "Max request line length",
@@ -1071,23 +1049,52 @@
       "port": "Port",
       "port_note": ""
     },
-    "Pleroma.Web.Metadata_form": {
-      "unfurl_nsfw": "Show nsfw attachments in previews",
-      "unfurl_nsfw_note": "",
-      "providers": "Providers",
-      "providers_note": "Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the <header> as <link rel=me>"
-    },
-    "Pleroma.Web.Federator.RetryQueue_form": {
-      "title": "Web.Federator.RetryQueue",
-      "enabled": "Enabled",
-      "enabled_note": "",
-      "max_jobs": "Max jobs",
-      "max_jobs_note": "The maximum amount of parallel federation jobs running at the same time",
-      "initial_timeout": "Initial timeout (seconds)",
-      "initial_timeout_note": "",
-      "max_retries": "The maximum number of times a federation job is retried",
-      "max_retries_note": ""
-    },
+
+    "Pleroma.Web": {
+      "Metadata_form": {
+        "unfurl_nsfw": "Show nsfw attachments in previews",
+        "unfurl_nsfw_note": "",
+        "providers": "Providers",
+        "providers_note": "Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the <header> as <link rel=me>"
+      },
+      "Federator.RetryQueue_form": {
+        "title": "Web.Federator.RetryQueue",
+        "enabled": "Enabled",
+        "enabled_note": "",
+        "max_jobs": "Max jobs",
+        "max_jobs_note": "The maximum amount of parallel federation jobs running at the same time",
+        "initial_timeout": "Initial timeout (seconds)",
+        "initial_timeout_note": "",
+        "max_retries": "The maximum number of times a federation job is retried",
+        "max_retries_note": ""
+      },
+      "Auth.Authenticator_form": {
+        "authenticator": "Authenticator",
+        "authenticator_note": ""
+      },
+      "Endpoint_form": {
+        "instrumenters": "Instrumenters",
+        "instrumenters_note": "",
+        "secure_cookie_flag": "Secure cookie flag",
+        "secure_cookie_flag_note": "",
+        "check_origin": "Check origin",
+        "check_origin_note": "",
+        "protocol": "Protocol",
+        "protocol_note": "",
+        "signing_salt": "Signing salt",
+        "signing_salt_note": "",
+        "secret_key_base": "Secret key base",
+        "secret_key_base_note": "",
+        "debug_errors": "Debug errors",
+        "debug_errors_note": "",
+        "code_reloader": "Code reloader",
+        "code_reloader_note": "",
+        "extra_cookie_attrs": "Extra cookie attributes",
+        "extra_cookie_attrs_note": "",
+        "watchers": "Watchers",
+        "watchers_note": ""
+      }
+  },
     ":queues_form": {
       "federator_outgoing": "Outgoing federation",
       "federator_outgoing_note": "",
@@ -1147,6 +1154,19 @@
         "client_secret": "Client secret",
         "client_secret_note": ""
       }
+    },
+    ":markup_form": {
+      "title": "Markup",
+      "allow_inline_images": "Allow inline images",
+      "allow_inline_images_note": "",
+      "allow_headings": "Allow headings",
+      "allow_headings_note": "",
+      "allow_tables": "Allow tables",
+      "allow_tables_note": "",
+      "allow_fonts": "Allow fonts",
+      "allow_fonts_note": "",
+      "scrub_policy":"Scrub policy",
+      "scrub_policy_note": "Separate items with ;"
     }
   }
 }
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 38a9b66..951294e 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -40,7 +40,8 @@ export const selectOptions = {
   valid_schemes: ['https', 'http', 'dat', 'dweb', 'gopher', 'ipfs', 'ipns', 'irc', 'ircs', 'magnet', 'mailto', 'mumble', 'ssb', 'xmpp'],
   referrer_policy: ['same-origin', 'no-referrer'],
   parsers: ['Pleroma.Web.RichMedia.Parsers.TwitterCard', 'Pleroma.Web.RichMedia.Parsers.OGP', 'Pleroma.Web.RichMedia.Parsers.OEmbed'],
-  providers: ['Pleroma.Web.Metadata.Providers.OpenGraph', 'Pleroma.Web.Metadata.Providers.TwitterCard', 'Pleroma.Web.Metadata.Providers.RelMe']
+  providers: ['Pleroma.Web.Metadata.Providers.OpenGraph', 'Pleroma.Web.Metadata.Providers.TwitterCard', 'Pleroma.Web.Metadata.Providers.RelMe'],
+  authenticator: [ 'none', 'Pleroma.Web.Auth.PleromaAuthenticator', 'Pleroma.Web.Auth.LDAPAuthenticator']
 }
 
 export default (formData) => {
-- 
GitLab


From c441dfc0e68b916d7b11e29c37e9b9eef1b5e42b Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 14 Aug 2019 17:38:49 +0300
Subject: [PATCH 58/61] feat: rename web_pish_encryption to :vapid_details,
 :mrf_vocabulary, :markup, :ssshd

---
 .../configSettings/ConfigSettingsPage.vue     |  12 +
 src/data/Config.ts                            |  41 ++-
 src/entities/settings/EsshdConfig.ts          |  12 +
 src/entities/settings/MrfVocabularyConfig.ts  |  10 +
 ...ryptionConfig.ts => VapidDetailsConfig.ts} |   2 +-
 src/i18n/en.json                              | 261 ++++++++++--------
 src/utils/GetFieldList.ts                     |   3 +-
 7 files changed, 210 insertions(+), 131 deletions(-)
 create mode 100644 src/entities/settings/EsshdConfig.ts
 create mode 100644 src/entities/settings/MrfVocabularyConfig.ts
 rename src/entities/settings/{PushEncryptionConfig.ts => VapidDetailsConfig.ts} (83%)

diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index dd47ed5..e0e17a8 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -79,6 +79,12 @@
           v-if="tabs[value].key === configKeysEnum.AUTH"
           v-model="config[configKeysEnum.WEB_AUTHENTICATOR]"
         />
+        <config-form
+          :title="configKeysEnum.LDAP"
+          v-if="tabs[value].key === configKeysEnum.AUTH"
+          v-model="config[configKeysEnum.LDAP]"
+          showTitle
+        />
         <config-form
           :title="configKeysEnum.TWITTER_OAUTH"
           v-if="tabs[value].key === configKeysEnum.AUTH"
@@ -156,6 +162,12 @@
           v-model="config[configKeysEnum.MRF_USER_ALLOWLIST]"
           showTitle
         />
+        <config-form
+          :title="configKeysEnum.MRF_VOCABULARY"
+          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
+          v-model="config[configKeysEnum.MRF_VOCABULARY]"
+          showTitle
+        />
         <config-form
           :title="configKeysEnum.WEB_FEDERATOR_RETRY_QUEUE"
           v-if="tabs[value].key === configKeysEnum.JOB_QUEUE"
diff --git a/src/data/Config.ts b/src/data/Config.ts
index 32e29a1..b398c35 100644
--- a/src/data/Config.ts
+++ b/src/data/Config.ts
@@ -33,7 +33,7 @@ import MRFKeyword, {normalizeMrfKeywordConfig} from '../entities/settings/MRFKey
 import MRFMention from '../entities/settings/MRFMention'
 import SuggestionsConfig from '../entities/settings/SuggestionsConfig'
 import EctoReposConfig, {normalizeEctoReposConfig} from '../entities/settings/EctoReposConfig'
-import PushEncryptionConfig from '../entities/settings/PushEncryptionConfig'
+import VapidDetailsConfig from '../entities/settings/VapidDetailsConfig'
 import MRFUserAllowlist, {normalizeMRFUserAllowlistConfig} from '../entities/settings/MRFUserAllowlistConfig'
 import MRFNormalizeMarkupConfig from '../entities/settings/MRFNormalizeMarkupConfig'
 import WebEndpointConfig, {normalizeWebEndpointConfig} from '../entities/settings/WebEndpointConfig'
@@ -47,14 +47,19 @@ import {
   UeberauthFacebookOAuth,
   UeberauthGoogleOAuth, UeberauthMicrosoftOAuth,
   UeberauthTwitterOAuth
-} from "../entities/settings/UeberauthConfigs";
-import WebAuthenticatorConfig, {normalizeWebAuthenticatorConfig} from "../entities/settings/WebAuthenticatorConfig";
-import MarkupConfig from "../entities/settings/MarkupConfig";
+} from '../entities/settings/UeberauthConfigs'
+import WebAuthenticatorConfig, {normalizeWebAuthenticatorConfig} from '../entities/settings/WebAuthenticatorConfig'
+import MarkupConfig from '../entities/settings/MarkupConfig'
+import EsshdConfig from "../entities/settings/EsshdConfig";
+import MrfVocabularyConfig from "../entities/settings/MrfVocabularyConfig";
 
 export enum configGroups {
   PLEROMA = 'pleroma',
   PLEROMA_JOB_QUEUE = 'pleroma_job_queue',
-  UEBERAUTH = 'ueberauth'
+  LOGGER = ':logger',
+  UEBERAUTH = 'ueberauth',
+  ESSHD = ':esshd',
+  PUSH_ENCRYPTION = ':web_push_encryption'
 }
 
 export enum configKeys {
@@ -99,8 +104,9 @@ export enum configKeys {
   MRF_MENTION = ':mrf_mention',
   MRF_USER_ALLOWLIST = ':mrf_user_allowlist',
   MRF_NORMALIZE_MARKUP = ':mrf_normalize_markup',
+  MRF_VOCABULARY = ':mrf_vocabulary',
   SUGGESTIONS = ':suggestions',
-  PUSH_ENCRYPTION = ':web_push_encryption',
+  VAPID_DETAILS = ':vapid_details',
   JOB_QUEUE = ':queues',
   WEB_FEDERATOR_RETRY_QUEUE = 'Pleroma.Web.Federator.RetryQueue',
   ADMIN_TOKEN = ':admin_token',
@@ -108,7 +114,8 @@ export enum configKeys {
   FACEBOOK_OAUTH = 'Ueberauth.Strategy.Facebook.OAuth',
   GOOGLE_OAUTH = 'Ueberauth.Strategy.Google.OAuth',
   MICROSOFT_OAUTH = 'Ueberauth.Strategy.Microsoft.OAuth',
-  MARKUP = ':markup'
+  MARKUP = ':markup',
+  ESSHD = ':esshd'
 }
 
 export const arrayParams = {
@@ -130,6 +137,7 @@ export const arrayParams = {
     'banner_removal'
   ],
   [configKeys.MRF_KEYWORD]: ['reject', 'federated_timeline_removal'],
+  [configKeys.MRF_VOCABULARY]: ['accept', 'reject'],
   [configKeys.WEB_ENDPOINT]: ['instrumenters', 'extra_cookie_attrs', 'watchers'],
   [configKeys.AUTH]: ['oauth_consumer_strategies'],
   [configKeys.MARKUP]: ['scrub_policy']
@@ -280,7 +288,7 @@ export const configKeysTabs = [
     key: configKeys.LDAP,
     group: configGroups.PLEROMA,
     constructor: LdapConfig,
-    tab: true,
+    tab: false,
   },
   {
     key: configKeys.ACTIVITY_PUB,
@@ -426,6 +434,12 @@ export const configKeysTabs = [
     tab: false,
     constructor: MRFNormalizeMarkupConfig,
   },
+  {
+    key: configKeys.MRF_VOCABULARY,
+    group: configGroups.PLEROMA,
+    tab: false,
+    constructor: MrfVocabularyConfig,
+  },
   {
     key: configKeys.SUGGESTIONS,
     group: configGroups.PLEROMA,
@@ -433,9 +447,10 @@ export const configKeysTabs = [
     constructor: SuggestionsConfig,
   },
   {
-    key: configKeys.PUSH_ENCRYPTION,
+    key: configKeys.VAPID_DETAILS,
+    group: configGroups.PUSH_ENCRYPTION,
     tab: true,
-    constructor: PushEncryptionConfig,
+    constructor: VapidDetailsConfig,
   },
   {
     key: configKeys.JOB_QUEUE,
@@ -454,5 +469,11 @@ export const configKeysTabs = [
     group: configGroups.PLEROMA,
     tab: false,
     constructor: MarkupConfig
+  },
+  {
+    key: configKeys.ESSHD,
+    group: configGroups.PLEROMA,
+    tab: true,
+    constructor: EsshdConfig
   }
 ]
diff --git a/src/entities/settings/EsshdConfig.ts b/src/entities/settings/EsshdConfig.ts
new file mode 100644
index 0000000..f3efc77
--- /dev/null
+++ b/src/entities/settings/EsshdConfig.ts
@@ -0,0 +1,12 @@
+import {normalizeApiConfig} from '../../utils/ConvertConfigToState'
+
+export default class EsshdConfig {
+  constructor(existConfig?){
+    normalizeApiConfig(existConfig, this)
+  }
+  enabled: boolean = false
+  priv_dir: string = ''
+  handler: string = ''
+  port: number = 10022
+  password_authenticator: string = 'Pleroma.BBS.Authenticator'
+}
diff --git a/src/entities/settings/MrfVocabularyConfig.ts b/src/entities/settings/MrfVocabularyConfig.ts
new file mode 100644
index 0000000..42ab2c3
--- /dev/null
+++ b/src/entities/settings/MrfVocabularyConfig.ts
@@ -0,0 +1,10 @@
+import {normalizeApiConfig} from '../../utils/ConvertConfigToState'
+import {arrayParams, configKeys} from '../../data/Config'
+
+export default class MrfVocabularyConfig {
+  constructor(existConfig?){
+    normalizeApiConfig(existConfig, this, arrayParams[configKeys.MRF_VOCABULARY])
+  }
+  accept: any = ''
+  reject: any = ''
+}
diff --git a/src/entities/settings/PushEncryptionConfig.ts b/src/entities/settings/VapidDetailsConfig.ts
similarity index 83%
rename from src/entities/settings/PushEncryptionConfig.ts
rename to src/entities/settings/VapidDetailsConfig.ts
index e170e09..1f2f12c 100644
--- a/src/entities/settings/PushEncryptionConfig.ts
+++ b/src/entities/settings/VapidDetailsConfig.ts
@@ -1,6 +1,6 @@
 import { normalizeApiConfig } from '../../utils/ConvertConfigToState'
 
-export default class PushEncryptionConfig {
+export default class VapidDetailsConfig {
   constructor(existConfig?) {
     normalizeApiConfig(existConfig, this)
   }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6fd2e39..d74238c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -448,7 +448,7 @@
       ":gopher": "Gopher",
       ":auth": "Authentication",
       ":mrf_simple": "MRF",
-      ":web_push_encryption": "Web push encryption",
+      ":valid_details": "Web push encryption",
       ":activitypub": "Activity pub",
       ":suggestions": "Suggestions",
       ":uri_schemes": "URI schemes",
@@ -463,56 +463,127 @@
       ":auto_linker": "Auto linker",
       "Pleroma.ScheduledActivity": "Scheduled activity",
       ":queues": "Job queue",
-      ":admin_token":  "Admin token"
-    },
-    "Pleroma.Upload_form": {
-      "uploader": "Uploader",
-      "uploader_note": "",
-      "filters": "Filters",
-      "filters_note": "",
-      "args": "List of actions for the mogrify command",
-      "args_note": "",
-      "link_name": "Link name",
-      "link_name_note": "When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe",
-      "base_url": "Base URL",
-      "base_url_note": "",
-      "proxy_remote": "Proxy remote",
-      "proxy_remote_note": "If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.",
-      "redirect_on_failure": "Redirect on failure",
-      "redirect_on_failure_note": "Redirects the client to the real remote URL if there's any HTTP errors. Any error during body processing will not be redirected as the response is chunked.",
-      "text": "Anonymize filename",
-      "text_note": "Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.\n"
-    },
-    "Pleroma.Uploaders.Local_form": {
-      "title": "Pleroma.Uploaders.Local",
-      "uploads": "Uploads",
-      "uploads_note": "Which directory to store the user-uploads in, relative to pleroma’s working directory"
-    },
-    "Pleroma.Uploaders.S3_form": {
-      "title": "Pleroma.Uploaders.S3",
-      "bucket": "S3 bucket name",
-      "bucket_note": "",
-      "public_endpoint": "S3 endpoint that the user finally accesses",
-      "public_endpoint_note": "",
-      "truncated_namespace": "Truncated_namespace",
-      "truncated_namespace_note": "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc.\n        For example, when using CDN to S3 virtual host format, set \"\".\n        At this time, write CNAME to CDN in public_endpoint."
-    },
-    "Pleroma.Uploaders.MDII_form": {
-      "title": "Pleroma.Uploaders.MDII",
-      "cgi": "CGI",
-      "cgi_note": "",
-      "files": "Files",
-      "files_note": ""
-    },
-    "Pleroma.Emails.Mailer_form": {
-      "adapters_options": "Adapter's option",
-      "add_adapted_option": "Add adapter option",
-      "option_name": "Name",
-      "option_value": "Value",
-      "enabled": "Enabled",
-      "adapter": "Adapter",
-      "choose_type_of_field": "Choose type of field"
-    },
+      ":admin_token":  "Admin token",
+      ":esshd": "BBS / SSH access"
+    },
+    "Pleroma": {
+      "Upload_form": {
+        "uploader": "Uploader",
+        "uploader_note": "",
+        "filters": "Filters",
+        "filters_note": "",
+        "args": "List of actions for the mogrify command",
+        "args_note": "",
+        "link_name": "Link name",
+        "link_name_note": "When enabled Pleroma will add a name parameter to the url of the upload, for example https://instance.tld/media/corndog.png?name=corndog.png. This is needed to provide the correct filename in Content-Disposition headers when using filters like Pleroma.Upload.Filter.Dedupe",
+        "base_url": "Base URL",
+        "base_url_note": "",
+        "proxy_remote": "Proxy remote",
+        "proxy_remote_note": "If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.",
+        "redirect_on_failure": "Redirect on failure",
+        "redirect_on_failure_note": "Redirects the client to the real remote URL if there's any HTTP errors. Any error during body processing will not be redirected as the response is chunked.",
+        "text": "Anonymize filename",
+        "text_note": "Text to replace filenames in links. If empty, {random}.extension will be used. You can get the original filename extension by using {extension}, for example custom-file-name.{extension}.\n"
+      },
+      "Uploaders": {
+        "Local_form": {
+          "title": "Pleroma.Uploaders.Local",
+          "uploads": "Uploads",
+          "uploads_note": "Which directory to store the user-uploads in, relative to pleroma’s working directory"
+        },
+        "S3_form": {
+          "title": "Pleroma.Uploaders.S3",
+          "bucket": "S3 bucket name",
+          "bucket_note": "",
+          "public_endpoint": "S3 endpoint that the user finally accesses",
+          "public_endpoint_note": "",
+          "truncated_namespace": "Truncated_namespace",
+          "truncated_namespace_note": "If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc.\n        For example, when using CDN to S3 virtual host format, set \"\".\n        At this time, write CNAME to CDN in public_endpoint."
+        },
+        "MDII_form": {
+          "title": "Pleroma.Uploaders.MDII",
+          "cgi": "CGI",
+          "cgi_note": "",
+          "files": "Files",
+          "files_note": ""
+        }
+      },
+      "Emails.Mailer_form": {
+        "adapters_options": "Adapter's option",
+        "add_adapted_option": "Add adapter option",
+        "option_name": "Name",
+        "option_value": "Value",
+        "enabled": "Enabled",
+        "adapter": "Adapter",
+        "choose_type_of_field": "Choose type of field"
+      },
+      "Captcha_form": {
+        "enabled": "Enabled",
+        "enabled_note": "Whether the captcha should be shown on registration",
+        "method": "Method",
+        "method_note": "The method/service to use for captcha",
+        "seconds_valid": "Seconds valid",
+        "seconds_valid_note": ""
+      },
+      "Captcha.Kocaptcha_form": {
+        "title": "Kocaptcha service",
+        "endpoint": "The kocaptcha endpoint to use",
+        "endpoint_note": ""
+      },
+      "ScheduledActivity_form": {
+        "daily_user_limit": "Daily user limit",
+        "daily_user_limit_note": "The number of scheduled activities a user is allowed to create in a single day (Default: 25)",
+        "total_user_limit": "Total user limit",
+        "total_user_limit_note": "The number of scheduled activities a user is allowed to create in total (Default: 300)",
+        "enabled": "Enabled",
+        "enabled_note": "Whether scheduled activities are sent to the job queue to be executed"
+      },
+      "Web": {
+      "Metadata_form": {
+        "unfurl_nsfw": "Show nsfw attachments in previews",
+        "unfurl_nsfw_note": "",
+        "providers": "Providers",
+        "providers_note": "Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the <header> as <link rel=me>"
+      },
+      "Federator.RetryQueue_form": {
+        "title": "Web.Federator.RetryQueue",
+        "enabled": "Enabled",
+        "enabled_note": "",
+        "max_jobs": "Max jobs",
+        "max_jobs_note": "The maximum amount of parallel federation jobs running at the same time",
+        "initial_timeout": "Initial timeout (seconds)",
+        "initial_timeout_note": "",
+        "max_retries": "The maximum number of times a federation job is retried",
+        "max_retries_note": ""
+      },
+      "Auth.Authenticator_form": {
+        "authenticator": "Authenticator",
+        "authenticator_note": ""
+      },
+      "Endpoint_form": {
+        "instrumenters": "Instrumenters",
+        "instrumenters_note": "",
+        "secure_cookie_flag": "Secure cookie flag",
+        "secure_cookie_flag_note": "",
+        "check_origin": "Check origin",
+        "check_origin_note": "",
+        "protocol": "Protocol",
+        "protocol_note": "",
+        "signing_salt": "Signing salt",
+        "signing_salt_note": "",
+        "secret_key_base": "Secret key base",
+        "secret_key_base_note": "",
+        "debug_errors": "Debug errors",
+        "debug_errors_note": "",
+        "code_reloader": "Code reloader",
+        "code_reloader_note": "",
+        "extra_cookie_attrs": "Extra cookie attributes",
+        "extra_cookie_attrs_note": "",
+        "watchers": "Watchers",
+        "watchers_note": ""
+      }
+    }
+  },
     ":uri_schemes_form": {
       "valid_schemes": "Valid schemes",
       "valid_schemes_note": "List of the scheme part that is considered valid to be an URL"
@@ -653,19 +724,6 @@
       "showInstanceSpecificPanel": "Show instance specific panel",
       "showInstanceSpecificPanel_note": "Whenether to show the instance’s specific panel."
     },
-    "Pleroma.Captcha_form": {
-      "enabled": "Enabled",
-      "enabled_note": "Whether the captcha should be shown on registration",
-      "method": "Method",
-      "method_note": "The method/service to use for captcha",
-      "seconds_valid": "Seconds valid",
-      "seconds_valid_note": ""
-    },
-    "Pleroma.Captcha.Kocaptcha_form": {
-      "title": "Kocaptcha service",
-      "endpoint": "The kocaptcha endpoint to use",
-      "endpoint_note": ""
-    },
     ":database_form" : {
       "rum_enabled": "RUM indexing for full text search",
       "rum_enabled_note": "If RUM indexes should be used"
@@ -816,14 +874,6 @@
       "validate_tld": "Validate TLD",
       "validate_tld_note": ""
     },
-    "Pleroma.ScheduledActivity_form": {
-      "daily_user_limit": "Daily user limit",
-      "daily_user_limit_note": "The number of scheduled activities a user is allowed to create in a single day (Default: 25)",
-      "total_user_limit": "Total user limit",
-      "total_user_limit_note": "The number of scheduled activities a user is allowed to create in total (Default: 300)",
-      "enabled": "Enabled",
-      "enabled_note": "Whether scheduled activities are sent to the job queue to be executed"
-    },
     ":oauth2_form": {
       "title": "OAuth2",
       "token_expires_in": "Token expires in",
@@ -999,6 +1049,13 @@
       "list_of_users": "List of users",
       "add_domain": "Add domain"
     },
+    ":mrf_vocabulary_form": {
+      "title": "MRF Vocabulary",
+      "accept": "Accept",
+      "accept_note": "A list of ActivityStreams terms to accept.  If empty, all supported messages are accepted. Separate items with ;",
+      "reject": "Reject",
+      "reject_note": "A list of ActivityStreams terms to reject.  If empty, no messages are rejected. Separate items with ;"
+    },
     ":suggestions_form": {
       "enabled": "Enabled",
       "enabled_note": "",
@@ -1011,7 +1068,7 @@
       "web": "Web",
       "web_note": ""
     },
-    ":web_push_encryption_form": {
+    ":vapid_details_form": {
       "subject": "Subject",
       "subject_note": "A mailto link for the administrative contact. It’s best if this email is not a personal email address, but rather a group email so that if a person leaves an organization, is unavailable for an extended period, or otherwise can’t respond, someone else on the list can",
       "public_key": "VAPID public key",
@@ -1049,52 +1106,6 @@
       "port": "Port",
       "port_note": ""
     },
-
-    "Pleroma.Web": {
-      "Metadata_form": {
-        "unfurl_nsfw": "Show nsfw attachments in previews",
-        "unfurl_nsfw_note": "",
-        "providers": "Providers",
-        "providers_note": "Pleroma.Web.Metadata.Providers.RelMe - add links from user bio with rel=me into the <header> as <link rel=me>"
-      },
-      "Federator.RetryQueue_form": {
-        "title": "Web.Federator.RetryQueue",
-        "enabled": "Enabled",
-        "enabled_note": "",
-        "max_jobs": "Max jobs",
-        "max_jobs_note": "The maximum amount of parallel federation jobs running at the same time",
-        "initial_timeout": "Initial timeout (seconds)",
-        "initial_timeout_note": "",
-        "max_retries": "The maximum number of times a federation job is retried",
-        "max_retries_note": ""
-      },
-      "Auth.Authenticator_form": {
-        "authenticator": "Authenticator",
-        "authenticator_note": ""
-      },
-      "Endpoint_form": {
-        "instrumenters": "Instrumenters",
-        "instrumenters_note": "",
-        "secure_cookie_flag": "Secure cookie flag",
-        "secure_cookie_flag_note": "",
-        "check_origin": "Check origin",
-        "check_origin_note": "",
-        "protocol": "Protocol",
-        "protocol_note": "",
-        "signing_salt": "Signing salt",
-        "signing_salt_note": "",
-        "secret_key_base": "Secret key base",
-        "secret_key_base_note": "",
-        "debug_errors": "Debug errors",
-        "debug_errors_note": "",
-        "code_reloader": "Code reloader",
-        "code_reloader_note": "",
-        "extra_cookie_attrs": "Extra cookie attributes",
-        "extra_cookie_attrs_note": "",
-        "watchers": "Watchers",
-        "watchers_note": ""
-      }
-  },
     ":queues_form": {
       "federator_outgoing": "Outgoing federation",
       "federator_outgoing_note": "",
@@ -1167,6 +1178,18 @@
       "allow_fonts_note": "",
       "scrub_policy":"Scrub policy",
       "scrub_policy_note": "Separate items with ;"
+    },
+    ":esshd_form": {
+      "enabled": "Enabled",
+      "enabled_note": "",
+      "priv_dir": "Priv dir",
+      "priv_dir_note": "",
+      "handler": "Habdler",
+      "handler_note": "",
+      "port": "Port",
+      "port_note": "",
+      "password_authenticator": "Password authenticator",
+      "password_authenticator_note": ""
     }
   }
 }
diff --git a/src/utils/GetFieldList.ts b/src/utils/GetFieldList.ts
index 951294e..291c5c2 100644
--- a/src/utils/GetFieldList.ts
+++ b/src/utils/GetFieldList.ts
@@ -25,7 +25,8 @@ export const selectOptions = {
     'Pleroma.Web.ActivityPub.MRF.RejectNonPublic',
     'Pleroma.Web.ActivityPub.MRF.EnsureRePrepended',
     'Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy',
-    'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy'
+    'Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy',
+    'Pleroma.Web.ActivityPub.MRF.VocabularyPolicy'
   ],
   allowed_post_formats: ['text/plain', 'text/html', 'text/markdown', 'text/bbcode'],
   limit_to_local_content: [':unauthenticated', ':all', 'false'],
-- 
GitLab


From 367b1e0eadda739c82794bdb7ca40988d07e9836 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Wed, 14 Aug 2019 23:59:35 +0300
Subject: [PATCH 59/61] feat: add config-settings-form component. fix some
 errors

---
 .../configSettings/ConfigSettingsForms.vue    | 225 ++++++++++++++++++
 .../configSettings/forms/AssetsForm.vue       |   7 +-
 .../configSettings/ConfigSettingsPage.vue     | 180 +-------------
 src/entities/settings/WebEndpointConfig.ts    |  31 ++-
 src/i18n/en.json                              |   2 +-
 5 files changed, 254 insertions(+), 191 deletions(-)
 create mode 100644 src/components/configSettings/ConfigSettingsForms.vue

diff --git a/src/components/configSettings/ConfigSettingsForms.vue b/src/components/configSettings/ConfigSettingsForms.vue
new file mode 100644
index 0000000..0b34078
--- /dev/null
+++ b/src/components/configSettings/ConfigSettingsForms.vue
@@ -0,0 +1,225 @@
+<template>
+<div>
+  <template v-for="tab in tabs">
+    <config-form
+      :key="tab.key"
+      v-model="config[tab.key]"
+      :title="tab.key"
+      :showTitle="showTabTitle(tab)"
+      v-if="showTab(tab)"
+    />
+  </template>
+  <emails-form
+    v-if="tabKey === configKeysEnum.EMAILS"
+    v-model="config[configKeysEnum.EMAILS]"
+  />
+  <config-form
+    :title="configKeysEnum.UPLOADERSS3"
+    v-if="tabKey === configKeysEnum.UPLOAD"
+    v-model="config[configKeysEnum.UPLOADERSS3]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.UPLOADERSLOCAL"
+    v-if="tabKey === configKeysEnum.UPLOAD"
+    v-model="config[configKeysEnum.UPLOADERSLOCAL]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.UPLOADERSMDII"
+    v-if="tabKey === configKeysEnum.UPLOAD"
+    v-model="config[configKeysEnum.UPLOADERSMDII]"
+    show-title
+  />
+  <config-form
+    :title="configKeysEnum.MARKUP"
+    v-if="tabKey === configKeysEnum.FRONTEND_CONFIGURATIONS"
+    v-model="config[configKeysEnum.MARKUP]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.CHAT"
+    v-if="tabKey === configKeysEnum.FRONTEND_CONFIGURATIONS"
+    v-model="config[configKeysEnum.CHAT]"
+    showTitle
+  />
+  <emoji-form
+    :title="configKeysEnum.EMOJI"
+    v-if="tabKey === configKeysEnum.FRONTEND_CONFIGURATIONS"
+    v-model="config[configKeysEnum.EMOJI]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.ECTO_REPOS"
+    v-if="tabKey === configKeysEnum.DATABASE"
+    v-model="config[configKeysEnum.ECTO_REPOS]"
+  />
+  <assets-form
+    :title="configKeysEnum.ASSETS"
+    v-show="tabKey === configKeysEnum.FRONTEND_CONFIGURATIONS"
+    v-model="config[configKeysEnum.ASSETS]"
+    showTitle
+    :error="error.assets"
+  />
+  <config-form
+    :title="configKeysEnum.KOCAPTCHA"
+    v-if="tabKey === configKeysEnum.CAPTCHA"
+    v-model="config[configKeysEnum.KOCAPTCHA]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.WEB_AUTHENTICATOR"
+    v-if="tabKey === configKeysEnum.AUTH"
+    v-model="config[configKeysEnum.WEB_AUTHENTICATOR]"
+  />
+  <config-form
+    :title="configKeysEnum.LDAP"
+    v-if="tabKey === configKeysEnum.AUTH"
+    v-model="config[configKeysEnum.LDAP]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.TWITTER_OAUTH"
+    v-if="tabKey === configKeysEnum.AUTH"
+    v-model="config[configKeysEnum.TWITTER_OAUTH]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.FACEBOOK_OAUTH"
+    v-if="tabKey === configKeysEnum.AUTH"
+    v-model="config[configKeysEnum.FACEBOOK_OAUTH]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.GOOGLE_OAUTH"
+    v-if="tabKey === configKeysEnum.AUTH"
+    v-model="config[configKeysEnum.GOOGLE_OAUTH]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.MICROSOFT_OAUTH"
+    v-if="tabKey === configKeysEnum.AUTH"
+    v-model="config[configKeysEnum.MICROSOFT_OAUTH]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.OAUTH2"
+    v-if="tabKey === configKeysEnum.AUTH"
+    v-model="config[configKeysEnum.OAUTH2]"
+    showTitle
+  />
+  <auto-linker-form
+    :title="configKeysEnum.AUTO_LINKER"
+    v-if="tabKey === configKeysEnum.AUTO_LINKER"
+    v-model="config[configKeysEnum.AUTO_LINKER]"
+  />
+  <mrf-subchain-form
+    :title="configKeysEnum.MRF_SUBCHAIN"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_SUBCHAIN]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.MRF_REJECTNONPUBLIC"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_REJECTNONPUBLIC]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.MRF_HELLTHREAD"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_HELLTHREAD]"
+    showTitle
+  />
+  <mrf-keyword-form
+    :title="configKeysEnum.MRF_KEYWORD"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_KEYWORD]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.MRF_MENTION"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_MENTION]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.MRF_NORMALIZE_MARKUP"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_NORMALIZE_MARKUP]"
+    showTitle
+  />
+  <m-r-f-user-allowlist-form
+    :title="configKeysEnum.MRF_USER_ALLOWLIST"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_USER_ALLOWLIST]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.MRF_VOCABULARY"
+    v-if="tabKey === configKeysEnum.MRF_SIMPLE"
+    v-model="config[configKeysEnum.MRF_VOCABULARY]"
+    showTitle
+  />
+  <config-form
+    :title="configKeysEnum.WEB_FEDERATOR_RETRY_QUEUE"
+    v-if="tabKey === configKeysEnum.JOB_QUEUE"
+    v-model="config[configKeysEnum.WEB_FEDERATOR_RETRY_QUEUE]"
+    showTitle
+  />
+</div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue } from 'vue-property-decorator'
+import { FulfillingBouncingCircleSpinner } from 'epic-spinners'
+import { configKeys } from '../../data/Config'
+import EmailsForm from './forms/EmailsForm.vue'
+import AutoLinkerForm from './forms/AutoLinkerForm.vue'
+import EmojiForm from './forms/EmojiForm.vue'
+import AssetsForm from './forms/AssetsForm.vue'
+import MrfSubchainForm from './forms/MRFSubchainForm.vue'
+import MrfKeywordForm from './forms/MRFKeywordForm.vue'
+import MRFUserAllowlistForm from './forms/MRFUserAllowlistForm.vue'
+import ConfigForm from './forms/ConfigForm.vue'
+
+@Component({
+  components: {
+    ConfigForm,
+    MRFUserAllowlistForm,
+    EmojiForm,
+    AutoLinkerForm,
+    EmailsForm,
+    AssetsForm,
+    MrfSubchainForm,
+    MrfKeywordForm,
+    FulfillingBouncingCircleSpinner
+  },
+})
+export default class ConfigSettingsForms extends Vue {
+  configKeysEnum = configKeys
+  @Prop(Object) readonly value!: Object
+  @Prop(String) readonly tabKey!: String
+  @Prop(Array) readonly tabs!: Array
+  @Prop(Object) readonly error!: Object
+  showTab ({ key }) {
+    return this.tabKey === key &&
+      key !== this.configKeysEnum.AUTO_LINKER &&
+      key !== this.configKeysEnum.EMAILS &&
+      key !== this.configKeysEnum.ASSETS
+  }
+  showTabTitle ({ key }) {
+    return this.tabKey === this.configKeysEnum.LDAP ||
+      this.tabKey === this.configKeysEnum.MRF_SIMPLE
+  }
+  get config () {
+    return this.value
+  }
+  set config (value) {
+    this.$emit('change', value)
+  }
+}
+</script>
+
+<style lang="scss">
+</style>
diff --git a/src/components/configSettings/forms/AssetsForm.vue b/src/components/configSettings/forms/AssetsForm.vue
index bd84247..9073a2e 100644
--- a/src/components/configSettings/forms/AssetsForm.vue
+++ b/src/components/configSettings/forms/AssetsForm.vue
@@ -60,10 +60,10 @@ export default class AssetsForm extends Vue {
   @Prop(String) readonly title!: string
   @Prop(Boolean) readonly showTitle!: boolean
   @Prop(String) readonly margin!: string
+  @Prop(Object) readonly error!: Object
   get selectOptions () {
     return this.formData.mascots.map(({ name }) => name)
   }
-  error: {mascots?: any} = { mascots: [] }
   get formData () {
     return this.value
   }
@@ -76,11 +76,6 @@ export default class AssetsForm extends Vue {
   removeMascots (index) {
     this.formData.mascots.splice(index, 1)
   }
-  // public method
-  validate () {
-    this.error.mascots = this.formData.mascots.map((item) => (!item.name.length || !item.url.length || !item.mime_type.length ? 'mascots_error' : ''))
-    return this.error.mascots.findIndex(error => error.length) === -1
-  }
 }
 </script>
 
diff --git a/src/components/pages/configSettings/ConfigSettingsPage.vue b/src/components/pages/configSettings/ConfigSettingsPage.vue
index e0e17a8..edd5043 100644
--- a/src/components/pages/configSettings/ConfigSettingsPage.vue
+++ b/src/components/pages/configSettings/ConfigSettingsPage.vue
@@ -9,172 +9,13 @@
       </va-tab>
     </va-tabs>
     <div class="config-settings-page__content pt-4">
-      <div v-if="config">
-        <template v-for="tab in tabs">
-          <config-form
-            :key="tab.key"
-            v-model="config[tab.key]"
-            :title="tab.key"
-            :showTitle="showTabTitle(tab)"
-            v-if="showTab(tab)"
-          />
-        </template>
-        <emails-form v-if="tabs[value].key === configKeysEnum.EMAILS" v-model="config[configKeysEnum.EMAILS]"/>
-        <config-form
-          :title="configKeysEnum.UPLOADERSS3"
-          v-if="tabs[value].key === configKeysEnum.UPLOAD"
-          v-model="config[configKeysEnum.UPLOADERSS3]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.UPLOADERSLOCAL"
-          v-if="tabs[value].key === configKeysEnum.UPLOAD"
-          v-model="config[configKeysEnum.UPLOADERSLOCAL]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.UPLOADERSMDII"
-          v-if="tabs[value].key === configKeysEnum.UPLOAD"
-          v-model="config[configKeysEnum.UPLOADERSMDII]"
-          show-title
-        />
-        <config-form
-          :title="configKeysEnum.MARKUP"
-          v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
-          v-model="config[configKeysEnum.MARKUP]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.CHAT"
-          v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
-          v-model="config[configKeysEnum.CHAT]"
-          showTitle
-        />
-        <emoji-form
-          :title="configKeysEnum.EMOJI"
-          v-if="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
-          v-model="config[configKeysEnum.EMOJI]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.ECTO_REPOS"
-          v-if="tabs[value].key === configKeysEnum.DATABASE"
-          v-model="config[configKeysEnum.ECTO_REPOS]"
-        />
-        <assets-form
-          :title="configKeysEnum.ASSETS"
-          v-show="tabs[value].key === configKeysEnum.FRONTEND_CONFIGURATIONS"
-          v-model="config[configKeysEnum.ASSETS]"
-          showTitle
-          ref="ASSETS"
-        />
-        <config-form
-          :title="configKeysEnum.KOCAPTCHA"
-          v-if="tabs[value].key === configKeysEnum.CAPTCHA"
-          v-model="config[configKeysEnum.KOCAPTCHA]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.WEB_AUTHENTICATOR"
-          v-if="tabs[value].key === configKeysEnum.AUTH"
-          v-model="config[configKeysEnum.WEB_AUTHENTICATOR]"
-        />
-        <config-form
-          :title="configKeysEnum.LDAP"
-          v-if="tabs[value].key === configKeysEnum.AUTH"
-          v-model="config[configKeysEnum.LDAP]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.TWITTER_OAUTH"
-          v-if="tabs[value].key === configKeysEnum.AUTH"
-          v-model="config[configKeysEnum.TWITTER_OAUTH]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.FACEBOOK_OAUTH"
-          v-if="tabs[value].key === configKeysEnum.AUTH"
-          v-model="config[configKeysEnum.FACEBOOK_OAUTH]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.GOOGLE_OAUTH"
-          v-if="tabs[value].key === configKeysEnum.AUTH"
-          v-model="config[configKeysEnum.GOOGLE_OAUTH]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.MICROSOFT_OAUTH"
-          v-if="tabs[value].key === configKeysEnum.AUTH"
-          v-model="config[configKeysEnum.MICROSOFT_OAUTH]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.OAUTH2"
-          v-if="tabs[value].key === configKeysEnum.AUTH"
-          v-model="config[configKeysEnum.OAUTH2]"
-          showTitle
-        />
-        <auto-linker-form
-          :title="configKeysEnum.AUTO_LINKER"
-          v-if="tabs[value].key === configKeysEnum.AUTO_LINKER"
-          v-model="config[configKeysEnum.AUTO_LINKER]"
-        />
-        <mrf-subchain-form
-          :title="configKeysEnum.MRF_SUBCHAIN"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_SUBCHAIN]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.MRF_REJECTNONPUBLIC"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_REJECTNONPUBLIC]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.MRF_HELLTHREAD"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_HELLTHREAD]"
-          showTitle
-        />
-        <mrf-keyword-form
-          :title="configKeysEnum.MRF_KEYWORD"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_KEYWORD]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.MRF_MENTION"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_MENTION]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.MRF_NORMALIZE_MARKUP"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_NORMALIZE_MARKUP]"
-          showTitle
-        />
-        <m-r-f-user-allowlist-form
-          :title="configKeysEnum.MRF_USER_ALLOWLIST"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_USER_ALLOWLIST]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.MRF_VOCABULARY"
-          v-if="tabs[value].key === configKeysEnum.MRF_SIMPLE"
-          v-model="config[configKeysEnum.MRF_VOCABULARY]"
-          showTitle
-        />
-        <config-form
-          :title="configKeysEnum.WEB_FEDERATOR_RETRY_QUEUE"
-          v-if="tabs[value].key === configKeysEnum.JOB_QUEUE"
-          v-model="config[configKeysEnum.WEB_FEDERATOR_RETRY_QUEUE]"
-          showTitle
-        />
-      </div>
+      <config-settings-forms
+        v-if="config"
+        v-model="config"
+        :tabKey="currentTabKey"
+        :tabs="tabs"
+        :error="error"
+      />
       <div class="loading flex-center" v-if="loading">
         <fulfilling-bouncing-circle-spinner
           :animation-duration="2500"
@@ -204,9 +45,11 @@ import MrfSubchainForm from '../../configSettings/forms/MRFSubchainForm.vue'
 import MrfKeywordForm from '../../configSettings/forms/MRFKeywordForm.vue'
 import MRFUserAllowlistForm from '../../configSettings/forms/MRFUserAllowlistForm.vue'
 import ConfigForm from '../../configSettings/forms/ConfigForm.vue'
+import ConfigSettingsForms from '../../configSettings/ConfigSettingsForms.vue'
 
 @Component({
   components: {
+    ConfigSettingsForms,
     ConfigForm,
     MRFUserAllowlistForm,
     EmojiForm,
@@ -223,6 +66,7 @@ export default class ConfigSettingsPage extends Vue {
   config:any = null
   configKeysEnum = configKeys
   loading:boolean = false
+  error: {assets: {mascots?: any}} = { assets: { mascots: [] } }
   async mounted () {
     this.loading = true
     try {
@@ -277,8 +121,8 @@ export default class ConfigSettingsPage extends Vue {
     return configKeysTabs.filter(({ tab }) => tab)
   }
   validateForms () {
-    // @ts-ignore
-    return this.$refs.ASSETS.validate()
+    this.error.assets.mascots = this.config[configKeys.ASSETS].mascots.map((item) => (!item.name.length || !item.url.length || !item.mime_type.length ? 'mascots_error' : ''))
+    return this.error.assets.mascots.findIndex(error => error.length) === -1
   }
 }
 </script>
diff --git a/src/entities/settings/WebEndpointConfig.ts b/src/entities/settings/WebEndpointConfig.ts
index bdc40a7..4f9808e 100644
--- a/src/entities/settings/WebEndpointConfig.ts
+++ b/src/entities/settings/WebEndpointConfig.ts
@@ -74,20 +74,19 @@ export default class WebEndpointConfig {
 }
 
 export const normalizeWebEndpointConfig = (config) => {
-  // const apiConf = [...config]
-  // const render_errors = apiConf.find(({ tuple }) => tuple[0] === ':render_errors')
-  // if (render_errors.tuple && render_errors && render_errors.tuple[1]) {
-  //   const accepts = render_errors.tuple[1].find(({ tuple }) => tuple[0] === ':accepts')
-  //   accepts.tuple[1] = accepts.tuple[1].split(';')
-  // }
-  // const http = apiConf.find(({ tuple }) => tuple[0] === ':http')
-  // if (http.tuple && http && http.tuple[1]) {
-  //   const dispatch = http.tuple[1].find(({ tuple }) => tuple[0] === ':dispatch')
-  //   dispatch.tuple[1] = [dispatch.tuple[1]]
-  //   const ip = http.tuple[1].find(({ tuple }) => tuple[0] === ':ip')
-  //   ip.tuple[1] = { tuple: ip.tuple[1].split(';')}
-  // }
-  // return apiConf
-  console.log(data)
-  return data.configs.find(({ key }) => key === configKeys.WEB_ENDPOINT).value
+  const apiConf = [...config]
+  const render_errors = apiConf.find(({ tuple }) => tuple[0] === ':render_errors')
+  if (render_errors.tuple && render_errors && render_errors.tuple[1]) {
+    const accepts = render_errors.tuple[1].find(({ tuple }) => tuple[0] === ':accepts')
+    accepts.tuple[1] = accepts.tuple[1].split(';')
+  }
+  const http = apiConf.find(({ tuple }) => tuple[0] === ':http')
+  if (http.tuple && http && http.tuple[1]) {
+    const dispatch = http.tuple[1].find(({ tuple }) => tuple[0] === ':dispatch')
+    dispatch.tuple[1] = [dispatch.tuple[1]]
+    const ip = http.tuple[1].find(({ tuple }) => tuple[0] === ':ip')
+    ip.tuple[1] = { tuple: ip.tuple[1].split(';')}
+  }
+  return apiConf
+  // return data.configs.find(({ key }) => key === configKeys.WEB_ENDPOINT).value
 }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d74238c..4ef53d5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -448,7 +448,7 @@
       ":gopher": "Gopher",
       ":auth": "Authentication",
       ":mrf_simple": "MRF",
-      ":valid_details": "Web push encryption",
+      ":vapid_details": "Web push encryption",
       ":activitypub": "Activity pub",
       ":suggestions": "Suggestions",
       ":uri_schemes": "URI schemes",
-- 
GitLab


From e3b5afbe9d3855f71dba3037766edd94160f5ce3 Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 15 Aug 2019 12:09:14 +0300
Subject: [PATCH 60/61] fix: type error

---
 src/components/configSettings/ConfigSettingsForms.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/configSettings/ConfigSettingsForms.vue b/src/components/configSettings/ConfigSettingsForms.vue
index 0b34078..d16eb7c 100644
--- a/src/components/configSettings/ConfigSettingsForms.vue
+++ b/src/components/configSettings/ConfigSettingsForms.vue
@@ -200,7 +200,7 @@ export default class ConfigSettingsForms extends Vue {
   configKeysEnum = configKeys
   @Prop(Object) readonly value!: Object
   @Prop(String) readonly tabKey!: String
-  @Prop(Array) readonly tabs!: Array
+  @Prop(Array) readonly tabs!: Array<Object>
   @Prop(Object) readonly error!: Object
   showTab ({ key }) {
     return this.tabKey === key &&
-- 
GitLab


From ded929e76d989430957fb120b1a604383e558f7a Mon Sep 17 00:00:00 2001
From: "nastassia.danilova" <nastassia.danilova@epicmax.co>
Date: Thu, 15 Aug 2019 12:24:03 +0300
Subject: [PATCH 61/61] feat: remove unnecessary chart data

---
 src/data/charts/BubbleChartData.js            | 236 ------------------
 src/data/charts/DonutChartData.js             |   8 -
 src/data/charts/HorizontalBarChartData.js     |  17 --
 src/data/charts/LineChartData.js              |  43 ----
 src/data/charts/PieChartData.js               |   8 -
 src/data/charts/VerticalBarChartData.js       |  17 --
 .../va-chart/VaChart.demo.vue                 |  50 ----
 .../vuestic-components/va-chart/VaChart.vue   |  65 -----
 .../va-chart/VaChartConfigs.js                |  27 --
 .../va-chart/chart-types/BubbleChart.js       |   7 -
 .../va-chart/chart-types/DonutChart.js        |   7 -
 .../chart-types/HorizontalBarChart.js         |   7 -
 .../va-chart/chart-types/LineChart.js         |   7 -
 .../va-chart/chart-types/PieChart.js          |   7 -
 .../va-chart/chart-types/VerticalBarChart.js  |   7 -
 .../va-chart/chart-types/chartMixin.js        |  21 --
 16 files changed, 534 deletions(-)
 delete mode 100644 src/data/charts/BubbleChartData.js
 delete mode 100644 src/data/charts/DonutChartData.js
 delete mode 100644 src/data/charts/HorizontalBarChartData.js
 delete mode 100644 src/data/charts/LineChartData.js
 delete mode 100644 src/data/charts/PieChartData.js
 delete mode 100644 src/data/charts/VerticalBarChartData.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/VaChart.vue
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/VaChartConfigs.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/chart-types/BubbleChart.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/chart-types/DonutChart.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/chart-types/HorizontalBarChart.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/chart-types/LineChart.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/chart-types/PieChart.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/chart-types/VerticalBarChart.js
 delete mode 100644 src/vuestic-theme/vuestic-components/va-chart/chart-types/chartMixin.js

diff --git a/src/data/charts/BubbleChartData.js b/src/data/charts/BubbleChartData.js
deleted file mode 100644
index 0653460..0000000
--- a/src/data/charts/BubbleChartData.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import { hex2rgb } from '../../services/color-functions'
-
-export const getBubbleChartData = (themes) => ({
-  datasets: [
-    {
-      label: 'USA',
-      backgroundColor: hex2rgb(themes['danger'], 0.9).css,
-      borderColor: 'transparent',
-      data: [
-        {
-          x: 23,
-          y: 25,
-          r: 15,
-        },
-        {
-          x: 40,
-          y: 10,
-          r: 10,
-        },
-        {
-          x: 30,
-          y: 22,
-          r: 30,
-        },
-        {
-          x: 7,
-          y: 43,
-          r: 40,
-        },
-        {
-          x: 23,
-          y: 27,
-          r: 120,
-        },
-        {
-          x: 20,
-          y: 15,
-          r: 11,
-        },
-        {
-          x: 7,
-          y: 10,
-          r: 35,
-        },
-        {
-          x: 10,
-          y: 20,
-          r: 40,
-        },
-      ],
-    },
-    {
-      label: 'Russia',
-      backgroundColor: hex2rgb(themes['primary'], 0.9).css,
-      borderColor: 'transparent',
-      data: [
-        {
-          x: 0,
-          y: 30,
-          r: 15,
-        },
-        {
-          x: 20,
-          y: 20,
-          r: 20,
-        },
-        {
-          x: 15,
-          y: 15,
-          r: 50,
-        },
-        {
-          x: 31,
-          y: 46,
-          r: 30,
-        },
-        {
-          x: 20,
-          y: 14,
-          r: 25,
-        },
-        {
-          x: 34,
-          y: 17,
-          r: 30,
-        },
-        {
-          x: 44,
-          y: 44,
-          r: 10,
-        },
-        {
-          x: 39,
-          y: 25,
-          r: 35,
-        },
-      ],
-    },
-    {
-      label: 'Canada',
-      backgroundColor: hex2rgb(themes['warning'], 0.9).css,
-      borderColor: 'transparent',
-      data: [
-        {
-          x: 10,
-          y: 30,
-          r: 45,
-        },
-        {
-          x: 10,
-          y: 50,
-          r: 20,
-        },
-        {
-          x: 5,
-          y: 5,
-          r: 30,
-        },
-        {
-          x: 40,
-          y: 30,
-          r: 20,
-        },
-        {
-          x: 33,
-          y: 15,
-          r: 18,
-        },
-        {
-          x: 40,
-          y: 20,
-          r: 40,
-        },
-        {
-          x: 33,
-          y: 33,
-          r: 40,
-        },
-      ],
-    },
-    {
-      label: 'Belarus',
-      backgroundColor: hex2rgb(themes['info'], 0.9).css,
-      borderColor: 'transparent',
-      data: [
-        {
-          x: 35,
-          y: 30,
-          r: 45,
-        },
-        {
-          x: 25,
-          y: 40,
-          r: 35,
-        },
-        {
-          x: 5,
-          y: 5,
-          r: 30,
-        },
-        {
-          x: 5,
-          y: 20,
-          r: 40,
-        },
-        {
-          x: 10,
-          y: 40,
-          r: 15,
-        },
-        {
-          x: 3,
-          y: 10,
-          r: 10,
-        },
-        {
-          x: 15,
-          y: 40,
-          r: 40,
-        },
-        {
-          x: 7,
-          y: 15,
-          r: 10,
-        },
-      ],
-    },
-    {
-      label: 'Ukraine',
-      backgroundColor: hex2rgb(themes['success'], 0.9).css,
-      borderColor: 'transparent',
-      data: [
-        {
-          x: 25,
-          y: 10,
-          r: 40,
-        },
-        {
-          x: 17,
-          y: 40,
-          r: 40,
-        },
-        {
-          x: 35,
-          y: 10,
-          r: 20,
-        },
-        {
-          x: 3,
-          y: 40,
-          r: 10,
-        },
-        {
-          x: 40,
-          y: 40,
-          r: 40,
-        },
-        {
-          x: 20,
-          y: 10,
-          r: 10,
-        },
-        {
-          x: 10,
-          y: 27,
-          r: 35,
-        },
-        {
-          x: 7,
-          y: 26,
-          r: 40,
-        },
-      ],
-    },
-  ],
-})
diff --git a/src/data/charts/DonutChartData.js b/src/data/charts/DonutChartData.js
deleted file mode 100644
index e8877d2..0000000
--- a/src/data/charts/DonutChartData.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const getDonutChartData = (themes) => ({
-  labels: ['North America', 'South America', 'Australia'],
-  datasets: [{
-    label: 'Population (millions)',
-    backgroundColor: [themes['danger'], themes['info'], themes['success']],
-    data: [2478, 5267, 734],
-  }],
-})
diff --git a/src/data/charts/HorizontalBarChartData.js b/src/data/charts/HorizontalBarChartData.js
deleted file mode 100644
index 19dbe89..0000000
--- a/src/data/charts/HorizontalBarChartData.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export const getHorizontalBarChartData = (themes) => ({
-  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
-  datasets: [
-    {
-      label: 'Vuestic Satisfaction Score',
-      backgroundColor: themes['warning'],
-      borderColor: 'transparent',
-      data: [80, 90, 50, 70, 60, 90, 50, 90, 80, 40, 72, 93],
-    },
-    {
-      label: 'Bulma Satisfaction Score',
-      backgroundColor: themes['danger'],
-      borderColor: 'transparent',
-      data: [20, 30, 20, 40, 50, 40, 15, 60, 30, 20, 42, 53],
-    },
-  ],
-})
diff --git a/src/data/charts/LineChartData.js b/src/data/charts/LineChartData.js
deleted file mode 100644
index 7191ed5..0000000
--- a/src/data/charts/LineChartData.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { hex2rgb } from '../../services/color-functions'
-
-const generateValue = () => {
-  return Math.floor(Math.random() * 100)
-}
-
-const generateYLabels = () => {
-  const flip = !!Math.floor(Math.random() * 2)
-  return flip ? ['Debit', 'Credit'] : ['Credit', 'Debit']
-}
-
-const generateArray = (length) => {
-  return Array.from(Array(length), generateValue)
-}
-
-const getSize = () => {
-  const minSize = 4
-  return minSize + Math.floor(Math.random() * 3)
-}
-
-export const getLineChartData = (themes) => {
-  const size = getSize()
-  const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July']
-  const yLabels = generateYLabels()
-
-  return {
-    labels: months.splice(0, size),
-    datasets: [
-      {
-        label: yLabels[0],
-        backgroundColor: hex2rgb(themes['primary'], 0.6).css,
-        borderColor: 'transparent',
-        data: generateArray(size),
-      },
-      {
-        label: yLabels[1],
-        backgroundColor: hex2rgb(themes['info'], 0.6).css,
-        borderColor: 'transparent',
-        data: generateArray(size),
-      },
-    ],
-  }
-}
diff --git a/src/data/charts/PieChartData.js b/src/data/charts/PieChartData.js
deleted file mode 100644
index d012189..0000000
--- a/src/data/charts/PieChartData.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const getPieChartData = (themes) => ({
-  labels: ['Africa', 'Asia', 'Europe'],
-  datasets: [{
-    label: 'Population (millions)',
-    backgroundColor: [themes['primary'], themes['warning'], themes['danger']],
-    data: [2478, 5267, 734],
-  }],
-})
diff --git a/src/data/charts/VerticalBarChartData.js b/src/data/charts/VerticalBarChartData.js
deleted file mode 100644
index 778721c..0000000
--- a/src/data/charts/VerticalBarChartData.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export const getVerticalBarChartData = (themes) => ({
-  labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
-  datasets: [
-    {
-      label: 'USA',
-      backgroundColor: themes['primary'],
-      borderColor: 'transparent',
-      data: [50, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11],
-    },
-    {
-      label: 'USSR',
-      backgroundColor: themes['info'],
-      borderColor: 'transparent',
-      data: [50, 10, 22, 39, 15, 20, 85, 32, 60, 50, 20, 30],
-    },
-  ],
-})
diff --git a/src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue b/src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue
deleted file mode 100644
index 4e07de2..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/VaChart.demo.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<template>
-  <VbDemo>
-    <VbCard>
-      <va-button @click="refreshData()">
-        refreshData
-      </va-button>
-    </VbCard>
-    <VbCard title="Pie">
-      <va-chart :data="chartData" type="pie"/>
-    </VbCard>
-    <VbCard title="Line">
-      <va-chart :data="chartData" type="line"/>
-    </VbCard>
-    <VbCard title="Bubble">
-      <va-chart :data="chartData" type="bubble"/>
-    </VbCard>
-    <VbCard title="Donut">
-      <va-chart :data="chartData" type="donut"/>
-    </VbCard>
-    <VbCard title="Horizontal-bar">
-      <va-chart :data="chartData" type="horizontal-bar"/>
-    </VbCard>
-    <VbCard title="Vertical-bar">
-      <va-chart :data="chartData" type="vertical-bar"/>
-    </VbCard>
-  </VbDemo>
-</template>
-
-<script>
-import VaButton from '../va-button/VaButton'
-import VaChart from './VaChart.vue'
-import { getLineChartData } from '../../../data/charts/LineChartData'
-
-export default {
-  data () {
-    return {
-      chartData: getLineChartData(this.$themes),
-    }
-  },
-  components: {
-    VaButton,
-    VaChart,
-  },
-  methods: {
-    refreshData () {
-      this.chartData = getLineChartData(this.$themes)
-    },
-  },
-}
-</script>
diff --git a/src/vuestic-theme/vuestic-components/va-chart/VaChart.vue b/src/vuestic-theme/vuestic-components/va-chart/VaChart.vue
deleted file mode 100644
index a9c9398..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/VaChart.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-  <component
-    ref="chart"
-    class='va-chart'
-    :is="chartComponent"
-    :options="options"
-    :chart-data="data"
-  />
-</template>
-
-<script>
-import PieChart from './chart-types/PieChart'
-import BubbleChart from './chart-types/BubbleChart'
-import DonutChart from './chart-types/DonutChart'
-import HorizontalBarChart from './chart-types/HorizontalBarChart'
-import VerticalBarChart from './chart-types/VerticalBarChart'
-import LineChart from './chart-types/LineChart'
-import { chartTypesMap } from './VaChartConfigs'
-
-export default {
-  name: 'va-chart',
-  props: {
-    data: {},
-    options: {},
-    type: {
-      validator (type) {
-        return type in chartTypesMap
-      },
-    },
-  },
-  components: {
-    PieChart,
-    LineChart,
-    VerticalBarChart,
-    HorizontalBarChart,
-    DonutChart,
-    BubbleChart,
-  },
-  computed: {
-    chartComponent () {
-      return chartTypesMap[this.type]
-    },
-  },
-}
-</script>
-
-<style lang='scss'>
-.va-chart {
-  width: 100%;
-  height: 100%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  > * {
-    height: 100%;
-    width: 100%;
-  }
-
-  canvas {
-    width: 100%;
-    height: auto;
-  }
-}
-</style>
diff --git a/src/vuestic-theme/vuestic-components/va-chart/VaChartConfigs.js b/src/vuestic-theme/vuestic-components/va-chart/VaChartConfigs.js
deleted file mode 100644
index fa04ed5..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/VaChartConfigs.js
+++ /dev/null
@@ -1,27 +0,0 @@
-export const defaultConfig = {
-  legend: {
-    position: 'bottom',
-    labels: {
-      fontColor: '#34495e',
-      fontFamily: 'sans-serif',
-      fontSize: 14,
-      padding: 20,
-      usePointStyle: true,
-    },
-  },
-  tooltips: {
-    bodyFontSize: 14,
-    bodyFontFamily: 'sans-serif',
-  },
-  responsive: true,
-  maintainAspectRatio: false,
-}
-
-export const chartTypesMap = {
-  pie: 'pie-chart',
-  donut: 'donut-chart',
-  bubble: 'bubble-chart',
-  line: 'line-chart',
-  'horizontal-bar': 'horizontal-bar-chart',
-  'vertical-bar': 'vertical-bar-chart',
-}
diff --git a/src/vuestic-theme/vuestic-components/va-chart/chart-types/BubbleChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/BubbleChart.js
deleted file mode 100644
index 4ff5592..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/chart-types/BubbleChart.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Bubble } from 'vue-chartjs'
-import { chartMixin } from './chartMixin'
-
-export default {
-  extends: Bubble,
-  mixins: [chartMixin],
-}
diff --git a/src/vuestic-theme/vuestic-components/va-chart/chart-types/DonutChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/DonutChart.js
deleted file mode 100644
index 2478866..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/chart-types/DonutChart.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Doughnut } from 'vue-chartjs'
-import { chartMixin } from './chartMixin'
-
-export default {
-  extends: Doughnut,
-  mixins: [chartMixin],
-}
diff --git a/src/vuestic-theme/vuestic-components/va-chart/chart-types/HorizontalBarChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/HorizontalBarChart.js
deleted file mode 100644
index 8d51ee4..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/chart-types/HorizontalBarChart.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { HorizontalBar } from 'vue-chartjs'
-import { chartMixin } from './chartMixin'
-
-export default {
-  extends: HorizontalBar,
-  mixins: [chartMixin],
-}
diff --git a/src/vuestic-theme/vuestic-components/va-chart/chart-types/LineChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/LineChart.js
deleted file mode 100644
index 92b1bfb..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/chart-types/LineChart.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Line } from 'vue-chartjs'
-import { chartMixin } from './chartMixin'
-
-export default {
-  extends: Line,
-  mixins: [chartMixin],
-}
diff --git a/src/vuestic-theme/vuestic-components/va-chart/chart-types/PieChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/PieChart.js
deleted file mode 100644
index c7330b7..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/chart-types/PieChart.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Pie } from 'vue-chartjs'
-import { chartMixin } from './chartMixin'
-
-export default {
-  extends: Pie,
-  mixins: [chartMixin],
-}
diff --git a/src/vuestic-theme/vuestic-components/va-chart/chart-types/VerticalBarChart.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/VerticalBarChart.js
deleted file mode 100644
index 3fc0904..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/chart-types/VerticalBarChart.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Bar } from 'vue-chartjs'
-import { chartMixin } from './chartMixin'
-
-export default {
-  extends: Bar,
-  mixins: [chartMixin],
-}
diff --git a/src/vuestic-theme/vuestic-components/va-chart/chart-types/chartMixin.js b/src/vuestic-theme/vuestic-components/va-chart/chart-types/chartMixin.js
deleted file mode 100644
index 6dd4050..0000000
--- a/src/vuestic-theme/vuestic-components/va-chart/chart-types/chartMixin.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { mixins } from 'vue-chartjs'
-import { defaultConfig } from '../VaChartConfigs'
-
-export const chartMixin = {
-  mixins: [mixins.reactiveProp],
-  props: ['data', 'chartOptions'],
-  mounted () {
-    this.refresh()
-  },
-  methods: {
-    refresh () {
-      this.renderChart(this.chartData, this.options)
-    },
-  },
-  computed: {
-    // `this.options` is used by vue-chartjs mixin on refresh.
-    options () {
-      return Object.assign({}, defaultConfig, this.chartOptions)
-    },
-  },
-}
-- 
GitLab