Introduce display of all annotations at the same time.
- Add stats to serialized annotation.
- Introduce AnnotationList component.
- Pass array of annotations to component.
- Handle click on annotation & drawing.
--- a/src/iconolab/serializers.py Thu Mar 09 12:39:36 2017 +0100
+++ b/src/iconolab/serializers.py Mon Mar 13 18:48:35 2017 +0100
@@ -1,9 +1,15 @@
-from iconolab.models import AnnotationRevision, IconolabComment
+from iconolab.models import AnnotationRevision, IconolabComment, AnnotationStats
from rest_framework import serializers
+class AnnotationStatsSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = AnnotationStats
+ fields = ('submitted_revisions_count', 'awaiting_revisions_count', 'accepted_revisions_count', 'contributors_count', 'views_count', 'comments_count', 'tag_count')
+
class AnnotationRevisionSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField('get_normalized_tags')
annotation_guid = serializers.SerializerMethodField()
+ stats = AnnotationStatsSerializer(source='annotation.stats')
def get_normalized_tags(self, obj):
tags = []
@@ -21,8 +27,7 @@
class Meta:
model = AnnotationRevision
- fields = ('annotation_guid', 'title', 'description', 'fragment', 'tags')
-
+ fields = ('annotation_guid', 'title', 'description', 'fragment', 'tags', 'stats')
class IconolabCommentSerializer(serializers.ModelSerializer):
allow_thread = serializers.BooleanField()
--- a/src/iconolab/templates/iconolab/detail_image.html Thu Mar 09 12:39:36 2017 +0100
+++ b/src/iconolab/templates/iconolab/detail_image.html Mon Mar 13 18:48:35 2017 +0100
@@ -10,25 +10,11 @@
<div class="annotation-navigator-list">
<div class="panel panel-default">
{% if image.annotations.exists %}
- <div class="list-group">
- {% for annotation in image.latest_annotations %}
- <a href="#{{ annotation.annotation_guid }}" class="list-group-item"
- data-annotation-id="{{ annotation.annotation_guid }}"
- data-revision-id="{{ annotation.current_revision.revision_guid }}">
- <h3 class="small list-group-item-heading">
- {{ annotation.current_revision.title }}
- <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
- </h3>
- {% for tagging_info in annotation.current_revision.tagginginfo_set.all %}
- <p class="list-group-item-text">{{ tagging_info.tag.label }}</p>
- {% endfor %}
- <div class="list-group-item-footer">
- <i class="fa fa-comment"></i>
- {{ annotation.stats.comments_count }} commentaire{% if annotation.stats.comments_count > 1 %}s{% endif %}
- </div>
- </a>
- {% endfor %}
- </div>
+ <annotation-list
+ v-bind:annotations="annotations"
+ v-bind:annotation="annotation"
+ v-on:click:annotation="onAnnotationClick($event)"
+ v-on:close:annotation="onAnnotationClose"><annotation-list>
{% else %}
<div class="panel-body">
<div class="alert alert-warning text-center">Pas d'annotation pour cette image</div>
@@ -41,7 +27,10 @@
ref="annotator"
image="{% with image.media as img %}{{ img.url }}{% endwith %}"
thumbnail="{% thumbnail image.media '100x100' crop=False as thumb %}{{ thumb.url }}{% endthumbnail %}"
- v-bind:is-authenticated="isAuthenticated"></image-annotator>
+ v-bind:is-authenticated="isAuthenticated"
+ v-bind:annotations="annotations"
+ v-on:click:annotation="onAnnotationClick($event)"
+ v-on:close:annotation="onAnnotationClose"></image-annotator>
<form id="form-annotation" action="{% url 'annotation_create' collection_name image.image_guid %}" method="POST">
{% csrf_token %}
<input type="hidden" name="{{ form.title.name }}">
@@ -61,7 +50,7 @@
<div class="annotation-comment-box" id="form-comment">
<comment-form v-if="annotation"></comment-form>
</div>
- <div style="display: none;" class="annotation-comment-list">
+ <div v-show="annotation" class="annotation-comment-list">
<label class="small text-muted">Commentaires</label>
<comment-list v-bind:annotation="annotation"
fetch="{% url 'get_annotation_comments_json' ':annotation_guid' %}"></comment-list>
@@ -112,82 +101,38 @@
}
});
+ var annotations = [];
+ {% if image.annotations.exists %}
+ {% for annotation in image.latest_annotations %}
+ annotations.push({{ annotation.current_revision|json|safe }});
+ {% endfor %}
+ {% endif %}
+
var vm = new Vue({
el: '.annotation-navigator',
data: function() {
return {
- annotation: null,
- isAuthenticated: isAuthenticated
+ annotations: annotations,
+ annotation: getAnnotationFromHash(),
+ isAuthenticated: isAuthenticated,
};
+ },
+ methods: {
+ onAnnotationClick: function(annotation) {
+ var self = this;
+ getCommentForm(annotation).then(function(template) {
+ updateCommentFormComponent(template);
+ self.annotation = annotation;
+ location.hash = '#' + annotation.annotation_guid;
+ });
+ },
+ onAnnotationClose: function() {
+ this.annotation = null;
+ location.hash = '';
+ }
}
});
- var annotations = {};
- {% if image.annotations %}
- {% for annotation in image.annotations.all %}
- annotations["{{ annotation.current_revision.revision_guid }}"] = {{ annotation.current_revision|json|safe }}
- {% endfor %}
- {% endif %}
-
- function displayAnnotation($el) {
-
- var annotation = $el.data('annotation-id');
- var revision = $el.data('revision-id');
-
- // Load the form for selected annotation via AJAX
- $.get(getCommentFormURL.replace(':annotation_guid', annotation), { next: currentPath + '#' + annotation })
- .then(function(form) {
-
- Vue.component('comment-form', function (resolve, reject) {
- resolve({
- props: ['reply-to'],
- template: form,
- data: function() {
- return {
- active: null
- }
- },
- watch: {
- active: function(active) {
- if (active) {
- setTimeout(() => $(this.$el).find('[name="comment"]').focus(), 200);
- }
- }
- },
- computed: {
- btnGroupClass: function() {
- var classes = [
- { active: this.active || false }
- ];
-
- if (this.active) {
- classes.push('btn-group-' + this.active)
- }
-
- return classes;
- }
- }
- });
- });
-
- $('.list-group a[data-annotation-id]').removeClass('active');
- $el.addClass('active');
-
- $('.annotation-comment-list').show();
-
- vm.annotation = annotations[revision];
-
- location.hash = '#' + annotation;
-
- });
- }
-
- $('.list-group a[data-annotation-id]').on('click', function(e) {
- e.preventDefault();
- var $el = $(e.currentTarget);
- displayAnnotation($el);
- });
-
vm.$refs.annotator.$on('save', function(data) {
$('#form-annotation').find('input[name="title"]').val(data.title);
$('#form-annotation').find('input[name="description"]').val(data.description);
@@ -205,39 +150,55 @@
$('#form-annotation').submit();
})
- $('.list-group a[data-annotation-id] .close').on('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- var $target = $(e.currentTarget);
- $target.closest('.list-group-item').removeClass('active');
-
- vm.annotation = null;
+ function updateCommentFormComponent(template) {
+ Vue.component('comment-form', function (resolve, reject) {
+ resolve({
+ props: ['reply-to'],
+ template: template,
+ data: function() {
+ return {
+ active: null
+ }
+ },
+ watch: {
+ active: function(active) {
+ if (active) {
+ setTimeout(() => $(this.$el).find('[name="comment"]').focus(), 200);
+ }
+ }
+ },
+ computed: {
+ btnGroupClass: function() {
+ var classes = [
+ { active: this.active || false }
+ ];
- $('.annotation-comment-list').hide();
-
- location.hash = '';
- });
+ if (this.active) {
+ classes.push('btn-group-' + this.active)
+ }
- function scrollTo($container, $el) {
- $container.animate({
- scrollTop: $container.scrollTop() - $container.offset().top + $el.offset().top
- }, 600);
+ return classes;
+ }
+ }
+ });
+ });
}
- function router (scroll) {
+ function getCommentForm(annotation) {
+ return $.get(getCommentFormURL.replace(':annotation_guid', annotation.annotation_guid), {
+ next: currentPath + '#' + annotation.annotation_guid
+ })
+ }
+
+ function getAnnotationFromHash() {
var url = location.hash.slice(1);
if (url.length) {
- var annotation = url;
- var $el = $('.list-group a[data-annotation-id="'+annotation+'"]');
- displayAnnotation($el);
- if (scroll) {
- scrollTo($el.closest('.panel'), $el);
- }
+ var annotation_guid = url;
+ return _.find(annotations, function(annotation) {
+ return annotation.annotation_guid === annotation_guid;
+ });
}
}
- window.addEventListener('hashchange', function() { router(true) });
- window.addEventListener('load', function() { router(true) });
</script>
{% endblock %}
--- a/src_js/iconolab-bundle/src/components/editor/AnnotationForm.vue Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/AnnotationForm.vue Mon Mar 13 18:48:35 2017 +0100
@@ -76,17 +76,15 @@
data() {
return defaults;
},
+ mounted() {
+ if (this.annotation) {
+ this.loadAnnotation(this.annotation);
+ }
+ },
watch: {
annotation: function(annotation) {
if (annotation) {
- // Make sure we have an actual copy
- Object.assign(this, {
- title: annotation.title,
- description: annotation.description,
- fragment: annotation.fragment,
- tags: annotation.tags.slice(),
- readonly: true,
- });
+ this.loadAnnotation(annotation);
} else {
this.reset();
}
@@ -128,6 +126,16 @@
reset: function() {
Object.assign(this, defaults);
},
+ loadAnnotation(annotation) {
+ // Make sure we have an actual copy
+ Object.assign(this, {
+ title: annotation.title,
+ description: annotation.description,
+ fragment: annotation.fragment,
+ tags: annotation.tags.slice(),
+ readonly: true,
+ });
+ }
}
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src_js/iconolab-bundle/src/components/editor/AnnotationList.vue Mon Mar 13 18:48:35 2017 +0100
@@ -0,0 +1,79 @@
+<template>
+ <div class="list-group">
+ <a v-for="annotation in annotations"
+ ref="annotations"
+ @click="onAnnotationClick($event, annotation)"
+ :href="'#' + annotation.annotation_guid"
+ class="list-group-item"
+ v-bind:class="{ active: isActive(annotation) }">
+ <h3 class="small list-group-item-heading">
+ {{ annotation.title }}
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close" @click="onAnnotationClose">
+ <span aria-hidden="true">×</span>
+ </button>
+ </h3>
+ <p class="list-group-item-text" v-for="tag in annotation.tags">{{ tag.tag_label }}</p>
+ <div class="list-group-item-footer">
+ <i class="fa fa-comment"></i> {{ getCommentsCount(annotation) }}
+ </div>
+ </a>
+ </div>
+</template>
+
+<script>
+
+ import _ from 'lodash';
+
+ export default {
+ props: [
+ 'annotations',
+ 'annotation'
+ ],
+ mounted() {
+ if (this.annotation) {
+ this.slideToAnnotation();
+ }
+ },
+ watch: {
+ annotation: function(annotation) {
+ if (annotation) {
+ this.slideToAnnotation();
+ }
+ }
+ },
+ methods: {
+ onAnnotationClick: function(e, annotation) {
+ e.preventDefault();
+ this.$emit('click:annotation', annotation);
+ },
+ onAnnotationClose: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.$emit('close:annotation');
+ },
+ isActive: function(annotation) {
+ return annotation === this.annotation;
+ },
+ getCommentsCount: function(annotation) {
+ var commentsCount = annotation.stats.comments_count;
+ return commentsCount + ' commentaire' + (commentsCount > 1 ? 's' : '');
+ },
+ slideToAnnotation: function() {
+ var el = _.find(this.$refs.annotations, (el) => {
+ return $(el).attr('href') === ('#' + this.annotation.annotation_guid);
+ });
+ if (el) {
+ var $container = $(this.$el.closest('.panel'));
+ $container.animate({
+ scrollTop: $container.scrollTop() - $container.offset().top + $(el).offset().top
+ }, 600);
+ }
+ }
+ }
+ }
+
+</script>
+
+<style scoped>
+
+</style>
--- a/src_js/iconolab-bundle/src/components/editor/Canvas.vue Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/Canvas.vue Mon Mar 13 18:48:35 2017 +0100
@@ -12,14 +12,40 @@
x="0" y="0"
v-bind:width="imageWidth"
v-bind:height="imageHeight" />
- <shape-rect ref="rect" v-show="loaded && mode == 'rect'"
+
+ <!-- These are the existing fragments -->
+
+ <!-- FIXME using <component :is="..."> does not work -->
+ <shape-rect
+ v-for="annotation in normalizedAnnotations"
+ :key="annotation.annotation_guid"
+ v-if="loaded && readonly && annotation.mode == 'rect'"
v-bind:paper="paper"
v-bind:original-annotation="annotation"
- v-bind:readonly="readonly"></shape-rect>
- <shape-free ref="free" v-show="loaded && mode == 'free'"
+ v-bind:original-path="annotation.path"
+ v-bind:readonly="readonly"
+ v-on:click="onAnnotationClick(annotation)"></shape-rect>
+ <shape-free
+ v-for="annotation in normalizedAnnotations"
+ :key="annotation.annotation_guid"
+ v-if="loaded && readonly && annotation.mode == 'free'"
v-bind:paper="paper"
v-bind:original-annotation="annotation"
- v-bind:readonly="readonly"></shape-free>
+ v-bind:original-path="annotation.path"
+ v-bind:readonly="readonly"
+ v-on:click="onAnnotationClick(annotation)"></shape-free>
+
+ <!-- These are the new fragments -->
+
+ <shape-rect ref="rect"
+ v-show="loaded && !readonly && mode == 'rect'"
+ v-bind:paper="paper"
+ :readonly="false"></shape-rect>
+ <shape-free ref="free"
+ v-show="loaded && !readonly && mode == 'free'"
+ v-bind:paper="paper"
+ :readonly="false"></shape-free>
+
<defs>
<filter id="shadow" width="200%" height="200%">
<feOffset result="offOut" in="SourceAlpha" dx="0" dy="0"/>
@@ -31,6 +57,10 @@
</div>
<div class="controls">
<div class="controls-left">
+ <button @click="readonly = !readonly" type="button" class="btn">
+ <i v-show="readonly" class="fa fa-pencil"></i>
+ <i v-show="!readonly" class="fa fa-close"></i>
+ </button>
<button @click="setMode('rect')" type="button" class="btn"
v-bind:class="{ 'active': mode === 'rect', 'disabled': readonly }">
<svg width="14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"><g><rect x="352" y="432" width="64" height="48"/><polygon points="416,352 416,96 176,96 176,160 352,160 352,352 160,352 160,32 96,32 96,96 32,96 32,160 96,160 96,416 480,416 480,352"/></g><text x="0" y="527" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Bluetip Design</text><text x="0" y="532" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>
@@ -72,6 +102,7 @@
import ShapeRect from './ShapeRect.vue'
import ShapeFree from './ShapeFree.vue'
import ZoomThumbnail from './ZoomThumbnail.vue'
+ import _ from 'lodash'
export default {
props: {
@@ -88,6 +119,10 @@
isAuthenticated: {
type: Boolean,
default: false
+ },
+ annotations: {
+ type: Array,
+ default: []
}
},
components: {
@@ -116,17 +151,37 @@
'canvas--rect': !this.readonly && this.isAuthenticated && this.mode === 'rect',
'canvas--free': !this.readonly && this.isAuthenticated && this.mode === 'free'
}
+ },
+ normalizedAnnotations: function() {
+ var normalizedAnnotations = _.map(this.annotations, (annotation) => {
+ if (annotation.fragment.length > 0) {
+ var pieces = annotation.fragment.split(';');
+ var path = this.denormalizePath(pieces[0]);
+ var mode = pieces[1].toLowerCase();
+
+ Object.assign(annotation, {
+ mode: mode,
+ path: path,
+ });
+ }
+
+ return annotation;
+ });
+
+ if (this.annotation) {
+ return _.filter(normalizedAnnotations, (annotation) => {
+ return annotation === this.annotation;
+ });
+ }
+
+ return normalizedAnnotations;
}
},
watch: {
mode: function(mode) {
this.reset();
- if (mode === 'free') {
- this.handleDrawFree();
- }
- if (mode === 'rect') {
- this.handleDrawRect();
- }
+ if (this.readonly) { return; }
+ this.handleDraw();
},
loaded: function(loaded) {
if (!loaded) { return; }
@@ -153,23 +208,13 @@
this.paper.attr({"viewBox": this.viewBox});
- if (this.annotation) {
- this.loadAnnotation();
- } else {
- if (this.mode === 'free') {
- this.handleDrawFree();
- }
- if (this.mode === 'rect') {
- this.handleDrawRect();
- }
+ if (_.size(this.annotations) > 0) {
+ this.readonly = true;
}
+
},
annotation: function(annotation) {
- this.reset();
this.readonly = !!annotation || !this.isAuthenticated;
- if (this.annotation) {
- this.loadAnnotation();
- }
},
scale: function(scale) {
var factor = 0;
@@ -195,6 +240,16 @@
this.hideTooltip();
this.animateViewBox(viewBox, () => this.showTooltip());
}
+ },
+ readonly: function(readonly, previous) {
+ this.reset();
+
+ if (!readonly) {
+ this.handleDraw();
+ if (this.annotation) {
+ this.$emit('close:annotation');
+ }
+ }
}
},
mounted() {
@@ -219,13 +274,12 @@
},
methods: {
+ onAnnotationClick: function(annotation) {
+ this.$emit('click:annotation', annotation);
+ },
hideTooltip: function() {
- if (this.mode === 'free') {
- this.$refs.free.hideTooltip();
- }
- if (this.mode === 'rect') {
- this.$refs.rect.hideTooltip();
- }
+ this.$refs.free.hideTooltip();
+ this.$refs.rect.hideTooltip();
},
showTooltip: function() {
if (this.mode === 'free') {
@@ -236,20 +290,13 @@
}
},
reset: function() {
- // Clear shapes
+
this.$refs.rect.clear();
this.$refs.free.clear();
this.removeEventHandlers();
this.resetZoom();
this.resetViewBox();
-
- if (this.mode === 'free') {
- this.handleDrawFree();
- }
- if (this.mode === 'rect') {
- this.handleDrawRect();
- }
},
removeEventHandlers: function() {
@@ -259,26 +306,6 @@
this.paper.unclick();
},
- loadAnnotation: function() {
- if (this.annotation.fragment.length > 0) {
- var pieces = this.annotation.fragment.split(';');
- var path = pieces[0];
- var mode = pieces[1].toLowerCase();
-
- this.mode = mode;
-
- this.$nextTick(() => {
- path = this.denormalizePath(path);
- if (mode === 'free') {
- this.$refs.free.fromSVGPath(path, this.tooltip);
- }
- if (mode === 'rect') {
- this.$refs.rect.fromSVGPath(path, this.tooltip);
- }
- });
- }
- },
-
setMode: function(mode) {
if (this.readonly) { return; }
@@ -403,6 +430,15 @@
}
},
+ handleDraw: function() {
+ if (this.mode === 'free') {
+ this.handleDrawFree();
+ }
+ if (this.mode === 'rect') {
+ this.handleDrawRect();
+ }
+ },
+
handleDrawFree: function() {
if (!this.isAuthenticated) { return; }
--- a/src_js/iconolab-bundle/src/components/editor/ShapeFree.vue Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/ShapeFree.vue Mon Mar 13 18:48:35 2017 +0100
@@ -1,5 +1,5 @@
<template>
- <g filter="url(#shadow)">
+ <g ref="g" filter="url(#shadow)">
<path ref="path" v-bind:d="path"
class="path"
v-bind:stroke-width="handlerRadius / 2"
@@ -29,6 +29,7 @@
'paper',
'original-annotation',
'readonly',
+ 'original-path'
],
data() {
return {
@@ -39,7 +40,11 @@
}
},
mounted() {
-
+ if (this.originalPath) {
+ this.fromSVGPath(this.originalPath, false);
+ var g = new Snap(this.$refs.g);
+ g.click(() => this.$emit('click'));
+ }
},
watch: {
closed: function(closed) {
@@ -195,8 +200,12 @@
}
.path {
stroke: #000;
- fill: #fff;
- opacity: 0.6;
+ fill: transparent;
+}
+.path:hover {
+ cursor: pointer;
+ fill: #333;
+ opacity: 0.5;
}
.handler--first {
fill: yellow;
--- a/src_js/iconolab-bundle/src/components/editor/ShapeRect.vue Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/ShapeRect.vue Mon Mar 13 18:48:35 2017 +0100
@@ -6,7 +6,8 @@
v-bind:width="width" v-bind:height="height"
v-bind:stroke-width="handlerSize / 5"
class="shape"
- v-bind:class="{ 'shape--draggable': !readonly }"></rect>
+ v-bind:class="{ 'shape--draggable': !readonly }"
+ v-bind:style="{ 'outline-width': (handlerSize / 5) + 'px' }"></rect>
<rect
ref="topLeft"
v-show="showResizeHandlers"
@@ -37,6 +38,7 @@
'original-annotation',
'tooltip',
'readonly',
+ 'original-path',
],
data() {
return {
@@ -79,6 +81,11 @@
var g = new Snap(this.$refs.g);
g.drag(groupEvents.onMove, groupEvents.onStart, groupEvents.onEnd);
+
+ if (this.originalPath) {
+ this.fromSVGPath(this.originalPath, false);
+ g.click(() => this.$emit('click'))
+ }
},
watch: {
x: function (val, oldVal) {
@@ -208,8 +215,15 @@
<style scoped>
.shape {
fill: transparent;
- stroke: #000;
- opacity: 0.6;
+ stroke: #fff;
+ outline-color: #000;
+ outline-style: solid;
+}
+.shape:hover,
+.shape.active {
+ cursor: pointer;
+ fill: #333;
+ opacity: 0.5;
}
.shape--draggable:hover {
cursor: move;
--- a/src_js/iconolab-bundle/src/components/editor/index.js Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/components/editor/index.js Mon Mar 13 18:48:35 2017 +0100
@@ -1,9 +1,11 @@
import Canvas from './Canvas.vue'
import AnnotationForm from './AnnotationForm.vue'
+import AnnotationList from './AnnotationList.vue'
import CommentList from './CommentList.vue'
export default {
Canvas: Canvas,
AnnotationForm: AnnotationForm,
+ AnnotationList: AnnotationList,
CommentList: CommentList,
}
--- a/src_js/iconolab-bundle/src/main.js Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/src/main.js Mon Mar 13 18:48:35 2017 +0100
@@ -34,8 +34,8 @@
Vue.component('color-buttons', ColorButtons);
Vue.component('image-annotator', Editor.Canvas);
-Vue.component('annotation', Editor.Annotation);
Vue.component('annotation-form', Editor.AnnotationForm);
+Vue.component('annotation-list', Editor.AnnotationList);
Vue.component('comment-list', Editor.CommentList);
if (!window.iconolab) {
--- a/src_js/iconolab-bundle/webpack.config.js Thu Mar 09 12:39:36 2017 +0100
+++ b/src_js/iconolab-bundle/webpack.config.js Mon Mar 13 18:48:35 2017 +0100
@@ -77,7 +77,8 @@
new ExtractTextPlugin("iconolab/css/[name].css"),
new webpack.ProvidePlugin({
$: "jquery",
- jQuery: "jquery"
+ jQuery: "jquery",
+ _: "lodash"
})
],
devServer: {