diff --git a/.circleci/config.yml b/.circleci/config.yml index 7f58b95ab..5917a3e17 100755 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -108,7 +108,6 @@ jobs: name: Upload coverage results to Code Climate command: | yarn run codeclimate-test-reporter < coverage*/lcov.info - www_build: <<: *defaults environment: @@ -195,7 +194,6 @@ jobs: exit 1 esac curl https://api.rollbar.com/api/1/sourcemap/download -F access_token="${ROLLBAR_KEY}" -F version="${APP_VERSION}" -F minified_url=$ASSET_HOST_URL/$SOURCE_MAP - ember_cordova_build: <<: *defaults environment: diff --git a/Gemfile.lock b/Gemfile.lock index 5db66f0dd..d2284aaa7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,17 +15,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.1.0) - aws-partitions (1.417.0) - aws-sdk-core (3.111.2) + aws-partitions (1.429.0) + aws-sdk-core (3.112.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.41.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-kms (1.42.0) + aws-sdk-core (~> 3, >= 3.112.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.87.0) - aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-s3 (1.89.0) + aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) aws-sigv4 (1.2.2) @@ -83,9 +83,9 @@ GEM domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - emoji_regex (3.2.1) + emoji_regex (3.2.2) escape (0.0.4) - excon (0.78.1) + excon (0.79.0) faraday (1.3.0) faraday-net_http (~> 1.0) multipart-post (>= 1.2, < 3) @@ -96,8 +96,8 @@ GEM faraday-net_http (1.0.1) faraday_middleware (1.0.0) faraday (~> 1.0) - fastimage (2.2.1) - fastlane (2.172.0) + fastimage (2.2.3) + fastlane (2.176.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) artifactory (~> 3.0) @@ -121,6 +121,7 @@ GEM jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (~> 2.0.0) + naturally (~> 2.2) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -145,7 +146,7 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.0) signet (~> 0.12) - google-apis-core (0.2.0) + google-apis-core (0.2.1) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.14) httpclient (>= 2.8.1, < 3.0) @@ -154,9 +155,10 @@ GEM retriable (>= 2.0, < 4.0) rexml signet (~> 0.14) + webrick google-apis-iamcredentials_v1 (0.1.0) google-apis-core (~> 0.1) - google-apis-storage_v1 (0.1.0) + google-apis-storage_v1 (0.2.0) google-apis-core (~> 0.1) google-cloud-core (1.5.0) google-cloud-env (~> 1.0) @@ -172,7 +174,7 @@ GEM google-cloud-core (~> 1.2) googleauth (~> 0.9) mini_mime (~> 1.0) - googleauth (0.14.0) + googleauth (0.15.1) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -220,7 +222,7 @@ GEM ruby2_keywords (0.0.4) rubyzip (2.3.0) security (0.1.3) - signet (0.14.0) + signet (0.14.1) addressable (~> 2.3) faraday (>= 0.17.3, < 2.0) jwt (>= 1.5, < 3.0) @@ -247,6 +249,7 @@ GEM unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) + webrick (1.7.0) word_wrap (1.0.0) xcodeproj (1.19.0) CFPropertyList (>= 2.3.3, < 4.0) diff --git a/Rakefile b/Rakefile index b3627f559..e7b52e926 100755 --- a/Rakefile +++ b/Rakefile @@ -104,7 +104,7 @@ namespace :cordova do if platform == 'android' add_plugin('phonegap-plugin-push', '2.1.2') elsif platform == 'ios' - add_plugin('phonegap-plugin-push', '1.9.2', { SENDER_ID: 'XXXXXXX' }) + add_plugin('phonegap-plugin-push', '2.3.0', { SENDER_ID: 'XXXXXXX' }) end log("Preparing app for #{platform}") diff --git a/app/components/camera-detect.js b/app/components/camera-detect.js new file mode 100644 index 000000000..8928f8236 --- /dev/null +++ b/app/components/camera-detect.js @@ -0,0 +1,93 @@ +import Ember from "ember"; +import AjaxPromise from "stock/utils/ajax-promise"; +import AsyncMixin, { ERROR_STRATEGIES } from "stock/mixins/async"; + +export default Ember.Component.extend(AsyncMixin, { + packageService: Ember.inject.service(), + store: Ember.inject.service(), + dataUri: null, + + videoStream: "", + useFrontCamera: true, + + constraints: { + video: { + width: { + min: 1280, + ideal: 1920, + max: 2560 + }, + height: { + min: 720, + ideal: 1080, + max: 1440 + } + } + }, + + async initializeCamera() { + this.stopVideoStream(); + const cameraType = this.get("useFrontCamera") ? "user" : "environment"; + this.set("constraints.video.facingMode", cameraType); + + const vStream = await navigator.mediaDevices.getUserMedia( + this.get("constraints") + ); + this.set("videoStream", vStream); + this.set("video.srcObject", this.get("videoStream")); + }, + + stopVideoStream() { + const videoStream = this.get("videoStream"); + if (videoStream) { + videoStream.getTracks().forEach(track => { + track.stop(); + }); + } + }, + + displayWebcam: Ember.computed.alias("packageService.openImageOverlay"), + + didRender() { + if (this.get("packageService.openImageOverlay")) { + let video = this.element.children[0]; + video.play(); + this.set("video", video); + this.initializeCamera(); + } + }, + + actions: { + snap() { + const canvas = document.getElementById("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext("2d").drawImage(video, 0, 0); + canvas.toDataURL("image/png"); + this.send("didSnap", canvas.toDataURL("image/png")); + }, + + changeCamera() { + this.toggleProperty("useFrontCamera"); + this.initializeCamera(); + }, + + async didSnap(dataUri) { + this.set("packageService.openImageOverlay", false); + this.runTask(async () => { + const signature = await this.get("packageService").generateSignature(); + signature.file = dataUri; + const image = await this.get("packageService").uploadToCloudinary( + signature + ); + this.get("getImageCallback")(image); + this.stopVideoStream(); + }); + }, + + closeOverlay() { + this.stopVideoStream(); + this.set("packageService.openImageOverlay", false); + } + } +}); diff --git a/app/components/order-inline-number-input.js b/app/components/order-inline-number-input.js deleted file mode 100644 index 05d95c812..000000000 --- a/app/components/order-inline-number-input.js +++ /dev/null @@ -1,48 +0,0 @@ -import Ember from 'ember'; -import AjaxPromise from 'stock/utils/ajax-promise'; -import BeneficiaryInlineInput from './beneficiary-inline-input'; -const { getOwner } = Ember; - -export default BeneficiaryInlineInput.extend({ - type: "tel", - orderCopy: Ember.computed.readOnly('order'), - - whichKey(e, key) { - var keyList = [13, 8, 9, 39, 46, 32]; - return e.ctrlKey && key === 86 || keyList.indexOf(key) >= 0 || key >= 48 && key <= 57; - }, - - didInsertElement() { - Ember.$('.people-helped').val(this.get('orderCopy.data.peopleHelped')); - }, - - focusOut() { - var value = this.attrs.value.value || ""; - var order = this.get("order"); - var url = `/orders/${order.get('id')}`; - var key = this.get('name'); - var orderParams = {}; - var element = this.element; - - if(value.length === 0 || value === '0') { - this.set('value', ""); - Ember.$(element).siblings().show(); - return false; - } - orderParams[key] = value; - - Ember.$(this.element).removeClass('inline-text-input'); - if (value !== this.get('previousValue')){ - var loadingView = getOwner(this).lookup('component:loading').append(); - new AjaxPromise(url, "PUT", this.get('session.authToken'), { order: orderParams }) - .then(data => { - this.get("store").pushPayload(data); - Ember.$(element).siblings().hide(); - }) - .finally(() => { - loadingView.destroy(); - }); - } - Ember.$(this.element).removeClass('inline-text-input'); - } -}); diff --git a/app/components/order-inline-textarea-input.js b/app/components/order-inline-textarea-input.js deleted file mode 100644 index efc90dfd2..000000000 --- a/app/components/order-inline-textarea-input.js +++ /dev/null @@ -1,85 +0,0 @@ -import Ember from "ember"; -import AutoResizableTextarea from "./auto-resize-textarea"; -import AjaxPromise from "stock/utils/ajax-promise"; -const { getOwner } = Ember; - -export default AutoResizableTextarea.extend({ - previousValue: "", - store: Ember.inject.service(), - order: null, - orderCopy: Ember.computed.readOnly("order"), - - keyDown() { - var value = this.element.value; - if (value.charCodeAt(value.length - 1) === 10 && event.which === 13) { - return false; - } - }, - - didInsertElement() { - Ember.$(".description-textarea").val( - this.get("orderCopy.data.purposeDescription") - ); - }, - - focusOut() { - if (this.get("disableCallbacks")) return; - var order = this.get("order"); - var url = `/orders/${order.get("id")}`; - var key = this.get("name"); - var value = this.attrs.value.value || ""; - var orderParams = {}; - orderParams[key] = this.get("value").trim() || ""; - var element = this.element; - - if ( - orderParams[key].toString() !== - this.get("previousValue") - .toString() - .trim() && - value !== "" - ) { - var loadingView = getOwner(this) - .lookup("component:loading") - .append(); - new AjaxPromise(url, "PUT", this.get("session.authToken"), { - order: orderParams - }) - .then(data => { - this.get("store").pushPayload(data); - Ember.$(element) - .siblings() - .hide(); - }) - .finally(() => { - loadingView.destroy(); - }); - } - this.element.value = value.trim(); - if (this.element.value === "") { - this.$().focus(); - Ember.$(this.element) - .siblings() - .show(); - return false; - } - Ember.$(this.element).removeClass("item-description-textarea"); - }, - - focusIn() { - this.addCssStyle(); - }, - - addCssStyle() { - Ember.$(this.element).addClass("item-description-textarea"); - this.set("previousValue", this.get("value") || ""); - }, - - click() { - this.addCssStyle(); - }, - - willDestroyElement() { - this.set("disableCallbacks", true); - } -}); diff --git a/app/controllers/items/detail.js b/app/controllers/items/detail.js index ea95c9c6a..67c2009f2 100644 --- a/app/controllers/items/detail.js +++ b/app/controllers/items/detail.js @@ -178,35 +178,6 @@ export default GoodcityController.extend( } }), - canApplyDefaultValuation: Ember.computed( - "valueHkDollar", - "valuationIsFocused", - function() { - const valueHkDollar = +this.get("valueHkDollar"); - const defaultValue = +this.get("defaultValueHkDollar"); - return this.get("valuationIsFocused") && valueHkDollar !== defaultValue; - } - ), - - /** - * Returns true if valueHkDollar is modified and not empty - * and its value is different from previous saved value - */ - canUpdateValuation: Ember.computed( - "model.valueHkDollar", - "prevValueHkDollar", - function() { - const valueHkDollar = this.get("valueHkDollar"); - const prevValueHkDollar = parseFloat(this.get("prevValueHkDollar")); - const defaultValue = this.get("defaultValueHkDollar"); - if (!prevValueHkDollar) { - return Math.abs(valueHkDollar - defaultValue); - } else { - return Math.abs(valueHkDollar - prevValueHkDollar); - } - } - ), - allowPublish: Ember.computed( "model.isSingletonItem", "model.availableQuantity", @@ -464,10 +435,6 @@ export default GoodcityController.extend( this.updatePackageOffers(offerIds); }, - setValuationIsFocused(val) { - this.set("valuationIsFocused", val); - }, - onGradeChange({ id, name }) { this.set("selectedGrade", { id, name }); this.set("defaultValueHkDollar", null); @@ -670,7 +637,6 @@ export default GoodcityController.extend( const item = this.get("item"); item.set("valueHkDollar", this.get("defaultValueHkDollar")); this.set("valueHkDollar", this.get("defaultValueHkDollar")); - this.set("prevValueHkDollar", null); await this.saveItem(item); }, @@ -693,18 +659,6 @@ export default GoodcityController.extend( this.send("updateAttribute", name, description); }, - /** - * Updates the valueHkDollar - * Updates the previous saved value - */ - async updateItemValuation() { - const item = this.get("item"); - const value = item.get("valueHkDollar"); - item.set("valueHkDollar", Number(value)); - await this.saveItem(item); - this.set("prevValueHkDollar", value); - }, - openAddItemOverlay(item) { this.set("openAddItemOverlay", true); this.set("addableItem", item); diff --git a/app/controllers/items/edit_images.js b/app/controllers/items/edit_images.js index 5bff45e4b..d0c774dab 100644 --- a/app/controllers/items/edit_images.js +++ b/app/controllers/items/edit_images.js @@ -1,17 +1,18 @@ import Ember from "ember"; import { translationMacro as t } from "ember-i18n"; -import config from '../../config/environment'; -import AjaxPromise from 'stock/utils/ajax-promise'; +import config from "../../config/environment"; +import AjaxPromise from "stock/utils/ajax-promise"; const { getOwner } = Ember; export default Ember.Controller.extend({ - item: Ember.computed.alias("model"), session: Ember.inject.service(), store: Ember.inject.service(), + packageService: Ember.inject.service(), messageBox: Ember.inject.service(), i18n: Ember.inject.service(), cordova: Ember.inject.service(), + isMobileApp: config.cordova.enabled, itemId: null, noImage: Ember.computed.empty("item.images"), @@ -24,115 +25,155 @@ export default Ember.Controller.extend({ uploadedFileDate: null, initActionSheet: function(onSuccess) { - return window.plugins.actionsheet.show({ - buttonLabels: [this.locale("edit_images.upload").string, this.locale("edit_images.camera").string, this.locale("edit_images.cancel").string] - }, function(buttonIndex) { - if (buttonIndex === 1) { - navigator.camera.getPicture(onSuccess, null, { - quality: 40, - destinationType: navigator.camera.DestinationType.DATA_URL, - sourceType: navigator.camera.PictureSourceType.PHOTOLIBRARY - }); - } - if (buttonIndex === 2) { - navigator.camera.getPicture(onSuccess, null, { - correctOrientation: true, - quality: 40, - destinationType: navigator.camera.DestinationType.DATA_URL, - sourceType: navigator.camera.PictureSourceType.CAMERA - }); - } - if (buttonIndex === 3) { - window.plugins.actionsheet.hide(); + return window.plugins.actionsheet.show( + { + buttonLabels: [ + this.locale("edit_images.upload").string, + this.locale("edit_images.camera").string, + this.locale("edit_images.cancel").string + ] + }, + function(buttonIndex) { + if (buttonIndex === 1) { + navigator.camera.getPicture(onSuccess, null, { + quality: 40, + destinationType: navigator.camera.DestinationType.DATA_URL, + sourceType: navigator.camera.PictureSourceType.PHOTOLIBRARY + }); + } + if (buttonIndex === 2) { + navigator.camera.getPicture(onSuccess, null, { + correctOrientation: true, + quality: 40, + destinationType: navigator.camera.DestinationType.DATA_URL, + sourceType: navigator.camera.PictureSourceType.CAMERA + }); + } + if (buttonIndex === 3) { + window.plugins.actionsheet.hide(); + } } - }); + ); }, - previewMatchesFavourite: Ember.computed("previewImage", "favouriteImage", function(){ - return this.get("previewImage") === this.get("favouriteImage"); - }), + previewMatchesFavourite: Ember.computed( + "previewImage", + "favouriteImage", + function() { + return this.get("previewImage") === this.get("favouriteImage"); + } + ), - images: Ember.computed("item.images.[]", function(){ + images: Ember.computed("item.images.[]", function() { //The reason for sorting is because by default it's ordered by favourite //then id order. If another image is made favourite then deleted the first image //by id order is made favourite which can be second image in list which seems random. //Sort by id ascending except place new images id = 0 at end - return (this.get("item.images") || Ember.A()).toArray().sort(function(a,b) { - if(a && b) { - a = parseInt(a.get("id"), 10); - b = parseInt(b.get("id"), 10); - if (a === 0) { return 1; } - if (b === 0) { return -1; } - return a - b; - } - }); + return (this.get("item.images") || Ember.A()) + .toArray() + .sort(function(a, b) { + if (a && b) { + a = parseInt(a.get("id"), 10); + b = parseInt(b.get("id"), 10); + if (a === 0) { + return 1; + } + if (b === 0) { + return -1; + } + return a - b; + } + }); }), - favouriteImage: Ember.computed("item.images.@each.favourite", function(){ - return this.get("images").filterBy("favourite").get("firstObject"); + favouriteImage: Ember.computed("item.images.@each.favourite", function() { + return this.get("images") + .filterBy("favourite") + .get("firstObject"); }), - initPreviewImage: Ember.on('init', Ember.observer("model", "model.images.[]", function () { - var image = this.get("item.favouriteImage") || this.get("item.images.firstObject"); - if (image) { this.send("setPreview", image); } - })), + initPreviewImage: Ember.on( + "init", + Ember.observer("model", "model.images.[]", function() { + var image = + this.get("item.favouriteImage") || this.get("item.images.firstObject"); + if (image) { + this.send("setPreview", image); + } + }) + ), //css related - previewImageBgCss: Ember.computed("previewImage", "isExpanded", "previewImage.angle", { - - get() { - var css = this.get("instructionBoxCss"); - if (!this.get("previewImage")) { - return css; - } + previewImageBgCss: Ember.computed( + "previewImage", + "isExpanded", + "previewImage.angle", + { + get() { + var css = this.get("instructionBoxCss"); + if (!this.get("previewImage")) { + return css; + } - var imgTag = new Image(); - imgTag.onload = () => { - var newCSS = new Ember.String.htmlSafe( - css + "background-image:url(" + this.get("previewImage.imageUrl") + ");" + - "background-size: " + (this.get("isExpanded") ? "contain" : "cover") + ";" - ); - this.set("previewImageBgCss", newCSS); - }; - imgTag.src = this.get("previewImage.imageUrl"); + var imgTag = new Image(); + imgTag.onload = () => { + var newCSS = new Ember.String.htmlSafe( + css + + "background-image:url(" + + this.get("previewImage.imageUrl") + + ");" + + "background-size: " + + (this.get("isExpanded") ? "contain" : "cover") + + ";" + ); + this.set("previewImageBgCss", newCSS); + }; + imgTag.src = this.get("previewImage.imageUrl"); - return new Ember.String.htmlSafe( - css + "background-image:url('assets/images/image_loading.gif');" + - "background-size: 'inherit';" + return new Ember.String.htmlSafe( + css + + "background-image:url('assets/images/image_loading.gif');" + + "background-size: 'inherit';" ); - }, + }, - set(key, value) { - return value; + set(key, value) { + return value; + } } - }), + ), - instructionBoxCss: Ember.computed("previewImage", "isExpanded", function(){ + instructionBoxCss: Ember.computed("previewImage", "isExpanded", function() { var height = Ember.$(window).height() * 0.6; return new Ember.String.htmlSafe("min-height:" + height + "px;"); }), - thumbImageCss: Ember.computed(function(){ + thumbImageCss: Ember.computed(function() { var imgWidth = Math.min(120, Ember.$(window).width() / 4 - 14); - return new Ember.String.htmlSafe("width:" + imgWidth + "px; height:" + imgWidth + "px;"); + return new Ember.String.htmlSafe( + "width:" + imgWidth + "px; height:" + imgWidth + "px;" + ); }), - locale: function(str){ + locale: function(str) { return this.get("i18n").t(str); }, removeImage: function(controller, item) { var img = item.get("images.firstObject"); - var loadingView = getOwner(controller).lookup('component:loading').append(); + var loadingView = getOwner(controller) + .lookup("component:loading") + .append(); img.deleteRecord(); - img.save() + img + .save() .then(i => { i.unloadRecord(); this.reloadItem(); controller.transitionToRoute("items.edit_images", item); }) - .finally(() => loadingView.destroy()); + .finally(() => loadingView.destroy()); }, reloadItem: function() { @@ -144,7 +185,8 @@ export default Ember.Controller.extend({ var item = this.get("item"); this.get("messageBox").custom( this.locale("edit_images.last_image_with_item"), - this.locale("edit_images.remove_image"), () => this.removeImage(this, item), + this.locale("edit_images.remove_image"), + () => this.removeImage(this, item), "Cancel" ); }, @@ -165,37 +207,49 @@ export default Ember.Controller.extend({ var currentImage = this.get("previewImage"); currentImage.set("favourite", true); - new AjaxPromise(`/images/${currentImage.get('id')}`, "PUT", this.get('session.authToken'), { image: { favourite: true } }) - .then(data => this.get("store").pushPayload(data)) - .catch(error => { - this.get("item.images").forEach(img => img.rollbackAttributes()); - throw error; - }); + new AjaxPromise( + `/images/${currentImage.get("id")}`, + "PUT", + this.get("session.authToken"), + { image: { favourite: true } } + ) + .then(data => this.get("store").pushPayload(data)) + .catch(error => { + this.get("item.images").forEach(img => img.rollbackAttributes()); + throw error; + }); }, deleteImage() { - if (this.get("item.images.length") === 1) - { + if (this.get("item.images.length") === 1) { this.confirmRemoveLastImage(); return; - } - else { - this.get("messageBox").confirm(this.get("i18n").t("edit_images.delete_confirm"), () => { - var loadingView = getOwner(this).lookup('component:loading').append(); - var img = this.get("previewImage"); - img.deleteRecord(); - img.save() - .then(i => { - i.unloadRecord(); - this.reloadItem(); - this.initPreviewImage(); - if (!this.get("favouriteImage")) { - this.send("setFavourite"); - } - }) - .catch(error => { img.rollbackAttributes(); throw error; }) - .finally(() => loadingView.destroy()); - }); + } else { + this.get("messageBox").confirm( + this.get("i18n").t("edit_images.delete_confirm"), + () => { + var loadingView = getOwner(this) + .lookup("component:loading") + .append(); + var img = this.get("previewImage"); + img.deleteRecord(); + img + .save() + .then(i => { + i.unloadRecord(); + this.reloadItem(); + this.initPreviewImage(); + if (!this.get("favouriteImage")) { + this.send("setFavourite"); + } + }) + .catch(error => { + img.rollbackAttributes(); + throw error; + }) + .finally(() => loadingView.destroy()); + } + ); } }, @@ -206,31 +260,33 @@ export default Ember.Controller.extend({ //file upload triggerUpload() { - // For Cordova application if (config.cordova.enabled) { - var onSuccess = ((function() { + var onSuccess = (function() { return function(path) { console.log(path); var dataURL = "data:image/jpg;base64," + path; - Ember.$("input[type='file']").fileupload('option', 'formData').file = dataURL; - Ember.$("input[type='file']").fileupload('add', { files: [ dataURL ] }); + Ember.$("input[type='file']").fileupload( + "option", + "formData" + ).file = dataURL; + Ember.$("input[type='file']").fileupload("add", { + files: [dataURL] + }); }; - })(this)); + })(this); this.initActionSheet(onSuccess); } else { - // For web application - if(navigator.userAgent.match(/iemobile/i)) - { + if (navigator.userAgent.match(/iemobile/i)) { //don't know why but on windows phone need to click twice in quick succession //for dialog to appear - Ember.$("#photo-list input[type='file']").click().click(); - } - else - { + Ember.$("#photo-list input[type='file']") + .click() + .click(); + } else { Ember.$("#photo-list input[type='file']").trigger("click"); } } @@ -246,14 +302,19 @@ export default Ember.Controller.extend({ }, cancelUpload() { - if(this.get("uploadedFileDate")){ this.get("uploadedFileDate").abort(); } + if (this.get("uploadedFileDate")) { + this.get("uploadedFileDate").abort(); + } }, uploadProgress(e, data) { e.target.disabled = true; // disable image-selection - var progress = parseInt(data.loaded / data.total * 100, 10) || 0; + var progress = parseInt((data.loaded / data.total) * 100, 10) || 0; this.set("addPhotoLabel", progress + "%"); - this.set("loadingPercentage", this.get("i18n").t("edit_images.image_uploading") + progress + "%"); + this.set( + "loadingPercentage", + this.get("i18n").t("edit_images.image_uploading") + progress + "%" + ); }, uploadComplete(e) { @@ -261,36 +322,60 @@ export default Ember.Controller.extend({ this.set("uploadedFileDate", null); Ember.$(".loading-image-indicator.hide_image_loading").hide(); this.set("addPhotoLabel", this.get("i18n").t("edit_images.add_photo")); - this.set("loadingPercentage", this.get("i18n").t("edit_images.image_uploading")); + this.set( + "loadingPercentage", + this.get("i18n").t("edit_images.image_uploading") + ); }, uploadSuccess(e, data) { - var identifier = data.result.version + "/" + data.result.public_id + "." + data.result.format; + var identifier = + data.result.version + + "/" + + data.result.public_id + + "." + + data.result.format; var item = this.get("item"); var favourite = item.get("images.length") === 0; var imageAttributes = { - cloudinary_id: identifier, - imageable_id: this.get("item.id"), - imageable_type: "Package", - favourite: favourite - }; + cloudinary_id: identifier, + imageable_id: this.get("item.id"), + imageable_type: "Package", + favourite: favourite + }; - new AjaxPromise("/images", "POST", this.get('session.authToken'), { image: imageAttributes }) - .then(data => { - this.get("store").pushPayload(data); - this.send("setPreview", this.get("store").peekRecord("image", data.image.id)); - }); + new AjaxPromise("/images", "POST", this.get("session.authToken"), { + image: imageAttributes + }).then(data => { + this.get("store").pushPayload(data); + this.send( + "setPreview", + this.get("store").peekRecord("image", data.image.id) + ); + }); + }, + + openImageOverlay() { + this.set("packageService.openImageOverlay", true); + }, + + saveImageURI(image) { + image.set("imageableId", this.get("item.id")); + image.set("imageableType", "Package"); + image.save().then(data => { + this.send("setPreview", this.get("store").peekRecord("image", data.id)); + }); }, rotateImageRight() { var angle = this.get("previewImage.angle"); - angle = (angle + 90)%360; + angle = (angle + 90) % 360; this.send("rotateImage", angle); }, rotateImageLeft() { var angle = this.get("previewImage.angle"); - angle = (angle ? (angle - 90) : 270)%360; + angle = (angle ? angle - 90 : 270) % 360; this.send("rotateImage", angle); }, @@ -302,7 +387,11 @@ export default Ember.Controller.extend({ }, saveImageRotation(image) { - new AjaxPromise(`/images/${image.get('id')}`, "PUT", this.get('session.authToken'), { image: { angle: image.get("angle") } }) - .then(data => this.get("store").pushPayload(data)); + new AjaxPromise( + `/images/${image.get("id")}`, + "PUT", + this.get("session.authToken"), + { image: { angle: image.get("angle") } } + ).then(data => this.get("store").pushPayload(data)); } }); diff --git a/app/controllers/items/new.js b/app/controllers/items/new.js index 457a5dff9..34c73693c 100644 --- a/app/controllers/items/new.js +++ b/app/controllers/items/new.js @@ -565,6 +565,14 @@ export default GoodcityController.extend( ); }, + openImageOverlay() { + this.set("packageService.openImageOverlay", true); + }, + + saveImageURI(imageObject) { + this.set("newUploadedImage", imageObject); + }, + setPkgDescriptionLang(language) { this.set("selectedDescriptionLanguage", language); }, diff --git a/app/controllers/order/search_users.js b/app/controllers/order/search_users.js index 4db4059fe..5181837dd 100644 --- a/app/controllers/order/search_users.js +++ b/app/controllers/order/search_users.js @@ -9,6 +9,8 @@ export default searchModule.extend({ filteredResults: "", queryParams: ["prevPath"], prevPath: null, + messageBox: Ember.inject.service(), + i18n: Ember.inject.service(), onSearchTextChange: Ember.observer("searchText", function() { if (this.get("searchText").length) { @@ -65,10 +67,22 @@ export default searchModule.extend({ goToRequestPurpose(user) { const orderId = this.get("model.order.id"); const userId = user.get("id"); - const organisation_id = user.get( + const organisationId = user.get( "organisationsUsers.firstObject.organisation.id" ); - const orderParams = { created_by_id: userId, organisation_id }; + + if (!organisationId) { + this.get("messageBox").custom( + this.get("i18n").t("search_users.assign_charity_to_user"), + this.get("i18n").t("search_users.edit_user"), + () => this.transitionToRoute("users.details", userId), + this.get("i18n").t("not_now") + ); + + return; + } + + const orderParams = { created_by_id: userId, organisationId }; new AjaxPromise( "/orders/" + orderId, "PUT", diff --git a/app/controllers/orders/client_summary.js b/app/controllers/orders/client_summary.js index c5313cfc9..add79cdcc 100644 --- a/app/controllers/orders/client_summary.js +++ b/app/controllers/orders/client_summary.js @@ -1,9 +1,14 @@ import detail from "./detail"; import Ember from "ember"; +import _ from "lodash"; import AsyncMixin, { ERROR_STRATEGIES } from "stock/mixins/async"; export default detail.extend(AsyncMixin, { showBeneficiaryModal: false, + noPurposeDescription: Ember.computed.not("model.purposeDescription"), + isInvalidPeopleCount: Ember.computed("model.peopleHelped", function() { + return isNaN(this.get("model.peopleHelped")); + }), designationService: Ember.inject.service(), orderService: Ember.inject.service(), @@ -20,6 +25,13 @@ export default detail.extend(AsyncMixin, { return this.get("store").peekAll("identity_type"); }), + isErrorPresent() { + if (this.get("isInvalidPeopleCount") || this.get("noPurposeDescription")) { + this.get("model").rollbackAttributes(); + return true; + } + }, + actions: { removeBeneficiaryModal() { this.toggleProperty("showBeneficiaryModal"); @@ -37,6 +49,24 @@ export default detail.extend(AsyncMixin, { ); }, + updateOrder(field, value) { + const order = this.get("model"); + if (this.isErrorPresent() || !_.keys(order.changedAttributes()).length) { + return; + } + this.updateRecord(order, { [field]: value }); + }, + + updatePeopleHelped(e) { + const value = parseInt(e.target.value); + this.set("order.peopleHelped", value); + this.send("updateOrder", e.target.name, value); + }, + + updatePurposeDescription(e) { + this.send("updateOrder", e.target.name, e.target.value); + }, + deleteBeneficiary() { const order = this.get("model"); diff --git a/app/locales/en/translations.js b/app/locales/en/translations.js index c1ed68980..9c3e33387 100644 --- a/app/locales/en/translations.js +++ b/app/locales/en/translations.js @@ -627,6 +627,13 @@ export default { add_to_set: "Add to set", already_in_set: "This item already belongs to a set" }, + camera: { + click: "Click picture", + cancel: "Cancel", + take: "Take Photo", + choose: "Choose Image{{click_image}}", + switch: "Switch Camera" + }, orders_package: { actions: { edit_quantity: "Edit Qty", @@ -812,7 +819,9 @@ export default { }, search_users: { - new_user: "Which user is this request being made for ?" + new_user: "Which user is this request being made for ?", + edit_user: "Edit User", + assign_charity_to_user: "Please add this user to a valid charity first." }, item_details: { diff --git a/app/locales/zh-tw/translations.js b/app/locales/zh-tw/translations.js index ace5b35f9..327c1347d 100644 --- a/app/locales/zh-tw/translations.js +++ b/app/locales/zh-tw/translations.js @@ -598,6 +598,13 @@ export default { add_to_set: "Add to set", already_in_set: "This item already belongs to a set" }, + camera: { + click: "Click picture", + cancel: "Cancel", + take: "Take Photo", + choose: "Choose Image{{click_image}}", + switch: "Switch Camera" + }, orders_package: { actions: { edit_quantity: "Edit Qty", @@ -776,7 +783,9 @@ export default { }, search_users: { - new_user: "Which user is this request being made for ?" + new_user: "Which user is this request being made for ?", + edit_user: "Edit User", + assign_charity_to_user: "Please add this user to a valid charity first." }, item_details: { diff --git a/app/mixins/preload_data.js b/app/mixins/preload_data.js index 6ba46e38c..04c1a8176 100644 --- a/app/mixins/preload_data.js +++ b/app/mixins/preload_data.js @@ -4,6 +4,8 @@ import AjaxPromise from "../utils/ajax-promise"; export default Ember.Mixin.create({ messages: Ember.inject.service(), + i18n: Ember.inject.service(), + userService: Ember.inject.service(), preloadData: function() { var promises = []; @@ -11,15 +13,13 @@ export default Ember.Mixin.create({ if (this.get("session.authToken")) { promises.push( - new AjaxPromise( - "/auth/current_user_profile", - "GET", - this.session.get("authToken") - ).then(data => { - this.store.pushPayload(data); - this.store.pushPayload({ user: data.user_profile }); - this.notifyPropertyChange("session.currentUser"); - }) + this.get("userService") + .currentUser() + .then(data => { + this.store.pushPayload(data); + this.store.pushPayload({ user: data.user_profile }); + this.notifyPropertyChange("session.currentUser"); + }) ); promises = promises.concat(retrieve(config.APP.PRELOAD_TYPES)); promises.push(this.get("messages").fetchUnreadMessageCount()); diff --git a/app/routes/items/index.js b/app/routes/items/index.js index 284357b76..e0196109b 100644 --- a/app/routes/items/index.js +++ b/app/routes/items/index.js @@ -8,7 +8,7 @@ export default AuthorizeRoute.extend({ itemSetId: "", searchInput: "" }, - + userService: Ember.inject.service(), partial_qnty: Ember.computed.localStorage(), previousPage(transition) { @@ -28,11 +28,7 @@ export default AuthorizeRoute.extend({ return; } if (!this.session.get("currentUser")) { - let data = await new AjaxPromise( - "/auth/current_user_profile", - "GET", - this.session.get("authToken") - ); + let data = await this.get("userService").currentUser(); this.store.pushPayload(data); } }, diff --git a/app/routes/orders/index.js b/app/routes/orders/index.js index 5ff325903..df42ee8cc 100644 --- a/app/routes/orders/index.js +++ b/app/routes/orders/index.js @@ -7,17 +7,15 @@ import { STATE_FILTERS } from "../../services/filter-service"; export default AuthorizeRoute.extend({ filterService: Ember.inject.service(), utilityMethods: Ember.inject.service(), + userService: Ember.inject.service(), /* jshint ignore:start */ async model(params, transition) { if (!this.session.get("currentUser")) { // @TODO: Move this user api call into the session service // Checking the user, if needed, should probably be in AuthorizeRoute - let data = await new AjaxPromise( - "/auth/current_user_profile", - "GET", - this.session.get("authToken") - ); + let data = await this.get("userService").currentUser(); + this.store.pushPayload(data); } }, diff --git a/app/services/order-service.js b/app/services/order-service.js index 677cf5d5d..429bda338 100644 --- a/app/services/order-service.js +++ b/app/services/order-service.js @@ -36,6 +36,11 @@ export default ApiBaseService.extend({ }); }, + async updateOrder(order, params) { + const data = await this.PUT(`/orders/${order.id}`, params); + this.get("store").pushPayload(data); + }, + cancelOrder(order, reason) { return this.changeOrderState(order, "cancel", reason); }, diff --git a/app/services/package-service.js b/app/services/package-service.js index 7f2d61a3c..8b9c3dc05 100644 --- a/app/services/package-service.js +++ b/app/services/package-service.js @@ -2,16 +2,35 @@ import _ from "lodash"; import ApiBaseService from "./api-base-service"; import { toID } from "stock/utils/helpers"; import NavigationAwareness from "stock/mixins/navigation_aware"; +import axios from "npm:axios"; +import config from "../config/environment"; export default ApiBaseService.extend(NavigationAwareness, { store: Ember.inject.service(), i18n: Ember.inject.service(), packageTypeService: Ember.inject.service(), + CLOUD_URL: config.APP.CLOUD_URL, init() { this._super(...arguments); this.set("openPackageSearch", false); this.set("packageSearchOptions", {}); + this.set("openImageOverlay", false); + }, + + async uploadToCloudinary(params) { + const image = await axios.post(this.CLOUD_URL, params); + const identifier = + image.data.version + "/" + image.data.public_id + "." + image.data.format; + const newUploadedImage = this.get("store").createRecord("image", { + cloudinaryId: identifier, + favourite: true + }); + return newUploadedImage; + }, + + generateSignature() { + return this.GET("/images/generate_signature"); }, generateInventoryNumber() { diff --git a/app/services/user-service.js b/app/services/user-service.js index 0bdc82742..3669cbfc0 100644 --- a/app/services/user-service.js +++ b/app/services/user-service.js @@ -16,6 +16,10 @@ export default ApiBaseService.extend({ ); }, + currentUser() { + return this.GET("/auth/current_user_profile"); + }, + getRoleId(roleName) { return this.get("store") .peekAll("role") diff --git a/app/styles/app.scss b/app/styles/app.scss index 81065d2e3..a27cc00c3 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -1,4 +1,3 @@ -@import "scroll-to-bottom"; @import "settings"; @import "bower_components/foundation/scss/foundation"; @import "fonts"; diff --git a/app/styles/templates/components/goodcity/_search_overlay.scss b/app/styles/templates/components/goodcity/_search_overlay.scss index 522c4b688..2ead58511 100644 --- a/app/styles/templates/components/goodcity/_search_overlay.scss +++ b/app/styles/templates/components/goodcity/_search_overlay.scss @@ -1,6 +1,30 @@ .cmp.search-overlay { .main-container { + video, canvas { + max-width: 100%; + height: auto; + } + + .is-hidden { + display: none; + } + + .click-snapshot { + float: right; + margin: 10%; + width: 30%; + margin-top: 40%; + } + + .snapshot { + width: 100%; + + @media #{$small-only} { + font-size: 85%; + } + } + .search-container { display: flex; align-items: stretch; diff --git a/app/styles/templates/items/_detail.scss b/app/styles/templates/items/_detail.scss index 0c5584045..1c5c46e0a 100644 --- a/app/styles/templates/items/_detail.scss +++ b/app/styles/templates/items/_detail.scss @@ -16,6 +16,10 @@ font-style: italic; } + .description_suggestion_container { + padding-bottom: 10px !important; + } + .description-language-container { display: flex; align-items: baseline; diff --git a/app/styles/templates/orders/_detail.scss b/app/styles/templates/orders/_detail.scss index 4e176056b..17fe2beec 100644 --- a/app/styles/templates/orders/_detail.scss +++ b/app/styles/templates/orders/_detail.scss @@ -60,6 +60,10 @@ font-size: 0.8rem !important; } +.people-helped:focus{ + border: 1px solid #8091a9 !important; +} + .beneficiary-title { width: 90% !important; margin-left: 0rem !important; @@ -91,6 +95,11 @@ font-size: 0.8rem !important; } +.order-description-textarea:focus { + border: 1px solid #8091a9 !important; + background-color: #002352 !important; +} + .order-description-textarea:disabled { background-color: transparent; } @@ -386,6 +395,21 @@ margin-top: 3%; } + &.small-options { + font-size: 80%; + } + + &.option-list-padding { + margin-top: -40%; + margin-right: -100%; + font-size: 80%; + + @media #{$small-only} { + margin-top: -20%; + margin-right: -150%; + } + } + .menu-option-text { display: inline-block; width: 50%; @@ -871,9 +895,9 @@ margin-left: 5px; } -.client_summary_description_error, .people_helped_error { - background-color: red; - display: none; +.client_summary_description_error, .people_helped_error{ + background-color: $coral-red; + color: $white; padding: 2px; width: 100%; } diff --git a/app/templates/components/camera-detect.hbs b/app/templates/components/camera-detect.hbs new file mode 100644 index 000000000..831d8ecb8 --- /dev/null +++ b/app/templates/components/camera-detect.hbs @@ -0,0 +1,20 @@ +{{#if displayWebcam}} + +{{/if}} +
+
+ +
+
+ +
+
+ +
+
+ + + + diff --git a/app/templates/components/goodcity/click-image-overlay.hbs b/app/templates/components/goodcity/click-image-overlay.hbs new file mode 100644 index 000000000..94c6ab839 --- /dev/null +++ b/app/templates/components/goodcity/click-image-overlay.hbs @@ -0,0 +1,7 @@ +
+ {{#popup-overlay open=open}} +
+ {{camera-detect getImageCallback=getImageCallback}} +
+ {{/popup-overlay}} +
\ No newline at end of file diff --git a/app/templates/items/detail/tabs/_item_detail.hbs b/app/templates/items/detail/tabs/_item_detail.hbs index 3d4031954..8b1cc0eea 100644 --- a/app/templates/items/detail/tabs/_item_detail.hbs +++ b/app/templates/items/detail/tabs/_item_detail.hbs @@ -13,7 +13,7 @@
-
+
{{#if showDescriptionSuggestion}} {{#if (is-and (is-equal selectedDescriptionLanguage 'en') item.code.descriptionEn)}} @@ -108,13 +108,11 @@ maxlength="6" acceptFloat=true class='numeric-input valuation-input' - onFocusIn=(action 'setValuationIsFocused' true) - onFocusOut=(action 'setValuationIsFocused' false) onSettingInput=(action 'updatePackage') }}
- {{#if canApplyDefaultValuation}} + {{#if (is-not-equal valueHkDollar defaultValueHkDollar)}} {{t "items.apply_default"}} {{defaultValueHkDollar}} diff --git a/app/templates/items/edit_images.hbs b/app/templates/items/edit_images.hbs index ecb5a5272..5051ac77c 100644 --- a/app/templates/items/edit_images.hbs +++ b/app/templates/items/edit_images.hbs @@ -38,12 +38,28 @@
  • - - - {{fa-icon 'camera'}} - {{addPhotoLabel}} - - + {{#composable-drop-down as |dropDown|}} + {{#dropDown.dropDownHeader}} + + + {{fa-icon 'camera'}} + {{addPhotoLabel}} + + + {{/dropDown.dropDownHeader}} + {{#dropDown.dropDownBody}} +
    +
    + Choose Image{{if isMobileApp "/Click Image"}} +
    + {{#unless isMobileApp}} +
    + Take Photo +
    + {{/unless}} +
    + {{/dropDown.dropDownBody}} + {{/composable-drop-down}} {{cloudinary-upload ready="uploadReady" progress="uploadProgress" always="uploadComplete" done="uploadSuccess" submit="uploadProgress" itemId=item.id}} @@ -68,3 +84,10 @@
+ +{{#let-alias packageService as |s|}} + {{goodcity/click-image-overlay + open=s.openImageOverlay + getImageCallback = (action "saveImageURI") + }} +{{/let-alias}} diff --git a/app/templates/items/new.hbs b/app/templates/items/new.hbs index 0c9594da0..49864c4ec 100644 --- a/app/templates/items/new.hbs +++ b/app/templates/items/new.hbs @@ -48,6 +48,7 @@

+
{{t "items.new.type"}} @@ -97,16 +98,30 @@ {{/if}}
- - - {{#if newUploadedImage}} - - {{else}} -
- {{t "items.new.add_images"}} - {{/if}} -
-
+ {{#composable-drop-down as |dropDown|}} + {{#dropDown.dropDownHeader}} + + {{#if newUploadedImage}} + + {{else}} +
+ {{t "items.new.add_images"}} + {{/if}} +
+ {{/dropDown.dropDownHeader}} + {{#dropDown.dropDownBody}} +
+
+ {{t "camera.choose" click_image=(if isMobileApp "/Click Image")}} +
+ {{#unless isMobileApp}} +
+ {{t "camera.take"}} +
+ {{/unless}} +
+ {{/dropDown.dropDownBody}} + {{/composable-drop-down}} {{cloudinary-upload ready="uploadReady" progress="uploadProgress" always="uploadComplete" done="uploadSuccess" submit="uploadStart"}}
@@ -385,3 +400,11 @@ {{/validatable-form}} {{partial "loading_image"}} + +{{#let-alias packageService as |s|}} + {{goodcity/click-image-overlay + open=s.openImageOverlay + getImageCallback = (action "saveImageURI") + }} +{{/let-alias}} + diff --git a/app/templates/order/appointment_details.hbs b/app/templates/order/appointment_details.hbs index c32200c4f..79504741d 100644 --- a/app/templates/order/appointment_details.hbs +++ b/app/templates/order/appointment_details.hbs @@ -1,7 +1,3 @@ -{{!-- {{#if isMobileApp}} - {{scroll-to-bottom }} -{{/if}} --}} -
diff --git a/package.json b/package.json index 5817df947..2ee828f1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stock", - "version": "0.21.2", + "version": "0.21.3", "private": true, "directories": { "doc": "doc", @@ -77,7 +77,6 @@ "ember-pretty-test": "^1.0.4", "ember-resolver": "^2.1.0", "ember-rollbar-client": "0.2.2", - "ember-scroll-to-bottom": "1.3.0", "ember-searchable-select": "^0.11.0", "ember-smart-banner": "0.1.3", "ember-welcome-page": "^1.0.3", @@ -91,6 +90,7 @@ "prettier": "1.15.3" }, "dependencies": { + "axios": "^0.21.1", "azure-api": "git://github.com/crossroads/azure-api.git#master", "blueimp-file-upload": "^9.17.0", "bower": "^1.8.4", diff --git a/yarn.lock b/yarn.lock index a4d39d64b..73939a462 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3069,6 +3069,13 @@ aws4@^1.2.1, aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== + dependencies: + follow-redirects "^1.10.0" + "azure-api@git://github.com/crossroads/azure-api.git#master": version "0.0.1" resolved "git://github.com/crossroads/azure-api.git#5b7e4d4e3aa5f71a731bebd1f6f7831936fb7eba" @@ -7870,14 +7877,6 @@ ember-runtime-enumerable-includes-polyfill@^2.0.0: ember-cli-babel "^6.9.0" ember-cli-version-checker "^2.1.0" -ember-scroll-to-bottom@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ember-scroll-to-bottom/-/ember-scroll-to-bottom-1.3.0.tgz#7b0318cc1baa77e7dd97ca872945a127e09d2780" - integrity sha512-1dhkLncYHekMO5xIqrsH8jlcqG4Lq6iwnZx7EcX2UtyPRilQi+9UVd3hCzIFiPNpT5v8hCJ2ur3WGFLIHDBgFw== - dependencies: - ember-cli-babel "^6.3.0" - ember-cli-sass "5.5.2" - ember-searchable-select@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/ember-searchable-select/-/ember-searchable-select-0.11.0.tgz#785406483a8e1573f63880bebe1efd3aa79b2737" @@ -8956,6 +8955,11 @@ follow-redirects@^1.0.0: dependencies: debug "^3.2.6" +follow-redirects@^1.10.0: + version "1.13.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" + integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"