Skip to content
Permalink
Browse files

Autocomplete field (#3301)

* Add autocomplete field to admin

* Apply linterns suggestions and fix things

* Remove COMPONENT flags for webpack

* Add change log entry
  • Loading branch information...
rbngzlv authored and oriolgual committed May 9, 2018
1 parent eb75f6f commit 0daf8b89bcdfc69679b5fd5ef8a3345b3bb10154
@@ -23,15 +23,28 @@ references:
restore_cache:
keys:
- bundler-dependencies-{{ checksum "Gemfile.lock" }}
restore_npm_cache: &restore_npm_cache
restore_cache:
keys:
- npm-dependencies-{{ checksum "package-lock.json" }}
install_ruby_dependencies: &install_ruby_dependencies
run:
name: Install ruby dependencies
command: bundle install
install_npm_dependencies: &install_npm_dependencies
run:
name: Install npm dependencies
command: npm install
save_ruby_cache: &save_ruby_cache
save_cache:
key: bundler-dependencies-{{ checksum "Gemfile.lock" }}
paths:
- /usr/local/bundle/
save_npm_cache: &save_npm_cache
save_cache:
key: npm-dependencies-{{ checksum "package-lock.json" }}
paths:
- node_modules
wait_for_db: &wait_for_db
run:
name: Wait for db
@@ -80,8 +93,11 @@ jobs:
steps:
- checkout
- *restore_ruby_cache
- *restore_npm_cache
- *install_ruby_dependencies
- *install_npm_dependencies
- *save_ruby_cache
- *save_npm_cache
- *wait_for_db
- run:
name: Generate test app
@@ -198,6 +214,7 @@ jobs:
<<: *defaults
steps:
- *attach_workspace
- *restore_npm_cache
- *restore_ruby_cache
- *wait_for_db
- *create_test_db
@@ -240,16 +257,7 @@ jobs:
<<: *defaults
steps:
- *attach_workspace
- restore_cache:
keys:
- npm-dependencies-{{ checksum "package-lock.json" }}
- run:
name: Install npm dependencies
command: npm install
- save_cache:
key: npm-dependencies-{{ checksum "package-lock.json" }}
paths:
- node_modules
- *restore_npm_cache
- run:
name: Run main folder lint & tests
command: npm run test:ci
@@ -40,6 +40,7 @@ plugins:
csslint:
enabled: true
exclude_patterns:
- "decidim-admin/app/assets/stylesheets/decidim/admin/bundle.scss"
- "decidim-core/app/assets/stylesheets/decidim/email.css"

duplication:
@@ -107,6 +108,7 @@ plugins:
stylelint:
enabled: true
exclude_patterns:
- "decidim-admin/app/assets/stylesheets/decidim/admin/bundle.scss"
- "decidim-core/app/assets/stylesheets/decidim/email.css"

exclude_patterns:
@@ -1207,6 +1207,7 @@ RSpec/DescribeClass:
- spec/bundle_spec.rb
- spec/i18n_spec.rb
- spec/generator_spec.rb
- decidim-admin/spec/bundles_spec.rb
- decidim-comments/spec/bundle_spec.rb
- decidim-core/spec/lib/global_engines_spec.rb

@@ -28,6 +28,7 @@ end
- **decidim-meetings**: Add organizer to meeting and meeting types [\#3136](https://github.com/decidim/decidim/pull/3136)
- **decidim-meetings**: Add Minutes entity to manage Minutes. [\#3213](https://github.com/decidim/decidim/pull/3213)
- **decidim-admin**: Links to participatory space index & show pages from the admin dashboard. [\#3325](https://github.com/decidim/decidim/pull/3325)
- **decidim-admin**: Add autocomplete field with customizable url to fetch results. [\#3301](https://github.com/decidim/decidim/pull/3301)

**Changed**:

@@ -0,0 +1 @@
app/assets/javascripts/decidim/admin/bundle.js.map
@@ -14,17 +14,21 @@
// = require ./auto_buttons_by_position.component
// = require ./dynamic_fields.component
// = require ./field_dependent_inputs.component
// = require ./bundle
// = require_self

window.Decidim = window.Decidim || {};
window.DecidimAdmin = window.DecidimAdmin || {};

const pageLoad = () => {
const { toggleNav, createSortList } = window.DecidimAdmin;
const { toggleNav, createSortList, renderAutocompleteSelects } = window.DecidimAdmin;

$(document).foundation();

toggleNav();

renderAutocompleteSelects('[data-plugin="autocomplete"]');

createSortList("#steps tbody", {
placeholder: $('<tr style="border-style: dashed; border-color: #000"><td colspan="4">&nbsp;</td></tr>')[0],
onSortUpdate: ($children) => {

Large diffs are not rendered by default.

@@ -14,3 +14,5 @@
@import "decidim/modules/data-picker";
@import "modules/modules";
@import "plugins/jquery.auto-complete";
@import "components/autocomplete_select.component";
@import "bundle";
@@ -0,0 +1 @@
.Select{position:relative}.Select input::-webkit-contacts-auto-fill-button,.Select input::-webkit-credentials-auto-fill-button{display:none!important}.Select input::-ms-clear,.Select input::-ms-reveal{display:none!important}.Select,.Select div,.Select input,.Select span{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.Select.is-disabled .Select-arrow-zone{cursor:default;pointer-events:none}.Select.is-disabled>.Select-control{background-color:#f9f9f9}.Select.is-disabled>.Select-control:hover{box-shadow:none}.Select.is-searchable.is-focused:not(.is-open)>.Select-control,.Select.is-searchable.is-open>.Select-control{cursor:text}.Select.is-open>.Select-control{border-bottom-right-radius:0;border-bottom-left-radius:0;background:#fff;border-color:#b3b3b3 #ccc #d9d9d9}.Select.is-open>.Select-control .Select-arrow{top:-2px;border-color:transparent transparent #999;border-width:0 5px 5px}.Select.is-focused>.Select-control{background:#fff}.Select.is-focused:not(.is-open)>.Select-control{border-color:#08c #0099e6 #0099e6;box-shadow:inset 0 1px 2px rgba(0,0,0,.1),0 0 5px -1px fade(#08c,50%)}.Select.has-value.is-clearable.Select--single>.Select-control .Select-value{padding-right:42px}.Select.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value .Select-value-label,.Select.has-value.Select--single>.Select-control .Select-value .Select-value-label{color:#333}.Select.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label,.Select.has-value.Select--single>.Select-control .Select-value a.Select-value-label{cursor:pointer;text-decoration:none}.Select.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label:focus,.Select.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label:hover,.Select.has-value.Select--single>.Select-control .Select-value a.Select-value-label:focus,.Select.has-value.Select--single>.Select-control .Select-value a.Select-value-label:hover{color:#08c;outline:none;text-decoration:underline}.Select.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value a.Select-value-label:focus,.Select.has-value.Select--single>.Select-control .Select-value a.Select-value-label:focus{background:#fff}.Select.has-value.is-pseudo-focused .Select-input{opacity:0}.Select.is-open .Select-arrow,.Select .Select-arrow-zone:hover>.Select-arrow{border-top-color:#666}.Select.Select--rtl{direction:rtl;text-align:right}.Select-control{background-color:#fff;border-color:#d9d9d9 #ccc #b3b3b3;border-radius:4px;border:1px solid #ccc;color:#333;cursor:default;display:table;border-spacing:0;border-collapse:separate;height:36px;outline:none;overflow:hidden;position:relative;width:100%}.Select-control:hover{box-shadow:0 1px 0 rgba(0,0,0,.06)}.Select-control .Select-input:focus{outline:none;background:#fff}.Select--single>.Select-control .Select-value,.Select-placeholder{bottom:0;color:#aaa;left:0;line-height:34px;padding-left:10px;padding-right:10px;position:absolute;right:0;top:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.Select-input{height:34px;padding-left:10px;padding-right:10px;vertical-align:middle}.Select-input>input{width:100%;background:none transparent;border:0 none;box-shadow:none;cursor:default;display:inline-block;font-family:inherit;font-size:inherit;margin:0;outline:none;line-height:17px;padding:8px 0 12px;-webkit-appearance:none}.is-focused .Select-input>input{cursor:text}.Select-control:not(.is-searchable)>.Select-input{outline:none}.Select-loading-zone{cursor:pointer;display:table-cell;text-align:center}.Select-loading,.Select-loading-zone{position:relative;vertical-align:middle;width:16px}.Select-loading{-webkit-animation:Select-animation-spin .4s infinite linear;-o-animation:Select-animation-spin .4s infinite linear;animation:Select-animation-spin .4s infinite linear;height:16px;box-sizing:border-box;border-radius:50%;border:2px solid #ccc;border-right-color:#333;display:inline-block}.Select-clear-zone{-webkit-animation:Select-animation-fadeIn .2s;-o-animation:Select-animation-fadeIn .2s;animation:Select-animation-fadeIn .2s;color:#999;cursor:pointer;display:table-cell;position:relative;text-align:center;vertical-align:middle;width:17px}.Select-clear-zone:hover{color:#d0021b}.Select-clear{display:inline-block;font-size:18px;line-height:1}.Select--multi .Select-clear-zone{width:17px}.Select--multi .Select-multi-value-wrapper{display:inline-block}.Select .Select-aria-only{position:absolute;display:inline-block;height:1px;width:1px;margin:-1px;clip:rect(0,0,0,0);overflow:hidden;float:left}.Select-arrow-zone{cursor:pointer;display:table-cell;position:relative;text-align:center;vertical-align:middle;width:25px;padding-right:5px}.Select--rtl .Select-arrow-zone{padding-right:0;padding-left:5px}.Select-arrow{border-color:#999 transparent transparent;border-style:solid;border-width:5px 5px 2.5px;display:inline-block;height:0;width:0;position:relative}@-webkit-keyframes Select-animation-fadeIn{0%{opacity:0}to{opacity:1}}@keyframes Select-animation-fadeIn{0%{opacity:0}to{opacity:1}}.Select-menu-outer{border-bottom-right-radius:4px;border-bottom-left-radius:4px;background-color:#fff;border:1px solid #ccc;border-top-color:#e6e6e6;box-shadow:0 1px 0 rgba(0,0,0,.06);box-sizing:border-box;margin-top:-1px;max-height:200px;position:absolute;left:0;top:100%;width:100%;z-index:1000;-webkit-overflow-scrolling:touch}.Select-menu{max-height:198px;overflow-y:auto}.Select-option{box-sizing:border-box;background-color:#fff;color:#666;cursor:pointer;display:block;padding:8px 10px}.Select-option:last-child{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.Select-option.is-selected{background-color:#f5faff;color:#333}.Select-option.is-focused{background-color:#f2f9fc;color:#333}.Select-option.is-disabled{color:#ccc;cursor:default}.Select-noresults{box-sizing:border-box;color:#999;cursor:default;display:block;padding:8px 10px}.Select--multi .Select-input{vertical-align:middle;margin-left:10px;padding:0}.Select--multi.Select--rtl .Select-input{margin-left:0;margin-right:10px}.Select--multi.has-value .Select-input{margin-left:5px}.Select--multi .Select-value{background-color:#f2f9fc;border-radius:2px;border:1px solid #c9e6f2;color:#08c;display:inline-block;font-size:.9em;margin-left:5px;margin-top:5px;vertical-align:top}.Select--multi .Select-value-icon,.Select--multi .Select-value-label{display:inline-block;vertical-align:middle}.Select--multi .Select-value-label{border-bottom-right-radius:2px;border-top-right-radius:2px;cursor:default;padding:2px 5px}.Select--multi a.Select-value-label{color:#08c;cursor:pointer;text-decoration:none}.Select--multi a.Select-value-label:hover{text-decoration:underline}.Select--multi .Select-value-icon{cursor:pointer;border-bottom-left-radius:2px;border-top-left-radius:2px;border-right:1px solid #c9e6f2;padding:1px 5px 3px}.Select--multi .Select-value-icon:focus,.Select--multi .Select-value-icon:hover{background-color:#ddeff7;color:#0077b3}.Select--multi .Select-value-icon:active{background-color:#c9e6f2}.Select--multi.Select--rtl .Select-value{margin-left:0;margin-right:5px}.Select--multi.Select--rtl .Select-value-icon{border-right:none;border-left:1px solid #c9e6f2}.Select--multi.is-disabled .Select-value{background-color:#fcfcfc;border:1px solid #e3e3e3;color:#333}.Select--multi.is-disabled .Select-value-icon{cursor:not-allowed;border-right:1px solid #e3e3e3}.Select--multi.is-disabled .Select-value-icon:active,.Select--multi.is-disabled .Select-value-icon:focus,.Select--multi.is-disabled .Select-value-icon:hover{background-color:#fcfcfc}@keyframes Select-animation-spin{to{transform:rotate(1turn)}}@-webkit-keyframes Select-animation-spin{to{-webkit-transform:rotate(1turn)}}
@@ -0,0 +1,13 @@
.autocomplete-field{
margin-bottom: 1.5rem;

.Select{
.Select-control,
.Select-input,
.Select-placeholder,
&.Select--single > .Select-control .Select-value{
height: 3rem;
line-height: 3rem;
}
}
}
@@ -23,6 +23,8 @@ class ApplicationController < ::DecidimController
helper Decidim::LanguageChooserHelper
helper Decidim::ComponentPathHelper

default_form_builder Decidim::Admin::FormBuilder

protect_from_forgery with: :exception, prepend: true

def user_has_no_permission_path
@@ -0,0 +1,19 @@
import { shallow } from "enzyme";
import * as React from "react";

import Autocomplete from "./autocomplete.component";

describe("<Autocomplete />", () => {
const name = "custom[name]";
const selected = "";
const options = Array();
const placeholder = "Pick a value";
const noResultsText = "No results found";
const searchPromptText = "Type to search";
const searchURL = "/some/url";

it("renders a div of Select", () => {
const wrapper = shallow(<Autocomplete name={name} selected={selected} options={options} noResultsText={noResultsText} placeholder={placeholder} searchPromptText={searchPromptText} searchURL={searchURL} />);
expect(wrapper.find(".autocomplete-field").exists()).toBeTruthy();
});
});
@@ -0,0 +1,169 @@
import axios, { CancelTokenSource } from "axios";
import * as React from "react";

import {Async as AsyncSelect, ReactAsyncSelectProps} from "react-select";
import "react-select/scss/default.scss";

declare module "react-select" {
interface ReactAsyncSelectProps<TValue = OptionValues> {
searchPromptText?: any;
}
}

export interface AutocompleteProps {
/**
* Autoload from search url on initialize
*/
autoload?: boolean;
/**
* The name of the input to be submitted with the form
*/
name: string;
/**
* The value of the actually selected option
*/
selected: any;
/**
* An array objects with the preloded options (needs to include the selected option)
*/
options: any[];
/**
* placeholder displayed when there are no matching search results or a falsy value to hide it
*/
noResultsText: string;
/**
* Field placeholder, displayed when there's no value
*/
placeholder: string;
/**
* Text to prompt for search input
*/
searchPromptText: string;
/**
* The URL where fetch content
*/
searchURL: string;
}

interface AutocompleteState {
/**
* The value of the actually selected option
*/
selectedOption: any;
/**
* An array objects with the preloded options (needs to include the selected option)
*/
options: any[];
/**
* Text to prompt for search input
*/
searchPromptText: string;
/**
* Placeholder displayed when there are no matching search results or a falsy value to hide it
*/
noResultsText: string;
}

export class Autocomplete extends React.Component<AutocompleteProps, AutocompleteState> {
public static defaultProps: any = {
autoload: false
};

private cancelTokenSource: CancelTokenSource;
private minCharactersToSearch: number = 3;

constructor(props: AutocompleteProps) {
super(props);

this.state = {
options: props.options,
selectedOption: props.selected,
searchPromptText: props.searchPromptText,
noResultsText: props.noResultsText
};
}

public render(): JSX.Element {
const { autoload, name, placeholder } = this.props;
const { selectedOption, options, searchPromptText, noResultsText } = this.state;

return (
<div className="autocomplete-field">
<AsyncSelect
cache={false}
name={name}
value={selectedOption}
options={options}
placeholder={placeholder}
searchPromptText={searchPromptText}
noResultsText={noResultsText}
onChange={this.handleChange}
onInputChange={this.onInputChange}
loadOptions={this.loadOptions}
filterOptions={this.filterOptions}
autoload={autoload}
removeSelected={true}
escapeClearsValue={false}
onCloseResetsInput={false}
/>
</div>
);
}

private handleChange = (selectedOption: any) => {
this.setState({ selectedOption });
}

private filterOptions = (options: any, filter: any, excludeOptions: any) => {
// Do no filtering, just return all options because
// we return a filtered set from server
return options;
}

private onInputChange = (query: string) => {
if (query.length < this.minCharactersToSearch) {
this.setState({ noResultsText: this.props.searchPromptText });
} else {
this.setState({ noResultsText: this.props.noResultsText });
}
}

private loadOptions = (query: string, callback: any) => {
query = query.toLowerCase();

if (this.cancelTokenSource) {
this.cancelTokenSource.cancel();
}

if (query.length < this.minCharactersToSearch) {
callback (null, { options: [], complete: false });
} else {
this.cancelTokenSource = axios.CancelToken.source();

axios.get(this.props.searchURL, {
cancelToken: this.cancelTokenSource.token,
headers: {
Accept: "application/json"
},
withCredentials: true,
params: {
term: query
}
})
.then((response) => {
// CAREFUL! Only set complete to true when there are no more options,
// or more specific queries will not be sent to the server.
callback (null, { options: response.data, complete: true });
})
.catch((error: any) => {
if (axios.isCancel(error)) {
// console.log("Request canceled", error.message);
} else {
callback (error, { options: [], complete: false });
}
});
}
}
}

export default Autocomplete;
@@ -0,0 +1,17 @@
import * as React from "react";
import * as ReactDOM from "react-dom";

import Autocomplete, { AutocompleteProps } from "./components/autocomplete.component";

window.DecidimAdmin = window.DecidimAdmin || {};

window.DecidimAdmin.renderAutocompleteSelects = (nodeSelector: string) => {
window.$(nodeSelector).each((index: number, node: HTMLElement) => {
const props: AutocompleteProps = { ...window.$(node).data("autocomplete") };

ReactDOM.render(
React.createElement(Autocomplete, props),
node
);
});
};
@@ -0,0 +1,4 @@
import { configure } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";

configure({ adapter: new Adapter() });
@@ -211,6 +211,9 @@ en:
update:
error: There was an error when updating this attachment.
success: Attachment updated successfully.
autocomplete:
no_results: No results found
search_prompt: Type at least three characters to search
categories:
create:
error: There was an error creating this category.
@@ -10,5 +10,6 @@ module Decidim
#
module Admin
autoload :Components, "decidim/admin/components"
autoload :FormBuilder, "decidim/admin/form_builder"
end
end

0 comments on commit 0daf8b8

Please sign in to comment.
You can’t perform that action at this time.