Kaynağa Gözat

Adds custom entry support to autocomplete component.

Also prepares credentials and woinfo components to support it.
Christopher Leggett 5 yıl önce
ebeveyn
işleme
3d1be82732

+ 12 - 0
app/CredDesc.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+class CredDesc extends Model
+{
+    protected $table = 'creddesc';
+    protected $primaryKey = 'creddescid';
+    public $timestamps = null;
+}

+ 66 - 0
app/Http/Controllers/Api/CredDescController.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Http\Controllers\Api;
+
+use App\CredDesc;
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+
+class CredDescController extends Controller
+{
+    /**
+     * Display a listing of the resource.
+     *
+     * @return \Illuminate\Http\Response
+     */
+    public function index()
+    {
+        $credDescList = CredDesc::all();
+        return response()->json($credDescList, 200);
+    }
+
+    /**
+     * Store a newly created resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @return \Illuminate\Http\Response
+     */
+    public function store(Request $request)
+    {
+        //
+    }
+
+    /**
+     * Display the specified resource.
+     *
+     * @param  \App\CredDesc  $credDesc
+     * @return \Illuminate\Http\Response
+     */
+    public function show(CredDesc $credDesc)
+    {
+        //
+    }
+
+    /**
+     * Update the specified resource in storage.
+     *
+     * @param  \Illuminate\Http\Request  $request
+     * @param  \App\CredDesc  $credDesc
+     * @return \Illuminate\Http\Response
+     */
+    public function update(Request $request, CredDesc $credDesc)
+    {
+        //
+    }
+
+    /**
+     * Remove the specified resource from storage.
+     *
+     * @param  \App\CredDesc  $credDesc
+     * @return \Illuminate\Http\Response
+     */
+    public function destroy(CredDesc $credDesc)
+    {
+        //
+    }
+}

+ 2 - 0
app/Http/Controllers/Api/CredentialsController.php

@@ -49,6 +49,8 @@ class CredentialsController extends Controller
      */
     public function update(Request $request, Credential $credential)
     {
+        $credential->credtype = $request->input('credtype');
+        $credential->creddesc = $request->input('creddesc');
         $credential->creduser = $request->input('creduser');
         $credential->credpass = $request->input('credpass');
         $credential->save();

+ 44 - 0
public/css/app.css

@@ -16882,3 +16882,47 @@ a.text-dark:focus {
   display: block;
 }
 
+/* For autocomplete-custom-dropdown Vue component */
+
+.dropdown.open .suggestion-list {
+  display: block;
+}
+
+.dropdown .suggestion-list {
+  display: none;
+}
+
+.toggle .arrow-up {
+  display: none;
+}
+
+.open .toggle .arrow-up {
+  display: inline-block;
+}
+
+.open .toggle .arrow-down {
+  display: none;
+}
+
+.suggestion-list li {
+  cursor: pointer;
+}
+
+.suggestion-list li:hover {
+  color: #fff;
+  background-color: #663399;
+}
+
+.suggestion-list {
+  background-color: rgba(255, 255, 255, 0.95);
+  border: 1px solid #ddd;
+  list-style: none;
+  display: block;
+  margin: 0;
+  padding: 0.3rem;
+  width: 100%;
+  overflow: hidden;
+  position: relative;
+  z-index: 2;
+}
+

+ 400 - 72
public/js/app.js

@@ -2024,6 +2024,144 @@ __webpack_require__.r(__webpack_exports__);
 
 /***/ }),
 
