Merge pull request #2 from imulus/feature/dev-tasks
Grunt Tasks for Development
This commit is contained in:
+27
-6
@@ -18,7 +18,13 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
spawn: false,
|
||||
}
|
||||
},
|
||||
|
||||
dev: {
|
||||
files: ['app/**'],
|
||||
tasks: ['deploy']
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
jshint: {
|
||||
@@ -34,9 +40,22 @@ module.exports = function(grunt) {
|
||||
copy: {
|
||||
main: {
|
||||
files: [
|
||||
{expand: true, src: ['app/package.manifest'], dest: '<%= dest %>', flatten: true},
|
||||
{expand: true, src: ['app/views/archetype.html'], dest: '<%= dest %>/views', flatten: true}
|
||||
{expand: true, cwd: 'app/', src: ['package.manifest'], dest: '<%= dest %>', flatten: true},
|
||||
{expand: true, cwd: 'app/views/', src: ['archetype.html'], dest: '<%= dest %>/views', flatten: true}
|
||||
]
|
||||
},
|
||||
deploy: {
|
||||
files: [
|
||||
{expand: true, cwd: '<%= dest %>/', src: ['**'], dest: '<%= grunt.option("target") %>\\App_Plugins\\Imulus.Archetype', flatten: false},
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
touch: {
|
||||
options: {},
|
||||
webconfig: {
|
||||
src: ['D:\\dev\\projects\\imulus-neue\\src\\imulus.umbraco\\web.config']
|
||||
}
|
||||
},
|
||||
|
||||
@@ -57,8 +76,8 @@ module.exports = function(grunt) {
|
||||
},
|
||||
application: {
|
||||
src: [
|
||||
'app/controllers/archetype_controller.js',
|
||||
'app/directives/content_item.js'
|
||||
'app/controllers/controller.js',
|
||||
'app/directives/archetypeproperty.js'
|
||||
],
|
||||
dest: '<%= dest %>/js/archetype.js'
|
||||
}
|
||||
@@ -75,10 +94,12 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-touch');
|
||||
|
||||
|
||||
grunt.registerTask('touchwebconfigifenabled', function() { if (grunt.option("touch")) grunt.task.run("touch:webconfig") });
|
||||
grunt.registerTask('deploy', ['default', 'copy:deploy', 'touchwebconfigifenabled']);
|
||||
grunt.registerTask('css:build', ['less']);
|
||||
grunt.registerTask('js:build', ['concat']);
|
||||
grunt.registerTask('default', ['clean', 'css:build', 'js:build', 'copy']);
|
||||
grunt.registerTask('default', ['clean', 'css:build', 'js:build', 'copy:main']);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,4 +12,7 @@ Archetype
|
||||
|
||||
|
||||
## Deploy ##
|
||||
Copy the files in `/dist/` to your Umbraco site, at `/App_Plugins/Archetype/`
|
||||
grunt deploy --target=C:\\path\\to\\umbraco\\site
|
||||
grunt watch:dev --target=C:\\path\to\\umbraco\\site
|
||||
|
||||
* Add `--touch` to either command to automatically touch the web.config on a deploy
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
//$scope.model.value = "";
|
||||
|
||||
//test config/model
|
||||
//$scope.model.config.emptyFieldSetModel = '{ remove: false, properties:[{ type: "textbox", options: { label: "Name" }, data: "" }, { type: "contentPicker", options: { label: "Pick some content" }, data: "" }]}';
|
||||
|
||||
//set default value of the model
|
||||
//this works by checking to see if there is a model; then cascades to the default model then to an empty fieldset
|
||||
var validDefaultModel = getValidJson("$scope.model.config.defaultModel", $scope.model.config.defaultModel);
|
||||
var validEmptyFieldsetModel = getValidJson("$scope.model.config.emptyFieldsetModel", $scope.model.config.emptyFieldsetModel);
|
||||
|
||||
$scope.model.value = $scope.model.value || (validDefaultModel || { fieldsets: [validEmptyFieldsetModel] });
|
||||
//validate the user configs
|
||||
$scope.model.config.defaultModel = getValidJson("$scope.model.config.defaultModel", $scope.model.config.defaultModel);
|
||||
$scope.model.config.fieldsetModels = getValidJson("$scope.model.config.fieldsetModels", $scope.model.config.fieldsetModels);
|
||||
|
||||
$scope.model.value = $scope.model.value || ($scope.model.config.defaultModel || { fieldsets: [getEmptyRenderItem($scope.model.config.fieldsetModels[0])] });
|
||||
|
||||
//ini
|
||||
$scope.archetypeRenderModel = {};
|
||||
@@ -20,7 +19,10 @@
|
||||
|
||||
//defines the options for the jquery sortable
|
||||
//i used an ng-model="archetypeRenderModel" so the sort updates the right model
|
||||
$scope.sortableOptions = {
|
||||
//configuration overrides the default
|
||||
var configSortableOptions = getValidJson("$scope.model.config.sortableOptions", $scope.model.config.sortableOptions);
|
||||
|
||||
$scope.sortableOptions = configSortableOptions || {
|
||||
axis: 'y',
|
||||
cursor: "move",
|
||||
handle: ".handle",
|
||||
@@ -28,18 +30,26 @@
|
||||
|
||||
},
|
||||
stop: function (ev, ui) {
|
||||
console.log($scope.archetypeRenderModel);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addRow = function ($index) {
|
||||
if (true)
|
||||
//handles a fieldset add
|
||||
$scope.addRow = function (fieldsetAlias, $index) {
|
||||
if ($scope.canAdd())
|
||||
{
|
||||
var validJson = getValidJson("$scope.model.config.emptyFieldsetModel", $scope.model.config.emptyFieldsetModel);
|
||||
|
||||
if (validJson)
|
||||
if ($scope.model.config.fieldsetModels)
|
||||
{
|
||||
$scope.archetypeRenderModel.fieldsets.splice($index + 1, 0, validJson);
|
||||
var newRenderItem = getEmptyRenderItem($scope.getConfigFieldsetByAlias(fieldsetAlias));
|
||||
|
||||
if (typeof $index != 'undefined')
|
||||
{
|
||||
$scope.archetypeRenderModel.fieldsets.splice($index + 1, 0, newRenderItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
$scope.archetypeRenderModel.fieldsets.push(newRenderItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,11 +74,13 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
//helper that returns if an item can be removed
|
||||
$scope.canRemove = function ()
|
||||
{
|
||||
return countVisible() > 1;
|
||||
}
|
||||
|
||||
//helper that returns if an item can be sorted
|
||||
$scope.canSort = function ()
|
||||
{
|
||||
return countVisible() > 1;
|
||||
@@ -79,6 +91,15 @@
|
||||
$scope.archetypeRenderModel = $scope.model.value;
|
||||
}
|
||||
|
||||
//helper to get the correct fieldset from config
|
||||
$scope.getConfigFieldsetByAlias = function(alias) {
|
||||
for (var i in $scope.model.config.fieldsetModels) {
|
||||
if ($scope.model.config.fieldsetModels[i].alias == alias) {
|
||||
return $scope.model.config.fieldsetModels[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//helper returns valid JS or null
|
||||
function getValidJson(variable, json)
|
||||
{
|
||||
@@ -97,6 +118,7 @@
|
||||
//developerMode helpers
|
||||
$scope.archetypeRenderModel.toString = stringify;
|
||||
|
||||
//encapsulate stringify (should be built into browsers, not sure of IE support)
|
||||
function stringify() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
@@ -104,7 +126,7 @@
|
||||
//watch for changes
|
||||
$scope.$watch('archetypeRenderModel', function (v) {
|
||||
if ($scope.model.config.developerMode) {
|
||||
console.log(v);
|
||||
//console.log(v);
|
||||
if (typeof v === 'string') {
|
||||
$scope.archetypeRenderModel = JSON.parse(v);
|
||||
$scope.archetypeRenderModel.toString = stringify;
|
||||
@@ -138,6 +160,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
//helper to add an empty fieldset
|
||||
function getEmptyRenderItem (fieldsetModel)
|
||||
{
|
||||
return eval("({ alias: '" + fieldsetModel.alias + "', remove: false, properties: []})");
|
||||
}
|
||||
|
||||
//sync things up on save
|
||||
$scope.$on("formSubmitting", function (ev, args) {
|
||||
syncModelToRenderModel();
|
||||
@@ -146,10 +174,10 @@
|
||||
//custom js
|
||||
if ($scope.model.config.customJsPath) {
|
||||
assetsService.loadJs($scope.model.config.customJsPath);
|
||||
}
|
||||
}
|
||||
|
||||
//archetype css
|
||||
assetsService.loadCss("/App_Plugins/Archetype/css/archetype.css");
|
||||
assetsService.loadCss("/App_Plugins/Imulus.Archetype/css/archetype.css");
|
||||
|
||||
//custom css
|
||||
if($scope.model.config.customCssPath)
|
||||
@@ -0,0 +1,89 @@
|
||||
angular.module("umbraco").directive('archetypeProperty', function ($compile, $http) {
|
||||
|
||||
function getFieldsetByAlias(fieldsetModels, alias)
|
||||
{
|
||||
for (var i in fieldsetModels)
|
||||
{
|
||||
if (fieldsetModels[i].alias == alias)
|
||||
{
|
||||
return fieldsetModels[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPropertyIdByAlias(properties, alias)
|
||||
{
|
||||
for (var i in properties)
|
||||
{
|
||||
if (properties[i].alias == alias) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var linker = function (scope, element, attrs) {
|
||||
|
||||
var configFieldsetModel = getFieldsetByAlias(scope.archetypeConfig.fieldsetModels, scope.fieldset.alias);
|
||||
|
||||
var view = configFieldsetModel.properties[scope.propertyConfigIndex].view;
|
||||
var label = configFieldsetModel.properties[scope.propertyConfigIndex].label;
|
||||
var config = configFieldsetModel.properties[scope.propertyConfigIndex].config;
|
||||
var alias = configFieldsetModel.properties[scope.propertyConfigIndex].alias;
|
||||
|
||||
if (view)
|
||||
{
|
||||
$http.get(view).success(function (data) {
|
||||
if (data) {
|
||||
if (scope.archetypeConfig.developerMode == '1')
|
||||
{
|
||||
console.log(scope);
|
||||
}
|
||||
|
||||
var rawTemplate = data;
|
||||
|
||||
//define the initial model and config
|
||||
scope.model = {};
|
||||
scope.model.config = {};
|
||||
|
||||
//ini the property value after test to make sure a prop exists in the renderModel
|
||||
var renderModelPropertyIndex = getPropertyIdByAlias(scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex].properties, alias);
|
||||
|
||||
if (!renderModelPropertyIndex)
|
||||
{
|
||||
scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex].properties.push(eval("({alias: '" + alias + "', value:''})"));
|
||||
renderModelPropertyIndex = getPropertyIdByAlias(scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex].properties, alias);
|
||||
}
|
||||
scope.model.value = scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex].properties[renderModelPropertyIndex].value;
|
||||
|
||||
//set the config from the prevalues
|
||||
scope.model.config = config;
|
||||
|
||||
//some items need an alias
|
||||
scope.model.alias = "scope-" + scope.$id;
|
||||
|
||||
//watch for changes since there is no two-way binding with the local model.value
|
||||
scope.$watch('model.value', function (newValue, oldValue) {
|
||||
scope.archetypeRenderModel.fieldsets[scope.fieldsetIndex].properties[renderModelPropertyIndex].value = newValue;
|
||||
});
|
||||
|
||||
element.html(rawTemplate).show();
|
||||
$compile(element.contents())(scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: "E",
|
||||
rep1ace: true,
|
||||
link: linker,
|
||||
scope: {
|
||||
property: '=',
|
||||
propertyConfigIndex: '=',
|
||||
archetypeConfig: '=',
|
||||
fieldset: '=',
|
||||
fieldsetIndex: '=',
|
||||
archetypeRenderModel: '='
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
angular.module("umbraco").directive('contentItem', function ($compile, $http) {
|
||||
|
||||
var linker = function (scope, element, attrs) {
|
||||
|
||||
if (scope.content.view)
|
||||
{
|
||||
$http.get(scope.content.view).success(function (data) {
|
||||
if (data) {
|
||||
var rawTemplate = data;
|
||||
|
||||
//define the initial model and config
|
||||
scope.model = {};
|
||||
scope.model.config = {};
|
||||
|
||||
//pull these from the content
|
||||
scope.model.value = scope.content.value;
|
||||
scope.model.config = scope.content.config;
|
||||
|
||||
//some items need an alias
|
||||
scope.model.alias = "scope-" + scope.$id;
|
||||
|
||||
//watch for changes since there is no two-way binding with the child values
|
||||
scope.$watch('model.value', function (newValue, oldValue) {
|
||||
scope.content.value = newValue;
|
||||
});
|
||||
|
||||
//add label
|
||||
if (true) {
|
||||
rawTemplate = '<label>{{content.options.label}}</label>' + rawTemplate;
|
||||
}
|
||||
|
||||
element.html(rawTemplate).show();
|
||||
$compile(element.contents())(scope);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: "E",
|
||||
rep1ace: true,
|
||||
link: linker,
|
||||
scope: {
|
||||
content: '='
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
padding: 5px;
|
||||
margin-bottom: 5px;
|
||||
background-color: #fff;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.archetypeEditor label{
|
||||
@@ -10,6 +11,12 @@
|
||||
float: left;
|
||||
}
|
||||
|
||||
.archetypeEditor fieldset>label{
|
||||
float: left;
|
||||
width: 200px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.archetypeEditor .multiPropertyTextbox{
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
@@ -24,6 +31,7 @@
|
||||
|
||||
.archetypeEditor .archetypeProperty{
|
||||
clear: both;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.archetypeEditor .umb-contentpicker{
|
||||
@@ -37,4 +45,35 @@
|
||||
min-height: 100px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.archetypeEditor .archetypeFieldsetToolbar {
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.archetypeEditor .archetypeFieldsetToolbar li {
|
||||
float: left;
|
||||
padding: 2px 4px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.archetypeEditor .archetypeFieldsetHeaderText{
|
||||
clear:both;
|
||||
margin-bottom: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.archetypeEditor .archetypeFieldsetFooterText{
|
||||
clear:both;
|
||||
margin-bottom: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.archetypeEditor .archetypeFieldsetHelpText{
|
||||
clear:both;
|
||||
margin-bottom: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
+53
-16
@@ -1,27 +1,56 @@
|
||||
{
|
||||
propertyEditors: [
|
||||
{
|
||||
alias: "Archetype",
|
||||
name: "Archetype",
|
||||
alias: "Imulus.Archetype",
|
||||
name: "Imulus Archetype",
|
||||
editor: {
|
||||
view: "~/App_Plugins/Archetype/views/archetype.html",
|
||||
view: "~/App_Plugins/Imulus.Archetype/views/archetype.html",
|
||||
valueType: "JSON"
|
||||
},
|
||||
prevalues: {
|
||||
fields: [
|
||||
{
|
||||
label: "Empty Fieldset Model",
|
||||
description: "(Required) What would you like a new fieldset model to be?",
|
||||
key: "emptyFieldsetModel",
|
||||
label: "Empty Fieldset Models",
|
||||
description: "(Required) Define your fieldset models?",
|
||||
key: "fieldsetModels",
|
||||
view: "textarea",
|
||||
validation: [
|
||||
{
|
||||
type: "Required"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Hide fieldset toolbar?",
|
||||
description: "Hides the fieldset toolbar that appears when more than one fieldset model appears.",
|
||||
key: "hideFieldsetToolbar",
|
||||
view: "boolean",
|
||||
validation: [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Hide editor controls?",
|
||||
description: "Hide the add/remove/sort controls",
|
||||
key: "hideControls",
|
||||
label: "Hide fieldset editor controls?",
|
||||
description: "Hides the fieldset add/remove/sort controls.",
|
||||
key: "hideFieldsetControls",
|
||||
view: "boolean",
|
||||
validation: [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Hide fieldset labels?",
|
||||
description: "Hides the fieldset labels.",
|
||||
key: "hideFieldsetLabels",
|
||||
view: "boolean",
|
||||
validation: [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Hide property labels?",
|
||||
description: "Hides the property labels.",
|
||||
key: "hidePropertyLabels",
|
||||
view: "boolean",
|
||||
validation: [
|
||||
|
||||
@@ -46,8 +75,8 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Custom Class",
|
||||
description: "(Optional) Enter a custom CSS class.",
|
||||
label: "Custom Wrapper Class",
|
||||
description: "(Optional) Enter a custom CSS class that will be applied to the wrapper div.",
|
||||
key: "customCssClass",
|
||||
view: "textstring",
|
||||
validation: [
|
||||
@@ -69,24 +98,32 @@
|
||||
key: "customJsPath",
|
||||
view: "textstring",
|
||||
validation: [
|
||||
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Developer Mode",
|
||||
description: "Turn on for developer options.",
|
||||
description: "(Advanced) Turn on for developer options.",
|
||||
key: "developerMode",
|
||||
view: "boolean",
|
||||
validation: [
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Sortable Options",
|
||||
description: "(Advanced) Override the default sortable controls with a JSON object.",
|
||||
key: "sortableOptions",
|
||||
view: "textarea",
|
||||
validation: [
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
,
|
||||
],
|
||||
javascript: [
|
||||
'~/App_Plugins/Archetype/js/archetype.js'
|
||||
'~/App_Plugins/Imulus.Archetype/js/archetype.js'
|
||||
]
|
||||
}
|
||||
@@ -1,16 +1,41 @@
|
||||
<div class="archetypeEditor ng-class:model.config.customCssClass" ng-controller="Imulus.ArchetypeController">
|
||||
<textarea class="archetypeDeveloperModel" ng-show="model.config.developerMode" ng-model="archetypeRenderModel"></textarea>
|
||||
<div class="archetypeFieldsetToolbar" ng-show="model.config.fieldsetModels.length > 1 && model.config.hideFieldsetToolbar != '1'">
|
||||
<ul>
|
||||
<li ng-repeat="fieldsetModel in model.config.fieldsetModels" ng-click="addRow(fieldsetModel.alias)">
|
||||
<img ng-src='{{fieldsetModel.icon}}' title="{{fieldsetModel.tooltip}}" />
|
||||
<span>{{fieldsetModel.label}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul ui-sortable="sortableOptions" ng-model="archetypeRenderModel.fieldsets">
|
||||
<li ng-repeat="fieldset in archetypeRenderModel.fieldsets" ng-hide="fieldset.remove">
|
||||
<fieldset>
|
||||
<div class="archetypeEditorControls" ng-hide="model.config.hideControls || model.config.maxProperties == '1'">
|
||||
<i class="icon icon-add" ng-click="addRow($index)" ng-show="canAdd()"></i>
|
||||
<fieldset ng-class="fieldset.alias" ng-init="fieldsetConfigModel=getConfigFieldsetByAlias(fieldset.alias)">
|
||||
<label ng-hide="model.config.hideFieldsetLabels == '1'">
|
||||
<img ng-src='{{fieldsetConfigModel.icon}}' title="{{fieldsetConfigModel.tooltip}}"/>
|
||||
<span>{{fieldsetConfigModel.label}}</span>
|
||||
</label>
|
||||
<div class="archetypeEditorControls" ng-hide="model.config.hideFieldsetControls || model.config.maxProperties == '1'">
|
||||
<i class="icon icon-add" ng-click="addRow(fieldset.alias, $index)" ng-show="canAdd()"></i>
|
||||
<i class="icon icon-delete" ng-click="removeRow($index)" ng-show="canRemove()"></i>
|
||||
<i class="icon icon-navigation handle" ng-show="canSort()"></i>
|
||||
</div>
|
||||
<div class="archetypeProperty" ng-repeat="property in fieldset.properties">
|
||||
<content-item content="property"></content-item>
|
||||
</div>
|
||||
<div class="archetypeFieldsetHeaderText" ng-show="fieldsetConfigModel.headerText">
|
||||
<em>{{fieldsetConfigModel.headerText}}</em>
|
||||
</div>
|
||||
<div class="archetypeProperty" ng-repeat="property in fieldsetConfigModel.properties">
|
||||
<label ng-hide="archetypeConfig.hidePropertyLabels == '1'">
|
||||
<span>{{property.label}}</span>
|
||||
<div class="archetypeFieldsetHelpText" ng-show="property.helpText">
|
||||
<em>{{property.helpText}}</em>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<archetype-property class="archetypeEditor ng-class:property.alias" property="property" fieldset-index="$parent.$index" fieldset="fieldset" archetype-config="model.config" property-config-index="$index" archetype-render-model="archetypeRenderModel"></archetype-property>
|
||||
</div>
|
||||
<div class="archetypeFieldsetFooterText" ng-show="fieldsetConfigModel.footerText">
|
||||
<em>{{fieldsetConfigModel.footerText}}</em>
|
||||
</div>
|
||||
</fieldset>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"grunt-contrib-jshint": "~0.7.2",
|
||||
"grunt-cli": "~0.1.11",
|
||||
"grunt-contrib-clean": "~0.5.0"
|
||||
"grunt-contrib-clean": "~0.5.0",
|
||||
"grunt-touch": "~0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user