Skip to content

Commit

Permalink
Merge pull request SUSE#1394 from vitoravelino/namespace-inline-valid…
Browse files Browse the repository at this point in the history
…ation

js: added inline validation for namespace's new form
  • Loading branch information
vitoravelino committed Aug 22, 2017
2 parents 82be2ae + 9c223b7 commit 2bae81b
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 46 deletions.
93 changes: 90 additions & 3 deletions app/assets/javascripts/modules/namespaces/components/new-form.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Vue from 'vue';

import EventBus from '~/utils/eventbus';
import { required } from 'vuelidate/lib/validators';

import { setTypeahead } from '~/utils/typeahead';

import EventBus from '~/utils/eventbus';
import Alert from '~/shared/components/alert';
import FormMixin from '~/shared/mixins/form';

Expand All @@ -24,9 +25,14 @@ export default {
return {
namespace: {
namespace: {
name: '',
team: this.teamName || '',
},
},
timeout: {
name: null,
team: null,
},
};
},

Expand All @@ -37,6 +43,7 @@ export default {
const name = namespace.attributes.clean_name;

this.toggleForm();
this.$v.$reset();
set(this.namespace, 'namespace', {});

Alert.show(`Namespace '${name}' was created successfully`);
Expand All @@ -53,12 +60,92 @@ export default {
},
},

validations: {
namespace: {
namespace: {
name: {
required,
format(value) {
// extracted from models/namespace.rb
const regexp = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/;

// required already taking care of this
if (value === '') {
return true;
}

return regexp.test(value);
},
available(value) {
clearTimeout(this.timeout.name);

// required already taking care of this
if (value === '') {
return true;
}

return new Promise((resolve) => {
const searchName = () => {
const promise = NamespacesService.existsByName(value);

promise.then((exists) => {
// leave it for the back-end
if (exists === null) {
resolve(true);
}

// if exists, invalid
resolve(!exists);
});
};

this.timeout.name = setTimeout(searchName, 1000);
});
},
},
team: {
required,
available(value) {
clearTimeout(this.timeout.team);

// required already taking care of this
if (value === '') {
return true;
}

return new Promise((resolve) => {
const searchTeam = () => {
const promise = NamespacesService.teamExists(value);

promise.then((exists) => {
// leave it for the back-end
if (exists === null) {
resolve(true);
}

// if exists, valid
resolve(exists);
});
};

this.timeout.team = setTimeout(searchTeam, 1000);
});
},
},
},
},
},

mounted() {
const $team = setTypeahead(TYPEAHEAD_INPUT, '/namespaces/typeahead/%QUERY');

// workaround because of typeahead
$team.on('change', () => {
const updateTeam = () => {
set(this.namespace.namespace, 'team', $team.val());
});
};

$team.on('typeahead:selected', updateTeam);
$team.on('typeahead:autocompleted', updateTeam);
$team.on('change', updateTeam);
},
};
43 changes: 43 additions & 0 deletions app/assets/javascripts/modules/namespaces/services/namespaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ const customActions = {
method: 'PUT',
url: '/namespaces/{id}/change_visibility',
},
teamTypeahead: {
method: 'GET',
url: '/namespaces/typeahead/{teamName}',
},
existsByName: {
method: 'HEAD',
url: '/namespaces',
},
};

const resource = Vue.resource('/namespaces{/id}.json', {}, customActions);
Expand All @@ -20,6 +28,22 @@ function changeVisibility(id, params = {}) {
return resource.changeVisibility({ id }, params);
}

function searchTeam(teamName) {
return resource.teamTypeahead({ teamName });
}

function existsByName(name) {
return resource.existsByName({ name })
.then(() => true)
.catch((response) => {
if (response.status === 404) {
return false;
}

return null;
});
}

function get(id) {
return resource.get({ id });
}
Expand All @@ -28,9 +52,28 @@ function save(namespace) {
return resource.save({}, namespace);
}

function teamExists(value) {
return searchTeam(value)
.then((response) => {
const collection = response.data;

if (Array.isArray(collection)) {
return collection.some(e => e.name === value);
}

// some unexpected response from the api,
// leave it for the back-end validation
return null;
})
.catch(() => null);
}

export default {
get,
all,
save,
changeVisibility,
searchTeam,
teamExists,
existsByName,
};
2 changes: 2 additions & 0 deletions app/assets/javascripts/vue-shared.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
import Vuelidate from 'vuelidate';

Vue.use(Vuelidate);
Vue.use(VueResource);