+/***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=script&lang=js&":
+/*!***************************************************************************************************************************************************************************************!*\
+  !*** ./node_modules/babel-loader/lib??ref--4-0!./node_modules/vue-loader/lib??vue-loader-options!./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=script&lang=js& ***!
+  \***************************************************************************************************************************************************************************************/
+/*! exports provided: default */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+/* harmony default export */ __webpack_exports__["default"] = ({
+  props: {
+    value: null,
+    // An object with the following format
+    // {
+    //     key: value,
+    //     key: value,
+    // }
+    options: {
+      type: Object,
+      required: true
+    },
+    allowCustom: {
+      type: Boolean,
+      "default": false
+    }
+  },
+  data: function data() {
+    return {
+      searchText: '',
+      selectedOption: null,
+      open: false,
+      // allows for checking whether button was clicked or text was typed.
+      // If button was clicked, we show all options without having to clear
+      // out any searchText that was autofilled when page loaded.
+      isSearching: false
+    };
+  },
+  computed: {
+    matches: function matches() {
+      var _this = this;
+
+      // If button was clicked, show all options, if typing, filter
+      // options based on searchText
+      return Object.entries(this.options).filter(function (option) {
+        var optionText = option[1].toUpperCase();
+
+        if (_this.isSearching) {
+          return optionText.match(_this.searchText.toUpperCase());
+        } else {
+          return optionText.match('');
+        }
+      });
+    }
+  },
+  methods: {
+    // if button is clicked, show all options
+    setOpen: function setOpen(isOpen) {
+      this.open = isOpen;
+      this.isSearching = false;
+    },
+    // if typing in the box, start filtering options
+    searchChanged: function searchChanged() {
+      if (!this.open) {
+        this.open = true;
+      }
+
+      if (!this.isSearching) {
+        this.isSearching = true;
+      } // If custom values are allowed, emits {id: 'custom', name: searchtext}
+
+
+      if (this.allowCustom) {
+        this.$emit('input', {
+          'id': 'custom',
+          'name': this.searchText
+        });
+      }
+    },
+    // emits { id: suggestion_id, name: suggestion_name} to v-model when 
+    // a suggestion is selected
+    suggestionSelected: function suggestionSelected(suggestion) {
+      this.open = false;
+      this.searchText = suggestion[1];
+      this.$emit('input', {
+        'id': suggestion[0],
+        'name': suggestion[1]
+      });
+    },
+    updateComponentWithValue: function updateComponentWithValue(newValue) {
+      if (this.allowCustom) {
+        this.searchText = this.value.name;
+      } else {
+        if (newValue.id > -1) {
+          // Find the matching text for the supplied option value
+          for (var id in this.options) {
+            if (this.options.hasOwnProperty(id)) {
+              if (this.options[id] === this.options[newValue.id]) {
+                this.searchText = this.options[id];
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  mounted: function mounted() {
+    this.updateComponentWithValue(this.value);
+  },
+  watch: {
+    value: function value(newValue) {
+      this.updateComponentWithValue(newValue);
+    }
+  }
+});
+
+/***/ }),
+
 /***/ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./resources/js/components/credential-form-modal.vue?vue&type=script&lang=js&":
 /*!********************************************************************************************************************************************************************************!*\
   !*** ./node_modules/babel-loader/lib??ref--4-0!./node_modules/vue-loader/lib??vue-loader-options!./resources/js/components/credential-form-modal.vue?vue&type=script&lang=js& ***!
@@ -2056,15 +2194,28 @@ __webpack_require__.r(__webpack_exports__);
 //
 //
 //
+//
+//
+//
 /* harmony default export */ __webpack_exports__["default"] = ({
-  props: ['populateWith', 'modalId'],
+  props: ['populateWith', 'modalId', 'descriptions'],
   data: function data() {
     return {
       data: JSON.parse(JSON.stringify(this.populateWith)),
+      creddesc: {},
       id: this.modalId,
       errors: []
     };
   },
+  mounted: function mounted() {
+    // Definitely needs to be refactored into something that runs once
+    // per page instead of once per credential. Only putting this here now
+    // to test something else.
+    this.creddesc = {
+      'id': this.data.credtype,
+      'name': this.data.creddesc
+    };
+  },
   methods: {
     rndStr: function rndStr(len) {
       var text = "";
@@ -2080,6 +2231,16 @@ __webpack_require__.r(__webpack_exports__);
       this.data.credpass = this.rndStr(16);
     },
     updateCredential: function updateCredential() {
+      console.log(this.creddesc);
+
+      if (this.creddesc.id === "custom") {
+        this.data.credtype = 1;
+        this.data.creddesc = this.creddesc.name;
+      } else {
+        this.data.credtype = this.creddesc.id;
+        this.data.creddesc = this.creddesc.name;
+      }
+
       axios.put('/api/credentials/' + this.data.credid, this.data).then(function (response) {})["catch"](function (error) {
         console.error(error);
       });
@@ -2131,12 +2292,21 @@ __webpack_require__.r(__webpack_exports__);
 //
 //
 /* harmony default export */ __webpack_exports__["default"] = ({
-  props: ['credential'],
+  props: ['credential', 'descriptions'],
   data: function data() {
     return {
       data: JSON.parse(this.credential)
     };
   },
+  computed: {
+    creddescList: function creddescList() {
+      var list = {};
+      JSON.parse(this.descriptions).map(function (val) {
+        list[val.creddescid] = val.credtitle;
+      });
+      return list;
+    }
+  },
   mounted: function mounted() {
     var _this = this;
 
@@ -2296,6 +2466,7 @@ __webpack_require__.r(__webpack_exports__);
 //
 //
 //
+//
 /* harmony default export */ __webpack_exports__["default"] = ({
   props: ['populateWith', 'storeList', 'modalId'],
   data: function data() {
@@ -2304,15 +2475,22 @@ __webpack_require__.r(__webpack_exports__);
       // from the parent component. Parent component will be updated via
       // Websocket if updateWorkOrder is successful.
       data: JSON.parse(JSON.stringify(this.populateWith)),
-      storeArray: this.storeList,
       id: this.modalId,
+      store: {},
       errors: []
     };
   },
+  mounted: function mounted() {
+    this.store = {
+      'id': this.data.storeid,
+      'name': this.storeList[this.data.storeid]
+    };
+  },
   methods: {
     updateWorkOrder: function updateWorkOrder() {
       var _this = this;
 
+      this.data.storeid = this.store.id;
       axios.put('/api/workorders/' + this.data.woid, this.data).then(function (response) {
         $('#workordereditModal').modal('hide');
       })["catch"](function (error) {
@@ -2357,27 +2535,27 @@ __webpack_require__.r(__webpack_exports__);
 
 /* harmony default export */ __webpack_exports__["default"] = ({
   mixins: [_mixins_dateMixin__WEBPACK_IMPORTED_MODULE_0__["default"]],
-  props: ['workOrder'],
+  props: ['workOrder', 'stores'],
   data: function data() {
     return {
-      data: JSON.parse(this.workOrder),
-      storeList: null
+      data: JSON.parse(this.workOrder)
     };
   },
+  computed: {
+    storeList: function storeList() {
+      var list = {};
+      JSON.parse(this.stores).map(function (val) {
+        list[val.storeid] = val.storesname;
+      });
+      return list;
+    }
+  },
   mounted: function mounted() {
     var _this = this;
 
     Echo.channel('work-orders').listen('WorkOrderUpdated', function (e) {
       _this.data = JSON.parse(e.data);
     });
-    axios.get('/api/stores').then(function (response) {
-      _this.storeList = {};
-      response.data.map(function (val) {
-        _this.storeList[val.storeid] = val;
-      });
-    })["catch"](function (error) {
-      console.error(error);
-    });
   }
 });
 
@@ -48938,6 +49116,102 @@ render._withStripped = true
 
 
 
+/***/ }),
+
+/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a&":
+/*!*******************************************************************************************************************************************************************************************************************************!*\
+  !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a& ***!
+  \*******************************************************************************************************************************************************************************************************************************/
+/*! exports provided: render, staticRenderFns */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "render", function() { return render; });
+/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "staticRenderFns", function() { return staticRenderFns; });
+var render = function() {
+  var _vm = this
+  var _h = _vm.$createElement
+  var _c = _vm._self._c || _h
+  return _c("div", { staticClass: "dropdown", class: { open: _vm.open } }, [
+    _c("div", { staticClass: "input-group" }, [
+      _c("input", {
+        directives: [
+          {
+            name: "model",
+            rawName: "v-model",
+            value: _vm.searchText,
+            expression: "searchText"
+          }
+        ],
+        staticClass: "form-control",
+        attrs: { type: "text" },
+        domProps: { value: _vm.searchText },
+        on: {
+          input: [
+            function($event) {
+              if ($event.target.composing) {
+                return
+              }
+              _vm.searchText = $event.target.value
+            },
+            _vm.searchChanged
+          ]
+        }
+      }),
+      _vm._v(" "),
+      _c("div", { staticClass: "form-group-append" }, [
+        _c(
+          "a",
+          {
+            staticClass: "toggle input-group-text bg-primary text-light",
+            on: {
+              mousedown: function($event) {
+                $event.preventDefault()
+              },
+              click: function($event) {
+                return _vm.setOpen(!_vm.open)
+              }
+            }
+          },
+          [
+            _c("span", { staticClass: "arrow-up" }, [_vm._v("▲")]),
+            _vm._v(" "),
+            _c("span", { staticClass: "arrow-down" }, [_vm._v("▼")])
+          ]
+        )
+      ])
+    ]),
+    _vm._v(" "),
+    _c(
+      "ul",
+      { staticClass: "suggestion-list list-unstyled" },
+      _vm._l(_vm.matches, function(suggestion, index) {
+        return _c(
+          "li",
+          {
+            key: index,
+            on: {
+              mousedown: function($event) {
+                $event.preventDefault()
+              },
+              click: function($event) {
+                return _vm.suggestionSelected(suggestion)
+              }
+            }
+          },
+          [_vm._v("\n            " + _vm._s(suggestion[1]) + "\n        ")]
+        )
+      }),
+      0
+    )
+  ])
+}
+var staticRenderFns = []
+render._withStripped = true
+
+
+
 /***/ }),
 
 /***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./resources/js/components/credential-form-modal.vue?vue&type=template&id=5ba19d42&":
@@ -48977,6 +49251,28 @@ var render = function() {
       ),
       _vm._v(" "),
       _c("div", { attrs: { slot: "body" }, slot: "body" }, [
+        _c(
+          "div",
+          { staticClass: "form" },
+          [
+            _c("autocomplete-custom-dropdown", {
+              attrs: {
+                options: _vm.descriptions,
+                value: _vm.creddesc,
+                "allow-custom": true
+              },
+              model: {
+                value: _vm.creddesc,
+                callback: function($$v) {
+                  _vm.creddesc = $$v
+                },
+                expression: "creddesc"
+              }
+            })
+          ],
+          1
+        ),
+        _vm._v(" "),
         _c("div", { staticClass: "form-group" }, [
           _c("label", { attrs: { for: "newusername" } }, [_vm._v("Username")]),
           _vm._v(" "),
@@ -49109,6 +49405,7 @@ var render = function() {
     [
       _c("credential-form-modal", {
         attrs: {
+          descriptions: _vm.creddescList,
           "modal-id": "credential" + this.data.credid + "editModal",
           populateWith: this.data
         }
@@ -49640,60 +49937,25 @@ var render = function() {
           1
         ),
         _vm._v(" "),
-        _c("div", { staticClass: "form-group" }, [
-          _c("label", { attrs: { for: "storelist" } }, [_vm._v("Store")]),
-          _vm._v(" "),
-          _vm.storeList
-            ? _c(
-                "select",
-                {
-                  directives: [
-                    {
-                      name: "model",
-                      rawName: "v-model",
-                      value: _vm.data.storeid,
-                      expression: "data.storeid"
-                    }
-                  ],
-                  staticClass: "form-control",
-                  attrs: { name: "storelist", id: "storelist" },
-                  on: {
-                    change: function($event) {
-                      var $$selectedVal = Array.prototype.filter
-                        .call($event.target.options, function(o) {
-                          return o.selected
-                        })
-                        .map(function(o) {
-                          var val = "_value" in o ? o._value : o.value
-                          return val
-                        })
-                      _vm.$set(
-                        _vm.data,
-                        "storeid",
-                        $event.target.multiple
-                          ? $$selectedVal
-                          : $$selectedVal[0]
-                      )
-                    }
-                  }
+        _c(
+          "div",
+          { staticClass: "form-group" },
+          [
+            _c("label", { attrs: { for: "storelist" } }, [_vm._v("Store")]),
+            _vm._v(" "),
+            _c("autocomplete-custom-dropdown", {
+              attrs: { options: _vm.storeList, value: this.store },
+              model: {
+                value: _vm.store,
+                callback: function($$v) {
+                  _vm.store = $$v
                 },
-                _vm._l(_vm.storeList, function(store) {
-                  return _c(
-                    "option",
-                    { key: store.storeid, domProps: { value: store.storeid } },
-                    [
-                      _vm._v(
-                        "\r\n                    " +
-                          _vm._s(store.storesname) +
-                          "\r\n                "
-                      )
-                    ]
-                  )
-                }),
-                0
-              )
-            : _vm._e()
-        ])
+                expression: "store"
+              }
+            })
+          ],
+          1
+        )
       ]),
       _vm._v(" "),
       _c("div", { attrs: { slot: "footer" }, slot: "footer" }, [
@@ -49777,11 +50039,7 @@ var render = function() {
       _c("p", [
         _c("i", { staticClass: "fas fa-fw fa-building" }),
         _vm._v(" "),
-        this.storeList
-          ? _c("span", [
-              _vm._v(_vm._s(this.storeList[this.data.storeid].storesname))
-            ])
-          : _vm._e()
+        _c("span", [_vm._v(_vm._s(_vm.storeList[_vm.data.storeid]))])
       ]),
       _vm._v(" "),
       _c("p", [
@@ -62052,6 +62310,7 @@ Vue.component('modal', __webpack_require__(/*! ./components/modal.vue */ "./reso
 Vue.component('woinfo-edit-modal', __webpack_require__(/*! ./components/woinfo-edit-modal.vue */ "./resources/js/components/woinfo-edit-modal.vue")["default"]);
 Vue.component('errorlist', __webpack_require__(/*! ./components/errorlist.vue */ "./resources/js/components/errorlist.vue")["default"]);
 Vue.component('credential-form-modal', __webpack_require__(/*! ./components/credential-form-modal.vue */ "./resources/js/components/credential-form-modal.vue")["default"]);
+Vue.component('autocomplete-custom-dropdown', __webpack_require__(/*! ./components/autocomplete-custom-dropdown.vue */ "./resources/js/components/autocomplete-custom-dropdown.vue")["default"]);
 /**
  * Next, we will create a fresh Vue application instance and attach it to
  * the page. Then, you may begin adding components to this application
@@ -62261,6 +62520,75 @@ __webpack_require__.r(__webpack_exports__);
 
 
 
+/***/ }),
+
+/***/ "./resources/js/components/autocomplete-custom-dropdown.vue":
+/*!******************************************************************!*\
+  !*** ./resources/js/components/autocomplete-custom-dropdown.vue ***!
+  \******************************************************************/
+/*! exports provided: default */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _autocomplete_custom_dropdown_vue_vue_type_template_id_18875e5a___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a& */ "./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a&");
+/* harmony import */ var _autocomplete_custom_dropdown_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./autocomplete-custom-dropdown.vue?vue&type=script&lang=js& */ "./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=script&lang=js&");
+/* empty/unused harmony star reexport *//* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ "./node_modules/vue-loader/lib/runtime/componentNormalizer.js");
+
+
+
+
+
+/* normalize component */
+
+var component = Object(_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__["default"])(
+  _autocomplete_custom_dropdown_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__["default"],
+  _autocomplete_custom_dropdown_vue_vue_type_template_id_18875e5a___WEBPACK_IMPORTED_MODULE_0__["render"],
+  _autocomplete_custom_dropdown_vue_vue_type_template_id_18875e5a___WEBPACK_IMPORTED_MODULE_0__["staticRenderFns"],
+  false,
+  null,
+  null,
+  null
+  
+)
+
+/* hot reload */
+if (false) { var api; }
+component.options.__file = "resources/js/components/autocomplete-custom-dropdown.vue"
+/* harmony default export */ __webpack_exports__["default"] = (component.exports);
+
+/***/ }),
+
+/***/ "./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=script&lang=js&":
+/*!*******************************************************************************************!*\
+  !*** ./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=script&lang=js& ***!
+  \*******************************************************************************************/
+/*! exports provided: default */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _node_modules_babel_loader_lib_index_js_ref_4_0_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_custom_dropdown_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib??ref--4-0!../../../node_modules/vue-loader/lib??vue-loader-options!./autocomplete-custom-dropdown.vue?vue&type=script&lang=js& */ "./node_modules/babel-loader/lib/index.js?!./node_modules/vue-loader/lib/index.js?!./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=script&lang=js&");
+/* empty/unused harmony star reexport */ /* harmony default export */ __webpack_exports__["default"] = (_node_modules_babel_loader_lib_index_js_ref_4_0_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_custom_dropdown_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__["default"]); 
+
+/***/ }),
+
+/***/ "./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a&":
+/*!*************************************************************************************************!*\
+  !*** ./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a& ***!
+  \*************************************************************************************************/
+/*! exports provided: render, staticRenderFns */
+/***/ (function(module, __webpack_exports__, __webpack_require__) {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_custom_dropdown_vue_vue_type_template_id_18875e5a___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../node_modules/vue-loader/lib??vue-loader-options!./autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a& */ "./node_modules/vue-loader/lib/loaders/templateLoader.js?!./node_modules/vue-loader/lib/index.js?!./resources/js/components/autocomplete-custom-dropdown.vue?vue&type=template&id=18875e5a&");
+/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "render", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_custom_dropdown_vue_vue_type_template_id_18875e5a___WEBPACK_IMPORTED_MODULE_0__["render"]; });
+
+/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "staticRenderFns", function() { return _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_autocomplete_custom_dropdown_vue_vue_type_template_id_18875e5a___WEBPACK_IMPORTED_MODULE_0__["staticRenderFns"]; });
+
+
+
 /***/ }),
 
 /***/ "./resources/js/components/credential-form-modal.vue":