Vue.http.interceptors.push((_request, next) => {
Expand Down
15 changes: 15 additions & 0 deletions app/assets/stylesheets/includes/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,18 @@ textarea.fixed-size {
.twitter-typeahead, .tt-dropdown-menu {
width: 100%;
}

// help block
.form-group {
.help-block {
display: none;
margin-bottom: 0;
}
}
.has-success,
.has-warning,
.has-error {
.help-block {
display: block;
}
}
45 changes: 31 additions & 14 deletions app/controllers/namespaces_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,24 @@ class NamespacesController < ApplicationController
# GET /namespaces
# GET /namespaces.json
def index
respond_to do |format|
format.html { skip_policy_scope }
format.json do
@special_namespaces = Namespace.where(
"global = ? OR namespaces.name = ?", true, current_user.username
).order("created_at ASC")
@namespaces = policy_scope(Namespace).order(created_at: :asc)

accessible_json = serialize_as_json(@namespaces)
special_json = serialize_as_json(@special_namespaces)
render json: {
accessible: accessible_json,
special: special_json
}
if request.head?
check_namespace_by_name if params[:name]
else
respond_to do |format|
format.html { skip_policy_scope }
format.json do
@special_namespaces = Namespace.where(
"global = ? OR namespaces.name = ?", true, current_user.username
).order("created_at ASC")
@namespaces = policy_scope(Namespace).order(created_at: :asc)

accessible_json = serialize_as_json(@namespaces)
special_json = serialize_as_json(@special_namespaces)
render json: {
accessible: accessible_json,
special: special_json
}
end
end
end
end
Expand Down Expand Up @@ -115,6 +119,19 @@ def change_visibility

private

# Checks if namespaces exists based on the name parameter.
# Renders an empty response with 200 if exists or 404 otherwise.
def check_namespace_by_name
skip_policy_scope
namespace = Namespace.find_by(name: params[:name])

if namespace
head :ok
else
head :not_found
end
end

# Fetch the namespace to be created from the given parameters. Note that this
# method assumes that the @team instance object has already been set.
def fetch_namespace
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ html
meta content="/favicon/browserconfig.xml" name="msapplication-config"
meta content="#205683" name="theme-color"

script src="//cdn.polyfill.io/v2/polyfill.js?features=Array.prototype.findIndex,Array.from"
script src="//cdn.polyfill.io/v2/polyfill.js?features=Array.prototype.some,Array.prototype.findIndex,Array.from"
= javascript_include_tag(*webpack_asset_paths("application"))
= yield :js_header

Expand Down
23 changes: 18 additions & 5 deletions app/views/namespaces/components/_form.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@ div ref="form" class="collapse"
= form_for :namespace, url: namespaces_path, html: {id: "new-namespace-form", class: "form-horizontal", role: "form", name: "form", "@submit.prevent" => "onSubmit", novalidate: true} do |f|
= f.hidden_field(:team, "v-model" => "namespace.namespace.team", "v-if" => "teamName")

.form-group
.form-group :class=="{ 'has-error': $v.namespace.namespace.name.$error }"
= f.label :name, {class: "control-label col-md-2"}
.col-md-7
= f.text_field(:name, class: "form-control", required: true, placeholder: "New namespace's name".html_safe, ref: "firstField", "v-model" => "namespace.namespace.name")
= f.text_field(:name, class: "form-control", placeholder: "New namespace's name".html_safe, ref: "firstField", "@input" => "$v.namespace.namespace.name.$touch()", "v-model.trim" => "namespace.namespace.name")
span.help-block
span v-if="!$v.namespace.namespace.name.required"
| Name can't be blank
span v-if="!$v.namespace.namespace.name.format"
| Name can only contain lower case alphanumeric characters, with optional underscores and dashes in the middle
span v-if="!$v.namespace.namespace.name.available"
| Name has already been taken

.form-group.has-feedback v-if="!teamName"
.form-group.has-feedback :class=="{ 'has-error': $v.namespace.namespace.team.$error }" v-if="!teamName"
= f.label :team, {class: "control-label col-md-2"}
.col-md-7
.remote
= f.text_field(:team, class: "form-control typeahead", required: true, placeholder: "Name of the team", "v-model" => "namespace.namespace.team")
= f.text_field(:team, class: "form-control typeahead", required: true, placeholder: "Name of the team", "@input" => "$v.namespace.namespace.team.$touch()", "v-model.trim" => "namespace.namespace.team")
span.fa.fa-search.form-control-feedback
span.help-block
span v-if="!$v.namespace.namespace.team.required"
| Team can't be blank
br
span v-if="!$v.namespace.namespace.team.available"
| Selected team does not exist

.form-group
= f.label :description, {class: "control-label col-md-2"}
Expand All @@ -21,4 +34,4 @@ div ref="form" class="collapse"

.form-group
.col-md-offset-2.col-md-7
= f.submit("Create", class: "btn btn-primary")
= f.submit("Create", class: "btn btn-primary", ":disabled" => "$v.$invalid")
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"vue-loader": "^12.0.3",
"vue-resource": "^1.3.1",
"vue-template-compiler": "^2.3.3",
"vuelidate": "^0.5.0",
"webpack": "^2.2.1"
},
"babel": {
Expand Down
Loading

0 comments on commit 2bae81b

Please sign in to comment.