+ 1 - 0
resources/js/app.js

@@ -31,6 +31,7 @@ Vue.component('modal', require('./components/modal.vue').default);
 Vue.component('woinfo-edit-modal', require('./components/woinfo-edit-modal.vue').default);
 Vue.component('errorlist', require('./components/errorlist.vue').default);
 Vue.component('credential-form-modal', require('./components/credential-form-modal.vue').default);
+Vue.component('autocomplete-custom-dropdown', require('./components/autocomplete-custom-dropdown.vue').default);
 
 /**
  * Next, we will create a fresh Vue application instance and attach it to

+ 120 - 0
resources/js/components/autocomplete-custom-dropdown.vue

@@ -0,0 +1,120 @@
+<template>
+    <div class="dropdown" :class="{'open': open}">
+        <div class="input-group">
+            <input type="text" class="form-control" v-model="searchText" @input="searchChanged">
+            <div class="form-group-append">
+                <a class="toggle input-group-text bg-primary text-light" @mousedown.prevent @click="setOpen(!open)">
+                    <span class="arrow-up">▲</span>
+                    <span class="arrow-down">▼</span>
+                </a>
+            </div>
+        </div>
+        <ul class="suggestion-list list-unstyled">
+            <li v-for="(suggestion,index) in matches" :key="index"
+                @mousedown.prevent
+                @click="suggestionSelected(suggestion)"
+            >
+                {{ suggestion[1] }}
+            </li>
+        </ul>
+    </div>
+</template>
+<script>
+export default {
+    props: {
+        value: null,
+        // An object with the following format
+        // {
+        //     key: value,
+        //     key: value,
+        // }
+        options: {
+            type: Object,
+            required: true
+        },
+        allowCustom: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data () {
+        return {
+            searchText: '',
+            selectedOption: null,
+            open: false,
+            // allows for checking whether button was clicked or text was typed.
+            // If button was clicked, we show all options without having to clear
+            // out any searchText that was autofilled when page loaded.
+            isSearching: false
+        }
+    },
+    computed: {
+        matches () {
+            // If button was clicked, show all options, if typing, filter
+            // options based on searchText
+            return Object.entries(this.options).filter((option) => {
+                var optionText = option[1].toUpperCase()
+                if (this.isSearching) {
+                    return optionText.match(this.searchText.toUpperCase())
+                }
+                else {
+                    return optionText.match('')
+                }
+            })
+        }
+    },
+    methods: {
+        // if button is clicked, show all options
+        setOpen (isOpen) {
+            this.open = isOpen
+            this.isSearching = false
+        },
+        // if typing in the box, start filtering options
+        searchChanged () {
+            if (!this.open) {
+                this.open = true
+            }
+            if (!this.isSearching) {
+                this.isSearching = true
+            }
+
+            // If custom values are allowed, emits {id: 'custom', name: searchtext}
+            if (this.allowCustom) {
+                this.$emit('input', {'id': 'custom', 'name': this.searchText})
+            }
+        },
+        // emits { id: suggestion_id, name: suggestion_name} to v-model when 
+        // a suggestion is selected
+        suggestionSelected (suggestion) {
+            this.open = false
+            this.searchText = suggestion[1]
+            this.$emit('input', { 'id': suggestion[0], 'name': suggestion[1]})
+        },
+        updateComponentWithValue(newValue) {
+            if (this.allowCustom) {
+                this.searchText = this.value.name
+            }
+            else {
+                if (newValue.id > -1) {
+                    // Find the matching text for the supplied option value
+                    for (var id in this.options) {
+                        if (this.options.hasOwnProperty(id)) {
+                            if (this.options[id] === this.options[newValue.id]) {
+                                this.searchText = this.options[id]
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    },
+    mounted () {
+        this.updateComponentWithValue(this.value)
+    },
+    watch: {
+        value: function (newValue) {
+            this.updateComponentWithValue(newValue)
+        }
+    }
+}
+</script>

+ 23 - 1
resources/js/components/credential-form-modal.vue

@@ -4,6 +4,9 @@
             Edit Credential
         </h5>
         <div slot="body">
+            <div class="form">
+                <autocomplete-custom-dropdown :options="descriptions" :value="creddesc" v-model="creddesc" :allow-custom="true"></autocomplete-custom-dropdown>
+            </div>
             <div class="form-group">
                 <label for="newusername">Username</label>
                 <input type="text" :name="'newusername'+data.credid" :id="'newusername'+data.credid" class="form-control credential" v-model="data.creduser">
@@ -23,14 +26,24 @@
 </template>
 <script>
 export default {
-    props: ['populateWith', 'modalId'],
+    props: ['populateWith', 'modalId', 'descriptions'],
     data() {
         return {
             data: JSON.parse(JSON.stringify(this.populateWith)),
+            creddesc: {},
             id: this.modalId,
             errors: []
         }
     },
+    mounted () {
+        // Definitely needs to be refactored into something that runs once
+        // per page instead of once per credential. Only putting this here now
+        // to test something else.
+        this.creddesc = {
+            'id': this.data.credtype,
+            'name': this.data.creddesc
+        }
+    },
     methods: {
         rndStr(len) {
             let text = ""
@@ -45,6 +58,15 @@ export default {
             this.data.credpass = this.rndStr(16);
         },
         updateCredential() {
+            console.log(this.creddesc)
+            if (this.creddesc.id === "custom") {
+                this.data.credtype = 1
+                this.data.creddesc = this.creddesc.name
+            }
+            else {
+                this.data.credtype = this.creddesc.id
+                this.data.creddesc = this.creddesc.name
+            }
             axios.put('/api/credentials/' + this.data.credid, this.data).then(response => {}).catch(error =>{console.error(error)});
             $('#credential'+this.data.credid+'editModal').modal('hide');
         }

+ 11 - 2
resources/js/components/credential.vue

@@ -1,6 +1,6 @@
 <template>
 <div class="bg-lightgray m-1 p-2 border rounded container-fluid">
-    <credential-form-modal :modal-id="'credential'+this.data.credid+'editModal'" :populateWith="this.data"></credential-form-modal>
+    <credential-form-modal :descriptions="creddescList" :modal-id="'credential'+this.data.credid+'editModal'" :populateWith="this.data"></credential-form-modal>
     <div class="row no-gutters">
         <div class="h5 col-3 text-left">{{this.data.creddesc}}</div>
         <div class="col-9 text-right">{{this.data.creddate}}</div>
@@ -31,12 +31,21 @@
 </template>
 <script>
 export default {
-    props: ['credential'],
+    props: ['credential', 'descriptions'],
     data() {
         return {
             data: JSON.parse(this.credential),
         }
     },
+    computed : {
+        creddescList: function () {
+            let list = {}
+            JSON.parse(this.descriptions).map(val => {
+                list[val.creddescid] = val.credtitle
+            })
+            return list
+        }
+    },
     mounted() {
         Echo.channel('credentials')
             .listen('CredentialUpdated', (e) => {

+ 11 - 3
resources/js/components/woinfo-edit-modal.vue

@@ -16,11 +16,12 @@
         </div>
         <div class="form-group">
             <label for="storelist">Store</label>
-            <select name="storelist" id="storelist" class="form-control" v-if="storeList" v-model="data.storeid">
+            <autocomplete-custom-dropdown :options="storeList" :value="this.store" v-model="store"></autocomplete-custom-dropdown>
+            <!-- <select name="storelist" id="storelist" class="form-control" v-if="storeList" v-model="data.storeid">
                 <option v-for="store in storeList" v-bind:key="store.storeid" v-bind:value="store.storeid">
                     {{ store.storesname }}
                 </option>
-            </select>
+            </select> -->
         </div>
     </div>
 
@@ -39,13 +40,20 @@ export default {
             // from the parent component. Parent component will be updated via
             // Websocket if updateWorkOrder is successful.
             data: JSON.parse(JSON.stringify(this.populateWith)),
-            storeArray: this.storeList,
             id: this.modalId,
+            store: {},
             errors: []
         }
     },
+    mounted () {
+        this.store = {
+            'id': this.data.storeid,
+            'name': this.storeList[this.data.storeid]
+        }
+    },
     methods: {
         updateWorkOrder() {
+            this.data.storeid = this.store.id
             axios.put('/api/workorders/' + this.data.woid, this.data)
                 .then((response) => { $('#workordereditModal').modal('hide'); })
                 .catch((error) => { this.errors = JSON.parse(error.response.request.response).errors; });

+ 11 - 9
resources/js/components/woinfo.vue

@@ -4,7 +4,7 @@
         <p><i class="fas fa-fw fa-info-circle"></i> <span v-text="this.data.probdesc"></span></p>
         <p><i class="far fa-fw fa-lightbulb"></i> <span v-text="this.data.suggested"></span></p>
         <p><i class="fas fa-fw fa-paste"></i> <span v-text="this.data.woid"></span></p>
-        <p><i class="fas fa-fw fa-building"></i> <span v-if="this.storeList">{{ this.storeList[this.data.storeid].storesname }}</span></p>
+        <p><i class="fas fa-fw fa-building"></i> <span>{{ storeList[data.storeid] }}</span></p>
         <p><i class="fas fa-fw fa-sign-in-alt"></i> 
             <span class="dashed-underline" data-toggle="tooltip" data-placement="bottom" v-bind:title=this.getHRDate(this.data.dropdate)>
                 {{ Math.floor(this.daysSinceToday(this.data.dropdate)) }} days ago
@@ -22,11 +22,19 @@
     import dateMixin from '../mixins/dateMixin'
     export default {
         mixins: [dateMixin],
-        props: ['workOrder'],
+        props: ['workOrder', 'stores'],
         data() {
             return {
                 data: JSON.parse(this.workOrder),
-                storeList: null,
+            }
+        },
+        computed: {
+            storeList: function () {
+                let list = {}
+                JSON.parse(this.stores).map(val => {
+                    list[val.storeid] = val.storesname
+                })
+                return list
             }
         },
         mounted() {
@@ -34,12 +42,6 @@
                 .listen('WorkOrderUpdated', (e) => {
                     this.data = JSON.parse(e.data);
                 });
-            axios.get('/api/stores').then((response) => {
-                    this.storeList = {};
-                    response.data.map(val => { 
-                        this.storeList[val.storeid] = val;
-                    });
-                }).catch((error) => { console.error(error) });
         }
     }
 </script>

+ 44 - 0
resources/sass/app.scss

@@ -69,3 +69,47 @@
 .modal-header {
     display: block;
 }
+
+
+/* For autocomplete-custom-dropdown Vue component */
+.dropdown.open .suggestion-list {
+    display: block;
+}
+
+.dropdown .suggestion-list {
+    display: none;
+}
+
+.toggle .arrow-up {
+    display: none;
+}
+
+.open .toggle .arrow-up {
+    display: inline-block;
+}
+
+.open .toggle .arrow-down {
+    display: none;
+}
+
+.suggestion-list li {
+    cursor: pointer;
+}
+
+.suggestion-list li:hover {
+    color: #fff;
+    background-color: $primary
+}
+
+.suggestion-list {
+    background-color: rgba(255, 255, 255, 0.95);
+    border: 1px solid #ddd;
+    list-style: none;
+    display: block;
+    margin: 0;
+    padding: 0.3rem;
+    width: 100%;
+    overflow: hidden;
+    position: relative;
+    z-index: 2;
+}

+ 2 - 2
resources/views/workorders/show.blade.php

@@ -40,7 +40,7 @@
                         @endif
                         <div class="tab-pane" id="credentials" role="tabpanel" aria-labelledby="credentials-tab">
                             @foreach ($workOrder->asset->credentials as $cred)
-                                <credential credential="{{$cred}}"></credential>
+                                <credential credential="{{$cred}}" descriptions="{{App\CredDesc::all()}}"></credential>
                             @endforeach
                         </div>
                     </div>
@@ -68,7 +68,7 @@
                 <div class='card-body'>
                     <div class='tab-content'>
                         <div class="tab-pane active" id="workordersumm" role="tabpanel" aria-labelledby="workordersumm-tab">
-                            <woinfo work-order="{{$workOrder}}"></woinfo>
+                            <woinfo work-order="{{$workOrder}}" stores="{{App\Store::all()}}"></woinfo>
                         </div>
                         <div class="tab-pane" id="attachments" role="tabpanel" aria-labelledby="attachments-tab">
                             TODO 2

+ 2 - 0
routes/api.php

@@ -28,4 +28,6 @@ Route::middleware('auth:api')->group( function() {
 
     Route::put('/credentials/{credential}', 'Api\CredentialsController@update');
 
+    Route::get('credtypes', 'Api\CredDescController@index');
+
 });